本章重点介绍用于文件输入/输出的系统调用。本章开篇会讨论文件描述符的概念,随后会逐一讲解构成通用 I/O 模型的系统调用,其中包括:打开文件、关闭文件、从文件中读数据和向文件中写数据。
所有执行 I/O 操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。
文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。
文件描述符 | 用途 | POSIX 名称 | stdio 流 |
---|---|---|---|
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准错误 | STDERR_FILENO | stderr |
在程序开始运行之前,shell 代表程序打开这 3 个文件描述符。更确切地说,程序继承了 shell 文件描述符的副本—在 shell 的日常操作中,这 3 个文件描述符始终是打开的。
针对 stdout 调用 freopen () 函数后,无法保证 stdout 变量值仍然为 1
执行文件I/O操作的4个主要系统调用:
fd = open(pathname, flags, mode)
- 打开 pathname 所标识的文件,并返回文件描述符,用以在后续函数调用中指代打开的文件
- 如果文件不存在,open () 函数可以创建之,这取决于对位掩码参数 flags 的设置
- flags 参数可指定文件的打开方式:只读、只写亦或是读写方式
- mode 参数则指定了由 open () 调用创建文件的访问权限
numread = read(fd, buffer, count)
- 调用从 fd 所指代的打开文件中读取至多 count 字节的数据,并存储到 buffer 中
- read () 调用的返回值为实际读取到的字节数,无字节可读返回0
numwritten = write(fd, buffer, count)
- 调用从 buffer 中读取多达 count 字节的数据写入由fd 所指代的已打开文件中
status = close(fd)
- 释放文件描述符 fd 以及与之相关的内核资源
例子:实现拷贝文件操作
./copy oldfile newfile
#include<sys/stat.h>
#include<fcntl.h>
#include"lib/tlpi_hdr.h"
#ifndef BUF_SIZE // allow "cc -D" to override
#define BUF_SIEZ 1024
#endif
int main(int argc, char *argv[])
{
int inputFd, outputFd, openFlags;
mode_t filePerms;
ssize_t numRead;
char buf[BUF_SIEZ];
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
while ((numRead = (read(inputFd, buf, BUF_SIEZ)) > 0)) {
if (write(outputFd, buf, numRead) != numRead)
fatal("couldn't write whole buffer");
}
if (numRead == -1)
errExit("read");
if (close(inputFd) == -1)
errExit("close input");
if (close(outputFd) == -1)
errExit("close output");
exit(EXIT_SUCCESS);
}
1. 打开文件:open ()
#include<sys/stat.h>
#include<fcntl.h>
int open(const char *pathname, int flags, ...);
// return file descripter on success, or -1 on error
访问模式 | 描述 |
---|---|
O_RDONLY | 以只读方式打开文件 |
O_WRONLY | 以只写方式打开文件 |
O_RDWR | 以读写方式打开文件 |
早期的 UNIX 实现中使用数字 0、1、2,而非表中所列的常量名称。
O_RDWR 并不等同于 O_RDONLY | O_WRONLY,后者(或组合)属于逻辑错误。
当调用 open()
创建新文件时,位掩码参数 mode 指定了文件的访问权限。
新建文件的访问权限不仅仅依赖于参数 mode,而且受到进程的 umask 值和(可能存在的)父目录的默认访问控制列表影响。
open() 调用中的flags参数
常量分为以下几组:
- 文件访问模式标志:先前描述的 O_RDONLY、O_WRONLY 和 O_RDWR 标志均在此列,调用 open () 时,上述三者在 flags 参数中不能同时使用,只能指定其中一种。调用 fcntl () 的 F_GETFL 操作能够检索文件的访问模式。
- 文件创建标志:这些标志在表中位于第二部分,其控制范围不拘于 open() 调用行为的方方面面,还涉及后续 I/O 操作的各个选项。这些标志不能检索,也无法修改。
- 已打开文件的状态标志:这些标志是表中的第三部分,使用 fcntl () 的 F_GETFL 和F_SETFL 操作可以分别检索和修改此类标志。有时干脆将其称之为文件状态标志。
open () 报错信息
- EACCES:文件权限不允许调用进程以 flags 参数指定的方式打开文件。
- EISDIR:所指定的文件属于目录,而调用者企图打开该文件进行写操作。
- EMFILE:进程已打开的文件描述符数量达到了进程资源限制所设定的上限。
- ENFILE:文件打开数量已经达到系统允许的上限。
- ENOENT:要么文件不存在且未指定 O_CREAT 标志,要么指定了 O_CREAT 标志,但 pathname 参数所指定路径的目录之一不存在,或者 pathname 参数为符号链接,而该链接指向的文件不存在(空链接)。
- EROFS:所指定的文件隶属于只读文件系统,而调用者企图以写方式打开文件。
- ETXTBSY:所指定的文件为可执行文件(程序),且正在运行。
早期UNIX实现中,open () 只有两个参数无法创建新文件,使用create () xi系统调用来创建并打开一个新文件,但由于 open()的 flags 参数能对文件打开方式提供更多控制,对 creat()的使用现在已不多见
2. 读取文件:read()
#include<unistd.h>
ssize_t read(int fd, void *buffer, size_t count);
// returns number of bytes read, 0, on EOF, or -1 on error
count 参数指定最多能读取的字节数。(size_t 数据类型属于无符号整数类型。)buffer 参数提供用来存放输入数据的内存缓冲区地址。缓冲区至少应有 count 个字节。
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);
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);
3. 数据写入:write()
#include<unistd.h>
ssize_t write(int fd, void *buffer, size_t count);
// returns number of bytes written, or -1 on error
对磁盘文件执行 I/O 操作时,write () 调用成功并不能保证数据已经写入磁盘。
4. 关闭文件:close()
#include<unistd.h>
int close(int fd);
// returns 0 on success, or -1 on error
像其他所有系统调用一样,应对 close () 的调用进行错误检查,如下所示:
if (close(fd) == -1)
errExit("close");
上述代码能够捕获的错误有:
- 企图关闭一个未打开的文件描述符
- 或者两次关闭同一文件描述符
- 捕获特定文件系统在关闭操作中诊断出的错误条件
5. 改变文件偏移量:lseek()
#include<unistd.h>
off_t lseek(int fd, off_t offset, int whence);
// returns new file offset if successful, or -1 on error
whence 参数则表明应参照哪个基点来解释 offset 参数,应为下列其中之一:
SEEK_SET
:将文件偏移量设置为从文件头部起始点开始的 offset 个字节。SEEK_CUR
:相对于当前文件偏移量,将文件偏移量调整 offset 个字节。SEEK_END
:将文件偏移量设置为起始于文件尾部的 offset 个字节。
如果 whence 参数值为 SEEK_CUR 或 SEEK_END,offset 参数可以为正数也可以为负数;
如果 whence 参数值为 SEEK_SET,offset 参数值必须为非负数。
lseek()并不适用于所有类型的文件。不允许将 lseek()应用于管道、FIFO、socket 或者终端。一旦如此,调用将会失败,并将 errno 置为 ESPIPE。
从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看,文件空洞中是存在字节的,读取空洞将返回以 0(空字节)填充的缓冲区。
6. 通用 I/O 模型以外的操作:ioctl()
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...);
// return on success depends on request, or -1 on error
7. 综合示例
该程序的第一个命令行参数为将要打开的文件名称,余下的参数则指定了在文件上执行的输入/输出操作。每个表示操作的参数都以一个字母开头,紧跟以相关值(中间无空格分隔)。
- soffset:从文件开始检索到 offset 字节位置。
- rlength:在当前文件偏移量处,从文件中读取 length 字节数据,并以文本形式显示。
- Rlength:在当前文件偏移量处,从文件中读取 length 字节数据,并以十六进制形式显示。
- wstr:在当前文件偏移量处,向文件写入由 str 指定的字符串。
#include "lib/tlpi_hdr.h"
#include <ctype.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
size_t len;
off_t offset;
int fd, ap, j;
unsigned char *buf;
ssize_t numRead, numWritten;
// command wrong or help
if (argc < 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s file {r<length>|R<length>|w<string>|s<offset>}...\n", argv[0]);
int openFlags = O_CREAT | O_RDWR;
mode_t filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; // rw-rw-rw
fd = open(argv[1], openFlags, filePerms);
if (fd == -1)
errExit("open");
for (ap = 2; ap < argc; ++ap) {
switch (argv[ap][0]) {
case 'r':
len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
buf = malloc(len);
if (buf == NULL)
errExit("malloc");
numRead = read(fd, buf, len);
if (numRead == -1)
errExit("read");
else if (numRead == 0)
printf("%s: end-of-file\n", argv[ap]);
else {
printf("%s: ", argv[ap]);
for (j = 0; j < len; ++j)
printf("%c", isprint(buf[j]) ? buf[j] : '?');
}
printf("\n");
free(buf);
break;
case 'R':
len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
buf = malloc(len);
if (buf == NULL)
errExit("malloc");
numRead = read(fd, buf, len);
if (numRead == -1)
errExit("read");
else if (numRead == 0)
printf("%s: end-of-file\n", argv[ap]);
else {
printf("%s: ", argv[ap]);
for (j = 0; j < len; ++j)
printf("%02x ", buf[j]);
}
printf("\n");
free(buf);
break;
case 'w':
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':
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]);
break;
}
}
if (close(fd) == -1)
errExit("close");
exit(EXIT_SUCCESS);
}