本章重点介绍用于文件输入/输出的系统调用。本章开篇会讨论文件描述符的概念,随后会逐一讲解构成通用 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);
}