首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Linux多种IO模型

Linux多种IO模型

原创
作者头像
Eulogy
发布2025-08-14 00:26:06
发布2025-08-14 00:26:06
11700
代码可运行
举报
文章被收录于专栏:笔记本笔记本
运行总次数:0
代码可运行

Linux多种IO模型

阻塞IO

阻塞IO模型中,用户进程在内核等待网卡数据和内核数据拷贝到用户缓冲区两个阶段都处于等待状态。

非阻塞IO

在非阻塞IO模式下,用户应用执行recvfrom系统调用命令操作时会立即返回结果而不是阻塞用户进程等待命令执行完毕。虽然非阻塞IO不会让用户进程进入阻塞状态,但是性能也并不会很高,因为它会让该进程进入忙等状态,不断地占用CPU去调用recvfrom命令,导致CPU使用率暴增。同时,第二阶段的从内核缓冲区中拷贝数据到用户缓冲区依旧会阻塞等待。

IO多路复用

无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案:

  • 如果调用recvfrom时,恰好 没有 数据,阻塞IO会使进程阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU的作用。
  • 如果调用recvfrom时,恰好 数据,则用户进程可以直接进入第二阶段,读取并处理数据。

但是在单线程情况下,一个线程同时只能处理一个socket请求。当这个socket请求处于等待数据就绪的状态下,所有其它客户端的socket都必须等待,这种服务器的性能自然就很差。

提高效率的方法有哪些?

方案一: 增加多个线程,服务端一共可以有多少个线程,那么就可以同时处理多少个客户端socket请求。

方案二: socket不排队,哪个数据就绪,服务端就去处理哪个数据对应的socket请求。

文件描述符(File Descriptor): 简称FD,是一个从0开始递增的无符号整数,用来 关联 Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。

而IO多路复用就是利用 单个线程 去同时监听多个FD(Socket请求),当某个FD可读、可写的时候这个线程就会得到通知,然后去进行后续的处理,这样可以避免浪费CPU资源。

监听FD的方式、通知的方式又有多种实现,常见的有:select、poll、epoll。

select

代码语言:c
代码运行次数:0
运行
复制
// 定义类型别名 __fd_mask,本质是 long int
typedef long int __fd_mask;

/* fd_set 记录要监听的fd集合,及其对应状态 */
typedef struct {
    // fds_bits是long类型数组,长度为 1024 / 32 = 32
    // long int 是32位的,这个数组一共32个元素,每个元素32个bit,所以一共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪。可以监听1024个fd。
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;

// select函数,用于监听多个fd的集合
int select(
    int nfds,                // 要监视的fd_set的最大fd + 1
    fd_set *readfds,         // 要监听读事件的fd集合
    fd_set *writefds,        // 要监听写事件的fd集合
    fd_set *exceptfds,       // 要监听异常事件的fd集合
    struct timeval *timeout  // 超时时间,null-永不超时;0-不阻塞等待;大于0-固定等待时间
);

1.调用 select()

用户空间传入三个 fd_set(可读、可写、异常)和超时时间 timeout。这些 fd_set 会被拷贝到内核空间(因为内核需要监控这些 fd)。

2.内核等待就绪事件

内核会遍历 fd_set 里的所有文件描述符,注册到等待队列中(网络情况下就是 socket 对应的等待队列)。如果所有描述符当前都没就绪,当前线程会阻塞,直到:

  • 某个 fd 就绪(读/写/异常);
  • 超时 timeout 到达;
  • 收到信号(导致提前返回 EINTR)。

3.就绪唤醒

当某个 fd 的底层设备(例如 socket)在内核中被标记为可读/可写/异常时,内核会唤醒 select() 阻塞的线程。此时,内核会再次遍历全部 fd,找出哪些真的就绪(因为可能唤醒时多个 fd 同时可用)。

4.结果拷贝回用户空间

内核会把已就绪的 fd 信息更新到传入的 fd_set 中,并把这个结构拷贝回用户空间。

注意:返回值是就绪的 fd 数量,而不是具体 fd。

5.用户空间遍历

用户程序在自己的 fd_set 中遍历,找出已就绪的 fd,然后调用 read() 或 write() 去处理数据。

select存在的问题:

  • select 需要将 fd_set 从用户空间拷贝到内核空间,select 结束还要再次拷贝回用户空间。
  • select 所能监视的文件描述符个数被限制,需要遍历整个 fd_set。
  • select 无法得知具体哪个 fd 就绪。

poll

poll模式对select模式做了改进,但是性能提升不是很明显,

代码语言:c
代码运行次数:0
运行
复制
#include <sys/poll.h>  // 标准头文件,实际使用时应包含

// pollfd 中的事件类型
#define POLLIN      0x001   // 可读事件
#define POLLPRI     0x002   // 高优先级数据可读
#define POLLOUT     0x004   // 可写事件
#define POLLERR     0x008   // 错误事件
#define POLLHUP     0x010   // 挂起事件
#define POLLNVAL    0x020   // 文件描述符未打开

// pollfd结构
struct pollfd {
    int fd;               /* 要监听的文件描述符 */
    short int events;     /* 要监听的事件类型(输入参数) */
    short int revents;    /* 实际发生的事件类型(输出参数) */
};

// poll函数声明
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

IO流程:

  1. 创建pollfd数组,向其中添加关注的fd信息,数组大小自定义。
  2. 调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限。
  3. 内核遍历fd,判断是否就绪。
  4. 数据就绪或超时后,将pollfd数组变为只保留就绪的fd,然后拷贝pollfd数组到用户空间,返回就绪fd数量n。
  5. 用户进程判断n是否大于0
  6. 大于0则遍历pollfd数组,找到就绪的fd。

比select改进的点:1.定义了pollfd结构,不需要传多个fd数组了,可以根据pollfd结构本身判断该fd应该监听的是什么事件。2.fd集合在内核中采用链表,理论上无上限了。

但是这些改进提升都很小,尤其是fd集合无上限这种情况,会导致遍历消耗的事件很长,性能反而下降。

epoll

epoll对select和poll做了很大的改进。 它提供了三个函数,分别是 int epoll_create , int epoll_ctl , int epoll_wait

代码语言:c
代码运行次数:0
运行
复制
struct eventpoll {
    //...
    struct rb_root rbr; // 一颗红黑树,记录要监听的FD
    struct list_head rdlist;// 一个链表,记录就绪的FD
    //...
};

// 1.会在内核创建eventpoll结构体,返回对应的句柄epfd
int epoll_create(int size);

// 2.将一个FD添加到epoll的红黑树中,并设置ep_poll_callback
// callback触发时,就把对应的FD加入到rdlist这个就绪列表中
int epoll_ctl(
    int epfd,  // epoll实例的句柄
    int op,    // 要执行的操作,包括:ADD、MOD、DEL
    int fd,    // 要监听的FD
    struct epoll_event *event // 要监听的事件类型:读、写、异常等
);

// 3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量
int epoll_wait(
    int epfd,               // eventpoll实例的句柄
    struct epoll_event *events, // 空event数组,用于接收就绪的FD
    int maxevents,          // events数组的最大长度
    int timeout             // 超时时间,-1用不超时;0不阻塞;大于0为阻塞时间
);

select和poll模式下,我们会不断地把需要监听的fd数组拷贝到内核空间,然后再把监听到了的fd数组拷贝回用户空间,很浪费事件,而在epoll模式下,得益于 events 数据结构以及在内核空间中创建的eventpoll结构体,每个事件触发的fd都会被直接放到events中,不再像select和poll模式中每次拷贝都是整个fd数组一起拷贝,减少了拷贝的数量。使用 epoll_ctl 方法也是添加一个fd到需要监听的列表中,不再需要反复传递和拷贝fd数组了。并且,select和poll模式下,监听到就绪fd之后,返回的是数量,还需要遍历所有的fd,而epoll模式下就不需要遍历,因为 events 中的所有fd都是就绪的。

总结

select 模式存在的三个问题:

  • 能监听的 FD 最大不超过 1024
  • 每次 select 都需要把所有要监听的 FD 都拷贝到内核空间
  • 每次都要遍历所有 FD 来判断就绪状态

poll 模式的问题:

  • poll 利用链表解决了 select 中监听 FD 上限的问题,但依然要遍历所有 FD,如果监听较多,性能会下降

epoll 模式中如何解决这些问题的?

  • 基于 epoll 实例中的红黑树保存要监听的 FD,理论上无上限,而且增删改查效率都非常高,性能不会随监听的 FD 数量增多而下降
  • 每个 FD 只需要执行一次 epoll_ctl 添加到红黑树,以后每次 epoll_wait 无需传递任何参数,无需重复拷贝 FD 到内核空间
  • 内核会将就绪的 FD 直接拷贝到用户空间的指定位置,用户进程无需遍历所有 FD 就能知道就绪的 FD 是谁

信号驱动IO

信号驱动IO 是与内核建立SIGIO的信号关联并设置回调,当内核有FD就绪时,就会发出SISIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。注意这里和 非阻塞IO 有点相同,就是都会在第一阶段不阻塞,第二阶段读取数据的时候还是会阻塞。但是非阻塞IO是不断轮询,而这里会释放该线程去做别的事情。

缺点: 当有大量IO操作的时候,信号较多,SIGIO处理函数不能及时处理可能会导致信号队列溢出。而且内核空间和用户空间的频繁信号交互性能也较低。

异步IO

异步IO 的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其他事情,内核等待数据就绪并且拷贝到用户空间后才会递交信号,通知用户进程。

异步IO的时候用户进程是不会阻塞的,可以一直处理请求,但是用户进程把任务的控制都交给了内核,如果交给内核的请求太多,那么内核就会太忙了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Linux多种IO模型
    • 阻塞IO
    • 非阻塞IO
    • IO多路复用
      • select
      • poll
      • epoll
      • 总结
    • 信号驱动IO
    • 异步IO
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档