![信号内部如何工作?](https://linux22.com/image/33199/%E4%BF%A1%E5%8F%B7%E5%86%85%E9%83%A8%E5%A6%82%E4%BD%95%E5%B7%A5%E4%BD%9C%EF%BC%9F.png)
一般来说,为了终止进程,我们会生成诸如SIGKILL
等SIGTSTP
信号。
但是如何知道谁发出了该特定信号、谁将其发送到特定进程以及信号通常如何执行其操作?信号内部如何工作?
答案1
50,000英尺的视野是:
信号由内核内部生成(例如,
SIGSEGV
当访问无效地址时,或者SIGQUIT
当您点击Ctrl+时\),或者由使用系统调用的程序kill
(或几个相关的)生成。如果是由系统调用之一执行,则内核确认调用进程有足够的权限来发送信号。如果不是,则返回错误(并且信号不会发生)。
如果它是两个特殊信号之一,内核将无条件地对其进行操作,而无需来自目标进程的任何输入。这两个特殊信号是SIGKILL 和SIGSTOP。下面所有关于默认操作、阻塞信号等的内容与这两者无关。
接下来,内核计算出如何处理该信号:
对于每个进程,每个信号都有一个关联的操作。有很多默认值,程序可以使用 、 等设置不同的默认值
sigaction
。signal
其中包括“完全忽略它”、“终止进程”、“使用核心转储终止进程”、“停止进程”等ETC。程序还可以逐个信号地关闭信号传送(“阻止”)。然后信号保持待决状态,直到解除阻塞。
程序可以请求内核不自行采取某些操作,而是同步(使用
sigwait
、等人 或signalfd
)或异步(通过中断进程正在执行的任何操作并调用指定的函数)将信号传递给进程。
还有第二组信号,称为“实时信号”,没有特定含义,也允许多个信号排队(正常信号在信号阻塞时只对每个信号排队一个)。它们在多线程程序中用于线程之间的通信。例如,有几个用于 glibc 的 POSIX 线程实现中。它们还可以用于在不同进程之间进行通信(例如,您可以使用多个实时信号让 fooctl 程序向 foo 守护进程发送消息)。
对于非 50,000 英尺的视图,请尝试查看man 7 signal
内核内部文档(或源代码)。
答案2
信号实现非常复杂并且是特定于内核的。换句话说,不同的内核会以不同的方式实现信号。简单解释如下:
CPU基于一个特殊的寄存器值,在内存中有一个地址,它希望在其中找到一个“中断描述符表”,它实际上是一个向量表。每个可能的异常都有一个向量,例如被零除或陷阱,例如 INT 3(调试)。当CPU遇到异常时,它会将标志和当前指令指针保存在堆栈上,然后跳转到相关向量指定的地址。在 Linux 中,这个向量总是指向内核,其中有一个异常处理程序。现在CPU已经完成了,Linux内核接管了。
请注意,您还可以从软件触发异常。例如,用户按CTRL- C,然后此调用将转到内核,内核调用其自己的异常处理程序。一般来说,有不同的方法可以到达处理程序,但无论如何都会发生相同的基本事情:上下文被保存在堆栈上,并且跳转到内核的异常处理程序。
然后异常处理程序决定哪个线程应该接收信号。如果发生除零之类的情况,那么很容易:导致异常的线程获取信号,但对于其他类型的信号,决策可能非常复杂,在某些不寻常的情况下,或多或少的随机线程可能会得到信号。
要发送信号,内核首先要设置一个指示信号类型SIGHUP
或其他值的值。这只是一个整数。每个进程都有一个“待处理信号”存储区域,用于存储该值。然后内核创建一个包含信号信息的数据结构。该结构包括信号“处置”,其可以是默认的、忽略的或处理的。然后内核调用它自己的函数do_signal()
。下一阶段开始。
do_signal()
首先决定是否它将处理该信号。例如,如果它是一个杀,然后do_signal()
就杀死进程,故事结束。否则,它会查看配置。如果处置是默认的,则do_signal()
根据依赖于信号的默认策略来处理信号。如果处理是句柄,则意味着用户程序中有一个函数被设计来处理相关信号,并且指向该函数的指针将位于上述数据结构中。在这种情况下, do_signal() 调用另一个内核函数 ,handle_signal()
然后该函数经历切换回用户模式并调用该函数的过程。这种切换的细节极其复杂。当您使用 中的函数时,程序中的这段代码通常会自动链接到您的程序中signal.h
。
通过适当地检查挂起的信号值,内核可以确定进程是否正在处理所有信号,如果没有,则将采取适当的操作,这可能会使进程进入睡眠状态或杀死它或其他操作,具体取决于信号。
答案3
虽然这个问题已经得到解答,但让我发布一下 Linux 内核中的详细事件流程。
这是完全复制自Linux 帖子:Linux 信号 - 内部结构
位于 sklinuxblog.blogspot.com 上的“Linux posts”博客。这些博客也是我写的。
信号用户空间 C 程序
让我们从编写一个简单的信号用户空间 C 程序开始:
#include<signal.h>
#include<stdio.h>
/* Handler function */
void handler(int sig) {
printf("Receive signal: %u\n", sig);
};
int main(void) {
struct sigaction sig_a;
/* Initialize the signal handler structure */
sig_a.sa_handler = handler;
sigemptyset(&sig_a.sa_mask);
sig_a.sa_flags = 0;
/* Assign a new handler function to the SIGINT signal */
sigaction(SIGINT, &sig_a, NULL);
/* Block and wait until a signal arrives */
while (1) {
sigsuspend(&sig_a.sa_mask);
printf("loop\n");
}
return 0;
};
此代码为 SIGINT 信号分配一个新的处理程序。可以使用Ctrl+C组合键将 SIGINT 发送到正在运行的进程。当按下Ctrl+C时,异步信号 SIGINT 被发送到任务。也相当于kill -INT <pid>
在其他终端发送命令。
如果您执行 a kill -l
(这是一个小写字母L
,代表“列表”),您将了解可以发送到正在运行的进程的各种信号。
[root@linux ~]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
还可以使用以下组合键来发送特定信号:
- Ctrl+ C– 发送 SIGINT,默认操作是终止应用程序。
- Ctrl+</kbd> – sends SIGQUIT which default action is to terminate the application dumping core.
- Ctrl+ Z– 发送 SIGSTOP 来暂停程序。
如果编译并运行上面的 C 程序,您将得到以下输出:
[root@linux signal]# ./a.out
Receive signal: 2
loop
Receive signal: 2
loop
^CReceive signal: 2
loop
即使使用Ctrl+C,kill -2 <pid>
进程也不会终止。相反,它将执行信号处理程序并返回。
信号如何发送到进程
如果我们看到发送到进程的信号的内部结构并将 Jprobe 与 dump_stack 放在__send_signal
函数中,我们将看到以下调用跟踪:
May 5 16:18:37 linux kernel: dump_stack+0x19/0x1b
May 5 16:18:37 linux kernel: my_handler+0x29/0x30 (probe)
May 5 16:18:37 linux kernel: complete_signal+0x205/0x250
May 5 16:18:37 linux kernel: __send_signal+0x194/0x4b0
May 5 16:18:37 linux kernel: send_signal+0x3e/0x80
May 5 16:18:37 linux kernel: do_send_sig_info+0x52/0xa0
May 5 16:18:37 linux kernel: group_send_sig_info+0x46/0x50
May 5 16:18:37 linux kernel: __kill_pgrp_info+0x4d/0x80
May 5 16:18:37 linux kernel: kill_pgrp+0x35/0x50
May 5 16:18:37 linux kernel: n_tty_receive_char+0x42b/0xe30
May 5 16:18:37 linux kernel: ? ftrace_ops_list_func+0x106/0x120
May 5 16:18:37 linux kernel: n_tty_receive_buf+0x1ac/0x470
May 5 16:18:37 linux kernel: flush_to_ldisc+0x109/0x160
May 5 16:18:37 linux kernel: process_one_work+0x17b/0x460
May 5 16:18:37 linux kernel: worker_thread+0x11b/0x400
May 5 16:18:37 linux kernel: rescuer_thread+0x400/0x400
May 5 16:18:37 linux kernel: kthread+0xcf/0xe0
May 5 16:18:37 linux kernel: kthread_create_on_node+0x140/0x140
May 5 16:18:37 linux kernel: ret_from_fork+0x7c/0xb0
May 5 16:18:37 linux kernel: ? kthread_create_on_node+0x140/0x140
因此发送信号的主要函数调用如下:
First shell send the Ctrl+C signal using n_tty_receive_char
n_tty_receive_char()
isig()
kill_pgrp()
__kill_pgrp_info()
group_send_sig_info() -- for each PID in group call this function
do_send_sig_info()
send_signal()
__send_signal() -- allocates a signal structure and add to task pending signals
complete_signal()
signal_wake_up()
signal_wake_up_state() -- sets TIF_SIGPENDING in the task_struct flags. Then it wake up the thread to which signal was delivered.
现在一切都已设置完毕,并对task_struct
流程进行了必要的更改。
信号处理
当进程从系统调用返回或从中断返回完成时,该信号由进程检查/处理。系统调用的返回结果存在于 file 中entry_64.S
。
函数 int_signal 被调用,从中entry_64.S
调用函数do_notify_resume()
。
我们来检查一下这个函数do_notify_resume()
。该函数检查我们是否TIF_SIGPENDING
在以下位置设置了标志task_struct
:
/* deal with pending signal delivery */
if (thread_info_flags & _TIF_SIGPENDING)
do_signal(regs);
do_signal calls handle_signal to call the signal specific handler
Signals are actually run in user mode in function:
__setup_rt_frame -- this sets up the instruction pointer to handler: regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
系统调用和信号
“慢”系统调用,例如阻塞读/写,将进程置于等待状态:
TASK_INTERRUPTIBLE
或TASK_UNINTERRUPTIBLE
。
状态中的任务将通过信号TASK_INTERRUPTIBLE
更改为状态。意味着可以安排一个进程。TASK_RUNNING
TASK_RUNNING
如果执行,其信号处理程序将在“慢速”系统调用完成之前运行。默认情况下不syscall
完成。
如果SA_RESTART
设置了标志,syscall
则在信号处理程序完成后重新启动。