第十章——系统级I/O

文章目录
  1. 1. 文件
  2. 2. 读写文件
    1. 2.1. 文件描述符(file descriptor)
    2. 2.2. 不足值(short count)
    3. 2.3. 描述符表、文件表、v-node 表
      1. 2.3.1. 调用 open 打开文件的具体过程
      2. 2.3.2. open 两次:
      3. 2.3.3. fork:
  3. 3. 基于缓冲区的读写

文件

所有的 I/O 设备都被视为文件,因此我们能用一致的方式处理各种输入输出。

文件类型分为以下几类

  • 普通文件:包含用户数据,可以是文本文件(如.txt)或二进制文件(如.jpg、.exe)
  • 目录:包含一系列链接,每个连接将一个文件名映射到一个文件
  • 套接字(socket):跨网络通信用的文件

读写文件

文件描述符(file descriptor)

文件描述符是一个非负整数,代表进程打开的文件的标识符。在进程中,每当打开一个文件时,操作系统会分配一个文件描述符给它。

默认情况下,0 表示标准输入(stdin),1 表示标准输出(stdout),2 表示标准错误(stderr)。通过系统调用(如 open)打开文件后,会返回一个新的文件描述符(如 3、4 等),具体值取决于当前进程中已使用的描述符情况。

以下是一个使用 open 系统调用的示例,展示如何打开一个文件并获取文件描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
// 打开文件 "example.txt",以只读模式
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open failed");
return 1;
}
printf("文件描述符: %d\\n", fd);

// 使用文件描述符进行操作(如读取)
char buffer[100];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read failed");
close(fd);
return 1;
}

// 关闭文件描述符
close(fd);
return 0;
}

不足值(short count)

“不足值”是指在读写操作中,实际读取或写入的字节数少于请求的字节数。原因有遇到 EOF、从终端读文本行(如果读终端,read 一次只传输一个文本行)、读写网络套接字。

比如调用 read(fd, buffer, 100)请求读取 100 字节,但文件只剩 50 字节可用,则返回 50(不足值)。

我们需要检查返回值,确认实际读写字节数,并根据需要调整逻辑(如循环读取剩余数据)。

描述符表、文件表、v-node 表

操作系统在内核中维护了三层数据结构来管理文件:

  • 描述符表:每个进程独有,记录该进程打开的所有文件描述符及其对应的文件表项。
  • 文件表:所有进程共享,表项包括偏移值、引用计数(即当前指向该表的描述符表项数)、指向 v-node 表中对应项的指针。关闭一个描述符会减少相应的文件表表项的引用计数,减到零会删除。
  • v-node 表(或 inode 表):与具体文件系统相关,记录文件的元数据(如文件大小、权限、存储位置)。每个文件在 v-node 表中有一个唯一条目。

调用 open 打开文件的具体过程

当调用 open 系统调用打开一个文件时,操作系统会执行以下步骤:

  1. 验证和查找文件
    • 内核检查文件路径、权限等,确定文件是否存在且进程有权访问。
    • 找到文件对应的 v-node(或 inode),如果文件已在 v-node 表中,则复用,否则创建新条目。
  2. 分配文件表项
    • 内核在文件表(file table)中创建一个新表项,记录文件的偏移量(初始为 0)、访问模式(如只读、读写)、引用计数(初始为 1)以及指向对应 v-node 的指针。
  3. 更新描述符表
    • 内核在调用进程的描述符表(file descriptor table)中分配一个未使用的最小描述符编号(如 3,若 0、1、2 已占用)。
    • 将该描述符指向新创建的文件表项。
  4. 返回文件描述符
    • open 调用返回分配的文件描述符给进程,供后续操作(如 readwrite)使用。

open 两次:

fork:

基于缓冲区的读写

基于缓冲区的读写将数据先写入内存缓冲区,等缓冲区满或显式刷新(如 fflush)时再一次性与底层设备做数据交换,降低了 I/O 开销。

在使用读写时,我们应尽可能使用 stdio 标准库。stdio(标准输入输出库)提供了基于缓冲区的 I/O 操作(如 fopen、fread、fwrite、printf 等),相比直接使用低级系统调用(如 read、write),它的效率更高而且更不易出错。