文件I/O(二)

Unix系统支持不同进程可以通过dup函数共享打开文件。内核通过三中数据结构的映射关系标识打开文件。

2.1、内核数据结构

结构如下:

  • 每一个进程在进程表中都有一个记录项,每个记录项包含文件描述符与对应的文件指针。
  • 每个指针指向内核为打开文件维护的文件表项,每个文件表项包含文件状态(读,写,同步…),当前文件偏移量,V节点指针。
  • 每一个V节点包含文件类型,以及对该文件操作的文件指针。并且包含一个i节点
  • i节点包含当前问价长度,文件所有者,文件数据实际存储的磁盘位置指针等信息。

2.2、两个进程同时打开

两个进程同时打开一个文件,结构一样,只是各自进程的文件表项中的v指针指向同一个v节点。但是两个进程拥有自己独立的文件状态标志,以及当前文件偏移量。

如果进程A的fd1打开了文件file,同样,进程B的fd2打开文件file,打开文件的进程各自获取一个文件表项,指向同一个V节点,A、B进程各自拥有file的偏移量。

  • 每次Write后,文件表项中的文件偏移量增加写入字节数。如果文件偏移量超过了文件大小,i节点当前长度设置为当前偏移量。
  • 如果使用用O_APPEND标志打开一个文件,文件的偏移量会被设置到i节点的文件长度。是追加的数据写到文件尾部。
  • Lseek函数只修改文件表项项中的文件偏移量。不进行任何I/O操作。

以上操作对于两个进程打开同一个文件进行读取可以正常进行。每个进程拥有自己的文件表项,拥有自己的偏移量。如果多个进程同时进行写可能就会产生不确定性的问题了。

2.3、原子操作

  • 例子1:早期的Unix并未提供O_APPEND标识。
1
2
lseek();
write();

对于以上的两个函数操作,单进程没有问题,如果多进程,同时指向以上操作。

假设A、B两个进程同时对文件file进行追写。没有使用O_APPEND标志。A与B各自的文件表项共享V节点,A先调用lseek设置偏移量,做写操作。内核切换进程,B调用lseek,然后进行写。此时,B很有可能将A写的数据覆盖掉。

任何多余一个函数的操作,都不是原子操作。在两个函数执行之间,进程有可能被挂起。也就是A的lseek()刚刚执行完,还没有写就被挂起。此时,另一个进程B调用lseek,并开始写,结束后,系统切换进程,恢复A之前的进程操作,开始写。此时就有可能把刚刚进程B写的数据覆盖。

UNIX系统为这种操作提供了一个原子。打开文件的时候设置O_APPEND,内核每次写之前都会将当前文件的偏移量设置到尾部。于是,每次写之前,A、B都不需要额外调用lseek()。

例子2:创建文件。

open函数的O_CREAT和O_EXCL其实就是一个原子操作。创建文件前,检测文件是否存在,不存在,创建,存在open失败。将检测文件和创建文件两个动作作为一个原子操作。如果没有这种原子操作就可能会出现问题。

假设A进程检查一个文件file不存在,然后准备执行创建文件,这个时候被挂起。然后进程B执行,创建了一个文件file,并且写入一些数据,进程切换,恢复A。A创建文件,覆盖之前写入file的数据。

将检查文件与创建文件合成一个原子操作,就不会出现以下问题。

总结:原子操作就是多步组成的操作,要么全部执行,要么都不执行,不可能执行一个子集。÷