前情提要:
《redis 从0到1完整学习 (一):安装&初识 redis》
《redis 从0到1完整学习 (二):redis 常用命令》
《redis 从0到1完整学习 (三):redis 数据结构》
《redis 从0到1完整学习 (四):字符串 SDS 数据结构》
《redis 从0到1完整学习 (五):集合 IntSet 数据结构》
《redis 从0到1完整学习 (六):Hash 表数据结构》
《redis 从0到1完整学习 (七):ZipList 数据结构》
《redis 从0到1完整学习 (八):QuickList 数据结构》
《redis 从0到1完整学习 (九):SkipList 数据结构》
《redis 从0到1完整学习 (十):RedisObject 数据结构》
《redis 从0到1完整学习 (十一):RedisObject 之 String 类型》
《redis 从0到1完整学习 (十二):RedisObject 之 List 类型》
《redis 从0到1完整学习 (十三):RedisObject 之 Set 类型》
《redis 从0到1完整学习 (十四):RedisObject 之 ZSet 类型》
《redis 从0到1完整学习 (十五):RedisObject 之 Hash 类型》
《redis 从0到1完整学习 (十六):内存回收之 key 过期处理策略》
《redis 从0到1完整学习 (十七):内存回收之内存淘汰策略》
《redis 从0到1完整学习 (十八):阻塞/非阻塞 IO》
之前介绍了阻塞、非阻塞 I/O 技术,本文主要介绍 IO 多路复用机制的select 和 poll 的机制。
Redis 源码可以点击这里下载,方便查看其中定义的一些数据结构。
IO多路复用(I/O Multiplexing)是一种同步 IO 模型,主要用于网络编程中高效地处理多个文件描述符(通常是 socket 连接)的事件。在单个进程中,通过调用特定的系统 API,程序可以同时监听多个文件描述符的状态变化,而无需为每一个单独的连接创建一个线程或进程。
IO多路复用模型:
在 IO 多路复用机制下,当没有 IO 事件发生时,调用会阻塞进程等待,直到至少有一个文件描述符准备好进行读写操作为止。一旦某个文件描述符就绪(例如有数据可读或可写),系统会立即通知应用程序,并返回所有准备好的文件描述符列表,然后应用程序就可以针对性地对这些已就绪的文件描述符进行相应的读取或写入操作。
以下是 IO 多路复用常见的系统调用实现方式:
select
:能够同时监控多个文件描述符的读写状态,但其性能受限于内核硬编码的最大文件描述符数,并且每次调用都需要将所有监视的文件描述符复制到内核空间,随着连接数增加,效率逐渐降低。
poll
:类似于select
,但去除了最大文件描述符数量的限制,采用链表结构存储待监控的文件描述符集合,但仍存在需要遍历整个集合来确定就绪文件描述符的问题。
epoll
( Linux 系统):相较于select
和poll
,它提供了更高效的解决方案。epoll使用事件驱动的方式,在内核中维护了一个红黑树结构来管理待监控的文件描述符,仅返回真正有事件发生的文件描述符,从而避免了无谓的循环扫描,极大地提升了大规模并发连接下的性能。
IO多路复用的优势在于:
select
是一个在许多操作系统中(包括Unix和Windows)广泛使用的I/O多路复用技术,它允许单个进程或线程同时监控多个文件描述符的读写事件。在处理网络编程时,特别是在服务器端需要管理大量并发连接的情况下,select
能够有效地监听多个套接字(socket)的状态变化,从而避免为每个连接创建单独的线程或进程。
基本原理与使用流程:
初始化:
fd_set
结构体来维护一组待监控的文件描述符集合,通常分为三类:读就绪、写就绪和异常就绪。添加监控:
FD_SET
函数将需要监控的套接字加入到相应的 fd_set
集合中。调用 select 函数:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数含义:
nfds
:一般设置为所有待监控文件描述符中的最大值加1。readfds
:指向读操作就绪的文件描述符集合。writefds
:指向写操作就绪的文件描述符集合。exceptfds
:指向异常就绪(如错误条件)的文件描述符集合。timeout
:指定等待超时时间或者设置为NULL表示无限制等待。阻塞等待:
select
时,如果没有任何文件描述符准备好,函数会阻塞直到至少有一个描述符变为可读、可写或发生异常,或者到达了指定的超时时间。检查结果:
select
返回后,程序通过 FD_ISSET
函数检查各个集合,找出哪些文件描述符已经准备就绪进行读取、写入或其他操作。执行对应操作:
循环处理:
缺点:
select
的性能瓶颈在于每次调用都需要将整个监视集复制到内核空间,并且随着被监控的文件描述符数量的增长,效率会急剧下降。select
在返回时仍然需要遍历整个描述符集合,这在高并发情况下是低效的。poll
是对 select
函数的改进版,用于解决同时监控多个文件描述符(如网络套接字)的读写事件问题。与 select
相似,poll
允许程序在单个线程中等待多个文件描述符中的任何一个变为可读或可写状态,从而避免了为每个连接创建单独的线程或进程所带来的资源开销。
基本原理与使用流程:
初始化:
struct pollfd
结构体数组来定义待监控的文件描述符集合,每个结构体成员包含一个文件描述符及其关注的事件类型(POLLIN、POLLOUT等)。添加监控:
pollfd
结构体数组中。调用 poll 函数:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数含义:
fds
:指向 struct pollfd
结构体数组的指针。nfds
:数组中元素的数量,即要监控的文件描述符总数。timeout
:超时时间(毫秒),负数表示无限等待,0表示立即返回当前状态而不阻塞。阻塞等待:
poll
时,如果没有文件描述符就绪,函数将阻塞直到至少有一个描述符变为可读、可写或其他指定的状态,或者达到指定的超时时间。检查结果:
poll
返回后,通过查看 pollfd
结构体数组中每个元素的 revents
成员,确定哪些文件描述符发生了关注的事件。执行操作:
优缺点:
优点:
FD_SETSIZE
的限制,poll 在内核中采用链表,理论上能够处理任意数量的文件描述符。select
更加灵活和易于理解。缺点:
select
解决了最大文件描述符数量的问题,但仍然存在效率上的瓶颈,因为每次调用 poll
都会遍历整个待监控的文件描述符列表,即使没有事件发生。相较于更高级的 epoll
机制,poll
在处理大量并发连接时性能相对较差,因为它同样不具备高效的内核事件通知机制。epoll
使用了一种更为优化的方法来跟踪和报告就绪的文件描述符,因此在高并发场景下,epoll
常被推荐作为首选的 I/O 多路复用技术。
下一篇会继续介绍 epoll
的相关内容。
《redis 从0到1完整学习 (一):安装&初识 redis》
《redis 从0到1完整学习 (二):redis 常用命令》
《redis 从0到1完整学习 (三):redis 数据结构》
《redis 从0到1完整学习 (四):字符串 SDS 数据结构》
《redis 从0到1完整学习 (五):集合 IntSet 数据结构》
《redis 从0到1完整学习 (六):Hash 表数据结构》
《redis 从0到1完整学习 (七):ZipList 数据结构》
《redis 从0到1完整学习 (八):QuickList 数据结构》
《redis 从0到1完整学习 (九):SkipList 数据结构》
《redis 从0到1完整学习 (十):RedisObject 数据结构》
《redis 从0到1完整学习 (十一):RedisObject 之 String 类型》
《redis 从0到1完整学习 (十二):RedisObject 之 List 类型》
《redis 从0到1完整学习 (十三):RedisObject 之 Set 类型》
《redis 从0到1完整学习 (十四):RedisObject 之 ZSet 类型》
《redis 从0到1完整学习 (十五):RedisObject 之 Hash 类型》
《redis 从0到1完整学习 (十六):内存回收之 key 过期处理策略》
《redis 从0到1完整学习 (十七):内存回收之内存淘汰策略》
《redis 从0到1完整学习 (十八):阻塞/非阻塞 IO》
欢迎关注本人,我是喜欢搞事的程序猿; 一起进步,一起学习;
也欢迎关注我的wx公众号:一个比特定乾坤