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结构中。
- 信号的发送:通过相关系统调用吧一个指定的信号发送到目标进程,如果该信号没有被屏蔽,就把信号的相关信息添加到信号队列中,如果有必要就唤醒目标进程。
- 信号响应:进程被唤醒后,根据信号队列中的信息,调用信号回调函数。
我们再来看一下信号回调函数,sa_handler的类型是__sihandler_t(include/asm-generic/singal.h),其定义为: - typedef void __signalfn_t(int);
- typedef __signalfn_t __user *__sighandler_t;
|