Linux0.11信号处理详解

之前在看操作系统信号这一章的时候,一直是云里雾里的,不知道信号到底是个啥玩意儿。。比如在看《Unix环境高级编程》时,就感觉信号是个挺神奇的东西。比如看到下面这段代码:

#include<signal.h>

#include<stdio.h>

#include<unistd.h>

void handler(int sig)

{

  printf("The signal is %d\n",sig);

}

int main()

{

  (void) signal(SIGINT,handler);

  while(1)

  {

    printf("Signal test.\n");

    sleep(1);

  }

}

运行这个程序,当我在屏幕敲入ctrl+c时:

Linux0.11信号处理详解

那个SIGINT就是一个信号,表示来自键盘的中断,用ctrl+c产生。下面我就来详细分析Linux0.11内核对信号是怎么处理的。

在内核中用一个无符号的长整数(32位)中的比特位来表示各种不同信号,在task_struct中有一个变量signal,这个就是信号位图,里面每一个比特位代表一个信号。在Linux0.11版本中定义了22种不同的信号。定义在linux/include/signal.h中:

Linux0.11信号处理详解

当一个进程接收到一个信号时,有三种不同的处理或操作方式。(1)忽略该信号。但有两个信号不能被忽略掉:SIGKILL和SIGSTOP。(2)执行默认操作。内核为每种信号都提供了一种默认信号,通常的默认操作就是终止进程的执行。(3)捕获该信号。我们可以在这里自定义信号处理函数,来以我们喜欢方式处理信号。

那么,我们如何来定义信号处理函数呢?

(我们这里不讨论可靠信号和不可靠信号,在Linux中1-31是不可靠信号,32-64是可靠信号)

可以通过signal()函数或sigaction()函数。其中signal()的声明如下:

void (*signal(int signr,void(*handler)(int)))(int);

signr代表需要捕获的信号,handler代表信号处理函数指针。signal()函数有一个特点:在新句柄(处理函数)被调用执行过一次后,信号处理句柄又会恢复成默认处理句柄。

sigaction()函数采用了sigaction数据结构来保存指定的信号信息。在task_struct中就有struct sigaction sigaction[32]这一字段,这是一个sigaction数组,对应task_struct中的signal信号位图每一个信号的处理方式。sigaction结构如下:

struct sigaction{

  void (*sa_handler)(int);

  sigset_t sa_mask;

  int sa_flags;

  void (*sa_restorer)(void);

}

那个sa_handler就是信号处理句柄,sa_mask表示信号屏蔽码,代表在处理这一信号时需要屏蔽的信号,sa_flags用于指定一些处理信号的选项,定义在include/signal.h:

SA_NOCLDSTOP表示进程处于停止状态,不对信号做处理,SA_NOMASK表示对当前信号不进行屏蔽,即这个时候可以运行信号嵌套。SA_ONESHOT指明信号处理函数一旦被调用过就恢复到默认的信号处理函数去。

sigaction()函数的声明为:

int sigaction(

  int sig,

  struct sigaction *act,

  struct sigaction *oldact

);

sig表示指定的信号,如果act不是NULL时,就把act作为信号sig新的处理方式。当oldact不为空时,oldact返回该信号原来的处理方式。

 

介绍完了有关信号的基本知识,下面我们来具体看看信号时如何被处理的。

首先,你要明白:信号时异步的。进程本身也不知道信号什么时候到达,因此它不必通过什么操作来等待信号的到达。那么进程什么时候处理信号呢?一般是在系统调用发生后,从系统空间返回到用户空间前夕。

我们在系统调用详解那一篇中讲到系统调用的大致实现过程,在系统调用返回之前内核会去检查进程是否有需要处理的信号。System_call.s中的109到119代码如下:

Linux0.11信号处理详解

109表示将当前进程的信号位图放入%ebx中,110表示将当前信号的阻塞位图放入%ecx中,111中将阻塞信号位图每位取反,然后通过112获取许可的信号位图,113从低位开始扫描位图,看是否有1,如果有则ecx保留该位的偏移值(0-31),114如果没有则跳出。115复位该信号,116重新保存signal位图信息,117将信号调整为从1开始的数(1=32),然后通过118将信号值入栈作为调用do_signal的参数之一。最后119调用信号处理程序。

为了方便理解,在介绍do_signal()函数之前,我们先来大致了解一下执行信号处理时内核堆栈和用户态堆栈所发生的一些变化,这对理解整个信号处理有好处。

Linux0.11信号处理详解

我们知道,当进程在执行系统调用时,它会陷入内核态,对应每个进程都有一个内核堆栈和一个用户堆栈,当进程陷入内核态时,它会在内核堆栈保存原来的用户堆栈寄存器的SS和esp,以及代码寄存器cs和eip,其中eip中保存着用户进程的下一个指令。接着把原来的数据段寄存器ds、es、fs放入堆栈。然后把edx、ecx、ebx入栈,这些寄存器通常用来放系统调用的参数,至于系统调用的返回值则放在eax中。记住:上面这些寄存器的值将来都是要恢复的。当系统调用结束后,这些寄存器将依次出栈以使得进程回到用户态的执行流中。

那么如果要进行信号处理,堆栈会发生什么变化呢?当系统调用处理函数运行结束后,内核开始检查该进程有没有需要处理的信号,如果有,则调用do_signal()函数,那么我们来看看do_signal()这个函数到底做了什么。do_signal()函数定义在Kernerl/Signal.c中。

Linux0.11信号处理详解

首先,它把eip的值保存在old_eip中(后面会解释为什么),然后它找到所要处理的信号对应的sigaction结构(第89行),并找到对应的处理句柄,接下来,94—101行检查该信号是不是默认处理方式或者忽略处理方式。第102和103检查是不是SA_ONESHOT处理方式。接下来的从104行开始比较难理解,它让eip指向信号处理句柄地址,如图所示,也就是说此时eip不再指向用户进程中系统调用后的下一条指令了。接下来的代码,将内核堆栈中的eflags,edx,ecx,eax(系统调用返回值)入栈,同时包括old_eip,[blocked],signr,sa_restorer也入栈(记住这里是用户空间堆栈,而不是内核堆栈)。这个时候do_signal()函数的使命就完成了。那么为什么要将这些寄存器入栈呢?当so_signal()执行完成后,System_call.s中的接下来代码是这样的:

Linux0.11信号处理详解

121—127将相应的寄存器出栈(恢复用户空间的现场),然后通过iret指令将cs,eip以及ss,esp等出栈,至此,进程就进入了用户空间运行,由于之前eip已经指向了信号处理程序,于是接下来,信号处理程序将被执行。那么为什么偏偏要在do_signal中把edx,ecx等入栈,而其他几个寄存器并不入栈呢?那是因为在信号处理程序中需要用到eax,ecx,edx等寄存器,因此需要将这些寄存器的值保存起来,将要再“恢复”一次。信号处理程序执行完成后,CPU会通过ret指令把控制器移交给sa_restorer恢复程序去执行,我们就以没有blocked的restorer函数为例:

Linux0.11信号处理详解

你看这个sa_restorer程序就是做一些用户堆栈的清理工作的,将原先入栈的eax,ecx,edx等等出栈,然后通过ret将old_eip恢复到eip中,这样就彻底恢复了原先的进程执行环境了。

你可能感兴趣的:(linux)