阻塞、非阻塞、异步、同步以及select/poll和epoll

本文详细解释了IO操作中的阻塞、非阻塞、同步与异步的区别,以及select、poll和epoll等多路复用技术的工作原理和应用。

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

针对IO,总是涉及到阻塞、非阻塞、异步、同步以及select/poll和epoll的一些描述,那么这些东西到底是什么,有什么差异?

一般来讲一个IO分为两个阶段:
  1. 等待数据到达
  2. 把数据从内核空间拷贝到用户空间

现在假设一个进程/线程A,试图进行一次IO操作。
  • A发出IO请求,两种情况:
      1)立即返回
      2)由于数据未准备好,需要等待,让出CPU给别的线程,自己sleep
      第一种情况就是非阻塞,A为了知道数据是否准备好,需要不停的询问,而在轮询的空歇期,理论上是可以干点别的活,例如喝喝茶、泡个妞。
      第二种情况就是阻塞,A除了等待就不能做任何事情。
  • 数据终于准备好了,A现在要把数据取回去,有几种做法:  1)A自己把数据从内核空间拷贝到用户空间。
      2)A创建一个新线程(或者直接使用内核线程),这个新线程把数据从内核空间拷贝到用户空间。
      第一种情况,所有的事情都是同一个线程做,叫做同步,有同步阻塞(BIO)、同步非阻塞(NIO)
      第二种情况,叫做异步,只有异步非阻塞(AIO)


    同步阻塞:

      同一个线程在IO时一直阻塞,直到读取数据成功,把数据从核心空间拷贝到用户空间

      

      

    同步非阻塞:

      同一个线程发起IO后,立即获得返回,后面定期轮询数据读取情况,发现数据读取成功,把数据从核心空间拷贝到用户空间


     

      异步非阻塞:

      一个线程发起IO后,立即返回,由另外的线程发现数据读取成功,把数据从核心空间拷贝到用户空间。


      


    下面说一下多路复用:select/poll、epoll

    • select是几乎所有unix、linux都支持的一种多路IO方式,通过select函数发出IO请求后,线程阻塞,一直到数据准备完毕,然后才能把数据从核心空间拷贝到用户空间,所以select是同步阻塞方式。
        int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    • 参数n表示监控的所有fd中最大值+1
    • readfds、writefds和exceptfds分别表示可读、可写、异常的文件句柄,这个文件句柄中每一个bit表示一个文件fd,所以能够表示的最大文件数和fd_set的长度有关,

    •   假设fd_set的长度为1字节(即8bit),则可以表示8个可读文件、8个可写文件、8个异常文件句柄。下面以读文件为例:

    •   使用select的时候,先初始化FD_ZERO(fd_set *set),把8bit全部置为0,readfds=00000000
        使用FD_SET(int fd, fd_set *set)来把文件fd设置到fd_set中,例如3个文件fd=2,fd=3,fd=5,则readfds=00010110
        然后使用select(6, readfds, 0, 0, 0)阻塞等待,若此时fd=2文件可读,则此时readfds=00000010(fd=5和fd=3对应的bit被清0)
        使用FD_ISSET(int fd, fd_set *set)函数来判断fd对应的bit是否为1,如果为1则可读。


    • poll对select的使用方法进行了一些改进,突破了最大文件数的限制,同时使用更加方便一些。
      int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
      struct pollfd {
          int fd;           /* 对应的文件描述符 */
          short events;     /* 要监听的事件,例如POLLIN|POLLPRI */
          short revents;    /* 返回的事件,用于在poll返回时携带该fd上发生的事情,在poll调用时,该字段会自动被清空 */
      };

    • 通过poll函数发出IO请求后,线程阻塞,直到数据准备完毕,poll函数在pollfd中通过revents字段返回事件,然后线程把数据从核心空间拷贝到用户空间,

    • 所以poll同样是同步阻塞方式,性能同select相比没有改进。


    • epoll是linux为了解决select/poll的性能问题而新搞出来的机制,基本的思路是:由专门的内核线程来不停地扫描fd列表,有结果后,把结果放到fd相关的链表中,
      用户线程只需要定期从该fd对应的链表中读取事件就可以了。同时,为了节省把数据从核心空间拷贝到用户空间的消耗,采用了mmap的方式,允许程序在用户空间直接访问数据所在的内核空间,不需要把数据copy一份。

    • epoll一共有3个函数:

    • 1.创建epoll文件描述符
      int epoll_create(int size);

      2.把需要监听的文件fd和事件加入到epoll文件描述符,也可以对已有的fd进行修改和删除
      文件fd保存在一个红黑树中,该fd的事件保存在一个链表中(每个fd一个事件链表),事件由内核线程负责填充,用户线程读取
      int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 */
                  };

      3.用户线程定期轮询epoll文件描述符上的事件,事件发生后,读取事件对应的epoll_data,该结构中包含了文件fd和数据地址,由于采用了mmap,程序可以直接读取数据。
      int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

      有人把epoll这种方式叫做同步非阻塞(NIO),因为用户线程需要不停地轮询,自己读取数据,看上去好像只有一个线程在做事情
      也有人把这种方式叫做异步非阻塞(AIO),因为毕竟是内核线程负责扫描fd列表,并填充事件链表的
      个人认为真正理想的异步非阻塞,应该是内核线程填充事件链表后,主动通知用户线程,或者调用应用程序事先注册的回调函数来处理数据,如果还需要用户线程不停的轮询来获取事件信息,就不是太完美了,所以也有不少人认为epoll是伪AIO,还是有道理的。


      另外一个epoll的变化,是支持了边沿触发,以前select/poll中,每次遍历fd列表,发现fd可写、可读或异常后,就把bit置1(select)或返回对应事件(poll),
      而在epoll中,同样支持这种方式,每次fd可写、可读或异常后,就写入事件到事件链表中,还支持只在事件发生变化时才写入事件链表,例如如果事件一直是可读,则只在第一次写入链表
      业界把这两种方式分别叫做电平触发和边沿触发,像电信号(方波)一样,从高电平到低电平或低电平到高电平的“拐角”处的触发,叫做边沿触发,其他上下两个平面上的连续触发叫电平触发
      epoll支持电平触发(Level Triggered)和边沿触发(Edge Triggered),默认为电平触发
<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特有 | ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值