80-33 IO 模型-多路复用 epoll

背景

epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说,epoll 更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

epoll 接口

epoll 操作过程需要三个接口,分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# include <sys/epoll.h>

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 */
};

int epoll_create(int size);
int epoll_create1(int flags);

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

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

epoll_create:创建一个 struct eventpoll 的内核对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int epoll_create(int size);

作用:
创建一个 struct eventpoll 的内核对象,类似 socket,把它关联到当前进程的已打开文件列表中。
每一个 epoll 对象都有一个独立的 eventpoll 结构体,这个结构体会在内核空间中创造独立的内存,用于存放通过 epoll_ctl() 方法向 epoll 对象中添加进来的事件。
这些事件都会挂载在 eventpoll 的 rbr 红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫 ep_poll_callback,它会将发生的事件添加到 eventpoll 的 rdlist 双链表中。
需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/进程id/fd/,是能够看到这个 fd 的,所以在使用完epoll后,必须调用 close() 关闭,否则可能导致fd被耗尽。

返回值:返回一个新的文件描述符。失败时,返回-1

参数:
size 自 2.6.8 开始无意义,但必须大于 0。



epoll_create1()

作用:参数 flags 为 0 则等效于 epoll_create(),flags 可以为 EPOLL_CLOEXEC, 在exec新程序时关闭文件描述符。

struce eventpoll

1
2
3
4
5
struct eventpoll {
wait_queue_head_t wq; /*等待队列链表,存放阻塞的进程*/
struct rb_root rbr; /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct list_head rdlist; /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
};
  • wq:等待队列,如果当前进程没有数据需要处理,会把当前进程描述符和回调函数 default_wake_functon 构造一个等待队列项,放入当前 wq 对待队列,软中断数据就绪的时候会通过 wq 来找到阻塞在 epoll 对象上的用户进程。
  • rbr:一棵红黑树,管理用户进程下添加进来的所有 socket 连接。
  • rdllist:就绪的描述符的链表。当有的连接数据就绪的时候,内核会把就绪的连接放到 rdllist 链表里。这样应用进程调用 epoll_wait 时,只需要判断链表就能找出就绪进程,而不用去遍历整棵树。

epoll_ctl:向 eventpoll 对象中添加或删除一些套接字,将 epitem 添加到 eventpoll 的红黑树中。

1
2
3
4
5
6
7
8
9
10
11
12
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

作用:管理维护 epoll 所监控的 socket。如果你的 epoll 要新加一个 socket 来管理,那就调用 epoll_ctl,要删除一个 socket 也调用 epoll_ctl,通过不同的入参来控制增删改。

参数:
epfd:为 epoll_create() 返回的新文件描述符
op:为 epoll_ctl() 提供的控制操作:
EPOLL_CTL_ADD, 向 epoll 中注册一个新的文件描述符;
EPOLL_CTL_MOD, 修改关联文件描述符中的事件;
EPOLL_CTL_DEL, 移除 epoll 中的描述符,且无视 @event 参数;
fd:为需要控制的文件描述符
event:为相关联的 struct epoll_event 结构。

epoll_ctl 函数主要负责把服务端和客户端建立的 socket 连接注册到 eventpoll 对象里,会做三件事:

  1. 创建一个 epitem 对象,主要包含两个字段,分别存放 socket fd 即连接的文件描述符,和所属的 eventpoll 对象的指针;

  2. 将一个数据到达时用到的回调函数添加到 socket 的进程等待队列中,注意,跟阻塞 IO 模式不同的是,这里添加的 socket 的进程等待队列结构中,只有回调函数,没有设置进程描述符,因为在 epoll 中,进程是放在 eventpoll 的等待队列中,等待被 epoll_wait 函数唤醒,而不是放在 socket 的进程等待队列中;

  3. 将第 1 步创建的 epitem 对象插入红黑树;

简单说:epoll_ctl():向 epoll 对象中添加或删除一些套接字,将 epitem 添加到 epoll 的红黑树中。所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,当相对应的事件发生时会调用这个回调方法,这个回调方法在内核中叫ep_poll_callback,它将发生的事件的 epitem 结构体添加到 rdlist 双链表中

struct epitem

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

1
2
3
4
5
6
7
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 元素即可。如果 rdllist 链表不为空,则把发生的事件复制到用户态,同时把事件的数量返回给用户。

struct epoll_event

1
2
3
4
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
  • events:是一组标记,系统所感兴趣的事件和可能发生的返回事件,如EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLET(设置为边缘触发模式)、EPOLLONESHOT(一次性的)、EPOLLRDHUP(对端断开连接或者关闭写操作的一种表示)等。
  • data:用于存储用户数据,可以是一个指针,也可以是一个整型的标识符。

epoll_wait:等待 epoll 中监听文件描述符就绪的 I/O 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

作用:等待 epoll 中监听文件描述符就绪的 I/O 事件,类似于 select() 调用

返回值:返回需要处理的事件数目,如返回 0 表示已超时。失败时,返回-1

参数:
epfd:为 epoll 实例对应的文件描述符,由 epoll_create() 创建
events:为就绪的事件集合的地址,
maxevents:为需要就绪事件集合的大小,必须大于 0,不能大于创建 epoll_create() 时的 size
timeout:为超时时间,单位为 微秒。
0 会立即返回,
-1 将不确定,也有说法说是永久阻塞

epoll_wait 函数的动作比较简单,检查 eventpoll 对象的就绪的连接 rdllist 上是否有数据到达,如果没有就把当前的进程描述符添加到 eventpoll 的进程等待队列里,然后阻塞当前进程,等待数据到达时通过回调函数被唤醒。

当 eventpoll 监控的连接上有数据到达时,通过下面几个步骤唤醒对应的进程处理数据:

  1. socket 的数据接收队列有数据到达,会通过进程等待队列的回调函数 ep_poll_callback 唤醒红黑树中的节点 epitem;

  2. ep_poll_callback 函数将有数据到达的 epitem 添加到 eventpoll 对象的就绪队列 rdllist 中;

  3. ep_poll_callback 函数检查 eventpoll 对象的进程等待队列上是否有等待项,通过回调函数 default_wake_func 唤醒这个进程,进行数据的处理;

  4. 当进程醒来后,继续从 epoll_wait 时暂停的代码继续执行,把 rdlist 中就绪的事件返回给用户进程,让用户进程调用 recv 把已经到达内核 socket 等待队列的数据拷贝到用户空间使用。

从阻塞 IO 到 epoll 的实现中,我们可以看到 wake up 回调函数机制被频繁的使用,至少有三处地方:

  • 一是阻塞 IO 中数据到达 socket 的等待队列时,通过回调函数唤醒进程,
  • 二是 epoll 中数据到达 socket 的等待队列时,通过回调函数 ep_poll_callback 找到 eventpoll 中红黑树的 epitem 节点,并将其加入就绪列队 rdllist,
  • 三是通过回调函数 default_wake_func 唤醒用户进程 ,并将 rdllist 传递给用户进程,让用户进程准确读取数据。

从中可知,这种回调机制能够定向准确的通知程序要处理的事件,而不需要每次都循环遍历检查数据是否到达以及数据该由哪个进程处理,提高了程序效率,在日常的业务开发中,我们也可以借鉴下这一机制。

原理

讲解完了 epoll 的机理,我们便能很容易掌握 epoll 的用法了。一句话描述就是:三步曲。

  1. 第一步:epoll_create() 系统调用。此调用返回一个 epoll 实例,该实例是 eventpoll 结构体,结构体中含有关键成员(存放 epoll 中的事件的红黑树根节点 rbr、保存 epoll_wait 返回的事件的双向链表 rdllist),同时返回一个引用该实例的 fd。所以在使用完 epoll 后,必须调用 close() 关闭对应的文件描述符。

  2. 第二步:epoll_ctl() 系统调用。通过此调用绑定 fd 到 epoll 实例,将 fd 添加到 epoll 实例的监听列表(红黑树),同时为 fd 设置一个回调函数监听事件 event。

    • 当 fd 上发生相应事件时,会调用回调函数,将 fd 添加到 epoll 实例的就绪队列(rdlist 双链表)上。
  3. 第三步:epoll_wait() 系统调用。通过此调用收集收集在 epoll 监控中已经发生的事件。只需要判断就绪列表(rdlist 双链表)是否为空即可。这样,在没有描述符就绪时,epoll 能更早地让出系统资源。

工作模式

epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger):

  • LT 模式(默认模式):只要该 fd 还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作。

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

ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

看到这有人会问,这两种模式的使用会造成哪种不一样的结果?

如果此时一个客户端同时发来了 5 个数据包,按正常的逻辑,只需要唤醒一次 epoll ,把当前 socket 加一次到 ready_list 就行了,不需要加 5 次。然后用户程序可以把 socket 接收队列的所有数据包都读完。

但假设用户程序就读了一个包,然后处理报错了,后面不读了,那后面的 4 个包咋办?

如果是 ET 模式,就读不了了,因为没有把 socket 加入到 ready_list 的触发条件了。除非这个客户端发了新的数据包过来,这样才会再把当前 socket 加入到 ready_list,在新包过来之前,这 4 个数据包都不会被读到。

而 LT 模式不一样,因为每次读完有感兴趣的事件发生之后,会把当前 socket 再加入到 ready_list,所以下次肯定能读到这个 socket,所以后面的 4 个数据包会被访问到,不论客户端是否发送新包。

优缺点

优点:

  1. epoll_ctl() 函数中,为每个文件描述符都指定了回调函数,基于回调函数把就绪事件放到就绪队列中,因此,把时间复杂度从0(n)降到了0(1)

  2. 只需要在 epoll_ct1() 时传递一次文件描述符,epoll_wait() 不需要再次传递文件描述符。

  3. epoll 基于 红黑树 + 双链表 存储 没有最大连接数的限制,不存在 C1OK 问题。

注意:epoll 没有使用 MMAP 零拷贝技术。

epoll 所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 1024,举个例子,在 1GB 内存的机器上大约是 10 万左右,具体数目可以下面语句查看,一般来说这个数目和系统内存关系很大。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 系统最大打开文件描述符数
cat /proc/sys/fs/file-max


# 进程最大打开文件描述符数
ulimit -n
修改这个配置:


# 写入以下配置,soft软限制,hard硬限制
sudo vi /etc/security/limits.conf
soft nofile 65536
hard nofile 100000

伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(void)
struct epoll_event events[5];
int epfd = epoll_create(10); // 创建一个 epoll 对象
for(i = 0; i < 5; i++) {
static struct epoll_event ev;
ev.data.fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); // 向 epoll 对象中添加要管理的连接
}


while(1){
nfds = epoll_wait(epfd, events, 5, 10000); // 等待其管理的连接上的 IO 事件

for(i=0; i {
......
read(events[i].data.fd, buff, MAXBUF)
}
}

代码

c

服务器代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/types.h>

#define IPADDRESS "127.0.0.1"
#define PORT 8787
#define MAXSIZE 1024
#define LISTENQ 5
#define FDSIZE 1000
#define EPOLLEVENTS 100

//函数声明
//创建套接字并进行绑定
static int socket_bind(const char* ip,int port);
//IO多路复用epoll
static void do_epoll(int listenfd);
//事件处理函数
static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf);
//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd);
//读处理
static void do_read(int epollfd,int fd,char *buf);
//写处理
static void do_write(int epollfd,int fd,char *buf);
//添加事件
static void add_event(int epollfd,int fd,int state);
//修改事件
static void modify_event(int epollfd,int fd,int state);
//删除事件
static void delete_event(int epollfd,int fd,int state);

int main(int argc,char *argv[])
{
int listenfd;
listenfd = socket_bind(IPADDRESS,PORT);
listen(listenfd,LISTENQ);
do_epoll(listenfd);
return 0;
}

static int socket_bind(const char* ip,int port)
{
int listenfd;
struct sockaddr_in servaddr;
listenfd = socket(AF_INET,SOCK_STREAM,0);
if (listenfd == -1)
{
perror("socket error:");
exit(1);
}
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,ip,&servaddr.sin_addr);
servaddr.sin_port = htons(port);
if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
{
perror("bind error: ");
exit(1);
}
return listenfd;
}

static void do_epoll(int listenfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
int ret;
char buf[MAXSIZE];
memset(buf,0,MAXSIZE);
//创建一个描述符
epollfd = epoll_create(FDSIZE);
//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);
for ( ; ; )
{
//获取已经准备好的描述符事件
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,listenfd,buf);
}
close(epollfd);
}

static void
handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
int i;
int fd;
//进行选好遍历
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
//根据描述符的类型和事件类型进行处理
if ((fd == listenfd) &&(events[i].events & EPOLLIN))
handle_accpet(epollfd,listenfd);
else if (events[i].events & EPOLLIN)
do_read(epollfd,fd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,buf);
}
}
static void handle_accpet(int epollfd,int listenfd)
{
int clifd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
if (clifd == -1)
perror("accpet error:");
else
{
printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
//添加一个客户描述符和事件
add_event(epollfd,clifd,EPOLLIN);
}
}

static void do_read(int epollfd,int fd,char *buf)
{
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1)
{
perror("read error:");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else if (nread == 0)
{
fprintf(stderr,"client close.\n");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else
{
printf("read message is : %s",buf);
//修改描述符对应的事件,由读改为写
modify_event(epollfd,fd,EPOLLOUT);
}
}

static void do_write(int epollfd,int fd,char *buf)
{
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1)
{
perror("write error:");
close(fd);
delete_event(epollfd,fd,EPOLLOUT);
}
else
modify_event(epollfd,fd,EPOLLIN);
memset(buf,0,MAXSIZE);
}

static void add_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

static void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

static void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

客户端也用 epoll 实现,控制 STDIN_FILENO、STDOUT_FILENO、和 sockfd 三个描述符,程序如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define MAXSIZE 1024
#define IPADDRESS "127.0.0.1"
#define SERV_PORT 8787
#define FDSIZE 1024
#define EPOLLEVENTS 20

static void handle_connection(int sockfd);
static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_read(int epollfd,int fd,int sockfd,char *buf);
static void do_write(int epollfd,int fd,int sockfd,char *buf);
static void add_event(int epollfd,int fd,int state);
static void delete_event(int epollfd,int fd,int state);
static void modify_event(int epollfd,int fd,int state);

int main(int argc,char *argv[])
{
int sockfd;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//处理连接
handle_connection(sockfd);
close(sockfd);
return 0;
}


static void handle_connection(int sockfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
char buf[MAXSIZE];
int ret;
epollfd = epoll_create(FDSIZE);
add_event(epollfd,STDIN_FILENO,EPOLLIN);
for ( ; ; )
{
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,sockfd,buf);
}
close(epollfd);
}

static void
handle_events(int epollfd,struct epoll_event *events,int num,int sockfd,char *buf)
{
int fd;
int i;
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
if (events[i].events & EPOLLIN)
do_read(epollfd,fd,sockfd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,sockfd,buf);
}
}

static void do_read(int epollfd,int fd,int sockfd,char *buf)
{
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1)
{
perror("read error:");
close(fd);
}
else if (nread == 0)
{
fprintf(stderr,"server close.\n");
close(fd);
}
else
{
if (fd == STDIN_FILENO)
add_event(epollfd,sockfd,EPOLLOUT);
else
{
delete_event(epollfd,sockfd,EPOLLIN);
add_event(epollfd,STDOUT_FILENO,EPOLLOUT);
}
}
}

static void do_write(int epollfd,int fd,int sockfd,char *buf)
{
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1)
{
perror("write error:");
close(fd);
}
else
{
if (fd == STDOUT_FILENO)
delete_event(epollfd,fd,EPOLLOUT);
else
modify_event(epollfd,fd,EPOLLIN);
}
memset(buf,0,MAXSIZE);
}

static void add_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

static void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

static void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

Reference


80-33 IO 模型-多路复用 epoll
https://flepeng.github.io/010-network-80-33-IO-模型-多路复用-epoll/
作者
Lepeng
发布于
2021年3月8日
许可协议