可以说一切存储系统的基础是系统调用: open , creat, seek , read, write, mmap …的运用,要想写个高性能FS,就要深入IO系统调用.这篇博文主要分析了操作文件的内核数据结构的变化及相关tips, 相关I/O基础操作,这里不讲,请参阅APUE.
在linux内核中对所有打开的文件使用了三种数据结构来表示:
1) 每个进程在进程表中(PCB)中都有一个记录项
struct task_struct { //... struct file* filep[NP_OPEN]; // ... };
如上面内核代码filep就是一张该进程打开的文件描述符表,可以看成一个数组,从0开始计数. 当我们调用open打开一个文件时,描述符会++1.
int main() { int fd1; fd1 = open("./test", O_RDONLY, 0); printf("%dn",fd1); return 0; }
比如这段代码, open返回的是文件创建时返回的文件描述符,在本机无其它打开的文件时运行程序可以看到结果为: 3 .这里是3是有原因的, 因为0,1,2这三个文件描述符已经被OS内核占用掉了,依次为STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO.所以文件描述符从3开始计数,依次下去 ,那有没有最大限制呢? 再用下面代码测试下:
int main() { int fd1; int i; for (i = 0; i < 1025; i++) { fd1 = open("./test", O_RDONLY, 0); printf("%dn",fd1); } getchar(); return 0; }
文件描述符在内核中会存成这样:
2) 所有打开的文件有一张公共的文件表(被所有进程共享), 包含一些文件读写标志(r,w…),文件位置,引用计数(由描述符表指向的引用数),还有一个指向i-node表的指针.内核数据结构代码如下:
struct file { unsigned short f_mode; //文件操作模式 unsigned short f_flags; //文件打开和控制标志 unsigned short f_count; //打开文件的引用计数 struct m_inode* f_inode; //对应i-node off_t f_pos; //文件位置 };
3) 另外对于所有打开的文件还有一张公共的i节点表(所有进程共享), 里面有所有文件的元数据信息, 比如文件大小, 文件所有者.
struct stat { int st_size; //... };
另外这边说明下,linux只有i-node没有v-node, v-node指的是UNIX. 这里我们以用的较多的linux为例.
三个数据结构可以通过下面这张图联系起来:
现在让我们根据上面的知识看下读取文件text的过程,当我们调用open函数时会产生一个中断(软),linux内核实际调用的是sys_open.
static inline long open(const char *file, int flag, int mode) { extern long sys_open(const char *, int, int); return sys_open(file, flag, mode); }
调用sys_open函数, 当前进程会将task_struct中的文件struct file* filep[NP_OPEN];管理指针表与内核中的file_table(所有进程共享,前面说过)进行指针连接,整个file_table可以看成一个数组, 进行指针连接的依据是找到ref=0的项. 因为如果有哪项已经有文件了,ref肯定会>=1的,大于1是因为有共享 比如fork.dup2之类的操作.
filep和file_table引用操作如图:
然后从硬盘读取text文件,获取inode节点.这个过程主要根据用户传入的路径(相对/绝对)找到i-node, 如果text是在一个多层目录下,比如~/a/text 会先读取根目录的i节点然后得到a的目录项再读取到a的i节点再得到a文件下的目录文件再得到text目录项最后得到i-node.如图所示:
最后找到text文件的i-node节点后需要注册到file_table的结构体中.
以上是整个打开文件的过程, 我们来看下新建文件的过程, 新建文件和打开文件类似,不过这里和打开文件不同的是新建i节点的过程.创建文件选用creat函数时其实和open一样 调用的也是sys_open函数. 只不过传参不一样了. O_CREAT | O_TRUNC .
int sys_creat(const char* pathname, int mode) { return sys_open(pathname, O_CREAT | O_TRUNC. mode); }
我们直接进行创建i-node节点的地方,当以~/a/text路径查找text的i节点时,由于a这个数据结构中还没有text这个目录项,所以查找函数会返回空.不过这里要注意我们来调用下内核代码看下:
int open_namei(int dfd, const char *pathname, int flag, int mode, struct nameidata *nd) { //... if (!(flag & O_CREAT)) { } }
代码中有O_CREAT的判断,所以此时是不会去找这个i-node 而是会去创建文件.创建文件的内容会包含权限等判断.先去a文件中添加目录项,查找空闲目录项(ref=0,前面提到过),找到了就把目录项挂载到此位置处.目录项的名字为text.最后再把文件的i-node也载入file_table中,并根据这个inode生成一张i-node表,里面有此文件的所有元数据信息.到最后sys_open(实际调用的是do_sys_open)操作会返回文件描述符fd.
long do_sys_open(const char* filename, int flag, int mode) { //... return fd; }
整个文件创建过程结束.下面我们就可以拿着这个fd对它进行一切操作(在有权限的范围内).比如写,读.
写操作,内核数据结构块主要是要注意 当前文件指针偏移量f_pos, 这个会决定你写入的位置, 很多时候,操作文件I/O函数时, 打开了一个文件进行写入后, 这个时候f_pos为文件最后的指针的位置大小, 而再用这个fd进行读取的话 , 会发现根本读取不了数据, 不懂”行情”的人可能会纳闷了, 其实原因就在这里, 这个是后你就需要调用lseek函数把f_pos移动到首位.再进行读取就可以了.
最后再来说说文件关闭操作close. 我们调用close时,其实并没有真正关闭, 而是将前面说的file_table中的ref–.因为一个文件并不是你一个人在使用, 这也是file_table是所有进程共享的原因. ref告诉os有多少进程在使用,当ref变为0了, 也即OS要回收的时候. OS会把所有ref=0的对应的i节点从内核i节点数据结构中删除.不可能让这些没有人使用的一直使用系统资源.
转载请注明:爱开源 » Linux低级I/O深析