AI图鉴

AI图鉴首页 › AI 资讯 › epoll 回显服务器是怎么工作的:从 accept 队列到就绪链表,以及它和

epoll 回显服务器是怎么工作的:从 accept 队列到就绪链表,以及它和 select / poll 的区别

来源:掘金 · 发布时间:11-02 · 由 AI图鉴每小时自动聚合

一个 epoll 回显服务器里,listen_fd 为什么「可读」就代表有新连接,epoll_wait 返回后的 for 循环和 select 有什么不同。

一句话结论:listen_fd 这个整数从来不变,变的是它背后那个监听 socket 的 accept 队列:三次握手由内核独立完成,握手成功的连接排进队列,队列非空,监听 socket 就「可读」。epoll 把「登记 fd」和「等待事件」拆开,fd 就绪时内核通过回调把它挂到就绪链表,epoll_wait 只把就绪的那几个交给你;select 每次都要把整个集合拷进内核、两边各扫一遍。所以 for (i = 0; i < n; i++) 里的 n 是就绪数,不是连接总数。

这篇是我自己的复习笔记,围绕一份 epoll 回显服务器(下文称 demo.c)讲清两个问题:监听 fd 可读到底意味着什么,以及 epoll 和 select / poll 的区别到底在哪。事实以 Linux man pages(epoll(7)、select(2)、poll(2)、listen(2))和内核源码为准;文中的命令输出来自 WSL2 Ubuntu 24.04(内核 5.10)上的真实运行。TCP 握手本身的序号细节见另一篇《从三次握手到 TLS 1.3》。

先用一家餐厅把全貌讲一遍

把服务器想成一家餐厅:

这家餐厅运转起来是这样:前台在门口接待新客人(内核做握手),客人在门口排好队;服务员不用挨桌去问「要不要点菜」,而是谁按铃就去谁那里(epoll 的就绪通知)。服务员每次回到厨房(epoll_wait 返回),手里拿着的是「这几桌按了铃」的单子:如果是前台按的铃,就去领位(accept);如果是某张桌子按的,就去上菜(read / write)。

select 的服务员则是另一种工作方式:每一轮都把所有桌号抄一遍交给经理,经理挨桌问一遍,再把单子还回来,服务员自己还要从头到尾再看一遍是谁举了手。

**类比到此为止。**下面讲技术细节时都回到真实概念:fd、socket、队列、回调。餐厅里没有「内核态和用户态之间的拷贝」,而这恰恰是 select 和 epoll 差别最大的地方。

整个程序:一个循环,两个分支

demo.c 的结构可以压成一句话:先建好监听 socket 和 epoll,然后永远循环 epoll_wait,返回的每个就绪 fd 只有两种可能——是监听 fd 就 accept,是连接 fd 就 read。

- socket() 创建 TCP socket,得到 listen_fd(通常是 3)。

- bind() 绑定 0.0.0.0:8888。

- listen(listen_fd, 128):socket 变成监听状态,内核开始替它完成三次握手。

- epoll_create1(0) 创建 epoll 实例,得到 epfd(通常是 4)。

- epoll_ctl(ADD, listen_fd):把监听 fd 登记进 epoll,关心 EPOLLIN。

- 循环 epoll_wait():阻塞到至少一个 fd 就绪,返回就绪数 n;对每个就绪事件取出 fd = events[i].data.fd。

- 分支一,fd == listen_fd:有新连接 → accept() 得到 conn_fd → epoll_ctl(ADD, conn_fd) 也登记进 epoll。

- 分支二,其他 fd:某个连接有数据 → read();返回值大于 0 就 write() 原样写回,返回 0(对方关闭)或小于 0(出错)就 close(fd)。

- 处理完 n 个事件,回到 epoll_wait()。

▶ 本节有交互演示「demo.c 的总流程」,可在原文中动手操作:打开交互版

对应到代码,循环体就是下面这一段(摘自 demo.c,省略了打印和错误处理):

for (;;) {

int n = epoll_wait(epfd, events, MAX_EVENTS, -1);

for (int i = 0; i < n; i++) {

int fd = events[i].data.fd;

if (fd == listen_fd) {

int conn_fd = accept(listen_fd, (struct sockaddr *)&cli, &len);

ev.events = EPOLLIN;

ev.data.fd = conn_fd;

epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);

} else {

ssize_t r = read(fd, buf, sizeof(buf));

if (r <= 0) { close(fd); continue; }

write(fd, buf, r); // 回显

}

}

}

两个问题都藏在这段代码里:为什么 listen_fd 会出现在 events 里(下一节),以及外层这个 for 和 select 的循环有什么不同(再下一节)。

listen_fd 为什么「可读」就说明有新连接

fd 只是一个下标

listen_fd = 3 是进程 fd 表里的下标,它指向内核里的一个 socket 对象。整数本身没有状态,状态都在它指向的对象上。运行中的 demo 可以直接看到这张表:

$ ls -l /proc/$(pidof demo)/fd | awk '{print $9, $10, $11}'

0 -> /dev/pts/0

1 -> /dev/pts/0

2 -> /dev/pts/0

3 -> socket:[19642]

4 -> anon_inode:[eventpoll]

5 -> socket:[19643]

0、1、2 是标准输入输出,3 是监听 socket,4 是 epoll 实例,5 是第一个 accept 出来的连接。方括号里是 socket 的 inode 号:3 和 5 是两个不同的 socket 对象。这也是第一个连接的 conn_fd 通常是 5 的原因——Linux 总是分配当前最小的空闲 fd。

listen 之后,握手由内核独立完成

listen(listen_fd, 128) 把 socket 变成监听状态。从这一刻起,内核会替这个 socket 处理所有到达 8888 端口的 SYN,而且整个握手过程不需要用户程序执行任何一行代码:

- 客户端的 SYN 到达,内核记下这个「半连接」(概念上叫 SYN 队列),回 SYN+ACK;

- 客户端回 ACK,握手完成,内核为它创建一个新的 socket,放进监听 socket 的全连接队列(accept 队列);

- accept() 只做一件事:从 accept 队列头部取走一个已经建好的连接,给它分配一个新 fd。

listen 的第二个参数 128 限制的是 accept 队列的长度,并且会被 /proc/sys/net/core/somaxconn 截断(Linux 5.4 起默认 4096,更早是 128)。半连接的数量由另外的参数管:tcp_max_syn_backlog,开启 syncookies 时没有逻辑上限——这一段 listen(2) 的手册写得很清楚。

「可读」对监听 socket 的定义

对普通连接 socket,可读的意思是「接收缓冲区里有数据」;对监听 socket,内核把可读定义成 accept 队列非空(内核源码 inet_csk_listen_poll():队列不空就返回 EPOLLIN)。所以 demo.c 里那行注释是准确的:

ev.events = EPOLLIN; // 关心「可读」事件(对监听 fd 来说 = 有新连接)

握手完成、新连接入队的那一刻,内核还会唤醒在这个 socket 上等待的人——epoll 就是在这里收到通知的(下一节细讲回调)。下面把客户端、内核、用户程序三个区域放在一起,一步步走一遍:

客户端(nc)、内核、用户程序三方的时间线:

- 程序执行 socket → bind → listen:内核里出现一个监听 socket(fd 3),它带着两个队列:SYN 队列和 accept 队列,accept 队列上限 128。

- 程序执行 epoll_create1(fd 4)和 epoll_ctl(ADD, 3),然后阻塞在 epoll_wait。

- 客户端发 SYN:连接进入 SYN 队列。用户程序仍阻塞在 epoll_wait,一行代码都没执行。

- 内核回 SYN+ACK,客户端回 ACK:连接从 SYN 队列移到 accept 队列。客户端的 connect() 已经返回,nc 认为自己连上了。用户程序还是一行代码都没执行。

- accept 队列从空变成非空:监听 socket 变成可读。listen_fd = 3 这个数字没变,变的是它背后 socket 的状态。

- epoll 的回调把 fd 3 挂上就绪链表,epoll_wait 返回 1,events[0].data.fd == 3。

- 程序调用 accept():从 accept 队列取走这个连接,得到新的 conn_fd = 5。队列又空了,监听 socket 不再可读。

- 实验:如果程序一直不 accept,第二、第三个客户端照样能握手成功,只是排在 accept 队列里。

- 队列满了以后,新来的 SYN 会被丢弃,客户端只能重传 SYN;具体行为取决于内核参数(见下文)。

▶ 本节有交互演示「三次握手与 accept 队列」,可在原文中动手操作:打开交互版

listen_fd 这个数字从头到尾都是 3,变的是它背后那条 accept 队列是空还是不空。

accept 队列满了会怎样:取决于内核参数

这是容易被问、也容易说错的地方。以 Linux 5.x 的实现为准,分两种情况:

- 队列满时又来一个 SYN:内核直接丢掉这个 SYN,不回任何东西,计数器 ListenOverflows / ListenDrops 加一。客户端收不到 SYN+ACK,会按指数退避重传 SYN,表现为 connect() 卡住,最后可能超时。

- 握手的最后一个 ACK 到达时队列已满:默认(net.ipv4.tcp_abort_on_overflow = 0)内核丢掉这个 ACK,连接留在半连接状态,稍后重传 SYN+ACK,如果到时候队列有空位就能「自愈」;设为 1 时,内核直接回 RST,客户端会看到 Connection reset by peer。

不同内核版本在细节上有差别(比如队列的判满条件),所以描述这一点时说「取决于 tcp_abort_on_overflow,默认是丢弃而不是 RST」就够了,不要说死。下文「动手验证」里有一个把 backlog 改成 1 的实验,可以亲眼看到 SYN 被丢。

epoll_wait 之后还要写 for 循环,和 select 有什么区别

区别不在「有没有循环」,而在循环的对象是谁、循环之前内核做了多少事。先把三种机制各自在一次调用里做了什么摆出来。

select:每次都交出整张表,再拿回整张表

select(maxfd + 1, &readfds, NULL, NULL, NULL);

fd_set 是一个位图,每个 bit 代表一个 fd。每次调用:

- 把整个位图从用户态拷贝进内核;

- 内核从 0 扫到 maxfd,对每个置位的 fd 调一次它的 poll 函数,问「你现在可读吗」;都没就绪,就把自己挂到每个 fd 的等待队列上睡眠;

- 任何一个 fd 就绪都会把它唤醒,醒来后内核再扫一遍所有 fd,把没就绪的 bit 清零;

- 把位图拷回用户态,返回就绪数;

- 用户程序只知道「有几个」,不知道「是哪几个」,必须再用 FD_ISSET 把每个 fd 检查一遍;

- 位图被内核改写了,下一轮调用前要用 FD_ZERO / FD_SET 重新设置。

另外,fd_set 的大小固定为 FD_SETSIZE(1024),编号大于等于 1024 的 fd 不能用 select 监视。

poll:换了数据结构,没换工作方式

poll 用 struct pollfd 数组代替位图:events 写你关心什么,内核把结果写到另一个字段 revents。所以 poll 解决了两个问题——没有 1024 的上限,也不用每轮重建集合。但每次调用依然要把整个数组拷进内核、内核依然要逐个检查、用户依然要逐个看 revents,开销仍然和监视的 fd 总数成正比。

epoll:登记一次,就绪时回调

epoll 把 select 一个调用里的事拆成三个调用:

- epoll_create1:在内核里建一个 epoll 实例,里面有两样东西——兴趣列表(你登记了哪些 fd,内核用红黑树存)和就绪链表(哪些 fd 已经就绪)。

- epoll_ctl:登记、修改、删除一个 fd。登记时内核把一个回调函数(ep_poll_callback)挂到这个 socket 的等待队列上。只在登记时做一次,之后每次等待都不用再交出集合。

- epoll_wait:只看就绪链表。链表空就睡,非空就把就绪的事件拷给用户,返回个数。

socket 收到数据时,网络协议栈唤醒它的等待队列,回调函数就把对应的条目挂到就绪链表尾部。于是 epoll_wait 返回时,events 数组里只有就绪的 fd。

场景:已连接 10 个 fd(fd 5~14),其中只有 fd 7 和 fd 12 收到数据。

一轮下来,按「逐个检查一个 fd」计数:select 内核检查 20 次、用户检查 10 次;epoll 内核只确认就绪链表上的 2 个,用户循环 2 次。

▶ 本节有交互演示「select 与 epoll:10 个连接,2 个有数据」,可在原文中动手操作:打开交互版

那个 for 和 if 到底在做什么

回到 demo.c:

int n = epoll_wait(epfd, events, MAX_EVENTS, -1);

for (int i = 0; i < n; i++) {

int fd = events[i].data.fd;

if (fd == listen_fd) {

- n 是这一次就绪的事件数,不是连接总数。1 万个连接里只有 3 个有数据,n 就是 3,循环跑 3 次。select 的用户循环则要从 0 扫到 maxfd。

- if (fd == listen_fd) 不是在「搜索」哪个 fd 就绪了——events 里的每一项都已经是就绪的。它只是在已就绪的 fd 里分类型:这个是监听 socket(去 accept),那个是连接 socket(去 read)。select 版本里同样需要这个判断,而且是在 FD_ISSET 找到之后。

所以两者的差别是:select 的循环长度是「监视了多少个 fd」,epoll 的循环长度是「就绪了多少个 fd」;select 每次调用都要把整个集合在用户态和内核态之间来回拷贝、由内核扫一遍,epoll 只在 epoll_ctl 时付一次登记成本。

epoll 不是在所有情况下都更快

- ✔ 连接多、活跃少时优势最大 等待的开销跟就绪数走,而不是跟连接总数走,上万个空闲长连接的服务器最受益。

- ✘ 连接全都很活跃时优势变小 每次大部分 fd 都就绪,epoll 也要把它们逐个交给你,和 select / poll 的线性扫描差不了多少。

- ✘ 登记本身有成本 每个 epoll_ctl 都是一次系统调用,大量短连接频繁增删时这部分开销会显现出来。

- ✘ fd 很少时没有明显差别 只盯几个 fd 的程序(很多嵌入式应用就是这样),select / poll 足够用,而且可移植。

- 只是「等」的部分更快 epoll 不会让 read / write 本身变快,业务处理慢的瓶颈它解决不了。

ev.data.fd:你自己贴的标签,内核原样还给你

epoll_event 的定义(来自 epoll_ctl(2))是这样的:

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

};

data 是一个联合体,内核不解释它的内容,只负责存下来、在事件发生时原样还给你。真正决定「监视哪个 fd」的是 epoll_ctl 的第三个参数,不是 data.fd:

ev.events = EPOLLIN; // 关心「可读」事件(对监听 fd 来说 = 有新连接)

ev.data.fd = listen_fd; // 事件发生时,靠这个知道是哪个 fd

if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) die("epoll_ctl listen");

demo.c 把 fd 本身当标签,所以 events[i].data.fd 正好就是那个 fd——这是约定,不是 epoll 的规定。更复杂的服务器常常在 data.ptr 里放一个指向连接结构体的指针,事件来了直接拿到这个连接的缓冲区和状态,省掉一次查表。

标签的一次往返:

- 用户填好 ev:events = EPOLLIN,data.fd = 3。

- epoll_ctl(epfd, EPOLL_CTL_ADD, 3, &ev):内核按第三个参数 3 找到要监视的 socket,同时把整个 ev(包括 data)拷贝一份存进兴趣列表里的条目。

- fd 3 背后的 socket 就绪:这个条目被挂上就绪链表。

- epoll_wait 把条目里存着的 events 和 data 原样拷进用户的 events[0]。

- 用户读到 events[0].data.fd == 3——就是第 1 步自己写进去的那个值。

如果第 1 步写的是 data.u64 = 0xBEEF 或者一个指针,第 5 步拿回来的就是 0xBEEF 或那个指针;如果故意写成 data.fd = 99,epoll 也照样还给你 99,它不检查。

▶ 本节有交互演示「ev.data 这张标签怎么来回走」,可在原文中动手操作:打开交互版

这也解释了 demo.c 里两个容易让人犯嘀咕的地方:

- 同一个 ev 变量被反复使用:先给监听 fd 用,后面每 accept 一个连接又改写一次。这是安全的,因为 epoll_ctl 在调用时就把内容拷进了内核,之后你改用户态的 ev 不会影响已经登记的条目。

- 「close 之后 epoll 会自动把这个 fd 移除」:在 demo.c 里成立,但有前提。epoll 跟踪的其实是 fd 背后的「打开文件」,只有当指向它的所有 fd 都关闭了(没有被 dup 过、没有 fork 出去的子进程还拿着)才会自动移除——这是 epoll(7) 问答部分专门提醒的一条。稳妥的写法是先 epoll_ctl(EPOLL_CTL_DEL) 再 close。

水平触发和边缘触发:同一份数据,通知几次

epoll 给每个 fd 提供两种通知方式,由登记时 ev.events 里有没有 EPOLLET 决定:

- 水平触发(LT,默认):只要 fd 处于就绪状态,比如接收缓冲区里还有数据,每次 epoll_wait 都会报告它。demo.c 用的就是 LT。

- 边缘触发(ET):只在状态发生变化时报告。对「可读」来说,就是有新数据到达的那一刻;之后缓冲区里就算还剩数据,只要没有新数据进来,epoll_wait 就不会再报告这个 fd。

下面是实测(WSL2,内核 5.10):对端一次发来 3000 字节,服务端每次被唤醒只 read 1024 字节。

ET 只读一次的那一行里,剩下的 1976 字节并没有丢,只是不会再触发通知。实测中对端随后再发 2 字节,epoll_wait 又返回了 1 次:新数据到达是一次新的「边缘」。如果对端正在等你的回复、不再发东西,这条连接就会一直卡着。

▶ 本节有交互演示「LT 与 ET:3000 字节,每次读 1024」,可在原文中动手操作:打开交互版

ET 必须配非阻塞 fd,读到 EAGAIN 为止

ET 只通知一次,所以收到通知后必须把数据读空。读空的标志是 read 返回 -1 且 errno == EAGAIN(或 EWOULDBLOCK),这要求 fd 是非阻塞的。accept 出来的 fd 默认是阻塞的:如果不改,读空之后的最后那次 read 会一直阻塞,整个事件循环都卡在这一个连接上。

如果把 demo.c 改成 ET,accept 之后要先把 fd 设成非阻塞,登记时加上 EPOLLET,5b 分支改成循环读。下面这段是示意,不是 demo.c 原文:

// accept 之后:

fcntl(conn_fd, F_SETFL, fcntl(conn_fd, F_GETFL) | O_NONBLOCK);

ev.events = EPOLLIN | EPOLLET;

// 5b 分支:

for (;;) {

ssize_t r = read(fd, buf, sizeof(buf));

if (r > 0) { write(fd, buf, r); continue; } // 读到数据,接着读

if (r == 0) { close(fd); break; } // 对方关闭

if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 读空了,等下一次通知

if (errno == EINTR) continue; // 被信号打断,重试

close(fd); break; // 其他错误

}

监听 fd 也一样:ET 下几个连接同时到达只会通知一次,accept 也要循环调用到返回 EAGAIN,否则剩下的连接会一直排在 accept 队列里。

什么时候用哪个

- LT:默认值,容易写对。「每次通知只读一次」这种写法只在 LT 下正确,漏读了下一轮还会被提醒。不确定时用 LT。Redis 的事件库用的就是 LT。

- ET:同一份数据只通知一次,epoll_wait 的返回次数更少,代价是必须用非阻塞 fd、每次都读写到 EAGAIN,每个连接的状态要管得更严。Nginx 用的是 ET。

- 写事件的差别更明显:LT 下只要发送缓冲区还有空间,EPOLLOUT 每次都会报告;ET 下只在发送缓冲区从满变成不满时报告一次。下一节会讲 EPOLLOUT 该怎么用。

read 和 write:每种返回值意味着什么

read

两点要注意:

- **TCP 是字节流,没有消息边界。**客户端一次发的一行字,服务端可能分两次读到,也可能和下一行一起读到。回显服务器不在乎,但只要有自己的协议(比如按行、按长度头分包),就得在应用层自己拼包。

- demo.c 用 r <= 0 把 0 和 -1 一起处理、一律关闭。在阻塞 fd + LT 下基本够用:epoll 报告可读之后,那一次 read 不会返回 EAGAIN。一旦换成非阻塞 fd 或 ET,就必须把 EAGAIN 单独拿出来,否则会把正常的连接误关掉。

write

实测两个细节:

- 对方关闭之后,第一次 write 仍然返回 1(数据被拷进了发送缓冲区,对方回了 RST),第二次才返回 -1 / EPIPE。如果没有忽略 SIGPIPE,第二次 write 时进程就直接退出了。demo.c 没有处理 SIGPIPE。

- 让对方一直不读,在本机回环上用非阻塞 fd 一直写,写进约 2.5 MB 后返回 EAGAIN(自己的发送缓冲区和对方的接收缓冲区都满了,具体大小取决于内核的缓冲区自动调节)。demo.c 的 fd 是阻塞的,这时 write 会一直卡住,整个事件循环陪它一起卡住:一个不读数据的客户端,就能让所有客户端都失去响应。demo.c 也没有检查 write 的返回值。

EPOLLOUT 只在需要时登记

实测:在 LT 下登记 EPOLLOUT、什么都不写,连续 5 次 epoll_wait 都立即报告可写。发送缓冲区几乎总是有空间,所以一直挂着 EPOLLOUT 会让事件循环空转。正确的用法是:

- 平时只关注 EPOLLIN;

- write 没写完(部分写入或 EAGAIN)时,把剩余数据存进这个连接自己的发送缓冲,用 EPOLL_CTL_MOD 加上 EPOLLOUT;

- 等 EPOLLOUT 到来,把剩余数据发完,再用 EPOLL_CTL_MOD 把 EPOLLOUT 去掉。

断开时 epoll 报告什么

EPOLLHUP 和 EPOLLERR 不用登记,epoll 总会报告;EPOLLRDHUP 需要自己在 ev.events 里加上。demo.c 只登记了 EPOLLIN,靠 read 的返回值判断断开,也是可以的。

动手验证:两个终端,亲眼看内核做握手

1. 跑起来

终端 A 编译并运行:

gcc demo.c -o demo

./demo

终端 B 连上去,输入什么就会回什么:

nc 127.0.0.1 8888

终端 A 会打印 new client 127.0.0.1:xxxxx (fd=5)。再开一个终端连第二个,就是 fd=6。

2. 看监听队列:ss -lnt

ss -lnt 'sport = :8888'

State Recv-Q Send-Q Local Address:Port Peer Address:Port Process

LISTEN 0 128 0.0.0.0:8888 0.0.0.0:*

对 LISTEN 状态的 socket,这两列的含义和普通连接不一样:

- Recv-Q:accept 队列里当前排着多少个已完成握手、还没被 accept 的连接;

- Send-Q:accept 队列的上限,也就是 min(backlog, somaxconn),这里是 128。

demo 正常运行时 accept 很及时,Recv-Q 基本一直是 0。

3. 不 accept,nc 依然能连上

把 5a 分支改成「只打印、不 accept」:在 if (fd == listen_fd) { 下面加一行,原来的代码不用删,只是执行不到了:

if (fd == listen_fd) {

puts("listen_fd 可读,但我不 accept"); fflush(stdout); sleep(1); continue;

// ── 5a:新连接 → accept,拿到一个专门和这个客户端说话的新 fd

重新编译运行,然后开三个终端分别 nc 127.0.0.1 8888,每个都输入一行字(比如 msg1)。能观察到三件事。

第一,三个 nc 都没有报错,客户端认为自己已经连上了。握手是内核做的,跟程序有没有 accept 无关。

第二,监听 socket 的 Recv-Q 变成 3:三个连接都排在 accept 队列里。

State Recv-Q Send-Q Local Address:Port Peer Address:Port Process

LISTEN 3 128 0.0.0.0:8888 0.0.0.0:*

第三,连客户端发来的数据都已经被内核收下了。看服务端这一侧的三个连接,每个的 Recv-Q 都是 5(msg1 加换行正好 5 个字节)——数据躺在内核的接收缓冲区里,等着一个永远不来的 read:

ss -tn state established '( sport = :8888 )'

Recv-Q Send-Q Local Address:Port Peer Address:Port Process

5 0 127.0.0.1:8888 127.0.0.1:34192

5 0 127.0.0.1:8888 127.0.0.1:34190

5 0 127.0.0.1:8888 127.0.0.1:34188

终端 A 还会每秒打印一次 listen_fd 可读,但我不 accept:队列一直不空,监听 socket 一直可读,epoll 默认的水平触发就会每次 epoll_wait 都报告它。这就是那行 sleep(1) 的用途——去掉它,程序会在 epoll_wait 和 continue 之间空转,把一个 CPU 核跑满。

4. (可选)把队列撑满

再把 listen(listen_fd, 128) 临时改成 listen(listen_fd, 1),保持

本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。

相关 AI 工具

相关免费羊毛

同源资讯

阅读原文(juejin.cn)

标题、摘要与正文均来自该信源的公开页面,版权归原作者所有;本站自动聚合与翻译,仅作出处跳转。

← 返回 AI 资讯列表AI工具图鉴首页

返回 AI图鉴首页 · 提交工具 · 商务合作 · 关于本站 · 607 款 AI 工具分类导航 · 每日 AI 热门项目与动态