4、Socket通信中的内存泄漏:TCP发送缓冲区与接收缓冲区溢出、UDP数据报丢失导致的内存黑洞、Unix Domain Socket的SOCK_DGRAM与SOCK_STREAM内存差异、epoll事件驱动下的内存管理
Socket通信,说白了就是进程间传数据。但传着传着,内存就丢了。
我见过太多项目,上线跑几天,内存蹭蹭往上涨。一查,全是Socket缓冲区惹的祸。今天咱们就把这块硬骨头啃下来。
4.1 TCP发送缓冲区与接收缓冲区溢出
TCP有俩缓冲区:一个发送,一个接收。内核帮你管着,但管得不彻底。
发送缓冲区溢出,怎么回事?
你调用send(),数据先拷到内核的发送缓冲区。如果对端收得慢,缓冲区就满了。send()会阻塞,或者返回EAGAIN。但很多人不管返回值,继续发。数据没发出去,内存却已经分配了。
⚠ 避坑指南
我曾经接手过一个网关项目,日志里全是send()返回-1,但代码里没处理。结果发送缓冲区堆积了上万个未发送的数据包,每个包几KB,内存直接爆了。
接收缓冲区溢出,更隐蔽。
你调用recv(),但读得不及时。内核缓冲区满了,新数据来了怎么办?
- TCP有流量控制,会通知对端别发了。但如果你程序卡死,对端一直发,缓冲区会持续增长。
- 默认缓冲区大小一般是几十KB到几百KB。但可以调大,调得越大,风险越高。
我建议:
- 每次
send()/recv()后,检查返回值。没发完的,要缓存起来,下次再发。 - 设置合理的缓冲区上限。别让内核无限分配。
- 用
setsockopt()设置SO_SNDBUF和SO_RCVBUF,但注意,内核会翻倍。
// 设置发送缓冲区大小
int sendbuf = 1024 * 1024; // 1MB
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
// 但实际大小可能是2MB,内核会翻倍
// 所以别设太大,否则内存扛不住
4.2 UDP数据报丢失导致的内存黑洞
UDP无连接,无确认,无重传。数据报丢了就丢了。
但内存黑洞是怎么回事?
你想想看:
- 发送端:调用
sendto(),数据拷到内核缓冲区,然后就不管了。如果对端没收到,这数据在内核里待多久? - 接收端:调用
recvfrom(),如果缓冲区满了,新来的数据报直接丢弃。但旧数据还在缓冲区里占着内存。
说白了,UDP没有流控。发送端可以疯狂发,接收端来不及收,缓冲区就满了。满了之后,新数据丢弃,旧数据占着内存不放。
🔍 个人经验
我记得有个视频流项目,用UDP传实时画面。发送端每秒30帧,接收端处理不过来。结果接收缓冲区里积压了上千帧数据,每帧几百KB,内存直接飙到几百MB。最后发现,是接收线程的recvfrom()循环里加了个sleep(1),导致读得太慢。
怎么防?
- 接收端:用非阻塞模式,及时清空缓冲区。
- 设置
SO_RCVBUF上限,别让内核无限分配。 - 应用层自己做流控,比如丢帧策略。
// 非阻塞接收
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
// 循环读取,直到EAGAIN
while (1) {
n = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
if (n < 0) {
if (errno == EAGAIN) break; // 缓冲区空了
// 其他错误处理
}
// 处理数据
}
4.3 Unix Domain Socket的SOCK_DGRAM与SOCK_STREAM内存差异
Unix Domain Socket,本地进程通信用的。比TCP快,不走网络栈。
但SOCK_DGRAM和SOCK_STREAM,内存管理完全不同。
| 类型 | 特点 | 内存风险 |
|---|---|---|
| SOCK_STREAM | 面向连接,可靠,有序 | 缓冲区溢出,类似TCP |
| SOCK_DGRAM | 无连接,有消息边界 | 数据报丢失,类似UDP |
SOCK_STREAM:
- 有发送和接收缓冲区,大小可调。
- 如果对端读得慢,缓冲区会堆积。和TCP一样,要检查返回值。
SOCK_DGRAM:
- 每个数据报独立存储。如果接收端不读,数据报会堆积在缓冲区。
- 缓冲区满了,新数据报丢弃。但旧数据报占着内存不放。
- 而且,数据报有大小限制。默认可能只有几KB,但可以调大。
💡 小技巧
我个人习惯:用SOCK_STREAM做控制通道,用SOCK_DGRAM做数据通道。控制通道要可靠,数据通道可以丢。但不管哪种,都要设置缓冲区上限。
// 创建Unix Domain Socket
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
// 设置接收缓冲区大小
int rcvbuf = 1024 * 64; // 64KB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
4.4 epoll事件驱动下的内存管理
epoll是Linux下高性能IO多路复用机制。但用不好,内存泄漏比谁都狠。
epoll的内存泄漏常见场景:
- 事件注册后不注销:用
epoll_ctl()注册了fd,但fd关闭时没调用EPOLL_CTL_DEL。epoll内部还保留着这个fd的引用,内存泄漏。 - 边缘触发(ET)模式下的数据残留:ET模式下,数据没读完,epoll不会再次通知。如果程序没循环读,数据留在缓冲区里,内存泄漏。
- 事件处理函数中分配的内存没释放:比如
epoll_wait()返回后,你分配了内存处理数据,但处理完忘了free。
⚠ 避坑指南
我曾经排查过一个epoll内存泄漏,查了三天。最后发现,是某个fd在关闭前没调用EPOLL_CTL_DEL。epoll内部维护了一个红黑树,每个节点对应一个fd。fd关了,但节点还在树上,内存就漏了。
我建议:
- 关闭fd前,一定要调用
epoll_ctl(fd, EPOLL_CTL_DEL, ...)。 - ET模式下,用循环读,直到返回
EAGAIN。 - 事件处理函数中,用RAII或智能指针管理内存。
// 正确的epoll使用方式
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 关闭前注销
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
close(sockfd);
// ET模式下循环读
while (1) {
n = read(sockfd, buf, sizeof(buf));
if (n <= 0) {
if (errno == EAGAIN) break; // 读完了
// 错误处理
break;
}
// 处理数据
}
🔍 总结一下
Socket通信的内存泄漏,核心就三点:
- 缓冲区管理:设上限,检查返回值,及时清空。
- 事件驱动:注册要注销,ET模式要循环读。
- 资源释放:分配的内存、注册的事件,都要成对释放。
嗯,做到这三点,大部分内存泄漏都能防住。