I/O多路复用这块内容最近找实习被提问率挺高的,重新梳理了下。
I/O多路复用是指通过一种机制,让单个进程可以同时处理多个I/O事件,而不需要通过多线程或多进程来实现。这种机制可以提高网络应用程序的并发性能和响应速度。
在Linux下,常见的I/O多路复用机制有select、poll和epoll。
原理是在内核中维护一个文件描述符集合,通过调用select函数来监控这个集合中的文件描述符是否有可读、可写或异常等事件发生。当有事件发生时,select函数会返回,并将就绪的文件描述符返回给应用程序。
select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024
,只能监听 0~1023 的文件描述符。
总结下select有以下几个缺点:
poll也是Unix下的I/O多路复用机制之一,与select类似,它的原理是在内核中维护一个文件描述符集合,通过调用poll函数来监控这个集合中的文件描述符是否有可读、可写或异常等事件发生。当有事件发生时,poll函数会返回,并将就绪的文件描述符返回给应用程序。
poll相对于select来说,没有文件描述符集合大小的限制,poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
epoll是Linux下的新一代I/O多路复用机制,它采用了事件驱动(Event-Driven)方式,可以处理大量的文件描述符,并且只需遍历那些发生事件的文件描述符。这使得epoll具有更高的效率和更低的延迟。
epoll的工作原理是在内核中维护一个红黑树和一个双向链表。
红黑树用于存储要监控的文件描述符,它的每个节点都对应一个文件描述符,每个节点中存储有文件描述符的相关信息,比如是否可读、是否可写等等。
双向链表用于存储就绪的文件描述符及其对应的事件类型,当某个文件描述符就绪时,它会被加入到这个链表中,等待应用程序调用epoll_wait函数来处理。
epoll_wait函数在等待就绪的文件描述符时,实际上是将红黑树中所有就绪的文件描述符节点取出来,然后将它们对应的文件描述符和事件类型存储到双向链表中,最后将链表中的数据返回给应用程序。
由于红黑树的查找和插入操作的时间复杂度都是O(logN),所以epoll能够高效地处理大量的文件描述符。同时,由于双向链表只用于存储就绪的文件描述符,所以它的长度通常比较短,而且每次只需要遍历一次就能够获取所有就绪的文件描述符,因此epoll的性能也比较高。
总结一下:epoll 通过两个方面解决了 select/poll 的问题。
1、epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
2、epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。
相对于select和poll,epoll具有以下几个优点:
边缘触发(Edge-Triggered)是指当文件描述符上有新的事件发生时,内核会通知应用程序,只通知一次,应用程序需要立即对文件描述符进行操作,否则会导致数据丢失。这种模式需要应用程序具有较高的并发处理能力和良好的状态机设计。(需要一次性处理完!)
水平触发(Level-Triggered)是指当文件描述符上有新的事件发生时,内核会通知应用程序,并且一直通知,直到事件处理完毕。这种模式比较适合于普通的应用程序。
下面通过一个简单的例子来介绍epoll的使用。假设我们有一个服务器程序,需要同时处理多个客户端的连接请求。为了实现这个功能,我们可以使用epoll来监控多个套接字的I/O事件,并在事件触发后及时进行处理。
当调用epoll_wait()
返回时,可以根据事件的类型进行处理。下面是使用epoll的一些常见步骤:
1.创建一个 epoll 句柄(文件描述符)
int epoll_fd = epoll_create(max_events);// 创建epoll文件描述符,max_events表示要监听的文件描述符数量
创建一个 epoll 文件描述符,max_events
是该 epoll 对象能监听的最大文件描述符数量
2.将需要监听的文件描述符添加到 epoll 集合中
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = fd;// 监听的文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);// 将fd添加到epoll_fd中进行监听
创建一个 epoll_event 结构体,设置需要监听的事件类型和文件描述符,然后将其添加到 epoll 中。
其中,EPOLLIN
表示可读事件,EPOLLOUT
表示可写事件,EPOLLET
表示采用边缘触发方式。EPOLL_CTL_ADD
表示添加一个文件描述符到 epoll 中。
3.等待事件的发生
struct epoll_event events[max_events];
int nfds = epoll_wait(epoll_fd, events, max_events, timeout);// 等待事件发生,最多返回max_events个事件
调用 epoll_wait 等待事件的发生。max_events
表示一次最多能够处理的事件数,timeout
表示超时时间。函数返回时,返回的是发生事件的文件描述符的个数。
4.处理事件
for (int i = 0; i < nfds; i++) {if (events[i].data.fd == fd) {// 处理文件描述符为 fd 的事件}
}
根据返回的事件类型和文件描述符,处理相应的事件。可以使用 events[i].events & EPOLLIN
来判断事件类型,events[i].data.fd
来获取文件描述符。
5.删除文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, &event);
当不再需要监听某个文件描述符时,可以将其从 epoll 中删除。
其中,EPOLL_CTL_DEL
表示删除一个文件描述符从 epoll 中。
6.关闭 epoll 对象
close(epoll_fd);
当不再需要 epoll 对象时,需要将其关闭以释放资源。
以上就是使用 epoll 的常见步骤。使用 epoll 可以大大提高网络编程的效率和可靠性。
1.《Linux多线程服务端编程:使用muduo C++网络库》
2.《Linux高性能服务器编程》
3. 小林coding的图解系列
下一篇: 感恩祖国的诗歌