Linux环境编程之文件I/O(四):文件I/O的数据结构
(一)
Linux系统支持不同进程间共享打开的文件。内核使用三种数据结构表示打开的文件:进程表项、文件表项、v节点表。
1、进程表项:每个进程在进程表中都有一个记录项,记录项中年包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a、文件描述符标志
b、指向一个文件表项的指针
2、内核为所有打开文件维持一张文件表。每个文件表项包含:
a、文件状态标志,如读写、添加、同步和非阻塞等。
b、当前文件偏移量
c、指向该文件v节点表项的指针
3、每个打开的文件都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,v节点还包含了该文件的i节点。
如果两个独立进程各自打开了同一个文件,假定第一个进程在文件描述符3上打开该文件,而另一个进程则在文件描述符4上打开该文件。打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个进程都有自己的文件表项的理由是:这样使得每个进程都有它自己的对该文件的当前偏移量。如下图:
注意三个概念:文件描述符标志、文件状态标志、文件描述符。
文件描述符标志(close_on_exec),它仅仅是一个标志,当fork了一个子进程,然后在子进程中调用了exec函数时就用到了该标志,它的含义就是,执行exec钱是否要关闭这个文件描述符,它的作用域只用于一个进程的一个描述符。而文件状态标志是在系统文件表中,读写可执行等这些标志,适用于指向该给定文件表项的任何进程中的所有描述符。
(二)
既然文件可以共享,那不同进程间共享同一个文件时,就可能出现并发竞态等问题。针对不同的竞态问题,我们有不同的策略。
1、若两个进程同时对一个文件进行写操作,则可能使其中一个的写数据被另外一个的写数据覆盖,针对这种情况,采取的办法是在open打开文件时设置O_APPEND标志。
2、Linux还允许原子性地定位搜索(seek)和执行I/O。其中pread和pwrite就是这种的函数。
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
调用pread相当于顺序调用lseek和read,但pread又与这种顺序调用有下列区别:
a、调用pread时,无法中断其定位和读操作。
b、不更改文件指针
调用pwrite相当于顺序调用lseek和write,但也与他们有类似的区别。
(三)
可以使用dup和dup2函数复制一个现存的文件描述符
#include <unistd.h>
int dup(int oldfd); // 返回的新文件描述符一定是当前可用文件描述符中的最小数值。
int dup2(int oldfd, int newfd); // 可以用newfd参数指定新描述符的数值,如果newfd已经打开,则现先将其关闭。如果newfd等于oldfd,则dup2返回newfd,而不关闭它。
#include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char *argv[]) { int fd; int nfd; char buf[] = "fjakdjl"; fd = open("test.txt", O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR); if(fd < 0) printf("open error\n"); printf("fd = %d.\n", fd); //dup2 nfd = dup2(fd,4); if(nfd < 0){ printf("error!\n"); } printf("nfd = %d.\n", nfd); return 0; }编译测试:
编译 gcc dup.c 执行 ./a.out 显示结果 fd = 3. nfd = 4.
(四)
为保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性。Linux系统提供了sync、fsync、fdatasync三个函数。
#include <unistd.h>
int fsync(int fd); //只对文件描述符fd指定的单一文件起作用,并且等待写磁盘操作结束,然后返回
int fdatasync(int fd); // 类似于fsync,但它只影响文件的数据部分,出数据部分外,fsync还会同步更新文件的属性。
void sync(void); // 只是对所有修改过的快缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作。