第三章 文件 IO
3.1 引言
UNIX系统中的大多数文件I/O只需用到5个函数:open、read、write、lseek和close。 本章所说明的函数经常被称为不带缓冲的I/O。
3.2 文件描述符
对内核而言,所有打开的文件都通过文件描述符引用。 文件描述符是一个非负整数,当打开或创建一个文件时,内核向进程返回一个文件描述符。 按照惯例,shell使用文件描述符0与进程的标准输入关联,文件描述符1与进程的标准输出关联,文件描述符2与进程的标准出错输出关联。 依从POSIX的程序中,0、1、2应替换成符号常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,这些常量定义在头文件中。
3.3 open函数
调用open函数可以打开或创建一个文件。 #include
int open(const char *pathname, int oflag, ... /* mode_t mode */);
返回值:成功返回文件描述符,出错返回-1。
第三个参数为...表示余下参数的类型和数量根据具体调用不同。 对应open函数而言,仅当创建新文件时才需要第三个参数。 pathname是要打开或创建的文件的名称。 oflag参数可以用来说明此函数的多个选项,用下列一个或多个常量进行或运算构成oflag参数,这些常量定义在头文件中: 选项 | 说明 | O_RDONLY | 只读打开 | O_WRONLY | 只写打开 | O_RDWR | 读写打开 | 以上三个常量必须指定一个且只能指定一个 | 下列常量可选: | O_APPEND | 每次写时都追加到文件的尾端。 | O_CREAT | 若文件不存在,则创建它。使用此选项时,需要第三个参数mode,以指定该文件的访问权限位(详见4.5节)。 | O_EXCL | 如果同时指定了O_CREAT,而文件已经存在,则会出错。用此可以测试一个文件是否存在,如果不存在,则创建文件,这使测试和创建成为一个原子操作。 | O_TRUNC | 如果文件存在,而且为只写或读写成功打开,则将其长度截短为0。 | O_NOCTTY | 如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终端。9.6节说明控制终端。 | O_NONBLOCK | 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞模式。14.2节将说明此工作模式。 | 下面三个标志也是可选的,它们是Single UNIX Specification(以及POSIX.1)同步输入和输出选项的一部分 | O_DSYNC | 使每次write操作等待物理I/O操作完成,但如果写操作并不影响读取刚写入的数据,则不等待文件属性被更新。 | O_RSYNC | 使每一个以文件描述符作为参数的read操作等待,直到任何对文件同一部分进行的未决写操作都完成。 | O_SYNC | 使每次write操作等待物理I/O操作完成,包括由write操作引起的文件属性更新所需的I/O。3.14节使用此项。 |
由open返回的文件描述符一定是最小的未用描述符数值,这一点被某些程序用来在标准输入,标准输出,标准出错输出上打开新文件。 如先关闭标准输出(文件描述符1),然后打开另一个文件,则新文件就会在文件描述符1上打开。 文件名和路径名截断: 在POSIX.1Z中,常量_POSIX_NO_TRUNC决定是要截断过长的文件名或路径名,还是返回一个错误。 若_POSIX_NO_TRUNC有效,则在整个路径名超过PATH_MAX或路径名中的任一文件名超过NAME_MAX时,返回出错状态,并将errno设置为ENAMETOOLONG。
3.4 creat函数
create函数用来创建一个新文件。 #include
int create(const char *pathname, mode_t mode);
返回值:成功返回只写打开的文件描述符,出错返回-1。
此函数等效于: open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
create不足之处在于只能使用只写方式打开创建的文件,所以open函数可以替代create函数。
3.5 close函数
close函数关闭一个打开的文件。 #include
int close(int filedes);
返回值:成功返回0,出错返回-1。
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。 当一个进程终止时,内核自动关闭它所打开的所有文件。
3.6 lseek函数
每个打开的文件都有一个与其相关联的当前文件偏移量。 当打开一个文件时,除非指定了O_APPEND选项,否则该偏移量被设置为0。 #include
off_t lseek(int filedes, off_t offset, int whence);
返回值:成功返回新的文件偏移量,出错返回-1。
参数说明: 若whence是SEEK_SET,则将文件的偏移量设置为距文件开始处offset个字节。 若whence是SEEK_CUR,则将文件的偏移量设置为当前值加offset,offset可为正或负。 若whence是SEEK_END,则将文件的偏移量设置为文件长度加offset,offset可为正或负。 如果lseek成功执行,则返回新的文件偏移量,因此可以用如下方式确定文件当前偏移量: off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方式也可以测试文件是否可以设置偏移量,如果文件描述符是一个管道,FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。 说明:SEEK_SET,SEEK_CUR,SEEK_END引入前,whence被指定为: 0 绝对偏移量 1 相对当前位置偏移量 2 相对文件尾端偏移量 注意:某些设备允许负的偏移量,但普通文件的偏移量必须是非负值。 因为偏移量可能是负值,所以在比较lseek的返回值时不能测试是否小于0,要测试是否等于-1。 文件偏移量可以大于文件的当前长度,这样,对该文件的下一次写操作将加长该文件,并在文件中构成一个空洞,这一点是允许的。 文件中的空洞并不要求在磁盘上占用存储区,具体处理方式和文件系统实现有关。
3.7 read函数
read函数从打开的文件读数据。 #include
ssize_t read(int filedes, void *buf, size_t nbytes);
返回值:成功返回读到的字节数,若已到文件结尾返回0,出错返回-1。
多种情况可能导致读到的字节数少于要求读的字节数: 1 读普通文件时读到要求字节数前已经到达文件尾。 2 从终端设备读时,通常一次最多读一行。 3 从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。 4 从管道或FIFO读时,如果管道中的字节少于所需字节的数量。 5 从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。 6 当某一信号造成中断,而已经读了部分数据量时。
3.8 write函数
write函数向打开的文件写数据。 #include
ssize_t write(int filedes, const void *buf, size_t nbytes);
返回值:成功返回已写的字节数,出错返回-1。
3.10 文件共享
内核使用三种数据结构标示打开的文件,它们的关系决定了文件共享方面一个进程对另一个进程可能产生的影响。 1 每个进程在进程表中都有一个记录项,包含有一张打开文件描述符表。与每个文件描述符相关联的是: a 文件描述符标志。 b 指向一个文件表项的指针。 2 内核为所有打开文件维持一张文件表。每个文件表项包含: a 文件状态标志(读、写、添加、同步、非阻塞等)。 b 当前文件偏移量。 c 指向该文件v节点表项的指针。 3 每个打开的文件都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数指针。 对于大多数文件,v节点还包含了该文件的i节点。这些信息是在打开文件的时候读入内存的。 i节点包含了文件的所有者,文件长度,文件所在设备,指向文件实际数据块在磁盘上所在位置的指针等等。 Linux没有使用v节点,而是直接使用了i节点。 图3-1显示了一个进程三张表之间的关系。 如果俩个独立的进程各自打开了同一个文件,则有图3-2中的安排。 打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。 每个进程都有自己的文件表项的一个理由是:使每个进程都有它自己的对该文件的当前偏移量。 现在对前面所述的操作做进一步说明: 1 完成write后,文件表项中的当前文件偏移量增加所写的字节数。 如果这使当前文件偏移量超出了文件长度,则在i节点表项中的当前文件长度被设置为当前文件偏移量。 2 如果用O_APPEND标志打开一个文件,则相应的标志也被设置到文件表项的文件状态标志中。 每次对这种具有添写标志的文件执行写操作时,在文件表项中的当前文件偏移量首先被设置为i节点表项中的文件长度。 这就使得每次写数据都写到文件的当前尾端。 3 若一个文件用lseek定位到文件当前尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。 注意:这与用O_APPEND标志打开文件是不同的, 详见3.11节。 4 lseek函数只是修改文件表项中的当前文件偏移量,没有进行任何I/O操作。
最后,可能有多个文件描述符项指向同一文件表项。dup函数或父子进程中会出现文件描述符共享同一个文件表项。 注意,文件描述符标志和文件状态标志在作用域方面的区别,前者只用于一个进程的一个描述符,后者则适用于指向该给定文件表项的任何进程中的所有描述符。 本节上面所述一切对多个进程读同一文件都能正确工作,每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。 但是,当多个进程写同一文件时,则可能产生预期不到的结果,这就需要理解原子操作的概念。
3.11 原子操作
1 添写至一个文件 任何一个需要多个函数调用的操作都不可能成为原子操作,因为在俩个函数调用之间,内核有可能会临时挂起该进程。 打开文件时设置O_APPEND标志,使内核每次对文件进行写之前,都将进程的当前偏移量设置到该文件的尾端,于是每次写之前都不需要调用lseek函数。 2 pread和pwrite函数 原子性地定位搜索和执行I/O。 #include
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
返回值:成功返回独到的字节数,若已到文件结尾返回0,出错返回-1。
ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);
返回值:成功返回已写的字节数,出错返回-1。
调用pread相当于顺序调用lseek和read函数,重要区别: a 调用pread时,无法中断其定位和读操作。 b 不更新文件指针。 pwrite也有类似区别。 3 创建一个文件 open函数同时指定O_CREAT和O_EXCL选项时就是一个判断文件是否存在然后创建文件的原子操作。
3.12 dup和dup2函数
复制一个现存的文件描述符。 #include
int dup(int filedes);
int dup2(int filedes, int filedes2);
俩函数的返回值:成功返回新的文件描述符,出错返回-1。
由dup返回的文件描述符一定是当前可用的文件描述符的最小值。 用dup2则可用filedes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。如果filedes等于filedes2,则dup2返回filedes2,而不关闭它。 这些函数返回的新文件描述符与参数filedes共享同一个文件表项,所以俩个文件描述符共享同一文件状态标志(读、写、添写等)以及同一当前文件偏移量。 图3-3显示了这种情况。 复制一个描述符的另一个方式是使用fcntl函数。
3.13 sync、fsync和fdatasync函数
内核的缓冲会使输出延迟写,这样减少了磁盘读写次数,但却降低了文件内容的更新速度,系统故障时可能会造成数据丢失。 为保证磁盘中实际文件系统和缓冲区中内容一致,UNIX提供了一下三个函数: #include
int fsync(int filedes);
int fdatasync(int filedes);
返回值:成功返回0,出错返回-1。
void sync(void);
sync函数只是将修改过的块缓冲区排入写队列,然后就返回,不等待实际写磁盘操作结束。 fsync函数只对由文件描述符filedes指定的单一文件起作用,并等待写磁盘操作结束,然后返回。 fdatasync函数类似于fsync,但他只影响文件的数据部分,而除数据外,fsync还会同步更新文件的属性。
3.14 fcntl函数
fcntl函数可以改变已打开文件的性质。 #include
int fcntl(int filedes, int cmd, ... /* int arg */);
返回值:成功则依赖于cmd,出错则返回-1。
fcntl函数有5***能: 1 复制一个现有的描述符(cmd = F_DUPFD)。 2 获取/设置文件描述符标记(cmd = F_GETFD或F_SETFD)。 3 获取/设置文件状态标志(cmd = F_GETFL或F_SETFL)。 4 获取/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)。 5 获取/设置记录锁(cmd = F_GETLK、F_SETLK或F_SETLKW)。 先说明前7中cmd(后3中在14.3节中说明,它们都与记录锁有关): cmd | 说明 | F_DUPFD | 复制文件描述符filedes。新文件描述符作为返回值返回。 它是尚未打开的文件描述符中大于等于第三个参数中各值的最小值。 新描述符与filedes共享同一文件表项(见图3-3)。 但是新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除(这标示该描述符在通过一个exec时仍保持有效,第8章讨论)。 | F_GETFD | 对应于filedes的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。 | F_SETFD | 对于filedes设置文件描述符标志。新标志值按第三个参数设置。 | F_GETFL | 对应于filedes的文件状态标志作为函数值返回。 | | 文件状态标志 | 说明 | O_RDONLY | 只读打开 | O_WRONLY | 只写打开 | O_RDWR | 读写打开 | O_APPEND | 每次写时追加 | O_NONBLOCK | 非阻塞模式 | O_SYNC | 等待写完成(数据和属性) | O_DSYNC | 等待写完成(仅数据) | O_RSYNC | 同步读写 | O_FSYNC | 等待写完成(仅FreeBSD和Mac OS X) | O_ASYNC | 异步I/O(仅FreeBSD和Mac OS X) |
| 不幸的是,三个访问方式标志O_RDONLY、O_WRONLY、O_RDWR并不各占1位(其值为0、1、2)。 因此必须首先用屏蔽字O_ACCMODE取得访问模式位,然后将结果与这三种值中的任一种做比较。 | F_SETFL | 将文件状态标志设置为第三个参数的值。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC、O_ASYNC。 | F_GETOWN | 获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。14.6.2节详细讨论这俩种异步I/O信号。 | F_SETOWN | 设置当前接收SIGIO和SIGURG信号的进程ID或进程组ID。正的arg指定一个进程ID,负的arg标示等于arg绝对值的一个进程组ID。 |
fcntl的返回值与命令有关,出错所有命令都返回-1,成功则返货某个其他值。 下列四个命令有特定返回值: F_DUPFD 返回新文件描述符。 F_GETFD 返回相应的标志。 F_GETFL 返回相应的标志。 F_GETOWN 返回一个正的进程ID或负的进程组ID。
fsync和fdatasync在需要的时候更新文件内容。 O_SYNC在每次写文件时更新文件内容。 fcntl函数允许仅知道打开文件描述符时修改其性质。
3.15 ioctl函数
ioctl函数是I/O操作的杂物箱,不能用本章其他函数表示的I/O操作通常都能用ioctl标示。终端I/O是ioctl的最大使用方面。 #include /* System V */
#include /* BSD and Linux */
#include /* XSI STREAMS */
int ioctl(int filedes, int request, ...);
以后的章节中会陆续用到ioctl函数。
3.16 /dev/fd
较新的系统都提供名为/dev/fd的目录,其目录项是名为0,1,2等的文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。 函数调用: fd = open("/dev/fd/0", mode);
大多数系统忽略mode,另外一些则要求mode必须是所涉及的文件原先打开所使用的mode的子集。 因为上面的函数调用等效于: fd = dup(0);
所以描述符0和fd共享同一文件表项(见图3-3)。例如,若描述符0先前被打开为只读,那么对fd也只能进行读操作。
示例程序3-1 #include
#include
int
main()
{
/*标准输入不能移动当前文件位置*/
if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
printf("can not seek\n");
else
printf("seek OK\n");
exit(0);
}
示例程序3-2 #include
#include
#include
int
main()
{
char read_buffer[1024];
char write_buffer[1024];
int count;
long pos;
memset(read_buffer, 0, sizeof(read_buffer));
memset(write_buffer, 0, sizeof(write_buffer));
/*追加方式只影响写会写到文件尾部,读的时候可以选择任意位置*/
int fd = open("data.dat", O_RDWR | O_CREAT/* | O_APPEND*/, 0);
strcpy(write_buffer, "0123456789");
count = write(fd, write_buffer, strlen(write_buffer));
printf("write char: %d\n", count);
pos = lseek(fd, 0, SEEK_SET);
printf("lseek pos: %ld\n", pos);
count = read(fd, read_buffer, 5);
printf("read char: %d\n", count);
printf("read char: %s\n", read_buffer);
pos = lseek(fd, 0, SEEK_CUR);
printf("current pos: %ld\n", pos);
strcpy(write_buffer, "abcdef");
count = write(fd, write_buffer, strlen(write_buffer));
printf("write char: %d\n", count);
close(fd);
exit(0);
}
示例程序3-3 #include
#include
#include
int
main()
{
char read_buffer[1024];
char write_buffer[1024];
int count;
long pos;
memset(read_buffer, 0, sizeof(read_buffer));
memset(write_buffer, 0, sizeof(write_buffer));
int fd = open("data.dat", O_RDWR);
/*pwrite不影响文件当前位置*/
strcpy(write_buffer, "xxxxx");
count = pwrite(fd, write_buffer, strlen(write_buffer), 5);
printf("write char: %d\n", count);
pos = lseek(fd, 0, SEEK_CUR);
printf("current pos: %ld\n", pos);
/*pread不影响文件当前位置*/
count = pread(fd, read_buffer, 5, 1);
printf("read char: %d\n", count);
printf("read char: %s\n", read_buffer);
pos = lseek(fd, 0, SEEK_CUR);
printf("current pos: %ld\n", pos);
close(fd);
exit(0);
}
|