目录

一. select/poll/epoll函数详解(函数说明以及代码实现)

        1.1 select 函数

        1.1.1 fd_set文件描述符集合

        1.1.2 宏操作

        1.1.3 select核心函数

        1.1.4 TCP+select代码实现

        1.1.5 select函数总结

        1.2 poll 函数

        1.2.1 poll核心函数

          1.2.2 struct pollfd结构体

        1.2.3 核心事件宏

        1.2.4 TCP+poll代码实现

        1.2.5 poll函数总结

        1.3 epoll函数

        1.3.1 epoll函数族

        1.3.2 核心数据结构(struct epoll_event + epoll_data_t 联合体)        

        1.3.3 核心事件宏

        1.3.4 TCP+epoll代码实现

        1.3.5 epoll核心总结

二. select/poll/epoll函数的优劣对比

        2.1 epoll函数的核心优势


一. select/poll/epoll函数详解(函数说明以及代码实现)

        1.1 select 函数

        select函数它作为最早的IO多路复用接口,我们主要从【fd_set 文件描述符集合】+【四个宏操作】+【select核心函数】三个方面进行介绍;

        1.1.1 fd_set文件描述符集合

        fd_set 是select 函数用于存储事件的文件描述符集合,与RTOS中的事件标志组有些相似。与后买你epoll函数的事件表啥的都比较相似;

        1.1.2 宏操作
宏定义 函数原型(简化) 核心作用 适用场景
FD_ZERO void FD_ZERO(fd_set *set); 清空 fd_set 集合中的所有比特位,将所有位设为 0,初始化空集合。 每次调用 select 前,必须先调用该宏初始化要使用的 fd_set(避免残留旧数据)。
FD_SET void FD_SET(int fd, fd_set *set); 将文件描述符 fd 对应的比特位设为 1,把 fd 添加到 fd_set 集合中(监控该 fd)。 初始化 fd_set 后,将监听 socket、客户端 socket 加入待监控集合(如读集合 readfds)。
FD_CLR void FD_CLR(int fd, fd_set *set); 将文件描述符 fd 对应的比特位设为 0,把 fdfd_set 集合中删除(取消监控)。 客户端断开连接后,将对应的 fd 从监控集合中移除,避免后续无效监控。
FD_ISSET int FD_ISSET(int fd, fd_set *set); 判断文件描述符 fd 对应的比特位是否为 1(即该 fd 是否触发了事件)。 select 函数返回后,遍历所有监控的 fd,判断哪个 fd 触发了事件(如可读、可写)。
        1.1.3 select核心函数

        select函数是单线程多路IO服用的基础函数,可以实现【一对多】的fd监控,无需为每个FD创建独立的线程,减少了线程资源的开销;同时要注意传入进入的是整个位图,而返回的时候仅保留触发事件的位图为1,所以要设置临时的位图来在select中进行使用;

        1. 函数原型:

#include <sys/select.h>  

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

        2. 函数参数说明:

参数名 数据类型 核心含义与说明
nfds int 「最大文件描述符 + 1」,告诉内核需要监控的 fd 范围(内核只需遍历到该值即可,无需遍历全部 FD_SETSIZE)。示例:若监控的 fd 有 357,则 nfds = 7 + 1 = 8
readfds fd_set * 「读事件监控集合」(输入 / 输出参数):- 输入:用户想要监控的、等待可读事件的 fd 集合;- 输出:select 返回后,仅保留触发了可读事件的 fd 集合。无需监控读事件可传入 NULL
writefds fd_set * 「写事件监控集合」(输入 / 输出参数):- 输入:用户想要监控的、等待可写事件的 fd 集合;- 输出:select 返回后,仅保留触发了可写事件的 fd 集合。无需监控写事件可传入 NULL
exceptfds fd_set * 「异常事件监控集合」(输入 / 输出参数):监控 fd 的异常事件(如带外数据到达),使用逻辑同读 / 写集合。无需监控异常事件可传入 NULL
timeout struct timeval * 超时时间设置(输入参数),控制 select 的阻塞行为,结构体定义如下:c<br>struct timeval {<br> long tv_sec; // 秒数<br> long tv_usec; // 微秒数(1秒=10^6微秒)<br>};<br>三种取值:1. NULL:永久阻塞,直到有 fd 触发事件;2. tv_sec=0 && tv_usec=0:非阻塞,立即返回,无论是否有事件;3. 大于 0:阻塞指定时间,超时后若无事件则返回 0。

        3. 函数返回值说明

返回值 含义
大于 0 成功,返回触发了事件的 fd 总数量(读、写、异常事件的 fd 总数)。
0 超时,没有任何 fd 触发事件,fd_set 集合被清空。
-1

失败,设置 errno 标识错误原因(如被信号中断、参数错误等),可通过 perror() 打印错误信息。

        1.1.4 TCP+select代码实现

        首先简述一下代码实现的流程:

        1.初始化需要使用到的位图(FD_ZERO);

        2.将服务器的监听套接字设置进入位图(FD_SET);

        3.进入while循环,使用select函数进行阻塞,判断是监听套接字还是客户端套接字(FD_ISSET);

        4.当为监听套接字后使用accept函数进行接收,并使用FD_SET宏将accept函数的返回值设置进位图;

        5.当为客户端套接字后开始遍历整个位图范围,使用FD_ISSET宏判断是否被触发,被触发则处理客户端数据;

#include <stdio.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define PROCESS_PORT 5001   //进程端口号
#define SERVER_ADDR 0       //服务端接收地址
#define LISTEN_LEN 50       //监听队列长度
#define BUF_MAXLEN 50       //接收字符最大长度
#define handle_error(msg)   do{ perror(msg);exit(EXIT_FAILURE);} while(0)
                            //处理错误
#define MAX_SOCK_FD 1024    //文件描述符容量


int client_pro(int fd);

int main(){
//遍历操作变量定义
 	int i=0;
//定义函数返回值
	int ret=0;
//定义socket描述符
	int server_fd = 0;
	int client_fd = 0;
	int flag = 1;            //套接字属性:
//定义套接字信息结构体
	struct sockaddr_in addr;
	struct sockaddr_in client_addr;
	socklen_t addr_len=sizeof(client_addr); 
//定义文件描述符集合
	fd_set cfd,tmp_cfd;
//初始化文件描述符集合
	FD_ZERO(&cfd);
	FD_ZERO(&tmp_cfd);

/*1.创建socket描述符*/
	server_fd = socket(AF_INET, SOCK_STREAM, 0);
	if( server_fd == -1)  handle_error("socket"); 
	
	if( setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )
	perror("setsockopt");   //设置套接字属性

	FD_SET(server_fd,&cfd); //将服务端的文件描述符设置进位图
/*2.设置套接字信息结构体*/
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5001);
	addr.sin_addr.s_addr = SERVER_ADDR;
/*3.绑定地址信息*/
	ret = bind(server_fd,(struct sockaddr*)&addr,sizeof(addr));
	if(ret == -1) handle_error("bind");
/*4.监听客户端*/
	ret = listen(server_fd,LISTEN_LEN); 
	if(ret == -1) handle_error("listen");

	while(1){
		tmp_cfd =cfd;
		if((ret = select(MAX_SOCK_FD,&tmp_cfd,NULL,NULL,NULL)) <0 )
			handle_error("select");   //核心函数select阻塞
		if(FD_ISSET(server_fd,&tmp_cfd)){
			if((client_fd = accept(server_fd,(struct sockaddr*)&client_addr,&addr_len) )< 0){
				handle_error("accept");
				 }
			printf("[%s][%d]连接成功!\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
			FD_SET(client_fd,&cfd);

		}else{
			for(i=server_fd+1;i<MAX_SOCK_FD;i++){
				if(FD_ISSET(i,&tmp_cfd)){
                    if(client_pro(i) <= 0){
						getpeername(i,(struct sockaddr*)&client_addr,&addr_len);
						printf("[%s][%d]断开连接\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
						FD_CLR(i,&cfd);
					}
				}
			}
		}
	}
/*8.关闭文件描述符*/
	close(server_fd);
	close(client_fd);
}


int client_pro(int fd){
	//定义接收缓冲区
	char buf[BUF_MAXLEN]={0};
	struct sockaddr_in peeraddr;
    socklen_t addrlen=sizeof(peeraddr);

	int rev = read(fd,buf,BUF_MAXLEN);
	if(rev > 0){
		getpeername(fd,(struct sockaddr*)&peeraddr,&addrlen);
		printf("[%s][%d]buf:%s\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port),buf);
	}else if(rev < 0){
		perror("read");
	}
	return rev;
}
        1.1.5 select函数总结

        (1)核心组成:fd_set 集合 + 4 个操作宏 + select 核心函数,跨平台兼容性好(支持 Linux、Windows、Unix);
        (2)核心劣势:fd 数量限制(默认 1024)、重复拷贝(每次调用 select 都要拷贝 fd_set 到内核态)、轮询效率低(返回后需遍历所有 fd 判断事件);
        (3)适用场景:低并发(fd < 1024)、需要跨平台运行的简单多路复用场景。

        1.2 poll 函数

        poll函数是select函数的优化版本,这次围绕【poll函数】+【struct pollfd结构体】+【IO事件宏】来实现;

        1.2.1 poll核心函数

        1. 函数原型:

#include <poll.h>  

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

        2. 函数参数说明:

参数名 数据类型 核心含义 补充说明
fds struct pollfd * 指向 struct pollfd 结构体数组的指针(输入 / 输出参数) 1. 输入:用户初始化好的,包含所有待监控 fd 及对应监听事件的结构体数组;2. 输出:内核仅修改数组中每个元素的 revents 字段(填充实际发生的事件),fdevents 字段保持不变;3. 可预先分配固定大小数组(如 1024),无需动态扩容,简化代码。
nfds nfds_t 待监控的 struct pollfd 结构体数组的长度(即要监控的 fd 总数) 1. nfds_t 是无符号整数类型(通常为 unsigned int);2. 该参数告知内核要遍历的数组元素个数,无「最大 fd+1」的要求(对比 select 的 nfds 参数,更简洁);3. 无硬限制,仅受系统内存和文件描述符上限约束。
timeout int 超时时间(单位:毫秒) 三种取值(与 select 类似,方便联想记忆):1. -1:永久阻塞,直到有 fd 触发 IO 事件;2. 0:非阻塞,立即返回,无论是否有事件发生;3. 大于 0:阻塞指定毫秒数,超时后若无事件则返回 0。

             3.函数返回值说明:

返回值 核心含义
大于 0 成功,返回触发了 IO 事件的 fd 总数(即数组中 revents 字段非 0 的元素个数)。
0 超时,没有任何 fd 触发事件,数组中所有元素的 revents 字段均为 0。
-1 失败,设置 errno 标识错误原因(如被信号中断、参数错误、数组为空等),可通过 perror() 打印错误信息。
          1.2.2 struct pollfd结构体

        poll没有像select那样的位图,使用的是结构体数组来管理监控的每一个fd及其事件,这是poll函数与select函数的区别:

struct pollfd {
    int   fd;         // 待监控的文件描述符(socket fd、文件 fd 等)
    short events;     // 输入参数:用户想要监听的 IO 事件(通过事件宏组合)
    short revents;    // 输出参数:内核填充的、该 fd 实际发生的 IO 事件(无需用户初始化)
};
成员名 数据类型 核心作用 关键使用要点
fd int 指定要监控的文件描述符

1. 填写有效 fd(如监听 socket、客户端 socket),内核会监控该 fd 的事件;

2. 当某个 fd 无需再监控时(如客户端断开),无需从数组中删除该元素,只需将 fd 设为 -1,内核会自动忽略该结构体元素;

3. 后续可复用 fd=-1 的数组位置,存放新的待监控 fd,简化代码逻辑。

events short 设定要监听的事件(输入参数) 1. 通过「事件宏」赋值(如 POLLIN 表示监听可读事件);2. 可通过「位或 `
revents short 存放实际发生的事件(输出参数) 1. 由内核填充,用户无需初始化,也不要手动修改;2. 内核会根据 fd 的实际状态,填充对应的事件宏(可能包含用户未监听的异常事件,如 POLLERR);3. 需通过「位与 &」判断具体发生了什么事件(如 revents & POLLIN,判断是否可读);4. 事件处理完成后,该字段会保留上一次的结果,下次 poll() 调用时内核会覆盖更新。
        1.2.3 核心事件宏

1. 常用可读事件

事件宏 核心含义 是否需要主动设置(events 中)
POLLIN 对应的 fd 可读:1. 套接字(TCP/UDP)接收缓冲区有数据可读取;2. 监听套接字有新的客户端连接请求;3. 对端关闭连接(可读返回 0)。
POLLPRI 对应的 fd 有紧急数据可读(如 TCP 带外数据到达)。

2. 常用可写事件

事件宏 核心含义 是否需要主动设置(events 中)
POLLOUT 对应的 fd 可写:1. 套接字发送缓冲区有空闲空间,可发送数据;2. 套接字连接成功(客户端 connect 成功后)。

3. 常用异常 / 被动事件(无需主动监听,内核自动返回)

事件宏 核心含义 是否需要主动设置(events 中)
POLLERR 对应的 fd 发生错误(如套接字异常关闭、读写失败)。 否(内核自动填充到 revents
POLLHUP 对应的 fd 被挂断(如 TCP 客户端主动断开连接、对端进程退出)。 否(内核自动填充到 revents
POLLNVAL 对应的 fd 无效(如未打开的 fd、已关闭的 fd 被传入监控数组)。 否(内核自动填充到 revents
        1.2.4 TCP+poll代码实现
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <sys/socket.h>

#define PROCESS_PORT 5001   //进程端口号
#define SERVER_ADDR 0
#define LISTEN_LEN 50
#define BUF_MAXLEN 50
#define handle_error(msg)   do{ perror(msg);exit(EXIT_FAILURE);} while(0)

#define MAX_SOCK_FD 1024
//客户端处理函数声明
int client_pro(int fd);

int main(){
//遍历使用变量
	int i=0,j=0;;
//定义文件描述符计数变量
	nfds_t nfds=0;
//定义函数返回值
	int ret=0;
//定义socket描述符
	int server_fd = 0;
	int client_fd = 0;
	int flag = 1;
//定义套接字信息结构体
	struct sockaddr_in addr;
	struct sockaddr_in client_addr;
	socklen_t addr_len=sizeof(client_addr);
//创建结构体数组
	struct pollfd fds[MAX_SOCK_FD]={};
/*1.创建socket描述符*/
	server_fd = socket(AF_INET, SOCK_STREAM, 0);
	if( server_fd == -1)  handle_error("socket"); 
	
	if( setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )
	perror("setsockopt");
	
//将监听套接字放入结构体数组
	fds[0].fd = server_fd;
	fds[0].events = POLLIN;
	nfds++;
/*2.设置套接字信息结构体*/
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5001);
	addr.sin_addr.s_addr = SERVER_ADDR;
/*3.绑定地址信息*/
	ret = bind(server_fd,(struct sockaddr*)&addr,sizeof(addr));
	if(ret == -1) handle_error("bind");
/*4.监听客户端*/
	ret = listen(server_fd,LISTEN_LEN); 
	if(ret == -1) handle_error("listen");

	while(1){
		//poll阻塞
		if(poll(fds,nfds,-1) == -1)  handle_error("poll");
		//通过阻塞,遍历判断;
		for(i = 0;i < nfds; i++){//这里是顺序表的逻辑
			if(fds[i].fd == server_fd && (fds[i].revents) & POLLIN){//判断是监听套接字
				if((client_fd = accept(server_fd,(struct sockaddr*)&client_addr,&addr_len)) == -1) 
					handle_error("accept");//使用accept函数进行接收;
				fds[nfds].fd = client_fd;
				fds[nfds].events = POLLIN;
				nfds++;
				printf("[%s][%d][%lu] connect successful\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),nfds);
			}
			if(i>0 && (fds[i].revents & POLLIN)){
				if(client_pro(fds[i].fd) <= 0){
					if(getpeername(fds[i].fd,(struct sockaddr*)&client_addr,&addr_len) == -1) handle_error("getpeername");
					printf("[%s][%d][%lu] over\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),nfds);
					close(fds[i].fd);
					for(j=i;j<nfds-1;j++) fds[j] = fds[j+1]; //顺序表的删除
					nfds--;
					i--;
				}
			}
				
		}
		}

/*8.关闭文件描述符*/
	close(server_fd);
	close(client_fd);
}

int client_pro(int fd){
	//定义接收缓冲区
	char buf[BUF_MAXLEN]={0};
	struct sockaddr_in peeraddr;
    socklen_t addrlen=sizeof(peeraddr);

	int rev = read(fd,buf,BUF_MAXLEN);
	if(rev > 0){
		getpeername(fd,(struct sockaddr*)&peeraddr,&addrlen);
		printf("[%s][%d]buf:%s\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port),buf);
	}else if(rev < 0){
		perror("read");
	}
	return rev;
}
        1.2.5 poll函数总结

        (1)核心组成:poll() 单一函数 + struct pollfd 结构体数组 + 多类事件宏,无额外集合操作宏,接口更简洁;
        (2)核心优势:无 fd 数量硬限制、events/revents 分离无需重复初始化、支持更多事件类型;
        (3)核心局限:仍采用轮询模式(返回后需遍历数组)、每次调用需拷贝整个结构体数组到内核态,高并发(fd 上万)场景效率下降;
        (4)适用场景:中低并发(fd 上千级)、需要跨平台(支持 Linux、Unix,不支持 Windows)、追求接口简洁的多路复用场景。

        1.3 epoll函数

        epoll函数作为Linux特有的高并发IO多路复用方案,核心围绕【三个核心函数】+【两个数据结构】+【IO事件模式宏】来进行介绍;

        1.3.1 epoll函数族

        (1)函数 1:epoll_create()—— 创建 epoll 句柄,初始化内核事件表;

          1. 函数原型:

#include <sys/epoll.h>

int epoll_create(int size);

           2.函数参数说明:

参数名 数据类型 核心含义 补充说明
size int 历史遗留参数,用于指定内核事件表的初始大小 1. 该参数已被弃用(Linux 2.6.8 及以上版本),仅为兼容旧代码,只需传入一个大于 0 的整数即可(如 11024);2. 内核不再受 size 限制,事件表会根据实际需要动态扩容,无需手动指定大小;3. 该参数不影响后续监控的 fd 数量,fd 上限仅受系统内存和文件描述符上限(ulimit -n)约束。

           3. 函数返回值说明:

返回值 核心含义
大于 0 成功,返回一个epoll 专用文件描述符(epfd),该 fd 是内核事件表的唯一标识,后续 epoll_ctlepoll_wait 均需依赖该句柄。
-1 失败,设置 errno 标识错误原因(如内存不足、参数无效等),可通过 perror() 打印错误信息。

            4.核心作用

            a. 创建一个 epoll 句柄(epfd),内核会为该句柄分配一块内存,用于维护「epoll 内核事件表」(存储待监控的 fd 及其对应的事件信息);

            b. 初始化内核事件表,为后续添加、修改、删除 fd 及事件做好准备;
            c. 类比:相当于 select 中初始化 fd_set、poll 中初始化 pollfd 数组,但 epoll 的事件表在内核中维护,且无需重复初始化。

             5.注意事项

             a. 使用完毕后使用close函数关闭;

          1.3.2 函数 2:epoll_ctl()—— 控制内核事件表,管理 fd 与事件(添加 / 修改 / 删除)

              1.函数原型

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

                2.函数参数说明

参数名 数据类型 核心含义 补充说明
epfd int epoll_create() 返回的 epoll 句柄 指向要操作的内核事件表,确保后续操作的是同一个事件表,不可传入无效 fd。
op int 对内核事件表的操作类型,由 3 个宏指定

核心操作(3 种,覆盖 fd 全生命周期管理):1. EPOLL_CTL_ADD:向事件表注册新的 fd及对应的监听事件(对应 select 的 FD_SET、poll 的填充 pollfd 数组);2. EPOLL_CTL_MOD修改已注册 fd 的监听事件(如从「可读」改为「可写」,select/poll 需重新初始化集合 / 数组,更繁琐);3. EPOLL_CTL_DEL:从事件表删除已注册的 fd(对应 select 的 FD_CLR、poll 的将 fd 设为 -1),删除后不再监控该 fd 的任何事件。

fd int 要管理的文件描述符(监听 socket、客户端 socket 等) 需传入有效 fd(已打开、未关闭),传入无效 fd 会返回错误(errno=EINVAL)。
event struct epoll_event * 指向 struct epoll_event 结构体的指针 1. 输入参数:告知内核要监听的事件类型,以及要关联的用户数据;2. 当 op=EPOLL_CTL_DEL 时,该参数可传入 NULL(内核无需再获取事件信息,只需删除 fd 即可);3. 该结构体是 fd 与事件的绑定载体,详细解析见下文「核心结构体」部分。

        3. 返回值说明

返回值 核心含义
0 成功,内核事件表已完成对应的添加 / 修改 / 删除操作。
-1 失败,设置 errno 标识错误原因(如 epfd 无效、fd 未注册、op 非法等)。

        4.核心作用

        作为「用户态与内核事件表的桥梁」,负责维护内核事件表中的 fd 及其事件信息,实现 fd 的注册、事件修改和注销,是 epoll 实现高效多路复用的基础(仅需一次注册,后续无需重复拷贝 fd 信息)。

        (3)函数 3:epoll_wait()—— 等待事件发生,获取触发的 IO 事件

           1.函数原型

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

            2.参数解析

参数名 数据类型 核心含义 补充说明
epfd int epoll_create() 返回的 epoll 句柄 指向要监控的内核事件表,内核会从该事件表中检测触发事件的 fd。
events struct epoll_event * 指向用户态预先分配的 struct epoll_event 结构体数组的指针(输出参数) 1. 内核不会主动分配内存,需用户提前分配固定大小的数组(如 struct epoll_event events[1024]);2. 函数返回后,内核会将「触发了 IO 事件的 fd」对应的事件信息填充到该数组中(仅填充有效事件,无需填充全部);3. 数组中的每个元素对应一个触发事件的 fd,通过 data 字段可获取该 fd,通过 events 字段可获取具体触发的事件。
maxevents int 预先分配的 events 数组的长度(元素个数) 1. 告知内核 events 数组的最大容量,防止内核越界写入;2. 该值必须大于 0,且不能超过数组的实际长度,否则会导致未定义行为;3. 与 epoll_create()size 参数无关,仅限制本次返回的最大事件数量。
timeout int 超时时间(单位:毫秒) 三种取值(与 select/poll 类似,方便联想记忆):1. -1:永久阻塞,直到有 fd 触发 IO 事件(最常用,对应高并发服务端的阻塞场景);2. 0:非阻塞,立即返回,无论是否有事件发生;3. 大于 0:阻塞指定毫秒数,

            3.返回值说明

返回值 核心含义
大于 0 成功,返回触发了 IO 事件的 fd 总数(即 events 数组中有效元素的个数,小于等于 maxevents)。
0 超时,没有任何 fd 触发事件,events 数组中的内容无有效数据。
-1 失败,设置 errno 标识错误原因(如被信号中断、epfd 无效等)。

             4.核心作用

                阻塞等待内核事件表中的 fd 触发 IO 事件,是 epoll 的「事件检测接口」,对应 select/poll 的核心功能;
             5.核心优势(对比 select/poll):
                无需轮询:仅返回触发事件的 fd 信息,用户只需遍历 events 数组前「返回值」个元素即可,效率为 高;
                减少拷贝:仅将触发事件的 fd 信息拷贝到用户态,而非全部待监控 fd 信息,大幅降低拷贝开销;
                事件驱动:内核主动将触发事件的 fd 通知给用户态,而非用户态主动轮询检测。

        1.3.2 核心数据结构(struct epoll_event + epoll_data_t 联合体)        
//联合体
typedef union epoll_data {
    void    *ptr;  // 指向自定义数据结构的指针(如封装了 fd、客户端信息的结构体)
    int      fd;   // 待监控的文件描述符(最常用,直接关联 fd,方便后续处理)
    __uint32_t u32; // 32 位无符号整数(少用)
    __uint64_t u64; // 64 位无符号整数(少用)
} epoll_data_t;

struct epoll_event {
    __uint32_t events;  // Epoll 事件类型(通过事件宏组合赋值)
    epoll_data_t data;  // 用户数据载体(联合体,关联 fd 或自定义数据)
};

        在实际过程中,经常使用到的是 events 以及 data.fd 这两个参数,在epoll_ctl 和 epoll_wait两个函数中使用,一个是填写进去,一个输出出来;

        1.3.3 核心事件宏

        1. 常用可读事件(对应 TCP 实践核心场景,需主动设置)

事件宏 核心含义 是否需要主动设置(events 中)
EPOLLIN 对应的 fd 可读:1. 套接字接收缓冲区有数据可读取;2. 监听套接字有新的客户端连接请求;3. 对端关闭连接(read 返回 0)。
EPOLLPRI 对应的 fd 有紧急数据可读(如 TCP 带外数据到达)。

        2. 常用可写事件(需主动设置)

事件宏 核心含义 是否需要主动设置(events 中)
EPOLLOUT 对应的 fd 可写:1. 套接字发送缓冲区有空闲空间,可发送数据;2. 客户端 connect 连接成功后。

        3. 常用异常事件(无需主动设置,内核自动填充到 revents

事件宏 核心含义 是否需要主动设置(events 中)
EPOLLERR 对应的 fd 发生错误(如套接字异常关闭、读写失败)。
EPOLLHUP 对应的 fd 被挂断(如 TCP 客户端主动断开连接、对端进程退出)。

        4. 特有触发模式宏(epoll 专属,需主动设置,与事件宏组合使用)

事件宏 核心含义 使用要点
EPOLLET 将 epoll 设为「边缘触发(Edge Trigger,ET)」模式(默认是水平触发)。 1. 仅在 fd 状态「变化瞬间」触发一次事件,后续无状态变化则不再触发;2. 需配合非阻塞 fd 使用,且要一次性处理完缓冲区所有数据;3. 效率更高,是高并发场景首选;4. 组合使用:`EPOLLIN EPOLLET`。
EPOLLLT 将 epoll 设为「水平触发(Level Trigger,LT)」模式(默认模式,可省略不写)。 1. 只要 fd 缓冲区有未处理数据 / 空闲空间,就会持续触发事件;2. 编程简单,容错率高,无需一次性处理完所有数据;3. 兼容 select/poll 的触发逻辑,易上手;4. 组合使用:EPOLLIN(等价于 `EPOLLIN EPOLLLT`)。
EPOLLONESHOT 仅监听一次事件,事件处理完成后,若需继续监控该 fd,需重新调用 epoll_ctl 注册。 1. 避免同一个 fd 的事件被多次触发(如多线程处理场景);2. 适合一次性 IO 场景,减少无效事件通知。

        5. 事件宏使用要点

        (1)多事件组合:ev.events = EPOLLIN | EPOLLET;(同时监听可读事件和边缘触发模式);
        (2)多事件判断:if (events[i].events & EPOLLIN) { ... }(通过位与 & 判断具体事件);
        (3)异常事件优先处理:if (events[i].events & (EPOLLERR | EPOLLHUP)) { ... }(避免资源泄露)。

        1.3.4 TCP+epoll代码实现

        实现过程:

        1.创建事件表(epoll_create),获取epoll专用文件描述符

        2.将监听套接字添加进事件表(epoll_ctl);

        3.进入while循环,进行阻塞等待,等待函数返回被触发的IO个数以及结构体数组(epoll_wait);

        4.进入for循环,循环次数为epoll_wait的返回值,被触发事件个数,被触发事件的文件描述符以及宏等信息都存储在结构体数组中;

#include <stdio.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define PROCESS_PORT 5001   //进程端口号
#define SERVER_ADDR 0
#define LISTEN_LEN 50
#define BUF_MAXLEN 50
#define handle_error(msg)   do{ perror(msg);exit(EXIT_FAILURE);} while(0)

#define MAX_SOCK_FD 1024


int client_pro(int fd);

int main(){
//定义遍历返回值变量
	int i=0;
//定义epoll_wait返回值
	int nfds=0;
//定义事件表
	int epfd=0;
//定义事件表结构体
	struct epoll_event tmp,events[MAX_SOCK_FD];
//定义函数返回值
	int ret=0;
//定义socket描述符
	int server_fd = 0;
	int client_fd = 0;
	int flag = 1;
//定义套接字信息结构体
	struct sockaddr_in addr;
	struct sockaddr_in client_addr;
	socklen_t addr_len=sizeof(client_addr);
/*1.创建socket描述符*/
	server_fd = socket(AF_INET, SOCK_STREAM, 0);
	if( server_fd == -1)  handle_error("socket"); 
	
	if( setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )
	perror("setsockopt");
//创建事件表
	epfd = epoll_create(1);
	if(epfd == -1) handle_error("epoll_create");
//将监听套接字写进事件表
	tmp.events = EPOLLIN;
	tmp.data.fd = server_fd;
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,server_fd,&tmp) == -1) handle_error("epoll_ctl");
/*2.设置套接字信息结构体*/
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5001);
	addr.sin_addr.s_addr = SERVER_ADDR;
/*3.绑定地址信息*/
	ret = bind(server_fd,(struct sockaddr*)&addr,sizeof(addr));
	if(ret == -1) handle_error("bind");
/*4.监听客户端*/
	ret = listen(server_fd,LISTEN_LEN); 
	if(ret == -1) handle_error("listen");

	while(1){
	//使用epoll_wait进行阻塞
		nfds = epoll_wait(epfd,events,MAX_SOCK_FD,-1);
		if(nfds == -1) handle_error("epoll_wait");
	//进行循环
		for(i=0;i<nfds;i++){
			if(events[i].data.fd == server_fd){  //接收客户端
				client_fd = accept(server_fd,(struct sockaddr*)&client_addr,&addr_len);
				if(client_fd == -1)   handle_error("accept");
				tmp.events = EPOLLIN;
				tmp.data.fd= client_fd;
				if(epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&tmp) == -1) handle_error("epoll_ctl");
				printf("[%s][%d] connect success\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
			}
			else if(events[i].events & EPOLLIN){     //处理客户端数据;
				if(client_pro(events[i].data.fd) <= 0){
					if(getpeername(events[i].data.fd,(struct sockaddr*)&client_addr,&addr_len) == -1) handle_error("getpeername");
					printf("[%s][%d] exit\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
					epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
				}
			}
		}
	}
/*8.关闭文件描述符*/
    close(epfd);
	close(server_fd);
	close(client_fd);
}


int client_pro(int fd){
	//定义接收缓冲区
	char buf[BUF_MAXLEN]={0};
	struct sockaddr_in peeraddr;
    socklen_t addrlen=sizeof(peeraddr);

	int rev = read(fd,buf,BUF_MAXLEN);
	if(rev > 0){
		getpeername(fd,(struct sockaddr*)&peeraddr,&addrlen);
		printf("[%s][%d]buf:%s\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port),buf);
	}else if(rev < 0){
		perror("read");
	}
	return rev;
}


        1.3.5 epoll核心总结

        (1)核心组成:3 个函数族(epoll_create/epoll_ctl/epoll_wait)+ 2 个数据结构(struct epoll_event/epoll_data_t)+ 多类事件宏,分工明确,接口高效;

        (2) 核心优势:

           a.无 fd 数量硬限制,支持百万级 fd 监控,满足高并发场景;
           b.事件驱动模式,无需轮询,效率 高,不随 fd 数量增长而下降;
           c.仅一次拷贝(注册时),后续无需重复拷贝 fd 信息,拷贝开销极小;
           d.支持水平触发 / 边缘触发两种模式,灵活性远超 select/poll;

          (3) 核心局限

            a.仅支持 Linux 平台,无跨平台兼容性(对比 select/poll);
            b.边缘触发(ET)模式编程复杂度高,需配合非阻塞 IO,且要一次性处理完缓冲区数据;

           

二. select/poll/epoll函数的优劣对比

        2.1 epoll函数的核心优势

        (1)减少【用户态<------>内核态】的数据拷贝开销;

           这是epoll节约资源的基础。首先select函数,select需要每次将位图fd_set在内核和用户态复制来复制去的,很麻烦,特别是在高并发场景中,而且是读写异常三个事件类型的位图,而poll则需要将结构体数组在内核与用户态中复制来复制去;结构体数组占用的内存空间就更大了;

           epoll函数避免了这样的问题,epoll函数仅在注册时拷贝一次,后续循环调用 epoll_wait() 时,无需再拷贝任何待监控 fd 的信息,当调用 epoll_ctl(EPOLL_CTL_ADD) 注册 fd 时,会将该 fd 及其对应的事件信息(struct epoll_event)从用户态拷贝到内核态,并存入「epoll 内核事件表」(红黑树存储)。因为内核已经持久化存储了这些信息,无需重复传递,这就避免了 select/poll 的「重复全量拷贝」。而且当epoll_wait函数调用时仅拷贝有效数据;

        (2) 减少CPU遍历开销;

          select/poll 调用返回后,只告诉你「有多少个 fd 触发了事件」,但不告诉你「具体是哪些 fd」,因此你必须进行「全量遍历」:
          select:遍历所有待监控的 fd(从 0 到 max_fd),逐个调用 FD_ISSET() 判断该 fd 是否触发事件,大部分 fd 都是未触发事件的,遍历这些 fd 属于「无效 CPU 运算」;
          poll:遍历整个 pollfd 结构体数组,逐个判断 revents 是否非 0,同样,大部分数组元素都是无效的,遍历过程消耗大量 CPU 资源;
          时间复杂度:两者的遍历时间复杂度都是 O(n)(n 为待监控 fd 总数),fd 越多,CPU 遍历开销越大,在 fd 上万的场景下,CPU 会被大量无效遍历占据,无法处理实际业务逻辑。

         (3)减少内核内部的存储与事件检测开销

·         这部分是 epoll 底层的优化,虽然不直接暴露给用户,但对资源节省至关重要,select/poll 在核内部存在大量「重复工作」,而 epoll 做到了「持久化存储,高效检测」。
          1. 先看 select/poll 的内核资源浪费
无持久化存储:select/poll 内核内部没有专门的结构存储待监控 fd 信息,每次调用都要重新构建待监控集合,再逐个 fd 调用底层驱动接口查询事件状态,这个过程重复且低效;
轮询式事件检测:内核内部对 select/poll 的待监控 fd 采用「轮询检测」(逐个查询 fd 是否有事件),fd 越多,轮询时间越长,内核资源消耗越大;
资源限制:select 有 FD_SETSIZE 硬限制,无法支撑大量 fd 监控,即使 poll 无硬限制,内核也无法承受大量 fd 的轮询与存储开销。
          2. 再看 epoll 的内核资源优化
        持久化存储:红黑树管理待监控 fd
        epoll 内核用「红黑树」存储待监控 fd 及其事件信息,红黑树的插入、删除、查找效率都很高(时间复杂度 O (logn)),支持大量 fd 的高效管理,且无需每次调用都重新构建集合,持久化存储减少了内核的重复工作;
        高效检测:回调机制替代轮询
        epoll 采用「回调机制」检测事件,当 fd 准备就绪(有数据 / 可写)时,底层驱动会主动调用 epoll 注册的回调函数,将该 fd 加入就绪队列,无需内核主动轮询所有 fd,大幅节省内核 CPU 资源;
        低资源消耗:就绪队列仅存有效事件
内核的就绪队列只存储触发事件的 fd 信息,无需存储所有待监控 fd,内存开销极小,即使待监控 100 万个 fd,就绪队列也只占用少量内存。

           

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐