本文讨论如何在多个进程间共享文件,以及所涉及的内核数据结构。然后会介绍dup,dup2,fcntl等函数的使用。
1 数据结构
内核使用三种数据结构表示打开的文件:
(1)每个进程在进程表中都有一个记录项,记录项中有一张打开文件文件描述符表,每项包括:
a )文件描述符标志(close_on_exec)
b)指向一个文件表的指针
(2)内核为所有打开的文件维护一张文件表,每个文件表项包括:
a)文件状态标志(读 写 添加 同步和非阻塞)
b) 当前文件偏移量
c) 指向v节点的指针
(3) v节点 v节点中包含了文件类型和对文件各种操作的函数的指针,i节点,当前文件长度.
i节点包括:文件所有者,文件长度,文件所在的设备,指向文件实际数据块所在位置的指针.
下图显示一个进程的三张表的关系,该进程打开了两个文件。
图1 打开文件的内核数据结构
下图显示两个进程打开同一个文件 fork后:
图2 两个独立进程各自打开同一个文件
2 原子操作
由多步组成的操作,如果操作原子的执行,要么执行完所有操作,要么一步也不执行,不会被中途打断。
2.1 添加至一个文件
如果两个进程A和B都对同一个文件进行添加操作,如果没有在调用open时使用O_APPEND选项,而使用lseek函数将文件偏移量定位到文件尾。由于定位(lseek)和写(write)不是原子操作,当内核在两个进程间切换时会导致A,B进程write在同一位置。解决这一问题的方法是在调用open时使用O_APPEND选项。
2.2 pread和pwrite
定位搜索(seek)和I/O可以原子性的执行。
#include<unistd.h> ssize_t pread(int filedes,void *buf,size_t nbytes,off_t offset); 返回值:读到的字节数,若到达文件尾则返回0,出错返回-1. ssize_t pwrite(int filedes,const void *buf,size_t nbytes,off_t offset); 返回值:若成功返回已写的字节数,出错返回-1。
调用pread相当于顺序的调用lseek和read,但又有所不同:
a)不能中断定位和读操作(原子的执行)
b)不更新文件指针。
pwrite同上。
2.3 创建一个文件
在调用open时使用O_CREAT和O_EXCL选项。会使测试文件是否存在和创建文件组成一个原子操作。
3 dup和dup2函数
dup和dup2用来复制一个现存的文件描述符。
#include <unistd.h> int dup(int filedes); int dup2(int filedes,int filedes2); 返回值:若成功返回新的文件描述符,否则返回-1.
dup返回的文件描述符一定时当前可以文件描述符中最小的一个。
dup2可以指定filedes2为返回的文件描述符,如果filedes2存在,则先将其关闭,如果filedes2 和 filedes相等,则返回filedes2 ,而不关闭它。
此时新文件描述符与之前的文件描述符共享一个文件表项。
图3 执行dup后的内核数据结构。
每个文件描述符都有它自己的一套文件描述符标志。dup 得到的新文件描述符的执行时关闭(close-on-exec)标志总是被 dup 函数清除。
dup(filedes) == fcntl(filedes,F_DUPFD,0)
dup2(filedes,filedes2);
等效与
close(filedes2);
fcntl(filedes,F_DUPFD,filedes2);
不过dup2是原子操作。
4 fcntl函数
fcntl可以改变已打开文件的性质。
#include <fcntl.h> int fcntl(int filedes,int cmd,.../* int arg */); 返回值:若成功依赖于cmd,出错返回-1。
在本节中第三个参数总是整数,而在记录锁中,第三个参数是一个指向结构的指针。
fcntl的五项功能:
1)复制一个现有的文件描述符(cmd =F_DUPFD)。
2)获得/设置文件描述符标志(cmd = F_GETFD或F_SETFD)。
3)获得/设置文件状态(cmd = F_GETFL或F_SETFL)
4)获得/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)
5)获得/设置记录锁(cmd = F_GETLK ,cmd = F_SETLK或F_SETLKW )
F_DUPFD:复制文件描述符filedes,返回新文件描述符。返回值为大于或等于第三个参数的值。新文件描述符的FD_CLOEXEC标志被关闭。
F_GETFD:获取filedes的文件描述符标志作为函数返回值,当前只定义了一个FD_CLOEXEC。
F_SETFD:对filedes设置文件描述符标志。现在一般把第三个参数设置为0(系统默认,在exec时不关闭)或 1(在exec时关闭)而不直接使用FD_CLOEXEC。
F_GETFL:获得文件状态标志。即调用open时的参数:
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 为读,写打开
O_APPEND 每次写添加
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(仅数据)
O_RSYNC 同步读写
不过前三个访问标志并不各占一位,分别为0,1,2.而且这三个值互斥,必须先用屏蔽字O_ACCMODE取得访问模式位,然后将结果与这三个值中的任何一个比较。
val = fcntl(fd,O_GETFL,0); switch(val & O_ACCMODE) { case O_RDONLY: case O_WRONLY: case O_RDWR: } if(val & O_APPEND) printf("append\n"); ....
F_SETFL:将文件状态标志设置为第三个参数,除了前三个,其他的都可以设置。
F_GETOWN:取得当前获得SIGIO和SIGURG的进程ID或进程组ID。
F_SETOWN:设置接受SIGIO和SIGURG的进程ID和进程组ID。正的arg为进程ID,负的arg的绝对值为进程组ID。
若fcntl函数出错返回-1,若执行成功:F_DUPFD,F_GETFD,F_GETFL,F_GETOWN,第一个返回新的文件描述符,二三返回相应的标志,最后一个返回进程或进程组ID。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <fcntl.h> 4 #include <stdlib.h> 5 6 int main(int argc,char *argv[]) 7 { 8 int val; 9 if((val = fcntl(atoi(argv[1]),F_GETFL,0)) < 0) 10 printf("fcntl error\n"); 11 switch(val & O_ACCMODE) 12 { 13 case O_RDONLY: 14 printf("read only"); 15 break; 16 case O_WRONLY: 17 printf("write only"); 18 break; 19 case O_RDWR: 20 printf("read write"); 21 break; 22 default: 23 printf("unknow access mode"); 24 } 25 if(val & O_APPEND) 26 printf(",append"); 27 if(val & O_NONBLOCK) 28 printf(",nonblock"); 29 printf("\n"); 30 return 0; 31 32 }
编译执行:
root@debian:/program# ./3-4 0 < /dev/tty read only root@debian:/program# ./3-4 1 > tmp root@debian:/program# cat tmp write only root@debian:/program# ./3-4 5 5<>tmp read write root@debian:/program# ./3-4 2 2>>tmp write only,append root@debian:/program#