linux文件IO之整理笔记(一)

时间:2022-02-03 08:58:15

以下是个人觉得比较详细而且易于理解的几篇文章,所以把它一起贴出来做为笔记(一)、(二)、(三),也希望对过路者提供一点方便。

 

第3章文件I / O
3.1 引言
        本章开始讨论U N I X系统,先说明可用的文件I / O函数——打开文件、读文件、写文件等等。大多数U N I X文件I / O只需用到5个函数:o p e n、r e a d、w r i t e、lseek 以及c l o s e。然后说明不同缓存器长度对r e a d和w r i t e函数的影响。本章所说明的函数经常被称之为不带缓存的I / O(u n b u ffered I/O,与将在第5章中说明的标准I / O函数相对照)。术语——不带缓存指的是每个r e a d和w r i t e都调用内核中的一个系统调用。
这些不带缓存的I / O函数不是ANSI C的组成部分,但是是P O S I X . 1和X P G 3的组成部分。
        只要涉及在多个进程间共享资源,原子操作的概念就变成非常重要。我们将通过文件I / O和传送给o p e n函数的参数来讨论此概念。并进一步讨论在多个进程间如何共享文件,并涉及内核的有关数据结构。在讨论了这些特征后,将说明d u p、f c n t l和i o c t l函数。


3.2 文件描述符
        对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,用o p e n或c r e a t返回的文件描述符标识该文件,将其作为参数传送给r e a d或w r i t e。按照惯例,UNIX shell使文件描述符0与进程的标准输入相结合,文件描述符1与标准输出相结合,文件描述符2与标准出错输出相结合。这是UNIX shell以及很多应用程序使用的惯例,而与内核无关。尽管如此,如果不遵照这种惯例,那么很多U N I X应用程序就不能工作。
        在P O S I X . 1应用程序中,幻数0、1、2应被代换成符号常数S T D I N _ F I L E N O、S T D O U T _F I L E N O和S T D E R R _ F I L E N O。这些常数都定义在头文件< u n i s t d . h >中。

        文件描述符的范围是0 ~ O P E N _ M A X (见表2 - 7 )。早期的U N I X版本采用的上限值是1 9 (允许每个进程打开2 0个文件),现在很多系统则将其增加至6 3。
        S V R 4和4 . 3 + B S D对文件描述符的变化范围没有作规定,它只受到系统配置的存储器的总量、整型字的字长以及系统管理员所配置的软性或硬性限制的约束。


3.3 open函数
调用o p e n函数可以打开或创建一个文件。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const charp a *t h n a m e, int o f l a g,.../*, mode_tm o d e * / ) ;
返回:若成功为文件描述符,若出错为- 1
        我们将第三个参数写为. . .,这是ANSI C说明余下参数的数目和类型可以变化的方法。对于o p e n函数而言,仅当创建新文件时才使用第三个参数。(我们将在稍后对此进行说明。)在函数原型中此参数放置在注释中。
        p a t h n a m e是要打开或创建的文件的名字。o f l a g参数可用来说明此函数的多个选择项。用下列一个或多个常数进行或运算构成o f l a g参数(这些常数定义在< f c n t l . h >头文件中):
• O_RDONLY 只读打开。
• O_WRONLY 只写打开。
• O_RDWR 读、写打开。
很多实现将O _ R D O N LY定义为0,O _ W R O N LY定义为1,O _ R D W R定义为2,以与早期的系统兼容。在这三个常数中应当只指定一个。下列常数则是可选择的:
• O_APPEND 每次写时都加到文件的尾端。3 . 11节将详细说明此选择项。
• O_CREAT 若此文件不存在则创建它。使用此选择项时,需同时说明第三个参数m o d e,用其说明该新文件的存取许可权位。( 4 . 5节将说明文件的许可权位,那时就能了解如何说明m o d e,以及如何用进程的u m a s k值修改它。)
• O_EXCL 如果同时指定了O _ C R E AT,而文件已经存在,则出错。这可测试一个文件是否存在,如果不存在则创建此文件成为一个原子操作。3 . 11节将较详细地说明原子操作。
• O_TRUNC 如果此文件存在,而且为只读或只写成功打开,则将其长度截短为0。
• O_NOCTTY 如果p a t h n a m e指的是终端设备,则不将此设备分配作为此进程的控制终端。9 . 6节将说明控制终端。
• O_NONBLOCK 如果p a t h n a m e指的是一个F I F O、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I / O操作设置非阻塞方式。1 2 . 2节将说明此工作方式。
        较早的系统V版本引入了O _ N D E L AY(不延迟)标志,它与O _ N O N B L O C K(不阻塞)选择项类似,但在读操作的返回值中具有两义性。如果不能从管道、F I F O或设备读得数据,则不延迟选择项使r e a d返回0,这与表示已读到文件尾端的返回值0相冲突。S V R 4仍支持这种语义的不延迟选择项,但是新的应用程序应当使用不阻塞选择项以代替之。
• O_SYNC 使每次w r i t e都等到物理I / O操作完成。3 . 1 3节将使用此选择项。
• O _ S Y N C选择项不是P O S I X . 1的组成部分,但S V R 4支持此选择项。
        由o p e n返回的文件描述符一定是最小的未用描述符数字。这一点被很多应用程序用来在标准输入、标准输出或标准出错输出上打开一个新的文件。例如,一个应用程序可以先关闭标准输出(通常是文件描述符1 ),然后打开另一个文件,事先就能了解到该文件一定会在文件描述符1上打开。在3 . 1 2节说明d u p 2函数时,可以了解到有更好的方法来保证在一个给定的描述符上打开一个文件。

 

文件名和路径名截短
        如果N A M E _ M A X是1 4,而我们却试图在当前目录中创建一个其文件名包含1 5个字符的新文件,此时会发生什么呢? 按照传统,早期的系统V版本,允许这种使用方法,但是总是将文件名截短为1 4个字符,而B S D类的系统则返回出错E N A M E TO O L O N G。这一问题不仅仅与创建新文件有关。如果N A M E _ M A X是1 4,而存在一个其文件名恰恰就是1 4个字符的文件,那么以p a t h n a m e作为其参数的任一函数( o p e n , s t a t等)都会遇到这一问题。
        在P O S I X . 1中,常数_ P O S I X _ N O _ T R U N C决定了是否要截短过长的文件名或路径名,或者返回一个出错。第1 2章将说明此值可以针对各个不同的文件系统进行变更。
FIPS 151-1要求返回出错。
        S V R 4对传统的系统V文件系统( S 5 )并不保证返回出错(见表2 - 6 ),但是对B S D风格的文件系统( U F S ),S V R 4保证返回出错,4 . 3 + B S D总是返回出错。
        若_ P O S I X _ N O _ T R U N C有效,则在整个路径名超过PAT H _ M A X,或路径名中的任一文件名超过N A M E _ M A X时,返回出错E N A M E TO O L O N G。


3.4 creat函数
也可用c r e a t函数创建一个新文件。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const charp a t* h n a m e, mode_tm o d e) ;
返回:若成功为只写打开的文件描述符,若出错为- 1
注意,此函数等效于:
o p e n (p a t h n a m e, O_WRONLY|O _ C R E A T|O_TRUNC, m o d e) ;
        在早期的U N I X版本中, o p e n的第二个参数只能是0、1或2。没有办法打开一个尚未存在的文件,因此需要另一个系统调用c r e a t以创建新文件。现在, o p e n函数提供了选择项O _ C R E AT和O _ T R U N C,于是也就不再需要c r e a t函数了。在4 . 5节中,我们将详细说明文件存取许可权,并说明如何指定m o d e。
        c r e a t的一个不足之处是它以只写方式打开所创建的文件。在提供o p e n的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用c r e a t,c l o s e,然后再调用o p e n。现在则可用下列方式调用o p e n:
o p e n (p a t h n a m e, O_RDWR|O _ C R E A T|O_TRUNC, m o d e) ;


3.5 close函数
可用c l o s e函数关闭一个打开文件:
#include <unistd.h>
int close (intf i l e d e s);
返回:若成功为0,若出错为- 1
关闭一个文件时也释放该进程加在该文件上的所有记录锁。1 2 . 3节将讨论这一点。当一个进程终止时,它所有的打开文件都由内核自动关闭。很多程序都使用这一功能而不显式地用c l o s e关闭打开的文件。实例见程序1 - 2。


3.6 lseek函数

        每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。(本节稍后将对“非负”这一修饰词的某些例外进行说明。)通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。按系统默认,当打开一个文件时,除非指定O _ A P P E N D选择项,否则该位移量被设置为0。
可以调用l s e e k显式地定位一个打开文件。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(intf i l e d e s, off_t o f f s e t, int w h e n c e) ;
返回:若成功为新的文件位移,若出错为- 1
对参数offset 的解释与参数w h e n c e的值有关。
• 若w h e n c e是S E E K _ S E T,则将该文件的位移量设置为距文件开始处offset 个字节。
• 若w h e n c e是S E E K _ C U R,则将该文件的位移量设置为其当前值加offset, offset可为正或负。
• 若w h e n c e是S E E K _ E N D,则将该文件的位移量设置为文件长度加offset, offset可为正或负。
• 若l s e e k成功执行,则返回新的文件位移量,为此可以用下列方式确定一个打开文件的当前位移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
         这种方法也可用来确定所涉及的文件是否可以设置位移量。如果文件描述符引用的是一个管道或F I F O,则l s e e k返回-1,并将e r r n o设置为E P I P E。三个符号常数S E E K _ S E T,S E E K _ C U R和S E E K _ E N D是由系统V引进的。在系统V之前, w h e n c e被指定为0 (绝对位移量),1 ( 相对于当前位置的位移量)或2 (相对文件尾端的位移量)。很多软件仍直接使用这些数字进行编码。在l s e e k中的字符l表示长整型。在引入o ff _ t数据类型之前, o f f s e t参数和返回值是长整型的。l s e e k是由V 7引进的,当时C语言中增加了长整型。(在V 6中,用函数s e e k和t e l l提供类似功能。)实例
程序3 - 1用于测试其标准输入能否被设置位移量。
程序3-1 测试标准输入能否被设置位移量


如果用交互方式调用此程序,则可得:
$ a.out < /etc/motd
seek OK
$ cat < /etc/mot|da . o u t
cannot seek
$ a.out < /var/spool/cron/FIFO
cannot seek
        通常,文件的当前位移量应当是一个非负整数,但是,某些设备也可能允许负的位移量。
但对于普通文件,则其位移量必须是非负值。因为位移量可能是负值,所以在比较l s e e k的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1。在8 0 3 8 6上运行的S V R 4支持/ d e v / k m e m设备,它可以具有负的位移量。因为位移量( o ff _ t)是带符号数据类型(见表2 - 8),所以文件最大长度减少一半。例如,若o ff_t 是3 2位整型,则文件最大长度是231 字节。l s e e k仅将当前的文件位移量记录在内核内,它并不引起任何I / O操作。然后,该位移量用于下一个读或写操作。
        文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空调,这一点是允许的。位于文件中但没有写过的字节都被读为0。实例
程序3 - 2用于创建一个具有空洞的文件。
程序3-2 创建一个具有空洞的文件


运行该程序得到:
$ a . o u t
$ ls -1 file.hole 检查其大小
-rw-r--r-- 1 stevens 50 Jul 31 05:50 file.hole
$ od -c file.hole 观察实际内容
0000000 a b c d e f g h i j /0 /0 /0 /0 /0 /0
0000020 /0 /0 /0 /0 /0 /0 /0 /0 /0 /0 /0 /0 /0 /0 /0 /0
0000040 /0 /0 /0 /0 /0 /0 /0 /0 A B C D E F G H
0000060 I J
0 0 0 0 0 6 2
使用o d ( 1 )命令观察该文件的实际内容。命令行中的- c标志表示以字符方式打印文件内容。从中可以看到,文件中间的3 0个未写字节都被读成为0。每一行开始的一个七位数是以八进制形式表示的字节位移量。本例调用了将在3 . 8节中说明的w r i t e函数。4 . 1 2节将对具有空洞的文件进行更多说明。


3.7 read函数
用r e a d函数从打开文件中读数据。
#include <unistd.h>
ssize_t read(intf i l e d e s, void *b u f f, size_tn b y t e s) ;
返回:读到的字节数,若已到文件尾为0,若出错为- 1
如r e a d成功,则返回读到的字节数。如已到达文件的尾端,则返回0。有多种情况可使实际读到的字节数少于要求读字节数:
• 读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前还有3 0个字节,而要求读1 0 0个字节,则r e a d返回3 0,下一次再调用r e a d时,它将返回0 (文件尾端)。
• 当从终端设备读时,通常一次最多读一行(第11章将介绍如何改变这一点)。
• 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
• 某些面向记录的设备,例如磁带,一次最多返回一个记录。
读操作从文件的当前位移量处开始,在成功返回之前,该位移量增加实际读得的字节数。P O S I X . 1在几个方面对此函数的原型作了更改。其经典定义是:

int read(intf i l e d e s, char *b u f f, unsignedn b y t e s) ;
        首先,为了与ANSI C一致,其第二个参数由char *改为void *。在ANSI C中,类型void *用于表示类属指针。其次,其返回值必须是一个带符号整数( s s i z e _ t),以返回正字节数、0(表示文件尾端)或- 1(出错)。最后,第三个参数在历史上是一个不带符号整数,以允许一个1 6位的实现可以一次读或写至6 5 5 3 4个字节。在1990 POSIX.1标准中,引进了新的基本系统数据类型ssize_t 以提供带符号的返回值, s i z e _ t则被用于第三个参数(见表2 - 7中的S S I Z E _ M A X常数)。
3.8 write函数
用w r i t e函数向打开文件写数据。
#include <unistd.h>
ssize_t write(inft i l e d e s, const void b* u f f, size_tn b y t e s) ;
返回:若成功为已写的字节数,若出错为- 1
其返回值通常与参数n b y t e s的值不同,否则表示出错。w r i t e出错的一个常见原因是:磁盘已写满,或者超过了对一个给定进程的文件长度限制(见7 . 11节及习题1 0 . 11 )。
对于普通文件,写操作从文件的当前位移量处开始。如果在打开该文件时,指定了O _ A P P E N D选择项,则在每次写操作之前,将文件位移量设置在文件的当前结尾处。在一次成功写之后,该文件位移量增加实际写的字节数。