Linux 软件编程(十三)网络编程:TCP 并发服务器模型与 IO 多路复用机制、原理epoll
一、TCP 并发服务器核心模型
(一)多进程模型
- 原理:服务端通过
fork()
创建子进程,父进程专注监听新连接,子进程独立处理客户端交互。利用进程的独立性,实现多客户端并行处理。 - 优缺点:
- 优点:进程资源隔离性强,一个子进程异常通常不会牵连其他进程,安全性高。
- 缺点:进程创建、切换涉及独立地址空间的分配与销毁,资源开销大,并发量高时性能易受影响。
(二)多线程模型
- 原理:借助
pthread_create()
创建子线程,主线程负责accept
新连接,子线程处理客户端数据收发。线程共享进程地址空间,轻量化实现并发。 - 优缺点:
- 优点:相较于进程,线程创建、切换成本低,相同资源下可支撑更高并发量。
- 缺点:线程共享资源,需通过互斥锁等同步机制避免竞争,否则易引发数据混乱,增加开发复杂度。
(三)线程池模型
- 背景:多线程 / 多进程模型中,频繁创建、销毁线程 / 进程会产生大量时间消耗。线程池基于 生产者 - 消费者模式 与 任务队列 优化,预先创建固定数量线程,复用线程处理任务,减少资源开销。
- 流程:
- 生产者(主线程):
accept
客户端连接,将任务(如请求处理)放入队列。 - 消费者(线程池线程):从队列取任务执行,循环复用,无需频繁创建销毁。
- 生产者(主线程):
二、IO 多路复用
IO 多路复用让 单个进程 / 线程 可同时监测、处理 多个文件描述符(fd) 的读写事件,无需为每个 fd 单独创建进程 / 线程,核心解决阻塞 IO 导致的效率问题。主流实现有 select
、poll
、epoll
。
(一)select 机制
- 实现逻辑:
- 用 位图(数组) 存储待监测的 fd 集合,最多支持 1024 个 fd (系统限制 )。
- 需反复在 应用层与内核层拷贝 fd 集合 ,内核监测事件后,应用层需 遍历集合 找触发事件的 fd 。
- 仅支持 水平触发(LT) :只要 fd 有未处理数据,就持续触发事件,易导致重复处理。
- 关键函数:
// 清空/操作 fd 集合 void FD_ZERO(fd_set *set); void FD_SET(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); // 核心监测函数 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
- 特点总结:实现简单但效率有限,受限于 fd 数量与反复拷贝、遍历的开销,适合低并发场景。
(二)poll 机制
- 实现逻辑:
- 用 链表 存储 fd 集合,突破 1024 个 fd 的数量限制。
- 仍需 应用层与内核层反复拷贝 fd 数据 ,且需遍历链表判断触发事件的 fd 。
- 仅支持 水平触发(LT) ,未解决根本性能瓶颈。
- 关键函数:
struct pollfd {int fd; // 监测的文件描述符short events; // 关注的事件(如 POLLIN 读事件)short revents; // 实际触发的事件 };int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 特点总结:解决了 fd 数量限制,但核心性能问题(拷贝、遍历)未优化,适合中等并发但对性能要求不极致的场景。
(三)epoll 机制
epoll 是 Linux 下高性能 IO 多路复用方案,专为高并发设计,从存储、数据交互、事件触发全链路优化。
1. 核心优势
- 存储优化:用 红黑树(二叉搜索树) 存储 fd 集合,无数量限制,且查找效率高(O (logN) )。
- 数据交互优化:fd 集合直接 创建在内核层 ,避免应用层与内核层反复拷贝,降低资源消耗。
- 事件触发优化:支持 水平触发(LT) 和 边沿触发(ET) :
- LT(默认):类似 select/poll,有未处理数据持续触发。
- ET(高速模式):仅在数据状态变化时触发(如新增数据),减少重复事件,提升效率。
- 结果处理优化:
epoll_wait
直接返回 触发事件的 fd 列表 ,无需遍历所有 fd ,处理更高效。
2. 关键函数与流程
epoll 使用分三步:创建集合 → 操作集合 → 监测事件 。
1. 创建文件描述符集合 : int epoll_create(int size);
2. 添加关注的文件描述符:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3. epoll通知内核开始进行事件监测 :int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
4. epoll返回时,获取到达事件的结果
5. 根据到达事件做任务处理
创建 epoll 集合:
int epoll_create(int size);
功能:通知内核创建文件描述符集合
参数:
size:监测的文件描述符个数
返回值:
成功:文件描述符(代表内核创建的集合)
失败:-1操作 fd 集合(添加、修改、删除):
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:对epoll的文件描述符集合进行操作
参数:
epfd:创建的epoll集合
op:对文件描述符集合进行的操作
EPOLL_CTL_ADD : 添加文件描述符到集合
EPOLL_CTL_MOD : 修改集合中的文件描述符
EPOLL_CTL_DEL :删除集合中的文件描述符
fd:要操作的文件描述符
event:文件描述符对应的事件
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events:文件描述符的事件:
EPOLLIN: 读事件
EOPLLOUT:写事件
data.fd : 关注的文件描述符返回值:
成功:0
失败:-1监测事件:
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
功能:通知内核开始监测文件描述符的事件
参数:
epfd:监测的文件描述符集合
events:保存返回的到达事件的结果(数组)
struct epoll_event evs[MAX_FD_CNT];
evs;
maxevents:最大的事件个数
timeout:监测的超时时间
-1 :不设置超时(一直阻塞)返回值:
成功:到达的事件的个数
失败:-1
3. 工作模式对比(LT vs ET)
- 水平触发(LT):只要 fd 有数据未读,就持续触发事件。优点是简单可靠,缺点是可能重复处理,适合对实时性要求不高的场景。
- 边沿触发(ET):仅在数据状态变化时触发(如从无到有、从少到多)。需一次性读完 fd 数据,否则后续不会触发,效率更高,适合高并发、低延迟场景,但编程时需更严谨处理数据读取。
三、技术选型与应用场景
技术 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
多进程 | 安全性高,进程隔离 | 资源开销大,并发量受限 | 低并发、对稳定性要求高场景 |
多线程 | 轻量,并发量高于多进程 | 需处理线程同步,易引发资源竞争 | 中等并发、逻辑简单场景 |
线程池 | 优化线程资源,减少创建销毁开销 | 增加代码复杂度 | 高并发、任务重复性高场景 |
select | 实现简单,跨平台性好 | fd 数量受限,拷贝 / 遍历开销大 | 低并发、简单网络应用 |
poll | 突破 fd 数量限制 | 仍需拷贝 / 遍历,效率一般 | 中等并发、无严格性能要求场景 |
epoll | 高性能,支持高并发、边沿触发 | 仅支持 Linux 平台,编程稍复杂 | 高并发服务器(如 Web 服务) |
四、总结
TCP 并发服务器的实现,从多进程、多线程的基础模型,到线程池的优化,再到 IO 多路复用的高效处理,本质是在 资源开销 与 并发能力 间寻找平衡。select
、poll
适合简单场景,而 epoll
凭借红黑树存储、内核层集合、边沿触发等特性,成为高并发服务器的首选。