4.0 写在前面
上一章介绍了错误诊断函数和解析数值型命令行参数的函数,分别由以下三个头文件声明:
-
error_functions.h:声明了本书自定义的错误处理函数。
-
get_num.h:声明了本书自定义的数值提取函数。
-
tlpi_hdr.h:则包含了后续需用到的系统调用头文件。
还有前文中提到的ename.c.inc文件,该文件定义了一个字符串数组,用于对应错误码的名称。
本章开始需要开始跑作者的程序,在此按以下步骤将这些头文件复制到默认的编译器寻找路径下,并将实现打包成静态库,然后使用别名来默认链接静态库。
第一步:下载本书所给的源码文件
wget "http://man7.org/tlpi/code/download/tlpi-161214-dist.tar.gz"
第二步:解压后,make编译
tar -zxvf tlpi-161214-dist.tar.gz
cd tlpi-dist/
make -j
第三步:拷贝头文件至系统目录
cd lib/
sudo cp tlpi_hdr.h /usr/local/include/
sudo cp get_num.h /usr/local/include/
sudo cp error_functions.h /usr/local/include/
sudo cp ename.c.inc /usr/local/include/
第四步:制作静态库文件
g++ -c get_num.c error_functions.c # 对get_num.c和error_functions.c
文件进行编译,生成get_num.o和error_functions.o
ar -crv libtlpi.a get_num.o error_functions.o # 创建一个名为libtlpi.a的静态库并
将两个目标文件get_num.o和error_functions.o添加到其中
sudo cp libtlpi.a /usr/local/lib
完成这些之后就可以对某个源文件进行编译并且链接生成的静态库libtlpi.a了
g++ main.cpp -o main -ltlpi
上述操作出处:tlpi_hdr.h头文件的使用以及配置 - 简书
4.1 概述
所有执行 I/O 操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。大多数程序都期望能够使用 3 种标准的文件描述符,见下表。
文件描述符 | 用途 | POSIX名称 | stdio流 |
---|---|---|---|
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准错误 | STDERR_FILENO | stderr |
下面介绍执行文件 I/O 操作的 4 个主要系统调用 open()、read()、write()和 close()(编程语言和软件包通常会利用 I/O 函数库对它们进行间接调用)。
UNIX I/O 模型的显著特点之一是其输入/输出的通用性概念。这意味着使用 4 个同样的系统调用 open()、read()、write()和 close()可以对所有类型的文件执行 I/O 操作,包括终端之类的设备。因此,仅使用这些系统调用编写的程序,将对任何类型的文件有效。
4.2 open()
int open(const char *pathname,int flags,.../*mode_t mode*/)
函数打开 参数pathname 所标识的文件,并返回文件描述符,用以在后续函数调用中指代打开的文件。如果文件不存在,open()函数可以创建之,这取决于对位掩码参数 flags 的设置。flags 参数还可指定文件的打开方式:只读、只写亦或是读写方式。mode 参数则指定了由 open()调用创建文件的访问权限,如果 open()函数并未创建文件,那么可以忽略或省略 mode 参数。
如果 pathname 是一符号链接,会对其进行解引用。如果调用成功,open()将返回一文件描述符,用于在后续函数调用中指代该文件。若发生错误,则返回−1,并将 errno 置为相应的错误标志。
4.3.1 open()调用中的 flags 参数
flags 参数除了使用文件访问标志外,还使用了其他操作标志(O_CREAT、O_TRUNC 和 O_APPEND)。现在将详细介绍 flags 参数。下表总结了可参与 flags 参数逐位或运算(|)的一整套常量。最后一列显示常量标准化于 SUSv3还是 SUSv4。
标志 | 用途 | 统一UNIX规范版本 |
---|---|---|
O_RDONLY | 以只读方式打开 | v3 |
O_WRONLY | 以只写方式打开 | v3 |
O_RDWR | 以读写方式打开 | v3 |
O_CLOEXEC | 设置 close-on-exec 标志 | v4 |
O_CREAT | 若文件不存在则创建之 | v3 |
O_DIRECT | 无缓冲的输入/输出 | |
O_DIRECTORY | 如果 pathname 不是目录,则失败 | v4 |
O_EXCL | 结合 O_CREAT 参数使用,专门用于创建文件 | v3 |
O_LARGEFILE | 在 32 位系统中使用此标志打开大文件 | |
O_NOATIME | 调用 read()时,不修改文件最近访问时间(自 Linux 2.6.8版本开始) | |
O_NOCTTY | 不要让 pathname(所指向的终端设备)成为控制终端 | v3 |
O_NOFOLLOW | 对符号链接不予解引用 | v4 |
O_TRUNC | 截断已有文件,使其长度为零 | v3 |
O_APPEND | 总在文件尾部追加数据 | v3 |
O_ASYNC | 当 I/O 操作可行时,产生信号(signal)通知进程: | |
O_DSYNC | 提供同步的 I/O 数据完整性(自 Linux 2.6.33 版本开始) | v3 |
O_NONBLOCK | 以非阻塞方式打开 | v3 |
O_SYNC | 以同步方式写入文件 | v3 |
上表中常量分为如下几组。
-
文件访问模式标志:先前描述的 O_RDONLY、O_WRONLY 和 O_RDWR 标志均在此列,调用 open()时,上述三者在 flags 参数中不能同时使用,只能指定其中一种。调用fcntl()的 F_GETFL 操作能够检索文件的访问模式(见 5.3 节)。
-
文件创建标志:这些标志在表 4-3 中位于第二部分,其控制范围不拘于 open()调用行为的方方面面,还涉及后续 I/O 操作的各个选项。这些标志不能检索,也无法修改。
-
已打开文件的状态标志:这些标志是表 4-3 中的剩余部分,使用 fcntl()的 F_GETFL 和F_SETFL 操作可以分别检索和修改此类标志。有时干脆将其称之为文件状态标志。
4.3.2 open()函数的错误
若打开文件时发生错误,open()将返回−1,错误号 errno 标识错误原因。以下是一些可能发生的错误(除了在上节参数描述中已经提及的错误之外)。
-
EACCES:文件权限不允许调用进程以 flags 参数指定的方式打开文件。无法访问文件,其可能的原因有目录权限的限制、文件不存在并且也无法创建该文件。
-
EISDIR:所指定的文件属于目录,而调用者企图打开该文件进行写操作。不允许这种用法。(另一方面,在某些场合中,打开目录进行读操作是必要的。18.11 节将举例说明。)
-
EMFILE:进程已打开的文件描述符数量达到了进程资源限制所设定的上限(在 36.3 节将描述RLIMIT_NOFILE 参数)。
-
ENFILE:文件打开数量已经达到系统允许的上限。
-
ENOENT:要么文件不存在且未指定 O_CREAT 标志,要么指定了 O_CREAT 标志,但 pathname 参数所指定路径的目录之一不存在,或者 pathname 参数为符号链接,而该链接指向的文件不存在(空链接)。
-
EROFS:所指定的文件隶属于只读文件系统,而调用者企图以写方式打开文件。
-
ETXTBSY:所指定的文件为可执行文件(程序),且正在运行。系统不允许修改正在运行的程序(比如以写方式打开文件)。(必须首先终止程序运行,然后方可修改可执行文件。)
open函数使用的程序例程:
fd = open("startup", O_RDONLY);
if(fd == -1)
errExit("open");
/* O_TRUNC:打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0 */
fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if(fd == -1)
errExit("open");
/*O_APPEND:用于打开文件时,对写入操作进行追加。*/
fd = open("w.log", O_RDWR | O_CREAT | O_TRUNC | O_APPEND, S_IRUSR | S_IWUSR);
if(fd == -1)
errExit("open");
4.3 read()
ssize_t read(int fd, void *buffer, size_t count);
调用从 fd 所指代的打开文件中读取至多 count 字节的数据,并存储到 buffer 中。read()调用的返回值为实际读取到的字节数。如果再无字节可读(例如:读到文件结尾符 EOF 时),则返回值为 0,错误返回-1.
【注】系统调用不会分配内存缓冲区用以返回信息给调用者。所以,必须预先分配大小合适的缓冲区并将缓冲区指针传递给系统调用。一次 read()调用所读取的字节数可以小于请求的字节数。对于普通文件而言,这有可能是因为当前读取位置靠近文件尾部。当 read()应用于其他文件类型时,比如管道、FIFO、socket 或者终端,在不同环境下也会出现 read()调用读取的字节数小于请求字节数的情况。例如,默认情况下从终端读取字符,一遇到换行符(\n),read()调用就会结束。
使用 read()从终端读取一连串字符,我们也许期望下面的代码会起作用:
#define MAX_READ 20
char buffer[MAX_READ];
if(read(STDIN_FILENO, buffer, MAX_READ) == -1)
errExit("read");
printf("The input data was: %s\n", buffer);
这段代码的输出可能会很奇怪,因为输出结果除了实际输入的字符串外还会包括其他字符。这是因为 read()调用没有在 printf()函数打印的字符串尾部添加一个表示终止的空字符。思索片刻就会意识到这肯定是症结所在,因为 read()能够从文件中读取任意序列的字节。有些情况下,输入信息可能是文本数据,但在其他情况下,又可能是二进制整数或者二进制形式的 C语言数据结构。read()无从区分这些数据,故而也无法遵从 C 语言对字符串处理的约定,在字符串尾部追加标识字符串结束的空字符。如果输入缓冲区的结尾处需要一个表示终止的空字符,必须显式追加。
char buffer[MAX_READ + 1];
ssize_t numRead;
numRead = read(STDIN_FILENO, buffer, MAX_READ);
if(numRead == -1)
errExit("read");
buffer[numRead] = '\0';
printf("The input data was: %s\n", buffer);
4.4 write()
ssize_t write(int fd, void *buffer,size_t count)
调用从 buffer 中读取多达 count 字节的数据写入由fd 所指代的已打开文件中。write()调用的返回值为实际写入文件中的字节数,且有可能小于 count,或-1(错误)。若该返回值可能小于 count 参数值。这被称为“部分写”。对磁盘文件来说,造成“部分写”的原因可能是由于磁盘已满,或是因为进程资源对文件大小的限制。
对磁盘文件执行 I/O 操作时,write()调用成功并不能保证数据已经写入磁盘。因为为了减少磁盘活动量和加快 write()系统调用,内核会缓存磁盘的 I/O 操作
4.5 close()
int close(int fd)
系统调用关闭一个打开的文件描述符,释放文件描述符 fd 以及与之相关的内核资源。返回0(成功)或-1(错误)
显式关闭不再需要的文件描述符往往是良好的编程习惯,会使代码在后续修改时更具可读性,也更可靠。进而言之,文件描述符属于有限资源,因此文件描述符关闭失败可能会导致一个进程将文件描述符资源消耗殆尽。在编写需要长期运行并处理大量文件的程序时,比如 shell 或者网络服务器软件,需要特别加以关注。
像其他所有系统调用一样,应对 close()的调用进行错误检查,如下所示:
if(close(fd) == -1)
errExit("close");
上述代码能够捕获的错误有:企图关闭一个未打开的文件描述符,或者两次关闭同一文件描述符,也能捕获特定文件系统在关闭操作中诊断出的错误条件。
针对特定文件系统的错误,NFS(网络文件系统)就是一例。如果 NFS 出现提交失败,这意味着数据没有抵达远程磁盘,随之将这一错误作为close()调用失败的原因传递给应用系统。
4.6 使用I/O系统调用的程序例程
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
#ifndef BUF_SIZE /* Allow "cc -D" to override definition */
#define BUF_SIZE 1024
#endif
int
main(int argc, char *argv[])
{
int inputFd, outputFd, openFlags;
mode_t filePerms;
ssize_t numRead;
char buf[BUF_SIZE];
if (argc != 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s old-file new-file\n", argv[0]);
/* Open input and output files */
inputFd = open(argv[1], O_RDONLY);
if (inputFd == -1)
errExit("opening file %s", argv[1]);
openFlags = O_CREAT | O_WRONLY | O_TRUNC;
filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
S_IROTH | S_IWOTH; /* rw-rw-rw- */
outputFd = open(argv[2], openFlags, filePerms);
if (outputFd == -1)
errExit("opening file %s", argv[2]);
/* Transfer data until we encounter end of input or an error */
while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
if (write(outputFd, buf, numRead) != numRead)
fatal("write() returned error or partial write occurred");
if (numRead == -1)
errExit("read");
if (close(inputFd) == -1)
errExit("close input");
if (close(outputFd) == -1)
errExit("close output");
exit(EXIT_SUCCESS);
}
4.7 lseek()
对于每个打开的文件,系统内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个 read()或 write()操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为 0。
off_t lseek(int fd, off_t offset, int whence)
系统调用用于在 fd 所指代的打开文件中将文件指针依照 offset 和 whence 参数值调整偏移量。返回文件新的偏移量(成功的话)或-1(错误)。
下图展示了 whence 参数的含义
如果 whence 参数值为 SEEK_CUR 或 SEEK_END,offset 参数可以为正数也可以为负数;如果 whence 参数值为 SEEK_SET,offset 参数值必须为非负数。
这里给出了 lseek()调用的其他一些例子,在注释中说明了将文件偏移量移到的具体位置。
lseek(fd, 0, SEEK_CUR); /* 获取文件偏移量的当前位置 */
lseek(fd, 0, SEEK_SET); /* 文件的开始 */
lseek(fd, 0, SEEK_END); /* 文件结束的下一个字节 */
lseek(fd, -1, SEEK_END); /* 文件的最后一个字节 */
lseek(fd, -10, SEEK-CUR); /* 当前位置的前10个字节 */
lseek(fd, 10000, SEEK_END); /* 文件结束的10001个字节 */
【注】lseek()调用只是调整内核中与文件描述符相关的文件偏移量记录,并没有引起对任何物理设备的访问。lseek()并不适用于所有类型的文件。不允许将 lseek()应用于管道、FIFO、socket 或者终端。一旦如此,调用将会失败,并将 errno 置为 ESPIPE。另一方面,只要合情合理,也可以将 lseek()应用于设备。例如,在磁盘或者磁带上查找一处具体位置。
4.7.1 文件空洞
如果程序的文件偏移量已然跨越了文件结尾,然后再执行 I/O 操作,将会发生什么情况?read()调用将返回 0,表示文件结尾。有点令人惊讶的是,write()函数可以在文件结尾后的任意位置写入数据。
从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看,文件空洞中是存在字节的,读取空洞将返回以 0(空字节)填充的缓冲区。
然而,文件空洞不占用任何磁盘空间。直到后续某个时点,在文件空洞中写入了数据,文件系统才会为之分配磁盘块。文件空洞的主要优势在于,与为实际需要的空字节分配磁盘块相比,稀疏填充的文件会占用较少的磁盘空间。核心转储文件(core dump)(见 22.1 节)是包含空洞文件的常见例子。
【注】对于文件空洞不占用磁盘空间的说法需要稍微限定一下。在大多数文件系统中,文件空间的分配是以块为单位的(14.3 节)。块的大小取决于文件系统,通常是 1024 字节、2048字节、4096 字节。如果空洞的边界落在块内,而非恰好落在块边界上,则会分配一个完整的块来存储数据,块中与空洞相关的部分则以空字节填充。
空洞的存在意味着一个文件名义上的大小可能要比其占用的磁盘存储总量要大(有时会大出许多)。向文件空洞中写入字节,内核需要为其分配存储单元,即使文件大小不变,系统的可用磁盘空间也将减少。这种情况并不常见,但也需要了解。
4.7.2 lseek()与 read()、write()的协作使用示例程序
lseek()与 read()、write()的协作使用示例程序,该程序的第一个命令行参数为将要打开的文件名称,余下的参数则指定了在文件上执行的输入/输出操作。每个表示操作的参数都以一个字母开头,紧跟以相关值(中间无空格分隔)。
-
soffset:从文件开始检索到 offset 字节位置。
-
rlength:在当前文件偏移量处,从文件中读取 length 字节数据,并以文本形式显示。
-
Rlength:在当前文件偏移量处,从文件中读取 length 字节数据,并以十六进制形式显示。
-
wstr:在当前文件偏移量处,向文件写入由 str 指定的字符串。
/**********************************seek_io.c**********************************/
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
size_t len;
off_t offset;
int fd, ap, j;
unsigned char *buf;
ssize_t numRead, numWritten;
if (argc < 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s file {r<length>|R<length>|w<string>|s<offset>}...\n",
argv[0]);
fd = open(argv[1], O_RDWR | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
S_IROTH | S_IWOTH); /* rw-rw-rw- */
if (fd == -1)
errExit("open");
for (ap = 2; ap < argc; ap++) {
switch (argv[ap][0]) {
case 'r': /* Display bytes at current offset, as text */
case 'R': /* Display bytes at current offset, in hex */
len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
buf = (unsigned char*)malloc(len);
if (buf == NULL)
errExit("malloc");
numRead = read(fd, buf, len);
if (numRead == -1)
errExit("read");
if (numRead == 0) {
printf("%s: end-of-file\n", argv[ap]);
} else {
printf("%s: ", argv[ap]);
for (j = 0; j < numRead; j++) {
if (argv[ap][0] == 'r')
printf("%c", isprint(buf[j]) ? buf[j] : '?');
else
printf("%02x ", buf[j]);
}
printf("\n");
}
free(buf);
break;
case 'w': /* Write string at current offset */
numWritten = write(fd, &argv[ap][1], strlen(&argv[ap][1]));
if (numWritten == -1)
errExit("write");
printf("%s: wrote %ld bytes\n", argv[ap], (long) numWritten);
break;
case 's': /* Change file offset */
offset = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
if (lseek(fd, offset, SEEK_SET) == -1)
errExit("lseek");
printf("%s: seek succeeded\n", argv[ap]);
break;
default:
cmdLineErr("Argument must start with [rRws]: %s\n", argv[ap]);
}
}
if (close(fd) == -1)
errExit("close");
exit(EXIT_SUCCESS);
}
【注】上面这段程序使用g++
编译时,会出现一个错误如下
g++ seek_io.c -o seek_io -ltlpi
error: invalid conversion from ‘void*’ to ‘unsigned char*’ [-fpermissive]
43 | buf = malloc(len);
出现error的原因是C++设计得比C更加安全,它不能自动地将void *转换为其它指针类型。所以将43行的buf = malloc(len);
改为buf = (unsigned char*)malloc(len);
即可正常编译。
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ g++ seek_io.c -o seek_io -ltlpi # 编译生成seek_io.out文件
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ touch tfile # 新建一个空文件
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ ./seek_io tfile s100000 wabc # 相对文件头便宜到100000的位置并写入“abc”
s100000: seek succeeded
wabc: wrote 3 bytes
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ ls -l tfile # 查看tfile的大小
-rw-r--r-- 1 vainx vainx 100003 Jul 20 07:08 tfile
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ ./seek_io tfile s100000 r3 # 读取刚写入的字符
s100000: seek succeeded
r3: abc
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ ./seek_io tfile s10000 R5 # 相对文件头便宜到100000的位置并从该位置读5bytes
s10000: seek succeeded
R5: 00 00 00 00 00