Unix系统中的文件组织方式,本质上是一棵层次化的目录树。每个目录都可以包含文件和其他子目录。从理论上讲,这棵树的深度几乎可以无限延伸——然而,如果实际尝试挑战其极限,系统会提示“超出范围”并终止执行。使用以下脚本即可验证这一特性:
while true
do
mkdir deep_well
cd deep_well
done
运行几秒后,系统就会提示超过目录树范围并中断执行。
2. 一个磁盘可以划分为大量扇区,每个扇区存储512字节
扇区是磁盘上最小的物理存储单元,我们可以对每个扇区进行编号,从而使磁盘成为一系列已编号块的集合。
3. 磁盘块上存储文件时遵循固定规则
每个文件系统通常由三部分组成:超级块、i-节点表以及数据区。
- 超级块:存储文件系统自身的元信息,例如各区域的大小以及未使用的磁盘块信息(不同版本的实现略有差异)。
- i-节点表:每个文件都有其属性(如大小、最近修改时间等),这些属性被保存在
ino_t结构体中。所有i-节点的大小相同,i-节点表就是这些节点的线性列表。表中的每个i-节点通过其在表中的位置索引来标识,例如索引为2的i-节点位于文件系统i-节点表中的第3个位置。 - 数据块:用于存放文件的具体内容。由于块的大小固定,一个文件通常会被分散存储到多个磁盘块上。
4. 创建一个文件的4个步骤
- 存储属性:内核首先找到一个空闲的i-节点,将文件的属性信息填入其中;
- 存储数据:从磁盘上分配空闲块,将文件数据复制进去;
- 记录分配情况:内核在i-节点的磁盘分布区中记录刚才分配的磁盘块编号;
- 添加文件名到目录:将(i-节点号,文件名)的映射关系添加到对应目录中。
5. cat、more等命令的实现原理
执行 cat name 时,系统会在目录中查找文件名,定位到该文件名对应的i-节点号;然后根据i-节点号获取文件属性并检查权限——若权限不足,open()函数返回-1,打开失败并停止执行;接着根据i-节点中的磁盘位置访问文件的数据块,反复调用read读取数据(可将其存放到缓冲区)。
6. 大文件的存储方式
如果一个文件需要14个编号的磁盘块来存储,而i-节点只包含13个项的分配链表,此时可以将前10个直接块地址存放于i-节点中,剩余的4个块地址存入一个数据块,并在i-节点的第11个位置写入指向该数据块的指针。这样实际使用了10+4+1个数据块,多出来的那个块称为间接块。
同理,当间接块也被填满时,还可以设置二级间接块,以此类推,从而支持更大的文件。
7. 文件在目录中的本质含义
目录包含(i-节点号,文件名)的条目——也就是说,目录存储的是对文件的引用,每个引用被称为链接。
8. 目录包含子目录的含义
目录包含指向子目录i-节点的链接。
9. 目录包含父目录的含义
目录包含 .. 的链接,即指向其父目录。
10. 文件只有i-节点号而没有名称,链接则可以有名字
一个文件可以拥有多个链接(名字可以各不相同),但它们都指向同一个文件,对这些链接的任何操作实质上都是对源文件的操作。
11. Unix系统可以挂载多个文件系统
每个文件系统都是一棵独立的树,都有各自的根目录。但系统可以通过 mount 操作将它们整合为一棵大树——即将一个文件系统的根挂载到另一个文件系统的某个目录节点上。
12. 符号链接与硬链接的区别
符号链接通过文件名引用目标文件,可以跨越不同文件系统,也可以指向目录,类似于Windows中的快捷方式。
硬链接是将目录直接链接到树的指针,同时也是将文件名与文件自身链接起来的指针,通过i-节点号来引用文件。
13. 与目录树相关的命令和系统调用
-
命令
mkdir
实现:头文件#include#include
函数原型:int res = mkdir(char *path, mode_t mode); -
命令
rmdir:删除一个目录,该目录必须为空。
实现:#include
函数原型:int res = rmdir(const char* path); -
命令
rm:减少相应i-节点的链接计数,若链接数减为0,则释放数据块和i-节点。该命令不能用于删除目录。
实现:#include
函数原型:int res = unlink(const char *path); -
命令
ln:不能用于创建目录的硬链接。
实现:#include
函数原型:int res = link(const char *old, const char *new); -
命令
mv:先删除原目录链接,再复制到新位置。
实现:#include
函数原型:int res = rename(const char* from, const char *to);
原理:将链接复制到新名称/位置,然后删除原链接——if(link("x","z")!=-1) unlink("x"); -
命令
cd:仅影响当前进程的工作目录,对目录本身无影响。
实现:#include
函数原型:int res = chdir(const char *path);
14. pwd命令的实现原理
下面是一个pwd命令的简单实现示例,通过递归向上查找父目录来拼接出完整路径:
#include
#include
#include
#include
#include
#include
ino_t get_inode(char *);
void printpathto(ino_t);
void inum_to_name(ino_t, char *, int);
int main()
{
printpathto(get_inode("."));
putchar('\n');
return 0;
}
void printpathto(ino_t this_inode)
{
ino_t my_inode;
char its_name[BUFSIZ];
if (get_inode("..") != this_inode)
{
chdir(".."); // up one dir
inum_to_name(this_inode, its_name, BUFSIZ); // get its name
my_inode = get_inode(".");
printpathto(my_inode); // iterate
printf("/%s", its_name);
}
}
void inum_to_name(ino_t inode_to_find, char *namebuf, int buflen)
{
DIR *dir_ptr; // the directory
struct dirent *direntp; // each entry
dir_ptr = opendir(".");
if (dir_ptr == NULL)
{
perror(".");
return;
}
while ((direntp = readdir(dir_ptr)) != NULL)
{
if (direntp->d_ino == inode_to_find)
{
strncpy(namebuf, direntp->d_name, buflen);
namebuf[buflen-1] = '\0';
closedir(dir_ptr);
return;
}
}
fprintf(stderr, "error looking for inum %d\n", (int)inode_to_find);
return;
}
ino_t get_inode(char *fname)
{
struct stat info;
if (stat(fname, &info) == -1)
{
fprintf(stderr, "Can not stat");
perror(fname);
return 1;
}
return info.st_ino;
}
运行结果:
caoli@caoli-laptop:~/workspace/test$ ./pwd1
/home/caoli/workspace/test
caoli@caoli-laptop:~/workspace/test$