apue:文件I/O

来源:互联网 发布:微信网络推广 编辑:程序博客网 时间:2024/04/29 21:44

    UNIX系统可用的文件I/O函数—打开文件、读文件、写文件等等,大多数只需用到5个函数:open、read、write、lseek以及close,不同缓存器长度对read和write函数的影响不同。这些函数经常被称之为不带缓存的I/O(unbuffered I/O),不带缓存指的是每个read和write都调用内核中的一个系统调用。只要涉及在多个进程间共享资源,原子操作的概念就变成非常重要,通过文件I/O和传送给open函数的参数来讨论此概念。并进一步讨论在多个进程间如何共享文件,并涉及内核的有关数据结构。

文件描述符

    对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。UNIX shell使文件描述符0与进程的标准输入相结合,文件描述符1与标准输出相结合,文件描述符2与标准出错输出相结合。这是UNIX shell以及很多应用程序使用的惯例,而与内核无关。在POSIX.1应用程序中,幻数0、1、2应被代换成符号常数STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,这些常数都定义在头文件unistd.h中。文件描述符的范围是0~OPEN_MAX。

open函数

调用open函数可以打开或创建一个文件。

#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int open(const char* pathname, int oflag, ... /*,mode_t mode*/);//返回:若成功为文件描述符,若出错为-1

第三个参数写为…,这是ANSI C说明余下参数的数目和类型可以变化的方法。对
于open函数而言,仅当创建新文件时才使用第三个参数。pathname是要打开或创建的文件的名字。oflag参数可用来说明此函数的多个选择项。用下列一个或多个常数进行或运算构成oflag参数(这些常数定义在fcntl.h头文件中):
- O_RDONLY只读打开。
- O_WRONLY只写打开。
- O_RDWR读、写打开。
    很多实现将O_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2,以与早期的系统兼容。在这三个常数中应当只指定一个。下列常数则是可选择的:
- O_APPEND每次写时都加到文件的尾端。
- O_CREAT若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位。
- O_EXCL如果同时指定了O_CREAT,而文件已经存在,则出错。这可测试一个文件是
否存在,如果不存在则创建此文件成为一个原子操作。
- O_TRUNC如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0。
- O_NOCTTY如果pathname指的是终端设备,则不将此设备分配作为此进程的控制终端。
- O_NONBLOCK如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式。
- O_SYNC使每次write都等到物理I/O操作完成。
    由open返回的文件描述符一定是最小的未用描述符数字。这一点被很多应用程序用来在标准输入、标准输出或标准出错输出上打开一个新的文件。

creat函数

    也可用creat函数创建一个新文件。

#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int creat(const char* pathname, mode_t mode);//返回:若成功为只写打开的文件描述符,若出错为-1

    此函数等效于:open (pathname, O_WRONL|YO_CREAT|O_TRUNC, mode);
    creat的一个不足之处是它以只写方式打开所创建的文件。

close函数

    可用close函数关闭一个打开文件:

#include <unistd.h>int close(int filedes);//返回:若成功为0,若出错为-1

    关闭一个文件时也释放该进程加在该文件上的所有记录锁。当一个进程终止时,它所有的打开文件都由内核自动关闭。很多程序都使用这一功能而不显式地用close关闭打开的文件。

lseek函数

    每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。按系统默认,当打开一个文件时,除非指定O_APPEND选择项,否则该位移量被设置为0。可以调用lseek显式地定位一个打开文件。

#include <sys/types.h>#include <unistd.h>off_t lseek(int filedes, off_t offset, int whence);//返回:若成功为新的文件位移,若出错为-1

    对参数offset的解释与参数whence的值有关。
- 若whence是SEEK_SET,则将该文件的位移量设置为距文件开始处offset个字节。
- 若whence是SEEK_CUR,则将该文件的位移量设置为其当前值加offset,offset可为正或负。
- 若whence是SEEK_END,则将该文件的位移量设置为文件长度加offset,offset可为正或负。
    如果文件描述符引用的是一个管道或FIFO,则lseek返回-1,并将errno设置为EPIPE。lseek仅将当前的文件位移量记录在内核内,它并不引起任何I/O操作。然后,该位移量用于下一个读或写操作。文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空调,这一点是允许的。位于文件中但没有写过的字节都被读为0。

read函数

    用read函数从打开文件中读数据。

#include <unistd.h>ssize_t read(int filedes, void* buff, size_t nbytes);//返回:读到的字节数,若已到文件尾为0,若出错为-1

    有多种情况可使实际读到的字节数少于要求读字节数:
- 读普通文件时,在读到要求字节数之前已到达了文件尾端。
- 当从终端设备读时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
- 某些面向记录的设备,例如磁带,一次最多返回一个记录。
    读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读得的字节数。

write函数

    用write函数向打开文件写数据。

#include <unistd.h>ssize_t write(int filedes, const void* buff, size_t nbytes);//返回:若成功为已写的字节数,若出错为-1

    write出错的一个常见原因是:磁盘已写满,或者超过了对一个给定进程的文件长度限制。对于普通文件,写操作从文件的当前位移量处开始。如果在打开该文件时,指定了O_APPEND选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处。在一次成功写之后,该文件位移量增加实际写的字节数。

文件共享

    UNIX支持在不同进程间共享打开文件。
内核文件数据结构
    内核使用了三种数据结构,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
(a)文件描述符标志。
(b)指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
(a)文件状态标志(读、写、增写、同步、非阻塞等)。
(b)当前文件位移量。
(c)指向该文件v节点表项的指针。
(3)每个打开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针信息。对于大多数文件,v节点还包含了该文件的i节点(索引节点)。这些信息是在打开文件时从盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。
    两个独立进程各自打开了同一文件的情况如下图所示:
两个进程打开同一文件

说明

  • 在完成每个write后,在文件表项中的当前文件位移量即增加所写的字节数。如果这使当前文件位移量超过了当前文件长度,则在i节点表项中的当前文件长度被设置为当前文件位移量(也就是该文件加长了)。
  • 如果用O_APPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志的文件执行写操作时,在文件表项中的当前文件位移量首先被设置为i节点表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。
  • lseek函数只修改文件表项中的当前文件位移量,没有进行任何I/O操作。
  • 若一个文件用lseek被定位到文件当前的尾端,则文件表项中的当前文件位移量被设置为i节点表项中的当前文件长度。

原子操作

添加至一个文件

    假定有两个独立的进程A和B,都对同一文件进行添加操作。每个进程都已打开了该文件,但未使用O_APPEND标志。进程A和B先后对文件进行write操作,后面进程进行的write替换了前面进程write的内容,这里的问题出在逻辑操作“定位档到文件尾端处,然后写”使用了两个分开的函数调用。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。任何一个要求多于1个函数调用的操作都不能成为原子操作,因为在两个函数调用之间,内核有可能会临时挂起该进程(正如我们前面所假定的)。UNIX提供了一种方法使这种操作成为原子操作,其方法就是在打开文件时设置O_APPEND标志。正如前一节中所述,这就使内核每次对这种文件进行写之前,都将进程的当前位移量设置到该文件的尾端处,于是在每次写之前就不再需要调用lseek。

创建一个文件

    如果在打开和创建之间,另一个进程创建了该文件,那么就会发生问题。如果在这两个函数调用之间,另一个进程创建了该文件,而且又向该文件写进了一些数据,那么执行这段程序中的creat时,刚写上去的数据就会被擦去。将这两者合并在一个原子操作中,此种问题也就不会产生。一般而言,原子操作(atomicoperation)指的是由多步组成的操作。如果该操作原子地执行,则或者执行完所有步,或者一步也不执行,不可能只执行所有步的一个子集。

dup和dup2函数

    下面两个函数都可用来复制一个现存的文件描述符:

#include <unistd.h>int dup(int filedes);int dup2(int filedes, int filedes2);//两函数的返回:若成功为新的文件描述符,若出错为-1

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

fcntl函数

    fcntl函数可以改变已经打开文件的性质。

#include <sys/types.h>#include <unistd.h>#include <fcntl.h>int fcntl(int filedes, int cmd, .../*intarg*/);//返回:若成功则依赖于cmd,若出错为-1

    fcntl函数有五种功能:
- 复制一个现存的描述符(cmd=F_DUPFD)。
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。
- 获得/设置文件状态标志(cmd=F_GETFL或F_SETFL)。
- 获得/设置异步I/O有权(cmd=F_GETOWN或F_SETOWN)。
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)。
    fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。

0 0
原创粉丝点击