【UNIX/Linux】文件I/O【Part 3】

目录

  • dup和dup2
  • sync、fsync和fdatasync
  • fcntl

本文是笔者拜读《UNIX环境高级编程》第3章(文件I/O)的学习笔记。本文的主要内容是dup、sync、fcntl系统调用,文中不仅包含了书中的知识点,也穿插了笔者的理解。

dup和dup2

在这里插入图片描述
dupdup2系统调用用于复制一个现有的文件描述符。成功则返回一个新的文件描述符,失败返回-1.
dup返回的是当前最小的可用文件描述符数值。
对于dup2,可使用newfd参数指定新的文件描述符:如果newfd已经打开,则在先关闭再重用它(关闭和重用是原子操作);如果oldfd等于newfd,则返回newfd而不关闭它(相当于什么也没做);否则newfdFD_CLOEXEC文件描述符标志被清除,这样newfd在进程调用exec时是打开状态。
这些函数返回的文件描述符与oldfd共享同一文件表项。

如果oldfd不是一个有效的文件描述符,调用失败,newfd不会被关闭(如果newfd原来是打开的)。

执行

int newfd = dup(1);

的结果如下图:
【UNIX/Linux】文件I/O【Part 3】_第1张图片

调用dupdup2生成的newfdoldfd共享文件偏移量和文件状态标志(file status flags,如读、写、追加等),但没有共享文件描述符标志(file descriptor flags)。

每个文件描述符都有它自己的一套文件描述符标志。新文件描述符的执行时关闭(close_on_exec)标志是由dup函数清除的。
例:

// dup.c
#include 
#include 
#include 
#include 
#include 

int TryWrite(int fd, char *buf, int count) {
	int wNum = write(fd, buf, count);
	printf("writing to fd = %d\n", fd);
	if (wNum != count) {
		perror("write error");
		return -1;
	}
	perror("writing");
	return 0;
}
int TryRead(int fd, char *buf, int count) {
	int rNum = read(fd, buf, count);
	printf("reading from fd = %d\n", fd);
	if (rNum == -1) {
		perror("read error");
		return -1;
	}
	buf[rNum - 1] = '\0';
	perror("reading");
	return 0;
}

int main() {
	int fd = open("./test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
	if (fd == -1) {
		perror("open error");
		return -1;
	}
	char buf[20] = "hello APUE!!!";
	if (TryWrite(fd, buf, 20) == -1) {
		return -1;
	}
	int newfd1 = dup(fd);
	if (TryWrite(newfd1, buf, 20) == -1) {
		return -1;
	}
	int newfd2 = dup2(newfd1, 10);
	if (TryWrite(newfd2, buf, 20) == -1) {
		return -1;
	}
	lseek(fd, 0, SEEK_SET);
	if (TryRead(newfd2, buf, 10) == -1) {
		return -1;
	}
	else {
		printf("read string: %s\n", buf);
	}
	if (TryRead(newfd1, buf, 10) == -1) {
		return -1;
	}
	else {
		printf("read string: %s\n", buf);
	}
	if (TryRead(fd, buf, 10) == -1) {
		return -1;
	}
	else {
		printf("read string: %s\n", buf);
	}
	
	close(fd);
	close(newfd1);
	close(newfd2);
	return 0;
}

执行结果:
【UNIX/Linux】文件I/O【Part 3】_第2张图片
【UNIX/Linux】文件I/O【Part 3】_第3张图片

fdnewfd1newfd2共享了文件偏移量和文件状态标志(只能写文件),后续将介绍文件描述符标志。
笔者在Linux系统下测试发现,手动往文本文件里写数据(不论是用vim还是gedit),系统都会自动在文件末尾加上一个'\n'(空文件没有)。而使用write系统调用不会添加多余的字符。

通过系统调用fcntl也能复制描述符。dup(fd)等效于fcntl(fd, F_DUPFD, 0)dup2(fd, fd2)等效于close(fd2);fcntl(fd, F_DUPFD, fd2)dup2close + fcntl的区别如下:
(1)dup2是一个原子操作,而closefcntl之间可能被中断并修改文件描述符。
(2)dup2fcntl有一些不同的errno.

sync、fsync和fdatasync

传统的UNIX系统在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区,然后排入队列,晚些再写入磁盘(延迟写)。通常,当内核需要重用缓冲区来存放其他磁盘块数据时,他会把所有的延迟写数据块写到相应的磁盘块上。为了保证磁盘上的实际文件系统与缓冲区内容一致UNIX系统提供了syncfsyncfdatasync系统调用。
在这里插入图片描述
sync只是将所有修改过的块缓冲区排入写队列,然后返回,不等待实际写磁盘操作结束。
通常,称为update的系统守护进程周期性地调用sync函数。这就保证了定期刷新内核的块缓冲区。命令sync也调用sync系统调用
在这里插入图片描述
成功返回0,失败返回-1.
fsync只对由文件描述符fd指定的文件起作用,并等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
fdatdasync类似于fsync但它只影响文件的数据部分,fsync还会同步更新文件的属性。

fcntl

fcntl系统调用可以改变已经打开的文件的属性
在这里插入图片描述
若成功,返回值依赖于cmd,失败返回-1.
fcntl函数有如下功能:
(1)复制一个已有的描述符(cmd=F_DUPFDF_DUPFD_CLOEXEC)。
(2)获取/设置文件描述符标志(cmd=F_GETFDF_SETFD)。
(3)获取/设置文件状态标志(cmd=F_GETFLF_SETFL)。
(4)获取/设置异步I/O所有权(cmd=F_GETOWNF_SETOWN)。
(5)获取/设置记录锁(cmd=F_GETLKF_SETLKF_SETLKW)。

接下来将结合cmd的值,讨论与进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态标志
【UNIX/Linux】文件I/O【Part 3】_第4张图片

cmd 说明
F_DUPFD 复制文件描述符fd,新文件描述符作为函数值返回,它是尚未打开的各文件描述符中大于等于第3个参数值中的最小值。新的文件描述符与fd共享同一文件表项。但是新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除(这表示该描述符在exec时仍保持有效)。
F_DUPFD_CLOEXEC 复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符。
F_GETFD 对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。(默认将FD_CLOEXEC设置为0,在exec时不关闭)
F_SETFD 对于fd设置文件描述符标志。新标志值按第三个参数设置。
F_GETFL 对应于fd的文件状态标志作为函数值返回。使用open函数打开文件时,就设置了文件状态标志。
F_SETFL 将文件状态标志设置为第3个参数的值。可以更改的标志有:O_APPENDO_NONBLOCKO_SYNCO_DSYNCO_RSYNCO_FSYNCO_ASYNC
F_GETOWN 获取接收异步I/O信号SIGIOSIGURG的进程ID或进程组ID
F_SETOWN 设置接收异步I/O信号SIGIOSIGURG的进程ID或进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID

不论cmd是多少,fcntl出错的话都返回-1。下面4个命令有特定的返回值:F_DUPFDF_GETFDF_GETFLF_GETOWN。第1个命令返回新的文件描述符,第2和第3个命令返回相应的标志,第4个命令返回一个正的进程ID负的进程组ID(取绝对值)。

对于fcntl的文件状态标志:

文件状态标志 说明
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
O_EXEC 只执行打开
O_SEARCH 只搜索打开目录
O_APPEND 追加写
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(仅数据)
O_RSYNC 同步读和写

5个是必选标志(访问方式标志),必选标志并不各占1位,因此需要使用屏蔽字O_ACCMODE取得访问方式,然后将结果与这5个常量作比较。而可选标志各占1位,直接通过位与的方式进行判断。

使用fcntl获取文件状态标志。例:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char **argv) {
	if (argc < 2) {
		printf("input invalid");
		return -1;
	}
	// 获取文件状态标志
	int val = fcntl(atoi(argv[1]), F_GETFL);
	if (val == -1) {
		perror("fcntl error");
		return -1;
	}
	// 必选标志
	switch (val & O_ACCMODE) {
	case (O_RDONLY):
		printf("read only mode\n");
		break;
	case (O_WRONLY):
		printf("write only mode\n");
		break;
	case (O_RDWR):
		printf("read and write mode\n");
		break;
	default:
		printf("unknown access mode\n");
		break;
	}
	// 可选标志
	if (val & O_APPEND) {
		printf("append\n");
	}
	if (val & O_TRUNC) {
		printf("trunc\n");
	}
	if (val & O_NONBLOCK) {
		printf("nonblock\n");
	}
	if (val & O_SYNC) {
		printf("sync\n");
	}
	return 0;
}

【UNIX/Linux】文件I/O【Part 3】_第5张图片
从运行结果可以看出来,进程默认在文件描述符012上有打开的文件。重定向的输入流是只读的,重定向的输出流是只写的。n>>t表示在文件描述符n上以只写且追加的方式打开文件tn<>t表示在文件描述符n上以读写的方式打开文件t。

默认情况下,012分别对应的是标准输入、标准输出和标准错误流。虽然在不重定向的情况下,它们都是读写模式的文件,但是它们和普通的可读写文件是不一样的。

标准输入、标准输出和标准错误流不支持使用lseek移动文件指针,它们的文件指针只能根据读写情况增加

往标准输入流中写数据和往标准输出流中写数据,结果都将打印在终端。

从标准输入流中读数据和从标准输出流中读数据,程序都在等待用户的键盘输入。

别使用奇葩的I/O方式。

笔者猜想:标准输入、标准输出和标准错误流可能共享着一个缓冲区。

在修改文件描述符标志或者文件状态标志时,应该先获得当前的标志,然后按照期望修改它,最后设置新的标志。因为使用F_SETFDF_SETFL时,会清除掉旧的标志。如果要给描述符为fd的文件添加O_SYNC标志,应该:

val = fcntl(fd, F_GETFL); // 获取旧的标志
val |= O_SYNC; // 更新标志
fcntl(fd, F_SETFL, val); //设置新的标志

UNIX系统中,通常write只是将数据排入队列,实际的写磁盘操作在以后的某个时刻进行。但如果给文件设置了O_SYNC标志,那在使用writefd中写数据时,要执行完实际的写磁盘才能返回。

这样一来,write的执行时间、进程的系统时间和时钟时间都会增加,但可以避免系统异常时的数据丢失。

标准输入、标准输出、标准错误是由shell为进程打开的,打开方式是shell指定的。因此进程只能通过fcntl改变它们的文件状态标志。

文件状态标志O_SYNC和系统调用函数fsync都是为了及时写数据和更新文件属性到磁盘。
文件状态标志O_DSYNC和系统调用函数fdatasync都是为了及时写数据到磁盘。
只用一个达不到目的吗?它们在具体的实现上有什么不一样?

你可能感兴趣的:(UNIX/Linux)