Linux Kernel - 文件系统系列
open系统调用相信大家都不会陌生, ‘Everything is a file - 一切皆是文件’, 是Unix的一个设计特点, 和Unix有着密切关系的Linux也不例外. 设备是文件, 数据是文件, 连socket也是文件. 文件的抽象把数据的特性抽离了出来, 使得对系统编码和理解系统大大简化.
代码示例:
int main(int argc, char* argv[]) {
char wbuf[256];
char rbuf[256];
sprintf(wbuf, "this is data wirte to file\n");
int fd = open("aaa.txt", O_CREAT | O_RDWR, S_IRUSR|S_IWUSR);
write(fd, wbuf, strlen(wbuf));
close(fd);
fd = open("aaa.txt", O_RDWR);
int len = read(fd, rbuf, sizeof(rbuf));
rbuf[len] = '\0';
printf("data read: %s", rbuf);
close(fd);
return 0;
}
Linux下, 文件的主要操作是通过文件描述符(int类型)来进行的, 使用文件描述符使得上层应用无需知道操作系统内部的文件描述结构, 屏蔽掉实现的细节, 因此操作系统内部可以自由改变文件相关的结构而不会对上层应用产生影响. 而获取该描述符正是通过open调用.
内核里文件描述符相关的拓补结构通过图形可能更好理解一点(图是我自己画的, 希望能形象表达出背后的内容):
查看原图
通过open调用, 在进程内部将文件表的某个槽位(slot)和file文件对象关联起来. 其中fd的整型值正是槽位的位置, 内核后续的读写操作中, 首先将fd的值作为索引找到对应的file对象, 然后做进一步的操作. 在file对象内部, 包含了文件当前的操作位置, 以及操作的函数表.
从上面的结构我们大概可以猜想到open调用要做的几件事情:
1. 分配槽位, 即fd对应的索引位置
2. 分配file对象
3. 给file中的操作函数表赋值. 由于不同的文件系统的读写实现不同, 因此这一步应该是跟具体文件系统相关的.
值得一提的是下面几个特殊的fd值, 相信大家都遇到过:
0 - stdin, 标准输入
1 - stdout, 标准输出
2 - stderr, 标准错误
结合序列图的调用序号, 来看一看open调用主要做的几件事情.
step 3: get_unused_fd_flags(), 获取空闲的fd槽位
step 6: get_empty_filp(), 分配file对象
step 7: link_path_walk(), 解析文件路径
step 9: lookup_fast(), 从cache中查找dentry(目录项对象)
step 13: lookup(), cache中找不到的情况下, 调用文件所在文件系统的lookup函数获取
step 15: do_dentry_open(), 这里把file对象和具体文件系统的inode关联起来, 并把file中的操作函数表指向inode提供的操作表
step 16: open(), 调用文件系统提供的open函数, 完成一些文件系统相关的初始化, 有些文件系统的open函数可能为空, 即不做相关操作
step 17: fd_install(), 把fd和file对象关联起来
最后, 就完成了open的任务, 应用层在获取到fd后, 就可以做后续的操作了. 很显然, 后面的操作都是通过file对象中的函数表来完成的, 这样, 就完成了vfs层和具体的文件系统的连接.