ICS10 系统级I/O
type
status
date
slug
summary
tags
category
icon
password
在计算机系统中,输入/输出(I/O)是连接程序与外部世界的桥梁。I/O操作主要涉及两个层次:系统级和C标准库级。在C标准库层面,开发者可以通过调用如
fopen、fscanf、fprintf等函数进行文件操作。而在系统层面,程序则通过直接调用Unix系统调用(如open、read、write)来实现更底层的I/O控制。此外,Robust I/O(RIO)是CSAPP提供的一个经过封装的特殊I/O库,它提供了健壮的错误处理、信号处理以及数据截断等机制。Unix I/O
文件抽象
Linux系统将所有I/O设备抽象为文件,例如磁盘分区(
/dev/sda2)、终端(/dev/tty2)等。内核自身也通过文件形式组织,例如/boot/目录存放内核映像文件。通过open()、close()、read()、write()和lseek()这五个基本操作,即可完成对所有类型文件的通用操作。文件类型
- 普通文件:包含文本(ASCII/Unicode编码)或二进制数据。在Linux/MacOS上,文本文件以
\n作为换行符,而Windows系统使用\r\n组合,这源于打字机时代的回车(Carriage Return)和换行(Line Feed)操作传统。
- 目录:包含文件链接的特殊文件,每个目录至少包含
.(当前目录)和..(父目录)两个条目。
- 套接字(socket):用于进程间或网络通信的特殊文件,通过读写操作实现数据包收发。
- 其他类型:包括管道、符号链接等。
需要特别注意的是:系统内核并不区分文本和二进制文件,后缀名只是作为提示;所有文件都视为字节序列处理。
目录操作
每个进程都有内核维护的当前工作目录(Current Working Directory,CWD),路径可分为绝对路径(以
/开头)和相对路径,相对路径即从CWD引出。Linux系统中,通过mkdir创建目录,ls查看目录内容,rmdir删除空目录,cd修改当前工作目录。C语言下的文件操作
打开文件
open函数返回文件描述符,这是操作系统分配给打开文件的整型标识符,标准输入/输出/错误的描述符分别为 STDIN_FILENO(0)、STDOUT_FILENO(1)、STDERR_FILENO(2)。参数详解
参数 | 类型 | 说明 |
pathname | const char* | 文件路径,绝对的或相对的 |
flags | int | 打开模式(必选)和附加选项(可选)的组合 |
mode | mode_t | 文件权限(仅在 O_CREAT时生效),由S_IRUSR等权限位组合定义 |
Flags参数
返回值
- 成功:返回一个非负整数,表示文件描述符。
- 失败:返回
1,并设置errno。
可以通过以下示例健壮地打开文件:
文件读写与关闭
C语言中可以使用
read 和 write 函数进行文件的读写;使用 close 函数关闭已打开的文件。短计数现象(Short Counts)
Short Counts指的是
read/write 返回的字节数小于请求值,可能由以下原因引起:- 读取时遇到
EOF(文件结尾)
- 从终端读取行数据,用户按下回车键时被截断
- 网络套接字读写
- 信号中断操作
但以下场景不会出现短计数:
- 常规磁盘文件读写(除非遇到
EOF)
元数据管理
元数据(metadata)是描述数据的数据,通常包括条目:device,inode,mode(whether protected),uid,gid(group),size。每个文件的元数据由操作系统内核维护。用户可以使用
stat/fstat 访问文件的metadata。文件信息获取
权限管理
Linux中,可以在通过命令行指令
chmod 修改文件权限:文件的权限通常由三个八进制数表示,规则如下:

755 即表示文件所有者的权限是7,也即4+2+1(可读可写可执行);所属组用户和其他用户的权限是5,也即4+1(可读可执行)。内核文件管理
Linux通过三级结构管理打开文件:
- 文件描述符表:每个进程独立维护,以文件描述符为索引,记录进程打开的所有文件(0/1/2为标准输入输出流)。每个条目指向该文件在文件表中的一个条目。
- 文件表(Open File Table):所有进程共用,每个条目记录该文件被描述符引用的次数(refcnt)、文件偏移量、引用计数等共享状态。每个条目还指向该文件在v-node表中的条目。
- v-node表:存储文件元数据(inode、权限等)
进程可以使用
dup / dup2 使不同的文件描述符指向同一个文件表条目,它们共享文件读写权限和偏移量(也即读写位置)。单个进程多次打开同一个文件,或者多个进程同时打开一个文件,可以使得多个文件表条目指向同一个文件,它们不共享读写权限和偏移量。进程
fork 时,子进程的描述符表与父进程一致,内核会将进程所有打开文件的文件表条目的refcnt加一。文件描述符表 (File Descriptor Table)
每个进程都维护一个文件描述符表,其中的每一项是一个文件描述符(一个小整数索引),指向一个文件表项。
文件表 (File Table)
文件表是系统范围的数据结构,用于存储有关打开文件的信息。每个文件表项包含与打开文件相关的状态,例如文件读/写偏移量、访问模式等。支持多个进程通过自己的文件描述符表共享同一个文件表项。
vnode(或 inode)
vnode 是虚拟节点(Virtual Node)的简称,是操作系统中文件系统的抽象层。vnode 表示文件系统中具体文件的元数据和底层信息,是对文件的统一抽象。
特性 | 多个文件描述符指向同一个文件表项 | 多个文件表项指向同一个 vnode |
共享范围 | 同一文件表项 | 同一文件的 vnode |
偏移量 | 共享 | 独立 |
文件元数据 | 通过文件表共享 | 通过 vnode 共享 |
访问的数据 | 同一文件数据 | 同一文件数据 |
进程间影响 | 偏移量改变会影响其他文件描述符 | 偏移量互不影响 |
典型场景 | dup()、fork() | 多个进程分别 open() 同一文件 |
I/O 重定向
在命令行中,可以使用
> 重定向输出,使用 < 重定向输入。例如,ls > file.txt 将 ls 命令的输出重定向到 file.txt 文件中。在C语言中,可以使用
dup2(a, b) 函数将文件描述符表中 fd = a 位置的条目复制到 fd = b 中,覆盖 fd = b 的内容(即将 b 重定向到 a)。例如,b = 1 时,标准输出流将被重定向到 a。标准I/O
标准输入输出函数
标准I/O函数包含在共享库
libc.so 中,函数名通常以 f 开头,例如 fopen/fclose、fread/fwrite、fgets/fputs、fscanf/fprintf(格式化输入输出)。这些函数将文件视为流(stream),并使用缓冲区来提高效率。在包含
stdio.h 头文件时,会定义三个类型为 FILE* 的全局变量,分别表示三个标准流:stdin:标准输入流
stdout:标准输出流
stderr:标准错误流
例如,使用
fprintf(stdout, "Hello World!") 将字符串写入 stdout 文件中,即使用标准输出流将内容打印在终端上。含缓冲的输入输出流(Buffered I/O)
动机:使用标准I/O进行读写操作时,每次调用系统调用(system call)都会涉及内核操作,耗时较长。如果每次只读写一个字符,时间代价将非常昂贵。
解决方案:通过使用缓冲区,每次读写一块较长的文本。当用户调用标准输入函数时,先从缓冲区读取数据;当缓冲区为空时,再调用内核进行实际的输入操作。
行缓冲(Line Buffer)是常见的缓冲方式:当输出遇到换行符
\n 时,缓冲区会被刷新(flush)。例如,以下代码:和
write(1, "Hello\n", 6) 等效,即一次性将 "Hello\n" 写入标准输出流。Robust I/O (RIO)
Robust I/O(RIO)实际上是对Unix I/O的封装,增加了错误处理功能,适用于网络服务器等项目。RIO包含两组函数:
- 无缓冲区的二进制数据输入输出:
rio_readn 和 rio_writen:与Unix的 read 和 write 接口相同,接收文件描述符 fd、缓冲区指针 buf 和字节数 n,返回实际传输的字节数。- 带缓冲区的文本/二进制数据输入输出:
rio_readlineb 和 rio_readnb:使用 rio_readinitb 来初始化或重置缓冲区。I/O 的比较
- Unix I/O:最通用、基础、低级的方式,提供了所有功能。
- 优点:只有Unix I/O能访问文件元数据;它是异步信号安全的,可以在信号处理函数中使用。
- 缺点:难以处理短计数(Short Counts),缓冲区容易出错。
- 标准I/O:
- 优点:使用缓冲区提高了效率;自动处理短计数。
- 缺点:不能访问文件元数据;不是异步信号安全的;对网络套接字不适用。
- 使用准则:
- 尽可能使用最高级的I/O函数,例如标准I/O。
- 处理磁盘、终端文件时,使用标准I/O。
- 在信号处理函数内部使用Unix I/O;当需要追求极致效率时,使用Unix I/O。
处理二进制文件时,不能使用以下函数:
- 基于文本的I/O函数:
fgets、scanf、rio_readlineb。
- 字符串函数:
strlen、strcpy、strcat。
原因是这些函数容易被
0 或 EOF 干扰。处理二进制文件时,应使用 rio_readn 或 rio_readnb。补充
系统I/O库中的缓冲区类型包括:文本缓冲区(text buffer)、行缓冲区(line buffer)和无缓冲区(no buffer)。
printf 和 scanf 使用行缓冲区;fread 和 fwrite 使用文本缓冲区。进程
fork 时会将缓冲区一并复制。为了避免这种情况,可以在 printf 后加上 fflush 清空缓冲区。Prev
ICS09 虚拟内存
Next
ICS11 网络编程
Loading...