news 2026/10/19 9:16:32

Linux网络编程:send()与SIGPIPE信号导致进程崩溃的排查与修复

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux网络编程:send()与SIGPIPE信号导致进程崩溃的排查与修复

场景其实特别常见:排查线上服务时,发现进程突然消失,翻日志却什么都没打印,最后一看是Linux网络编程里那个“老熟人”——socket的send()函数返回了错误,进程直接终止退出。很多人第一反应是“send返回-1我处理了啊”,但真正的问题往往不只是错误码,而是进程在错误码还没到手之前,先被SIGPIPE信号干掉了。这条链路的坑很深,涉及socket读写语义、内核信号机制、阻塞与非阻塞行为,我打算把这几年调这类问题攒下来的经验完整拆一遍,给做网络服务的同学一个可直接照抄的排查和修复方案。

这个内容适合所有用C/C++在Linux下写网络程序的人,尤其是搞即时通信、消息推送、游戏服务器、代理网关这类高并发长连接业务的。不管你是刚接手一段老代码,还是正在写新服务的socket封装,搞清楚send()与SIGPIPE的关系、错误码的语义、以及怎么安全封装发送函数,都能少踩很多不必要的坑。

1. 现象重现:send()出错怎么会把整个进程带走

1.1 从一场线上事故说起

之前帮某推送服务排查过一次故障。现象很诡异:服务运行几分钟到几个小时不等,进程就会消失,没有core dump,没有业务异常日志,systemd里显示的是异常退出。一开始怀疑内存问题,查了OOM记录没有;怀疑死锁,看线程栈也没发现;最后用gdb挂着跑,等它崩,这才定位到真正的退出点。

使用gdb -p <pid>挂上去,等服务崩掉,gdb会停下来提示收到了SIGPIPE。再看调用栈,栈顶函数就是send(),底层是__libc_send。也就是说,进程是在调用send()发数据时,被系统送来的SIGPIPE信号终止的。很多同学在这里就会疑惑:send()明明有返回值,出错时会返回-1,我们只要判断返回值不就行了?为什么进程还没等我们的代码判断,就已经没了?

因为SIGPIPE信号的默认动作就是终止进程,它根本不等你的代码反应过来。信号是内核在检测到异常发送条件时立刻投递并执行默认处理的,而send()返回错误码只是信号处理完之后,系统调用退出的副产品。进程都没了,后面if (n < 0)这段代码永远不会执行。

1.2 事故现场的典型特征

这类故障有几个很明显的特征,符合两条以上基本可以往SIGPIPE方向查:

  • 进程突然消失,业务日志里没有对应记录;
  • dmesg或journal里看不到OOM、段错误之类的痕迹;
  • 服务通常运行在长连接场景,有大量客户端主动断开或异常断网;
  • 用echo $?看退出码是141(等于128+13,13就是SIGPIPE的信号编号);
  • 如果用strace -p跟踪崩溃前的系统调用,能看到send相关调用后面跟着SIGPIPE事件。

很多团队遇到“进程莫名其妙退出”的第一反应是查内存、查锁,这没错,但如果所有方向都没问题,我建议第一时间想一下是不是信号杀的。Linux下被信号终止的程序,shell显示退出码141的时候,就在明明白白告诉你:进程死于SIGPIPE。

2. 内核机制拆解:SIGPIPE为什么和socket绑定在一起

2.1 从管道到socket:同一套“写已关闭管道”规则

SIGPIPE的设计初衷其实是管道。在shell里执行cat file | head -1,head读一行就退出,cat还在往管道写,内核发现管道读端已经关闭,就给写端进程发SIGPIPE,默认动作就是终止,从而避免产生大量无意义的数据写操作和无用进程。

socket机制在底层借用了一部分管道语义。一个TCP socket本质上也是有读端和写端的“双向管道”。当对端关闭连接(或者执行了shutdown关闭了某一半)之后,本地继续调用send()往这个socket上写数据,内核判断这相当于“写一个读端已关闭的管道”,于是触发SIGPIPE信号。

这个行为加上TCP的RST机制之后,表现会更复杂一些。比如客户端直接断开、机器突然断电、中间网络设备发来RST,本端下一次send()可能就会收到ECONNRESET或者触发SIGPIPE。虽然触发路径不一样,但结论一致:对端已经不读你的数据了,你还傻傻地继续发,内核就会动用SIGPIPE这个“强制刹车”来终止你。

2.2 send()错误码与信号出现的先后顺序

很多人以为send()返回EPIPE错误,然后我们在代码里判断errno,这是一个先有鸡还是先有蛋的问题,实际上信号在前,错误码在后。内核在调用send()时发现socket的写半部已经关闭,或者对端RST导致连接不可用,流程是这样的:

  1. 在copy数据到发送缓冲之前,检查socket状态;
  2. 发现写端对端已经关闭,触发SIGPIPE发送给当前进程;
  3. SIGPIPE的默认处理是终止进程,进程直接退出;
  4. 如果进程忽略或屏蔽了SIGPIPE,系统调用才会返回-1,errno置为EPIPE,代码才有机会处理。

所以,想要吃掉EPIPE错误码,前提是你得先处理SIGPIPE信号。忽略SIGPIPE之后,再看send()返回的EPIPE才有意义。这是整个问题里最关键的一个认知。

还有一点需要注意:不是每个错误码都会伴随SIGPIPE。比如缓冲区满了返回EAGAIN,不会触发SIGPIPE;连接还在但路由不可达返回EHOSTUNREACH,也不会。只有EPIPE这类“对端已经关闭写入通道”的情况才触发SIGPIPE。理解这个区别,能帮你避免盲目把信号和错误码混为一谈。

3. 线上最实用的三种处理方案:怎么把“自杀”关掉

3.1 方案一:全局忽略SIGPIPE,让send()回归普通函数

最直接、最常用的做法,在程序入口处把SIGPIPE忽略掉。对于服务端程序,我推荐这种做法,因为服务端一般有自己的连接池和断连重连机制,不需要系统来“帮忙”终止进程。

#include <signal.h> void init_net_env(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGPIPE, &sa, NULL) < 0) { /* 记录启动日志,理论上不会失败 */ } }

这样设置之后,所有线程的SIGPIPE都被忽略了(信号设置是进程级的),后续send()如果遇到对端关闭的情况,就会正常返回-1并置errno为EPIPE。之后代码里再增加对EPIPE、ECONNRESET的处理即可。

为什么不直接用signal(SIGPIPE, SIG_IGN)?因为signal()在不同系统中的语义有差异,尤其在信号处理函数执行期间是否自动屏蔽信号、系统调用是否自动重启这些细节上,历史版本实现不同。sigaction更可控,而且SA_RESTART能减少EINTR误报。不过设置SA_RESTART也有一个副作用:一部分被信号中断的系统调用会自动重启,而不是返回EINTR。放在这里,我们的目的就是让业务逻辑尽量不感知SIGPIPE的干扰,所以没问题。

3.2 方案二:MSG_NOSIGNAL标志,只在发送时闭嘴

如果不想改全局信号处理,只想在某个send调用点屏蔽SIGPIPE,使用MSG_NOSIGNAL:

ssize_t n = send(fd, buf, len, MSG_NOSIGNAL); if (n < 0) { if (errno == EPIPE) { /* 对端已关闭,做连接清理 */ } }

这是Linux专门提供的send标志位。加了这个标志,本次send调用即使触发写关闭管道,也不会向进程发送SIGPIPE,而是直接返回-1,errno是EPIPE。实际效果等同于“本次调用的局部忽略信号”。

这个方案的问题在于它只对当前这个send调用有效。如果你的代码里有多个发送点,或者调用了第三方静态库发送数据,第三方库依然可能踩中SIGPIPE。所以工程上把两者结合使用,全局忽略+S指定位置显式处理错误码,效果最稳。

3.3 方案三:SO_NOSIGPIPE与跨平台陷阱

存在另一个叫SO_NOSIGPIPE的socket选项,在macOS和部分BSD系统上可以用:

int on = 1; setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on));

它可以从socket级别屏蔽SIGPIPE,作用范围比MSG_NOSIGNAL更广,但这个选项在Linux上并不存在。在Linux上设置SO_NOSIGPIPE,setsockopt()会返回-1,errno是ENOPROTOOPT。写跨平台网络库的同学经常会在这上面踩坑,最好的写法是平台宏区分:

#ifdef __linux__ int n = send(fd, buf, len, MSG_NOSIGNAL); #elif defined(__APPLE__) int on = 1; setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); int n = send(fd, buf, len, 0); #endif

三种方案对比:

方案作用范围优点缺点适用场景
sigaction全局忽略进程级,所有socket一行配置全覆盖,兼容性好全局生效,需代码内正常处理EPIPE服务端程序、长连接服务
MSG_NOSIGNAL单次send调用精确控制,不影响全局容易遗漏,第三方库仍可能触发局部核心发送逻辑
SO_NOSIGPIPE单个socket(非Linux)精确到连接Linux不支持跨平台客户端、macOS环境

我个人的习惯是:服务端一律采用方案一加方案二组合;客户端库则优先方案二,并在入口也忽略SIGPIPE,防止第三方组件炸掉整个App进程。

4. 实战代码:写一个健壮的send()封装

4.1 核心封装:把错误码语义全部处理干净

解决了信号问题之后,真正考验功力的是错误处理。下面这个封装我用了很久,核心逻辑很清晰:处理部分发送、处理EINTR重试、区分致命错误与可重试错误,并统一返回逻辑,方便上层做连接回收。

#include <sys/socket.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <unistd.h> ssize_t safe_send(int fd, const void *buf, size_t len, int flags) { const char *p = (const char *)buf; size_t left = len; ssize_t total = 0; while (left > 0) { ssize_t n = send(fd, p, left, flags); if (n > 0) { p += n; left -= n; total += n; continue; } if (n == 0) { /* 阻塞模式下send几乎不会返回0,防御性处理 */ return total > 0 ? total : -1; } if (errno == EINTR) { /* 被信号打断,重试 */ continue; } if (errno == EAGAIN || errno == EWOULDBLOCK) { /* 非阻塞模式下缓冲区满,交给上层等待POLLOUT */ return total > 0 ? total : -1; } if (errno == EPIPE) { /* 对端关闭,通常走连接管理逻辑 */ return -1; } if (errno == ECONNRESET) { /* 对端RST,同样走连接管理逻辑 */ return -1; } if (errno == ENOTCONN) { /* socket未连接完成 */ return -1; } /* 保温抓:其余错误 */ return -1; } return total; }

为什么要在循环里处理EINTR?因为信号恢复之后,send()可能还没有写入任何数据就被中断。尤其当你用了SA_RESTART又同时处理其他信号时,EINTR虽然在自动重启机制下不常出现,但服务端信号多了之后,总会有一两个漏网之鱼。正确处理EINTR是稳定性的一部分。

为什么EAGAIN时要返回已发送字节而不是一直循环?这涉及阻塞粒度。阻塞模式下send()通常会阻塞到全部发完或出错,但设置了SO_SNDTIMEO后,超时也会返回EAGAIN。非阻塞模式下频繁遇到EAGAIN。如果在这里自旋重试,CPU会被白白吃掉。正确做法是返回,上层使用poll/epoll监听POLLOUT事件,等缓冲区有空间后再继续调用。

补充一点,如果调用方要求“必须全部发完”才返回,比如在阻塞socket上想死等,可以设置合理超时后,在EAGAIN分支写usleep或poll等待,但这种模式在高并发服务端不推荐,阻塞发送会拖垮线程。真正高并发的服务几乎都走epoll边沿触发加非阻塞socket,发送逻辑配合发送队列异步完成。

4.2 非阻塞模式下配套的超时与缓冲机制

写服务端的发送逻辑,光封装一个send函数远远不够。非阻塞socket配合SO_SNDTIMEO,可以在发送卡住时及时退出。下面的代码演示了怎么设置发送超时:

int snd_timeout_ms = 3000; struct timeval tv; tv.tv_sec = snd_timeout_ms / 1000; tv.tv_usec = (snd_timeout_ms % 1000) * 1000; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

设置了SO_SNDTIMEO之后,阻塞socket的send()在超时返回时会带上EAGAIN/EWOULDBLOCK,此时不完全是“缓冲区满”,也可能是发送超时。对于业务层来说,EAGAIN统一处理为“稍后再试”或“优先清除这个连接”,取决于你的设计。

配合epoll的时候,建议用LT模式管理写事件。当send()返回EAGAIN时,向epoll注册EPOLLOUT事件,等可写事件到了再取出发送队列中的数据继续发。千万别在EAGAIN后无限重试,那等同自旋。事件驱动模式下,发送队列长度也要有上限,否则慢客户端会把内存吃掉。常见做法是单连接发送队列超过比如16MB就强制断开,或者直接应用限流策略。

4.3 多线程服务里SIGPIPE特殊注意点

多线程程序里信号是进程级的,任何一个线程触发SIGPIPE,都是整个进程被终止。很多人以为在创建线程之后设置忽略就够了,实际上主线程的signal设置会影响整个进程所有线程。不过,如果有些线程库或动态库在加载时自行修改了信号处理,那就不好说了,所以尽量在main()最开头调用设置函数,越早越好。

还有一点容易踩坑:千万不要在信号处理函数里做复杂操作。如果你不是忽略SIGPIPE而是自定义了handler,不要在handler里调用printf、malloc、free这类非异步信号安全函数,否则可能死锁或数据损坏。最安全的做法就两件事:要么SIG_IGN,要么用sigaction设置一个只写全局volatile标志位后配合sigwait或self-pipe处理。对于SIGPIPE,我甚至不建议自定义handler,直接忽略是最省心的。

实际上绝大多数网络服务需要的不是“捕获SIGPIPE然后提示”,而是“彻底忽略SIGPIPE并依赖错误码”。加了自定义handler反而引入更多复杂度。

5. 问题排查实录与结果速查

5.1 用gdb和strace定位一遍完整过程

如果你遇到“进程莫名退出”而还没有处理好SIGPIPE,可以按下面这个顺序快速坐实问题。

先用gdb挂住进程,等待崩溃:

gdb -p <pid>

崩溃后执行:

bt info signals SIGPIPE

如果gdb提示收到SIGPIPE,并且bt栈顶是send/sendto相关函数,基本就是坐实了。进一步用info signals看SIGPIPE配置,默认是Stop(终止),说明没有代码屏蔽它。

用strace跟踪退出逻辑更直观:

strace -f -e trace=sendto,send,write -p <pid>

等到退出时,会看到类似这样的输出:

sendto(3, "...", 1024, 0, NULL, 0) = -1 EPIPE (Broken pipe) --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER} --- +++ killed by SIGPIPE +++

看到killed by SIGPIPE这一行,问题就完全清楚了。这里有个细节:输出里有时是先显示EPIPE再显示SIGPIPE,但在代码层面,进程是先收到信号被杀死的;strace只是把系统调用返回和信号事件记录在同一行附近,不要被顺序迷惑。

如果是事后排查没有gdb挂载,可以检查core文件。前提是系统打开了core dump:

ulimit -c unlimited

然后用gdb 程序 core查看信号来源。如果当前环境没有core,也可以从退出码反推。shell脚本里执行程序,echo $?返回141,就基本确定是SIGPIPE(128 + 13)。

5.2 send()常见错误码与处理对照

错误码常见触发场景处理建议
EPIPE对端关闭连接或半关闭后继续写关闭本端连接,从连接池剔除,重连并重试(如果业务允许)
ECONNRESET对端RST,连接被重置同上,通常不需要重发,因为数据可能已被对端看到
EAGAIN / EWOULDBLOCK非阻塞发送缓冲区满,或SO_SNDTIMEO超时等待POLLOUT事件,或关闭慢连接
EINTRsend被信号中断立即重试send
ENOTCONNsocket没有完成连接检查握手流程,确认connect完成或需要重新connect
EBADFfd无效或已被关闭排查fd的并发关闭与重复释放问题
EMSGSIZE发送报文超过系统限制(常见于UDP)拆分数据报或检查socket缓冲区设置
EHOSTUNREACH路由不可达,常见于UDP和长时间无活动的TCP重试或向监控告警
ETIMEDOUT发送超时需要结合业务超时策略处理,不该无限等待

这么多错误码看着多,实际归纳起来就三类:一类是“对端已经不想收”,包括EPIPE、ECONNRESET、ENOTCONN;一类是“现在发不出去,但以后可能可以”,包括EAGAIN、EINTR;剩下的属于“资源或参数错了”,比如EBADF、EMSGSIZE。不同类别,处理策略完全不同,第一类直接断连,第二类重试或等待,第三类修代码。

5.3 避坑清单:这些坑我基本都踩过

先别急着上生产,有五个容易忽略的地方值得单独记:

第一,只判断返回值不够,很多团队在代码里写了if (n == -1) break;,但没屏蔽SIGPIPE,进程还是活不下来。顺序是:先忽略SIGPIPE,再处理返回值。忽略SIGPIPE不是可选项,而是基础前提。

第二,send()返回成功也不代表对端真的收到了数据。它只代表数据已经拷贝到内核发送缓冲区。对端是否收到,需要业务层ACK或者超时机制来确认。如果服务只依赖send返回值做可靠性判断,设计上就有问题。

第三,TLS/SSL层也有类似问题。使用OpenSSL的SSL_write()时,底层可能调用send()或者write(),如果底层socket没有屏蔽SIGPIPE,SSL通信同样可能把整个进程带走。用SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY)可以少处理一些内部重试,但底层信号问题必须先解决。

第四,自定义SIGPIPE handler时要格外小心。handler里调用非异步安全函数,轻则数据错乱,重则直接死锁。我见过有人想在SIGPIPE里打日志做统计,结果进程因为日志锁死锁,更难查。SIG_IGN是最简单的答案。

第五,关闭socket时的SO_LINGER设置会影响发EPIPE还是ECONNRESET。比如设置了linger超时且超时时间内数据发不完,close会继续发RST。这种场景不会直接触发SIGPIPE,但会在对端引发ECONNRESET,形成相互踩坑的循环。排查时要双向看。

6. 一次完整案例复盘:底层连接池的健康检查

6.1 场景描述

某模拟项目X是一个长连接代理服务,自己维护到后端的一组TCP连接。某个后端节点重启后,代理还维持着旧连接,等到业务请求到来时,代理往这条已经死掉的连接上写数据,进程就消失了。表面看是“socket通信导致进程挂掉”,深层原因其实是两层:一是没有及时识别死连接;二是send()遇错时被SIGPIPE带走了进程。

这种“死连接”非常隐蔽。TCP没有时刻在线的心跳,断电、拔网线、虚拟机漂移都不会让本地立即感知。等到你往这条连接上写数据时,内核才发现对端不可达,要么返回超时,要么直接收到RST。

复现过程我总结成一个固定模板:

  1. 本地建立到后端的TCP长连接;
  2. 后端节点直接kill掉进程(模拟对端断电);
  3. 等端口释放后,代理继续用旧连接send;
  4. 观察代理进程退出,gdb确认死于SIGPIPE。

6.2 排查和修复的完整动作链

第一步,用strace抓现场,确认是EPIPE加SIGPIPE组合。第二步,在代码入口加上sigaction忽略SIGPIPE。第三步,重跑压测,这次进程不再退出,但日志里开始出现大量EPIPE报错。这一步是必经之路:之前EPIPE没机会打印,被SIGPIPE秒杀了。加上屏蔽后,错误浮出水面,你才能真正做业务层的健康检查与重连。

第四步,对连接池增加主动健康检查。最简单的方案:每次从连接池取连接时,先做一次非阻塞的0字节send或者recv(fd, buf, 0, MSG_PEEK|MSG_DONTWAIT)探测。0字节send在连接正常时返回0,在连接关闭时返回-1并置EPIPE/ECONNRESET。但这里要小心:0字节send的检测不是绝对可靠,它依赖内核已经发现连接异常。更可靠的是应用层心跳,定时写一个Ping包并期待Pong响应,超时则剔除连接。

第五步,针对可能阻塞的发送,在socket上设置收发超时,同时把fd设为非阻塞,配合epoll管理写事件。这样即使后端节点再次异常,最坏结果也只是某条连接EAGAIN超时被回收,而不是进程崩溃。

6.3 这套方案的最终形态

修复后的连接管理逻辑是清晰的三层:

第一层,进程级安全网。入口处sigaction忽略SIGPIPE,让所有错误都能暴露为errno,不再直接杀死进程。主力逻辑里send统一走safe_send封装,EINTR重试,EAGAIN交还事件循环,EPIPE和ECONNRESET直接淘汰连接。

第二层,超时保护。设置SO_SNDTIMEO为2到3秒,连接建立后如果长时间无IO,定期发送应用层心跳。心跳连续两次超时就把连接关闭并触发重连。这个心跳间隔需要配合对端负载,别太频繁,否则自己刷爆连接。

第三层,连接池回收。取连接时校验健康状态,归还时标记最后使用时间。每次使用完成后扫描所有空闲连接,超过空闲阈值的主动关闭。这样即使某个后端节点挂掉,最迟一个心跳周期内就能把坏连接剔除干净。

最后分享一个小经验:很多网络问题看起来“玄学”,实际上是层层叠加的。SIGPIPE把EPIPE的痕迹抹掉,EPIPE又把死连接的问题掩盖住,死连接又让上层业务看到“偶发超时”。排查的时候,先把进程存活问题解决,再处理连接问题,最后优化业务层重试。次序对了,大部分诡异故障都能被捋顺。我后来写新项目,只要涉及socket,第一行就是忽略SIGPIPE,这已经成为肌肉记忆了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 9:15:39

open-code-review:用自动化流程让Code Review不再流于形式

1. 整体思路与核心矛盾&#xff1a;为什么多数 Code Review 都死在形式上先说说我为什么要折腾 open-code-review 这套东西。过去几年我带过不少研发小组&#xff0c;也见过太多团队上一秒刚宣布“从今天开始我们严格执行 Code Review”&#xff0c;下一秒 MR 就变成“LGTM&…

作者头像 李华
网站建设 2026/10/19 9:14:40

研发流程中的识别、评估与行动:实践一套可复用的工作流

1. 项目概述1.1 "rea"到底是什么&#xff1f;一个能够贯穿研发流程的代号有个老同事问我最近在忙什么&#xff0c;我跟他提了一嘴"我正在整理一个内部代号叫 rea 的工程实践"。他第一反应是&#xff1a;你说的是 React 生态里的某个东西&#xff1f;还是re…

作者头像 李华
网站建设 2026/10/19 9:14:01

RAG知识库实战指南:从架构原理到效果优化,解决大模型幻觉问题

做RAG的人应该都有过这种经历&#xff1a;你辛辛苦苦把一堆业务文档切好、向量化、灌进知识库&#xff0c;结果模型面对一个简单问题&#xff0c;依然答得“满脸自信但全是胡说”。这种挫败感我太熟了。RAG&#xff0c;也就是检索增强生成&#xff0c;本质上就是解决这个问题的…

作者头像 李华
网站建设 2026/10/19 9:12:17

深度网络自适应:五种方法攻克模型跨域应用难题

做模型落地的人大多有过这种体验&#xff1a;模型在实验室测试集上成绩漂亮&#xff0c;推到现场就开始翻车。工业质检、医学影像、用户行为预估……数据分布一变化&#xff0c;精度掉十个点都不稀奇。这个问题的学术名称叫领域漂移&#xff0c;解决思路里最实用的一支就是深度…

作者头像 李华
网站建设 2026/10/19 9:12:11

AI辅助文献综述高效写作全流程指南

每年到毕业季&#xff0c;我微信里问得最多的问题永远是&#xff1a;“文献综述到底怎么写&#xff1f;”这问题一点都不新鲜&#xff0c;但最近两年答案悄悄变了——以前学生问的是“怎么找文献”“怎么组织内容”&#xff0c;现在问的是“能不能用AI写”“AI写的能不能交”。…

作者头像 李华