首页 > 代码库 > Unix_文件I/O
Unix_文件I/O
lseek函数
off_t lseek(int filedes, off_t offset, int whence);
whence: SEEK_SET, 表示将文件的偏移量设置为距文件开始offset个字节。
SEEK_CUR, 当前+offset个字节,offset可正可负。
SEEK_END, 文件长度+offset个字节,可正可负。
成功,返回新的文件偏移量;失败,返回-1。
lseek仅将当前的文件偏移量记录在内核中,并不引起任何I/O操作。然后,该偏移量用于下一个读写操作。
空洞
offset可以大于文件的当前长度,这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这是允许的。位于文件中,但是没有写过的字节都被读为0。
空洞并不占用磁盘空间。具体处理方式与文件的实现有关,当定位到超出文件尾端后写时,新写的数据需要分配磁盘块,但对于原文件尾端和新开始写的位置之间的部分,则不需要分配空间。
有空洞文件与无空洞文件的长度虽然可以相同,but,它们实际占用的磁盘块数是不同的。
文件共享
UNIX支持在不同进程间共享打开的文件。内核使用三种数据结构表示打开的文件。
- 进程表项
- 文件描述符标志
- 指向文件表项目的指针
- 文件表项
- 文件状态标志(such as read, write, append, sync, and nonblocking)
- 当前文件偏移量
- 指向该文件V节点的指针
- V-node表项
- v节点信息(包含文件类型,对此文件进程各种操作的指针...)
- i节点信息(索引节点),(包含文件的所有者,文件长度,文件所在设备...)
- 当前文件长度
如图:
- 每一个进程都对应一个进程表项,一个进程能打开好多文件,所以进程表项里有好多fd;
- 一个fd就代表一个文件,也可以说一个文件可以由很多个fd来代表;
- 不同进程会对同一个文件有不同的操作,这个文件表项其实就是把文件和进程联系起来的东东,里面包含一些该进程对这个文件的操作,然后它再用一个指针指向文件本身。
dup、dup2函数
#include <unistd.h>int dup(int filedes);int dup2(int filedes, int filedes2);
dup和dup2都是用来复制一个现存的文件描述符(已经打开的文件描述符)的函数。成功,返回新的描述符;失败,返回-1.
dup:返回当前可用fd的最小值;
dup2:可以指定返回的描述符为fd2。如果fd2已经打开,则先将其关闭。如果fd等于fd2, 则直接返回fd2,即将fd替换为fd2, 而不关闭它。
这两个函数返回的新文件描述符与原来的fd共享同一个文件表项。如上图中,fd0与fd4。
/dev/fd
比较新的系统都提供名为/dev/fd的目录,目录项是名为0,1,2等的文件。打开/dev/fd/n 等效于复制描述符n。
某些系统提供路径名/dev/stdin, /dev/stdout, /dev/stderr。等效于/dev/fd/0, /dev/fd/1,/dev/fd/2。
原子操作
所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)
如果一个进程要在一个文件的尾端添加数据,由于早期UNIX不支持open的O_APPEND,所以采用先lseek到文件末尾,再write的方法。但若有多个进程,这种方法就会出现问题
if (lseek(fd, OL, 2) < 0) //position to EOF err_sys("lseek error");if (write(fd, buf, 100) != 100) err_sys("write error");
假设有两个独立的进程A和B都对同一个文件进行添加操作。每个进程都已经打开了该文件,但未使用O_APPEND标志。这时A,B都有自己的文件表项,但是共享一个v-node表项。如果A调用了lseek,它将进程A的该文件当前偏移量设置为1500字节(当前文件末尾)。然后内核切换到B,B再执行lseek,将进程B的当前偏移量也设置为1500字节(文件末尾)。然后B调用write,将B的该文件当前偏移量增加到1600。因为文件的长度增加了,所以内核对V节点中的当前文件长度更新为1600。然后,切换回A,A再write时,由于其当前文件表中记录的偏移量是1500,所以将数据写入时就覆盖了B刚写到这个文件中的数据。
解决方法: 使lseek和write这两个操作对于其他进程而言成为一个原子操作。UNIX提供了一种方法,在打开文件时设置O_APPEND标志,使内核每次对这个文件进行写之前,都将进程的当前偏移量更新到文件末尾,于是在每次写之前就不用调用lseek了。
sync、fsync、fdatasync
传统的UNIX系统实现在内核中设有缓冲区高速缓存或者页面高速缓存,大多数磁盘I/O都通过缓冲进行。当我们向一个文件写入数据时,数据通常被内核拷贝到它的其中一个缓冲区,并排队以便在之后的某个时刻写入到磁盘中。这被称为延迟写(delayed write)。这方法虽然减少了磁盘读写次数,BUT降低了文件内容的更新速度,若系统发生故障,将会造成文件更新内容的丢失。
So,为了保证磁盘上实际文件系统与缓冲区高速缓存中的内容一致,UNIX提供了sync、fsync和fdatasync函数来强制把缓冲里的东东写到磁盘文件里。
#include <unistd.h>int fsync(int filedes);int fdatasync(int filedes);//成功,返回0;出错,返回-1void sync(void);
sync: 全局性的,对整个系统都flush。不管你实际写磁盘结束否,有修改就排入写队列,也就是只要有点儿变动就冲洗内核的块缓冲区。
fsync: 仅引用一个文件,由文件描述符指定,并且在返回前等待磁盘写的完成。
fdatasync: 顾名思义,就是只更新data部分,因为fsync还会同步更新文件属性等。
以上三种方法是系统提供的系统调用,C语言里有一个对C标准扩充的函数fflush,它也有相似的功能。
#include <stdio.h>int fflush(FILE* stream);
Unix_文件I/O