文件共享
内核通过打开三个数据结构来表示打开文件:
- 描述符表(descriptor table)
每个进程都有一张描述符表,它的表项是由文件描述符来索引的,每个打开的文件描述符表项指向文件表的一个表项。 - 文件表(file table)
打开的文件集合由一张文件表表示,所有进程共享这张表。每一个文件表项组成包括当前的文件位置,引用计数(reference count)(即当前指向该表项的文件描述符个数),以及一个指向v-node表中对应表项的指针等。关闭一个描述符,引用计数相应减1,直到引用计数为0时,内核才删除这个文件表项。 v-node表(v-node table)
同文件表一样,所有的文件共享一张v-node表。每个表项包括stat结构中的大多数信息,包括st_mode和st_size。v-node表项唯一对应一个文件。不同描述符对应不同文件:
注:文件表只有一张。不同描述符对应同一文件,但是对应不同文件表,所以对于不同描述符可以从文件不同位置读取数据:
父进程fork产生子进程,父子进程共享文件描述符(各自的描述符表),文件表和v-node表:
I/O重定向
I/O重定向可以使用dup2来实现:
#include <unistd.h>
int dup2(int oldfd, int newfd);
//成功返回非负的描述符,失败返回-1
dup2复制描述符oldfd的表项到描述符newfd的表项,覆盖newfd表项以前的内容。如果newfd已经打开,dup2会先关闭newfd再复制oldfd的内容。例如,调用dup2(4,1)
,以前输入到fd1的内容,现在直接输入到fd4:
调用dup2前:
fd1指向文件A,fd4指向文件B,两个文件表项的引用计数均为1.
调用dup2后:
文件A的引用计数减1,为0,被内核关闭,并删除;dup2复制fd4的描述符表项内容到fd1,文件B的引用计数相应加1。
标准I/O
标准I/O库是Unix I/O的较高级别代替。它把一个打开的文件模型化为一个流,用FILE类型指针指向它。每个ACSI C程序都有3个打开的流stdin, stdout, stderr,分别表示标准输入,标准输出,标准错误。
FILE类型的流是对文件描述符和流缓冲区的抽象。流缓冲区和RIO读缓冲区的目的是一样的,都是为了减少Linux I/O的调用次数。把要读的内容一次性读到缓冲区中,然后再按用户程序需求一点一点返回给用户程序。
建议:
- 尽量使用标准I/O。
- 不要用scanf和rio_readlineb来读取二进制文件。像scanf和rio_readlinb这样的函数是专门为文本文件设计的。
对套接字的I/O使用RIO函数。因为标准I/O会导致一些问题产生。标准I/O是全双工的。即输入,输出共用了同一个流,这就导致了输入输出有可能产生冲突:
<1>跟在输出函数之后的输入函数。如果中间没有插入对fflush、fseek、fsetpos、rewind的调用,一个输入函数不能跟在一个输出函数之后。fflush 清空与流相关的缓冲区,其他三个都使用了unix I/O的lseek来重置当前文件的位置。
<2>跟在输入函数之后的输出函数。如果中间没有插入fseek、fsetpos、rewind的调用,一个输出函数不能跟在一个输入函数之后,除非该输入函数遇到了一个EOF。
这些限制给套接字的使用带来了一个问题,因为套接字使用lseek是非法的。对于<1>可以使用fflush解决,但是对于<2>唯一的方法就是对同一个套接字打开两个流,一个用来写,一个用来读:FILE *fpin, *fpout; fpin = fdopen(sockfd, "r"); fpout = fdopen(sockfd, "w");
但是这会带来另一个问题,应用程序必须在两个流上都使用fclose,这样才能释放与流相关的内存资源,否则会有内存泄漏:
fclose(fpin); fclose(fpout);
这些操作中的每一个都是要关闭同一个套接字描述符,第二个就会失败。对于顺序执行的程序是没有没问题的,但是,对于线程化的程序关闭一个已经关闭的描述符会导致严重后果。
因此,在套接字上使用RIO函数。如果需要格式化的输出,使用sprintf函数在内存中格式化一个字符串,然后用rio_writen把它发送给套接字接口。如果想格式化输入,使用rio_readlinb读取一个文本行,使用sscanf从文本行提取不同字段。