linux内核中的信号机制--信号机制的管理结构
Kernel version:2.6.14
CPU architecture:ARM920T
Author:ce123(http://blog.csdn.net/ce123)
信号只是一个数字,数字为0-31表示不同的信号,如下表所示。
信号名 | 默认动作 | 说明 | |
1 | SIGHUP | 进程终止 | 终端断开连接 |
2 | SIGINT | 进程终止 | 用户在键盘上按下CTRL+C |
3 | SIGQUIT | 进程意外结束(Dump) | 用户在键盘上按下CTRL+\ |
4 | SIGILL | 进程意外结束(Dump) | 遇到非法指令 |
5 | SIGTRAP | 进程意外结束(Dump) | 遇到断电,用于调试 |
6 | SIGABRT/SIGIOT | 进程意外结束(Dump) |
|
7 | SIGBUS | 进程意外结束(Dump) | 总线错误 |
8 | SIGFPE | 进程意外结束(Dump) | 浮点异常 |
9 | SIGKILL | 进程终止 | 其他进程发送SIGKILL将导致目标进程终止 |
10 | SIGUSR1 | 进程终止 | 应用程序可自定义使用 |
11 | SIGSEGV | 进程意外结束(Dump) | 非法的内存访问 |
12 | SIGUSR2 | 进程终止 | 应用程序可自定义使用 |
13 | SIGPIPE | 进程终止 | 管道读取端已经关闭,写入端进程会收到该信号 |
14 | SIGALRM | 进程终止 | 定时器到时 |
15 | SIGTERM | 进程终止 | 发送该信号使目标进程终止 |
16 | SIGSTKFLT | 进程终止 | 堆线错误 |
17 | SIGCHLD | 忽略 | 子进程退出时会向父进程发送该信号 |
18 | SIGCONT | 忽略 | 进程继续执行 |
19 | SIGSTOP | 进程暂停 | 发送该信号会使目标进程进入TASK_STOPPED状态 |
20 | SIGTSTP | 进程暂停 | 在终端上按下CTRL+Z |
21 | SIGTTIN | 进程暂停 | 后台进程从控制终端读取数据 |
22 | SIGTTOU | 进程暂停 | 后台进程从控制终端读取数据 |
23 | SIGURG | 忽略 | socket收到设置紧急指针标志的网络数据包 |
24 | SIGXCPU | 进程意外结束(Dump) | 进程使用CPU已经超过限制 |
25 | SIGXFSZ | 进程意外结束(Dump) | 进程使用CPU已经超过限制 |
26 | SIGVTALRM | 进程终止 | 进程虚拟定时器到期 |
27 | SIGPROF | 进程终止 | 进程Profile定时器到期 |
28 | SIGMNCH | 忽略 | 进程终端窗口大小改变 |
29 | SIGIO | 进程暂停 | 用于异步IO |
29 | SIGPOLL | 进程暂停 | 用于异步IO |
30 | SIGPWR | 进程暂停 | 电源失效 |
31 | SIGUNUSED | 进程暂停 | 保留未使用 |
注意在上标中的默认动作是指,在没有任何程序为相应的信号设置信号处理函数的情况下,内核接收到该信号的默认处理方式,但在实际中,有可能不是这样的。另外,在这里,进程终止一般是指进程通过do_exit()退出,进程意外结束(Dump)则表示进程遇到了一个异常。默认情况下,内核会根据进程当时的内存情况,在进程的当前目录中生成一个Core Dump文件,以后用户可以通过这个文件分析进程异常的原因。这个工作主要通过do_coredump()来完成。
由于早期只有31个信号,内核仅仅使用一个32位的变量signal来表示进程接收到的信号,因此如果要向一个进程发送一个信号,就把signal的第n位设置为1,这非常类似中断请求寄存器SRCPND寄存器,同时,还有一个blocked的变量,用来屏蔽信号,这类似中断屏蔽寄存器INTMSK。这样做的好处是可以“很快”判断出一个进程收到了哪些信号,如果采用链表或者数组,则需要扫描整个队列,但这也带来了新的问题,如果向一个进程发送了SIGINT信号,在这个信号处理之前,再次发送SIGINT,当这个进程开始处理信号时,它只知道收到了SIGINT信号,而无法判断出有几个SIGINT需要处理。此后加入了信号队列,把收到的信号保存在这个队列中,就可以很好的解决这个问题了。但是为了兼容的目的,仍能保留了旧的信号处理方式,因此1-32还是按原有的方式进行处理,而33-64则使用新的机制,为了区别对待,编号为33-64的信号又称为实时信号。需要注意的是:这里的“实时”和实时操作系统中的“实时”没有任何联系,实时信号在处理速度上并不会比普通信号快,它们之间的区别就是:普通信号会对多次的同一个信号进行“合并”处理,而实时信号会一一处理。因此我们这里仅讨论普通信号。
信号机制的相关管理结构位于task_struct结构中,其主要结构如下图所示。
实时信号引入了信号队列,为了处理上的方便,普通信号也使用了信号队列,仅仅从数字上无法区分实时信号和普通信号。由于在linux中,进程对象和线程对象都是task_struct,因此需要区别对待线程的信号和进程的信号。在上图中,Private Signal Queue是线程(在linux中称为轻权进程)信号队列,而Shared Signal Queue是进程(在linux中被称为进程组)信号队列。对于进程信号,则由进程组中的每一个线程共享。例如,在上图中,pending和shared_pending分别是Private Signal Queue和Shared Signal Queue其类型都是sigpending(/include/linux/signal.h),定义如下:
struct sigpending {
struct list_head list;
sigset_t signal;
};
list用于连接信号队列,signal是一个位图,每一位表示一个对应的信号,用于指示信号队列中有哪些信号等待处理,其类型为sigset_t(include/asm-arm/signal.h),其定义如下:
#define _NSIG 64
#define _NSIG_BPW 32
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
sigset_t是一个数组,总共有64位,对应64个信号位图(32个普通信号和32个实时信号)。在以后的信号发送的分析中,我们会看到,对于普通信号,只需要把sigset_t中对应的位置1就可以了,而对于实时信号,还需要把相关信息添加到list的信号队列,信号队列类型为sigqueue(include/linux/signal.h),定义如下:
/*
* Real Time signals may be queued.
*/
struct sigqueue {
struct list_head list;
spinlock_t *lock;
int flags;
siginfo_t info;
struct user_struct *user;
};
sigqueue中的list是队列链表指针,info为这个信号的相关信息,其定义如下(include/asm-generic/siginfo.h):
typedef struct siginfo {
int si_signo;
int si_errno;
int si_code;
union {
int _pad[SI_PAD_SIZE];
/* kill() */
struct {
pid_t _pid; /* sender's pid */
__ARCH_SI_UID_T _uid; /* sender's uid */
} _kill;
/* POSIX.1b timers */
struct {
timer_t _tid; /* timer id */
int _overrun; /* overrun count */
char _pad[sizeof( __ARCH_SI_UID_T) - sizeof(int)];
sigval_t _sigval; /* same as below */
int _sys_private; /* not to be passed to user */
} _timer;
/* POSIX.1b signals */
struct {
pid_t _pid; /* sender's pid */
__ARCH_SI_UID_T _uid; /* sender's uid */
sigval_t _sigval;
} _rt;
/* SIGCHLD */
struct {
pid_t _pid; /* which child */
__ARCH_SI_UID_T _uid; /* sender's uid */
int _status; /* exit code */
clock_t _utime;
clock_t _stime;
} _sigchld;
/* SIGILL, SIGFPE, SIGSEGV, SIGBUS */
struct {
void __user *_addr; /* faulting insn/memory ref. */
#ifdef __ARCH_SI_TRAPNO
int _trapno; /* TRAP # which caused the signal */
#endif
} _sigfault;
/* SIGPOLL */
struct {
__ARCH_SI_BAND_T _band; /* POLL_IN, POLL_OUT, POLL_MSG */
int _fd;
} _sigpoll;
} _sifields;
} siginfo_t;
上图中的sighand保存信号的处理函数指针,其作用类似于中断向量表,类型为sighand_struct(include/linux/sched.h),定义为:
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
};
_NSIG定义在asm-arm/signal.h中,为64,数组action,对应64个信号处理函数的相关信息,烈性为k_sigaction,在arm平台上,k_sigaction(include/asm-arm/signal.h)是死噶长提哦你的一个包装,定义如下:
struct k_sigaction {
struct sigaction sa;
};
sigantion(include/asm-arm/signal.h)的定义如下:
struct sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
__sigrestore_t sa_restorer;
sigset_t sa_mask; /* mask last for extensibility */
};
sa_handler就是信号处理函数指针。另外在task_struct结构中还有一个blocked可以用来屏蔽信号。明白了上面的主要数据结构的作用之后,很容易想到信号的处理主要有以下几方面。
- 设置信号回调函数:内核吧函数的相关信息保存到对应的sigantion结构中。
- 信号的发送:通过相关系统调用吧一个指定的信号发送到目标进程,如果该信号没有被屏蔽,就把信号的相关信息添加到信号队列中,如果有必要就唤醒目标进程。
- 信号响应:进程被唤醒后,根据信号队列中的信息,调用信号回调函数。
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;