本文是笔者拜读《UNIX环境高级编程》第3章(文件I/O)的学习笔记。本文的主要内容是dup、sync、fcntl系统调用,文中不仅包含了书中的知识点,也穿插了笔者的理解。
dup
和dup2
系统调用用于复制一个现有的文件描述符。成功则返回一个新的文件描述符,失败返回-1
.
dup
返回的是当前最小的可用文件描述符数值。
对于dup2
,可使用newfd
参数指定新的文件描述符:如果newfd
已经打开,则在先关闭再重用它(关闭和重用是原子操作);如果oldfd
等于newfd
,则返回newfd
而不关闭它(相当于什么也没做);否则newfd
的FD_CLOEXEC
文件描述符标志被清除,这样newfd
在进程调用exec
时是打开状态。
这些函数返回的文件描述符与oldfd
共享同一文件表项。
如果
oldfd
不是一个有效的文件描述符,调用失败,newfd
不会被关闭(如果newfd
原来是打开的)。
执行
int newfd = dup(1);
调用
dup
和dup2
生成的newfd
和oldfd
共享文件偏移量和文件状态标志(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;
}
fd
、newfd1
和newfd2
共享了文件偏移量和文件状态标志(只能写文件),后续将介绍文件描述符标志。
笔者在Linux
系统下测试发现,手动往文本文件里写数据(不论是用vim
还是gedit
),系统都会自动在文件末尾加上一个'\n'
(空文件没有)。而使用write
系统调用不会添加多余的字符。
通过系统调用fcntl
也能复制描述符。dup(fd)
等效于fcntl(fd, F_DUPFD, 0)
;dup2(fd, fd2)
等效于close(fd2);fcntl(fd, F_DUPFD, fd2)
。dup2
和close + fcntl
的区别如下:
(1)dup2
是一个原子操作,而close
和fcntl
之间可能被中断并修改文件描述符。
(2)dup2
和fcntl
有一些不同的errno
.
传统的UNIX系统在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O
都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区,然后排入队列,晚些再写入磁盘(延迟写)。通常,当内核需要重用缓冲区来存放其他磁盘块数据时,他会把所有的延迟写数据块写到相应的磁盘块上。为了保证磁盘上的实际文件系统与缓冲区内容一致,UNIX
系统提供了sync
、fsync
和fdatasync
系统调用。
sync
只是将所有修改过的块缓冲区排入写队列,然后返回,不等待实际写磁盘操作结束。
通常,称为update
的系统守护进程周期性地调用sync
函数。这就保证了定期刷新内核的块缓冲区。命令sync
也调用sync系统调用
。
成功返回0
,失败返回-1
.
fsync
只对由文件描述符fd
指定的文件起作用,并等待写磁盘操作结束才返回。fsync
可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
fdatdasync
类似于fsync
但它只影响文件的数据部分,fsync
还会同步更新文件的属性。
fcntl
系统调用可以改变已经打开的文件的属性。
若成功,返回值依赖于cmd
,失败返回-1
.
fcntl
函数有如下功能:
(1)
复制一个已有的描述符(cmd=F_DUPFD
或F_DUPFD_CLOEXEC
)。
(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
)。
接下来将结合cmd
的值,讨论与进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态标志。
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_APPEND 、O_NONBLOCK 、O_SYNC 、O_DSYNC 、O_RSYNC 、O_FSYNC 和O_ASYNC |
F_GETOWN |
获取接收异步I/O 信号SIGIO 和SIGURG 的进程ID 或进程组ID |
F_SETOWN |
设置接收异步I/O 信号SIGIO 和SIGURG 的进程ID 或进程组ID 。正的arg 指定一个进程ID ,负的arg 表示等于arg 绝对值的一个进程组ID 。 |
不论cmd
是多少,fcntl
出错的话都返回-1
。下面4
个命令有特定的返回值:F_DUPFD
、F_GETFD
、F_GETFL
和F_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;
}
从运行结果可以看出来,进程默认在文件描述符0
,1
,2
上有打开的文件。重定向的输入流是只读的,重定向的输出流是只写的。n>>t
表示在文件描述符n
上以只写且追加的方式打开文件t
,n<>t
表示在文件描述符n
上以读写的方式打开文件t。
默认情况下,0
、1
和2
分别对应的是标准输入、标准输出和标准错误流。虽然在不重定向的情况下,它们都是读写模式的文件,但是它们和普通的可读写文件是不一样的。
标准输入、标准输出和标准错误流不支持使用
lseek
移动文件指针,它们的文件指针只能根据读写情况增加。往标准输入流中写数据和往标准输出流中写数据,结果都将打印在终端。
从标准输入流中读数据和从标准输出流中读数据,程序都在等待用户的键盘输入。
别使用奇葩的
I/O
方式。笔者猜想:标准输入、标准输出和标准错误流可能共享着一个缓冲区。
在修改文件描述符标志或者文件状态标志时,应该先获得当前的标志,然后按照期望修改它,最后设置新的标志。因为使用F_SETFD
或F_SETFL
时,会清除掉旧的标志。如果要给描述符为fd的文件添加O_SYNC
标志,应该:
val = fcntl(fd, F_GETFL); // 获取旧的标志
val |= O_SYNC; // 更新标志
fcntl(fd, F_SETFL, val); //设置新的标志
在UNIX
系统中,通常write
只是将数据排入队列,实际的写磁盘操作在以后的某个时刻进行。但如果给文件设置了O_SYNC
标志,那在使用write
往fd
中写数据时,要执行完实际的写磁盘才能返回。
这样一来,write
的执行时间、进程的系统时间和时钟时间都会增加,但可以避免系统异常时的数据丢失。
标准输入、标准输出、标准错误是由
shell
为进程打开的,打开方式是shell
指定的。因此进程只能通过fcntl
改变它们的文件状态标志。
文件状态标志O_SYNC
和系统调用函数fsync
都是为了及时写数据和更新文件属性到磁盘。
文件状态标志O_DSYNC
和系统调用函数fdatasync
都是为了及时写数据到磁盘。
只用一个达不到目的吗?它们在具体的实现上有什么不一样?