第三章 文件I/O

第三章 文件I/O

引言

UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek以及close。

本章说明的函数经常被称为不带缓冲的I/O。术语 ****不带缓冲**** 指的是每个read和write都调用内核的一个系统调用。

多个进程共享文件 相关的函数:dup、fcntl、sync、fsync和ioctl

文件描述符

文件描述符的变化范围是0~OPENMAX。

open函数

#include
int open(const char pathname, int oflag, … / modet mode */);

ODSYNC和OSYNC标志有微妙的区别。仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多的数据)时,ODSYNC标志
才影响文件属性。而设置OSYNC标志后,数据和属性总是同步更新。当文件用ODSYNC标志打开,在重写其现有的部分内容时,文件时间属性不会同步更新。与此相反,如果文件是用OSYNC标志打开,那么对该文件的每一次
write操作都将在write返回前更新文件时间, 这与是否改写现有字节或增写文件无关。

由open返回的文件描述符一定是最小的未用描述符数值。

POSIXNNOTRUNC有效,则在整个路径名超过PATHMAX, 或路径名中任一文件名超过NAMEMAX时,返回出错状态,并将errno设置为ENAMETOOLONG。

create函数

#include
int create(const char *pathname, modet mode);
此函数等效于open(pathname, OWRONLY | OCREATE | OTRUNC, mode);

close函数

#include
int close(int filedes);

关闭一个文件时还会释放该进程加在该文件上的所有 ****记录锁****
当一个进程终止时,内核自动关闭它所有打开的文件。

lseek函数

#include
offt lseek(int filedes, offt offset, int whence);

可以用以下方式确认当前文件偏移量
offt currpos;
currpos = lseek(fd,0,SEEKCUR);

这种方法也可以用来确定所涉及的文件是否可以设置偏移量。如果文件描述符引用的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。

通常,文件的当前偏移量是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,则其偏移量必须是非负值 。因为偏移量可能是负值,所以在比较lseek的返回值时
应当谨慎,不要测试它是否小于0,而是要测试它是否等于-1。

lseek仅将当前文件的偏移量记录在内核中,它并不引起任何I/O操作。然后该偏移量用于下一个读/写操作。

文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写操作将加长该文件,并在文件中构成一个空洞。位于文件中但没有写过的字节都被 ****读为0****

文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘快,
但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。

read函数

#include
ssizet read(int filedes, void *buf, sizet nbytes);

有多种情况可使实际读到的字节数少于要求读的字节数:
1. 读普通文件时,在读到的要求字节数之前已达到了文件尾端。
2. 从终端设备读时,通常一次最多读一行。
3. 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
4. 当从管道或从FIFO读时,如若管道包含的字节少于所需要的字节数,那么read将只返回实际可用的字节数。
5. 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
6. 当某一信号造成中断,而已经读了部分数据量时。

write函数

#include
ssizet write(int filedes, const void *buf, sizet nbytes);
返回值通常和nbytes相同,不相同则出错。
出错的原因可能是:
1. 磁盘满
2. 超过了一个给定进程的文件长度限制

I/O效率

BUFFSIZE的选取,和块大小sbblksize一致,最高效。
操作系统检测到顺序读时,会采取某种预读技术(read ahead)

文件共享

内核使用三种数据结构表示打开的文件:
1. 每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,每个描述符占一项。与每个描述符相关联的是:
1. 文件描述符标志(closeonexit)。
2. 指向一个文件表项的指针。
2. 内核为所有打开文件维持一张文件表。每个文件表项包含:
1. 文件状态标志
2. 当前文件偏移量
3. 指向该文件v节点表项的指针
3. 每个打开文件都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,v节点还包含了该文件的i节点。
这些信息是在打开文件时从磁盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。

如果两个独立进程各自打开了同一个文件。打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。
每个进程都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前偏移量。

可能有多个文件描述符项指向同一个文件表项。譬如dup,fork。

文件描述符标志和文件状态标志在作用域方面的区别,前者只用于一个进程的一个文件描述符,而后者则适用于指向该文件表项的任何进程中的所有描述符。

当多个进程写同一个文件时,可能产生预期不到的效果。解决办法,参考下面的原子操作的概念。

原子操作

添写至一个文件

任何一个需要多个函数调用的操作都不可能是原子操作,因为在两个函数调用之间,内核可能会临时挂起该进程。
UNIX提供了OAPPEND标志,内核在写之前会将偏移量设置为文件尾端处,而不用调用lseek。

pread和pwrite函数

把lseek和I/O读写捆绑成了原子操作。由内核提供。

#include
ssizet pread(int filedes, void *buf, sizet nbytes, offt offset);
ssizet pwrite(int filedes, const void *buf, sizet nbytes, offt offset);

创建一个文件

open提供OCREATE和OEXCL选项。当同时指定这两个选项,而该文件又已经存在时,open将失败。
一般而言,原子操作指的是由多步组成的操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤中的一个子集。
* 内核是怎么实现的?当执行其中一个时,发生信号中断呢?或者两个语句中,第一个语句执行时间过长导致CPU时间片用完呢?*

dup和dup2函数

这两个函数都可用来复制一个现存的文件描述符,返回的新文件描述符与参数fieldes共享同一个文件表项。

#include
int dup(int fieldes);
int dup2(int fieldes, int fieldes2);

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用fieldes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。如若filedes等于filedes等于
filedes2,则dup2返回filedes2,而不关闭它。

复制一个描述符的另一种方法是fcntl函数。
调用
dup(filedes);
等效于
fcntl(filedes, FDUPFD, 0)

而调用
dup2(filedes, filedes2);
等效于
close(filedes);
fcntl(filedes, FDUPFD, filedes2);
第二个有区别,主要在于一个是原子操作,另一个不是。

sync、fsync和fdatasync函数

传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。写->缓冲区->输出队列->队首时,实际的I/O操作。这种方式称为延迟写。
内核已经提供了缓冲机制,不过这个缓冲机制是为了减少频繁的I/O操作。
之后说的标准I/O函数库的缓冲是指对系统调用的数据做了缓冲,这个缓冲的目的是为了减少系统调用次数。系统调用由于涉及到内核态和用户态的切换,是有一定的开销的。

延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。
为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。

#include
int fsync(int filedes);
int fdatasync(int filedes);

void sync(void);

sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
通过称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲写内核的块缓冲区。

fsync会等待磁盘操作结束,适合数据库应用。

fcntl函数

  • Note taken on [2016-04-05 Tue 06:56]

#include
int fcntl(int filedes, int cmd, … * int arg *);

fcntl函数有5种功能:
1. 复制一个现有的描述符(cmd = FDUPFD)
2. 获得/设置文件描述符标记(cmd = FGETFD或FSETFD)
3. 获得/设置文件状态标志(cmd = FGETFL或FSETFL)
4. 获得/设置异步I/O所有权(cmd = FGETOWN或FSETOWN)
5. 获得/设置记录锁(cmd = FGETLK、FSETLK或FSETLW)

由磁盘驱动器将队列数据写到磁盘上。
在UNIX系统中,通常write只是将数据排入队列,而实际的写磁盘操作则可能在以后的某个时刻进行。
程序运行时,设置OSYNC标志会增加时钟时间(等待磁盘IO操作结束).

ioctl函数

ioctl函数是I/O操作的杂物箱。终端I/O是ioctl的最大使用方面。

#include * System V *
\#include <sys/ioctl.h> * BSD and linux *
\#include * XSI STREAMS *

int ioctl(int filedes, int request, … );

每个设备驱动程序都可以定义它自己专用的一组ioctl命令。系统则为不同种类的设备提供通用的ioctl命令。

/dev/fd

打开文件/dev/fd/n 等效于复制描述符n。

/dev/fd文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其它路径名的相同方式处理标准输入和输出。

filter file2 | cat file1 – file3 | lpr
在命令行中用”-” 作为一个参数,特指标准输入或标准输出,这已由很多程序采用。但是这会带来一些问题,例如若用”-“指定第一个文件名,
那么它看起来就像指定了命令行中的一个选项,/dev/fd则提高了文件名参数的一致性,也更加清晰。