UNIX系统中的大多数文件I/O只需用到5个函数:open、read、write、lseek和close。
本章所说明的函数称为不带缓冲的I/O。不带缓冲指的是每个read和write都调用内核中的一个系统调用(即在内核中执行),这些不带缓冲的的I/O函数不是IOS C的组成部分。
只要涉及在多个进程间共享资源,原子操作的概念就变得非常重要。
1、文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。
按照惯例,UNIX系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准出错输出相关联。默认情况下,文件描述符0,1,2与终端关联。在shell中,可以使用重定向来改变文件描述符0,1,2的关联文件,如:
1
2
3
4
5
6
7
8 |
#define BUFFERSIZE 100 int main() { char
buf[BUFFERSIZE]; ssize_t n = read(0,buf,BUFFERSIZE); write(1,buf,n); exit (0); } |
输出结果为:
1
2
3
4
5
6 |
$ cat file.in Hello world! $ cat file.out $ ./test < file.in > file.out $ cat file.out Hello world! |
shell打开file.in并返回一个文件描述符fd,用dup或dup2或fcntl函数复制文件描述符0,文件描述符fd和0共享file.in的文件表项。同理,打开file.out并返回一个文件描述符fd,用dup或dup2或fcntl函数复制文件描述符1,文件描述符fd和1共享file.out的文件表项。
2、open函数
1
2 |
int open( const
char *pathname, int
flags); int open( const
char *pathname, int
flags, mode_t mode); |
pathname是要打开或创建文件的名字。
oflag参数用来说明此函数的多个选项,用下列一个或多个常量进行“或”运算构成oflag参数:
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
在这三个常量中必须指定一个且只能指定一个,下列常量是可选的:
O_APPEND 每次写时都追加到文件的尾端
O_CREATE 若此文件不存在,则创建它。使用此选项,需要第三个参数mode,用其指定该新文件的访问权限位
O_EXCL 如果同时指定了O_CREATE,而文件已经存在,则出错。可以用来测试一个文件是否存在。如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作
O_TRUNC 如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0
O_NOCTTY 如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终端
O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置为非阻塞模式
O_DSYNC 使每次write等待物理I/O操作完成,但如果写操作并不影响读取刚写入的数据,则不等待文件属性被更新
O_RSYNC 使每一个以文件描述符为参数的read操作等待,直至任何对文件同一部分进行的未决写操作完成
O_SYNC 使每次write都等到物理I/O操作完成,包括由write操作引起的文件属性更新所需的I/O
O_DSYNC和O_SYNC的区别:
仅当文件属性需要更新以反映文件数据变化(如,更新文件大小以反映文件中包含了更多的数据)时,O_DSYNC才会影响文件属性。而设置O_SYNC,数据和属性总是同步更新。
3、create函数
1 |
int creat( const
char *pathname, mode_t mode); |
此函数等效于
1 |
open(pathname,O_WRONLY | O_CREAT | O_TRUNC,mode); |
creat的不足之处是它以只写方式打开所创建的文件。
4、close函数
1 |
int close( int
fd); |
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
当一个进程终止时,内湖会自动关闭它所有打开的文件。
5、lseek函数
系统默认情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
1 |
off_t lseek( int
fd, off_t offset, int
whence); |
若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节;
若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负;
若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可为正或负。
如果文件描述符引用的是一个管道、FIFO或网络套接字,则返回-1,并将errno设置为ESPIPE。
lseek仅将当前的文件偏移量记录在内核中,它并不引起内核I/O操作。然后,该偏移量用于下一个读或写操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的写将加长该文件,并在文件中构成一个空洞。位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 |
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) int main() { char
buf1[] = "abcdefghij" ; char
buf2[] = "ABCDEFGHIJ" ; int
fd; if ((fd = creat( "file.hole" ,FILE_MODE)) < 0) { return
-1; } if (write(fd,buf1,10) != 10) { return
-1; } if (lseek(fd,16384,SEEK_SET) == -1) { return
-1; } if (write(fd,buf2,10) != 10) { return
-1; } _exit(0); } |
6、read函数
1 |
ssize_t read( int
fd, void
*buf, size_t
count); |
读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。
7、文件共享
UNIX系统支持在不同进程间共享打开的文件。
内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个描述符关联的是:
(a)文件描述符标志(close_on_exec);
(b)指向一个文件表项的指针;
(2)内核为所有打开文件维持一张文件表,每个文件表项包含:
(a)文件状态标志(读、写、同步、非阻塞等);
(b)当前文件偏移量;
(c)指向该文件v节点表项的指针;
(3)每个打开文件(或设备)都有一个v节点结构。V节点包含了文件类型和对此文件进行各种操作的函数的指针。V节点还包括了该文件的i节点。这些信息是在打开文件时从磁盘上读入内存的。例如,i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件实际数据块在磁盘上所在位置的指针等。
下图显示了一个进程的三张表之间的关系。
下图显示了两个独立进程各自打开同一个文件。
我们假定第一个进程在文件描述符3上打开该文件,而另一个进程则在文件描述符4上打开该文件。打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个进程都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前偏移量。
下面对一些操作进一步说明:
(a)在完成每个write后,在文件表项中的当前文件偏移量即增加所写的字节数。如果这使当前文件偏移量超过了当前文件长度,则在i节点表项中的当前文件长度被设置为当前文件偏移量;
(b)如果用Q_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志的文件执行写操作,在文件表项中的当前文件偏移量首先被设置为i节点表项中的文件长度。这使得每次写的数据都添加到文件的当前尾端处。
(c)若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前偏移量被设置为i节点表项中的当前文件长度。
注意,文件描述符只用于一个进程的一个描述符,文件状态标志适用于指向该给定文件表项的任何进程中的所有描述符。
上面所述的一切对于多个进程读同一个文件都能正确工作。但是,当多个进程写同一个文件时,则可能产生意想不到的结果。为了说明如何避免这种情况,需要理解原子操作的概念。
8、dup和dup2函数
1
2 |
int dup( int
oldfd); int dup2( int
oldfd, int
newfd); |
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数指定新描述符的数值,如果filedes2已经打开,则先将其关闭。如若filedes等于filedes2,则dup2返回filedes2,而不关闭它。
这些函数返回的新文件描述符与参数filedes共享同一个文件表项,如图:
新描述符的执行时关闭(close_on_exec)标志总是由dup函数清除。
复制一个描述符的另一种方法是使用fcntl函数,实际上,调用
1 |
dup(filedes); |
等效于
1 |
fcntl(filedes,F_DUPFD,0); |
而调用
1 |
dup2(filedes,filedes2); |
等效于
1
2 |
close(filedes2); fcntl(filedes,F_DUPFD,filedes2); |
在后一种操作中,dup2并不完全等同于fcntl,它们之间的区别是:
(1)dup2是一个原子操作;而close及fcntl则包括两个函数调用,有可能在close和fcntl之间插入执行信号捕获函数,它可能修改文件描述符;
(2)dup2和fcntl有某些不同的errno;
9、sync、fsync和fdatasync函数
传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列。而是等待其写满或者当内核需要重用缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达对首时,才进行实际的I/O操作。这种输出方式被称为延迟写。
当系统发生故障时,这种延迟可能造成文件更新内容的丢失。
1
2
3 |
int fsync( int
fd); int fdatasync( int
fd); void
sync( void ); |
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分,而除数据外,fsync还会同步更新文件的属性。
10、fcntl函数
fcntl函数可以改变已打开的文件的性质:
1 |
int fcntl( int
fd, int
cmd, ... /* arg */
); |
第三个参数可以是一个整数,但在使用记录锁时,它可以是指向一个结构的指针。
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);
F_DUPFD 复制文件描述符filedes。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第三个参数值中各值的最小值。新描述符与filedes共享同一个文件表项,但新描述符有它自己的文件描述符
F_GETFD 对应于filedes的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志F_CLOEXEC
F_SETFD 对于filedes设置文件描述符标志
F_GETFL 对应于filedes的文件状态标志作为函数值返回,在说明open函数时,已介绍了文件状态标志
F_SETFL 将文件状态标志设置为第三个参数的值
F_GETOWN 取当前接收SIGIO和SIGURG信号的的进程ID或进程组ID
F_SETOWN 设置接收SIGIO和SIGURG信号的进程ID或进程组ID
fcntl的返回值与命令有关,如果出错,所有命令都返回-1。
在修改文件描述符标志或文件状态标志时,先要取得现有的标志值,然后根据需要修改它。
11、/dev/fd
/dev/fd目录中,其目录项是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制文件描述符n(假定描述符n是打开的)。
在下列函数调用中:
1 |
fd = open( "/dev/fd/0" ,mode); |
大多数系统忽略它指定的mode,而另外一些则要求mode必须是所涉及的文件(在这里是标准输入)原先打开时所使用mode的子集。因为上面打开是等效于:
1 |
fd = dup(0); |
所以描述符0和fd共享同一个文件表项。例如,若描述符0先前被打开为只读,那么我们也只能对fd进行读操作。
1 |
fd = open( "/dev/fd/0" ,O_RDWR); |
即使调用成功,我们仍不能对fd进行写操作。
原文:http://www.cnblogs.com/sheshiji/p/3682172.html