信号也被称为软中断,主要用于在软件层面对中断机制的一种模拟。在所有的进程通信当中只有信号是异步的,接受信号的进程不知道什么时候信号会来,也不知道来的是什么信号。我们可以通过在控制台执行kill -l命令查看所有的信号变量。每一个信号在进程调度并由内核空间返回到用户空间的时候进行处理,在处理完信号之后,又需要返回到内核然后再进入到用户空间执行。至于为什么会这样会在下面的分析中看到原因。
在struct task_struct当中有一个sighand_struct结构体用于信号的处理:
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
wait_queue_head_t signalfd_wqh;
};
结构当中的action相当于一个信号处理向量表,确定当进程收到一个具体的信号的时候应该采取的行动。不过信号处理函数包含三种特殊处理,这三种在include\uapi\asm-generic\signal_defs.h文件中定义。
#define SIG_DFL ((__force __sighandler_t)0) /* default signal handling */
#define SIG_IGN ((__force __sighandler_t)1) /* ignore signal */
#define SIG_ERR ((__force __sighandler_t)-1) /* error return from signal */
其中__force是一个宏,表明这些数据是可以强制类型转换而不报警告的。其中sigaction在\include\linux\signal.h当中定义,这个结构体主要包括一个sigaction结构体。
struct sigaction {
__sighandler_tsa_handler;
unsigned longsa_flags;
sigset_tsa_mask;/* mask last for extensibility */
};
# define __user__attribute__((noderef, address_space(1)))
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t
上面是__sighandler_t的定义,表明__sighandler_t是一个void func(int)类型的函数指针,而_user则是表明这个指针地址必须是有效的,并且处于用户空间。
接下来是sa_mask,这个成员变量是一个位图,其中每一位都对应一种信号,如果某位为1则表明执行当前信号处理程序期间需要将相应的信号暂时屏蔽。而sa_flags则表明信号屏蔽的风格。在比较老的系统当中没有实现信号处理的嵌套,而是通过一种方法巧妙的避开嵌套处理,当执行一个信号处理程序的时候,内核自动将信号向量表中相应的函数指针设置为SIG_DFL,在进程结束的时候再设置回用户定义的信号处理函数,而对信号的默认处理是结束进程,所以才说老式的信号处理时不安全的。
#define _NSIG64
#define _NSIG_BPW32
#define _NSIG_WORDS(_NSIG / _NSIG_BPW)
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
上面是对信号处理位图的定义,64表明包含64个信号,而32表明每个字节所能包含的位数。下面是sa_flags的一些值的定义:
#define SA_NOCLDSTOP 0x00000001u当子进程结束的时候关闭SIGCHLD信号量
#define SA_NOCLDWAIT 0x00000002u等待子进程结束
#define SA_SIGINFO 0x00000004u表明利用新式的信号处理函数进行处理
#define SA_ONSTACK 0x08000000u表明使用一个用户定义的堆栈
#define SA_RESTART 0x10000000u不安全的信号处理在处理完信号SIG_DFL之后,自动将其还原。
#define SA_NODEFER 0x40000000u组织当前信号被屏蔽
#define SA_RESETHAND 0x80000000u在信号处理之后将信号处理程序清空
介绍了这么多,开始看整个信号处理的流程。首先看信号处理安装函数。老式的安装函数是:
sighandler_t signal(int signum, sighandler_t handler);
函数根据传入的函数指针,设置信号处理向量表,然后返回原来的信号处理向量表。新的安装函数是:
int sigaction (int signum, const struct sigaction *newact, struct sigaction *oldact);
这两个函数分别通过调用不同的系统函数进行处理,这里仅仅介绍一种:
SYSCALL_DEFINE5(rt_sigaction, int, sig, const struct sigaction __user *, act,
struct sigaction __user *, oact,
size_t, sigsetsize, void __user *, restorer)
{
struct k_sigaction new_ka, old_ka;
int ret;
if (sigsetsize != sizeof(sigset_t))
return -EINVAL;
if (act) {
new_ka.ka_restorer = restorer;
if (copy_from_user(&new_ka.sa, act, sizeof(*act)))
return -EFAULT;
}
ret = do_sigaction(sig, act ? &new_ka : NULL, oact ? &old_ka : NULL);
if (!ret && oact) {
if (copy_to_user(oact, &old_ka.sa, sizeof(*oact)))
return -EFAULT;
}
return ret;
}
上面的系统调用经过宏处理,需要展开,大致流程是先将用户区间的信息拷贝到内核区间,然后调用真正的处理将我们的信号处理函数放到任务结构体当中,然后将值返回到用户区间(见Linux内核当中的宏)。
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
struct task_struct *t = current;
struct k_sigaction *k;
sigset_t mask;
if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
return -EINVAL;
k = &t->sighand->action[sig-1];
spin_lock_irq(¤t->sighand->siglock);
if (oact)
*oact = *k;
if (act) {
sigdelsetmask(&act->sa.sa_mask,
sigmask(SIGKILL) | sigmask(SIGSTOP));
*k = *act;
if (sig_handler_ignored(sig_handler(t, sig), sig)) {
sigemptyset(&mask);
sigaddset(&mask, sig);
rm_from_queue_full(&mask, &t->signal->shared_pending);
do {
rm_from_queue_full(&mask, &t->pending);
t = next_thread(t);
} while (t != current);
}
}
spin_unlock_irq(¤t->sighand->siglock);
return 0;
}
函数的重点只有两句,也就是保存原有的信号处理函数指针,并将我们的信号处理函数给赋值到内核当中。在这之外加了一些其他的检查以及相应的处理。由于SIGKILL和SIGSTOP的处理不能被修改,所以在这里要剔除掉,另外如果是给SIGCONT、SIGCHLD和SIGWINCH设置SIG_IGN和SIG_DFL信号处理函数的时候,根据POSIX标准需要将这些已经到达的信号丢弃。
跟信号向量相关的系统调用还有如下一些函数:
Sigprocmask——改变本进程task_struct结构当中的blocked。这里的屏蔽作用会一直起作用,而不像sigaction当中哪样只是在执行的时候起作用。
Sigsuspend——暂时改变本进程的信号屏蔽位图,并使进城进入休眠状态,等待任何一个未被屏蔽的信号到来。
接下来看看,信号的发送过程。
老版本的发送函数原型如下:
int kill(pid_t pid, int sig);
新版本的发送函数原型:
int sigqueue(pid_t pid, int sig, const union sigval val);
Kill函数通过调用sys_kill来实现,下面是sys_kill的源代码:
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct 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);
}
主要是构造一个结构体,然后调用相应的函数,这个函数根据传递的PID进行下一步的调用,当PID大于0的时候,就按照PID去发送信号,当PID小于0并且不等于1的时候,那么信号发送给组内的所有进程,如果组号等于-1那么发送给除自己之外的所有的进程。由于这里面的函数调用比较深,所以就不再往下跟了。函数的实现主要是检验相应的参数,然后创建一个结构体挂载到到进程的sigpending当中。