Chinaunix首页 | 论坛 | 博客
  • 博客访问: 248923
  • 博文数量: 44
  • 博客积分: 1052
  • 博客等级: 少尉
  • 技术积分: 742
  • 用 户 组: 普通用户
  • 注册时间: 2010-12-17 16:51
文章分类

全部博文(44)

文章存档

2013年(7)

2012年(14)

2011年(23)

分类: LINUX

2013-03-12 22:00:57

本文简单描述linux0.11信号机制的实现

一:有关信号
当进程收到一个信号后,进程根据相关设定调用信号处理函数。

有三类信号处理方式:默认处理方式、忽略信号方式、执行用户设定的信号处理函数。

发送信号的方式:按下相应的键(如CTRL+C)、使用kill命令或函数向指定进程发送信号。

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

当在进程中调用signal(signo, handler)之后,
如果进程收到信号signo,则进程会执行handler指向的函数。

假设有进程A和进程B,当进程A给进程B发送了一个信号后,那进程B的信号处理函数什么时候执行?
若要执行进程B的信号处理函数,则进程B必须处于执行状态。也就是说只有当调度程序调度到了进程B后,
才可能执行进程B的信号处理函数。否则进程B不处于可执行状态,收到了信号也没用。

若进程B自己调用kill函数,给自己发送了一个信号,我们会假定这个信号处理函数会立即执行。
因此在执行系统函数kill之后会立即处理进程B的信号。


从这两点可以意识到,内核需要在时钟中断和系统调用后对当前进程的信号进行处理。
需要在时钟中断时是因为时钟中断会调用schedule函数,因为这是分时系统,
如果进程A给B发了信号,而且现在调度到了B,那理所当然要执行B的信号处理函数。


二:linux0.11的信号机制
以 kill 函数为例来简单说明大致流程, 下面再来详细描述内核中的do_signal函数。

当以kill函数给当前进程发送一个信号之后。
因为这是个系统函数,因此会执行int 0x80进入system_call的入口点
_system_call:
        cmpl    $nr_system_calls-1,    %eax        # %eax保存kill函数的调用号
        ja        bad_sys_call                              # 无效的系统调用
        push    %ds
        push    %es
        push    %fs
        push    %edx
        push    %ecx
        push    %ebx                                      # 相关数据入栈
.....................
        call    _sys_call_table(,%eax,4)               # 执行系统调用, 这里就是 sys_kill 函数了
        pushl    %eax                                      # 系统调用的返回值入栈,也即是 sys_kill 的返回值
......................
        
ret_from_sys_call:
        .....
        pushl    %ecx         # %ecx中保存了信号的信号值。
        call    _do_signal     # 对信号进行处理
        popl    %eax          #  将信号值出栈  
        popl    %eax          #  将系统调用返回值出栈,  也就是sys_kill的返回值存入%eax寄存器
        popl    %ebx
        popl    %ecx
        popl    %edx
        pop    %fs
        pop    %es
        pop    %ds
        iret


可见每次系统调用之后,可能会执行ret_from_sys_call,进而对信号进行处理。

除了在_system_call里会这样, 在一些中断下也会调用ret_from_sys_call,时钟中断就是其中之一。
现在已经知道内核是“何时”来处理进程的信号了。


三:do_signal函数。
do_signal的功能主要是设置了内核的堆栈和应用的用户堆栈,设置好堆栈后,
当执行ret_from_sys_call最下面的iret指令的时候,去自动执行进程的信号处理函数。当信号处理函数执行完成后,又会接着进程的下一条指令去执行。

下图是《Linux0.11内核完全注释》一书里的,很好的显示调用do_signal前后的堆栈变化。

左边的为内核态堆栈,就是在执行call _do_signal之前的堆栈内容。

do_signal执行如下操作
1:将堆栈中的eip值,保存到old_eip中,old_eip就指向了用户程序中即将执行代码
2:将eip执行信号处理函数。这样当执行ret_from_sys_call中的iret时,会执行cs:eip指向的代码,也就是信号处理函数。
3:将用户态堆栈的esp的值,向下移7或8个长字(32位)
4:然后将sa_resotrer, signr等值放入堆栈, 见图右边的用户堆栈。

完成上述操作后,do_signal执行完毕,返回到ret_from_sys_call中,
ret_from_sys_call执行一些pop操作后执行iret指令, 这时会跳转到信号处理函数去执行。
当信号处理函数执行完后,会执行ret操作(函数的返回使用ret,中断的返回使用iret),这时会将sa_restorer存入eip,
因此接下来就会执行sa_restorer

sa_restorer会恢复用户堆栈
__sig_restore:
        addl    $4,    %esp
        popl    %eax              # 将系统调用的返回值存入eax
        popl    %ecx
        popl    %edx
        popfl
        ret   
当执行完popfl之后,明显用户堆栈里面只剩下old_eip了, 因此执行ret,程序就会跳转到cs:old_eip去执行,也就是系统调用的下一条用户指令了。
至此信号处理函数已经执行,系统调用也已返回,用户程序无忧无虑的继续执行。









阅读(3991) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~