今天我们来看 kernel 目录下的 signal.c,在开始贴代码之前,先聊聊 Linux 中的信号机制

信号机制

基本概念

信号是 Linux 系统中最为古老的进程间通信机制。就如同日常生活中人与人之间通过互联网手段来联络沟通一样,有时需要让进程知道一些事件发生了,这样进程就能通过发生的事件类型做出相应的反应,信号就是进程间联络沟通的桥梁

信号发生的来源

上面的概念可能还是很抽象,来看个例子吧。

假设你在 Linux 终端执行了一个需要运行很久的程序,当你等了很久还是没反应想退出程序时,需要按下 Ctrl+C 来结束,这一按就会产生一个 SIGINT 信号发往当前正在运行的进程。进程收到该信号,就会去信号处理集中找到 SIGINT 对应的处理程序,也就是终止进程的处理程序,进而终止当前进程,返回命令行待输入的状态

言归正传,信号发生的具体来源:

  • 硬件来源:刚举的例子就是一种硬件来源,信号由硬件驱动程序产生
  • 软件来源:系统提供了一些发送信号的 API 函数,如 kill, raise, alarm, setitimer 等,这些信号由内核产生

进程对信号的响应及处理方式

当进程接收到一个信号时,有三种处理方式:

  • 忽略:忽略信号,不作处理,但 SIGKILL 与 SIGSTOP 信号无法被忽略(高版本中)
  • 执行默认操作:每个信号都有默认操作,大部分默认操作是终止进程
  • 执行自定操作:使用系统提供的函数修改信号处理函数,达到用户自定义响应方式的目的,但 SIGKILL 与 SIGSTOP 信号的处理函数无法被修改(高版本中)

代码实现

修改信号处理函数的系统 API 是 signal,其原型为:

1
2
3
#include <signal.h>

void (*signal(int _sig, void (*_func)(int)))(int);
  • 参数:
    • _sig:信号值
    • _func:三种取值:
      • SIG_IGN:忽略信号
      • SIG_DFL:系统默认方式
      • 自定信号处理函数指针
  • 返回值:
    • 成功返回之前的信号处理函数指针
    • 失败返回 SIG_ERR

那现在就来写一个程序捕获 SIGINT 信号并实现自己的信号处理函数吧(高版本 Linux)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void mySigIntFunc(int signo)
{
printf("You pressed Ctrl+C...but you can't stop me!HHHHHHH\n");
}

int main()
{
signal(SIGINT, mySigIntFunc);
printf("Pid: %d\n", getpid());
while(1) {}
return 0;
}

运行起来:

signal函数

现在按 Ctrl+C 就会调用我们自己写的函数 mySigIntFunc 了,因为这个函数没有退出程序的功能,所以 Ctrl+C 不会终止进程。如果想要将其终止掉,方法很多,我列出三种:

  • Ctrl + Z :发送 SIGTSTP 信号终止进程
  • Ctrl + \ :发送 SIGQUIT 信号终止进程
  • 在另一个终端中键入 kill 21302,这里的 21302 是要终止进程的 pid,作用是发送 SIGKILL 信号将其终止(这就是无法修改 SIGKILL 处理方式的原因,需保留一个杀死进程的最终手段)

signal.c

signal 系统调用

直观地把握了信号的效果后,再来深入它的实现细节,Linux 0.11 中只实现了 SIGKILL 信号无法被捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Line 58
int sys_signal(int signum, long handler, long restorer)
{
struct sigaction tmp;
// 判断信号值是否在 1 ~ 32 之间,且 SIGKILL 信号不能被捕获
if (signum<1 || signum>32 || signum==SIGKILL)
return -1;
// 设置新信号处理方式的 sigaction 结构体
tmp.sa_handler = (void (*)(int)) handler;
tmp.sa_mask = 0;
tmp.sa_flags = SA_ONESHOT | SA_NOMASK;
tmp.sa_restorer = (void (*)(void)) restorer;
// 获取当前信号对应的处理函数
handler = (long) current->sigaction[signum-1].sa_handler;
// 将当前信号对应的进程信号处理结构体修改为新结构体
current->sigaction[signum-1] = tmp;
return handler; // 返回之前的处理函数
}

该函数的三个参数分别为:

  • signum:信号值
  • handler:信号处理函数指针
  • restorer:恢复函数指针,该函数由 libc 库提供,用于在信号处理程序结束后恢复系统调用返回时几个寄存器的原有值以及系统调用的原返回值

各信号值及 sigaction 结构体定义在 signal.h 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Line 12
#define SIGHUP 1 // 挂断控制终端或进程
#define SIGINT 2 // 键盘中断
#define SIGQUIT 3 // 键盘退出
#define SIGILL 4 // 非法指令
#define SIGTRAP 5 // 跟踪断点
#define SIGABRT 6 // 异常结束
#define SIGIOT 6 // 异常结束
#define SIGUNUSED 7 // 未使用
#define SIGFPE 8 // 协处理器错误
#define SIGKILL 9 // 终止进程
#define SIGUSR1 10 // 用户信号 1
#define SIGSEGV 11 // 无效的内存引用
#define SIGUSR2 12 // 用户信号 2
#define SIGPIPE 13 // 管道写出错,读端全关闭
#define SIGALRM 14 // 定时器警报
#define SIGTERM 15 // 进程终止
#define SIGSTKFLT 16 // 栈出错
#define SIGCHLD 17 // 子进程状态改变
#define SIGCONT 18 // 恢复进程继续执行
#define SIGSTOP 19 // 暂停进程执行
#define SIGTSTP 20 // tty 发出的停止进程信号
#define SIGTTIN 21 // 后台进程请求输入
#define SIGTTOU 22 // 后台进程请求输出

// Line 37
#define SA_NOCLDSTOP 1 // 当子进程处于停止状态,就不对 SIGCHLD 信号做处理
#define SA_NOMASK 0x40000000 // 允许在指定信号处理程序中再次收到该信号
#define SA_ONESHOT 0x80000000 // 信号句柄一旦被调用过就恢复默认处理函数

// Line 45
#define SIG_DFL ((void (*)(int))0) // 默认处理程序
#define SIG_IGN ((void (*)(int))1) // 忽略信号对应的处理程序
typedef unsigned int sigset_t;

struct sigaction {
void (*sa_handler)(int); // 信号处理程序指针
sigset_t sa_mask; // 指出当前信号处理程序执行期间需要被屏蔽的信号
int sa_flags; // 从 37 行的三个定义中选出
void (*sa_restorer)(void); // 恢复函数指针,由 libc 提供
};

每个进程都有自己的 task_struct 任务结构体(定义在 include/linux/sched.h),结构体中就包含了 32 个这样的 sigaction 结构体(禁止套娃),每个 sigaction 结构体对应一个信号。当有信号到来时,CPU 就是通过当前任务 task_struct 中元素个数为 32 的 sigaction 结构体数组找到对应信号的处理方式的。当然 0.11 版本中,Linux 只实现了 22 个信号


0.11 版本中的信号处理流程

还记得在 kernel(一)介绍 system_call.s 部分时 ret_from_sys_call 的代码吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// kernel/system_call.s Line 101
ret_from_sys_call:
movl current,%eax /* 取当前任务的结构体指针 */
cmpl task,%eax /* 与 task[0] 的地址进行比较,判断是否为进程 0 */
je 3f /* 如果是进程 0,就不需要进行信号处理,直接返回 */
cmpw $0x0f,CS(%esp) /* 判断原调用程序的代码段选择子是否为 0x0f(RPL=3,LDT,代码段) */
jne 3f /* 来确定是否为用户任务,如果是某个中断服务程序则直接返回 */
cmpw $0x17,OLDSS(%esp) /* 如果原调用程序的堆栈选择符不在用户(局部)段中,也直接返回 */
jne 3f /* 以上为信号的预识别处理 */
movl signal(%eax),%ebx /* ebx = 当前任务信号位图 */
movl blocked(%eax),%ecx /* ecx = 阻塞(屏蔽)信号位图 */
notl %ecx /* 按位取反得到允许的信号位图 */
andl %ebx,%ecx /* 与当前任务信号位图按位与,得到本次中断处理后的信号位图 */
bsfl %ecx,%ecx /* 从 0 位开始扫描位图,遇到有 1 的位将其偏移(0-31 位)保存在 ecx 中 */
je 3f /* 如果没有信号则直接返回 */
btrl %ecx,%ebx /* 将 ebx 第 ecx 位复位(ebx 为原当前任务信号位图) */
movl %ebx,signal(%eax) /* 重新设置当前任务位图 */
incl %ecx /* 将信号值范围调整为 1 ~ 32 */
pushl %ecx /* 压栈作为 do_signal 的参数 */
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds /* 恢复现场 */
iret /* 中断返回 */

进程每次调用系统调用或发生时钟等中断时,在结束部分都会来到这里,进行信号的处理。如果进程收到了信号,则 do_signal 函数就会把信号处理函数指针插入到用户堆栈中。如此一来,当前 系统调用/中断 结束返回(iret)后就会立即执行信号处理函数(用户态下)。信号处理函数执行结束后,执行 ret 指令,执行流来到 sa_restorer 指向的恢复程序(sigaction 结构体最后一个字段,由 libc 提供),该程序将 CPU 及寄存器的状态恢复到系统调用后信号处理前的状态,仿佛没有执行过信号处理函数一样。最后 sa_restorer 通过 ret 指令回到原用户程序继续执行,流程如图:

信号处理


do_signal 的实现

do_signal 的参数是系统调用等压入的所有信息,其中 signr 是接收到的信号值,eax 是系统调用的返回值,其余都是保存的用户态的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Line 92
void do_signal(long signr,long eax, long ebx, long ecx, long edx,
long fs, long es, long ds,
long eip, long cs, long eflags,
unsigned long * esp, long ss)
{
unsigned long sa_handler;
long old_eip=eip; // 将原用户态 eip 保存在变量 old_eip 中
// 从当前任务 sigaction 结构体数组中取出信号值对应的 sigaction 结构体
struct sigaction * sa = current->sigaction + signr - 1;
int longs;
unsigned long * tmp_esp;

sa_handler = (unsigned long) sa->sa_handler; // 将信号处理函数指针保存在 sa_handler 中
if (sa_handler==1) // 如果信号处理函数是 SIG_IGN,表示忽略该信号,直接返回
return;
if (!sa_handler) { // 如果信号处理函数是 SIG_DFL,表示按默认方式处理
if (signr==SIGCHLD) // 如果信号值为 SIGCHLD,也不作处理,直接返回
return;
else // 否则终止进程,故默认处理方式一般效果是终止进程
do_exit(1<<(signr-1));
}
if (sa->sa_flags & SA_ONESHOT) // 如果信号处理函数只需被调用一次,则将其置为 NULL
sa->sa_handler = NULL; // 注意上面已经将 sa->sa_handler 保存在 sa_handler 里了
*(&eip) = sa_handler; // 将原用户程序返回地址替换为信号处理函数的地址
// 如果允许处理信号过程中再次收到该信号,longs 为 7,否则为 8
longs = (sa->sa_flags & SA_NOMASK)?7:8;
*(&esp) -= longs; // 在用户栈中开拓 4 * longs 大小的空间,用于存放 longs 个数据
verify_area(esp,longs*4); // 检查内存使用情况,如果存在内存超界,则分配新内存页等
tmp_esp=esp; // 获取现在的用户栈栈顶指针的值,准备在用户栈中放入数据
put_fs_long((long) sa->sa_restorer,tmp_esp++); // 存入恢复程序的地址
put_fs_long(signr,tmp_esp++); // 存入信号值
if (!(sa->sa_flags & SA_NOMASK)) // 如果不允许处理信号过程中再次收到该信号
put_fs_long(current->blocked,tmp_esp++); // 还需存入信号屏蔽码
put_fs_long(eax,tmp_esp++); // 存入系统调用返回值
put_fs_long(ecx,tmp_esp++); // 依次存入 ecx,edx,eflags,old_eip
put_fs_long(edx,tmp_esp++);
put_fs_long(eflags,tmp_esp++);
put_fs_long(old_eip,tmp_esp++);
current->blocked |= sa->sa_mask; // 当前进程的阻塞码添上 sa_mask 中的屏蔽位
}

用两张图来总结一下 do_signal 带来的一些改变。执行前,内核堆栈与用户堆栈情况如下:

do_signal之前

执行后,变成了这样:

do_signal之后


sa_restorer 恢复函数

因此调用完 do_signal 并且 iret 中断返回后,就会到用户态去执行信号处理函数,且 esp 指向 sa_restorer。信号处理函数通过 ret 指令,就会去到 sa_restorer 执行恢复函数,该函数在 Libc-2.2.2 函数库中有定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 如果没有屏蔽码,使用该函数作为恢复函数 */
sig_restore:
addl $4,%esp /* 丢弃 signr */
popl %eax /* 系统调用返回值还原到 eax */
popl %ecx /* 还原 ecx,edx */
popl %edx
popfl /* 恢复 eflags */
ret

/* 如果有屏蔽码,使用该函数 */
masksig_restore:
addl $4,%esp
call ssetmask /* 设置信号屏蔽码 */
addl $4,%esp /* 丢弃屏蔽码 */
popl %eax
popl %ecx
popl %edx
popfl
ret

ret 后回到原用户程序继续执行,一次信号处理就算完成了


signal.c 剩余部分

实际上,设置信号处理句柄的函数除了 signal 外,还有 sigaction。与 signal 不同的是,给 sigaction 传参时,除了信号值外,还需要两个 sigaction 结构体,一个用于设置新信号处理结构体,一个用于接收原信号处理结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Line 73
int sys_sigaction(int signum, const struct sigaction * action,
struct sigaction * oldaction)
{
struct sigaction tmp;
// 信号值在 1 ~ 32 之前,且 SIGKILL 不能被捕获
if (signum<1 || signum>32 || signum==SIGKILL)
return -1;
tmp = current->sigaction[signum-1]; // 获取信号对应结构体
get_new((char *) action,
(char *) (signum-1+current->sigaction)); // 设置新信号处理结构体
if (oldaction) // 如果 oldaction 非空
save_old((char *) &tmp,(char *) oldaction); // 则保存原信号处理结构体
// 如果允许信号在自己的信号处理过程中收到,则屏蔽码为 0
if (current->sigaction[signum-1].sa_flags & SA_NOMASK)
current->sigaction[signum-1].sa_mask = 0;
else // 否则设置屏蔽本信号
current->sigaction[signum-1].sa_mask |= (1<<(signum-1));
return 0;
}

// Line 38
// get_new 与 save_old 的作用都是将结构体将数据从 from 拷贝至 to
static inline void save_old(char * from,char * to)
{
int i;

verify_area(to, sizeof(struct sigaction));
for (i=0 ; i< sizeof(struct sigaction) ; i++) {
put_fs_byte(*from,to); // 从内核空间取 1 字节数据放到用户空间
from++;
to++;
}
}

static inline void get_new(char * from,char * to)
{
int i;

for (i=0 ; i< sizeof(struct sigaction) ; i++)
*(to++) = get_fs_byte(from++); // 将 1 字节数据从用户空间取到内核空间
}

余下的部分定义了一些系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Line 7
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/segment.h>
#include <errno.h>
#include <signal.h>

volatile void do_exit(int error_code);

// 获取当前任务信号屏蔽码
int sys_sgetmask()
{
return current->blocked;
}

// 设置当前任务信号屏蔽码,返回原屏蔽码
int sys_ssetmask(int newmask)
{
int old=current->blocked;

current->blocked = newmask & ~(1<<(SIGKILL-1));
return old;
}

// 检测并获取进程收到但被屏蔽的信号,0.11 中没有实现
int sys_sigpending()
{
return -ENOSYS;
}

// 临时把进程信号屏蔽码替换为给定的屏蔽码,0.11 中没有实现
int sys_sigsuspend()
{
return -ENOSYS;
}