原标题:Linux信号专题FAQ(1)
信号:基本概念 可重入、线程安全以及异步信号安全的区别?
参考可重入、线程安全和异步信号安全,需要强调的是异步信号安全,这个概念知道的人不多,平常大家在编写代码的时候也很少考虑这个因素,也不清楚哪些函数是异步信号安全的,哪些不是,典型的像printf就不是异步信号安全的,内部会加锁,但是平时很多人都喜欢在信号处理函数中调用。大多数情况下都不会出现问题的,所以让使用者错误的认为这是正确的写法。第二个需要注意的是可重入的概念,Linux有很不少系统调用的实现都是不可重入的,会将结果保存在内部的静态数据存储中,同时这类系统调用也提供了可重入版本的实现,其函数名就是尾部添加_r来标识。
信号的内部实现是怎么样的?
如上图所示一个进程/线程就是一个task_struct结构,该结构包含了属于这个进程/线程的阻塞信号集、pending的信号等,所有投递到该进程/线程的信号都会通过双向链表组织在一起,链表的元素是sigqueue,所有的信号对应的信号处理函数存放在sighand_struct中的一个类型为k_sigaction数组,每次程序由核心态切换到用户态时,内核都会发起信号处理,执行信号处理程序的时候为了避免对内核产生影响,所以使用的是用户栈,还可以自定义信号处理的备用栈。
信号处理函数是每次程序从核心态切换到用户态的时候,内核才会负责发起信号处理,也就是说信号处理的时机有以下两种:
进程在当前时间片用完后,获得了新的时间片时(会发生内核态到用户态的切换)
系统调用执行完成时(信号的传递可能会引起正在阻塞的系统调用过早完成)如何查看一个进程当前等待的、阻塞的、忽略的、捕获的信号?
通过查看/proc/PID/status文件,该文件中有几个字端的值,这些值按照十六进制的形式显示,最低的有效位表示信号1,相邻的左边一位代表信号2,依次类推,例如下面这几个数值:
SigQ: 0/38720 0是当前信号队列中的信号数,3872是信号队列的最大长度SigPnd: 0000000000000000 当前pending的信号,也就是没有信号投递给线程ShdPnd: 0000000000000000 当前pengding的信号,也就是没有信号投递给进程SigBlk: 7be3c0fe28014a03 当前阻塞的信号SigIgn: 0000000000001000 当前被忽略的信号SigCgt: 00000001800004ec 当前被捕获的信号 信号的默认处理方式有哪些?
当信号到达的时候,默认情况下信号有如下几种处理方式:
忽略信号,内核直接将信号丢弃,不对进程产生任何影响
终止进程,是一种异常的终止方式,和调用exit而发生的终止不同
产生核心存储文件,同时进程终止
停止进程,暂停进程的运行
恢复之前暂停的进程
执行用户自定义的信号处理器signal和sigaction区别?
这两者都可以用来改变信号处置,signal很原始,提供的接口也比较简单,而sigaction提供了signal所不具备的功能。为了兼容,signal系统调用仍然保存,但是glibc是使用sigaction实现了signal的功能。sigaction同时支持两种形式的信号处理,通过不用的flags区分,通过设置不同的flags可以得到不同的功能。
struct sigaction { // 两种handler,兼容老的signal对应的信号处理函数 void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; // 要屏蔽的信号集 int sa_flags; void (*sa_restorer)(void); // Not for application use};sa_flags:1. SA_NOCLDSTOP 当接收一个信号而停止或恢复某一个子进程时,将不会产生SIGCHLD信号2. SA_NOCLDWAIT 子进程终止时不会将其转化为僵尸3. SA_NODEFER 捕获信号后,不会在执行信号处理器程序的时候自动将该信号添加到进程掩码中,也就是不会被这个 信号再次打断,成为死循环。4. SA_ONSTACK 在执行信号处理函数时,使用sigaltstack安装的备选栈5. SA_RESETHAND 捕获信号后,会在调用处理器函数之前将信号处置为默认值6. SA_RESTART 自动重启由信号处理器程序中断的系统调用7. SA_SIGINFO 调用信号处理器程序时,携带额外的参数,也就是使用sa_sigaction类型的信号处理函数 通过Kill来检查进程是否存在?
kill系统调用可以用来向指定进程发送信号,如果指定的信号是0的时候,kill仅会进行错误的检查,查看是否可以想目标进程发送信号,而这一特点恰好可以用来检测特定进程ID所对应的进程是否存在,如果不存在那么kill调用失败,并且errno设置为ESRCH
如何打印信号枚举值对应的信号描述? #include #include #include int main() { printf("signal: %sn", strsignal(SIGKILL)); // 和strerror等同 psignal(SIGKILL, "signal"); // 和perror等同 return 0;} 信号集的内部实现?
信号集是一种用来表示一系列信号集合的数据结构,使用sigset_t来表示,它的底层存储类型其实只是一个unsigned long类型,如下:
typedef unsigned long sigset_t;
unsigned long一共是八个字节,总共是64位,每一位表示一个信号的话,最多可以表示64个信号,这个和信号的最大值是吻合的。信号集也提供了一系列用来操作信号集的方法,sigemptyset、sigfillset、sigaddset、sigdelset、sigismember、sigisemptyset等
如何阻塞信号,阻塞的信号在解除阻塞后是否会投递?
阻塞信号的实现不难,通过上文中对信号内部实现的分析可知,通过将要阻塞的信号放到task_struct结构中的blocked成员中,那么在信号的投递时会先查看下要投递的信号是否在阻塞信号集中,如果在就停止投递,否则就触发对应的信号处理,通过sigprocmask可以设置当前进程的阻塞信号集,对应到内核的实现如下:
int sigprocmask(int how, sigset_t *set, sigset_t *oldset){ struct task_struct *tsk = current; sigset_t newset; /* Lockless, only current can change ->blocked, never from irq */ if (oldset) *oldset = tsk->blocked; switch (how) { case SIG_BLOCK: sigorsets(&newset, &tsk->blocked, set); break; case SIG_UNBLOCK: sigandnsets(&newset, &tsk->blocked, set); break; case SIG_SETMASK: newset = *set; break; default: return -EINVAL; } __set_current_blocked(&newset); return 0;}
通过sigprocmask设置阻塞的信号集存在一个竞态,如果想在设置信号处理函数的同时再设置阻塞的信号集,那么这需要先调用signal/sigaction,然后再调用sigprocmask,在设置信号处理函数和调用sigprocmask之间存在一个间隙,如果在这个间隙期间后信号投递,那么就没有起到阻塞信号的作用了。为此sigaction的sa_mask成员可以用来设置阻塞信号集,这使得设置信号处理函数的同时就可以设置阻塞信号集。
另外一个问题就是被阻塞的信号在等待解除阻塞后是否会投递到进程进行处理?信号被阻塞后就会变成待决信号,并通过链表链接起来,task_struct结构中的pending成员就是链表头,如果一个信号发送多次,linux是不保证投递相同次数的,只会保存一次,也就是非实时,不对信号排队。其中SIGKILL和SIGSTOP是不能被阻塞的。
如何知道当前哪些信号被阻塞了?
说白了这里就是去查询待决信号的链表也就是task_struct结构中的pending成员,将里面的信号放到信号集中返回即可。对应到内核实现如下:
static int do_sigpending(void *set, unsigned long sigsetsize){ if (sigsetsize > sizeof(sigset_t)) return -EINVAL; spin_lock_irq(¤t->sighand->siglock); //加锁 sigorsets(set, ¤t->pending.signal, // 将pengding和signal->shared_pending中的信号区取并集 ¤t->signal->shared_pending.signal); spin_unlock_irq(¤t->sighand->siglock); /* Outside the lock because only this thread touches it. */ sigandsets(set, ¤t->blocked, set); //最后将待决信号和阻塞的信号取交集,因为待决信号并不一定是阻塞的,有可能是还没来得及投递的,所以这里要取交集 return 0;}
在用户态通过sigpending函数就可以查询当前哪些被阻塞的信号是未决的(也就是已经投递到进程了,但是因为被屏蔽了还没有被处理,也就是保存在进程的pending成员中)返回搜狐,查看更多
责任编辑: