• 首页
  • 搜索
  • 工具
  • 分类
  • 日志
  • 友链
  • 图片

It's Geek KingYoungy

KEEP CHALLENGE
系统编程基础

对底层IO的深度总结

2025-03-03 浏览量 127 评论数 1

对底层IO的深度总结

前言

本文介绍 stdio 库函数与系统调用 read/write 的核心行为,重点解析缓冲机制对 I/O 操作的影响。


一、read 系统调用

功能:原子地从内核缓冲区(内存)读取数据块到用户缓冲区(内存)。原子——多进程同时调用read也会保证每个进程读取到的数据块是完整的。

具体表格:

I/O类型是否启用O_NONBLOCK数据/状态条件read行为
标准输入否输入未就绪(如终端无输入)阻塞,直到输入数据或EOF
标准输入是输入未就绪立即返回EAGAIN错误
标准输入无所谓输入已就绪(用户键入回车读取实际输入字节数(≤请求字节数)
标准输入无所谓输入结束(如Ctrl+D)返回0(EOF)
普通文件无所谓文件未结束读取请求的字节数(除非文件剩余字节不足,返回实际剩余字节数)
普通文件无所谓已到文件结尾返回0(EOF)
管道/FIFO否管道中无数据,但写入端未关闭阻塞,直到有数据写入
管道/FIFO否管道中无数据,且写入端已关闭返回0(EOF)
管道/FIFO否管道中有数据(≤请求字节数)读取实际可用字节数
管道/FIFO是管道中无数据,但写入端未关闭立即返回EAGAIN错误
管道/FIFO是管道中有数据(≤请求字节数)读取实际可用字节数
套接字否无数据到达(面向连接,如TCP)阻塞,直到数据到达或连接关闭
套接字否连接关闭(如TCP收到FIN)返回0(EOF)
套接字否数据到达(≤请求字节数)读取实际到达字节数
套接字是无数据到达立即返回EAGAIN错误
套接字是数据到达(≤请求字节数)读取实际到达字节数

总结
read 何时阻塞:

  • 1.未设置O_NONBLOCK,
  • 2.对象是支持阻塞的设备(终端设备、套接字、管道等)而非普通文件,
  • 3.此时内核缓冲区恰好没有数据。

阻塞需同时满足上述三种情况。可以理解为read会等待可能提供数据的设备。其余情况,内核缓冲区一旦有数据块被write写入就一次性搬走并返回数据量,没读到就返回0(EOF/设备关闭)或-1(发生错误)。


二、write 系统调用

功能:原子地将用户缓冲区(内存)的数据块写入内核缓冲区(内存)。原子——多进程同时调用write也能保证每个进程写的数据块是独立的。

对象类型是否启用 O_NONBLOCK行为描述
标准输出未启用如果标准输出是终端设备,write 会阻塞,直到数据被读取或终端缓冲区满。
启用如果标准输出是终端设备,write 会立即返回。如果终端缓冲区满,返回 -1 并设置 errno 为 EAGAIN。
普通文件未启用write 会立即将数据写入文件,不会阻塞。
启用write 会立即将数据写入文件,不会阻塞。
管道/FIFO未启用如果写入字节数 n ≤ PIPE_BUF,write 会原子地写入n字节;如果 n > PIPE_BUF,write不能保证原子性。
启用如果写入字节数 n ≤ PIPE_BUF,write 会原子地写入n字节;如果 n > PIPE_BUF,返回 -1 并设置 errno 为 EAGAIN。
套接字未启用如果套接字缓冲区有足够空间,write 会立即写入;否则会阻塞,直到有足够空间。
启用如果套接字缓冲区有足够空间,write 会立即写入;否则返回 -1 并设置 errno 为 EAGAIN。
读取端关闭未启用如果写入对象是管道、FIFO 或套接字,且读取端关闭,write 会返回 -1 并设置 errno 为 EPIPE,同时可能会产生 SIGPIPE 信号。
启用如果写入对象是管道、FIFO 或套接字,且读取端关闭,write 会返回 -1 并设置 errno 为 EPIPE,同时可能会产生 SIGPIPE 信号。

关键点:

  • write 返回仅表示数据块已提交给内核,不保证已写入磁盘。
  • 阻塞模式下,若内核缓冲区满,write 会阻塞;非阻塞模式直接返回 EAGAIN。

三、stdio 库的缓冲机制

1. 输入类函数(如 scanf)

  • 何时调用 read:当用户缓冲区无足够数据时触发 read。
  • 缓冲影响:

    • 若有用户缓冲区:read 填充库缓冲区,数据按需提取(如 scanf 遇空格/换行停止)。
    • 若无缓冲区(_IONBF):read 直接填充用户指定内存。

示例对比:

// 例1:关闭缓冲区(_IONBF)
setvbuf(stdin, NULL, _IONBF, 0);
scanf("%s", string);  // read直接填充string,"world"留在内核缓冲区
// 例2:启用缓冲区(默认)
scanf("%s", string);  // read填充库缓冲区,"world"保留在库缓冲区中

2. 输出类函数(如 printf)

  • 何时调用 write:由缓冲模式决定:

    缓冲模式触发条件
    无缓冲(_IONBF)立即调用 write
    行缓冲(_IOLBF)遇到换行符或缓冲区满
    全缓冲(_IOFBF)缓冲区满或手动刷新(fflush)

示例对比:

// 行缓冲(默认)
printf("I'm A.\n");  // 遇换行符立即调用write
write(STDOUT_FILENO, "I'm B.\n", 7);  // 直接输出
// 输出顺序:A → B
// 全缓冲(_IOFBF)
printf("I'm A.\n");  // 缓冲区未满,不调用write
write(STDOUT_FILENO, "I'm B.\n", 7);  // 直接输出
// 输出顺序:B → A(程序退出时刷新缓冲区)

四、close的机制

对于进程打开一个文件而言,有三个关联的数据结构:进程层面的文件描述符表,内核层面的打开文件表,文件系统层面的inode表。
进程层面的文件描述符表记录了fd与fd指向的内核的打开文件表的文件描述(句柄);
内核层面的打开文件表的每个表项(即文件句柄)记录了当前文件IO操作的偏移量,以及文件在文件系统inode表中的位置(下标);
文件系统层面的inode表记录了每个文件的元数据,以及记录锁(系统调用fcntl()维护的POSIX锁)。

close(int fd)的作用就是断开文件描述符fd与文件描述(内核维护的打开文件表的表项)的指向关系,并在进程层面的文件描述符表删除这一表项。而内核层面的打开文件表的表项只有没有任何进程的文件描述符指向它的时候才会被删除。


总结

  • read/write 就是一个搬运者,可阻塞,也可立即返回。
  • stdio 封装了read与write,为用户进程建立缓冲区:

    • 输入类函数:缓冲决定数据暂存位置(库缓冲区或用户内存)。
    • 输出类函数:缓冲决定 write 的触发条件(换行符、缓冲区满或无缓冲)。
shell作业控制的两个问题:组长叛变与SIGHUP信号
C语言可变参数与命令行参数解析:stdarg与getopt详解

评论

  1. kingyoungy kingyoungy
    Chrome 133 Windows

    read与write在IO时写入与读取的数据量可以不同,此时内核缓冲区就像是水流一样,数据之间没有分隔,这被称为字节流。相比之下,数据包或消息就不能用read和write了。

    2025-03-06 13:29
  • 文章目录
  • 站点概览
    author

    38 日志
    7 分类
    Creative Commons
    • 热门文章
    • 热评文章
    • 随机文章
    • 在 Debian 服务器上部署 FileBrowser 并集成到现有博客路径
    • 高等数学重要定义整理
    • 高等数学重要定理总结
    • C语言原子量的使用
    • 库、链接与执行
    • 欢迎使用 Typecho
    • 对底层IO的深度总结
    • 数据结构——树
    • 库、链接与执行
    • shell作业控制的两个问题:组长叛变与SIGHUP信号
    • 数据结构——串
    • 实现安卓与类安卓系统的定位信息伪造
    • UNIX 98伪终端核心系统调用及函数封装
    • 管道与FIFO的技术解析及系统调用详解
    • Unix Domain Socket 编程:字节流与数据报套接字详解

    浏览量 : 5419

    © 2025 It's Geek KingYoungy. Power By Typecho . Theme by Shiyi

    浙ICP备2025160639号  |  浙公网安备33020502001222号

    This is just a placeholder img.