编程接口指南

第 5 章 输入/输出接口

本章介绍在未提供虚拟内存服务的系统上所提供的文件输入/输出操作。此外,还介绍虚拟内存功能所提供的改进的输入/输出方法。本章在使用文件和记录锁定中介绍较早的锁定文件和记录的方法。

文件和 I/O 接口

组织为一系列数据的文件称为常规文件。常规文件可以包含 ASCII 文本、以其他二进制数据编码的文本、可执行代码,或者文本、数据以及代码的任意组合。

常规文件由以下部分构成:

Solaris 操作系统提供以下基本的文件输入/输出接口形式:

基本文件 I/O

以下接口针对文件和字符 I/O 设备执行基本操作。

表 5–1 基本文件 I/O 接口

接口名称 

目的 

open(2)

打开文件进行读取或写入 

close(2)

关闭文件描述符 

read(2)

从文件中读取 

write(2)

向文件中写入 

creat(2)

创建新文件或重写现有文件 

unlink(2)

删除目录项 

lseek(2)

移动读/写文件指针 

以下代码样例说明了基本文件 I/O 接口用法。read(2)write(2) 都从文件当前偏移位置开始传送不超过指定数量的字节。返回实际传送的字节数。read(2) 返回零时指示已到达文件结尾。


示例 5–1 基本文件 I/O 接口

#include			<fcntl.h>

#define			MAXSIZE			256



main()

{

    int     fd;

    ssize_t n;

    char	    array[MAXSIZE];



    fd = open ("/etc/motd", O_RDONLY);

    if (fd == -1) {

        perror ("open");

        exit (1);

    }

    while ((n = read (fd, array, MAXSIZE)) > 0)

        if (write (1, array, n) != n)

            perror ("write");

    if (n == -1)

        perror ("read");

    close (fd);

}

完成读取或写入文件后,应始终调用 close(2)。不要针对不是从调用 open(2) 返回的文件描述符调用 close(2)

使用 read(2)write(2),或者调用 lseek(2) 可以改变已打开文件的文件指针偏移。以下示例说明了 lseek 的用法。

off_t		start, n;

 struct		record		rec;



 /* record current offset in start */

 start = lseek (fd, 0L, SEEK_CUR);



 /* go back to start */

 n = lseek (fd, -start, SEEK_SET);

 read (fd, &rec, sizeof (rec));



 /* rewrite previous record */

 n = lseek (fd, -sizeof (rec), SEEK_CUR);

 write (fd, (char *&rec, sizeof (rec));

高级文件 I/O

下表列出了高级文件 I/O 接口所执行的任务。

表 5–2 高级文件 I/O 接口

接口名称 

目的 

link(2)

链接到文件 

access(2)

确定文件的可访问性 

mknod(2)

生成特殊文件或普通文件 

chmod(2)

更改文件的模式 

chown(2), lchown(2), fchown(2)

更改文件的属主和组 

utime(2)

设置文件的访问时间和修改时间 

stat(2), lstat(2), fstat(2)

获取文件状态 

fcntl(2)

执行文件控制功能 

ioctl(2)

控制设备 

fpathconf(2)

获取可配置的路径名变量 

opendir(3C), readdir(3C), closedir(3C)

执行目录操作 

mkdir(2)

生成目录 

readlink(2)

读取符号链接的值 

rename(2)

更改文件的名称 

rmdir(2)

删除目录 

symlink(2)

生成到文件的符号链接 

文件系统控制

下表中列出的文件系统控制接口可用于控制文件系统的各个方面。

表 5–3 文件系统控制接口

接口名称 

目的 

ustat(2)

获取文件系统统计信息 

sync(2)

更新超块 

mount(2)

挂载文件系统 

statvfs(2), fstatvfs(2)

获取文件系统信息 

sysfs(2)

获取文件系统类型信息 

使用文件和记录锁定

不需要使用传统的文件 I/O 来锁定文件元素。应针对映射文件使用《多线程编程指南》中介绍的较轻重量级的同步机制。

锁定文件可防止当多个用户同时尝试更新一个文件时可能会发生的错误。可以锁定文件的一部分。

文件锁定可阻止对整个文件的访问。记录锁定可阻止对文件的指定段的访问。在 SunOS 中,所有文件都是一系列的数据字节:记录是使用此文件的程序的概念。

选择锁定类型

强制性锁定将暂停进程,直到所请求的文件段被释放。建议性锁定返回一个表明是否成功获得锁定的结果。进程可以忽略建议性锁定的结果。对于同一文件,不能同时使用强制性和建议性文件锁定。打开文件时所用的模式决定了对文件的锁定是强制性锁定还是建议性锁定。

在这两种基本锁定调用中,fcntl(2)lockf(3C) 更容易移植、功能更强大,但更不易用。fcntl(2) 在 POSIX 1003.1 标准中指定。提供 lockf(3C) 是为了与较早版本的应用程序兼容。

选择建议性或强制性锁定

对于强制性锁定,文件必须是设置了 set-group-ID 位且未设置组执行权限位的常规文件。如果不符合其中任一条件,则所有记录锁定均为建议性锁定。

按照如下方式设置强制性锁定。

#include <sys/types.h>

#include <sys/stat.h>



 int mode;

 struct stat buf;

 	...

 	if (stat(filename, &buf) < 0) {

 		perror("program");

 		exit (2);

 	}

 	/* get currently set mode */

 	mode = buf.st_mode;

 	/* remove group execute permission from mode */

 	mode &= ~(S_IEXEC>>3);

 		/* set 'set group id bit' in mode */

 	mode |= S_ISGID;

 	if (chmod(filename, mode) < 0) {

 		perror("program");

 		exit(2);

 	}

 	... 

当系统在执行文件时,操作系统会忽略记录锁定。所有带有记录锁定的文件都不应设置执行权限。

chmod(1) 命令还可用于将文件设置为允许强制性锁定。


$ chmod +l file

此命令在文件模式中设置 O20n0 权限位,它指示对文件进行强制性锁定。如果 n 是偶数,则此位将会解释为启用强制性锁定。如果 n 是奇数,则此位将会解释为“执行时设置组 ID”。

当使用 -l 选项请求长列表格式时,ls(1) 命令显示以下设置:


$ ls -l file

此命令显示以下信息:


-rw---l--- 1 user group size mod_time file

权限中的字母 "l" 指示 set-group-ID 位已设置。由于设置了 set-group-ID 位,因此启用强制性锁定。此外,还启用了 set group ID 的一般语义。

关于强制性锁定的注意事项

请牢记锁定的以下方面:

支持的文件系统

下表中列出的文件系统同时支持建议性锁定和强制性锁定。

表 5–4 支持的文件系统

文件系统 

说明 

ufs

缺省的基于磁盘的文件系统 

fifofs

为进程提供对数据的公用访问的命名管道文件的伪文件系统 

namefs

主要由 STREAMS 用来在文件顶部动态挂载文件描述符的伪文件系统 

specfs

提供对特殊字符设备和块设备的访问的伪文件系统 

NFS 只支持建议性文件锁定。procfd 文件系统不支持文件锁定。

打开文件进行锁定

只能使用有效的打开描述符来请求对文件进行锁定。对于读取锁定,文件必须至少是使用读取权限打开的。对于写入锁定,文件也必须是使用写入权限打开的。在以下示例中,将打开文件以进行读取和写入访问。

...

 	filename = argv[1];

 	fd = open (filename, O_RDWR);

 	if (fd < 0) {

 		perror(filename);

 		exit(2);

 	}

 	...

设置文件锁定

要锁定整个文件,请将偏移量设置为零并将大小设置为零。

可以使用多种方法在文件上设置锁定。方法的选择取决于锁定如何与程序的其余部分进行交互、性能以及可移植性。本示例使用 POSIX 标准兼容的 fcntl(2) 接口。此接口尝试锁定文件,直到发生以下情况之一:

设置和删除记录锁定

锁定记录时,请不要将锁定段的起始点和长度设置为零。否则,此锁定过程与文件锁定相同。

使用记录锁定的原因是存在数据争用。因此,当无法获取所有所需锁定时,应收到失败响应:

本示例说明使用 fcntl(2) 锁定的记录。

{

 	struct flock lck;

   	...

 	lck.l_type = F_WRLCK;	/* setting a write lock */

 	lck.l_whence = 0;	/* offset l_start from beginning of file */

 	lck.l_start = here;

 	lck.l_len = sizeof(struct record);



 	/* lock "this" with write lock */

 	lck.l_start = this;

 	if (fcntl(fd, F_SETLKW, &lck) < 0) {

 		/* "this" lock failed. */

 		return (-1);

 ...

}

下一示例说明 lockf(3C) 接口。

#include <unistd.h>



{

 ...

 	/* lock "this" */

 	(void) lseek(fd, this, SEEK_SET);

 	if (lockf(fd, F_LOCK, sizeof(struct record)) < 0) {

 		/* Lock on "this" failed. Clear lock on "here". */

 		(void) lseek(fd, here, 0);

 		(void) lockf(fd, F_ULOCK, sizeof(struct record));

 		return (-1);

}

 

应使用与设置锁定相同的方法来删除锁定。只是锁定类型有所不同 (F_ULOCK)。解除锁定不受其他进程的阻止,并且只影响调用进程所设置的锁定。解除锁定只影响在先前锁定调用中指定的文件段。

获取锁定信息

可以确定哪个进程在持有锁定。可以按照前面示例所示设置锁定,并在 fcntl(2) 中使用 F_GETLK

下一示例查找并列显文件中所有锁定段上的标识数据。


示例 5–2 列显文件的锁定段

struct flock lck;



 	lck.l_whence = 0;

 	lck.l_start = 0L;

 	lck.l_len = 0L;

 	do {

 		lck.l_type = F_WRLCK;

 		(void) fcntl(fd, F_GETLK, &lck);

 		if (lck.l_type != F_UNLCK) {

 			(void) printf("%d %d %c %8ld %8ld\n", lck.l_sysid, lck.l_pid,

            (lck.l_type == F_WRLCK) ? 'W' : 'R', lck.l_start, lck.l_len);

 			/* If this lock goes to the end of the address space, no

 			 * need to look further, so break out. */

 			if (lck.l_len == 0) {

 			/* else, look for new lock after the one just found. */

 					lck.l_start += lck.l_len;

 			}

 		}

 	} while (lck.l_type != F_UNLCK);

fcntl(2)F_GETLK 命令可以在等待服务器响应时处于休眠状态。如果客户机或服务器出现资源不足的情况,则此命令可能会失败,同时返回 ENOLCK

lockf(3C)F_TEST 命令一起使用来测试进程是否在持有锁定。此接口并不返回有关锁定的位置或拥有权的信息。


示例 5–3 使用 lockf 测试进程

(void) lseek(fd, 0, 0L);

 /* set the size of the test region to zero (0). to test until the

    end of the file address space. */

 if (lockf(fd, (off_t)0, SEEK_SET) < 0) {

 	switch (errno) {

 		case EACCES:

 		case EAGAIN:

 			(void) printf("file is locked by another process\n");

 			break;

 		case EBADF:

 			/* bad argument passed to lockf */

 			perror("lockf");

 			break;

 		default:

 			(void) printf("lockf: unexpected error <%d>\n", errno);

 			break;

 	}

 

进程派生和锁定

进程派生时,子进程会得到父进程打开的文件描述符的副本。子进程不继承锁定,因为锁定由特定进程拥有。父进程和子进程共享每个文件的公用文件指针。并且都可以尝试在同一文件中的相同位置设置锁定。使用 lockf(3C)fcntl(2) 都会出现此问题。如果持有记录锁定的程序进行派生,则子进程应该关闭此文件。关闭此文件之后,子进程应该重新打开此文件以设置新的独立文件指针。

死锁处理

UNIX 锁定功能可以检测和避免死锁。仅当系统准备将记录锁定接口置于休眠状态时才会发生死锁。执行搜索来确定两个进程是否处于死锁状态。如果检测到潜在的死锁,则锁定接口会失败,并设置 errno 来指示死锁。使用 F_SETLK 设置锁定的进程不会导致死锁,因为当不能被授予锁定时,这些进程并不会等待。

终端 I/O 函数

终端 I/O 接口处理用于控制异步通信端口的通用终端接口,如下表中所示。有关更多信息,请参见 termios(3C)termio(7I) 手册页。

表 5–5 终端 I/O 接口

接口名称 

目的 

tcgetattr(3C), tcsetattr(3C)

获取并设置终端属性 

tcsendbreak(3C), tcdrain(3C), tcflush(3C), tcflow(3C)

执行行控制接口 

cfgetospeed(3C), cfgetispeed(3C)cfsetispeed(3C), cfsetospeed(3C)

获取并设置波特率 

tcsetpgrp(3C)

获取并设置终端前台进程组 ID 

tcgetsid(3C)

获取终端会话 ID 

以下示例说明服务器如何从其处于非 DEBUG 操作模式下的调用方的控制终端分离出来。


示例 5–4 从控制终端分离

   (void) close(0);

   (void) close(1);

   (void) close(2);

   (void) open("/", O_RDONLY);

   (void) dup2(0, 1);

   (void) dup2(0, 2);

   setsid();

此操作模式可防止服务器从控制终端的进程组接收信号。服务器分离之后,便不能将错误报告发送到终端。已分离的服务器必须使用 syslog(3C) 记录错误。