
Linux中的IO模型
文件与IO
“Linux 一切皆文件”,Linux中,硬件设备、进程、网络连接、管道和普通文件一样,都可以用文件描述符(fd, file descriptor)来访问。
因此可以统一使用系统调用中的I/O函数(如open()、close()、write()、read()等)来对这些”文件”进行相操作
- 当你通过系统调用open、socket、pipe等来访问一个资源时,会为该资源分配一个唯一标识的fd(fd中当前最小未使用的整数),并为该fd分配内核缓冲区。后续可通过这个fd来操作相应资源
一般的IO流程(2阶段 :数据准备,数据拷贝)
io的分类,可根据io流程的1阶段是否等待资源分为 阻塞/非阻塞,2阶段是否等待拷贝分为 同步/异步
- read(fd, buffer, size) (buffer为用户的缓冲区,size为最大读取字节数)
- 调用时,发生系统调用,切换到内核态
- 检测内核缓冲区是否存在fd对应资源,不存在则阻塞等待,直到准备好后被唤醒(数据写好后会检测是否有等待这个fd的线程)
- 将内核缓冲区数据拷贝到用户缓冲区(同步等待这个过程)
- 切换为用户态,并返回读取的字节数
- write(fd, buffer, size)
- 系统调用切换到内核态
- 内核缓冲区若空间不够,阻塞等待
- 将用户缓冲区数据拷贝到fd对应的内核缓冲区,后续物理写入操作由内核异步完成
- 切换为用户态,并返回写入的字节数
Linux的五种IO模型
阻塞I/O(Blocking I/O)
即上述的io流程,易于使用但是全过程阻塞
非阻塞I/O模型
将read/write等设置为非阻塞,当我们调用时,如果内核缓冲区没有准备好,会直接返回错误。
虽然没有阻塞,但是代价是什么呢?线程可以做其他事,但是终归要轮询内核缓冲区是否准备好,浪费cpu资源。因此一般不单独使用
I/O多路复用
基于非阻塞I/O的基础上,由一个线程监视多个fd的IO事件(对fd们轮询或注册回调函数等来确认事件发生),实现方法有3种
select
- 流程
- 通过位图(bitmap)来管理fd,fd的值对应其比特位(缺点是,位图大小有限(限制为1024),因此过多fd则无法管理)
- 位图可分为3种,分别用于监听可读(socket连接accept,read读数据等)、可写、异常事件
- 每次调用select方法时,将位图拷贝到内核空间,轮询位图检测是否存在fd就绪事件:存在则把未就绪的fd位置0并返回,否则阻塞休眠等待数据就绪
- 当存在io事件把线程唤醒,或者超时唤醒,则再轮询一遍位图(因为不知道哪个fd就绪唤醒它)并标记返回
- 将修改后的位图拷贝到用户空间,返回就绪的FD数量并切换为用户态
- 线程遍历位图对就绪事件处理,然后开启新一轮select继续监听
- 常见的事件处理策略是,accept连接请求当前线程直接处理,并管理accept生成的新socket
- 其余读写io分发给其他线程去处理
- 通过位图(bitmap)来管理fd,fd的值对应其比特位(缺点是,位图大小有限(限制为1024),因此过多fd则无法管理)
- 缺点
- 前后需要2次拷贝位图
- 检测就绪事件需遍历所有fd
- 可监听fd数量受限(位图大小)
poll(基于select简单改进)
- 通过pollfd这个结构体来封装fd(fd值,监听事件值,实际发生事件值),用pollfd数组来管理fd
- 事件类型有多种(POLLIN、POLLOUT、POLLERR等),各占一个bit位,可以同时监听多种事件(如POLLIN | POLLOUT)
- poll函数步骤类似于select
- 缺点 :解决了fd监听数量有限的问题,但是拷贝和遍历整个数组的问题仍然存在 ##### epoll
- epoll_create方法 :提供了一个eventpoll实例用于管理fd
- 实例在内核创建和维护,避免了拷贝问题
- epoll_create返回一个epfd,用于用户与epoll实例交互
- eventpoll结构
- rbr红黑树 :用于管理fd
- Ready List :存储就绪fd
- Wait Queue :管理调用epoll_wait时被阻塞的线程
- epoll_ctl方法 :向 epoll 实例的红黑树注册fd(也可删除/修改);内核同时为该fd注册一个回调函数(用于事件就绪时将fd移动或复制到就绪列表)
- 水平触发(Level-Triggered,LT):当可读/写事件就绪,且数据未被读完/写入时。内核会不断通知应用程序(即每次调用epoll_wait都有就绪事件)
- 边缘触发(Edge-Triggered,ET):事件就绪时,只通知一次,需保证一次性读写完毕。只有新事件就绪,再次调用epoll_wait才会被唤醒
- epoll_wait方法 :获取就绪fd
- 调用后如果就绪列表为空,则线程被阻塞并添加到epoll的Wait Queue
- 当就绪列表不为空,唤醒Wait Queue的线程,并把就绪列表的fd拷贝到用户空间,返回就绪fd数量
信号驱动式I/O
线程发起I/O操作时,向内核注册一个信号处理函数(如 SIGIO)并返回。内核数据就绪时调用回调函数向线程发送一个信号,线程收到信号后即可执行io操作(该操作仍会阻塞) 缺点 :实现较为复杂,且每个事件都需要用户内核直接交互,性能低
异步I/O
异步io也是通过注册信号回调函数的方式实现,内核在数据就绪并把数据拷贝到用户空间后,才会通知用户线程(即全程无阻塞)
参考
【linux内核】五大经典IO模型(原理+动图+代码详解)
Linux 五种 IO 模型和三种多路复用技术select、poll、epoll详解 — title: Linux中的IO模型 date: 2025-08-28 03:04:27 mathjax: true categories: - CS基础 tags: - Linux cover: https://gcore.jsdelivr.net/gh/WQhuanm/Img_repo_1@main/img/202412222015910.png —
文件与IO
“Linux 一切皆文件”,Linux中,硬件设备、进程、网络连接、管道和普通文件一样,都可以用文件描述符(fd, file descriptor)来访问。
因此可以统一使用系统调用中的I/O函数(如open()、close()、write()、read()等)来对这些”文件”进行相操作
- 当你通过系统调用open、socket、pipe等来访问一个资源时,会为该资源分配一个唯一标识的fd(fd中当前最小未使用的整数),并为该fd分配内核缓冲区。后续可通过这个fd来操作相应资源
一般的IO流程(2阶段 :数据准备,数据拷贝)
io的分类,可根据io流程的1阶段是否等待资源分为 阻塞/非阻塞,2阶段是否等待拷贝分为 同步/异步
- read(fd, buffer, size) (buffer为用户的缓冲区,size为最大读取字节数)
- 调用时,发生系统调用,切换到内核态
- 检测内核缓冲区是否存在fd对应资源,不存在则阻塞等待,直到准备好后被唤醒(数据写好后会检测是否有等待这个fd的线程)
- 将内核缓冲区数据拷贝到用户缓冲区(同步等待这个过程)
- 切换为用户态,并返回读取的字节数
- write(fd, buffer, size)
- 系统调用切换到内核态
- 内核缓冲区若空间不够,阻塞等待
- 将用户缓冲区数据拷贝到fd对应的内核缓冲区,后续物理写入操作由内核异步完成
- 切换为用户态,并返回写入的字节数
Linux的五种IO模型
阻塞I/O(Blocking I/O)
即上述的io流程,易于使用但是全过程阻塞
非阻塞I/O模型
将read/write等设置为非阻塞,当我们调用时,如果内核缓冲区没有准备好,会直接返回错误。
虽然没有阻塞,但是代价是什么呢?线程可以做其他事,但是终归要轮询内核缓冲区是否准备好,浪费cpu资源。因此一般不单独使用
I/O多路复用
基于非阻塞I/O的基础上,由一个线程监视多个fd的IO事件(对fd们轮询或注册回调函数等来确认事件发生),实现方法有3种
select
- 流程
- 通过位图(bitmap)来管理fd,fd的值对应其比特位(缺点是,位图大小有限(限制为1024),因此过多fd则无法管理)
- 位图可分为3种,分别用于监听可读(socket连接accept,read读数据等)、可写、异常事件
- 每次调用select方法时,将位图拷贝到内核空间,轮询位图检测是否存在fd就绪事件:存在则把未就绪的fd位置0并返回,否则阻塞休眠等待数据就绪
- 当存在io事件把线程唤醒,或者超时唤醒,则再轮询一遍位图(因为不知道哪个fd就绪唤醒它)并标记返回
- 将修改后的位图拷贝到用户空间,返回就绪的FD数量并切换为用户态
- 线程遍历位图对就绪事件处理,然后开启新一轮select继续监听
- 常见的事件处理策略是,accept连接请求当前线程直接处理,并管理accept生成的新socket
- 其余读写io分发给其他线程去处理
- 通过位图(bitmap)来管理fd,fd的值对应其比特位(缺点是,位图大小有限(限制为1024),因此过多fd则无法管理)
- 缺点
- 前后需要2次拷贝位图
- 检测就绪事件需遍历所有fd
- 可监听fd数量受限(位图大小)
poll(基于select简单改进)
- 通过pollfd这个结构体来封装fd(fd值,监听事件值,实际发生事件值),用pollfd数组来管理fd
- 事件类型有多种(POLLIN、POLLOUT、POLLERR等),各占一个bit位,可以同时监听多种事件(如POLLIN | POLLOUT)
- poll函数步骤类似于select
- 缺点 :解决了fd监听数量有限的问题,但是拷贝和遍历整个数组的问题仍然存在 ##### epoll
- epoll_create方法 :提供了一个eventpoll实例用于管理fd
- 实例在内核创建和维护,避免了拷贝问题
- epoll_create返回一个epfd,用于用户与epoll实例交互
- eventpoll结构
- rbr红黑树 :用于管理fd
- Ready List :存储就绪fd
- Wait Queue :管理调用epoll_wait时被阻塞的线程
- epoll_ctl方法 :向 epoll 实例的红黑树注册fd(也可删除/修改);内核同时为该fd注册一个回调函数(用于事件就绪时将fd移动或复制到就绪列表)
- 水平触发(Level-Triggered,LT):当可读/写事件就绪,且数据未被读完/写入时。内核会不断通知应用程序(即每次调用epoll_wait都有就绪事件)
- 边缘触发(Edge-Triggered,ET):事件就绪时,只通知一次,需保证一次性读写完毕。只有新事件就绪,再次调用epoll_wait才会被唤醒
- epoll_wait方法 :获取就绪fd
- 调用后如果就绪列表为空,则线程被阻塞并添加到epoll的Wait Queue
- 当就绪列表不为空,唤醒Wait Queue的线程,并把就绪列表的fd拷贝到用户空间,返回就绪fd数量
信号驱动式I/O
线程发起I/O操作时,向内核注册一个信号处理函数(如 SIGIO)并返回。内核数据就绪时调用回调函数向线程发送一个信号,线程收到信号后即可执行io操作(该操作仍会阻塞) 缺点 :实现较为复杂,且每个事件都需要用户内核直接交互,性能低
异步I/O
异步io也是通过注册信号回调函数的方式实现,内核在数据就绪并把数据拷贝到用户空间后,才会通知用户线程(即全程无阻塞)
参考
【linux内核】五大经典IO模型(原理+动图+代码详解)
Linux 五种 IO 模型和三种多路复用技术select、poll、epoll详解 — title: Linux中的IO模型 date: 2025-08-28 03:04:27 mathjax: true categories: - CS基础 tags: - Linux cover: https://gcore.jsdelivr.net/gh/WQhuanm/Img_repo_1@main/img/202412222015910.png —
文件与IO
“Linux 一切皆文件”,Linux中,硬件设备、进程、网络连接、管道和普通文件一样,都可以用文件描述符(fd, file descriptor)来访问。
因此可以统一使用系统调用中的I/O函数(如open()、close()、write()、read()等)来对这些”文件”进行相操作
- 当你通过系统调用open、socket、pipe等来访问一个资源时,会为该资源分配一个唯一标识的fd(fd中当前最小未使用的整数),并为该fd分配内核缓冲区。后续可通过这个fd来操作相应资源
一般的IO流程(2阶段 :数据准备,数据拷贝)
io的分类,可根据io流程的1阶段是否等待资源分为 阻塞/非阻塞,2阶段是否等待拷贝分为 同步/异步
- read(fd, buffer, size) (buffer为用户的缓冲区,size为最大读取字节数)
- 调用时,发生系统调用,切换到内核态
- 检测内核缓冲区是否存在fd对应资源,不存在则阻塞等待,直到准备好后被唤醒(数据写好后会检测是否有等待这个fd的线程)
- 将内核缓冲区数据拷贝到用户缓冲区(同步等待这个过程)
- 切换为用户态,并返回读取的字节数
- write(fd, buffer, size)
- 系统调用切换到内核态
- 内核缓冲区若空间不够,阻塞等待
- 将用户缓冲区数据拷贝到fd对应的内核缓冲区,后续物理写入操作由内核异步完成
- 切换为用户态,并返回写入的字节数
Linux的五种IO模型
阻塞I/O(Blocking I/O)
即上述的io流程,易于使用但是全过程阻塞
非阻塞I/O模型
将read/write等设置为非阻塞,当我们调用时,如果内核缓冲区没有准备好,会直接返回错误。
虽然没有阻塞,但是代价是什么呢?线程可以做其他事,但是终归要轮询内核缓冲区是否准备好,浪费cpu资源。因此一般不单独使用
I/O多路复用
基于非阻塞I/O的基础上,由一个线程监视多个fd的IO事件(对fd们轮询或注册回调函数等来确认事件发生),实现方法有3种
select
- 流程
- 通过位图(bitmap)来管理fd,fd的值对应其比特位(缺点是,位图大小有限(限制为1024),因此过多fd则无法管理)
- 位图可分为3种,分别用于监听可读(socket连接accept,read读数据等)、可写、异常事件
- 每次调用select方法时,将位图拷贝到内核空间,轮询位图检测是否存在fd就绪事件:存在则把未就绪的fd位置0并返回,否则阻塞休眠等待数据就绪
- 当存在io事件把线程唤醒,或者超时唤醒,则再轮询一遍位图(因为不知道哪个fd就绪唤醒它)并标记返回
- 将修改后的位图拷贝到用户空间,返回就绪的FD数量并切换为用户态
- 线程遍历位图对就绪事件处理,然后开启新一轮select继续监听
- 常见的事件处理策略是,accept连接请求当前线程直接处理,并管理accept生成的新socket
- 其余读写io分发给其他线程去处理
- 通过位图(bitmap)来管理fd,fd的值对应其比特位(缺点是,位图大小有限(限制为1024),因此过多fd则无法管理)
- 缺点
- 前后需要2次拷贝位图
- 检测就绪事件需遍历所有fd
- 可监听fd数量受限(位图大小)
poll(基于select简单改进)
- 通过pollfd这个结构体来封装fd(fd值,监听事件值,实际发生事件值),用pollfd数组来管理fd
- 事件类型有多种(POLLIN、POLLOUT、POLLERR等),各占一个bit位,可以同时监听多种事件(如POLLIN | POLLOUT)
- poll函数步骤类似于select
- 缺点 :解决了fd监听数量有限的问题,但是拷贝和遍历整个数组的问题仍然存在 ##### epoll
- epoll_create方法 :提供了一个eventpoll实例用于管理fd
- 实例在内核创建和维护,避免了拷贝问题
- epoll_create返回一个epfd,用于用户与epoll实例交互
- eventpoll结构
- rbr红黑树 :用于管理fd
- Ready List :存储就绪fd
- Wait Queue :管理调用epoll_wait时被阻塞的线程
- epoll_ctl方法 :向 epoll 实例的红黑树注册fd(也可删除/修改);内核同时为该fd注册一个回调函数(用于事件就绪时将fd移动或复制到就绪列表)
- 水平触发(Level-Triggered,LT):当可读/写事件就绪,且数据未被读完/写入时。内核会不断通知应用程序(即每次调用epoll_wait都有就绪事件)
- 边缘触发(Edge-Triggered,ET):事件就绪时,只通知一次,需保证一次性读写完毕。只有新事件就绪,再次调用epoll_wait才会被唤醒
- epoll_wait方法 :获取就绪fd
- 调用后如果就绪列表为空,则线程被阻塞并添加到epoll的Wait Queue
- 当就绪列表不为空,唤醒Wait Queue的线程返回就绪fd数量
- 此处利用了mmap技术(内核和用户空间共享内存区域:各自的虚拟地址映射到同一片物理地址)避免了就绪列表到用户空间的拷贝
信号驱动式I/O
线程发起I/O操作时,向内核注册一个信号处理函数(如 SIGIO)并返回。内核数据就绪时调用回调函数向线程发送一个信号,线程收到信号后即可执行io操作(该操作仍会阻塞) 缺点 :实现较为复杂,且每个事件都需要用户内核直接交互,性能低
异步I/O
异步io也是通过注册信号回调函数的方式实现,内核在数据就绪并把数据拷贝到用户空间后,才会通知用户线程(即全程无阻塞)
参考
【linux内核】五大经典IO模型(原理+动图+代码详解)
Linux 五种 IO 模型和三种多路复用技术select、poll、epoll详解