当前位置: 首页 > news >正文

02-Linux-IO多路复用之select、poll和epoll详解

前言:

在linux系统中,实际上所有的 I/O 设备都被抽象为了文件这个概念,一切皆文件,磁盘、网络数据、终端,甚至进程间通信工具管道 pipe 等都被当做文件对待。

在了解多路复用 select、poll、epoll 实现之前,我们先简单回忆复习以下两个概念:

一、什么是多路复用:

  • 多路:多个 socket 网络连接。
  • 复用:复用一个线程,使用一个线程来检查多个文件描述符(socket)的就绪状态。
  • 多路复用主要有三种技术:select、poll、epoll。epoll 是最新的,也是目前最好的多路复用技术。

二、五种 I/O 模型

+ blocking I/O      - 阻塞I/O
+ non-blocking I/O  - 非阻塞I/O
+ signal-driven I/O - 信号驱动I/O
+ asynchronous I/O  - 异步I/O
+ I/O multiplexing  - I/O多路复用

1. 阻塞 I/O 模型

进程/线程在从调用 recvfrom() 开始到它返回的整段时间内是被阻塞的,recvfrom() 成功返回后,应用进程/线程开始处理数据报。
在这里插入图片描述

recvfrom() 是一个系统调用函数,用于从一个已连接或未连接的套接字(socket)接收数据。函数原型如下:

ssize_t recvfrom(int sockfd,					//套接字文件描述符void *buf,						//指向接收数据的缓冲区size_t len,					//缓冲区的大小int flags,						//可选的标志参数,用于控制接收操作的行为struct sockaddr *src_addr,		//用于存储发送方的地址信息socklen_t *addrlen				//src_addr 的长度);

recvfrom() 在调用时会阻塞,直到有数据到达或发生错误。当有数据到达时,它将数据读取到指定的缓冲区中,并填充发送方的地址信息到 src_addr 参数中。如果套接字是已连接的,src_addraddrlen 参数可以设置为 NULL

  • 主要特点
    • 进程阻塞挂起不消耗CPU资源,能及时响应每个操作。
    • 实现难度低。
    • 适用并发量小的网络应用开发,不适用并发量大的应用。因为一个请求IO会阻塞进程,所以每请求分配一个处理进程(线程)去响应,系统开销大。

2. 非阻塞 I/O 模型

进程发起 I/O 系统调用后,如果内核缓冲区没有数据,需要到 I/O 设备中读取,进程返回一个错误而不会被阻塞;如果内核缓冲区有数据,内核就会把数据返回进程。
在这里插入图片描述

  • 主要特点
    • 进程轮询(重复)调用,消耗CPU的资源。
    • 实现难度低、开发应用相对阻塞IO模式较难。
    • 适用并发量较小、且不需要及时响应的网络应用开发。

3. 信号驱动 I/O 模型

当进程发起一个 I/O 操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用 I/O 读取数据。与阻塞式 I/O 或非阻塞式 I/O 模型不同,信号驱动 I/O 模型允许应用程序在进行 I/O 操作时继续执行其他任务,而不需要显式地轮询或阻塞等待 I/O 操作的完成。
在这里插入图片描述

  • 工作流程

    • 应用程序通过调用 sigaction() 来注册一个 信号处理函数(Signal Handler),用于处理特定的 I/O 相关信号,如 SIGIO
    • I/O 文件描述符 设置为信号驱动模式,通常使用 fcntl() 并设置 F_SETOWN 标志,将文件描述符的拥有者设置为当前进程。这样,当 I/O 事件发生时,内核将向该进程发送相应的信号。
    • I/O 事件(如数据到达)发生时,操作系统将为相应的文件描述符生成一个信号(通常是 SIGIO),并将其发送给拥有者进程。
    • 拥有者进程接收到信号后,会调用事先注册的信号处理函数进行相应的处理。在信号处理函数中,可以执行读取数据、写入数据等操作。
    • 信号处理函数执行完毕后,应用程序可以继续执行其他任务,而不需要显式地等待 I/O 操作的完成。
  • 主要特点

    • 实现、开发应用难度大。需要合理处理信号处理函数和信号同步的问题。
    • 适用于需要同时处理多个 I/O 事件的情况,如网络编程中的异步处理。

4. 异步 I/O 模型

当进程发起一个 I/O 操作,进程返回(不阻塞),但也不能返回结果;内核把整个 I/O 处理完后,会通知进程结果。如果 I/O 操作成功则进程直接获取到数据。
在这里插入图片描述

  • 工作原理

    • 应用程序调用系统调用函数(如 aio_read、aio_write 等)发起异步 I/O 操作。这些函数通常是系统提供的异步 I/O 接口函数。
    • 在发起异步 I/O 操作时,应用程序还需要提供一个 回调函数,该函数将在 I/O 操作完成时被调用。
    • 异步 I/O 操作被提交给操作系统或 I/O 子系统进行处理。操作系统将负责执行实际的 I/O 操作,并在操作完成后触发相应的事件。
    • I/O 操作完成时,操作系统将调用之前注册的回调函数,并将操作的结果传递给回调函数。
    • 在回调函数中,应用程序可以处理操作的结果,例如读取已接收的数据、处理错误情况等。
    • 应用程序可以继续执行其它任务,而无需等待 I/O 操作的完成。
  • 主要特点

    • 不阻塞,数据一步到位。
    • 需要操作系统的底层支持,LINUX 2.5 版本内核首现,2.6 版本产品的内核标准特性。
    • 实现、开发应用难度大。
    • 非常适合高性能高并发应用。

5. I/O 复用模型

大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存(page cache)。也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间中。这种做法的缺点就是,需要在应用程序地址空间和内核进行多次拷贝,这些拷贝动作所带来的CPU以及内存开销是非常大的。

至于为什么不能直接让磁盘控制器把数据送到应用程序的地址空间中呢?最简单的一个原因就是应用程序不能直接操作底层硬件
在这里插入图片描述
总的来说,IO分两阶段:
1)数据准备阶段
2)内核空间复制回用户进程缓冲区阶段。如下图:
在这里插入图片描述

  • 工作原理

    • 应用程序通过将多个 I/O 流(如套接字)注册到 I/O 复用机制中,以便对这些流的状态进行监视。
    • 当应用程序调用 I/O 复用机制的函数(如 selectpollepoll)时,它会被阻塞,直到至少一个注册的 I/O 满足指定的条件(如可读、可写等)。
    • 当有流满足指定条件时,I/O 复用机制并返回,并通知应用程序哪些流满足条件。
    • 应用程序可以遍历返回的流集合,检查每个流的状态,并进行相应的处理。通常,应用程序会使用条件判断语句来确定流是可读还是可写,并执行相应的读取或写入操作。
    • 应用程序可以继续执行其他任务或再次调用 I/O 复用机制的函数,以便继续监视 I/O 流的状态变化。
  • 主要特点

    • 使用单个系统调用来监视多个 I/O 流的状态,而不是针对每个流进行阻塞式的等待。
    • 有效减少系统调用次数和上下文切换开销。
    • 提高应用程序的并发性和响应性。

三、I/O 多路复用之select、poll、epoll详解

目前支持 I/O 多路复用的系统调用有 select,pselect,poll,epoll。与多进程和多线程技术相比,I/O 多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

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

select

select函数 监视的文件描述符分3类,分别是 writefdsreadfdsexceptfds

  • 当用户线程调用 select 的时候,select 将需要监控的 readfds集合 拷贝到内核空间(假设监控的仅仅是 socket可读)。
  • 然后遍历自己监控的 skb(SocketBuffer),挨个调用 skbpoll 逻辑以便检查该 socket 是否有可读事件,遍历完所有的 skb 后。
  • 如果没有任何一个 socket 可读,那么 select 会调用schedule_timeout 进入 schedule 循环,使得线程进入睡眠。如果在 timeout 时间内某个 socket 上有数据可读了,或者等待timeout 了,则调用 select 的线程会被唤醒,接下来 select 就是遍历监控的集合,挨个收集可读事件并返回给用户了。

相应的伪码如下:

int select(int nfds,					//监控的文件描述符集里最大文件描述符+1fd_set *readfds,			//监控读数据到达文件描述符集合fd_set *writefds,			//监控写数据到达文件描述符集合fd_set *exceptfds,			//监控异常发生到达文件描述符集合struct timeval *timeout		//定时阻塞监控时间,3种情况:1.NULL,永远等下去。//						 2.设置timeval,等待固定时间//						 3.设置timeval里时间均为0,检查描述字后立即返回,轮询
);//----------------select服务端伪码---------------------
//首先一个线程不断接受客户端连接,并把socket文件描述符放到一个list里
while(1){connfd = accept(listenfd);fcntl(connfd, F_SETFL, O_NONBLOCK);fdlist.add(connfd);
}
/*
select函数还是返回刚刚提交的list,应用程序依然列出所有的fd,只不过操作系统会将准备就绪的文件描述符做上标识,
用户层将不会再有无意义的系统调用开销。
*/
struct timeval timeout;
int max = 0;					//用于记录最大的fd,在轮询中时刻更新即可
//初始化比特位
FD_ZERO(&read_fd);
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){//这里处理accept事件FD_SET(i, &read_fd);	//将客户端socket加入到集合中}if(FD_ISSET(i, &read_fd)){//这里处理read事件}}
}

下面是 select 工作原理的动图:

select工作图

通过上面的select逻辑过程分析,相信大家都意识到,select存在三个问题:

  • 每次调用 select,都需要把被监控的 fds 集合从用户态空间拷贝到内核态空间,高并发场景下这样的拷贝会使得消耗的资源是很大的。
  • 能监听端口的数量有限,单个进程所能打开的最大连接数由 FD_SETSIZE 宏定义,监听上限就等于 fds_bits 位数组中所有元素的二进制位总数,其大小是32个整数的大小(在32位的机器上,大小就是32,同理64位机器上为64),当然我们可以对宏 FD_SETSIZE 进行修改,然后重新编译内核,但是性能可能会受到影响,一般该数和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max 察看。32位机默认1024个,64位默认2048。
  • 被监控的 fds集合 中,只要有一个有数据可读,整个 socket集合 就会被遍历一次调用 skpoll 函数收集可读事件:由于仅关心是否有数据可读这样一个事件,数据的到来是异步的,于是,只能挨个遍历每个socket来收集可读事件了。

poll

poll 的实现和 select 非常相似,只是描述 fd 集合的方式不同。针对 select 遗留的三个问题中(问题(2)是fd限制问题,问题(1)和(3)则是性能问题),poll 使用 pollfd结构 而不是 selectfd_set结构,这就解决了 select 的问题(2)fds集合大小限制问题。
pollselect 同样存在一个性能缺点就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

下面是 poll 的函数原型:

int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
struct pollfd{int fd;					//文件描述符short events;			//监控的事件short revents;			//监控事件中满足条件返回的事件
};//-----------------poll服务端实现伪码---------------------
struct pollfd fds[POLL_LEN];
unsigned int nfds = 0;
fds[0].fd = server_sockfd;
fds[0].events = POLLIN | POLLPRI;
nfds++;
while(1){res = poll(fds, nfds, -1);if(fds[0].revents & (POLLIN | POLLPRI)){//执行accept并加入fds中,nfds++if(--res <= 0)	continue;}//循环之后的fdsif(fds[i].revents & (POLLIN | POLLERR)){//读操作或处理异常等if(--res <= 0)	continue;}
}

poll 相比于 select 的优点:使用了 pollfd结构,使得 poll 支持的 fds 集合限制远大于 select 的1024。

由于 poll 基于链表存储,无最大连接数限制,所以有如下缺点:(1)大量描述符数组被整体复制于用户态和内核态的地址空间之间,以及个别描述符就绪触发整体描述符集合的遍历的低效问题。(2)poll 随着监控的 socket 集合的增加性能线性下降,使得 poll 也并不适合用于大并发场景。(3)若报告了 fd 后未被处理,下次 poll 时会再次报告该 fd

epoll(基于Linux2.4.5)

epoll 模型将主动轮询改为被动通知,当有事件发生时,被动接收通知。所以 epoll 模型注册套接字后,主程序可做其它事情,当事件发生时,接收到通知后再去处理。可理解为event pollepoll 会把哪个流发生哪种 I/O 事件通知我们。所以 epoll 是事件驱动(每个事件关联 fd),此时我们对这些流的操作都是有意义的,复杂度也降到 O ( 1 ) O(1) O(1)

创建一个 epoll 的句柄,size 表明要监听的 fd 数目。这个参数不同于 select() 中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/进程id/fd/,是能够看到这个fd的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。

epoll 的接口非常简单,一共就三个函数:

  • epoll_create:创建一个 epoll 句柄
  • epoll_ctl:向 epoll 对象中添加/修改/删除要管理的连接
  • epoll_wait:等待其管理的连接时的 I/O 事件

epoll_create 函数

int epoll_create(int size);
  • 功能:生成一个 epoll 专用的文件描述符。
  • 参数 size:表明内核监听的文件描述符数目。并不是限制 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 后,size 参数可以填大于0的任意值。
  • 返回值:成功则返回 epoll 专用的文件描述符,失败则返回 -1。

epoll_create 的源码实现:

asmlinkage int sys_epoll_create(int maxfds){int error = -EINVAL, fd;unsigned long addr;struct inode *inode;struct file *file;struct eventpoll *ep;//eventpoll接口中不可能存储超过MAX_FDS_IN_EVENTPOLL的fdif (maxfds > MAX_FDS_IN_EVENTPOLL)goto eexit_1;/** Creates all the items needed to setup an eventpoll file. That is,* a file structure, and inode and a free file descriptor.*/error = ep_getfd(&fd, &inode, &file);if (error)goto eexit_1;/** 调用去初始化eventpoll file. 这和"open" file operation callback一样,因为 inside* ep_getfd() we did what the kernel usually does before invoking* corresponding file "open" callback.*/error = open_eventpoll(inode, file);if (error)goto eexit_2;/* "private_data" 由open_eventpoll()设置 */ep = file->private_data;/* 分配页给event double buffer */error = ep_do_alloc_pages(ep, EP_FDS_PAGES(maxfds + 1));if (error)goto eexit_2;//创建event double buffer的一个用户空间的映射,以避免当返回events给调用者时,内核到用户空间的内存复制down_write(&current->mm->mmap_sem);addr = do_mmap_pgoff(file, 0, EP_MAP_SIZE(maxfds + 1), PROT_READ,MAP_PRIVATE, 0);up_write(&current->mm->mmap_sem);error = PTR_ERR((void *) addr);if (IS_ERR((void *) addr))goto eexit_2;return fd;
​
eexit_2:sys_close(fd);
eexit_1:return error;
}

epoll_ctl

每次注册新事件到 epoll 句柄中时(在 epoll_ctl 中指定 EPOLL_CTL_ADD),会把所有 fd 拷贝进内核,而非在 epoll_wait 时重复拷贝。epoll 保证每个 fd 在整个过程中只会拷贝一次。

//成功则返回0,失败则返回-1
int epoll_ctl(int epfd,					//epoll专用的文件描述符,epoll_create的返回值int op,					//表示动作,用三个宏来表示:1.EPOLL_CTL_ADD:注册新的fd到epfd中//					    2.EPOLL_CTL_MOD:修改已注册的fd的监听事件//                      3.EPOLL_CTL_DEL:从epfd中删除一个fdint fd,					//需要监听的文件描述符struct epoll_event *event	//内核要监听的事件类型);

epoll_wait

//成功则返回要处理的事件数目,超时返回0,失败返回-1
int epoll_wait(int epfd, 					//epoll专用的文件描述符,epoll_create的返回值struct epoll_event * events, //内核要监听的事件类型int maxevents, 				//事件个数int timeout); 				//超时时间,为-1时,函数为阻塞

epoll 不像 select/poll 每次都把当前文件流加入 fd 对应的设备等待队列,而只在 epoll_ctl 时把当前文件挂一遍(这一遍必不可少),并为每个 fd 指定一个回调函数。

当设备就绪,唤醒等待队列上的等待者时,就会调用该回调函数,而回调函数会把就绪 fd 加入一个就绪链表。epoll_wait 实际上就是在该就绪链表中查看有无就绪 fd

函数实现伪代码如下:

const int MAX_EVENT_NUMBER = 10000; //最大事件数
// 设置句柄非阻塞
int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}int main(){// 创建套接字int nRet=0;int m_listenfd = socket(PF_INET, SOCK_STREAM, 0);if(m_listenfd<0){printf("fail to socket!");return -1;}// struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = htonl(INADDR_ANY);address.sin_port = htons(6666);int flag = 1;// 设置ip可重用setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));// 绑定端口号int ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));if(ret<0){printf("fail to bind!,errno :%d",errno);return ret;}// 监听连接fdret = listen(m_listenfd, 200);if(ret<0){printf("fail to listen!,errno :%d",errno);return ret;}// 初始化红黑树和事件链表结构rdlist结构epoll_event events[MAX_EVENT_NUMBER];// 创建epoll实例int m_epollfd = epoll_create(5);if(m_epollfd==-1){printf("fail to epoll create!");return m_epollfd;}// 创建节点结构体将监听连接句柄epoll_event event;event.data.fd = m_listenfd;//设置该句柄为边缘触发(数据没处理完后续不会再触发事件,水平触发是不管数据有没有触发都返回事件),event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;// 添加监听连接句柄作为初始节点进入红黑树结构中,该节点后续处理连接的句柄epoll_ctl(m_epollfd, EPOLL_CTL_ADD, m_listenfd, &event);//进入服务器循环while(1){int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);if (number < 0 && errno != EINTR){printf( "epoll failure");break;}for (int i = 0; i < number; i++){int sockfd = events[i].data.fd;// 属于处理新到的客户连接if (sockfd == m_listenfd){struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){printf("errno is:%d accept error", errno);return false;}epoll_event event;event.data.fd = connfd;//设置该句柄为边缘触发(数据没处理完后续不会再触发事件,水平触发是不管数据有没有触发都返回事件),event.events = EPOLLIN | EPOLLRDHUP;// 添加监听连接句柄作为初始节点进入红黑树结构中,该节点后续处理连接的句柄epoll_ctl(m_epollfd, EPOLL_CTL_ADD, connfd, &event);setnonblocking(connfd);}else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){//服务器端关闭连接,epoll_ctl(m_epollfd, EPOLL_CTL_DEL, sockfd, 0);close(sockfd);}//处理客户连接上接收到的数据else if (events[i].events & EPOLLIN){char buf[1024]={0};read(sockfd,buf,1024);printf("from client :%s");// 将事件设置为写事件返回数据给客户端events[i].data.fd = sockfd;events[i].events = EPOLLOUT | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);}else if (events[i].events & EPOLLOUT){std::string response = "server response \n";write(sockfd,response.c_str(),response.length());// 将事件设置为读事件,继续监听客户端events[i].data.fd = sockfd;events[i].events = EPOLLIN | EPOLLRDHUP;epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);}//else if 可以加管道,unix套接字等等数据}}}

触发模式

EPOLL LTEPOLL EF 两种:

  • LT,水平触发(默认),只要该 fd 还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去处理。
  • ET,边缘触发(高速),无论 fd 中是否还有数据都只提示一次,直到下次有数据流入前都不会提示。所以 ET 模式下,read 一个 fd 时,一定要把它的 buffer 读完,即读到 read 返回值小于请求值或遇到 EAGAIN 错误(稍后重试)。

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

ET 的意义
若用 LT,系统中一旦有大量无需读写的就绪文件描述符,它们每次调用 epoll_wait 都会返回,这大大降低处理程序检索自己关心的就绪文件描述符的效率。
而采用 ET,当被监控的文件描述符上有可读写事件发生时,epoll_wait 会通知处理程序去读写。若这次没有把数据全部读写完(如读写缓冲区太小),则下次调用 epoll_wait 时,它不会通知你,即只会通知你一次,直到该文件描述符上出现第二次可读写事件才通知你。这比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

优点

  • 无最大并发连接的限制,能打开的 fd 上限远大于1024(1G内存能监听约10万个端口)。
  • 效率提升,不是轮询,不会随 fd 数目增加而效率下降。只有活跃可用的 fd 才会调用 callback 函数,即 epoll 最大优点在于它只关心“活跃”连接,而跟连接总数无关。
  • 内存拷贝,利用 mmap() 文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
  • epoll 通过内核和用户空间共享一块内存而实现。

缺点

  • 在连接数少且都十分活跃情况下,selectpoll 性能都可能比 epoll 好,因为 epoll 通知机制需要很多函数回调。
  • epollLinux 所特有的,而 selectPOSIX 所规定,一般 os 均有实现。

四、总结

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

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

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

参考:

  • 一文搞懂select、poll和epoll区别
  • IO多路复用——深入浅出理解select、poll、epoll的实现

相关文章:

02-Linux-IO多路复用之select、poll和epoll详解

前言&#xff1a; 在linux系统中&#xff0c;实际上所有的 I/O 设备都被抽象为了文件这个概念&#xff0c;一切皆文件&#xff0c;磁盘、网络数据、终端&#xff0c;甚至进程间通信工具管道 pipe 等都被当做文件对待。 在了解多路复用 select、poll、epoll 实现之前&#xff…...

echo、print_r、print、var_dump 、die

die()和exit()函数都有终止线程的作用 是php断点调试需要使用的最主要的函数 die()函数一般与“or”一并使用&#xff0c;写作“or die()” var_dump()和print_r() var_dump() 显示关于一个或多个表达式的结构信息&#xff0c;包括表达式的类型与值。数组将递归展开值&#…...

【LeetCode75】第四十四题 省份数量

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 给我们一个二维数组&#xff0c;表示城市之间的连通情况&#xff0c;连在一起的城市为一个省份&#xff0c;问我们一共有多少个省份。 这…...

把DTC从Excel导入cdd的方法

本文是基于CANdelaStudio12.0讲解 问题一&#xff1a;当导入DTC的xxx.cdi文件报如下红色错误 可能原因&#xff1a;在设置具有下拉框的属性的内容时&#xff0c;输入的内容不在下拉框列表中 解决办法:在.cddt文件中更新“”Error Code Table“”内容&#xff0c;把新的选项更新…...

养猪废水处理设备的处理方法

诸城市鑫淼环保小编带大家了解一下养猪废水处理设备的处理方法 1.高有机负荷&#xff1a;猪粪尿含有大量有机物质&#xff0c;比如蛋白质、脂肪和淀粉等&#xff0c;这些有机物在水体中分解会消耗氧气&#xff0c;导致水体缺氧。 2.高氨氮含量&#xff1a;猪粪尿中的蛋白质分解…...

【React】React学习:从初级到高级(三)

3 状态管理 随着应用不断变大&#xff0c;应该更有意识的去关注应用状态如何组织&#xff0c;以及数据如何在组件之间流动。冗余或重复的状态往往是缺陷的根源。 3.1 用State响应输入 3.1.1 声明式地考虑UI 总体步骤如下&#xff1a; 定位组件中不同的视图状态 确定是什么…...

Rest和Http什么关系?

分析&回答 REST 定义了一组体系架构原则&#xff0c;您可以根据这些&#xff0c;包括使用不同语言编写的客户端如何通过 HTTP 处理和传输资源状态。 REST只是一种风格&#xff0c;不是一种标准REST是以资源为中心的 用不同的 HTTP 请求方法来处理对资源的 CRUD&#xff0…...

leetcode原题: 生存人数

题目&#xff1a; 给定 N 个人的出生年份和死亡年份&#xff0c;第 i 个人的出生年份为 birth[i]&#xff0c;死亡年份为 death[i]&#xff0c;实现一个方法以计算生存人数最多的年份。 你可以假设所有人都出生于 1900 年至 2000 年&#xff08;含 1900 和 2000 &#xff09;…...

K8S的介绍和架构

仅供入门 K8S的介绍和架构 一. 什么是kubernetes二、Kubernetes架构和组件 2.1 核心组件 2.1.1 Kubernetes Master控制组件&#xff0c;调度管理整个系统&#xff08;集群&#xff09;&#xff0c;包含如下组件: a、Kubernetes API Serverb、Kubernetes Schedulerc、Kubernet…...

linux信号量

通过学习linux的信号量&#xff0c;对linux的信号量进行了编程。...

Jupyter Notebook 好用在哪?

Jupyter Notebook 是一个 Web 应用程序&#xff0c;便于创建和共享文学化程序文档&#xff0c;支持实时代码、数学方程、可视化和 Markdown&#xff0c;其用途包括数据清理和转换、数值模拟、统计建模、机器学习等等。目前&#xff0c;数据挖掘领域中最热门的比赛 Kaggle 里的资…...

华为云云服务器评测|基于云服务器的minio部署手册

华为云云服务器评测|基于云服务器的minio部署手册 【软件安装版本】【集群安装&#xff08;是&#xff09;&#xff08;否&#xff09;】 版本 创建人 修改人 创建时间 备注 1.0 jz jz 2023.9.2 minio华为云耀服务器 一. 部署规划与架构 1. 规…...

【网络安全带你练爬虫-100练】第22练:数据包中参数提取与处理

目录 一、目标1&#xff1a;GET数据包的处理 1、GET数据包中参数的提取 2、GET请求中 统计参数个数 二、目标2&#xff1a;POST数据包的处理 1、post中参数个数的提取 2、POST请求中 统计参数个数 一、目标1&#xff1a;GET数据包的处理 1、GET数据包中参数的提取 impo…...

第64步 深度学习图像识别:多分类建模误判病例分析(Pytorch)

基于WIN10的64位系统演示 一、写在前面 上期我们基于TensorFlow环境介绍了多分类建模的误判病例分析。 本期以健康组、肺结核组、COVID-19组、细菌性&#xff08;病毒性&#xff09;肺炎组为数据集&#xff0c;基于Pytorch环境&#xff0c;构建SqueezeNet多分类模型&#xf…...

ES查询报错内容长度超过104857600

项目场景&#xff1a; 使用 ElasticsearchRestTemplate 或者使用 RestHighLevelClient 查询 ES 报错 内容长度超过 104857600 问题描述 ES 查询报错 entiity content is too long xxx for the configured buffer limit 104857600 Overridepublic void esQuery() {restHighL…...

2023欧亚合作发展大会暨国际公共采购大会在京举行

2023年9月2日至6日&#xff0c;以“合作、协同、共赢、共享”为主题的“2023欧亚合作发展大会暨国际公共采购大会等系列会议”在北京炎黄书院隆重举行&#xff0c;共有500多位中外贵宾参加了本次盛会。 本次大会指导单位是中国联合国采购促进会、北京市中医药局&#xff0c;由中…...

宝塔面板linux在终端使用命令开启服务保持服务不关闭

我们经常在宝塔面板终端开启服务&#xff08;比如socket等服务时&#xff09;&#xff0c;如果关闭面板标签页或者关闭终端&#xff0c;服务也随之关闭了&#xff0c;要保持服务一直运行&#xff0c;就需要把终端进程放在linux后台执行&#xff0c;方法如下&#xff1a; 1、先…...

面试题--从键盘输入网站到网页显示,之间发生了什么

文章目录 首先进入HTTP阶段协议栈阶段TCP阶段IP阶段MAC网卡交换机路由器抵达 首先进入HTTP阶段 1.解析对应的URL&#xff0c;访问一个对应的服务器xxx.com的一个文件index.html; 2 使用DNS查询对应的ip地址&#xff0c;通过DNS服务器进行查找 3 组装http报文&#xff0c;生成h…...

字节9.3秋招研发笔试 【后端方向】第三题

题目 小红拿到了一个无向图&#xff0c;初始每人节点是白色&#xff0c;其中有若干个节点被染成了红色。小红想知道&#xff0c;若将 i 号节点染成红色&#xff0c;当前的红色连块的数量是多少? 你需要回答i∈[1,n] 的答案。 定义&#xff0c;若干节点组成一个红色连通块&am…...

Solidity 小白教程:8. 变量初始值

Solidity 小白教程&#xff1a;8. 变量初始值 变量初始值 在solidity中&#xff0c;声明但没赋值的变量都有它的初始值或默认值。这一讲&#xff0c;我们将介绍常用变量的初始值。 值类型初始值 boolean: falsestring: “”int: 0uint: 0enum: 枚举中的第一个元素address: …...

时序预测 | MATLAB实现EEMD-SSA-LSTM、EEMD-LSTM、SSA-LSTM、LSTM时间序列预测对比

时序预测 | MATLAB实现EEMD-SSA-LSTM、EEMD-LSTM、SSA-LSTM、LSTM时间序列预测对比 目录 时序预测 | MATLAB实现EEMD-SSA-LSTM、EEMD-LSTM、SSA-LSTM、LSTM时间序列预测对比预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 时序预测 | MATLAB实现EEMD-SSA-LSTM、E…...

京东搜索EE链路演进 | 京东云技术团队

导读 搜索系统中容易存在头部效应&#xff0c;中长尾的优质商品较难获得充分的展示机会&#xff0c;如何破除系统的马太效应&#xff0c;提升展示结果的丰富性与多样性&#xff0c;助力中长尾商品成长是电商平台搜索系统的一个重要课题。其中&#xff0c;搜索EE系统在保持排序…...

【C++】反向迭代器精讲(以lIst为例)

目录 二&#xff0c;全部代码 三&#xff0c;设计思路 1. 讨论 2. 关于迭代器文档一个小细节 结语 一&#xff0c;前言 如果有小伙伴还未学习普通迭代器&#xff0c;请参考这篇文章中的普通迭代器实现。 【STL】list用法&试做_底层实现_花果山~~程序猿的博客-CSDN…...

时序预测 | MATLAB实现基于PSO-GRU、GRU时间序列预测对比

时序预测 | MATLAB实现基于PSO-GRU、GRU时间序列预测对比 目录 时序预测 | MATLAB实现基于PSO-GRU、GRU时间序列预测对比效果一览基本描述程序设计参考资料 效果一览 基本描述 MATLAB实现基于PSO-GRU、GRU时间序列预测对比。 1.MATLAB实现基于PSO-GRU、GRU时间序列预测对比&…...

2023年高教社杯 国赛数学建模思路 - 案例:感知机原理剖析及实现

文章目录 1 感知机的直观理解2 感知机的数学角度3 代码实现 4 建模资料 # 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 感知机的直观理解 感知机应该属于机器学习算法中最简单的一种算法&#xff0c;其…...

Java 利用pdfbox将图片和成到pdf指定位置

业务背景&#xff1a;用户在手机APP上进行签名&#xff0c;前端将签完名字的图片传入后端&#xff0c;后端合成新的pdf. 废话不多说&#xff0c;上代码&#xff1a; //控制层代码PostMapping("/imageToPdf")public Result imageToPdf(RequestParam("linkName&…...

大数据课程K19——Spark的电影推荐案例推荐系统的冷启动问题

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Spark的案例——电影推荐; ⚪ 掌握Spark的模型存储; ⚪ 掌握Spark的模型加载; ⚪ 掌握Spark的推荐系统的冷启动问题; 一、案例——电影推荐 1. 基于用户的推荐 1. 说明 我们现…...

Docker-安装(Linux,Windows)

目录 前言安装版本Docker版本说明前提条件Linux安装使用YUM源部署获取阿里云开源镜像站YUM源文件安装Docker-ce配置Docker Daemon启动文件启动Docker服务并查看已安装版本 使用二进制文件部署 Windows安装实现原理安装步骤基本使用 参考说明 前言 本文主要说明Docker及其相关组…...

若依富文本 html样式 被过滤问题

一.场景 进入页面&#xff0c;富文本编辑框里回显这条新闻内容&#xff0c;如下图&#xff0c; 然后可以在富文本编辑框里对它实现再编辑&#xff0c;编辑之后将html代码提交保存到后台数据库。可以点击详情页进行查看。 出现问题&#xff1a;在提交到后台controller时&#x…...

VS Code 快速消除前置空格和常用快捷键

目录 介绍&#xff1a; 消除前置空格&#xff1a;SHIFTTAB 常用的 VS Code 快捷键 介绍&#xff1a; 在使用 Visual Studio Code (VS Code) 进行代码编辑时&#xff0c;熟练掌握一些快捷键和编辑技巧可以大幅提高开发效率。本文将重点介绍如何使用快捷键 SHIFTTAB 快速消除代…...