在linux中,打开的文件组织结构如下:
与打开的文件相关的有三个数据结构,就是上图中的三部分。
在linux中,有一个进程表,每一个进程在进程表中有一个表项。每一个进程表项中都维护着一张打开文件的描述符表,每一个文件描述符占用了一个表项。只要文件被打开,就都会在这张表中存在一个文件描述符,而不管该描述符是否是该进程打开的。与文件描述符相关联的是文件描述符标志和文件表项指针。文件表项指针指向文件表项,什么是文件表项呢?内核为每一个打开的文件都维护了一个文件表项。这个文件表项记录了以下信息:文件打开的状态(只读,只写或者可读可写),当前文件的偏移量(用来记录文件的下次要读或者写文件的位置)以及文件的v节点指针。内核为每一个打开的文件还维护了一个v节点,这个节点包括了文件的类型以及各种操作文件的指针。另外它还包括i节点,i节点包括文件的所有者以及文件的长度等信息,这些信息是在打开文件时从磁盘上读进来的,因此可以看到在程序中获取打开文件的相关信息是十分快速的。
每次向一个文件中写入数据后,文件表项的文件位移会增加相应的字节数。当文件的位移超过i节点中当前文件的长度时,当前文件的长度会设置为当前文件的位移。如果文件以O_APPEND的方式打开时,每次向文件中写入数据前,都会将当前文件指针设置为i节点中的当前文件长度。
如果有两个进程打开了同一文件,则内核为这两个进程分别维护一个文件表项,这样做的理由是两个进程可以有独立的文件位移,这种情况,图示如下:
虽然内核为每一个进程都维护了一个文件表项,但是内核对每一个文件只维护了一个v节点。在这种情况下,多个进程同时对一个文件进行读操作是完全没有问题的,但是如果多个进程同时对一个文件进行写操作,就有可能造成最终的文件并不是我们所想要的结果。这个问题就引出了原子操作的概念。
原子操作是由一系列操作构成的一个集合,这个集合中的操作,要么全部执行,要么全不执行,绝不可能只执行了这个指令集合的一个真子集。考虑这样一个进程,它每次将数据添加到文件的末尾,在早期的linux中没有O_APPEND操作,所以程序会写成如下的形式:
if( lseek(fd,0L,SEEK_END)<0 ) { printf("lseek error!\n"); return -1; } if( write(fd,buff,100)!=100 ) { printf("write file error!\n"); return -2; }
dup函数和dup2函数用来复制一个文件描述符,它们的函数原型是:
int dup(int filedes); int dup2(int filedes,int filedes2);
如果函数执行成功,则返回新的文件描述符,否则返回-1。dup函数的参数是所要复制的文件描述符,返回系统中尚未使用的最小的文件描述符。dup2的作用是将第一个参数指定的文件描述符复制到第二个参数制定的文件描述符上,如果第二个参数已经使用,则先将其关闭。由dup函数复制后得到的文件描述符与原文件描述符指向同一个文件表项,如下图:
fcntl函数用来改变打开文件的性质,它的函数原型是:
int fcntl(int filedes,int cmd,...);如果函数执行成功返回值依赖于cmd,如果函数执行失败则返回-1。cmd的取值有以下几种情况:
F_DUPFD:复制文件描述符,相当于dup和dup2的功能 F_GETFD / F_SETFD:获取/设置文件描述符标志 F_GETFL / F_SETFD:获取/设置文件状态标志 F_GETOWN / FSETOWN:获取设置异步IO所有权 F_GETTLK / F_SETTLK / F_SETTLKW:获得设置记录锁其中,最常使用的是前面三个选项。
ioctl函数是io操作的杂物箱,可以进行各种乱七八糟的io操作。
系统中存在/dev/fd/n,这个目录,n的值为0,1,2等,打开文件/dev/fd/n等于复制文件描述符n。