当前位置:Linux教程 - Linux - UNIX环境高级编程(文件I/O)

UNIX环境高级编程(文件I/O)



        
    尤晋元 译

    第三章 文件I/O
    31 引言

    本章开始讨论Unix系统,先说明可用的文件I/O函数〖CD2〗打开文件、读文件、写文件等等。大多数Unix文件I/O只要用5个函数:open、read、write、lseetp以及close。然后说明不同缓冲器长度对read和write函数的影响。
    本章所说明的函数经常被称之为不带缓冲的I/O(与将在第五章中说明的标准I/O函数相对照)。术语一不带缓冲指的是每个read和write都调用一个核中的系统调用。这些不带缓冲的I/O函数不是ANSI C的组成部分,但是是POSIX1和XPG3的组成部分。
    只要涉及在多个进程间共享资源,原子操作的概念就变成非常重要。我们将通过文件I/O和传送给Open函数的参数来讨论此概念。这将导致讨论在多个进程间是如何共享文件的,并涉及系统核的有关数据结构。在讨论了这些特征后,我们将说明dup、fcntl和ioctl函数。

    32〓文件描述符
    对于系统核而言,所有打开文件都由文件描述符引用。一个文件描述符是个非负整数。当打开一个现存文件或创建一个新文件时,系统核向进程返回一个文件描述符。与我们要读、写一个文件时,用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。
    按照惯例,Unix Shell使文件描述符O与一个进程的标准输入相结合,使文件描述符1与标准输出相结合,使文件描述符2与标准出错输出相结合。这是Unix shell以及很多应用程序使用的惯例,而与系统核无关。尽管如此,如果不遵照这种惯例那么很多Unix应用程序就会不能工作。
    在POSIX1应用程序中,这些幻数0,1,2应被代换成符号常数STDIN〖CD#2〗FILENO,STDOUT[CD#*2]FILENO和STDERR[CD#*2]FILENO。这些常数都定义在头文件中。


    文件描述符的范围是0〖CD#*2]OPEN[CD#*2]MAX(请回忆图27)。较早的Unix版本采用的上限值是19(允许每个进程打开20个文件),现在很多系统则将其增加为63。SVR4和43+BSD对文件描述符的变化范围没有作规定,它只受到系统配置的存储器的总量、整型字的字长以及系统管理员所配置的软性或硬性限制的约束。
    33〓Open函数
    调用Open函数打开或创建一个文件。
    #include
    #include
    #include
    int open(const char *pathname,int oflag,/*,mode[CD#*2]t mode *

    /);
    返回:若成功为文件描述符,出错为-1
    我们将第3个参数写为,这是ANSI C说明余下参数的数目和类型可以变化的方法。对于Open函数而言,仅当创建新文件时才使用第3个参数(我们将在稍后对此进行说明。)在函数原型中我们将此参数放置在注释中。
    pathname是要打开或创建的文件的名字。flag参数可用来说明此函数的多个可选择项。用下列一个或多个常数相或(OR)构成flag参数(这些常数定义在头文件中)。
    O[CD#*2]RDONLY 只读打开
    O[CD#*2]WRONLY 只写打开
    O[CD#*2]RDWR 读、写打开
    很多实现将O[CD#*2]RDONLY定义为0,0〖CD#*2]WRONLY定义为1,O[CD#*2]RDWR定义为2,以与较早的系统兼容。
    在这三个常数中应当也只应指定一个。下列常数则是可选择的:
    O[CD#*2]APPEND〓在每次写时都加到文件的尾端。我们将在311中详细说明此选择项。
    O[CD#*2]CREAT〓若此文件不存在则创建它。使用此可选项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位。(我们将在45节中说明文件的许可权位,那时就能了解如何说明mode,以及如何用进程的vmask值修改它。)
    O[CD#*2]EXCL〓如果同时指定了O[CD#*2]CREAT,而文件已经存在,则出错。这使得测试一个文件是否存在,如果不存在则创建此文件成为一个原子操作。我们将在311节中较详细的说明原子操作。
    O[CD#*2]TRUNC〓如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0。
    O[CD#*2]NOCTTY〓如果pathname指的是终端设备,则不将此设备分配作为此进程的控制终端。我们在96节中说明控制终端。
    O[CD#*2]NONBLOCK〓如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式。在122中我们将说明此工作方式。
    较早的系统Ⅴ版本引入了O[CD#*2]NDELAY(不延迟)标志,它与O[CD#*2]NONBLOCK(不阻塞)选择项类似,但在读操作的返回值中具有两义性。如果不能从管道、FIFO或设备读得数据,则不延迟选择项使read返回0,这与表示已读到文件尾端的返回值0相冲突。SVR4仍支持这种语义的不延迟选择项,但是新的应用程序应当使用不阻塞选择项以代替之。
    O[CD#*2]SYNC〓使每次write都等到物理I/O操作完成。我们将在313使用此选择项。
    由Open返回的文件描述符一定是最小的未用描述符数字。这一类被很多应用程序用来在标准输入、标准输出或标准出错输出上打开一个新的文件。例如,一个应用程序可以先关闭标准输出(通常是文件描述符1),然后打开另一个文件,事先就能了解到该文件一定会在文件描述符1上打开。在312节说明dup函数时我们可以了解到有更好的方法能保证在一个给定的描述符上打开一个文件。
    文件名和路径名截短
    如果NAME[CD3*2]MAX是14,而我们却试图在当前目录中创建一个其文件名包含15个字符的新文件,此时会发生什么呢?按照传统的系统Ⅴ版本,允许这种使用方法,但是总是将文件名截短为14个字符,而BSD类的系统则返回出错ENAMETOOLONG。这一问题不仅仅与创建新文件有关。如果NAME[CD#*2]MAX是14,而存在一个其文件名恰恰就是14个字符的文件,那么以pathname作为其参数的任一函数(open,stat等)都会遇到这一问题。
    在POSIX1中,常数[CD#*2]POSIX[CD#*2]NO[CD#*2]TRUNC决定了是否要截短过长的文件名或路径名,或者返回一个出错。在第十二章中,我们将说明此值可以针对各个不同的文件系统进行更变。
    FIPS151-1要求返回出错。
    SVR4对传统的系统Ⅴ文件系统(S5)并不保证返回出错(见图26),但是对BSD风格的文件系统(UFS),SVR4保证返回出错,43+BSD总是返回出错。
    若[CD#*2]POSIX[CD#*2]NO[CD#*2]TRUNC有效,则在整个路径名超过PATH[CD#*2]MAX,或路径名中的任一文件名分量超过NAME[CD#*2]MAX时,返回出错ENAMETOOLONG。
    34〓Creat函数
    也可用creat函数创建一个新文件。
    #include
    #include
    #include
    int creat(const char *pathname,mode[CD#*2]t mode);
    返回:若成功为只写打开的文件描述符,出错为-1
    注意,此函数等效于:
    open(pathname,O[CD#*2]WRONLY|O[CD#*2]CREAT|O[CD#*2]TRUNC,mode);
    在早期的Unix版本中,open的第2个参数只能是0,1或2。没有办法打开一个尚未存在的文件

    ,因此需要另一个系统调用creat以创建新文件。现在,open函数提供了可选项O[CD#*2]CRE

    AT和O[CD#*2]TRUNC,于是也就不再需要creat函数了。
    在45节中详细说明文件存取权时,我们将说明如何指定mode。
    creat的一个不足之处是它以只写方式打开所创建的文件。在提供open的新版本之前,如果

    我们要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用creat,close

    ,然后再调用open。现在则可用下列方式调用open:
    open(pathname,O[CD#*2]RDWR|O[CD#*2]CREAT|O[CD#*2]TRUNC,mode);
    35〓close函数
    用close函数关闭一个打开文件:
    #include
    int close (int filedes);
    返回:若成功为0,出错为-1
    关闭一个文件时也释放该进程加在该文件上的所有记录锁。将在123节中讨论这一点。
    当一个进程终止时,它们所有打开文件都由系统核自动关闭。很多程序都使用这一功能而不

    显式地用close关闭打开文件。实例见程序12。
    36〓lseek函数
    每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量从

    文件开始处计算的字节数。(在本节稍后处,我们将对“非负”这一修饰词的某些例外进行

    说明。)通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数

    。按系统默认,当打开一个文件时,除非指定O[CD#*2]APPEND选择项,否则该位移量被设置

    为0。
    可以调用lseek显式地定位一个打开文件。
    #include
    #include
    off[CD#*2]t lseek(int filedes,off[CD#*2]t offset,int whence);
    返回:若成功为新的文件位移,出错为-1
    对参数offset的解释与参数whence的值有关。
    ·若whence是SEEK[CD#*2]SET,则将该文件的位移量设置为距文件开始处offset个字节。
    ·若whence是SEEK[CD#*2]CUR,则将该文件的位移量设置为其当前值加offset。offset可为

    正或负。
    ·若whence是SEEK[CD#*2]END,则将该文件的位移量设置为文件长度加offset,offset可为

    正或负。
    若lseek成功执行,则返回新的文件位移量,为此可以用下列方式确定一个打开文件的当前

    位移量:
    off[CD#*2]t currpos;
    currpos=lseek(fd,0,SEEK[CD#*2]CUR);
    这种方法也可用来确定所涉及的文件是否是可以设置位移量的。如果文件描述符引用的是一

    个管道或FIFO,则lseek返回-1,并将errno设置为EPIPE。
    三个符号常数SEEK[CD#*2]SET,SEEK[CD#*2]CUR和SEEK[CD#*2]END是由系统Ⅴ引进的。在系

    统Ⅴ之前,whence被指定为0(绝对位移量),1(相对于当前位置的位移量)或2(相对文件尾端

    的位移量)。很多软件仍直接使用这些数字进行编码。
    在lseek中的字符l表示长整型。在引入off[CD#*2]t数据类型之前,offset参数和返回值是

    长整型的。lseek是由Version 7引进的,当时C语言中增加了长整型。(在Version 6中,用

    函数seek和tell提供类似功能。)
    实例

    程序31测试其标准输入能否被设置位移量。
    程序31〓测试标准输入能否被设置位移量。
    如果我们用交互方式调用此程序,则可得:
    $ aout < /etc/motd
    seek OK
    $ cat < /etc/motd |aout
    cannot seek
    $ aout < /var/spool/cron/FIFO
    cannot seek
    通常,一个文件的当前位移量应当是一个非负整数,但是,某些设备也可能允许负的位移量

    。但对于普通文件,则其位移量必须是非负值。因为位移量可能是负值,所以在比较lseek

    的返回值时应谨慎,不要测试它是否小于0,而要测试它是否等于-1。
    在80386上运行的SVR4支持/dev/kmem设备可以具有负的位移量。
    因为位移量是带符号数据类型(off[CD#*2]t)(见图28),所以最大文件长度减少一半。例

    如,若off[CD#*2]t是32位整型,则最大文件长度是231字节。
    lseek仅将当前的文件位移量记录在系统核内,它并不引起任何I/O操作。然后,该位移量用

    于下一个读或写操作。
    文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,

    并在文件中构成一个空调,这一点是允许的。位于文件中但没有写过的字节都被读为0。
    实例
    程序32创建一个具有空洞的文件
    程序32创建一个具有空洞的文件
    运行该程序得到:
    $ aout
    $ ls -1 filehole 〓〓〓〓〓〓〖WB〗检查其大小
    -rw-r--r--〓1 stevens〓50 Jul 31 05:50 filehole
    $ od -c filehole〖DW〗观察实际内容
    000000〓〖WB〗a〓b〓c〓d〓e〓f〓g〓h〓i〓j〓\0〓\0〓\0〓\0〓\0〓\0
    000020〖DW〗\0〓\0〓\0〓\0〓\0〓\0〓\0〓\0〓\0〓\0〓\0〓\

    0〓\0〓\0〓\0〓\0
    000040〖DW〗\0〓\0〓\0〓\0〓\0〓\0〓\0〓\0〓A〓B〓C〓D〓E〓

    F〓G〓H
    000060〖DW〗I〓J
    000062
    我们使用od(1)命令观察该文件的实际内容。命令行中的-C标志表示以字节方式打印文件内

    容。从中可以看到,文件中间的30个未写字节都被读成为0。每一行开始的一个七位数是以

    八进制形式表示的字节位移量。在本例中,我们调用了将在38节中说明的write函数。在4

    12节中我们将对具有空洞的文件进行更多说明。
    37〓read函数
    用read函数从打开文件中读数据。
    #include
    ssize[CD#*2]t read(int filedes,void *buff,size[CD#*2]t nbytes);
    返回:读到的字节数,若已到文件尾为0,出错为-1
    如read成功则返回读到的字节数。如果已到达文件的尾端,则返回0。
    有多种情况可使实际读到的字节数少于要求读字节数:
    读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前还

    有30个字节,而我们却要求读100个字节,则read返回30,下一次我们再调用read时,它将

    返回0(文件尾端)。
    ·当从终端设备读时,通常一次最多读一行(在第十一章中将介绍如何改变这一点)。
    ·当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
    ·某些面向记录的设备,例如磁带,一次最多返回一个记录。
    读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读得的字节数。
    POSIX1在几个方面对此函数的原型作了更改。其经典定义是:
    int read(int fildes,char *buff,unsigned nbytes);
    首先,为了与ANSI C一致,其第2个参数由char *改为void *。在ANSI C中,类型void *用

    于表示类属指针。其次,其返回值必须是一个带符号整数(ssize[CD#*2]t),以返回正字节

    数、0(表示文件尾端)或-1(出错)。最后,第3个参数在历史上是个不带符号整数,以允许一

    个16位的实现可以一次读或写至65534个字节。在1990 POSIX1标准中,引进了新的基本系

    统数据类型ssize[CD#*2]t以提供带符号的返回值,size[CD#*2]t则被用于第3个参数(回忆

    图27中的SSIZE〖CD#*2]MAX常数。)
    38〓write函数
    用write函数向打开文件写数据。
    #include
    ssize[CD#*2]t write(int filedes,const void *buff,size[CD#*2]t nbytes

    );
    返回:若成功为已写的字节数,出错为-1
    其返回值通常导于参数nbyte,否则表示出错。write出错的一个常见原因是:或者磁盘已写

    满,或者超过了对一个给定进程的文件长度限制(见711节及练习1011)
    对于普通文件,写操作从文件的当前位移量处开始。如果在打开该文件时,指定了O[CD#*2]

    APPEND选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处,在一次成功

    写之后,该文件位移量增加实际写的字节数。
    39〓I/O的效率
    程序33只使用read和write函数来复制一个文件。关于该程序应注意下列各点:
    ·它从标准输入读,写至标准输出,这就假定在执行本程序之前,这些标准输入、输出已由

    shell安排好。确实,所有常用的Unix shell都提供一种方法,它在标准输入上打开一个文

    件用于读,在标准输出上创建(或重写)一个文件。
    ·很多应用程序假定标准输入是文件描述符0,标准输出是文件描述符1。在本例中,我们则

    用两个在中定义的名字STDIN[CD#*2]FILENO和STDOUT[CD#*2]FILENO。
    ·考虑到进程终止时,Unix会关闭所有打开文件描述符,所以此程序并不close输入和输出

    文件。
    ·在程序对文本文件和两进制代码文件都能工作,因为对Unix系统核而言,这两种文件并无

    区别。
    程序33〓将标准输入复制到标准输出
    我们没有回答的一个问题是如何选取BUFFSIZE值。在回答此问题之前,让我们先用各种不同

    的BUFFSIZE值来运行此程序。图31显示了用18种不同的缓存长度,读1,468,802字节文

    件所得到的结果。
    程序33读文件,其标准输出则被重新空向到/dev/null上。此测试所用的文件系统是贝克

    莱快速文件系统,其块长为8192字节。(块长由st[CD#*2]blksize表示,在412中说明为81

    92)。系统CPU时间的最小值开始出现在BUFFSIZE为8192处,继续增加缓存长度对此时间并我

    影响。
    我们以后还将回到这一实例上。在313节中我们将用此说明同步写的效果,在58节中,

    则将比较不带缓存所用的时间及标准I/O库所用的时间。
    310〓文件共享
    Unix支持在不同进程间共享打开文件。在介绍dup函数之间,我们需要先说明这种共享。为

    此先说明系统核用于所有I/O的数据结构。
    系统核使用了三个数据结构,它们之间的关系决定了文件共享方面一个进程对另一个进程可

    能产生的影响。
    1每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,我们可

    将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
    (a)文件描述符标志,
    (b)指向一个文件表项的指针。
    图31〓不同缓存长度进行读操作的时间结果
    2系统核为所有打开文件维持一张文件表。每个文件表项包含:
    (a)文件状态标志(读,写,增写,同步,非阻塞等),
    (b)当前文件位移量,
    (c)指向该文件v-node(v字节)表项的指针。
    3每个打开文件(或设备)都有一个V[CD#*2]node结构。V[CD#*2]node包含了文件类型和对

    此文件进行各种操作的函数的指针信息。对于大多数文件,v[CD#*2]node还包含了该文件的

    i[CD#*2]node(索引节点)。这些信息是在打开文件时从盘上读入内存的,所以所有关于文件

    的信息都是快速可供使用的。例如,i[CD#*2]node包含了文件的属主、文件长度、文件所在

    的设备、指向文件在盘上所使用的实际数据块的指针等等。(在414节中较详细地说明Unix

    文件系统时,会更多地说明i[CD#*2]node)。
    我们忽略了某些并不影响我们讨论的实现细节。例如,打开文件描述符表通常在用户区而不

    在进程表中。在SVR4中,此数据结构是一个链接表结构。文件表可以用多种方法实现一它不

    一定是文件表项数组。在43+BSD中,V[CD#*2]node包含了实际i[CD#*2]node(如图32中

    所示)。SVR4对于大多数文件系统类型,将v[CD#*2]node存放在i[CD#*2〗node中。这些实现

    细节并不影响我们对文件共享的讨论。
    图32图示了一个进程的这三张表之间的关系。该进程有两个不同的打开文件〖CD2〗一个

    文件打开为标准输入(文件描述符0),另一打开为标准输出(文件描述符为10。
    图32〓打开文件的系统核数据结构
    从Unix的早期版本〔Thompson1978〕以来,这三张表之间的基本关系一直保持至今。这种安

    排对于在不同进程之间共享文件的方式是非常重要的。在以后的章节中述及其它的文件共享

    方式时还会回到这张图上来。
    v[CD#*2]node结构是近来增设的。当在一个给定的系统上对多种文件系统类型提供支持时,

    就需要这种结构,这一工作是由Peter Weinberger(Bell实验室)和Bill Joy(Sun Microsyst

    ems)分别独立完成的。Sun称此种文件系统为虚拟文件系统(Virtual File System),称与文

    件系统类型无关的i[CD#*2]node部分为v[CD#*2]node〔Kleiman 1986〕。当各个制造商的实

    现增加了对Sun网络文件系统(NFS)的支持时,它们都广泛采用了v[CD#*2]node结构。
    在SVR4中,v[CD#*2]node代换了SVR3中的与文件系统类型无关的i[CD#*2]node结构。
    如果两个独立进程各自打开了同一文件,则有图33中所示的安排。我们假定第一个进程使

    该文件在文件描述符3上打开,而另一个进程则使此文件在文件描述符4上打开。打开此文件

    的每个进程得到一个文件表项,但对一个给定的文件只有一个v[CD#*2]node表项。每个进程

    都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前位移

    量。
    图33〓两个独立进程各自打开同一个文件
    给出了这些数据结构后,我们现在对前面所述的操作作进一步说明。
    ·在完成每个write后,在文件表项中的当前文件位移量即增加所写的字节数。如果这使当

    前文件位移量超过了当前文件长度,则在i〖CD#*2〗node表项中的当前文件长度被设置为当

    前文件位移量(也就是该文件加长了)。
    ·如果用O[CD#*2]APPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态

    标志中。每次对这种具有添写标志的文件执行写操作时;在文件表项中的当前文件位移量首

    先被设置为i[CD#*2]node表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾

    端处。
    ·lseek函数只修改文件表项中的当前文件位移量,没有进行任何I/O操作。
    ·若一个文件用lseek被空位到其当前的尾端,则所做的一切就是在文件表项中的当前文件

    位移量被设置为i[CD#*2]node表项中的当前文件长度。
    可能有多于1个文件描述符项指向同一文件表项。在312节中讨论dup函数时,我们就能看

    到这一点。在fork后同样也发生这种情况,此时父、子进程各自但数值相同的打开文件描述

    符指向(共享)同一个文件表项
    注意,文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个

    描述符,而后者则适用于指向该给定文件表项的在任何进程中的所有描述符。在313节说

    明fcntl函数时,我们将会了解如何存取和修改文件描述符标志和文件状态标志。
    在本节中至此所说明的一切对于多个进程读同一文件都能正确工作。每个进程都有它自己的

    文件表项,在其中也就有它自己的当前文件位移量。但是,当多个进程写同一文件时,则可

    能产生非预期的结果。为了说明如何避免这种情况,我们需要理解原子操作的概念。
    311〓原子操作
    添写至一个文件
    考虑一个进程,它要将数据添写到一个文件尾端。较早的Unix版本并不支持open的O[CD#*2]

    APPEND选择项,所以程序被编写成下列形式:
    if(lseek(fd,OL,2)<0)〓〓〓〓〓〓〖WB〗/*定位至文件尾端*/
    err[CD#*2]sys(“lseek error”);
    if (write(fd,buff,100)!=100)〖DW〗/*写*/
    err[CD#*2]sys(“write error”);
    对单个进程而言,这段程序能正常工作,但若有多个进程使用这种技术添写到同一文件中,

    则就会产生问题。(如果此程序由多个进程同时执行,各自将消息添写到一个日记文件中,

    则就会产生这种情况。)
    假定有二个独立的进程A和B,都对同一文件进行添写操作。每个进程都已打开了该文件,但

    未使用O[CD#*2]APPEND标志。此时各数据结构之间的关系如图33中所示一样。每个进程都

    有它自己的文件表项,但是共享一个v[CD#*2]node表项。假定进程A调用了lseek,它将对于

    进程A的该文件当前位移量设置为1500字节(当前文件尾端处)。然后系统核切换进程使进程B

    运行。进程B执行lseek,也将其对该文件的当前位移量设置为1500字节(当前文件尾端处)。

    然后B调用write,它将B的该文件当前文件位移量增至1600。因为该文件的长度已经增加了

    ,所以系统核对v[CD#*2]node中的当前文件长度更新为1600。然后,系统核又进行进程切换

    使进程A恢复运行。当A调用write时,就从其当前文件位移量(1500)处将数据写到文件中去

    。这样也就代换了进程B刚写到该文件中的数据。
    这里的问题出在逻辑操作“空档到文件尾端处,然后写”使用了二个分开的函数调用。解决

    问题的方法是使这两个操作对于其它进程而言成为一个原子操作。任何一个要求多于1个函

    数调用的操作都不能成为原子操作,因为在两个函数调用之间,系统核有可能会临时挂起该

    进程(正如我们前面所假定的)。
    Unix提供了一种方法使这种操作成为原子操作,其方法就是在打开文件时设置O[CD#*2]APPE

    ND标志。正如我们在前一节中所说明的,这就使系统核在每次对这种文件进行写之前,都将

    进程的当前位移量设置到该文件的必端处,于是在每次写之前就不再需要调用lseek。
    创建一个文件
    在对open函数的O[CD#*2]CREAT和O[CD#*2]EXCL选择项进行说明时,我们已见到了另一个有

    关原子操作的例子。当同时指定这两个选择项,而该文件又已经存在时,open将失败。我们

    曾提及检查该文件是否存在以及创建该文件这两个操作是作为一个原子操作执行的。如果我

    们没有这样一个原子操作,那么可能会编写下列程序段:
    if ((fd=open(pathname,O[CD#*2]WRONLY)) <0)
    〓〓if (errno==ENOENT) {
    〓〓〓〓if ((fd=creat (pathname,mode))<0)
    〓〓〓〓〓〓err[CD#*2]sys(\"creat error\");
    〓〓} else
    〓〓〓〓err[CD#*2]sys(\"open error\");
    如果在打开和创建之间,另一个进程创建了该文件,那么就会发生问题。如果在这两个函数

    调用之间,另一个进程创建了该文件,而且又向该文件写进了一些数据,那么执行这段程序

    中的creat时,刚写上去的数据就会被擦去。将这两者合并在一个原子操作中,此种问题也

    就不会产生。
    一般而言,术语原子操作指的是由多步组成的操作。如果该操作原子地执行,则或者执行完

    所有步,或者一步也不执行,没有可能只执行所有步的一个子集。在415节述及link函数

    以及在123节中述及记录锁时,我们还将讨论原子操作。
    312〓dup和dup2函数
    下面两个函数都可用来复制一个现存的文件描述符:
    #include
    int dup(int filedes);
    int dup2(int filedes,int filedes2);
    两函数的返回:若成功为新的文件描述符,出错为-1
    由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2我们则可以用f

    iledes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。如若filedes等

    于filedes2,则dup2返回filedes2,而不关闭它。
    这些函数返回的新文件描述符与参数filedes共享同一个文件表项。图34显示了这种情况

    。
    
    图34〓dup(1)后系统核数据结构
    在此图中,我们假定进程执行了:
    newfd=dup(1);
    当此函数开始执行时,我们假定下一个可用的描述符是3(这是非常有可能的,因为0,1和2

    是由shell打开的)。因为两个描述符指向同一文件表项,所以它们共享同一文件状态标志(

    读、写、添写等)以及同一当前文件位移量。
    每个文件描述符都有它自己的一套文件描述符标志。如我们在下一节中将说明的那样,新描

    述符的执行时关闭(close[CD#*2]on[CD#*2]exec)文件描述符标志总是由dup函数清除。
    复制一个描述符的另一种方法是使用fcntl函数,我们将在下一节对该函数进行说明。确实

    ,调用
    dup(filedes);
    等效于
    fcntl (filedes,F[CD#*2]DUPFD,0);
    而调用
    dup2(filedes,filedes2);
    等效于
    close(filedes2);
    fcntl(filedes,F[CD#*2]DUPFD,filedes2);
    在后一种情况下,dup2并不完全等同于close,然后跟附fcntl。它们之间的区别是:
    1dup2是一个原子操作,而close及fcntl则包括两个函数调用。有可能在close和fcntl之

    间插入执行信号捕获函数,它可能修改文件描述符(我们将在第十章说明信号。)
    2在dup2和fcntl之间有某些不同的errno。
    dup2系统调用起源于Version 7,然后传播至所有BSD版本。而复制文件描述符的fcntl方法

    则首先由系统Ⅲ使用,系统Ⅴ则继续采用之。SVR32取用了dup2函数,42BSD则取用了fc

    ntl函数及F[CD#*2]DUPFD功能。POSIX1要求dup2及fcntl的F[CD#*2]DUPFD功能两者。
    313〓fcntl函数
    fcntl函数可以改变已经打开的文件的性质。
    #include
    #include
    #include
    int fcntl(int filedes,int cmd,/* int arg */);
    返回:若成功,依赖于cmd(见下),出错为-1
    在本节的各实例中,第3个参数总是一个整数,与上面所示函数原型中的注释部分相对应。

    但是我们在123节中说明记录锁时,第3个参数则是指向一个结构的指针。
    fcntl函数有五种功能:
    ·复制一个现存的描述符(cmd=F〖CD#2〗DUPFD),
    ·获得/设置文件描述符标记(cmd=F[CD#*2]GETFD或F[CD#*2]SETFD),
    ·获得/设置文件状态标志(cmd=F[CD#*2]GETFL或F[CD#*2]SETFL),
    ·获得/设置异步I/O属主权(cmd=F[CD#*2]GETOWN或F[CD#*2]SETOWN),
    ·获得/设置记录锁(cmd=F[CD#*2]GETLK,F[CD#*2]SETLK或F[CD#*2]SETLKW)。
    我们先说明这十种命令值中的前七种(在123节中说明后三种,它们都与记录锁有关)我们

    将涉及与进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态

    标志,所以请参阅图32。
    F[CD#*2]DUPFD〓复制文件描述符filedes,新文件描述符作为函数值返回。它是尚未打开的

    各描述符中大于或等于第三个参数值(取为整型值)中各值的最小值。新描述符与filedes共

    享同一文件表项(参见图34)但是,新描述符有它自己的一套文件描述符标志,其FD[CD#*2

    ]CLOEXEC文件描述符标志则被清除(这表示该描述符在exec时仍保持开放,我们将在第八章

    对此进行讨论。)
    F[CD#*2]GETFD〓对应于filedes的文件描述符标志作为函数值返回。当前只定义了一个文件

    描述符标志FD[CD#*2]CLOEXEC标志。
    F[CD#*2]SETFD〓对于filedes设置文件描述符标志。新标志值是按第3个参数(取为整型值)

    设置的。
    应当了解很多现存的涉及文件描述符标志的程序并不使用常数FD[CD#*2]CLOEXEC。代替之,

    程序或将此标志设置为0(系统默认,在exec时不关闭),或1(在exec时关闭)。

    F[CD#*2]GETFL〓对应于filedes的文件状态标志作为函数值返回。在我们说明open函数时,

    已说明了文件状态标志。它们列于图35中
    图35〓对于fcntl的文件状态标志
    不幸的是三个存取方式标志(O[CD#*2]RDONLY,O[CD#*2]WRONLY,以及O[CD#*2]RDWR)并不各占

    1位。(正如前述,这三种标志的值各是0,1和2由于历史原因。这三种值是互斥的〖CD2〗一

    个文件只能有这3种值之1。)因此首先必须用屏蔽字O[CD#*2]ACCMODE取得存取方式位,然后

    将结果与这三种值相比较。
    F[CD#*2]SETFL〓将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志

    是:O[CD#*2]APPEND,O[CD#*2]NONBLOCK,O[CD#*2]SYNC和O[CD#*2]ASYNC。
    F[CD#*2]GETOWN〓取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。在1262节中将

    说明这两种43+BSD异步I/O信号。
    F[CD#*2]SETOWN〓设置接收SIGIO和SIGURG信号的进程ID或进程组ID。正的arg指定一个进程

    ID。负的arg表示等于arg绝对值的一个进程组ID。
    fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其它值。

    下列三种命令有特定返回值:F[CD#*2]DUPFD,F[CD#*2]GETFD以及F[CD#*2]GETOWN。第一个

    返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或一个负的进程

    组ID。
    实例
    程序34取指定一个文件描述符的命令行参数,并对于该描述符打印其文件标志说明。

    
    程序34〓对于指定的描述符打印文件标志
    注意,我们使用了功能测试宏[CD#*2]POSIX[CD#*2]SOURCE,并且条件编译了POSIX1中没

    有定义的文件存取标志。下面显示了从Kornshell调用该程序时的几种情况:
    $ aout 0 read only
    $ aout 1>tempfoo
    $ cat tempfoo
    write only
    $ aout 2 2>>tempfoo
    write only,append
    $ aout 5 5<>tempfoo
    read write
    Kornshell子句5<>tempfoo表示在文件描述符5上打开文件tempfoo供读、写。
    实例
    当修改文件描述符标志或文件状态标志时,必须谨慎,先要取得现在的标志值,然后按照希

    望修改它,最后设置新标志值。不能只是执行F[CD#*2]SETFD或F[CD#*2]SETFL命令,这样会

    关闭以前设置的标志位。
    程序35是一个对于一个文件描述符设置一个或多个文件状态标志的函数。
    程序35〓对一个文件描述符打开一个或多个文件状态指标
    如果我们将中间的一条语句改为
    这就构成了另一个函数,我们称其为clr[CD#*2]fl,并将在某个后面的例子中用到它。此语

    句使当前文件状态标志值val与flags的反码逻辑与。
    如果在程序33的开始处,加上下面1行以调用set[CD#*2]fl,则打开了同步写标志。这就

    造成每次write都要等待,直至数据已写到盘上再返回。在Unix中,通常write只是将数据排

    入队列,而实际的I/O操作则可能在以后的某个时刻进行。数据库系统很可能需要使用O[CD#

    *2]SYNC,这样,在系统崩溃情况下,它从write返回时就知道数据已确实写到了盘上。
    在程序运行时,设置O[CD#*2]SYNC标志会增加时钟时间。为了测试这一点,我们运行程序3

    3,它从盘上的一个文件中将15Mbyle复制到另一个文件中。然后,在此程序中设置O[CD

    #*2]SYNC标志,使其运行做上述同样工作,将两者得到的结果进行比较,这示于图36中。

    
    图36〓用同步写(O[CD#*2]SYNC)的时间结果
    图36中的3行都是在BUFSIZ为8192的情况下测量得到的。图31中的结果所测量的情况是

    读一个盘文件,然后写到/dev/null,所以没有盘输出。图36中的第2行对应于读一个盘文

    件,然后写到另一个盘文件中。这就是为什么图36中第1,2行有差别的原因。在写盘文件

    时,系统时间增加了,其原因是系统核需要从进程中复制数据,并将数据排入队列以便由盘

    驱动器将其写到盘上去。当写至盘文件时,时钟时间也增加了。当进行同步写时,系统时间

    稍稍增加,而时钟时间则增加为6倍。
    在这一例子中,我们看到了fcntl的必要性。我们的程序在一个描述符(标准输出)上进行操

    作,但是根本不知道由shell打开的相应文件的文件名。因为这是shell打开的,于是可能在

    打开时,按我们的要求设置O[CD#*2]SYNC标志。fcntl则允许当只知道打开文件的描述符时

    可以修改其性质。在说明非阻塞管道时(142节),我们也将了解到,由于我们对pipe所具

    有的标识只是其描述符,所以也需要使用fcntl的功能。
    314〓ioctl函数
    ioctl函数是I/O操作的杂物箱。不能用本章中其它函数表示的I/O操作通常都能用ioctl表示

    。终端I/O是ioctl的最大使用方面(在第十一章中,我们会了解到POSIX1已经用新的函数

    代替ioctl进行终端I/O操作。)
    #include /* SVR4 */
    #include /* 43+BSd */
    int ioctl(int filedes,int request,);
    返回:出错为-1,若成功则为其它源
    ioctl函数不是POSIX1的一部分,但是,SVR4和43+BSD用其进行很多杂项设备操作。
    我们所示的原型是SVR4和43+BSD所使用的,而较早的贝克莱系统则将第2个参数说明为uns

    igned long。因为第2个参数总是一个头文件中的定义名,所以这种细节并没有什么影响。


    对于ANSI C原型,它用省略号表示其余参数。但是,通常只有另外一个参数,它常常是指向

    一个变量或结构的指针。
    在此原型中,我们表示的只是ioctl函数本身所要求的头文件。通常,还要求另外的设备专

    用头文件。例如,在POSIX1所说明的基本操作之外,终端ioctl都需要头文件
    >。
    目前,ioctl的主要用途是什么呢?我们将43+BSD的ioctl操作分类示于图37中。


    图37〓43+BSD ioctl操作
    磁带操作使我们可以在磁带上写一个文件结束标志,反绕磁带,越过指定个数的文件或记录

    等等,用本章中的其它函数(read,write,lseek等)都难于表示这些操作,所以,用ioctl是

    对这些设备进行操作的最容易方法。
    在1112节中存取和设置终端窗口时,在124中说明流系统时,以及在197节中述及仿终

    端的高级功能时,我们都将使用ioctl。
    315〓/dev/fd
    比较新的系统都提供名为/dev/fd的目录,其目录项是名为0,1,2等的文件。打开文件/dev

    /fd/n等效于复制描述符n(假定描述符n是打开的)。
    /dev/fd这种特征是由Tom Duff开发的,它首先出现在research Unix System的第8版中,SV

    R4和43+BSD支持这种特征。它不是POSIX1的组成部分
    在函数中调用
    fd=open(“/dev/fd/0”,mode);
    大多数系统忽略所指定的mode,而另外一些则要求mode是所涉及的文件(在我们这里则是标

    准输入)原先打开时所使用的mode的子集。因为上面的打开等效于:
    fd=dup(0);
    描述符0和fd共享同一文件表项(图34)。例如,若描述符0被只读打开,那么我们也只对fd

    进行读操作。即使系统忽略打开方式,并且下列调用成功:
    fd=open(“/dev/fd/0”,O[CD#*2]RDWR);
    我们仍然不能对fd进行写操作。
    我们也可以用/dev/fd作为路径名参数调用creat,或调用open,并同时指定O[CD#*2]CREAT

    。这就允许调用creat的程序,如果路径名参数是/dev/fd/1等仍能工作。
    某些系统提供路径名/dev/stdin,/dev/stdout和/dev/stderr。这些等效于/dev/fd/0,/dev/

    fd/1和/dev/fd/2。
    /dev/fd文件主要由shell使用,这允许程序以对待其它路径名一样的方式使用路径名参数来

    处理标准输入和标准输出。例如,cat(1)程序将命令行中的一个单独的‘—’特别解释为一

    个输入文件名,该文件指的是标准输入。例如,
    filter file2 |cat file1-file3|lpr
    首先cat读file1,接着读其标准输入(也就是filter file2命令的输出),然后读file3,如

    若支持/dev/fd,则可以删除cat对一的特殊处理,于是我们就可键入下列命令行:
    filter file2 |cat file1 /dev/fd/0 file3 |lpr
    在命令行中用‘—’作为一个参数特指标准输入或标准输出已由很多程序采用。但是这会带

    来一些问题,例如若用‘—’指定第一个文件,那么它看来就象是开始了另一个命令行的选

    择项。/dev/fd则提高了文件名参数的一致性,也更加清晰。
    316〓摘要
    本章说明了传统的Unix I/O函数。因为
    发布人:netbull 来自:LinuxAid