客户端基础知识——关于 signal 不可不知的工程细节

656 阅读4分钟

一、信号(signal)机制

在客户端开发中,我们有时会用到信号(signal)机制。

信号是操作系统提供的一种用于通知应用程序某些特殊事情发生了的通知机制。signal man page[1]中列出了一系列不同的信号,常见的有:

  • SIGSEGV:著名的段错误,一般是访问了非法内存
  • SIGKILL:杀死进程
  • SIGABRT:一般程序运行出错会触发这个信号,如调用NSAssert(NO)
  • SIGQUIT:做苹果第三方输入法的同学应该对这个信号比较熟悉

应用程序在收到大多数类型的信号时,其默认行为都是直接退出(崩溃)。

少部分信号的默认处理行为是忽略(SIGCHLD)或其他逻辑(SIGSTOPSIGCOND等)。

开发者可以注册一个自定义的信号处理函数来定制信号处理逻辑,也可以直接设置忽略某种信号。

:一般崩溃收集模块会在应用程序运行时发生错误收到系统发来的SIGABRT信号时统计相关信息、记录崩溃栈等。

设置信号处理函数(或设置忽略信号)主要是signal()sigaction()这两个系统API。其具体用法网上资料较多,这里就不展开了。

signal()方法用起来简单,但是官方已经将它废弃(deprected),建议使用sigaction()

二、使用信号的一些常见问题

下面以问答的形式,解释几个有趣(且有用)的知识点:

1. 当信号发生时,注册的信号处理函数会在哪个线程执行呢?

发来的信号分两种:

  • 发给某个PID进程的信号(如:使用kill()方法时需要指定PID

    • 进程中的每个线程都可以独立地调用pthread_sigmask()方法来设置本线程block哪些信号

      注:block的信号会进入一个线程(或进程)维护的队列,然后在后续线程unblock此信号时得到处理。

    • 信号会被发到该进程中那些没有block该信号的线程中的某一个(具体是哪一个不确定)

    • 如果所有线程都block了该信号,该信号会进入一个由进程维护的队列中

    • 如果没有为该信号定义处理函数,则进程退出(崩溃)

  • 发给特定线程的(如某线程调用pthread_kill()方法,参数为另一线程的线程ID

    • 取决于该线程是否block此信号,信号会被排入其等待队列或者被其处理

    • 如果没有指定信号处理函数,整个进程会崩溃

2. 信号被派发到某个线程时,该线程之前正在运行的方法会中断还是继续?停止还是返回?

  • 很多(特别是比较耗时的)系统调用(system call)和标准库函数都是可以被信号中断的,如read()write()sleep()等。

  • 被中断的系统调用有的会返回错误信息EINTR,有的会返回该API特有的返回值(如read()可能会返回剩余多少数据未读,具体请查阅相关API文档),也有的不会返回,而是在信号处理函数执行完成后继续运行。应用程序需要处理好不正常返回情况下的返回值。

  • 系统调用被信号中断后,当前线程会转而运行信号处理函数,运行完成后,再接着运行之前的程序逻辑。

3. 信号处理函数里能调用的方法有限制吗?比如:能调用malloc吗?

  • 如果你跟笔者一样,曾经在信号处理函数里加了一两行Objective-C代码,然后上线后发现线上一堆奇怪的崩在malloc里的崩溃,你就知道信号处理函数里的确不是什么方法都能调的。

  • 其实,Linux中专门定义了一个信号安全(signal-safety[2])的概念,在其文档中列出了所有符合信号安全的系统APImalloc不在其列)。也就是说,在信号处理函数中,只有调用这个列表中的方法才是安全的,调用其他方法都可能会崩溃。

如果要调用第三方提供的方法,需要征询库开发人员该方法是否信号安全。

  • 如果自定义信号处理函数中使用了全局变量,则其本身也需要保证是可重入的

可重入是指在单cpu机器上用两个线程同时调用此函数,其调用结果与两线程先后调用两次此函数一致(哪个先调都可以,只要跟其中一个一致即可)。

4. 信号处理过程中又收到了别的信号怎么办?

  • 不同类型的信号会排队,哪个先触发是不确定的

  • 同一类型的信号,即使发送方发送了多次,操作系统也会保证接收进程/线程也只收到一个,不会出现多个同类型信号排队的情况

参考资料

最后,欢迎大家关注我的微信公众号(码工笔记),有空多多交流