之前在看操作系统信号这一章的时候,一直是云里雾里的,不知道信号到底是个啥玩意儿。。比如在看《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时:

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

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

当一个进程接收到一个信号时,有三种不同的处理或操作方式。(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代码如下:

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()函数之前,我们先来大致了解一下执行信号处理时内核堆栈和用户态堆栈所发生的一些变化,这对理解整个信号处理有好处。

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

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

首先,它把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中的接下来代码是这样的:

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

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

Linux0.11信号处理详解的更多相关文章

  1. 【小白学PyTorch】11 MobileNet详解及PyTorch实现

    文章来自微信公众号[机器学习炼丹术].我是炼丹兄,欢迎加我微信好友交流学习:cyx645016617. @ 目录 1 背景 2 深度可分离卷积 2.2 一般卷积计算量 2.2 深度可分离卷积计算量 2 ...

  2. Acunetix 11 配置详解

    Acunetix 扫描配置 Full Scan– 使用Full Scan来发起一个扫描的话,Acunetix会检查所有可能得安全漏洞. High Rish Vulnerabilities–这个扫描选项 ...

  3. Java容器解析系列(11) HashMap 详解

    本篇我们来介绍一个最常用的Map结构--HashMap 关于HashMap,关于其基本原理,网上对其进行讲解的博客非常多,且很多都写的比较好,所以.... 这里直接贴上地址: 关于hash算法: Ha ...

  4. Jmeter 常用函数(11)- 详解 __TestPlanName

    如果你想查看更多 Jmeter 常用函数可以在这篇文章找找哦 https://www.cnblogs.com/poloyy/p/13291704.html 作用 返回测试计划名称 语法格式 ${__T ...

  5. Appium自动化(11) - 详解 Applications 类里的方法和源码解析

    如果你还想从头学起Appium,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1693896.html 前言 Applications 类 ...

  6. 2020了你还不会Java8新特性?(五)收集器比较器用法详解及源码剖析

    收集器用法详解与多级分组和分区 为什么在collectors类中定义一个静态内部类? static class CollectorImpl<T, A, R> implements Coll ...

  7. C++11 并发指南三(Lock 详解)

    在 <C++11 并发指南三(std::mutex 详解)>一文中我们主要介绍了 C++11 标准中的互斥量(Mutex),并简单介绍了一下两种锁类型.本节将详细介绍一下 C++11 标准 ...

  8. C++11 并发指南六(atomic 类型详解四 C 风格原子操作介绍)

    前面三篇文章<C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍)>.<C++11 并发指南六( <atomic> 类型详解二 std::at ...

  9. C++11 并发指南六(atomic 类型详解三 std::atomic (续))

    C++11 并发指南六( <atomic> 类型详解二 std::atomic ) 介绍了基本的原子类型 std::atomic 的用法,本节我会给大家介绍C++11 标准库中的 std: ...

随机推荐

  1. [one day one question] Iscroll 5.0 在chrome上无法滑动

    问题描述: Iscroll 5.0 在chrome上无法滑动,不仅仅在chromePC的开发的时候,在手机上的chrome也有同样的问题,这怎么破? 解决方案: // 关闭 PointerEvent ...

  2. windows使用IPC和文件共享

    远程访问windows资源有很多方式,如果给自己用可以使用ipc或开启共享设置只共享给特定用户.如果给所有人用,可以开启everyone共享和guest账户 { "远程获取Windows资源 ...

  3. 如何用纯 CSS 创作单元素点阵 loader

    效果预览 在线演示 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览.https://codepen.io/comehope/pen/YvBvBr 可交互视频 此 ...

  4. 20145216《java程序设计》课程总结

    20145216<java程序设计>课程总结 每周读书笔记链接汇总 第一周学习总结 20145216<java程序设计>第一周总结 第二周学习总结 20145216<ja ...

  5. 20145314郑凯杰 《Java程序设计》实验四 实验报告

    20145314郑凯杰 <Java程序设计>实验四 实验报告 实验要求 完成实验.撰写实验报告,实验报告以博客方式发表在博客园,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用 ...

  6. LCS最长共同子序列

    2017-09-02 15:06:57 writer:pprp 状态表示: f(n,m)表示s1[0..n]和s2[0..m]从0开始计数,最终结果是f(N-1,M-1)考虑四种情况: 1/ s1[n ...

  7. RabbitMQ入门(4)——路由(Routing)

    这一篇我们将介绍如何订阅消息的一个子集.例如,我们只需要将日志中的error消息存储到日志文件中而将所有日志消息都在控制台打印出来. 绑定(Bindings) 在前面的例子中,我们创建了交换机和队列的 ...

  8. oracle sql - remove a user's all objects

    DECLARE TYPE cst_table_list IS TABLE OF VARCHAR2(40); TYPE cst_list IS TABLE OF VARCHAR2(40); TYPE n ...

  9. 1.JSON 转换对象失败问题 2.spring注入失效

    今天做项目中将一个json 字符串转换为对象,但结果怎么都转换不了!——————最后发现问题,原来是因为这个类我给他添加了带参数的构造器!导致转换失败! 在添加一个无参的构造器就好了! 第二个:今天调 ...

  10. js删除数组中某一项或几项的几种方法

    1:js中的splice方法 splice(index,len,[item])    注释:该方法会改变原始数组. splice有3个参数,它也可以用来替换/删除/添加数组内某一个或者几个值 inde ...