select poll epoll系统调用详细分析

本文详细解析了select、poll及epoll的工作原理及其内核实现过程,对比了三者的优劣,并重点介绍了epoll如何克服select和poll的不足。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、select,poll,epoll都是多路复用IO的函数,简单说就是在一个线程里,可以同时处理多个文件描述符的读写。

select/poll的实现很类似,epoll是从select/poll扩展而来,主要是为了解决select/poll天生的缺陷。
epoll在内核版本2.6以上才出现的新的函数,而他们在linux内核中的实现都是十分相似。

这三种函数都需要设备驱动提供poll回调函数,对于套接字而言,他们是 tcp_poll,udp_poll和datagram_poll;

2、select系统调用相关细节(2.6的内核,其他版本的内核,应该都相差不多

下图是select和poll在调用过程中,会使用到的内核函数,以及这写内核函数的作用。


简单来说就是:

应用程序调用select,进入内核调用sys_select,做些简单初始化工作,接着进入 core_sys_select,此函数主要工作是把描述符集合从用户空间复制到内核空间, 最终进入do_select,完成其主要的功能。do_select里,调用 poll_initwait,主要工作是注册poll_wait的回调函数为__pollwait,当在设备驱动的poll回调函数里调用poll_wait,其实就是调用__pollwait, __pollwait的主要工作是把当前进程挂载到等待队列里,当等待的事件到来就会唤醒此进程。接着执行for循环,循环里首先遍历每个文件描述符,调用对应描述符的poll回调函数,检测是否就绪,遍历完所有描述符之后,只要有描述符处于就绪状态,信号中断,出错或者超时,就退出循环,否则会调用schedule_xxx函数,让当前进程睡眠,一直到超时或者有描述符就绪被唤醒。接着又会再次遍历每个描述符,调用poll再次检测。如此循环,直到符合条件才会退出。

3、poll的系统调用相关细节

下图是poll调用的相关内核函数:


其中do_pollfd是对每个描述符调用 其回调poll状态轮训。
poll比select的好处就是没有描述多少限制,select 有1024 的限制,描述符不能超过此值,poll不受限制。

我们从上面分析,可以总结出select/poll天生的缺陷:
1)每次调用select/poll都需要要把描述符集合从用户空间copy到内核空间,检测完成之后,又要把检测的结果集合从内核空间copy到用户空间。当描述符很多,而且select经常被唤醒,这种开销会比较大。
2)如果说描述符集合来回复制不算什么,那么多次全部描述符遍历就比较恐怖了,我们在应用程序中,每次调用select/poll 都必须首先遍历描述符,把他们加到fd_set集合里,这是应用层的第一次遍历,接着进入内核空间,至少进行一次遍历和调用每个描述符的poll回调检测,一般可能是2次遍历,第一次没发现就绪描述符,加入等待队列,第二次是被唤醒,接着再遍历一遍。再回到应用层,我们还必须再次遍历所有描述符,用 FD_ISSET检测结果集。如果描述符很多,这种遍历就很消耗CPU资源了。


3)描述符多少限制,当然poll没有限制,select却有1024的硬性限制,除了修改内核增加1024限制外没别的办法。
既然有这么些缺点 ,那不是 select/poll变得一无是处了,那就大错特错了。他们依然是代码移植的最好函数,因为几乎所有平台都有对它们的实现提供接口。
在描述符不是太多,他们依然十分出色的完成多路复用IO, 而且如果每个连接上的描述符都处于活跃状态,他们的效率其实跟epoll也差不了多少。曾经使用多个线程+每个线程采用poll的办法开发TCP服务器,处理文件收发,连接达到几千个,当时的瓶颈已经不在网络IO,而在磁盘IO了。

3、epoll系统调用相关细节(图好像有点问题.....)


epoll只是select/poll的扩展,他不是在linux内核中另起炉灶,做颠覆性的设计的,他只是在select的基础上来解决他们的缺陷。他的底层依然需要设备驱动提供poll回调来作为状态检测基础。
epoll分为三个函数 epoll_create,epoll_ctl, epoll_wait 、他们的实现在 eventpoll.c代码里。

epoll_create创建epoll描述符,用来管理所有添加进去的描述符,epoll_ctl 用来添加,修改或者删除描述符。epoll_wait等待描述符事件。
epoll_wait的等待已经不再是轮训方式的等待了,epoll内部有个描述符就绪队列,epoll_wait只检测这个队列即可,他采用睡眠一会检测一下的方式,如果发现描述符就绪队列不为空,就把此队列中的描述符copy到用户空间,然后返回。
描述符就绪队列里的数据又是从何而来的?
原来使用 epoll_ctl添加新描述符时候,epoll_ctl内核实现里会修改两个回调函数,
一个是 poll_table结构里的qproc回调函数指针,在 select中是 __pollwait函数,在epoll中换成 ep_ptable_queue_proc,当在epoll_ctl中调用新添加的描述符的poll回调时候,底层驱动就会调用 poll_wait添加等待队列,底层驱动调用poll_wait时候,其实就是调用ep_ptable_queue_proc,此函数会修改等待队列的回调函数为 ep_poll_callback, 并加入到等待队列头里;一旦底层驱动发现数据就绪,就会调用wake_up唤醒等待队列,从而 ep_poll_callback将被调用,在ep_poll_callback中 会把这个就绪的描述符添加到 epoll的描述符就绪队列里,并同时唤醒 epoll_wait 所在的进程。如此这般,就是epoll的内核实现的精髓。看他是如何解决 select/poll的缺陷的, 首先他通过 epoll_ctl的EPOLL_CTL_ADD命令把描述符添加进epoll内部管理器里,只需添加一次即可,直到用 epoll_ctl的EPOLL_CTL_DEL命令删除此描述符为止,而不像select/poll是每次执行都必须添加,很显然大量减少了描述符在内核和用户空间不断的来回copy的开销。其次虽然 epoll_wait内部也是循环检测,但是它只需检测描述符就绪队列是否为空即可,比起select/poll必须轮训每个描述符的poll,其开销简直可以忽略不计。他同时也没描述符多少的限制,只要你机器的内存够大,就能容纳非常多的描述符。



<think>嗯,用户的问题是关于read系统调用select/poll/epoll之间的区别。我需要先理清楚每个系统调用的基本功能和用途,然后找出它们的区别在哪里。首先,read是一个用于从文件描述符读取数据的系统调用。它通常是阻塞的,也就是说,如果数据没有准备好,进程会一直等待直到有数据可读。这可能在高并发场景下导致性能问题,因为每个read调用都可能阻塞一个线程,需要多线程或多进程来处理多个连接,这样资源消耗会很大。接下来是selectpollepoll,这些都属于I/O多路复用机制。它们的作用是允许一个进程同时监视多个文件描述符,看看哪些已经准备好进行I/O操作。这样,单个线程就可以管理多个连接,提高了效率。用户提到的引用里提到,这些机制都是同步I/O,因为它们需要应用自己处理读写,而异步I/O则是由内核负责数据拷贝的。所以,select/poll/epoll属于同步但非阻塞的模式? 需要比较它们的区别。首先,read是直接进行I/O操作,而多路复用机制是用于检测I/O就绪状态。也就是说,当使用select等函数时,一旦检测到某个fd就绪,应用程序才会调用read,这样read就不会阻塞,因为已经有数据准备好了。这可以减少阻塞时间,提高吞吐量。用户提供的引用中提到,selectpoll需要遍历所有fd来检查状态,而epoll通过事件驱动的方式,只通知就绪的fd。这可能涉及到效率问题,尤其是在大量连接的情况下,epoll的效率更高。比如引用[1]和[2]提到,selectpoll需要无差别轮询,而epoll会通知具体的事件,这样应用程序不需要遍历所有fd,节省了CPU资源。 另外,关于同步和异步的问题,引用[3]指出,select/poll/epoll都是同步I/O,因为实际的读写操作还是由应用程序自己完成的,并且这个过程是阻塞的。而异步I/O则是内核完成读写后通知应用,不需要应用自己处理。所以,虽然多路复用机制提高了检测就绪状态的效率,但实际的read/write仍然是同步的。需要把read和这些多路复用机制结合起来解释。比如,使用epoll时,程序先用epoll_wait等待事件,当某个fd就绪后,再调用read,这时read会立即返回数据,不会阻塞。这样,结合非阻塞的fd,可以实现高效的I/O处理。 另外,还要考虑使用场景。比如,read适用于简单的单连接情况,而多路复用适用于高并发的网络服务器,如Web服务器、数据库等,需要同时处理成千上万的连接。这时候使用多路复用机制可以显著减少线程或进程的数量,降低资源消耗。 可能用户的问题背后是想了解如何优化I/O性能,或者理解阻塞与非阻塞、同步与异步的区别。需要确保回答中涵盖这些概念,并结合具体的系统调用进行说明。总结一下,区别主要在以下几个方面:1.功能目的:read直接读取数据,而多路复用用于监控多个fd的就绪状态。 2.阻塞行为:read可能阻塞在数据等待,而多路复用阻塞在检测就绪状态。 3.效率:多路复用尤其是epoll在处理大量连接时更高效。4.使用模式:多路复用通常与非阻塞I/O结合,减少线程资源消耗。 需要确保回答结构清晰,分点说明,并引用提供的资料来支持观点。比如引用[3]中的同步I/O解释,引用[2]的事件驱动机制等。</think>### read系统调用select/poll/epoll的区别分析 #### 1. **功能定位** - **`read`系统调用**:直接用于从文件描述符(如文件、套接字)读取数据。若数据未就绪,默认会**阻塞**当前线程,直到数据到达或超时[^3]。 - **`select/poll/epoll`**:属于**I/O多路复用**机制,用于**监控多个文件描述符的状态**(如可读、可写、异常),并通知用户哪些描述符已就绪,但**不直接执行读写操作**[^3][^2]。 #### 2. **阻塞行为** - **`read`的阻塞**:若未设置非阻塞模式(O_NONBLOCK),`read`会一直等待数据就绪,导致线程无法处理其他任务。 - **多路复用的阻塞**:`select/poll/epoll`的阻塞发生在**等待描述符就绪阶段**,一旦检测到就绪的fd,后续的`read`操作可立即执行,避免无效等待[^3][^2]。 #### 3. **效率与性能** - **`read`单线程模式**:需为每个fd创建独立线程/进程,资源消耗大,适用于低并发场景。 - **多路复用模式**: - **`select/poll`**:需遍历所有被监控的fd,时间复杂度为$O(n)$,性能随fd数量增加而下降[^1][^4]。 - **`epoll`**:基于事件驱动,仅关注活跃的fd,时间复杂度为$O(1)$,适合高并发场景(如万级连接)[^2][^3]。 #### 4. **使用模式** - **`read`直接操作数据流**: ```c char buffer[1024]; int bytes = read(fd, buffer, sizeof(buffer)); // 可能阻塞 ``` - **多路复用配合`read`**: ```c // 使用epoll_wait检测就绪fd int n = epoll_wait(epfd, events, MAX_EVENTS, timeout); for (int i = 0; i < n; i++) { if (events[i].events & EPOLLIN) { read(events[i].data.fd, buffer, sizeof(buffer)); // 非阻塞读取 } } ``` #### 5. **同步与异步特性** - 两者均为**同步I/O**:`read`和多路复用均需用户程序自行处理数据读写,且`read`操作本身是阻塞的(即使fd已就绪)[^3]。 - **异步I/O对比**:异步I/O(如Linux的`io_submit`)由内核完成数据拷贝后通知用户,无需主动调用`read`[^3]。 --- ### 总结对比表 | 特性 | `read` | `select/poll` | `epoll` | |--------------------|-------------------------|------------------------|------------------------| | **核心功能** | 读取数据 | 监控多个fd状态 | 事件驱动监控 | | **时间复杂度** | 无监控阶段 | $O(n)$ | $O(1)$ | | **适用场景** | 单连接/简单I/O | 低并发(数百连接) | 高并发(万级连接) | | **资源消耗** | 需多线程/进程 | 全量遍历fd | 仅处理活跃fd | | **内核支持** | 所有系统 | 所有系统 | Linux特有 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值