Linux 网络编程:IO 多路复用

一、TCP 并发服务器模型:多线程

多线程可以实现 TCP 并发:每来一个客户端,就创建一个子线程专门处理该客户端通信。

缺点

  1. 每个线程要独占独立栈内存,连接数量上限有限,大量并发客户端,系统有崩溃风险。
  2. 线程切换会带来 CPU 开销。

优化方案 :使用线程池,预先创建一批线程,复用线程,避免频繁创建销毁线程。

二、4 种 IO 模型

1. 阻塞 IO

没有数据的时候,CPU 把任务挂起,任务不占用 CPU,等待数据就绪后再返回。

优点:编码简单;缺点:一个线程只能处理一个 IO,无法同时处理多个连接。

2. 非阻塞 IO

没有数据时,函数不会阻塞,直接返回;用户代码要不停循环轮询,反复调用 IO 函数检查数据是否到来。

缺点:CPU 空转,消耗大量 CPU 资源。

3. 信号驱动 IO(异步 IO)

  1. 将文件描述符设置为异步 IO,向进程注册信号事件。
  2. 内核监测 fd,当 IO 事件发生,内核发送 SIGIO 信号通知用户进程。
  3. 用户程序写信号捕获函数,在回调里面读写数据。

缺点:信号处理框架不好控制,实际网络开发很少用。

4. IO 多路复用

核心:一个进程 / 线程,同时监听一堆文件描述符,哪个 fd 有事件就绪就处理哪个。

主流三种实现:selectpollepoll

三、select

工作原理

select 监听一组文件描述符集合;集合中只要至少一个 fd 产生事件,select 就解除阻塞。它会原地修改 fd 集合:保留发生事件的 fd,把没有事件的 fd 从集合清掉

fd_set 集合操作宏

函数 作用
FD_ZERO(fd_set *set) 清空集合,全部置 0
FD_SET(int fd, fd_set *set) 把 fd 加入集合
FD_CLR(int fd, fd_set *set) 把 fd 从集合移除
FD_ISSET(int fd, fd_set *set) 判断 fd 是否还在集合中(是否发生事件)

select 函数原型

c 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
  • nfds:最大文件描述符值 + 1
  • readfds:读事件集合;writefds:写事件集合;exceptfds:异常事件集合
  • timeout:超时时间。传 NULL 代表永久阻塞等待

返回值

  • > 0:就绪 fd 的总数量
  • -1:出错
  • 0:超时时间到,没有任何事件

select 缺点

  1. fd 集合有最大上限,默认 1024;
  2. 每次调用都会修改集合,循环调用前必须重新 FD_ZEROFD_SET
  3. 返回后需要循环遍历全部 fd 判断 FD_ISSET
  4. 用户态和内核态之间,每次调用都要拷贝整个 fd 集合。

四、epoll(Linux 高性能 IO 多路复用)

epoll 把事件表放在内核,不需要每次调用都拷贝全部 fd,适合高并发海量连接。

1. epoll_create

c 复制代码
int epoll_create(int size);
  • 参数 size:早期版本提示事件表大小,现在已经被忽略,填大于 0 数字即可
  • 返回值:成功返回 epoll 的文件描述符 epfd;失败返回 -1

作用:在内核创建 epoll 事件表。

2. epoll_ctl

向内核事件表增、删、改监听的 fd 和事件。

c 复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:epoll_create 返回的句柄
  • op 操作类型
    • EPOLL_CTL_ADD:向事件表添加 fd
    • EPOLL_CTL_MOD:修改已经注册 fd 的监听事件
    • EPOLL_CTL_DEL:从事件表删除 fd
  • fd:要操作的文件描述符
  • event:绑定这个 fd 的事件
c 复制代码
struct epoll_event {
    uint32_t events;      // EPOLL_IN / EPOLL_OUT / EPOLLET
    epoll_data_t data;    // 用户自定义数据,一般存 fd
};

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

常用事件:

  • EPOLLIN:读事件,有数据可读
  • EPOLLOUT:写事件,可以发送数据
  • EPOLLET:边沿触发模式,默认是水平触发

3. epoll_wait

阻塞等待事件就绪。

c 复制代码
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);
  • epfd:epoll 句柄
  • events:输出参数,内核把发生事件存到这个数组
  • maxevents:events 数组最大元素个数
  • timeout:毫秒,-1 代表永久阻塞

返回值

  • > 0:就绪事件数量,就是需要处理的连接个数
  • 0:超时,没有事件
  • -1:出错

epoll 两种触发模式

  1. 水平触发 LT(默认):只要缓冲区有数据,epoll_wait 就反复通知。
  2. 边沿触发 ET:只有状态发生变化那一刻通知一次;必须一次性把缓冲区数据全部读完,否则不会再次提醒。

epoll 优势

  1. 没有 1024 连接上限;
  2. 不需要每次传入全部 fd,内核维护事件表;
  3. epoll_wait 直接拿到就绪事件数组,不用遍历全部 fd;
  4. 海量空闲连接下性能远高于 select。

五、简单对比小结

模型 优点 缺点
select 跨平台,简单 上限 1024,每次拷贝集合,需要遍历所有 fd
epoll 高性能,海量连接 Linux 专属,接口略复杂
相关推荐
晓蛋2 天前
c语言指的是什么意思
c语言·编译器·编程开发·集成开发环境·程序实例
未济2 天前
linux 配置环境变量
linux
傲世仙尊2 天前
目录即文件-Ext文件系统收尾篇
linux·c语言
虎头金猫2 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
牵猫散步的鱼儿2 天前
重载、重写(覆盖)、重定义区别
c语言
phltxy2 天前
C 语言指针:从内存地址到灵活的数据访问
c语言
_艾伦 耶格尔.2 天前
进程间通信
linux
phltxy2 天前
C 语言中的数据存储:从类型到二进制位
c语言
AI职业加油站2 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
Liuqy-052 天前
Linux IO编程——静态库、动态库
linux