读linux programming interface chapter 4

读linux programming interface chapter 4

变量stdin stdout stderr最初是指进程的标准输入、输出和错误,但它们可用freopen()库函数更改为引用任何文件。freopen()可能会更改重新打开的流的底层文件描述符。对stdout执行freopen()后,不再安全地假设底层文件描述符仍然是1

  • fd = open(pathname, flags, mode) ,返回用于在后续调用中引用打开的文件的文件描述符。如文件不存在,open()可能创建它,取决于flags位掩码参数的设置
    • flags参数还指定文件是要打开为读、写还是两者兼有
    • mode参数指定如果此调用创建文件,则在文件上放置的权限。如果open()调用不用于创建文件,则此参数将被忽略并可省略
  • numread = read(fd, buffer, count) 从fd引用的打开文件中最多读取count字节,将它们存储在buffer中。read()调用返回实际读取的字节数。如果不能再读取更多字节即遇到文件尾,read()返回0
  • numwritten = write(fd, buffer, count) 将buffer中的最多count字节写入fd引用的打开文件。write()调用返回实际写入的字节数,可能少于count
  • status = close(fd) 在所有I/O完成后被调用,释放文件描述符fd及其相关的内核资源
#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);
}

两个标志:

  • FLAG_A 定义为 0b0001 (在二进制中,这是1)
  • FLAG_B 定义为 0b0010 (在二进制中,这是2)

用bitwise OR组合:

int combinedFlags = FLAG_A | FLAG_B;

combinedFlags 的二进制表示为 0b0011,十进制为3。

UNIX I/O模型显著特点是I/O的通用性。意味着用相同的四个系统调用——open() read() write() close()——来对所有类型的文件,包括像终端这样的设备,进行I/O操作

通过确保每个文件系统和设备驱动程序实现相同的I/O系统调用集,实现了I/O的通用性。由于特定文件系统或设备的细节是在内核内部处理的,在编写应用程序时通常可忽略设备特定的因素

当访问文件系统或设备的特定功能时,程序可以使用catchall ioctl()系统调用,它提供了一个接口,用于处理不在通用I/O模型之外的功能

open()系统调用要么打开一个现有的文件,要么创建并打开一个新文件

#include <sys/stat.h>
#include <fcntl.h>
 
int open(const char pathname, int flags, ... / mode_t mode */);

flags参数是一个位掩码,它使用O_RDONLY, O_WRONLY, O_RDWR常量指定文件的访问模式

早期UNIX实现使用数字0、1和2。大多数现代UNIX实现定义这些常量为这些值。因此可以看到O_RDWR不等于O_RDONLY | O_WRONLY;后者的组合是逻辑错误

open()创建新文件时,mode位掩码参数指定要放置在文件上的权限(用于类型化模式的mode_t数据类型在SUSv3中指定为整数类型)。如果open()不指定O_CREATmode可省略

/* Open existing file for reading */
 
fd = open("startup", O_RDONLY);
if (fd == -1)
    errExit("open");
 
/* Open new or existing file for reading and writing, truncating to zero
   bytes; file permissions read+write for owner, nothing for all others */
 
fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1)
    errExit("open");
 
/* Open new or existing file for writing; writes should always
   append to end of file */
 
fd = open("w.log", O_WRONLY | O_CREAT | O_APPEND,
                   S_IRUSR | S_IWUSR);
if (fd == -1)
    errExit("open");

SUSv3规定如果open()成功,保证使用进程中最低编号的未使用文件描述符。可利用这一特性确保文件使用特定的文件描述符打开。如下面的序列确保文件使用标准输入(文件描述符0)打开

if (close(STDIN_FILENO) == -1)      /* 关闭文件描述符0 */
    errExit("close");
 
fd = open(pathname, O_RDONLY);
if (fd == -1)
    errExit("open");

如果程序超出文件的末尾进行查找,执行I/O操作会怎样?调用 read() 会返回0表示文件结束。出乎意料的是可在文件结束之后的任意位置写入字节

文件的末尾和新写入的字节之间的空间被称为文件洞。从编程的角度看存在洞中的字节,从洞中读取会返回一个包含0(空字节)的字节缓冲区

文件洞并不占用任何磁盘空间。文件系统不会为洞分配任何磁盘块,直到在某时将数据写入其中。文件洞优点是,稀疏填充的文件比实际需要在磁盘块中分配的空字节所需的磁盘空间少。核心转储文件是包含大量洞的文件

文件洞不消耗磁盘空间的说法需要修正。大多数文件系统中文件空间以块的单位分配。块大小取决于文件系统,但通常是1024、2048或4096字节这样的数值。如果洞的边缘在块内,而不在块边界上,那么一个完整的块被分配来存储块的其他部分的数据,与洞对应的部分填充为空字节。

大多原生UNIX文件系统支持文件洞,许多非原生文件系统(如Microsoft的VFAT)则不支持。在不支持洞的文件系统上会显式地将空字节写入文件

洞的存在意味着文件名义大小可能大于它实际使用的磁盘存储量(某些情况下可能大得多)。在文件洞的中间写入字节会减少可用的磁盘空间,因为内核分配块来填充洞,尽管文件大小没有改变。这种情况不常见,但仍需注意

SUSv3指定了函数posix_fallocate(fd, offset, len),确保为描述符fd引用的磁盘文件指定的offsetlen的字节范围在磁盘上分配了空间。这样可确保后续对文件的write()操作不会因磁盘空间耗尽而失败(否则如果文件中的洞被填充,或其他应用程序在文件的全部范围被写入之前消耗了磁盘空间,可能发生这种情况)。历史上glibc实现的这个函数通过在指定范围的每个块中写入一个0字节来达到预期的结果。2.6.23版本以来Linux提供fallocate()系统调用,一个更高效的方式确保分配了必要的空间,glibc的posix_fallocate()实现在可用时使用了这个系统调用


#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 = 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]);
        }
    }
 
    exit(EXIT_SUCCESS);
}

ioctl()是一个通用的机制用于执行通用I/O模型之外的文件和设备操作

#include <sys/ioctl.h>
 
int ioctl(int fd, int request, ... /* argp */);

成功时根据request返回值,错误时返回-1

fd参数是为了执行由request指定的控制操作而打开的设备或文件的文件描述符。设备特定的头文件定义了可在request参数中传递的常数

标准C省略号(...)表示法所示,标记为argpioctl()的第三个参数可是任何类型。request参数的值使ioctl()能够确定在argp中预期的值类型。通常argp是指向整数或结构的指针;某些情况下它未被使用

SUSv3为ioctl()做的唯一规定是控制STREAMS设备的操作(STREAMS设施是System V特性,主线Linux内核不支持,尽管已开发了一些附加实现)。然而ioctl()自UNIX系统的早期版本以来一直是UNIX系统的一部分,因此许多ioctl()操作在许多其他UNIX实现中都被提供


Summary

在常规文件上执行I/O,首先使用open()获取文件描述符。使用read()write()执行I/O。执行所有I/O操作后应使用close()释放文件描述符及其关联的资源

每个打开的文件内核都维护一个文件偏移,它决定下一次读取或写入将发生的位置。文件偏移由读取和写入隐式更新。使用lseek()

ioctl()系统调用是用于不符合标准文件I/O模型的设备和文件操作的万能工具