UNIX环境高级编程学习之路(一)----文件I/O

对于UNIX环境编程,工作中经常会用到相关知识,作为学习UNIX环境编程的经典书籍--UNIX环境高级编程,是每个UNIX编程人员必看的经典书籍之一,为了将相关知识重新进行学习,以系统的整合所学知识,遂以博文形式作为总结。

一、概述
    Unix系统可用的文件I/O函数--打开文件,读文件,写文件等,大多数文件I/O只需用到5个函数:open、read、write、lseek、close;
以上的函数被称为不带缓冲的I/O(unbuffered I/O),不带缓冲指的是每个read和write都调用内核的一个系统调用。
文件描述符是一个非负整数,当打开一个现有文件或新建一个文件时,内核向进程返回一个文件描述符。读写文件时,使用open或create返回的文件描述符标识这个文件,在Unix系统中,fd(0)进程的标准输入,fd(1)进程的标准输出,fd(2)进程的标准出错输出,这是很多shell以及很多程序使用的惯例而与Unix内核无关。

二、open()函数
调用open函数可以打开或者创建一个文件
#include 
int open(const char* pathname,int oflag,..../*mode_t mode*/); 
返回值:成功的话返回fd,出错返回-1
对于open而言,仅当创建新文件时才使用第三个参数。
pathname是要打开或者创建文件的名字,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操作设置为非阻塞模式;

三、create函数
可以调用create函数创建一个新的文件
#include
int create(const char *pathname, mode_t mode);
返回值:如果成功返回为只写打开的fd,若出错则返回-1
此函数等效于:
open(pathname,O_WRONLY | O_CREATE | O_TRUNC,mode);
create的一个不足之处是他以只写方式打开所创建的文件。

四、close函数
可调用close函数关闭一个打开的文件
#include 
int close (int filedes);
当一个进程终止时,内核自动关闭它所有打开的文件。

五、lseek函数
每一个打开的文件都有一个与其相关联的“当前文件偏移量”,它通常是一个非负整数,通常,读写操作都是从文件开始处计算的字节数,并使偏移量增加所读写的字节数。
可以调用lseek显式的为一个打开的文件设置其偏移量。
#include 
off_t lseek(int filedes,off_t offset,int whence);
返回值:若返回则返回新的文件偏移量,若出错则返回-1;
whence的值通常为如下值:
SEEK_SET:偏移量设置为距文件开始处offset个字节
SEEK_CUR:偏移量设置为其当前值加offset,offset可为正或者负值
SEEK_END:偏移量设置为文件长度加offset,offset可为正或者负值
如果lseek成功执行,则返回新的文件偏移量,可以用如下方式确定打开的当前偏移量:
off_t currpos = lseek(fd,0,SEEK_CUR);
因为偏移量可以为负值,所以在比较lseek的返回值时应该谨慎,不要测试他是否小于0,而是测试他是否等于-1;
lseek仅仅将当前的文件偏移量记录在内核里,他不引起任何I/O操作。然后该偏移量用于下一个读或者写操作,文件的偏移量可以大于当前的文件长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件构成一个空洞,位于文件中但是没有写过的字节都被读为0;

六、read函数
调用read函数从打开文件中读数据
#include
ssize_t read(int filedes,void *buf,size_t nbytes);
如果read成功,则返回读到的字节数,如果已经到达结尾,则返回0。

七、write函数
调用write函数向打开的文件写数据。
#include
ssize_t write(int filedes,const char* buf, size_t nbytes);
返回值:若成功则返回已写的字节数,若出错则返回-1;
其返回值通常与参数 nbytes的值相同,否知表示出错。
write出错的一个常见原因是:磁盘已写满,或者超过了一个给定进程的文件长度限制。

八、文件共享
UNIX系统支持在不同进程间共享打开的文件。
内核使用三种数据结构表示打开的文件,他们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表项中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
        (a)文件描述符标志(close_on_exec)。详细: close_on_exec
  (b)指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
        (a)文件状态标志(读、写。添写、同步和非阻塞等)。
        (b)当前文件偏移量。
        (c)指向该文件V节点表项的指针。
(3)每个打开文件或者设备都有一个V节点结构。V节点包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,V节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘读入内存的,所以所有关于文件的信息都是快速可供使用的。例如,i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件实际数据块在磁盘上所在位置的指针等。
注意:Linux没有V节点,而是使用了通用i节点结构。显然两种实现有所不同,但是在概念上,V节点同i节点是一样的。两者丢指向文件系统特有的i节点结构。
下图显示了一个进程的三张表之间的关系。该进程有两个不同的打开文件:一个文件打开为标准输入(文件描述符为0),另一个打开为标准不=输出(文件描述符为1)。
UNIX环境高级编程学习之路(一)----文件I/O_第1张图片

如果两个独立进程各自打开了同一个文件,则有下图的安排。我们假定第一个进程在文件描述符3上打开文件,二另一个进程在文件描述符4上打开该文件。打开该文件的每一个进程都得到一个文件表项,但对一个给定为文件只有一个V节点表项。每个进程都有自己的文件表项的理由是:这种安排使每个进程都有他自己的对该文件的当前偏移量。
* 在完成每个write后,在文件表项中的当前文件偏移量即增加所写的字节数。如果这使当前文件偏移量超过了当前文件长度,则在i节点表项的当前长度被设置为当前文件偏移量(也就是该文件加长了)。
* 如果用O_APPEND标志打开了一个文件,则相应标志也该设置到文件表项的文件状态标志中。每次对这种具有添写标志的文件执行写操作时,在文件表项中的当前文件偏移量首先被设置为i节点表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。
* 若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。
* lseek 函数只修改文件表项中的当前文件偏移量,没有进行任何I/O操作。
UNIX环境高级编程学习之路(一)----文件I/O_第2张图片


九、原子操作
所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。
原子  意味着临界段被包含在 API 函数中。不需要额外的锁定,因为 API 函数已经包含了锁定。
(1)pread函数和pwrite函数
允许原子性的定位搜索(lseek)和执行I/O。
#include 
ssize_t pread(int filedes, void *buf, sizt_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,但是pread又与这种顺序调用有下列重要区别:
* 调用pread时,无法中断其定位和读操作。
* 不更新文件指针;
调用pwrite相当于调用lseek和write,也与他们有类似的区别。
一般而言,原子操作指的是有多步组成的操作,如果该操作原子的执行,则要么执行完所有的步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

十、dup和dup2函数
下面的额两个函数可以用来复制一个现存的文件描述符:
#include 
int dup(int filedes);
int dup2(int filedes,int filedes2);
返回值:若成功则返回新的文件描述符,若出错则返回-1;
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数指定新描述符的数值。如果filedes已经打开,则现将其关闭。如若filedes等于filedes,dup2返回filedes2,而不关闭它。
这些函数返回的新文件描述符与参数filedes共享一个文件表项(内核)。
因为两个描述符指向同一文件表项,所以他们共享同一文件状态标志(读、写、添加等)以及同一当前文件偏移量。
每个文件描述符都有他自己的一套文件描述符标志。
UNIX环境高级编程学习之路(一)----文件I/O_第3张图片
复制一个描述符的另一种方法是使用fcntl函数,调用dup(filedes),相当于调用fcntl(filedes,F_DUPFD,0);而调用 dup2(int filedes,int filedes2),等效于close(filedes2),fcntl(filedes,F_DUPFD,filedes2);
后一种情况下,dup2并不完全等同于close加上fcntl,他们之间的区别是:
(1)dup2是一个原子操作,而close加上 fcntl则包括两个函数调用。有可能在close哈fcntl之间插入执行信号捕获函数,他有可能修改文件描述符;
(2)dup2和fcntl有某些不同的errno。

十一、 sync、fsync和fdatasync函数
传统的UNIX实现有一种输出方式是延迟写(delayed write)
为了保证磁盘上的实际文件系统和与缓冲区高速缓冲区中的内容一致,UNIX系统提供了三个函数。
#include 
int fsync(int filedes);
int fdatasync(int filedes);
返回值:若成功则返回0,失败则返回-1;
void sync();
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,他并等待实际写磁盘操作结束。
通常称为update的系统守护进程会周期性(一般每隔30秒)调用sync函数。保证了定期冲洗内核的块缓冲区。
fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分。除数据外,fsync还会同步更新文件的属性。

十二、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_GETTLK、F_SETTLK或F_SETLKW);
F_DUPFD   复制文件描述符filedes。新文件描述符作为函数值返回。 他是尚未打开的各描述符中大于或者等于第三个参数值中各值的最小值。新描述符与filedes共享同一文件表项。但是,新描述符有他自己的一套文件描述符标志,其FD_CLOEXEC文件描述符被清除。
F_GETFD    对应于filedes的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。
F_SETFD   对于filedes设置文件描述符标志。新标志值按照第三个参数(取为整型值)设置。
解释:F_GETFD和F_SETFD,一般对于 FD_CLOEXEC而言,但是现有的涉及文件描述符标志的程序并不使用常量FD_CLOEXEC,而是将此标志设置为0(系统默认,在exec时不关闭)或1(在exec时关闭),常见的需要设置的是fcntl(fd, F_SETFD,1 ),设置为1。
F_GETFL    对应于filedes的文件状态标志作为函数值返回。在说明open函数时,已说明了文件状态标志。它们位于如下表中:
UNIX环境高级编程学习之路(一)----文件I/O_第4张图片
 F_SETFL    将文件状态设置为第三个参数的值,可以更改的几个标志是:O_APPEND,O_NONBLOCK,O_SYNC,O_DSYNC,O_RSYNC,O_FSYNC和O_ASYNC。
F_GETOWN    取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。
F_SETOWN    设置接收SIGIO和SIGURG信号的进程ID或进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。
fcntl的返回值与命令有关。如果出错,所有命令返回-1,如果成功则返回某个其他值。
下面四个命令有特定返回值:F_DUPFD、F_GETFD、F_GETFL以及F_GETOWN。第一个返回新的文件描述符,接下来的两个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
如下代码测试 对于指定的描述符打印文件标志。
#include "apue.h"
#include 
int
main(int argc, char *argv[])
{
	int		val;
	if (argc != 2)
		err_quit("usage: a.out ");
	if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
		err_sys("fcntl error for fd %d", atoi(argv[1]));
	switch (val & O_ACCMODE) {
	case O_RDONLY:
		printf("read only");
		break;
	case O_WRONLY:
		printf("write only");
		break;
	case O_RDWR:
		printf("read write");
		break;
	default:
		err_dump("unknown access mode");
	}
	if (val & O_APPEND)
		printf(", append");
	if (val & O_NONBLOCK)
		printf(", nonblocking");
#if defined(O_SYNC)
	if (val & O_SYNC)
		printf(", synchronous writes");
#endif
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC)
    if (val & O_FSYNC)
        printf(", synchronous writes");
#endif
	putchar('\n');
	exit(0);
}

在修改文件描述符标志或者文件状态状态时必须谨慎,先要取得现有的标志值,然后根据需要修改它,然后重新设置标志值。
如下代码测试:对一个文件描述符打开一个或多个文件状态标志。
#include 
void set_fl(int fd,int flags)
{
	int val;
	
	if ((val = fcntl(fd, F_GETFL, 0)) < 0)
		err_sys("fcntl F_GETFL error");
	val |= flags;
	if (fcntl(fd, F_SETFL, val) < 0)
		err_sys("fcntl F_SETFL error");
}

十三、ioctl函数
ioctl函数是I/O操作的杂物箱。不能用本章中其他函数表示的I/O操作都能用ioctl表示。终端是ioctl的最大使用方面。
#include 
#include 
#include 
int ioctl(int filedes, int request, ...);
返回值:若出错则返回-1,若成功则返回其他值
每个设备驱动程序都可以定义他自己专用的一组ioctl命令。系统则为不同种类的设备提供通用的ioctl命令。
下表总结了FreeBSD 所支持的通用的ioctl命令的一些类别。
UNIX环境高级编程学习之路(一)----文件I/O_第5张图片
磁带操作使我们可以在磁带上写一个文件结束标志、反绕磁带。越过指定个数的文件或记录等等,用本章的其他函数(read、write、lseek等)都难以表示这些操作,所以,用ioctl是对这些设备进行操作的最容易方法。

十四、/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共享同一文件表项。例如,若描述符0先前被打开为只读,那么我们也只能对fd进行读操作。即系统忽略打开模式,而且下列调用成功:
fd = open("/dev/fd/0", O_RDWR);
我们仍然不能对fd进行写操作。
我们也可以用/dev/fd做为路径名参数调用crate,这次调用open时,用O_CREATE作为第二个参数作用相同。例如,若程序调用create,并且路径名参数表是 /dev/fd/1等,那么改程序仍能工作。
某些系统提供路径名/dev/stdin、/dev/stdout和/dev/stderr。这些等效于/dev/fd/0、/dev/fd/1、/dev/fd/2。

-------------------------------------------------------------------------华丽的风格线---------------------------------------------------------------------------------

QQ群:西安C/C++开发者,诚邀您的加入
UNIX环境高级编程学习之路(一)----文件I/O_第6张图片

你可能感兴趣的:(Linux)