在说linux的文件前,先看看linux的文件系统。虽然实现细节有差异,但文件系统的大致组织形式如下图所示:
局部放大:
由以上两个图可见,对每一个文件都有一个i节点与之对应,这是linux文件系统把文件和文件信息分别抽象出来的结果,文件信息保存在i节点上(i节点长度固定,内容包括文件类型、文件存取许可权位、文件长度和指向该文件所占用的数据块的指针等等,但不包括文件名),可通过系统调用stat(2)、fstat(2)、lstat(2)获取。i节点数量太多,为提高查找效率,内核有时通过一张hash来索引。而文件则保存在数据块区。
由上图还可以看出,可以有多个目录项指向同一个i节点。实质上是每个i节点有一个连接计数(硬连接),只有在指向i节点的目录项数减为0时文件才被删除,相应数据块释放。硬连接的作用是允许一个文件拥有多个有效路径名。
BTW:
与之相对的另一种符号连接(Symbolic Link),也叫软连接。软连接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。它有自己的i节点,该i节点中的文件类型是S_IFLNK,于是系统知道这是一个符号连接。当我们删除了源文件后,链接文件不能独立存在,虽然仍保留文件名。
另外,在linux里,目录也是一种文件,它通常只包含文件名列表和i节点的位置信息,如图,i节点1267指向的目录创建了子目录testdir(inode2549):
由图易见,任何一个目录,连接计数至少为2
接着,看看内核处理I/O的数据结构:
进程表项存在在每一个进程里,文件表项是为每一个打开的文件维护的,其中的文件状态标志包括读、写、增写、同步、非阻塞等,v节点可理解为上文的i节点,不同系统实现细节稍有差异。
两个独立进程分别打开同一文件时(同一进程open两次也与之类似):
注意fork的差异,fork之后,父、子进程对于每一个打开的文件描述符共享同一个文件表项
文件描述符的复制:
#include <unistd.h> int dup( int oldfd ); int dup2( int oldfd, int newfd )
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用newfd参数指定新描述符的数值。如果newfd已经打开,则先将其关闭。如若oldfd等于newfd,则返回newfd而不关闭它。但他们两个都是原子操作。
如,执行dup(4,1)后,内核数据结构如下所示:
因为两个描述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、添写等)以及同一当前文件位移量。
如果想在复制后恢复,通常的做法是用一个save_fd来备份newfd。
比如:
save_fd = dup(STDOUT_FILENO); dup2(fd, STDOUT_FILENO); close(fd); write(STDOUT_FILENO, msg, strlen(msg)); dup2(save_fd, STDOUT_FILENO); write(STDOUT_FILENO, msg, strlen(msg)); close(save_fd);
过程如图:
注:
第3幅图,要执行dup2(fd, 1);,文件描述符1原本指向tty,现在要指向新的文件somefile,就把原来的关闭了,但是tty这个文件原本有两个引用计数,还有文件描述符save_fd也指向它,所以只是将引用计数减1,并不真的关闭文件。
第5幅图,要执行dup2(save_fd, 1);,文件描述符1原本指向somefile,现在要指向新的文件tty,就把原来的关闭了,somefile原本只有一个引用计数,所以这次减到0,是真的关闭了。
linux文件系统,dup2等