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

文件共享:

UNIX系统支持在不同进程间共享打开的文件。内核使用三种数据结构表示打开的文件,他们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响:

(1)每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符都占用一项,与每个文件描述符相关联的是:

  • 文件描述符标志
  • 指向一个文件表项的指针

(2)内核为所有打开文件维护一张文件表,每个文件表项包括:

  • 文件状态标志(读,写,添写,同步和非阻塞等);
  • 当前文件偏移量;
  • 指向该文件v节点表项的指针;

(3)每个打开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针;对于大多数文件,v节点还包括了该文件的i节点(i-node,索引节点)。这些信息是打开文件时从磁盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。例如,i节点内包含了文件的所有者,文件长度,文件所在设备,指向文件实际数据在磁盘上所在位置的指针等等。

上面的讨论是概念性的,与特定实现可能匹配,也可能不匹配(Linux没有使用v节点,而是使用了通用i节点结构,但是在概念上,v节点与i节点是一样的,两者都指向文件系统特有的i节点结构,创建v节点结构的目的是对在一个计算机系统上的多文件系统类型提供支持,sun称这种文件系统为虚拟文件系统,称与文件系统类型无关的i节点部分为v节点)。

如果两个独立进程各自打开了同一个文件,打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个文件都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前偏移量。

可能有多个文件描述符项指向同一个文件表项。例如dup函数,或者在fork后也会发生同样的情况,此时父、子进程对于每一个打开文件文件描述符共享同一个文件表项。

因此多个进程读同一文件都能正确工作,因为每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。但是多个进程写同一个文件时,则可能产生预期不到的结果。

原子操作:

添写至一个文件:

要在一个文件中进行添写,第一种方法是“定位到文件尾端,然后写”,它使用两个分开的函数调用(lseek,write)。这种方法会出问题,任何一个需要多个函数调用的操作都不可能是原子操作,因为在两个函数之间,内核有可能会临时挂起该进程。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。

UNIX系统提供了一种方法使这种操作成为原子操作,该方法是在打开文件时设置O_APPEND标志。这就使得内核每次对该文件进行写之前,都将进程的当前文件偏移量设置到该文件的尾端处,于是在每次写之前就不再需要调用lseek。

pread和pwrite:

SUS包含了XSI扩展,该扩展允许原子性地定位搜索(seek)与执行I/O,pread和pwrite就是这种扩展:

#include

ssize_t pread(int fieldes, void *buf, size_t nbytes, off_t offset);

返回值,读到的字节数,若已到文件结尾则返回0,若出错则返回-1;

ssize_t pwrite(int fieldes, const void *buf, size_t nbytes, off_t offset);

返回值,若成功则返回已写的字节数,若出错,则返回-1;

创建一个文件:

之前已经讲过open函数的O_CREAT和O_EXCL选项,当同时指定这两个选项,而该文件又已经存在时,open将失败。这两个选项使得检查文件是否存在以及创建该文件这两个操作是作为一个原子操作执行的。

综上所述,原子操作指的是由多步组成的操作,如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

dup和dup2函数:

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

#include

int dup(int fieldes);

int dup2(int fieldes, int fieldes2);

两函数的返回值:若成功则返回新的文件描述符,若出错则返回-1。

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值,用dup2则可以用fieldes2参数指定新描述符的数值。如果fieldes2已经打开,则先将其关闭。如果fieldes等于fieldes2,则dup2返回filedes2,而不关闭它。

这些函数返回的新文件描述符与参数filedes共享同一个文件表项。因为两个描述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、添写等)以及同一当前文件偏移量。 但是每个文件描述符都有它自己的一套文件描述符标志。

复制文件描述符的另一种方法是使用fcntl函数:

  • 调用dup(filedes) 等效于fcntl(filedes, F_DUPFD, 0);
  • 而调用dup2(filedes, filedes2)等效于close(filedes2); fcntl(filedes, F_DUPFD, filedes2);

但是后一种情况,dup2并不完全等同于close加上fcntl。它们之间的区别在于:
  • dup2是一个原子操作,而close及fcntl则包含两个函数调用;
  • dup2和fcntl有某些不同的errno;

sync、fsync、fdatasync函数:

传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者内核需要重用该缓冲区以存放其它磁盘块数据时,再将该缓冲排入到输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式被称为“延迟写”。

延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。

为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync,fsync和fdatasync三个函数。

#include

int fsync(int filedes);

int fdatasync(int filedes);

返回值:若成功则返回0,若出错则返回-1;

void sync(void);

sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。通常称为update的系统守护进程会周期性地调用sync函数,命令sync(1)也调用sync函数。fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fdatasync函数类似于fsync,但它只影响文件的数据部分,而fsync还会同步更新文件的属性。

fcntl函数:

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

#include

int fcntl(int filedes, int cmd, ... /* arg */);

返回值:若成功则依赖于cmd,若出错则返回-1。

fcntl函数有5种功能:

  • 复制一个现有的描述符(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);

F_DUPFD:在上面已经讲过了,复制文件描述符filedes,新文件描述符作为函数值返回,它是尚未打开的各描述符中大于或等于第三个参数值中各值的最小值。

F_GETFD:对应于filedes的文件描述符标志作为函数值返回,当前只定义了一个文件描述符标志FD_CLOEXEC;

F_SETFD:对于filedes设置文件描述符标志,新标志值按第三个参数设置。

F_GETFL:对应于filedes的文件状态标志作为函数值返回,在说明open函数时,已经说明了文件状态标志(注意,O_RDONLY、O_WRONLY、O_RDWR三个访问方式标志并不各占一位,因此首先要用屏蔽字O_ACCMODE取得访问模式位,然后将结果与这三种值中的任何一种作比较)。

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;

下列程序接受一个文件描述符作为参数,打印该文件描述符所指向的文件表项中的文件状态标志:

/*
 * Copyright (C) [email protected]
 */


#include 
#include 
#include 
#include 


int
main(int argc, char *argv[])
{
	int val;

	if (argc != 2) {
		printf("usage: ./fcntl fd\n");
		exit(1);
	}


	if ( (val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) {
		printf("fcntl error\n");
		exit(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:
			printf("unknown access mode");
			break;
	}

	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);
}

上述使用了功能测试宏_POSIX_C_SOURCE,并且条件编译了POSIX.1中没有定义的文件访问标志。下面是该程序在bash中执行结果其中5<>temp.foo表示在文件描述符5上打开文件temp.foo以供读写:

UNIX环境高级编程(6):文件I/O(2)_第1张图片

在修改文件描述符标志或文件状态标志时必须谨慎,先要取得现有的标志值,然后根据需要修改它,最后设置新标志值,不能只是执行F_SETFD或F_SETFL,这样会关闭以前设置的标志位:

下列程序显示了对一个文件描述符设置一个或多个文件状态标志的范例:

/*
 * Copyright (C) [email protected]
 */


#include 
#include 
#include 


void
set_fl(int fd, int flags)
{
	int val;
	
	if ( (val = fcntl(fd, F_GETFL, 0)) < 0) {
		printf("get file flag error\n");
		return ;
	}

	val |= flags;

	if (fcntl(fd, F_SETFL, val) < 0) {
		printf("set file flag error\n");
	}
}

如果将 var |= flags 改为 val &= ~flags则用来关闭某个标志位。

程序运行时,设置O_SYNC标志会增加时钟时间。因为每次write操作都要等待,直至数据已写到磁盘上再返回。所以当支持同步写时,系统时间和时钟时间应当会显著增加。

虽然可以在调用open函数时就设置文件状态标志,但是fcntl函数仍然非常有必要。fcntl函数允许在仅知道文件描述符的情况下修改其性质。例如,标准输出是由shell打开的,因此我们无法通过open函数来设置其文件状态标志,但是通过fcntl函数可以做到。

ioctl函数:

ioctl函数是I/O操作的杂物箱,不能用其它函数表示的I/O操作通常都能用ioctl表示,终端I/O是iotcl的最大使用方面。

#include /* System V */

#include /* BSD and Linux */

#include /* XSI STREAMS */

int ioctl(int filedes,int request,...);

iotcl函数只是SUS标准的一个扩展,以便处理STREAMS设备,但是UNIX系统实现用它进行很多杂项设备操作,有些实现甚至将它扩展到用于普通文件。

在此函数原型中,我们表示的只是iotcl函数本身所要求的头文件。通常,还要求另外的设备专用头文件。每个设备驱动程序都可以定义它自己专用的一组ioctl命令,系统则为不同种类的设备提供通用的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共享同一文件表项。

某些系统提供路径名/dev/stdin、/dev/stdout/和/dev/stderr,这些等效于/dev/fd/0、/dev/fd/1和/dev/fd/2。

/dev/fd文件主要由shell使用,它允许那些使用路径名作为调用参数的程序,能用处理其它路径名的相同方式处理标准输入和输出。虽然很多程序都支持在命令行中使用"-"作为一个参数,特指标准输入或输出。但是/dev/fd则提高了文件名参数的一致性,也更加清晰。


你可能感兴趣的:(UNIX编程)