九、文件共享
内核使用了3种数据结构表示打开的文件,他们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。以下是书中总结的三个部分:
如果说,两个单独的进程各自打开了同一个文件,其关系如下图:
由图3-8可以看出,两个进程打开同一个文件,他们各自有自己的文件表项,但是其v节点指向的是同一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有他自己的对文件的当前偏移量。以下是对其进行进一步说明:
注意:文件描述符标志 和 文件状态标志在左右范围方面是有区别的。
文件描述符标志,只用于对一个进程的一个描述符,每一个进程都有属于自己的一个。
文件状态标志,则应用于指向该给定文件表项的任何进行的所有描述符。只要指向该文件表项,都是这个状态
十、文件的原子性操作
1、对于一个稍微成型点的项目而言,多进程和多线程异步冲突问题,都是一件很重要也很头疼的事。对于多线程操作一个文件也是这样的。在Unix早期,open 中没有 O_APPEND 这个选项,打开文件追加一般都是通过两步去实现的:
这样就会造成一个问题,如果有A、B两个进程同时打开一个文件,然后同时利用 lseek 将文件指针移到1500字节处。这是他们都没有做 IO 操作。如果B进程先 write 100 字节的数据,这是在B进程的文件表项中,文件偏移量为1600。然后再用 A 进程write 100字节数据,这是A进程写入的100字节就会覆盖掉B进程写入的100字节。
为解决上述问题,引入了O_APPEND。如果还是上述情况,在B进程写入100字节数据之后,A进程的文件偏移也会向后移100字节,到文件的尾部,这时再写入数据就不至于覆盖了。
2、书中还介绍了两个在多线程或是多进程开发中非常好用的两个函数:
这两个读写函数相当于 lseek 之后,进行read 和 write。但是有两点需要注意(重点注意第二点):
(1) 它们都是原子操作,无法将 lseek 和读写操作分开。
(2) 它们可以指定一个偏移量进行读写,但是该读写无法改变当前的偏移量。也就是借助上一例子。A进程pwrite 1500处写入100字节之后,现在的偏移位置还是在1500处。如果用write会到1600处。
3、在创建一个文件时也需要考虑原子操作,书中举了一个不考虑原子性的一个原始做法去打开创建一个文件。
根据上述代码不难看出,在A进程操作时还没有这个文件,它会 open 出 ENOENT 的错,这时就要创建文件了,但是进程B去创建了这个文件,这时进程A在creat处创建文件失败,并且将B进程已创建的文件清空。
所以建议使用open 加 O_CREAT | O_EXCL 的方式去创建和检查一个文件的存在。
十一、文件描述符的复制 dup 和 dup2
对文件描述符的复制,主要有三个方式。下面的两个函数是两种,还有一种是用 fcntl 函数。
dup 函数返回的是当前可用文件描述符中最小的一个。
dup2则是用 fd2 指定的新的描述符的值。如果fd2已经打开,则将其关闭。若fd 和 fd2相等,则返回fd2,并不关闭它。
这些函数返回的新的文件描述符与参数fd共享一个文件表项,如下图:
用fcntl函数复制文件描述符方式如下:
十二、同步函数(sync、fsync 和 fdatafsync)
传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式被称为延迟写(delayed write)
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。
fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
书上介绍不够详细。
十二、函数 fcntl
这一部分就是讲了 fcntl 的各种用法,最后书中的一个例子不错。
fcntl函数有五个功能如下:
还有11种 cmd 介绍了前8个,下面是书中的介绍:
#include "apue.h"
#include <fcntl.h>int
main(int argc, char *argv[])
{
int val;if (argc != 2)
err_quit("usage: a.out <descriptor#>");if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
err_sys("fcntl error for fd %d", atoi(argv[1]));switch (val & O_ACCMODE) {
case O_RDONLY:
printf("read only");
break;case O_WRONLY:
printf("write only");
break;case O_RDWR:
printf("read write");
break;default:
err_dump("unknown access mode");
}if (val & O_APPEND)
printf(", append");
if (val & O_NONBLOCK)
printf(", nonblocking");
if (val & O_SYNC)
printf(", synchronous writes");#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
if (val & O_FSYNC)
printf(", synchronous writes");
#endifputchar('\n');
exit(0);
}