Linux 编程之信号篇:异常监控必知必会

为什么要了解信号

信号是 UNIX 系统进程管理非常重要的一环,下面这些场景都需要通过信号实现:

  1. 进程接收内核的通知(比如内核通知进程 用户输入了信息)
  2. 系统终止一个进程
  3. 管理父子进程(比如通知父进程子进程退出了)
  4. 进程间通信

信号在 Android 系统中的地位也非常重要。通过了解信号,我们可以实现对应用运行状态的监听,最实际的用途,就是监听应用发生崩溃、ANR 并上报。

因此,我们有必要掌握信号的使用和基本原理,从而对关注的信号进行处理。

什么是信号

信号是一种软中断, 是一种通知方式。

当收到内核或者其他进程发送的信号后,接收信号会从当前执行的代码转移到之前注册的信号处理函数(如果注册了的话),当信号处理函数执行完成后,恢复执行之前的代码(如果没有退出的话)。

上一句话括号里之所以有“如果注册了的话”,是因为在接收到信号后,进程可以有多种选择:

  1. 忽略
  2. 使用默认的处理函数
  3. 自己注册一个信号处理函数

当进程通过 syscall 指定要忽略某些信号后,再接收到这些信号,会直接无视,继续执行;如果是没有忽略的信号,就会执行默认或者注册的处理函数。

另外,我们也可以通过信号传递一些数据,这一般用于进程间通信。

信号有哪些

在 UNIX 系统中,信号有大概 31 个,具体定义在 asm/signal.h 中(使用 adb shell kill -l 也能看到这些信号),这里我们仅看下常见的几种:

  • SIGKILL(9)/SIGSTOP(19):被系统强制杀死/停止**(不能被捕获或者忽略)**
  • SIGQUIT(3): Android 进程 ANR 时会收到这个信号
  • SIGABRT(6): 主动 abort() 后触发,
  • SIGALRM(14):主动 alarm() 后触发
  • SIGSEGV(11):段错误,访问无效内存或者无权访问内存
  • SIGBUS(7):硬件或对齐错误
  • SIGFPE(8):算数异常,包括溢出、除以 0 等
  • SIGCHLD(17):子进程被终止时,内核给父进程发送
  • SIPROF(27):向没有读取端的管道写入,一般用作调试

可以看到,信号的名称都是 SIGxxx,另外每个信号也有唯一的整数值,在我们注册相关监听时,都是使用整数,名称只是为了直观理解其类型。

还有一个值为 0 的空信号,这个仅用于测试是否有权限发送信号。

如何发送/捕获信号

老 API:

  • kill()
  • signal()

推荐 API:

  • sigqueue()
  • sigaction()

推荐理由:可以发送的信息更多

发送

kill

我们可以通过 adb 使用 kill: adb shell kill -9 pid

在代码中使用 kill 的话,也很简单,它的函数签名:

int kill(pid_t __pid, int __signal);
  • pid > 0,会给该进程发送信号
  • pid = 0,会给调用进程的【进程组中的每个进程】发送信号
  • pid = -1,会向调用进程发送信号
  • pid < -1,会给 pid 绝对值的进程组发送信号

返回值:0 成功;-1 失败。

  • killpg
  • raise

raise

raise 用于给自己发信号,就是简化版的 kill:

int raise(int __signal);
//等于 kill(getpid(), signo);

返回值:0 成功;其他值失败。

killpg

给一个进程组发送信号:

int killpg(int __pgrp, int __signal);

等同于 kill(-1* pgrp, signal)

需要注意的是,发送信号,需要权限,用户只能给 UID 相同的进程发送信号,除非是 ROOT 设备。

sigqueue

sigqueue 可以发送待附加信息的信号,方法签名如下:

int sigqueue(pid_t __pid, int __signal, const union sigval __value)

typedef union sigval {
  int sival_int;
  void __user * sival_ptr;
} sigval_t;

sigqueue 只能给指定的进程发送信号,不能发给一个进程组。

使用案例:

    char* msg = static_cast<char *>(calloc(12, sizeof(char)));
    msg = "zhangshixin";

    union sigval value{};
    value.sival_ptr = msg;
    int ret = sigqueue(getpid(), signo, value);

    LOG(" send signal by sigqueue, signo:%d , ret: %d, msg: %s", signo, ret, msg);

监听

signal

signal 函数比较简单,第一个参数是要监听的信号,第二个参数是一个信号处理函数:

#if __ANDROID_API__ >= __ANDROID_API_L__
sighandler_t signal(int __signal, sighandler_t __handler)

通过宏可以看出,signal 只支持 5.0 级以上版本的设备。

写一个例子:

//信号处理函数必须返回 void
void my_signal_handler(int signo) {
    LOG("my_signal_handler called, sig_no: %d", signo);
}

void my_signal_handler_2(int signo) {
    LOG("my_signal_handler_2 called, sig_no: %d, signame: %s, name by sys_siglist: %s",
            signo, strsignal(signo), sys_siglist[signo]);
}

void old_signal_listen_test() {
    //监听 SIGPROF,返回该信号之前注册的信号处理函数
    auto ret = signal(SIGPROF, my_signal_handler);
    ret = signal(SIGPROF, my_signal_handler_2);
    LOG("register signal handler ret: %p, my_signal_handler: %p", ret, my_signal_handler);

    if (ret == SIG_ERR) {
        fprintf(stderr, "register signal handler failed!");
        return;
    }

    //忽略
    signal(SIGFPE, SIG_IGN);


    //只在接收到可捕获的信号时才返回,返回值返回 -1
    int pause_ret = pause();
    LOG("pause returned, ret: %d", pause_ret);

    //使用默认处理
    ret = signal(SIGPROF, SIG_DFL);
    LOG("set SIGPROF use default handler, ret: %p", ret);
}

上面的例子中,我们做了三件事:

  1. 注册了两次信号处理函数,然后打印出第二次注册时,返回值的地址
  2. 忽略了 SIGFPE 信号
  3. 调用 pause,阻塞当前代码
  4. 在收到信号后,设置 SIGPROF 信号继续使用默认处理方式

通过运行结果可以看到,的确是返回的第一次注册的信号处理函数的地址:

zsx_linux: register signal handler ret: 0x788d755264, my_signal_handler: 0x788d755264

注册成功后,我们可以在 adb shell 中发送一个 SIGPROF 信号进行测试(需要 ROOT 手机):

admin@C02ZL010LVCK debug % adb shell
ursa:/ $ top | grep performance
25263 shell        20   0 8.8M 2.2M 1.8M S  0.0   0.0   0:00.02 grep performance
25236 u0_a124      10 -10 4.3G 122M  73M S  0.0   1.5   0:00.57 top.shixinzhang.performance
ursa:/ $ su
:/ # kill
kill
usage:	kill [-s signame | -signum | -signame] { job | pid | pgrp } ...
	kill -l [exit_status ...]
1|:/ # kill -27 25236
kill -27 25236

kill 命令的格式:kill -signum pid

发送后,我们在信号处理函数里的日志就如期打印了:

zsx_linux: my_signal_handler_2 called, sig_no: 27, signame: Profiling timer expired, name by sys_siglist: Profiling timer expired
zsx_linux: pause returned, ret: -1
zsx_linux: set SIGPROF use default handler, ret: 0x788d756360

上面的测试代码还有 2 个细节:

  1. pause() 是 Linux 提供的测试信号 API,调用它会立刻阻塞,在接收到可捕获的信号时才返回
  2. strsignal(signo)sys_siglist[signo] 可以获取信号的名称,一般推荐用后者

需要注意的是,不是所有信号都可以捕获并处理的,比如注册 SIGKILL SIGSTOP 就直接会返回 SIG_ERR;注册 SIGUSR1 虽然 OK,但发送这些信号,也不会执行我们注册的信号处理函数 (可能被阻塞了)。

sigaction

https://man7.org/linux/man-pages/man2/sigaction.2.html

sigaction 比 signal 更为强大,它可以获取到信号发送者的信息和进程状态。

#include <signal.h>

int sigaction(int signum, const struct sigaction *restrict act,
                     struct sigaction *restrict oldact);

第一个参数是要注册的信号,第二个参数要信号处理相关的结构体,第三个参数是之前设置的信号处理行为。

sigaction 结构体内容如下:

struct sigaction {
  union {
    sighandler_t sa_handler;	//和 signal 一样的信号处理函数
    void (*sa_sigaction)(int, struct siginfo*, void*);		//可以获取到更详细信息的信号处理函数
  };
  sigset_t sa_mask;
  int sa_flags;
  void (*sa_restorer)(void);
};

sa_flags 支持这些参数:

能额外获取到的信息:

__SIGINFO struct { int si_signo; int si_errno; int si_code; union __sifields _sifields; }
union __sifields {
  struct {
    __kernel_pid_t _pid;
    __kernel_uid32_t _uid;
  } _kill;
  struct {
    __kernel_pid_t _pid;
    __kernel_uid32_t _uid;
    sigval_t _sigval;
  } _rt;
  //...
};

__sifields 内容非常多,这里我做了精简,重点关注信号发送者的信息、信号产生的原因和传递的数据。

使用例子:

void my_sa_sigaction(int signo, struct siginfo* info, void* context) {
    LOG("my_sa_sigaction called ! signo: %d", signo);

    auto _signo = info->si_signo;
    //错误原因
    auto sig_code = info->si_code;

    auto killer_pid = info->_sifields._kill._pid;
    auto killer_uid = info->_sifields._kill._uid;

    auto syscall = info->_sifields._sigsys._syscall;

    LOG("_signo: %d", _signo);
    LOG("sig_code: %d, send by sigqueue? %d", sig_code, sig_code == SI_QUEUE);
    LOG("killer_pid: %d , killer_uid: %d", killer_pid, killer_uid);
    LOG("syscall: %d", syscall);

    auto value = context;
    LOG("value: %s", value);

    //获取发送的数据
    sigval_t sig_value = info->_sifields._rt._sigval;
    LOG("sig_value %s", sig_value.sival_ptr);

}

//比 signal 更为强大,可以获取的信息更多
void sigaction_test() {
    struct sigaction sig_action{};
    //设置信号的处理方式,SA_SIGINFO 表示使用信息更多的信号处理函数
    sig_action.sa_flags = SA_SIGINFO;

    //设置执行信号处理函数时,要阻塞的信号集(避免重入)
    //目前正在被处理的信号,也是被阻塞的
//    sig_action.sa_mask = SIGPROF;

    sig_action.sa_sigaction = my_sa_sigaction;

    //成功时返回 0
    int ret = sigaction(SIGPROF, &sig_action, nullptr);
    if (ret) {
        LOG("sigaction set failed! ");
    }
    LOG("set sigaction ret: %d", ret);
}

使用 sigqueue 发送信号后,输出结果

zsx_linux: my_sa_sigaction called ! signo: 27
zsx_linux: _signo: 27
zsx_linux: sig_code: -1, send by sigqueue? 1
zsx_linux: killer_pid: 2919 , killer_uid: 10124
zsx_linux: sig_value zhangshixin

信号处理函数的注意事项

实现信号处理函数,有 2 点需要注意:

  1. 最好不要访问、修改全局数据
  2. 不调用不可重入函数

第一点建议的原因:由于信号处理函数是异步调用,可能在程序执行数据读写的过程中被切换到信号处理函数,如果在信号处理函数中访问、修改这部分数据,会导致结果不可预期。

第二点中,可重入的函数就是说多次调用不会有异常,建议在信号处理函数中仅使用可重入的函数。

Linux 信号可重入的安全函数列表见 https://man7.org/linux/man-pages/man7/signal-safety.7.html

信号阻塞

有些情况下,在用户程序或者信号处理函数中,可能需要对挂起某些信号,即使收到这些信号,也暂不处理,继续执行任务,等执行完当前任务,再开始接收信号。

Linux 提供了这种机制,称为信号的阻塞(Block)和解除阻塞(UnBlock)。

Linux 中管保存当前进程阻塞的信号数据,叫做“信号掩码”,signal mask。

获取、修改信号掩码的 API 是 sigprocmask:

int sigprocmask(int __how, const sigset_t* __new_set, sigset_t* __old_set);

https://man7.org/linux/man-pages/man2/sigprocmask.2.html

我们可以通过构造一个信号集(即多个信号),然后调用 sigprocmask

  1. 如果传入的参数是 SIG_BLOCK,就是把信号集里的信号,也都阻塞了(其余之前设置的不变)
  2. 如果传入的参数是 SIG_UNBLOCK,就是把信号集里的信号,解除阻塞(其余之前设置的不变)
  3. 如果传入的参数是 SIG_SETMASK,就是粗暴地把进程的阻塞的信号,设置成当前的信号集合(之前设置的不要了)

可以通过传入一个空的 set,获取 old_set,然后判断当前进程是否阻塞某个信号:

void process_block_signal_mask_test() {
    //信号集
    sigset_t set, fill_set, old_set;
    //1.初始化空信号集,把所有信号都排除
    int ret = sigemptyset(&set);
    LOG("sigemptyset ret: %d", ret);

    //把所有信号都包括在内
    sigfillset(&fill_set);

    //添加 SIGQUIT 到信号集
//    ret = sigaddset(&set, SIGQUIT);
//    LOG("sigaddset ret: %d", ret);

    ret = sigismember(&set, SIGQUIT);
    LOG("check sigismember, sig: %d ret: %d", SIGQUIT, ret);;
    ret = sigismember(&fill_set, SIGQUIT);
    LOG("check [fill_set] sigismember, sig: %d ret: %d", SIGQUIT, ret);;

    //2.可以传入空 set,只为看当前的信号掩码有哪些
    ret = sigprocmask(SIG_BLOCK, &set, &old_set);
    //检查是否默认阻塞 SIGQUIT,答案是是的
    LOG("check process mask, SIGQUIT ismember: %d", sigismember(&old_set, SIGQUIT));

    sigset_t pending_set;
    //3.获取当前进程待处理的信号
    if (sigpending(&pending_set)) {
        LOG("sigpending failed");
    }
    LOG("sigpending ret: %d, pending_set: %ld", ret, pending_set);

    //4.恢复旧的信号掩码,这个函数和 pause 一样会阻塞,直到收到信号
    ret = sigsuspend(&old_set);
    LOG("sigsuspend ret: %d, old_set: %ld", ret, old_set);
}

输出结果:

zsx_linux: sigemptyset ret: 0
//空集合,不包含 SIGQUIT
zsx_linux: check sigismember, sig: 3 ret: 0
//满集合,包含 SIGQUIT
zsx_linux: check [fill_set] sigismember, sig: 3 ret: 1
//进程初始化时,把 SIGQUIT 信号屏蔽了
zsx_linux: check process mask, SIGQUIT ismember: 1

上面我们看到,在我的 demo app 里,默认就把 SIGQUIT 屏蔽了,具体实现代码在 Runtime::init 里,会调用到 Runtime::BlockSignals:

void Runtime::BlockSignals() {
  SignalSet signals;
  signals.Add(SIGPIPE);
  // SIGQUIT is used to dump the runtime's state (including stack traces).
  signals.Add(SIGQUIT);
  // SIGUSR1 is used to initiate a GC.
  signals.Add(SIGUSR1);
  signals.Block();
}


void Block() {
  if (pthread_sigmask64(SIG_BLOCK, &set_, nullptr) != 0) {
    PLOG(FATAL) << "pthread_sigmask failed";
  }
}

可以看到,Runtime::BlockSignals 会调用 pthread_sigmask64 设置这三个信号阻塞,这里也解答了为什么前面我们注册了 SIGUSR1 信号但在测试时没有收到该信号。

pthread_sigmask 和 sigprocmask 基本一样,但 POSIX.1 标准里没有定义 sigprocmask 在多线程环境下的行为,所以可能在不同的系统上实现有些差异。
一般建议使用 pthread_sigmask

注意:前面提到无法注册的 SIGKILL 和 SIGSTOP,同样无法被阻塞。系统强制执行,无法修改。

我们可以通过 sigpending 获取当前进程被阻塞了、待处理的信号列表:

int sigpending(sigset_t* __set);

总结

通过本文,我们可以对 Linux 的信号机制有个基本的认识,知道信号的发送和注册方式,了解阻塞信号的方法,以及通过信号传递数据。

Thanks

  • 《Linux 系统编程》
  • https://man7.org/linux/man-pages/man7/signal.7.html
  • https://man7.org/linux/man-pages/man2/sigaction.2.html
  • https://man7.org/linux/man-pages/man7/signal-safety.7.html
  • https://man7.org/linux/man-pages/man3/pthread_sigmask.3.html
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拭心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值