基于linux master v4.9版本
信号是异步的,
一、信号何时来
信号是异步的,对于一个进程随时都会接收到信号。
二、选择线程(task)来处理
那么一个进程接收到信号时,需要选择一个task来处理。如何选择呢?
三、使线程达到能够处理信号的状态
设置信号的pending flag。
1)对于睡眠线程
a)处于TASK_INTERRUPTIBLE,唤醒,系统调用返回
1 while (dev->current_len == GLOBALFIFO_SIZE) { 2 if (filp->f_flags & O_NONBLOCK) { 3 ret = -EAGAIN; 4 goto out; 5 } 6 __set_current_state(TASK_INTERRUPTIBLE); 7 8 mutex_unlock(&dev->mutex); 9 10 schedule(); 11 if (signal_pending(current)) { 12 ret = -ERESTARTSYS; 13 goto out2; 14 } 15 16 mutex_lock(&dev->mutex); 17 }
b)处于TASK_UNINTERRUPTIBLE,
i)如果signal是SIGKILL(9),也会唤醒,返回系统调用
ii)如果signal不是SIGKILL(9),不会唤醒,等task自己从睡眠中醒来,返回系统调用;如果task一直处于深度睡眠中,那么该信号就没办法得到处理
c)处于TASK_RUNNING(queued/runnable就绪态和运行态,tsk->state = TASK_RUNNING),不需要做进一步处理
四、真正处理信号
线程通过系统调用(sys_call)陷入内核态。具体来说,通过int80陷入内核,然后int80的处理函数通过系统调用号来调用sys_call,sys_call返回后会检查该task是否pending信号,有则处理。
基于linux0.11版本的分析:https://blog.csdn.net/geekcome/article/details/6398414
基于新代码分析TODO
五、补充
从以上分析得出结论,信号只在系统调用结束,返回用户态前处理。那么一个不调用sys_call的进程就没有办法处理信号了?
实验:最简单的死循环
1 int main(){ 2 while(1){} 3 }
strace跟踪:为了说明进程的启动,和 一些属性,本文没有删除启动过程产生的系统调用打印;进程启动后,kill -2 pid杀死进程。
1 zsh@zsh-vm:~/program/C++$ strace ./signal_while.o 2 execve("./signal_while.o", ["./signal_while.o"], [/* 74 vars */]) = 0 3 brk(NULL) = 0x8993000 4 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) 5 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f8c000 6 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 7 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 8 fstat64(3, {st_mode=S_IFREG|0644, st_size=89534, ...}) = 0 9 mmap2(NULL, 89534, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7f76000 10 close(3) = 0 11 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) 12 open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 13 read(3, "177ELF1113 3 3 1 3202071 004 "..., 512) = 512 14 fstat64(3, {st_mode=S_IFREG|0755, st_size=1786484, ...}) = 0 15 mmap2(NULL, 1792540, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7dc0000 16 mmap2(0xb7f70000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1af000) = 0xb7f70000 17 mmap2(0xb7f73000, 10780, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7f73000 18 close(3) = 0 19 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7dbf000 20 set_thread_area({entry_number:-1, base_addr:0xb7dbf700, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 (entry_number:6) 21 mprotect(0xb7f70000, 8192, PROT_READ) = 0 22 mprotect(0x8049000, 4096, PROT_READ) = 0 23 mprotect(0xb7fb5000, 4096, PROT_READ) = 0 24 munmap(0xb7f76000, 89534) = 0 25 --- SIGWINCH {si_signo=SIGWINCH, si_code=SI_KERNEL} --- 这是窗口大小改变引起 26 --- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=11671, si_uid=1000} --- 27 +++ killed by SIGINT +++
可以看出信号还是得到了处理。那么就不符合以上的分析结果。再进一步分析,信号的处理时机肯定是统一接口的,那么一个死循环看似永远跑在用户态的进程什么时候会有系统调用呢?答案在进程调度引起了系统调用。普通进程CFS调度类,有233次上下文切换,180次非自愿,53次自愿。 当然这期间还有时钟中断的处理,这属于硬中断,不确定会不会引起信号处理。
1 zsh@zsh-vm:~/program/C++$ cat /proc/11689/sched 2 signal_while.o (11689, #threads: 1) 3 ------------------------------------------------------------------- 4 se.exec_start : 83967758.954863 5 se.vruntime : 251746.287113 6 se.sum_exec_runtime : 137316.047183 7 se.nr_migrations : 0 8 nr_switches : 233 9 nr_voluntary_switches : 53 10 nr_involuntary_switches : 180 11 se.load.weight : 1024 12 se.avg.load_sum : 48495214 13 se.avg.util_sum : 48483218 14 se.avg.load_avg : 1024 15 se.avg.util_avg : 1023 16 se.avg.last_update_time : 83967758954496 17 policy : 0 18 prio : 120 19 clock-delta : 46
五、拓展:1、通过signal no来获取信号处理函数。
task的信号处理函数保存在 task_struct里。
1 /* Signal handlers: */ 2 struct signal_struct *signal; 3 struct sighand_struct *sighand;
signal指向进程信号描述符,不是很明白,与进程组有关,sighand是64个信号处理函数的指针。
1 struct sighand_struct { 2 atomic_t count; 3 struct k_sigaction action[_NSIG]; 4 spinlock_t siglock; 5 wait_queue_head_t signalfd_wqh; 6 };
当然信号处理函数可以通过signal和sigaction函数设置用户自定义处理函数,不设置即为系统默认处理。linux里面,signal用sigaction实现。
2、驱动层ERESTARTSYS和EINTR的区别
参考:https://stackoverflow.com/questions/9576604/what-does-erestartsys-used-while-writing-linux-driver
ERESTARTSYS有两种结果:1)该信号设置了SA_RESTART,会重启系统调用,当然,用户态不感知,但是对于一些系统调用没办法或者很难直接重启系统调用,比如定时器,详见man说明。2)该信号没有设置SA_RESTART,那么会返回到用户态,返回值为-1,并设置errno=EINTR(return −1 and set errno to indicate the error)。
EINTR只是返回到用户态,返回值为-1,并设置errno=EINTR(return −1 and set errno to indicate the error),上面所述第二种情况。
syscall_get_nr判断是不是从系统调用返回,另外的情况是(硬件)中断和异常(比如page_falut)。
只有系统调用睡眠后,被信号唤醒,才会返回ERESTARTSYS或EINTR(具体返回那个视功能决定,比如conncet就不能返回ERESTARTSYS)。如果是不会发生睡眠的系统调用,则不会返回这两个值。那么在信号处理完成后,不会走handle_signal的ERESTARTSYS分支,也就不会返回EINTR错误。所以非阻塞的socket系统调用,比如read、write不会返回EINTR错误。部分代码分析见:https://blog.csdn.net/yusiguyuan/article/details/45669657
3、系统调用的改变
4、kill命令
DESCRIPTION The default signal for kill is TERM. Use -l or -L to list available signals. Particularly useful signals include HUP, INT, KILL, STOP, CONT, and 0. Alternate signals may be specified in three ways: -9, -SIGKILL or -KILL. Negative PID values may be used to choose whole process groups; see the PGID column in ps command output. A PID of -1 is special; it indicates all pro‐ cesses except the kill process itself and init. pid> 0 send_signal对应进程 pid = -1 send_signal除自己和init进程外的所有进程, pid = 0 send_signal本进程组 其他负值 send_signal pid所在进程组 当然是有权限的限制。这里还牵涉到namespace,就不说了。
5、signo < 32 是不能排队的,所以是不可靠信号 0-31共32个是不可靠信号,也叫非实时信号。
1 #define SIGRTMIN 32 2 static inline int legacy_queue(struct sigpending *signals, int sig) 3 { 4 return (sig < SIGRTMIN) && sigismember(&signals->signal, sig); 5 } 6 7 result = TRACE_SIGNAL_ALREADY_PENDING; 8 if (legacy_queue(pending, sig)) 9 goto ret;
不可靠的来源:
- 进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
- 信号可能丢失,因为不支持排队,pending住信号后,收到同一个信号,那么该信号只能丢弃
因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。目前linux通过sigaction函数已经解决了 signo>SIGRTMIN的两个不可靠来源,< SIGRTMIN为了兼容以前程序没有解决。
6、信号0
http://www.linuxjournal.com/content/monitoring-processes-kill-0
“signal 0″ is kind of like a moral equivalent of “ping”.
Using “kill -0 NNN” in a shell script is a good way to tell if PID “NNN” is alive or not:
signal 0 is just used to check process is exists or not.
题外话:
1、TASK_INTERRUPTIBLE:浅度睡眠
TASK_UNINTERRUPTIBLE:深度睡眠
2、SIGKILL和SIGSTOP信号是不能捕捉和忽略的,即用户态不能对这两个信号设置处理函数和mask,只能执行系统默认行为。
1 int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) 2 { 3 if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))sig_kernel_only --检查对kill 和 stop 行为的非法设置 4 return -EINVAL; 5 6 k = &p->sighand->action[sig-1]; 7 8 spin_lock_irq(&p->sighand->siglock); 9 if (oact) 10 *oact = *k; 11 12 sigaction_compat_abi(act, oact); 13 14 if (act) { 15 sigdelsetmask(&act->sa.sa_mask, 16 sigmask(SIGKILL) | sigmask(SIGSTOP)); --去除对kill 和 stop信号的屏蔽操作
后面发现的资料:http://kernel.meizu.com/linux-signal.html