epoll究竟是同步的还是异步的?

news2024/10/9 3:06:47

简单说一句话,你需要分层看这个事:

  • epoll 这个系统调用,是同步的,也就是必须等待操作系统返回值。

  • 而底层用了 epoll 的封装后的框架,可以是异步的,只要你暴露给外部的接口,无需等待你的返回值即可。

  • 再多说些,epoll 这个系统调用的底层内核设计里,每个 IO 事件的通知等待,是异步的。但这不影响,epoll 这个系统调用对外部来说,是一个同步的接口。

所以你说,有的地方说同步,有的地方说异步,其实是不同分层的视角看。

而之所以产生这个问题,一方面是计算机分层思维没有养成,另一方面是对 IO 模型的了解还不够透彻,我这里从头梳理下,希望可以帮到你。

我们先介绍下 UNIX/Linux 中几个基本概念

  • 用户空间与内核空间

  • 进程切换

  • 进程的阻塞

  • 文件描述符

  • 缓存 I/O

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对于 32 位系统而言,它的寻址空间 (虚拟存储空间) 为 4G (2 的 32 次幂)。操作系统核心就是内核。可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

针对 Unix/Linux 操作系统而言,将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为内核空间,而将较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个进程使用,称为用户空间。

进程切换

为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。

进程运行切换,进行下面几个变化:

  1. 保存处理机上下文,包括程序计数器和其他寄存器;

  2. 更新 PCB 信息;

  3. 把进程的 PCB 移入相应的队列,如就绪、在某事件阻塞等队列;

  4. 选择另一个进程执行,并更新其 PCB;

  5. 更新内存管理的数据结构;

  6. 恢复处理机上下文

进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新任务做等,则由系统自动执行阻塞 (Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了 CPU 资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用 CPU 资源的。

文件描述符

文件描述符是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于 Unix、Linux 这样的操作系统。

缓存 I/O

缓存 I/O 又称为标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

IO 多路复用

概念

IO 多路复用是一种 IO 同步模型,实现一个线程可以监控多个文件句柄;如果文件句柄就绪,就能通知应用程序进行相应读写操作;没有文件句柄就绪,就会阻塞应用程序,让出 CPU。

多路是指的是网络连接,复用是指的是同一线程。

阻塞 IO

服务端为了处理客户端的连接和请求的数据,写了如下代码。

listenfd = socket();   // 打开一个网络通信端口
bind(listenfd);        // 绑定
listen(listenfd);      // 监听
while(1) {
  connfd = accept(listenfd);  // 阻塞建立连接
  int n = read(connfd, buf);  // 阻塞读数据
  doSomeThing(buf);  // 利用读到的数据做些什么
  close(connfd);     // 关闭连接,循环等待下一个连接
}

这段代码会执行得磕磕绊绊,就像这样。

可以看到,服务端的线程阻塞在了两个地方,一个是 accept 函数,一个是 read 函数。

如果再把 read 函数的细节展开,我们会发现其阻塞在了两个阶段。

这就是传统的阻塞 IO。

整体流程如下图。

所以,如果这个连接的客户端一直不发数据,那么服务端线程将会一直阻塞在 read 函数上不返回,也无法接受其他客户端连接。

这肯定是不行的。

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

非阻塞 IO

为了解决上面的问题,其关键在于改造这个 read 函数。

有一种聪明的办法是,每次都创建一个新的进程或线程,去调用 read 函数,并做业务处理。

while(1) {
  connfd = accept(listenfd);  // 阻塞建立连接
  pthread_create(doWork);  // 创建一个新的线程
}
void doWork() {
  int n = read(connfd, buf);  // 阻塞读数据
  doSomeThing(buf);  // 利用读到的数据做些什么
  close(connfd);     // 关闭连接,循环等待下一个连接
}

这样,当给一个客户端建立好连接后,就可以立刻等待新的客户端连接,而不用阻塞在原客户端的 read 请求上。

不过,这不叫非阻塞 IO,只不过用了多线程的手段使得主线程没有卡在 read 函数上不往下走罢了。操作系统为我们提供的 read 函数仍然是阻塞的。

所以真正的非阻塞 IO,不能是通过我们用户层的小把戏,而是要恳请操作系统为我们提供一个非阻塞的 read 函数。

这个 read 函数的效果是,如果没有数据到达时(到达网卡并拷贝到了内核缓冲区),立刻返回一个错误值(-1),而不是阻塞地等待。

操作系统提供了这样的功能,只需要在调用 read 前,将文件描述符设置为非阻塞即可。

fcntl(connfd, F_SETFL, O_NONBLOCK);
int n = read(connfd, buffer) != SUCCESS);

这样,就需要用户线程循环调用 read,直到返回值不为 -1,再开始处理业务。

这里我们注意到一个细节。

非阻塞的 read,指的是在数据到达前,即数据还未到达网卡,或者到达网卡但还没有拷贝到内核缓冲区之前,这个阶段是非阻塞的。

当数据已到达内核缓冲区,此时调用 read 函数仍然是阻塞的,需要等待数据从内核缓冲区拷贝到用户缓冲区,才能返回。

整体流程如下图

IO 多路复用

为每个客户端创建一个线程,服务器端的线程资源很容易被耗光。

当然还有个聪明的办法,我们可以每 accept 一个客户端连接后,将这个文件描述符(connfd)放到一个数组里。

fdlist.add(connfd);

然后弄一个新的线程去不断遍历这个数组,调用每一个元素的非阻塞 read 方法。

while(1) {
  for(fd <-- fdlist) {
    if(read(fd) != -1) {
      doSomeThing();
    }
  }
}

这样,我们就成功用一个线程处理了多个客户端连接。

你是不是觉得这有些多路复用的意思?

但这和我们用多线程去将阻塞 IO 改造成看起来是非阻塞 IO 一样,这种遍历方式也只是我们用户自己想出的小把戏,每次遍历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用。

在 while 循环里做系统调用,就好比你做分布式项目时在 while 里做 rpc 请求一样,是不划算的。

所以,还是得恳请操作系统老大,提供给我们一个有这样效果的函数,我们将一批文件描述符通过一次系统调用传给内核,由内核层去遍历,才能真正解决这个问题。

IO 多路复用的实现

  • select

  • poll

  • epoll

select

这种实现只仅仅知道有 I/O 事件发生,但是不清楚哪个那几个流(可能有一个,多个,甚至全部), 因此我们只能无差别进行轮询所有流,获取读取数据的流以及写数据的流,来对其相应操作。因此 select 具有 o (n) 的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

select 调用过程

  1. copy_from_user 从用户空间拷贝 fd_set 到内核空间

  2. 注册回调函数__pollwait

  3. 遍历所有 fd,调用其对应的 poll 方法(对于 socket,这个 poll 方法是 sock_poll,sock_poll 根据情况会调用到 tcp_poll,udp_poll 或者 datagram_poll)

  4. 以 tcp_poll 为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

  5. __pollwait 的主要工作就是把 current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于 tcp_poll 来说,其等待队列是 sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时 current 便被唤醒了。

  6. poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个 mask 掩码给 fd_set 赋值。

  7. 如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 是调用 select 的进程(也就是 current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout 指定),还是没人唤醒,则调用 select 的进程会重新被唤醒获得 CPU,进而重新遍历 fd,判断有没有就绪的 fd。

  8. 把 fd_set 从内核空间拷贝到用户空间。

select 函数接口

#include <sys/select.h>
#include <sys/time.h>
 
#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS)
 
// 数据结构 (bitmap)
typedef struct {
    unsigned long fds_bits[__FDSET_LONGS];
} fd_set;
 
// API
int select(
    int max_fd, 
    fd_set *readset, 
    fd_set *writeset, 
    fd_set *exceptset, 
    struct timeval *timeout
)                              // 返回值就绪描述符的数目
 
FD_ZERO(int fd, fd_set* fds)   // 清空集合
FD_SET(int fd, fd_set* fds)    // 将给定的描述符加入集合
FD_ISSET(int fd, fd_set* fds)  // 判断指定描述符是否在集合中 
FD_CLR(int fd, fd_set* fds)    // 将给定的描述符从文件中删除  

select 使用示例

int main() {
  /*
   * 这里进行一些初始化的设置,
   * 包括socket建立,地址的设置等,
   */
 
  fd_set read_fs, write_fs;
  struct timeval timeout;
  int max = 0;  // 用于记录最大的fd,在轮询中时刻更新即可
 
  // 初始化比特位
  FD_ZERO(&read_fs);
  FD_ZERO(&write_fs);
 
  int nfds = 0; // 记录就绪的事件,可以减少遍历的次数
  while (1) {
    // 阻塞获取
    // 每次需要把fd从用户态拷贝到内核态
    nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
    // 每次需要遍历所有fd,判断有无读写事件发生
    for (int i = 0; i <= max && nfds; ++i) {
      if (i == listenfd) {
         --nfds;
         // 这里处理accept事件
         FD_SET(i, &read_fd);//将客户端socket加入到集合中
      }
      if (FD_ISSET(i, &read_fd)) {
        --nfds;
        // 这里处理read事件
      }
      if (FD_ISSET(i, &write_fd)) {
         --nfds;
        // 这里处理write事件
      }
    }
  }

select 局限性

select 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理,因此存在如下局限性:

  • 单个进程所打开的 FD (文件句柄) 是有限制的,通过 FD_SETSIZE 设置,默认 1024 ;

  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个资源开销在 fd 很多时会很大; (即需要维护一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大)

  • 对 socket 扫描时是线性扫描,采用轮询的方法,效率较低(高并发);

当套接字比较多的时候,每次 select () 都要通过遍历 FD_SETSIZE 个 Socket 来完成调度,不管哪个 Socket 是活跃的,都遍历一遍。这会浪费很多 CPU 时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是 epoll 与 kqueue 做的。

poll

poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态,但最本质区别就是它没有最大连接数的限制,因为它基于链表存储的。

poll 函数接口

#include <poll.h>
// 数据结构
struct pollfd {
    int fd;                         // 需要监视的文件描述符
    short events;                   // 需要内核监视的事件
    short revents;                  // 实际发生的事件
};
 
// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

poll 使用示例

// 先宏定义长度
#define MAX_POLLFD_LEN 4096  
 
int main() {
  /*
   * 在这里进行一些初始化的操作,
   * 比如初始化数据和socket等。
   */
 
  int nfds = 0;
  pollfd fds[MAX_POLLFD_LEN];
  memset(fds, 0, sizeof(fds));
  fds[0].fd = listenfd;
  fds[0].events = POLLRDNORM;
  int max  = 0;  // 队列的实际长度,是一个随时更新的,也可以自定义其他的
  int timeout = 0;
 
  int current_size = max;
  while (1) {
    // 阻塞获取
    // 每次需要把fd从用户态拷贝到内核态
    nfds = poll(fds, max+1, timeout);
    if (fds[0].revents & POLLRDNORM) {
        // 这里处理accept事件
        connfd = accept(listenfd);
        //将新的描述符添加到读描述符集合中
    }
    // 每次需要遍历所有fd,判断有无读写事件发生
    for (int i = 1; i < max; ++i) {     
      if (fds[i].revents & POLLRDNORM) { 
         sockfd = fds[i].fd
         if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
            // 这里处理read事件
            if (n == 0) {
                close(sockfd);
                fds[i].fd = -1;
            }
         } else {
             // 这里处理write事件     
         }
         if (--nfds <= 0) {
            break;       
         }   
      }
    }
  }

poll 局限性

它没有最大连接数的限制,原因是它是基于链表来存储的,同样存在局限性:

  • 每次调用 poll ,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;

  • 对 socket 扫描是线性扫描,采用轮询的方法,效率较低(高并发时)

epoll

epoll 即 event poll,相比之前 select 和 poll,epoll 会把哪个流发生了怎样的 I/O 事件通知用户。所以 epoll 实际上是 ** 事件驱动(每个事件关联上 fd)** 的,此时用户对这些流的操作都是有意义的。(复杂度降低到了 O (1))。

epoll 函数接口

当某一进程调用 epoll_create 方法时,Unix/Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与 epoll 的使用方式密切相关。具体结构如下:

#include <sys/epoll.h>
 
// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};
 
// API
int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程

每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来 (红黑树的插入时间效率是 lgn,其中 n 为红黑树元素个数)。

而所有添加到 epoll 中的事件都会与设备 (网卡) 驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫 ep_poll_callback, 它会将发生的事件添加到 rdlist 双链表中

在 epoll 中,对于每一个事件,都会建立一个 epitem 结构体,如下:

struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可。如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。如下图

从上图可以归纳出:通过红黑树和双链表数据结构,并结合回调机制,成就了 epoll 高效。从上述原理中,我们可以掌握 epoll 了用法,三部曲:

  • 第一步:epoll_create () 系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

  • 第二步:epoll_ctl () 系统调用。通过此调用向 epoll 对象中添加、删除、修改感兴趣的事件,返回 0 标识成功,返回 - 1 表示失败。

  • 第三部:epoll_wait () 系统调用。通过此调用收集收集在 epoll 监控中已经发生的事件。

epoll 使用示例

int main(int argc, char* argv[]){
   /*
   * 在这里进行一些初始化的操作,
   * 比如初始化数据和socket等。
   */
 
    // 内核中创建ep对象
    epfd=epoll_create(256);
    // 需要监听的socket放到ep中
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
 
    while(1) {
      // 阻塞获取
      nfds = epoll_wait(epfd,events,20,0);
      for(i=0;i<nfds;++i) {
          if(events[i].data.fd==listenfd) {
              // 这里处理accept事件
              connfd = accept(listenfd);
              // 接收新连接写到内核对象中
              epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
          } else if (events[i].events&EPOLLIN) {
              // 这里处理read事件
              read(sockfd, BUF, MAXLINE);
              //读完后准备写
              epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
          } else if(events[i].events&EPOLLOUT) {
              // 这里处理write事件
              write(sockfd, BUF, n);
              //写完后准备读
              epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
          }
      }
    }
    return 0;
}

epoll 优点

  • 没有最大并发连接的限制,能打开的 FD 的上限远大于 1024(1G 的内存上能监听约 10 万个端口);

  • 效率提升,不是轮询的方式,不会随着 FD 数目的增加效率下降。只有活跃可用的 FD 才会调用 callback 函数;即 Epoll 最大的优点就在于它只管你 “活跃” 的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll 的效率就会远远高于 select 和 poll;

  • 内存拷贝,利用 mmap () 文件映射内存加速与内核空间的消息传递;即 epoll 使用 mmap 减少复制开销。

epoll 局限性

只能工作在 Linux 平台上。

epoll 的 LT 与 ET 模式区别

epoll 有 EPOLLLT 和 EPOLLET 两种触发模式,LT 是默认的模式,ET 是 “高速” 模式。

  • LT 模式下,只要这个 fd 还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作;

  • ET 模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否还有数据可读。所以在 ET 模式下,read 一个 fd 的时候一定要把它的 buffer 读完,或者遇到 EAGIN 错误。

epoll 使用 “事件” 的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用类似 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知。

区别与联系

select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

epoll 跟 select 都能提供多路 I/O 复用的解决方案。在现在的 Linux 内核里有都能够支持,其中 epoll 是 Linux 所特有,而 select 则应该是 POSIX 所规定,一般操作系统均有实现。

类目

select

poll

epoll

操作方式

遍历

遍历

回调

数据结构

bitmap

数组

红黑树

最大连接数

1024(x86);2048(x64)

无上限

无上限

最大支持文件描述符数

一般有最大限制

65535

65535

fd 拷贝

每次调用 select,都需要将 fd 集合从用户态拷贝内核态

每次调用 poll,都需要将 fd 集合从用户态拷贝内核态

首次调用需要 epoll_ctl 拷贝,每次调用 epoll_wait

工作模式

LT

LT

支持 ET 高效模式

工作效率

每次调用都进行线性遍历,时间复杂度为 O (n)

每次调用都进行线性遍历,时间复杂度为 O (n)

事件通知方式,每当 fd 就绪,系统注册的回调函数就会被调用,将就绪 fd 放入 readList 存放,时间复杂为 O (1)

epoll 是 Linux 目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超 select 和 poll。目前流行的高性能 web 服务器 Nginx 正式依赖于 epoll 提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程 + 阻塞 I/O 方式可能性能更好。

单个进程支持打开最大连接数

  • select:单个进程所能打开的最大连接数有 FD_SETSIZE 宏定义,其大小是 32 个整数的大小(在 32 位的机器上,大小就是 32_32,同理 64 位机器上 FD_SETSIZE 为 32_64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

  • poll:poll 本质上和 select 没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。

  • epoll:虽然连接数有上限,但是很大,1G 内存的机器上可以打开 10 万左右的连接,2G 内存的机器可以打开 20 万左右的连接。

FD 剧增后带来的 IO 效率问题

  • select:因为每次调用时都会对连接进行线性遍历,所以随着 FD 的增加会造成遍历速度慢的 “线性下降性能问题”。

  • poll:同上

  • epoll:因为 epoll 内核中实现是根据每个 fd 上的 callback 函数来实现的,只有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下,使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活跃的情况下,可能会有性能问题。

消息传递方式

  • select:内核需要将消息传递到用户空间,都需要内核拷贝动作

  • poll:同上

  • epoll:epoll 通过内核和用户空间共享一块内存来实现的。

总结

select,poll 实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替,但是 select 和 poll 在 “醒着” 的时候要遍历整个 fd 集合,而 epoll 在 “醒着” 的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升。

select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(在 epoll_wait 的开始,注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列)。这也能节省不少的开销。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2197834.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DDD简介

概述 传统的数据驱动开发模式&#xff0c;View、Service、Dao这种三层分层模式&#xff0c;会很自然的写出过程式代码&#xff0c;这种开发方式中的对象只是数据载体&#xff0c;而没有行为&#xff0c;是一种贫血对象模型。以数据为中心&#xff0c;以数据库ER图为设计驱动&a…

什么是变阻器?

变阻器是一种电子元件&#xff0c;主要用于调整电路中的电阻值&#xff0c;从而实现对电流、电压等电学参数的控制。它在电路中起到非常重要的作用&#xff0c;广泛应用于各种电子设备和实验装置中。 变阻器的主要作用是改变电路中的电阻值。在电路中&#xff0c;电阻值的大小…

二部图简单理解

目录 二部图简单理解 一、定义 二、性质与定理 三、原理举例 四、应用 二部图简单理解 二部图(Bipartite Graph),又称二分图,是图论中的一种特殊模型。以下是对二部图的详细介绍及原理举例: 一、定义 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(…

目标检测or实例分割中AP、MAP的计算

参考链接&#xff1a; 目标检测中AP、MAP的计算_51CTO博客_目标检测map计算 举个例子&#xff1a;

【JVM】内存分析工具JConsole/Visual VM

1 缘起 日常补充JVM调优&#xff0c;调优实践前需要学习一些理论做支撑&#xff0c; JVM调优三步&#xff1a;理论>GC分析>JVM调优&#xff0c; 我们会有一些玩笑话说&#xff0c;做了这么久Java开发&#xff0c;做过JVM调优吗&#xff1f; 做过&#xff0c;面试时。当然…

【深度学习】yolov8n模型的剪枝操作记录

原始 剪枝微调后 可以看到模型大小了&#xff0c; 测试结果显示再cpu 上加速5%-10% from ultralytics import YOLOimport time # Load a pretrained YOLO11n model count_num 500 def test1():model YOLO("/home/justin/Desktop/code/v8_prun/runs/detect/train3/weig…

LC刷题专题:记忆化搜索

文章目录 576. 出界的路径数 本刷题专栏记录自己的记忆化搜索的做题。 576. 出界的路径数 题目链接及描述&#xff1a; https://leetcode.cn/problems/out-of-boundary-paths/description/ 第一次看到这个题目可能先入为主了&#xff0c;首先想到的是使用动态规划做&#xff…

19.安卓逆向-frida基础-hook分析调试技巧1-hookMD5

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;图灵Python学院 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要盲目相信。 工…

C#多线程基本使用和探讨

线程是并发编程的基础概念之一。在现代应用程序中&#xff0c;我们通常需要执行多个任务并行处理&#xff0c;以提高性能。C# 提供了多种并发编程工具&#xff0c;如Thread、Task、异步编程和Parallel等。 Thread 类 Thread 类是最基本的线程实现方法。使用Thread类&#xff0…

【题解】【模拟】—— [NOIP2013 普及组] 表达式求值

【题解】【模拟】—— [NOIP2013 普及组] 表达式求值 [NOIP2013 普及组] 表达式求值题目背景题目描述输入格式输出格式输入输出样例输入 #1输出 #1输入 #2输出 #2输入 #3输出 #3 提示 1.简单做法1.1.题意解析1.2.AC代码 2.使用栈的做法2.1.题意解析2.2.AC代码 [NOIP2013 普及组…

C高级--shell脚本实现分支判断

题目&#xff1a; 分支结构结合test指令完成一下编程 1>判断闰年 2>输入一个数判断是否为偶数 3>使用test指令实现等级判断 90--100A 60--89B 0-50C 其他错误 代码如下&#xff1a; #!/bin/bash read -p "请输入一个年份&#xff1a;" year if [ $((y…

Cisco Meraki平台登陆

登陆以下网址 https://n4.meraki.cn/ 输入之前注册的邮箱&#xff0c;点击Next 输入之前注册时输入的密码&#xff0c;注意不是企业邮箱的密码&#xff01; 查看邮箱&#xff0c;将验证码输入&#xff0c;点击Verify&#xff08;验证&#xff09;&#xff0c;此验证码10分…

C语言-文件IO

文件IO I :input 输入&#xff0c;从文件中读取数据到内存 O:output 输出&#xff0c;把数据写入到文件 Linux系统IO 和 c语言标准IO 1、linux系统IO 1.1 简介 linux操作系统把对文件的操作封装成了多个函数&#xff0c;统称为linux系统IO。 文件描述符(File descirptor)…

笔试算法day01

目录 1.除2 2.Fibonacci数列&#xff08;Fib 数列&#xff09; 3.单词搜索 1.除2 除2&#xff01; (nowcoder.com) 算法思路&#xff1a; 只需要对最大的n个偶数进行/2即可。 将所有的偶数存进大根堆中&#xff0c;执行k次操作即可 #include <iostream> #include <…

2024年AI知识库哪家强?8款主流软件对比分析

在当今这个信息爆炸的时代&#xff0c;如何高效地管理、搜索和共享知识成为了一个重要的问题。AI知识库作为一种先进的解决方案&#xff0c;正受到越来越多企业和个人的青睐。本文将对比分析8款主流的AI知识库软件&#xff0c;帮助大家找到最适合自己的工具。 1. HelpLook AI知…

机器学习K近邻算法——回归问题K近邻算法示例

针对“数据4.1”&#xff0c;讲解回归问题的K近邻算法&#xff0c;以V1&#xff08;营业利润水平&#xff09;为响应变量&#xff0c;以V2&#xff08;固定资产投资&#xff09;、V3&#xff08;平均职工人数&#xff09;、V4&#xff08;研究开发支出&#xff09;为特征变量。…

Flutter 进阶:根据IP地址判断用户国家/地区

在应用开发中根据IP地址判断用户国家/地区的两种方法 引言 在开发国际化应用时&#xff0c;了解用户的地理位置至关重要。这不仅影响用户体验&#xff0c;还关系到内容展示和合规性。本文将介绍两种通过IP地址判断用户所在国家或地区的方法。 方法一&#xff1a;使用 ip-api…

redis高级(面试题二)

目录 一、redis的五大数据结构有哪些&#xff1f;zset底层是什么结构&#xff1f; 1、redis五大数据结构有哪些&#xff1f; 2、什么是skiplist&#xff1f; 3、zset底层是什么结构&#xff1f; 二、Redis的内存过期策略是什么&#xff1f;Redis的内存淘汰策略有哪些&#…

【专题】数据库系统的基本原理

1. 数据库系统概述 1.1. 数据库系统的应用 电信业、银行业、金融业、销售业、联机的零售商、大学、航空业、人力资源、制造业等等。 1.2 数据库系统的概念 (1) 数据&#xff08;Data&#xff09; 数据是数据库存储的基本对象。是描述现实世界中各种具体事物或抽象概念的、可…

Nuxt日志监控(服务端及客户端日志检测)

此文章主要讲解如何使用Nuxt进行日志监控&#xff0c;例如服务端请求日志&#xff0c;客户端请求日志&#xff0c;方便线上出现问题能及时排查问题所在 一、下载依赖 npm install winston winston-daily-rotate-file二、plugin下创建日志处理插件winston.js&#xff0c;对日志…