[AI] curl -NT. 导致100% CPU原因
有AI就是好使,搁以前自己得盯半天也看不明白。。。
问题确认
curl -NT. 在连接一个持续推送数据的 streaming 服务器时,会产生一个 紧密的 pause/unpause 循环,导致 100% CPU。
根因分析
-T.(注意是点号,不是减号)将 stdin 设为非阻塞模式,然后依赖 EAGAIN + CURL_READFUNC_PAUSE 机制来避免阻塞。核心循环发生在三个回调之间:
第一步 — 上传尝试失败(src/tool_cb_rea.c):
{
rc = read(per->infd, buffer, sz * nmemb);
if(rc < 0) {
if(errno == EAGAIN) {
errno = 0;
config->readbusy = TRUE;
return CURL_READFUNC_PAUSE;
}
/* since size_t is unsigned we cannot return negative values fine */
rc = 0;
}
}
stdin 没数据 → read() 返回 EAGAIN → 设 readbusy=TRUE → 暂停上传。
第二步 — 下载数据到达立刻解除暂停(src/tool_cb_wrt.c):
if(config->readbusy) {
config->readbusy = FALSE;
curl_easy_pause(per->curl, CURLPAUSE_CONT);
}
streaming 服务器持续发送数据 → tool_write_cb 被不断调用 → 每次都无等待地立刻解除上传暂停。
第三步 — 进度回调只有 1ms 退避(src/tool_cb_rea.c):
if(config->readbusy) {
if(ulprev == ulnow) {
#ifndef _WIN32
waitfd(1, per->infd);
#else
/* sleep */
curlx_wait_ms(1);
#endif
}
config->readbusy = FALSE;
curl_easy_pause(per->curl, CURLPAUSE_CONT);
}
tool_readbusy_cb 有 1ms 的 waitfd 退避,但被 tool_write_cb 的无等待解除抢先了。
完整的死循环(每次 curl_multi_perform 内):
在 lib/transfer.c 的 Curl_sendrecv 中,处理顺序是先下载后上传:
if(k->keepon & KEEP_RECV) {
result = sendrecv_dl(data, k);
if(result || data->req.done)
goto out;
}
/* If we still have writing to do, we check if we have a writable socket. */
if(Curl_req_want_send(data)) {
result = sendrecv_ul(data);
if(result)
goto out;
}
所以单次迭代内:
1. sendrecv_dl → 读到 streaming 数据 → tool_write_cb → 看到 readbusy=TRUE → 立刻解除暂停(零等待)
2. sendrecv_ul → tool_read_cb → stdin EAGAIN → 又暂停,readbusy=TRUE
3. 回到 curl_multi_poll → 下载 socket 有数据 → 立刻返回(不会 sleep)
4. 重复 1-3
而 lib/easy.c 的主循环:
while(!done && !mresult) {
int still_running = 0;
mresult = curl_multi_poll(multi, NULL, 0, 1000, NULL);
if(!mresult)
mresult = curl_multi_perform(multi, &still_running);
curl_multi_poll 的 1000ms 超时形同虚设——因为下载 socket 始终可读,poll() 立刻返回。
结果:每秒跑上千次这个循环,每次包含多个系统调用(poll、read、write),CPU 直接打满。
问题本质
stdin 没有被加入 curl_multi_poll 的 pollset。只有网络 socket 参与了 poll。所以 curl 无法高效地"同时等待 stdin 和网络数据",只能靠 tool_write_cb 里的盲目 unpause 来弥补,在 streaming 场景下退化成忙等。
用户侧解决方案
如果你不需要从 stdin 上传数据(只是想看 streaming 响应),不要用 -T.:
# 正确的方式:只看 streaming 输出
curl -N https://streaming-server/events
# 或者重定向 stdin 为空
curl -NT. https://streaming-server/events < /dev/null
< /dev/null 会让 read() 返回 0(EOF)而不是 EAGAIN,不会进入 pause/unpause 循环。
如果确实需要双向 streaming(边上传 stdin 边接收),目前没有完美的用户侧 workaround。可以考虑:
# 用管道加一层缓冲,减轻空转(效果有限)
cat | curl -NT. https://streaming-server/events
源码级修复思路
如果要在源码层面修复,最直接的方案是在 tool_write_cb 中去掉盲目 unpause,改为只在 tool_readbusy_cb 中带退避地 unpause,并增大退避时间:
// tool_cb_wrt.c — 去掉这段,或加条件限制
// 不再从 write callback 无脑解除上传暂停
if(config->readbusy) {
config->readbusy = FALSE;
curl_easy_pause(per->curl, CURLPAUSE_CONT); // 删除或改为有条件触发
}
// tool_cb_rea.c — 增大退避时间
if(config->readbusy) {
if(ulprev == ulnow) {
waitfd(100, per->infd); // 从 1ms 提高到 100ms
}
config->readbusy = FALSE;
curl_easy_pause(per->curl, CURLPAUSE_CONT);
}
更彻底的方案是把 stdin fd 加入 curl_multi_poll 的 extra fds,但这需要改 curl_multi_poll 的调用方式,涉及面更大。
Posted
stdout
