linux三种IO多路复用模型
I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。
在unix网络编程中的五种模型,网络模型的区别:
- 阻塞IO【blocking IO】
- 非阻塞IO【noblocking IO】
- IO多路复用【IO multiplexing 】
- 信号驱动IO【signal driven IO】
- 异步IO【asynchronous IO】
前面四种可以归纳为同步IO,且select、poll和epoll本质上也都是同步IO,因为他们都需要在读写事件后自己负责读写,也就是说这个过程是阻塞的。
1. linux操作系统中的基础概念
1.1 用户空间&内核空间
现代操作系统采用虚拟存储器,对于32位的操作系统而言,它的寻址空间(虚拟存储空间)为232为4G。
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两个部分,一部分为内核空间,一部分为用户空间。
1.2 进程切换
任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。操作系统为了控制进程的执行,内核必须有能力挂起正在运行在CPU上的进程,并恢复以前被挂起的某一个进程的执行。这种行为被称为进程切换。并且进程切换是非常消耗资源的。
1.3 进程阻塞
正在执行的进程,由于期待的某些事件未发生,比如:请求系统资源失败,等待某一种操作的完成或者无新工作等时,则由操作系统自动执行阻塞源语(block),使得自己有运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行状态的进程(获得了CPU资源)才可能将其转为阻塞状态,当进程进入阻塞状态时是不占用CPU资源的。
1.4 文件描述符
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
1.5 缓存IO
缓存IO又叫做标准IO,大多数文件系统的默认IO都是缓存IO。在linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存中,即数据会西安北拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。这个过程经历了2次拷贝:从IO上将数据拷贝到操作系统内核,再从内核拷贝到应用程序的地址空间。
2. select
select函数原型int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
2.1 函数原型分析
【参数】
int maxfdp1: 指定待测试的文件描述字个数,他的值是待测试的最大描述符字+1
fd_set *readset , fd_set *writeset , fd_set *exceptset: fd_set
可以理解为一个集合,这个集合中存放的文件描述符,即文件句柄。这三个参数分别是想让内核测试读取(readset )、写(writeset )和异常(exceptset)的文件描述符集合。如果调用者对某一个不想获取的话,直接赋值为空指针即可。
const struct timeval *timeout: 告知内核等待所指定文件描述符集合中的任何一个就绪可花多少时间。其中timeval
结构用于指定这段时间的秒数和微秒数。
【返回值】
int : 若检测到有就绪描述符返回其数目,若超时则为0,出错为-1
2.2 运行机制
select()
的机制中提供一直名为fd_set
的数据结构,实际上是一个long型的数组,每一个数组元素都能与一个打开的文件句柄建立联系,建立联系的过程由程序员完成。当调用select()
时,由内核根据IO的状态修改fd_set
的内容,用此来通知执行了select()
函数的进程哪一个文件句柄发生了变化(可读或者可写或者异常)。
从流程上来看,使用select()
函数进行IO请求和同步阻塞模型咩有太大的区别,甚至还多添加监视socket,以及调用select()
函数的额外的操作,效率更差。但是,使用select()
函数以后的最大优势是用户可硬在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断的调用select读取被激活的socket句柄,即可达到在用一个线程内同时处理多个IO请求的目的。而在同步阻塞的模型中,必须要通过多线程的方式才能达到这个目的。
2.3. 存在的问题
- 每次调用
select()
函数时,都需要把fd_set
中用户态拷贝到内核态。当fd_set
很大时,拷贝开销也很大 - 每次调用
select()
函数时,都需要在内核中遍历用户态传入的所有的fd_set
,如果fd_set
很大,遍历开销也很大 - 为了减小数据拷贝带来的性能的损耗,内核被系统监控限制了
fd_set
的最大值,x86为210为1024,x64为211为2048
3. poll
poll的机制与select很类似,与select模型没有本质上的差别,同样是多个描述符进行轮询,根据描述符的状态进行不同的处理,但是poll没有最大文件描述符的限制。也就是说,poll只解决了select的第三个问题。
3.1 poll函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 需要被检测或选择的文件描述符
short events; // 对文件描述符fd上感兴趣的事件
short revents; // 文件描述符fd上当前实际发生的事件
} pollfd_t;
poll改变了文件描述符集合的描述方式,使用pollfd
结构替代了fd_set
结构,使得poll支持的文件描述符限制远大于select得到1024
【参数】
struct pollfd *fds: fds
是一个struct pollfd
类型的数组,用于存放需要检测其状态的文件描述符,并且调用poll
函数之后fds
数组不会被清空。一个pollfd
结构体表示一个被监视的文件描述符,通过传递fds
指示poll
监视多个文件描述符。其中,结构体的event
域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents
域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域
nfds_t nfds: 记录数组中fds
中描述符的总数量
【返回值】
int: 函数返回fds结合中就绪的读、写或者出错的描述符数量,返回0表示超时,-1表示出错
3.2 存在的问题
1.每次调用poll()函数时,都需要把fds
中用户态拷贝到内核态,当遍历很大的fds时,遍历的开销很大
4. epoll
epoll
在 linux2.6 的内核中正式提出,是基于事件驱动的IO方式,相对于select
来说,epoll
没有描述符的个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的拷贝只需要一次。
linux中提供的epoll相关的函数如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
4.1 函数解释
epoll_create
:创建一个epoll句柄,size
为内核要监听的描述符的数量。调用成功时返回一个epoll句柄描述符,失败时返回-1
epoll_ctl
:函数注册要监听的事件类型,参数:
-
epfd
:epoll
的句柄 -
op
: 表示fd的操作类型。有三种:EPOLL_CTL_ADD
: 注册新的fd
到epoll
句柄中EPOLL_CTL_MOD
: 修改已注册的fd的监听事件EPOLL_CTL_DEL
: 从epoll
句柄epfd
中删除一个fd
-
fd
: 要监听到描述符 -
event
: 要监听的事件
epoll_wait
: 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回-1,等待超时时返回0
epfd
:epoll
句柄events
: 表示从内核得到的就绪事件集合maxevents
: 告诉内核events的大小timeout
: 等待的超时时间
epoll是linux内核为处理大批量文件描述符而改进的poll模型,是linux下多路复用IO接口的增强版本,它能显著提高在大量并发连接中只有少量活跃的情况下的系统cpu利用率。原因就是获取事件的时候,它无需遍历整个被监听的描述符,只要遍历那些被内核IO事件异步唤醒而加入ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的水平触发外,还提供了边缘触发,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序的效率。
5. 总结
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 红黑树 |
IO效率 | 每次遍历都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 | 1024(x86)、2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
epoll
是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select
和poll
。目前流行的高性能web服务器nginx正式依赖于epoll
提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
资料
[1] https://www.jianshu.com/p/397449cadc9a