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...