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 专属,接口略复杂
相关推荐
fengyehongWorld1 小时前
Linux 域名申请与DNS解析
linux·运维·服务器
SKH.1 小时前
网络(3)TCP通信
网络·网络协议·tcp/ip
μθημα1 小时前
Kubernetes 微服务网络实践:Service 与 Ingress 从入门到灰度发布
网络·微服务·kubernetes
晊晌_h1 小时前
嵌入式从0到精通——Linux 网络通信|TCP、HTTP 网络编程
linux·c语言·网络
CounterGlew5111 小时前
Vscode通过ssh链接autodl云服务器并配置codex
运维·服务器·vscode
2501_927283581 小时前
数据采集,正在让工厂从“凭经验”走向“看数据”
运维·人工智能·自动化·数据采集·agv·立体仓库
姚不倒1 小时前
Tengine 实战:主动健康检查 + 动态 Upstream 配置
运维·网络·nginx
茶栀(*´I`*)1 小时前
Python网络机器人入门:从Robots协议、网页结构到Requests库的基础实践
网络·python·机器人
郝学胜-神的一滴2 小时前
CMake 047:解锁安装阶段自定义操作,告别配置阶段提前执行坑
运维·服务器·c++·游戏引擎·图形渲染·opengl