课程内容#
理解两者的真正含义及区别,能回答以上问题
两者的字面意思#
上班
- 阻塞:上班时堵车了,等或者使用其他方法,总之要去上班
- 非阻塞:上班堵车了,直接不去了
老妈让买酱油
- 阻塞:没有酱油了,问妈要不换别的,或者换别的买
- 非阻塞:没有酱油了,不买了
让小明写报告
- 阻塞:小明写的时候,等他写完
- 非阻塞:不用干等小明写,之后再告知结果 [自己问小明 - 同步 / 小明主动告诉我 - 异步]
烧开水
- 阻塞:烧开水的过程中,你不能干其他事情,只能站那等水开
- 非阻塞:烧开水的过程中,你可以干其他事情,比如去客厅看看电视
- 参考同步异步、阻塞非阻塞极简解释——CSDN
引入#
- 回顾 open 函数中的 O_NONBLOCK
- 一切皆文件,所以一切都可以阻塞 / 非阻塞
- 分析写文件的过程
- 打开→write→关闭
- 其中,write 属于系统调用,具体过程如下
- 数据→内核→磁盘:内核拷贝数据,放在缓冲区 [块缓冲],清刷缓冲时,调度 IO 设备,找到 inode 和 block,将数据写入磁盘
- 第一步数据→内核是用户态,第二步内核→磁盘是内核态,存在转换
- 参考怎样去理解 Linux 用户态和内核态?—— 知乎
- NON_BLOCK 设置的是用户态 [数据→内核 或者 内核→数据] 的过程
- 大部分的阻塞是在从内核拿数据出来
- 内核→磁盘的过程实际是阻塞的
- 为了方便上层应用快速返回写的状态,内核将数据写到磁盘的过程,普通用户不需要感知
- 内核拷贝完数据,程序返回,普通用户就认为写成功了,但数据很有可能还在缓冲区 [缓冲 IO]
- 数据→内核→磁盘:内核拷贝数据,放在缓冲区 [块缓冲],清刷缓冲时,调度 IO 设备,找到 inode 和 block,将数据写入磁盘
fcntl#
操作文件描述符 [可以将文件变成非阻塞的]
- man fcntl
- 原型
- fd:文件描述符。最常见的文件描述符:0、1、2
- cmd:操作方式
- ... /* arg */
- 可变参数
- arg,指示该参数是前面参数 [cmd] 的参数,其含义取决于 cmd
- 描述
- cmd 在圆括号里指示是否有可变参数,最多一个
- 可变参数类型一般是 int,使用用宏定义,本质是位掩码,通过位运算改变状态
- 其中有一类 cmd:与【文件状态标志】相关
- 即可获得或设置文件状态 [如 O_NONBLOCK]
- 返回值
- 观察返回值,考虑程序中的判断
- 出错都是返回 - 1,设置操作成功都是返回 0
select#
同步 I/O 多路复用 [接口]
- man select
- 原型
- nfds:文件描述符的数量
- fd_set:文件描述符的集合
- 可读、可写、异常
- 底层是用数组是实现的
- timeout:时间间隔
- 四个宏用来操作集合,见后
- 描述
- 允许程序去监控多个文件描述符,等待一个或多个文件描述符的 I/O 操作 "ready" 的情况
- ready:就绪,可以对文件进行相应的 IO 操作,如没有阻塞的读,或足够小的写
- 可监控的文件描述符数量小于 FD_SETSIZE [一般为 1024]
- 退出的时候,每个文件描述符集合会被修改,只留下状态发生变化的文件描述符,起指示作用
- 所以,如果循环使用 select,每次调用 select 前需要重新初始化每个集合
- 集合可以是 NULL,说明该类事件中没有文件被监控
- 宏用来操作集合
- FD_ZERO:清空一个集合
- FD_SET:向一个集合添加一个文件描述符
- FD_CLR:从一个集合移除一个文件描述符
- FD_ISSET:判断一个文件描述符的所属集合,可用于 select 返回后,因为集合已被修改
- nfds 的值是三个集合中数字最大的文件描述符加一
- 文件描述符的索引是从 0 开始的
- struct timeval timeout
- 指定 select 阻塞着等待每个文件描述符就绪的时间间隔
- 这里阻塞的含义就是单纯的阻塞,不是阻塞 I/O 的阻塞
- [个人理解] 同步 I/O 的多路复用的同步就体现在这
- 三种停止阻塞的情况
- 一个文件描述符就绪
- 信号中断 [kill]
- 超时
- 时间间隔不是精确的,很难做到真正的精确
- 系统时钟粒度、内核调度延迟
- timeval 结构体中有两个成员:秒、微秒
- 如果两者都为 0,就会立即返回,可用于轮询 [polling]
- 如果为 NULL,就会无期限等待
- timeout 更新功能只在 Linux 上生效
- 为了兼容性,尽量别用,尽量使用比较公共的功能
- 指定 select 阻塞着等待每个文件描述符就绪的时间间隔
- 返回值
- 返回此时所有集合中文件描述符 [ready] 数量
- 根据数量,再通过宏定义 FD_ISSET 询问所有文件描述符的所在集合,来感知状态变化
- 0:时间过完了,也没有感兴趣的事件发生
- -1:error,并设置 errno;此时集合不会改变,timeout 变成未定义的了
[PS]
- select → poll → epoll,越来越高级
- man poll
- man epoll
- 均与 select 做的任务相似
代码演示#
实现使文件非阻塞和阻塞的接口#
- common.h
- 两个接口
- head.h
- 头文件的顺序是有讲究的,一般把自己本项目的头文件放后面
- 参考#include 的路径及顺序——Google C++ 编程风格
- common.c
- 设置 flag 时不能改变原有的 flag
- 👉先 get,再通过位或 / 位与来添加 / 删除 flag
非阻塞的 0 号文件#
- 常用文件 0、1、2 不用手动打开
- 创建进程时自动打开这三个文件
- 它们是继承过来的
- 编译命令:
gcc 1.test.c ../common/common.c -I ../common/
- 编译记得添加 common.c 文件
- 0 号文件设置成非阻塞后,关键变化在于 0 号文件的读写变成了非阻塞的
- ① 没有 sleep
- scanf 就是读 0 号文件
- 但没有数据,直接走,不会阻塞着等 0 号文件有数据
- ② 有 sleep
- 在 sleep 的 5 秒里,在终端输入数据 [+ 回车,行缓冲]
- 进程暂停,不占用 CPU
- 但标准输入流仍然打开着,用户在终端输入的数据会由内核传给 0 号文件
- 文件是内核维护的,sleep 的过程内核可以给文件写数据
- 在 sleep 结束后,scanf 读走 0 号文件的数据
- 【明确】非阻塞是指某个对象,比如 0 号文件;而不是某个函数、某个操作
- ① 没有 sleep
- [PS]
- 阻塞:可能浪费时间
- 非阻塞:可能浪费资源
- 一般用于大型的程序、高并发服务器
对 0 号文件的 select#
- 拷贝自 man 手册 ——man select 里的 EXAMPLE
- select 可以感知到 I/O 的到来
- 运行效果
- 终端输入 ls,程序结束后还会执行 ls
- 因为缓冲区的内容没有被取走 [如 scanf],最终被 zsh 接走了
- select 在这监控的时间间隔里发生了阻塞
- 在所有的用户层面的程序里面,所谓的阻塞就是睡觉 [sleep ()]
- 阻塞终止条件:ready、signal 中断、超时
- [延伸应用]
- 给阻塞的地方设置一个定时,超时则使用默认值
- 避免异常情况阻塞过久 [SSH 主机不可达...]
- 更人性化
- 给阻塞的地方设置一个定时,超时则使用默认值
附加知识点#
- I/O 感知的方式可以有很多种,其中内核完全知道 IO 的到来
思考点#
- scanf 的时候,在终端输入了回车,是将数据放入了缓冲区,还是清空缓冲区给了 scanf 呢?
Tips#
- 课程预告:多进程 [fork...]
- 可以考虑 cp 的实现
- 考察文件的读写操作
- 必须阻塞