信号的使用及原理
信号实质上是一种软中断,既然是一种中断,就说明信号是异步的,信号接收函数不需要一直阻塞等待信号的到达。
当信号发出后,如果有地方注册了这个信号,就会执行响应函数,如果没有地方注册这个信号,该信号就会被忽略。
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler); //信号注册函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //信号注册函数
struct sigaction {
void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
int sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};
int kill(pid_t pid, int sig); //信号发送函数
int sigqueue(pid_t pid, int sig, const union sigval value); //信号发送函数
//……
注册信号有两个方法
-
signal()函数:signal不支持传递信息,signum入参为信号量,handler入参为信号处理函数
-
sigaction()函数:sigaction支持传递信息,信息放在sigaction数据结构中
信号发送
信号发送函数比较多,这里我列举一下。
- kill():用于向进程或进程组发送信号;
- sigqueue():只能向一个进程发送信号,不能向进程组发送信号;
- alarm():用于调用进程指定时间后发出SIGALARM信号;
- setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大;
- abort():向进程发送SIGABORT信号,默认进程会异常退出。
- raise():用于向进程自身发送信号;
通过kill -l指令可以查看Android手机支持的信号,从下图可以看到,总共有64个,前31个信号是普通信号,后33个信号是实时信号,实时信号支持队列,可以保证信号不会丢失。
前几个信号的作用
1 | SIGHUP | 挂起 |
---|---|---|
2 | SIGINT | 中断 |
3 | SIGQUIT | 中断 |
3 | SIGQUIT | 退出 |
4 | SIGILL | 非法指令 |
5 | SIGTRAP 断点或陷阱指令 | |
6 | SIGABRT | abort发出的信号 |
7 | SIGBUS | 非法内存访问 |
8 | SIGFPE | 浮点异常 |
9 | SIGKILL | 杀进程信息 |
当我们调用信号发送函数后,信号是怎么传到注册的方法调用中去的呢?这里以kill()这个信号发送函数讲解一下这个流程。
kill()函数会经过系统调用方法sys_tkill()进入内核,sys_tkill是SYSCALL_DEFINE2这个方法来实现,这个方法的实现是一个宏定义。
我会从这个方法一路往底追踪,这里我会忽略细节实现,只看关键部分的代码。
//文件-->syscalls.h
asmlinkage long sys_kill(int pid, int sig);
//文件-->kernel/signal.c
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
clear_siginfo(&info);
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}
static int kill_something_info(int sig, struct kernel_siginfo *info, pid_t pid)
{
int ret;
if (pid > 0) {
ret = kill_pid_info(sig, info, find_vpid(pid));
return ret;
}
……
}
int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid)
{
……
for (;;) {
error = group_send_sig_info(sig, info, p, PIDTYPE_TGID);
……
}
}
int group_send_sig_info(int sig, struct kernel_siginfo *info,
struct task_struct *p, enum pid_type type)
{
……
ret = do_send_sig_info(sig, info, p, type);
return ret;
}
int do_send_sig_info(int sig, struct kernel_siginfo *info, struct task_struct *p,
enum pid_type type)
{
……
ret = send_signal(sig, info, p, type);
return ret;
}
static int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type)
{
return __send_signal(sig, info, t, type, from_ancestor_ns);
}
我们从sys_kill函数一路追踪,最终调用了__send_signal函数,我们接着看这个函数的实现。
//文件-->kernel/signal.c
static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type, int from_ancestor_ns)
{
……
out_set:
signalfd_notify(t, sig); //将信号发送给监听的fd
sigaddset(&pending->signal, sig);
complete_signal(sig, t, type); //完成信号发送
ret:
trace_signal_generate(sig, info, t, type != PIDTYPE_PID, result);
return ret;
}
static void complete_signal(int sig, struct task_struct *p, enum pid_type type)
{
struct signal_struct *signal = p->signal;
struct task_struct *t;
//寻找处理信号的线程
if (wants_signal(sig, p))
t = p;
else if ((type == PIDTYPE_PID) || thread_group_empty(p))
return;
else {
t = signal->curr_target;
while (!wants_signal(sig, t)) {
t = next_thread(t);
if (t == signal->curr_target)
return;
}
signal->curr_target = t;
}
//如果是SIGKILL信号,则杀掉线程组
if (sig_fatal(p, sig) &&
!(signal->flags & SIGNAL_GROUP_EXIT) &&
!sigismember(&t->real_blocked, sig) &&
(sig == SIGKILL || !p->ptrace)) {
/*
* This signal will be fatal to the whole group.
*/
if (!sig_kernel_coredump(sig)) {
/*
* Start a group exit and wake everybody up.
* This way we don't have other threads
* running and doing things after a slower
* thread has the fatal signal pending.
*/
signal->flags = SIGNAL_GROUP_EXIT;
signal->group_exit_code = sig;
signal->group_stop_count = 0;
t = p;
do {
task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
} while_each_thread(p, t);
return;
}
}
/*
* The signal is already in the shared-pending queue.
* Tell the chosen thread to wake up and dequeue it.
*/
signal_wake_up(t, sig == SIGKILL);
return;
}
可以看到,信号最终被分发到了监听的fd中,交给了我们注册的函数处理,从最后部分也可以看到,如果是SIGKILL信号,内核会专门处理去杀进程。
信号在Android中的使用场景
那么我们在来看一个Android系统中使用信号的场景:杀进程。从上面部分可以看到,SIGKILL信号是由内核捕获并处理的,我们看一下Android是怎么调用杀进程的信号的吧。
//文件-->Process.java
public static final void killProcess(int pid) {
sendSignal(pid, SIGNAL_KILL);
}
//文件-->android_util_Process.cpp
void android_os_Process_sendSignal(JNIEnv* env, jobject clazz, jint pid, jint sig) {
if (pid > 0) {
//打印Signal信息
ALOGI("Sending signal. PID: %" PRId32 " SIG: %" PRId32, pid, sig);
kill(pid, sig);
}
}
可以看到,当我们调用Process的killProcess函数杀掉某个进程时,最终会调用到native方法kill(),入参sig信号量就是SIGKILL,这个kill()方法,就是我在上面讲的信号量发送函数,最终内核会响应我们的SIGKILL,杀掉进程。
4 信号(Signal)
4.1 什么是信号?
- 信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;
- Linux除了支持Unix早期信号语义函数sigal外,还支持语义服务Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,有能够统一对外接口,用sigaction函数重新实现了signal函数)
4.2 信号的种类
5713484-b703c7840590eaeb.png- 每种信号类型都有对应的信号处理程序(也叫信号的操作),就好像每个中断都有一个中断服务例程一样。
- 大多数信号的默认操作是结束接受信号的进程;然而一个进程通常可以请求系统采取某些代替的操作
- 各种代替操作是
- 忽略信号。随着这一选项的设置,进程将忽略信号的出现。有两个信号不可以被忽略:SIGKILL,它将结束进程:SIGSTOP,它是作业控制机制的一部分,将挂起作业的执行。
- 恢复信号的默认操作
- 执行一个预先安排的信号处理函数。进程可以登记特殊的信号处理函数。当进程收到信号时,信号处理函数将像中断服务例程一样被调用,当从信号处理函数返回时,控制被返回给主程序,并且继续正常执行。
- 但是,信号和中断有所不同。中断的响应和处理都发生在内核空间,而信号的响应发生在内核空间,信号处理程序的执行却发生在用户空间。
那么什么时候检测和响应信号?通常发生在两种情况下:
- 当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间前戏
- 当前进程在内核进入睡眠以后刚被唤醒的时候,由于检测到信号的存在而提前返回到用户空间
4.3 信号的本质
- 信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
- 信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
- 信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。
- 信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息
4.4 信号来源
- 信号事件的发生有两个来源:硬件来源(比如我们按下键盘或者其他硬件故障);润健来源,最常用发送信号的系统函数是kill,raise,alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
4.5 关于信号处理机制的原理(内核角度)
- 内核给一个进程发送中断信号,是在进程所在的进程表项的信号域设置对应于该信号的位。
- 如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒正在睡眠的进程;否则仅设置进程表中信号域相应的位,而不是唤醒进程。
- 进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程进入或离开一个适当的低调度优先级睡眠状态时。
- 内核处理一个进程吸收的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
- 内核处理一个进程收到的软中断信号是在该进程的上下文中。因此,进程必须处于运行状态。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。
- 而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数出,从函数返回再弹出栈顶时,才返回原先进入内核的地方,接着原来的地方继续运行。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行。
网友评论