最新消息:

Linux低级I/O深析

未分类 admin 2737浏览 0评论

可以说一切存储系统的基础是系统调用: 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;
}
 如果你ulimit 没做任何修改的话, 在到1023后就会打印-1. 这是文件描述符超过了系统最大限制,可用ulimit -n进行相应调整.
文件描述符在内核中会存成这样:

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深析

您必须 登录 才能发表评论!