不让 SIGPIPE signal 太嚣张

起因

由于项目迭代比较快, 大家还没有来得及做自我调整和总结, 就需要投入到新版本当中开发新功能了.

在最近的一次版本测试和体验过程中, 很多内测用户反馈进入或者退出 App 的聊天室, 会莫名其妙的崩溃掉(Crash).
对于 Crash 的问题, 我们开发同事绝对是零容忍, 于是就开始跟踪问题.

跟踪了很久, 发现这个 Crash 并不是那么的 ‘乖巧’, 很难复现!
既然用户已经反馈了并且后台也有 Crash 上报, 这个问题肯定存在, 所以我们不能放弃.

好吧, 继续加班搞…苦逼中…

最终, 我们发现一个规律, 在日志后台, 看到很多类似下面的日志:

1
Signal 13 was raised. SIGPIPE (_mh_execute_header + 420728)

很遗憾的是, 堆栈信息中没有提供给我们更有力的证据, 所以当时定位在 Signal 13 这个点上面.

也算是有了突破…这班没有白加…

排查问题

既然所有的罪证都指向了 Signal 13, 我们就需要去跟踪它, 去调查它, 去研究它.

signal.h 文件中, 可以发现其定义如下:

1
2
/* write on a pipe with no one to read it */
#define SIGPIPE 13

用通俗的话来讲, 就是管道破裂.

管道破裂,这个信号通常在进程间通信产生,比如采用 FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到 SIGPIPE 信号.
此外用 Socket 通信的两个进程,写进程在写 Socket 的时候,读进程已经终止.
另外, 在 send/write 时会引起管道破裂,关闭 Socket, 管道时也会出现管道破裂.
使用 Socket 一般都会收到这个 SIGPIPE 信号.

也就是说, 该信号是跟 Socket 的连接以及数据的读写相关联的.

这样的话,我们就知道为什么进退房间导致 Crash 了,我们的进退房间都和 Socket 有关联,这种 Crash 大都数是在用户网络不好的情况下发生的.

解决问题

类似 signal 13 这种错误是系统发出来的, 和内存使用异常和野指针一样,由于是系统级别崩溃,所以不能通过

1
2
3
4
5
6
@try {
}
@catch(NSException *exception) {
}

捕获到这类异常.

因此, try catch 是无法解决问题的.

目前有两个方案可用:

方案1. 忽略这类信号.

方案2. 修改源码, 在 IM 代码里面修改.

因为, IMSDK 我们是使用第三方的, 所以无法更改其源码, 所以采取了方案1: 忽略这类信号.

忽略的方案很简单, 在你连接或者初始化 IMSDK 之前, 只需要一行代码:

1
signal(SIGPIPE, SIG_IGN);

示例代码:

1
2
3
4
5
6
7
8
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// ...
signal(SIGPIPE, SIG_IGN);
// ...
}

对于 方案2, 我查阅了一下 CocoaAsyncSocket 的源码:

1
2
3
4
// Prevent SIGPIPE signals
int nosigpipe = 1;
setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe));

所以对于 方案2, 就是增加上面的代码即可, 即设置 Socket 不发送 SIGPIPE 信号.

对于上面的两种方案, 苹果开发者文档 Avoiding Common Networking Mistakes 都有提及.

-w380

问题复现

既然这个问题很难复现, 我们就想办法让他很容易复现.

可以采用手动发送 signal 的方式, 来复现这个问题.

先看一下 kill(3) - Linux man page 函数:

The kill() function shall send a signal to a process or a group of processes specified by pid. The signal to be sent is specified by sig and is either one from the list given in or 0. If sig is 0 (the null signal), error checking is performed but no signal is actually sent. The null signal can be used to check the validity of pid.

kill 函数是可移植操作系统接口 POSIX(Portable Operating System Interface of UNIX) 定义的, 可以参考 维基百科.

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)viewDidLoad
{
[super viewDidLoad];
//获取进程 id
pid_t cur_pid = getpid();
printf("current process's id: %i\n", cur_pid);
//延时10s 为了让 Bugtags 有时间上报日志.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
wpcSendSignal(cur_pid);
});
}
///手动发送信号
int wpcSendSignal(int pid)
{
int ret = kill(pid, SIGPIPE);
return ret;
}

我们将 signal(SIGPIPE, SIG_IGN) 代码先注释.

运行 APP 到手机, 然后退出 Xcode 的连接.

注意: 这里如果连接 Xcode, 不会直接 Crash, 所以需要断开手机与 Xcode 的连接.

在后台可以看到类似的 Crash 日志:

-w500

除了上面使用 kill 函数外, 我们还可以使用 raise 函数来发送 signal.

关于 raise 函数, 可以查阅 手册.

或者直接问男人(man):

1
man raise

问题再次来临: 与 Bugtags 的结合

本以为采用上述方案就万事大吉了, 在第二次提测后, 还是有这种 Crash 的问题在后台上报.

排查了很久后才发现,问题的原因是 BugTags 也会控制这个开头,默认是不忽略,这样:

1
2
3
4
/**
* 是否忽略 PIPE Signal (SIGPIPE) 闪退,默认 NO
*/
@property(nonatomic, assign) BOOL ignorePIPESignalCrash;

这个默认设置为 NO, 即可以上报 PIPE Signal Crash 的问题.

这里也说明一个问题, 即使我们采用 方案1 解决 Crash 的问题了, Bugtags 还是会将这种 Crash 上报到后台.

示例代码:

1
2
3
4
5
bugtag.option.ignorePIPESignalCrash = YES;
// ...
signal(SIGPIPE, SIG_IGN);

对比一下测试的两张图, 第一张图到第二张图是增加了两次崩溃次数, 原因是故意设置了 bugtag.option.ignorePIPESignalCrash = NO, 也正好验证了我们的想法.

-w600
-w600

总结

  1. Xcode 连接真机或者模拟器, 运行出现异常断点, 可能就是隐患点.

  2. 学会使用后台日志找到规律, 继而去思考并解决问题.

  3. 对 Crash 进行更深入的分析和总结, 不要轻易放弃.

后续研究

  1. 是否可以忽略其他的 signal, 来避免不必要的 Crash?

  2. 自定义一套关于 signal 捕获的跨平台库, 在开发阶段可以直接看到完整的日志.

推荐

  1. Avoiding Common Networking Mistakes

  2. linux die

  3. Using Sockets and Socket Streams


扫码关注,你我就各多一个朋友~

坚持原创技术分享!