本文是笔者拜读《UNIX环境高级编程》第3章(文件I/O)的学习笔记。本文的主要内容包括ioctl、/dev/fd,第3章的小结和习题。文中不仅包含书中的知识点,也包括笔者的理解。
ioctl函数
ioctl
函数可处理特殊文件的基础设备参数。 特别是,可以使用ioctl
请求来控制字符特殊文件(例如,终端)的许多操作特性。 参数fd
必须是一个打开的文件描述符。第二个参数是设备相关的请求代码。 第三个参数(图中省略了)是指向内存的无类型指针。成功返回非负值,失败返回-1
.
ioctl
函数一直是I/O操作的杂物箱。不能用本章其他函数表示的I/O
操作(如read
、write
、pread
、pwrite
、lseek
等)通常都能用ioctl
表示,如磁带I/O、终端I/O等。终端I/O是使用ioctl
最多的地方,终端I/O
的ioctl
命令需要头文件<termios.h>
。
每个设备驱动程序可以定义它自己专用的一组ioctl
命令,系统则为不同种类的设备提供通用的ioctl
命令。
/dev/fd
较新的系统都有/dev/fd/
目录,打开文件/dev/fd/n
等效于复制文件描述符n
。大多数系统会忽略open
函数指定的flag
,有些系统要求此时的打开模式必须是/dev/fd/0
(标准输入)初始打开时所使用的打开模式的子集。
fd = open("/dev/fd/0", flag);
等效于
fd = dup(0);
描述符0
和fd
共享同一文件表项。如果描述符0
先前被打开为只读,那么程序也只能对fd
进行只读操作。即使下面的调用是成功的:
fd = open("/dev/fd/0", O_RDWR);
但是在Linux
的实现中/dev/fd
是个例外。它把文件描述符映射成指向底层物理文件的符号链接。例如,当打开/dev/fd/0
时,事实上正在打开与标准输入关联的文件,因此返回的文件描述符与/dev/fd
文件描述符的模式并不相关。
标准输入、标准输出和标准错误关联到的是同一文件。
例(Linux环境):
// devfd.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void GetFlag(int fd) {
int val = fcntl(fd, F_GETFL);
if (val == -1) {
perror("get fileFlag error");
}
switch (val & O_ACCMODE) {
case (O_RDONLY):
printf("read only\n");
break;
case (O_WRONLY):
printf("write only\n");
break;
case (O_RDWR):
printf("read and write\n");
break;
default:
printf("unknown access mode\n");
break;
}
if (val & O_APPEND) {
printf("append\n");
}
if (val & O_SYNC) {
printf("sync\n");
}
printf("----------\n");
}
int main() {
GetFlag(0);
int fd = open("/dev/fd/0", O_RDWR | O_SYNC);
if (fd == -1) {
perror("open /dev/fd/0 error");
}
GetFlag(fd);
fd = dup(0);
GetFlag(fd);
close(fd);
return 0;
}
fcntl
获取不到O_TRUNC
和O_CREAT
,或许它们不属于文件状态标志。open
函数的第2
个参数并不都属于文件状态标志。文件状态标志只包含必选标志和非阻塞、追加写和一些同步标志。
运行结果:
笔者进一步测试发现,
open
函数的返回值是3
,dup
的返回值是4
。如果往描述符3
里写数据,数据并不会显示在终端上,而write
操作并没有出错。如果往描述符4
里写数据(标准输入流处于可读写模式下),数据就会显示在终端上(就像往标准输出写一样)。
描述符3
支持lseek操作,描述符4
不支持。
笔者猜想:open
函数打开的是和标准输入关联的底层文件,使用write
的确能将数据写入文件,但不会显示在终端上。而描述符4
和标准输入共享很多数据。说到底描述符3
和4
没有共享文件表项。
dup
返回的文件描述符和描述符0
共享文件表项,文件状态标志和文件偏移量始终是一样的。而使用open
打开/dev/fd/0
时,事实上正在以指定的状态标志打开与标准输入关联的文件。open返回的文件描述符和描述符0并不共享文件表项。
也可以用/dev/fd
作为路径名参数调用creat
,这与调用open
时指定O_CREAT
作用相同。但因为Linux
实现使用指向实际文件的符号链接,调用create
的话会导致底层文件被截断。
/dev/fd
文件主要由shell
使用,它允许使用路径名作为调用参数的程序,能使用路径名访问到标准输入输出。如下图的命令行所示,cat
将从标准输入读到的数据打印到标准输出。
小结
本章说明了UNIX
系统提供的基本I/O
函数。因为read
和write
都在内核执行,所以称这些函数为不带缓冲的I/O
函数。本章的主要内容总结为以下几点:
(1)open
和openat
函数的区别,flags
参数(必选标志、可选标志)。
(2)close
函数关闭文件描述符。
(3)lseek
函数移动文件指针。(pipe
、fifo
、socket
、标准输入输出错误等特殊文件无法使用lseek
)。
(4)读文件:read
和pread
。写文件:write
和pwrite
。
(5)文件的共享。进程表、进程表项、打开文件描述符表、文件表、文件表项、v/i
节点。有些描述符共享的是文件表项,有些共享的是v/i
节点。
(6)文件描述符标志、文件状态标志。
(7)原子操作。pread
和pwrite
是定位读写位置加读写数据的原子操作,且不改变文件指针。open
函数的flags
参数指定常量O_CREAT
和O_EXCL
是检测文件是否存在加创建文件的原子操作。
(8)dup
和dup2
函数用于复制文件描述符(新旧描述符共享文件表项)。使用open
函数才能创建自己的文件表项。
(9)sync
、fsync
和fdatasync
是文件同步更新的系统调用。
(10)fcntl
函数。用于获取/设置 打开文件的文件描述符标志/文件状态标志,复制文件描述符,获取/设置异步I/O
信号的所有权,获取/设置记录锁。
(11)ioctl
用于执行某些特殊文件的I/O
操作。
(12)/dev/fd/0
、/dev/fd/1
和/dev/fd/2
是标准输入、输出、错误的路径名。
习题
3.1
题目:当读/写磁盘文件时,本章中描述的函数确实是不带缓冲机制的吗?请说明原因。
答:read
和write
本身不带缓冲,但系统具体实现的I/O
是带缓冲的。现在的操作系统采用一种预读技术,当系统发现进程在顺序地读写文件时,在调用read
或write
的时候,会多读写一些数据放在缓冲区里。当下次调用read/write
的时候,直接使用缓冲区里的数据,进而提升I/O
的效率。
3.2
题目:编写一个与3.12节中dup2功能相同的函数,要求不调用fcntl函数,并且要有正确的出错处理。
答:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void clsFd(int *arrFd, int cls) {
for (int i = 0; i < cls; ++i) {
close(arrFd[i]);
}
}
int myDup2(int oldFd, int newFd) {
if (oldFd < 0 || newFd < 0) {
perror("invalid descriptor number");
return -1;
}
// 使用dup判断oldFd是否是打开的文件描述符
int fd = dup(oldFd);
if (fd == -1) {
perror("descriptor oldFd not open");
return -1;
}
close(fd);
// 相等,直接返回
if (oldFd == newFd) {
return newFd;
}
// 如果newFd打开,则关闭
close(newFd);
// 使用dup关联oldFd和newFd
int arrFd[newFd];
int cls = 0;
for (int i = 0; i <= newFd; ++i) {
int tmp = dup(oldFd);
if (tmp == -1) {
perror("dup error");
clsFd(arrFd, cls);
return -1;
}
else if (tmp == newFd) {
break;
}
else {
arrFd[cls++] = tmp;
}
}
clsFd(arrFd, cls);
return newFd;
}
3.3
题目:假设一个进程执行下面3个函数调用:
fd1 = open(path, oflags);
fd2 = dup(fd1);
fd3 = open(path, oflags);
画出以上代码涉及的进程表项、文件表项和v节点之间的共享关系图。对fcntl
作用于fd1
来说,F_SETFD
命令会影响哪一个文件描述符?F_SETFL
呢?
答:fd1
和fd2
共享文件表项,fd3
重新打开了文件。
每个文件描述符都有自己的文件描述符标志,对fd1
执行F_SETFD
命令只会影响fd1
的文件描述符标志。fd1
和fd2
共享文件表项(共享文件状态标志和文件偏移量),对fd1
执行F_SETFL
命令会影响fd1
和fd2
描述符对应的文件状态标志。
3.4
题目:许多程序中都包含下面一段代码:
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
if (fd > 2) {
close(fd);
}
为了说明if
语句的必要性,假设fd
是1
,画出每次调用dup2
时3
个描述符项及相应的文件表项的变化情况。然后再画出fd
为3
的情况。
答:
(1)当fd=1
时,第一条语句先关闭文件描述符0
,再重新打开它,并让它和文件描述符1
共享文件表项。函数返回0
。(相当于将0
重定向到1
)。
第二条语句,函数直接返回1
,图没有变化。
第三条语句先关闭文件描述符2
,再重新打开它,并让它和文件描述符1
共享文件表项。函数返回2
。
不执行close(fd);
最后文件描述符0
,1
,2
都被重定向到了标准输出。
(2)当fd=3
时,第一条语句先关闭文件描述符0
,再重新打开它,并让它和文件描述符3
共享文件表项。函数返回0
。
第二条语句先关闭文件描述符1
,再重新打开它,并让它和文件描述符3
共享文件表项。函数返回1
。
第三条语句先关闭文件描述符2
,再重新打开它,并让它和文件描述符3
共享文件表项。函数返回2
。
最后关闭文件描述符3
。文件描述符0
,1
,2
都指向原来文件描述符3
所对应的文件表项。
如果文件描述符
fd
是个可写的文件,在执行了语句dup2(fd,1);
后,所有往标准输出流打印的数据将被写在fd
对应的文件里。
3.5
题目:在Bourne shell、Bourne-again shell和Korn shell中,digit1>&digit2
表示要将描述符digit1
重定向至描述符digit2
的同一文件。请说明下面两条命令的区别。
$
:./a.out > outfile 2>&1
$
:./a.out 2>&1 > outfile
(shell从左到右处理命令行)
答:假设shell
打开outfile
的文件描述符为3
。
第一条命令相当于shell
执行了:dup2(3, 1); dup2(1, 2);
先让描述符1
和描述符3
共享描述符3
的文件表项,再让描述符2
和描述符1
共享描述符1
的文件表项,描述符3
没动。而此时描述符1
和描述符3
共享文件表项,所以这三个描述符都指向了描述符3
的文件表项。
导致标准输出和标准错误都重定向到了文件outfile
。
第二条命令相当于shell
执行了:dup2(1, 2);dup2(3, 1);
先让描述符2
和描述符1
共享描述符1
的文件表项,再让描述符1
和描述符3
共享描述符3
的文件表项,描述符3
没动。最终描述符2
指向了描述符1
原来的文件表项,描述符1
指向了描述符3
的文件表项。
导致标准错误重定向到了标准输出(终端),标准输出重定向到了文件outfile
。
3.6
题目:如果使用追加标志打开一个文件以便读、写,能否仍用lseek
在任一位置开始读?能否用lseek
更新文件中任一部分的数据?请编写一段程序验证。
答:可以使用lseek
从任一位置开始读,但不能更新原文件中任一部分的数据,只能追加写。
// t6.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("./t.txt", O_RDWR | O_APPEND);
if (fd == -1) {
perror("open error");
return -1;
}
lseek(fd, 0, SEEK_SET);
char buf[20] = "promising\n";
int wNum = write(fd, buf, 10);
if (wNum != 10) {
perror("write error");
return -1;
}
lseek(fd, 0, SEEK_SET);
int rNum = read(fd, buf, 6);
if (rNum == -1) {
perror("read error");
return -1;
}
buf[rNum] = '\0';
printf("read:%s", buf);
close(fd);
return 0;
}
如果既想更新原文件中的部分数据,又不想删除原文件的所有数据,那么在使用
open
时就不能指定O_TRUNC
和O_APPEND
,使用默认可写方式打开。