天天看点

2017-2018-1 20155227 《信息安全系统设计基础》第十四周学习总结2017-2018-1 20155227 《信息安全系统设计基础》第十四周学习总结

2017-2018-1 20155227 《信息安全系统设计基础》第十四周学习总结

找出全书你认为学得最差的一章,深入重新学习一下,要求(期末占5分):
•总结新的收获
•给你的结对学习搭档讲解或请教,并获取反馈
•参考上面的学习总结模板,把学习过程通过博客(随笔)发表,博客标题“学号 《信息安全系统设计基础》第十四周学习总结”,博客(随笔)要通过作业提交,截至时间本周日 23:59。
           

本书有很多章的内容都很难懂,但自我感觉学得最差的是第八章内容。

教材学习内容总结

第8章 异常控制流

现代系统通过使

控制流

发生突变对这些情况做出反应。我们称这种突变为

异常控制流( Exceptional Control Flow,ECF)

异常控制流发生在系统的各个层次。

理解

ECF

很重要

  • 理解

    ECF

    将帮助你理解重要的系统概念。
  • 理解

    ECF

    将帮助你理解应用程序如何与操作系统交互
    • 通过

      陷阱(trap)

      或者

      系统调用(system call)

      ECF

      形式,向操作系统请求服务。
  • 理解

    ECF

    将帮助你编写有趣的应用程序
  • 理解

    ECF

    将帮助你理解并发
  • 理解

    ECF

    将帮助你理解软件异常如何工作。

8.1 异常

异常是异常控制流的一种,一部分由

硬件

实现,一部分由

操作系统

实现。

异常(exception)就是

控制流

的突变,用来响应处理器状态的某些变化。

  • 状态变化又叫做

    事件(event)

    • 事件可能与当前执行指令有关
      • 存储器缺页,算数溢出
    • 也可能与当前执行指令无关
      • I/O请求

      • 定时器产生信号
  • 通过异常表(exception table) 的跳转表,进行一个间接过程调用,到专门设计处理这种事件的操作系统子程序

    (异常处理程序(exception handler))

  • 异常处理完成后,根据事件类型,会有

    三种情况

    • 返回当前指令,即发生事件时的指令。
    • 返回没有异常,所执行的下一条指令
    • 终止被

      中断

      的程序
8.1.1 异常处理
  • 为每个

    异常

    分配了一个非负的

    异常号(exception number)

    • 一些号码由处理器设计者分配
    • 其他号码由操作系统内核的设计者分配。
  • 系统启动时,操作系统分配和初始化一张称为

    异常表

    的跳转表。
    • 条目k包含异常k的处理程序的地址。
  • 异常表

    的地址放在叫

    异常表基址寄存器的特殊CPU寄存器中。)

  • 异常

    类似

    过程调用

    ,不过有以下不同
    • 过程调用

      ,跳转到处理程序前,处理器将返回地址压入栈中。对于异常,返回地址是当前,或下一跳指令。
    • 处理器会把额外的处理器状态压入栈中。
    • 如果控制一个用户程序到内核,那么所有这些项目会被压入内核栈中,而是

      用户栈

    • 异常处理程序运行在

      内核模式

      下,这意味他们对所有系统资源有完整访问权限。
8.1.2 异常的类别

异常分为一下四类:

中断(interrupt),陷阱(trap)

,

故障(fault)

终止(abort)

前者可以叫

异步中断/异

常或

外中断

,后三个可以叫

同步中断/异常

  • 中断
    • 中断

      异步

      发生,是来自处理器外部的

      I/O设备

      的信号的结果。硬件中断不是由任何一条专门的指令造成,从这个意义上它是异步的。
    • 硬件中断的

      异常处理

      程序通常称为

      中断处理程序(interrupt handle

      • I/O设备

        通过向处理器芯片的一个引```脚发信号,并将异常号放到系统总线上,以触发中断。
      • 在当前指令执行完后,处理器注意到中断引脚的电压变化,从系统总线读取异常号,调用适当的中断处理程序。
      • 当处理程序完成后,它将控制返回给下一条本来要执行的指令。
  • 剩下的异常类型(陷阱,故障,终止)是同步发生,执行当前指令的结果。我们把这类指令叫做

    故障指令(faulting instruction)

    .
  • 陷阱

    系统调用

    • 陷阱

      是有意的异常,是执行一个指令的结果。也会返回到下一跳本来要执行的指令。
    • 陷阱

      最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做

      系统调用

      • 用户程序经常需要向

        内核

        请求服务。
        • 1.

          读文件(read)

        • 2.

          创建进程(fork)

        • 3.新的程序

          (execve)

        • 4.终止当前进程(

          exit)

      • 为了运行对这些内核服务的受控访问,处理器提供了一条特殊的

        syscall n

        的指令
      • 系统调用

        是运行在

        内核模式

        下,而普通调用是

        用户模式

        下。
  • 故障
    • 故障

      由错误引起,可能被故障处理程序修正。
      • 如果能被

        修正

        ,返回引起故障的指令。
      • 否则返回

        abort

        例程,进行终结。
  • 终止
    • 终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如

      DRAM

      SRAM

      被损坏。
    • 终止处理程序从不将控制返回给应用程序。返回一个

      abort

      例程。
8.1.3 Linux/IA32 系统中的异常
  • 有高达

    256

    种不同的异常
    • 0~31

      Intel

      架构师定义的异常,对任何

      IA32

      系统都一样。
    • 23~255

      对应操作系统定义的中断和陷阱。
      • Linux/IA32

        故障和终止
    • 除法错误
    • 一般保护故障缺页
    • 机器检查
      • Linux/IA32

        系统调用
  • IA32

    系统中,系统调用是通过一条称为

    int n

    的陷阱指令完成,其中

    n

    可能是

    IA32异常

    256

    个条目中任何一个索引,历史中,系统调用是通过异常

    128(0x80)

    提供的。
  • C程序

    可用

    syscall函数

    来直接调用任何系统调用
    • 实际上没必要这么做
    • C库

      提供了一套方便的

      包装函数

      。这些包装函数将参数打包到一起,以适当的

      系统调用号

      陷入内核,然后将

      系统调用

      的返回状态传递回

      调用函数

    • 我们将系统调用与他们相关联的包装函数称为系统级函数。

研究程序如何使用

int指令

直接调用

Linux

系统调用是很有趣的。所有到

Linux

系统调用的参数都是通过通用寄存器而不是栈传递。

惯例

  • %eax

    包含系统调用号

    -

    %ebx,%ecx,%edx,%esi,%edi,%ebp

    包含六个任意的参数。
  • %esp

    不能使用,进入内核模式后,内核会覆盖它。
  • 系统级函数写的

    hello world

int main()
{
    write(1,"hello,world
",13);
    exit(0);
}
           
  • 汇编写的

    hello world

string:
        "hello world
"
main:
        movl $4,%eax
        movl $1,%ebx
        movl $String,%ecx
        movl $len,%edx
        int $0x80

        movl $1,%eax
        movl $0,%ebx
        int $0x80
           

8.2 进程

  • 异常

    是允许操作系统提供进程的概念的基本构造快,进程是计算机科学中最深刻,最成功的概念之一。
    • 假象

      ,觉得我们的程序是系统中唯一运行着的程序。我们的程序好像独占处理器和存储器。
    • 这些

      假象

      都是通过进程概念提供给我们的。
  • 进程

    经典定义:一个执行中的程序实例.
    • 系统中每个程序都是运行某个进程的上下文中的。
      • 上下文

        是由程序正确运行所需的状态组成。
      • 这个状态包括存储器中的代码和数据,它的

        通用目的寄存器

        程序计数器

        环境变量

        等。
  • 进程

    提供的

    假象

    • 一个独立的

      逻辑控制流

    • 一个私有的

      地址空间

8.2.1 逻辑控制流
  • PC

    值的序列叫做

    逻辑控制流

    ,或者简称

    逻辑流

8.2.2 并发流
  • 逻辑流

    也有不同的形式。
    • 异常处理程序

      ,进程,信号处理程序,线程和

      Java

      进程都是逻辑流的例子。
  • 一个逻辑流的执行在执行上与另一个流重叠,称为

    并发流

    ,这两个流被称为

    并发地运行

    • 更准确地说,

      流X和Y互相并发

  • 多个流并发执行的一般现象称为

    并发

    • 一个进程和其他进程轮流执行的概念称为

      多任务

    • 一个进程执行它的控制流的一部分的每一时间段叫做

      时间片

    • 因此,多任务 又叫

      时间分片

  • 并发的思想与流运行的处理器核数与计算机数无关。
    • 如果两个流在时间上重叠,即使运行在同一处理器,也是并发。
    • 并行流

      是并发流的一个真子集。
      • 两个流并发地运行在不同的处理器核或者计算机上,我们称为

        并行流

      • 它们并行地运行,且并行地执行
8.2.3 私有地址空间

进程

为个程序好像独占了

系统地址空间

  • 一个进程为每个程序提供它自己的

    私有地址空间

  • 不同系统一般都用

    相同

    的结构。
8.2.4 用户模式和内核模式

处理器提供一种机制,限制一个应用程序可以执行的指令以及它可以访问的地址空间范围。这就是

用户模式和内核模式

  • 处理器通过控制寄存器中的一个模式位来提供这个功能。
    • 该寄存器描述了进程当前享有的特权。
      • 设置了模式位后,进程就运行在

        内核模式

        中(有时也叫超级用户模式)
        • 内核模式

          下的进程可以执行指令集的任何指令,访问系统所有存储器的位置。
      • 没有设置模式位时,进程运行在用户模式。
        • 用户模式

          不允许程序执行特权指令。
          • 比如停止处理器,改变模式位,发起一个

            I/O操作

        • 不允许

          用户模式

          的进程直接引用地址空间的内核区代码和数据。
        • 任何尝试都会导致保护故障。
        • 用户通过系统调用间接访问内核代码和数据。
    • 进程从用户模式转变位内核模式的方法
      • 通过中断,故障,陷入系统调用这样的异常。
      • 在异常处理程序中会进入内核模式。退出后,又返回用户模式。
  • Linux

    提供一种聪明的机制,叫

    /proc文件系统

    • 允许用户模式访问

      内核数据

      结构的内容。
    • /proc

      文件将许多内核数据结构输出为一个用户程序可以读的文本文件的

      层次结构

      • CPU类型(/proc/cpuinfo)

      • 特殊进程使用的

        存储器段(‘/proc//maps’)

    • 2.6 版本引入

      Linux内核

      引入

      /sys文件系统

      • 输出关于

        系统总线和设备

        的额外的底层信息。
8.2.5 上下文切换

操作系统内核使用一种称为上下文切换的 较高层次 的异常控制流来实现多任务。

  • 上下文切换机制建立在之前讨论的较低层次异常机制上的。

内核为每个进程维护一个上下文。

  • 上下文

    就是重新启动一个被抢占的进程所需的状态。
    • 由一些

      对象的值

      组成
      • 通用目的寄存器
      • 浮点寄存器
      • 程序计数器(

        PC

        )
      • 用户栈
      • 状态寄存器
      • 内核栈
      • 各种内核数据结构
        • 描绘地址空间的

          页表

        • 包含当前进城信息的

          进程表

        • 进程已打开文件信息的

          文件表

  • 在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定叫做

    调度(shedule)

    ,由内核中称为

    调度器(scheduler)

    的代码处理的。
    • 当内核选择一个新的进程运行时,我们就说内核调度了这个进程。
  • 当调度进程时,使用一种上下文切换的机制来控制转移到新的进程
    • 保存当前进程的

      上下文

    • 恢复某个先前被抢占的进程被保存的

      上下文

    • 将控制传递给这个新恢复的进程
  • 什么时候会发生上下文切换
    • 内核代表用户执行系统调用。
      • 如果系统调用因为某个事件阻塞,那么内核可以让当前进程休眠,切换另一个进程。
      • 或者可以用

        sleep系统调用

        ,显式请求让调用进程休眠。
      • 即使系统调用没有阻塞,内核可以决定执行

        上下文切换

    • 中断也可能引发

      上下文切换

      • 所有系统都有某种产生周期性定时器中断的机制,典型为

        1ms

        ,或

        10ms

      • 每次定时器中断,内核就能判断当前进程运行了足够长的时间,切换新的进程。

8.3 系统调用错误处理

  • Unix系统级函数

    遇到错误时,他们典型地返回

    -1

    ,并设置全局变量

    errno

    来表示什么出错了。
if((pid=fork()<0){
        fprintf(stderr,"fork error: %s
", strerror(errno));
        exit(0);
}
           
  • strerror

    函数返回一个文本串,描述了个某个

    errno

    值相关联的错误。

8.4 进程控制

8.4.1 获取进程ID
#include<sys/types.h>
#include<unistd.h>

pid_t getpid(void);
pid_t getppid(void);
           
  • PID

    是每个进程唯一的正数。
  • getpid()

    返回调用进程的

    PID

    getppid()

    返回它的父进程的

    PID

  • 返回一个类型

    pid_t

    的值,在

    Linux

    系统下在

    type.h

    被定义为

    int

8.4.2 创建和终止进程

进程总是处于下面

三种状态

  • 运行

    。进程要么在

    CPU

    中执行,要么等待执行,最终被内核调度。
  • 停止

    。进程的执行被

    挂起

    ,且不会被调度。
    • 收到S

      IGSTOP,SIGTSTP,SIDTTIN或者SIGTTOU信号

      ,进程就会停止。
    • 直到收到一个

      SIGCONT信号

      ,在这个时刻,进程再次开始运行。
    • 信号是一种

      软件中断

      的形式。
  • 终止

    。进程永远停止。
    • 收到一个信号。信号默认行为是终止进程。
    • 从主程序返回
    • 调用

      exit函数

      • exit函数

        status

        退出状态来终止进程(另一种设置方式在

        main中return

        )

子进程

父进程通过调用

fork函数

创建一个新的运行

子进程

#include<sys/types.h>
#include<unistd.h>

pid_t fork(void);
返回:子进程返回0,父进程返回子进程的PID,如果出错,返回-1;
           

新创建的

子进程

几乎但不完全与

父进程

相同。

  • 子进程

    得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝。
    • 包括文本,

      数据和bss段

      ,堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。
    • 意味着当父进程调用

      fork

      时,子进程可以读写父进程中打开的任何文件。
    • 父进程和新创建的子进程之间最大的区别在于有不同的

      PID

  • fork()函数

    会第一次调用,返回两次,一次在

    父进程

    ,一次在

    子进程

    • 返回值用来明确是在父进程还是在子进程中执行。
  • 调用一次,返回两次
  • 对于具有多个

    fork实例

    的需要仔细推敲了
  • 并发执行
    • 父进程

      子进程

      是并发运行的独立进程。
    • 内核

      可能以任意方式觉得执行他们的顺序。
    • 不能对不同进程中指令的交替执行做任何假设。
  • 相同但是独立的地址空间
    • 在刚调用时,几乎什么都是相同的。
    • 但是它们都有自己的私人空间,之后对x的改变是相互独立的。
  • 共享文件
    • 父进程

      子进程

      都把他们的输出显示在屏幕上。
    • 子进程

      继承了

      父进程

      所有

      打开的文件

8.4.3 回收子进程

当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终结的状态,知道被它的父进程

回收(reap)

当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。

一个终止了但还未被回收的进程叫做

僵死进程

如果父进程没有回收,而终止了,那么内核安排

init

进程来回收它们。

  • init进程

    PID位1

    ,在系统初始化时由内核创建的。
  • 长时间运行的程序,如

    shell

    ,服务器,总是应该回收他们的僵死子进程

一个进程可以通过调用

waitpid函数

来等待它的子进程终止或停止

#include<sys/types.h>
#include<sys/wait.h>

pid_t waitpid(pid_t pid ,int *status, int options);
返回:如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1.
           

waitpid函数有点复杂。

默认(option=0)时

waitpid

挂起调用进程的执行,知道它的等待集合中的一个子进程终止,如果等待集合的一个子进程在调用时刻就已经终止,那么

waitpid

立即返回。在这两种情况下,

waitpid

返回导致

waitpid

返回的已终止子进程的

PID

,并且将这个已终止的子进程从系统中去除。

  • 判断等待集合的成员

    等待集合的成员通过参数

    pid

    确定
    • 如果

      pid>0

      ,那么等待集合就是一个独立的子进程,它的进程

      ID

      等于

      PID

    • 如果

      pid=-1

      ,那么等待集合就是由父进程所有的子进程组成的。
    • waitpid函数

      还支持其他类型的等待集合,包括

      UNIX进程

      组等,不做讨论。
  • 修改默认行为

    可以通过将

    options

    设置为常量

    WHOHANG

    WUNTRACED

    的各种组合,修改默认行为。
    • WHOHANG: 如果等待集合中的任何子进程都还没有终止,那么立即返回(返回值为0)
      • 默认的行为返回已终止的子进程。
      • 当你要检查已终止和被停止的子进程,这个选项会有用。
    • WUNTRACED:挂起调用进程的执行,知道等待集合中的一个进程变为已终结或被停止。
      • 返回的

        PID

        为导致的已终止或被停止的子进程

        PID

        ·
      • 默认的行为是挂起调用进程,直到有子进程终止。
    • WHOHANG|WUNTRACED: 立即返回,如果等待集合中没有任何子进程被停止或已终止,那么

检查已回收子进程的退出状态

  • 如果

    status

    参数是非空的,那么

    waitpid

    就会在

    status

    参数中放上关于导致返回的子进程的状态信息。

    wait.h

    头文件定义解释

    status

    参数的几个宏(函数宏):
    • WIFEXITED(status) : 如果子进程通过调用

      exit

      或者一个返回

      (return)

      正常终止,就返回真。
    • WEXITSATUS(status): 返回一个正常终止的子进程的退出状态。只有在WIFEXITED定义为真是,才会定义这个状态。
    • WIFSIGNALED(status): 如果子进程是因为一个未被捕获的信号终止的,那么就返回真
    • WTERMSIG(status): 返回导致子进程终止的信号的数目,只有在WIFSIGNALED返回真时,才会定义这个状态。
    • WIFSTOPPED(status): 如果引起返回的子进程当前是被停止的,那么就返回真。
    • WSTOPSIG(status): 取得引发子进程暂停的信号代码,只有在

      WIFSTOPPED

      为真,才定义这个状态。
  • 错误条件

    • 调用进程没有子进程,那么

      waitpid

      返回

      -1

      ,并且设置

      errno

      ECHILD

    • 如果

      waitpid

      函数被一个信号中断,那么它返回

      -1

      ,并且设置

      errno

      EINTR

  • wait 函数

wait函数是

waitpid函数

的简单版本:

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int *status);
           

调用

wait(&status)

等价于调用

waitpid(-1,&status,0)

  • waitpid

    实例,按顺序回收

    僵死进程

8.4.4 让进程休眠
  • sleep函数

    将一个进程挂起一段指定时间
#include <unistd.h>

unsigned int sleep (unsigned int secs);
返回:还要休眠的描述
           
  • pause

    让调用进程休眠,知道该进程收到一个信号
#include<unistd.h>

int pause(void);
           
8.4.5 加载并运行一个程序

execve函数在当前进程的上下文中加载并运行了一个新程序。

#include <unistd.h>

int execve(const char *filename,const char *argv[],const char *envp[]);
           

execve函数加载并运行可执行目标文件

filename

,且带参数

argv

和环境变量列表

envp

只有当出现错误时,

execve

才会返回到调用程序

  • *argv[]

    参数列表数据结构表示
  • 指向一个以

    null

    结尾的指针数组。
  • 每个指针指向一个参数串。
    • 一般来说,

      argv[0]

      是可执行目标文件的名字。
  • *envp[]

    环境列表数据结构表示类似
  • 指向一个以

    null

    结尾的指针数组。
  • 每个指针指向一个环境变量串。
    • 每个串都是形如

      KEY=VALUE

      的 键值对

execve

加载

filename

以后,调用7.9节的启动代码,启动代码设置用户栈。并将控制传递给新程序的主函数。

  • 主函数有如下原型
int main(int argc,char **argv,char **envp);
int main(int argc,char *argv[],char *envp[]);
           
  • 当开始执行时,用户栈如图。
  • argc

    : 命令行参数个数
  • argv

    : 命令行指针数组的地址
  • envp

    : 环境变量指正数组的地址

Unix提供一下几个函数来操作环境数组。

  • getenv
#include<stdlib.h>
char *getenv(const char *name);
//getenv函数在环境变量搜索字符串“name=value"。如果找到了,它就返回一个指向value的指针,否则返回NUL
           
  • setenv和unsetenv
#include<stdlib.h>
int setenv(const char *name,const char *newvalue,int overwrite);
//成功返回0,错误返回-1
void unsetenv(const char *name);

//如果环境数组包含一个形如"name=oldvalue"的字符串,那么unsetenv会删除它
//,而setenv会用newvalue代替oldvalue,但是只有在overwirte非零时才会这样。
//如果name不存在,那么setenv就把”name=newvalue"添加进指针数组。
           

fork与execve区别

  • fork

    :在新的子进程运行相同的程序。
    • 新进程是父进程的复制品。
  • execve

    :在当前进程的上下文加载并运行一个新的程序。
    • 覆盖当前进程的地址空间。
    • 但没有创建新进程。
    • 新的程序仍然有相同的

      PID

      ,并且继承了调用

      execve函数

      时已打开的所有文件描述。
8.4.6 利用fork和execve运行程序

Unix shell

Web

服务器 这样的程序大量使用

fork

execve

函数。

shell是一种交互型的应用级程序,代表用户运行其他程序。

  • 最早的

    shell

    sh

    程序。
  • 后面出现了

    csh,tcsh,ksh,bash

  • shell

    执行一系列

    read/evaluate

    • read

      :读取来自用户的命令。
    • evaluate

      :解析命令,并代表用户执行程序。
  • 对字符串的处理。考虑各种

    trick

  • 通过判断命令结尾是否有& - 来决定

    shell

    是否

    waitpid

    。即是否后台运行。
  • 输出一个

    >

    ,等待接收命令。
  • 调用

    eval

    对命令运算。
  • parseline

    解析以空格分割的命令行参数,并将分割后的值丢入

    argv

    中。
    • 如果末尾是

      &

      ,则返回

      1

      。表示后台运行
  • builtin_command

    判断一下是否存在这样的指令。
  • 如果

    bg=0

    ,那么等待程序结束,

    shell

    才会继续执行。
  • parseline

    具体代码就不贴了。

注意这个简单的

shell

是有缺陷的,因为它并不回收它的后台子进程。修改这个缺陷,就必须使用信号。

8.5 信号

研究一种更高层次的软件形式的异常, 也是一种软件中断,称为

Unix

信号,它允许进程中断其他进程。

一个信号就是一条小消息,它通知进程系统中发生一个某种类型的事件。

Linux系统支持

30多种

信号。

每种信号类型对应于某种系统事件

  • 底层的信号。
    • 当底层发生硬件异常,信号通知 用户进程 发生了这些异常。
      • 除以0

        :发送

        SIGILL

        信号。
      • 非法存储器引用:发送

        SIGSEGV

        信号
    • 较高层次的软件事件
      • 键入

        ctrl+c

        :发送

        SIGINT

        信号
      • 一个进程可以发送给另一个进程

        SIGKILL

        信号强制终止它。
      • 子进程终止或者停止,内核会发送一个

        SIGCHLD

        信号给父进程。
8.5.1 信号术语

传送一个信号到目的进程有

两个步骤

  • 发送信号

    : 内核通过更新目的进程上下文的某个状态,就说发送一个信号给目的进程。
  • 发送信号有

    两个原因

    • 内核检测到一个系统事件。比如被零除错误,或者子进程终止。
    • 一个进程调用了

      kill函数

      。显示要求进程发送信号给目的进程。
      • 一个进程可以发信号给它自己。
  • 接收信号: 当目的进程 被内核强迫以某种方式对信号的发送做出反应。目的进程就接收了信号。
    • 进程可以忽略这个信号,终止。
    • 或者通过一个称为

      信号处理程序(signal handler)

      的用户层函数捕获这个信号。

一个只发出而没有被接收的信号叫做

待处理信号(pending signal)

  • 一种类型至多只有一个

    待处理信号

    • 如果一个进程有一个类型为

      k

      的待处理信号。
    • 那么接下来发送到这个进程类型为

      k

      的信号都会被简单的丢弃。

一个进程可以有选择性地阻塞接收某种信号

  • 它任然可以被发送。但是产生的待处理信号不会被接收。
8.5.2 发送信号

Unix系统 提供大量向进程发送信号的机制。所有这些机制都是

基于进程组(process group)

  • 进程组

    • 每个进程都属于一个

      进程组

      • 由一个正整数进程组

        ID

        来标示
        • getpgrp()函数

          返回当前进程的

          进程组ID

          :
#include<unistd.h>
pid_t getpgrp(void);
           
- 默认,一个子进程和它的父进程同属于一个进程组

     - 一个进程可以通过setpgid()来改变自己或者其他进程的进程组。
           
#include<unistd.h>
int setpgid(pid_t pid,pid_t pgid);
如果pid是0 ,那么使用当前进程的pid。
如果pgid是0,那么使用指定的pid作为pgid(即pgid=pid)。

例如:进程15213调用setpgid(0,0)
那么进程15213会 创建/加入进程组15213.
           
  • /bin/kill

    程序发送信号
    • /bin/kill

      可以向另外的进程发送任意的信号。
      • 比如
unix>/bin/kill -9 15213
           

发送信号9(SIGKILL)给进程15213。

- 一个为负的PID会导致信号被发送到进程组PID中的每个进程。
           
unix>/bin/kill -9 -15213
           

发送信号9(SIGKILL)给进程组

15213

中的每个进程。

  • /bin/kill

    的原因是,有些

    Unix shell

    有自己的

    kill命令

  • 从键盘发送信号

    作业(job)

    :对一个命令行求值而创建的进程。
    • 在任何时候至多只有一个前台作业和0个或多个后台作业
      • 前台作业就是需要等待的
      • 后台作业就是不需要等待的
    • 键入unix>ls|sort
      • 创建一个两个进程组成的前台作业。
      • 两个进程通过Unix管道链接。
      • shell为每个作业创建了一个独立的进程组。
        • 进程组ID取自作业中父进程中的一个。

    在键盘输入ctrl-c 会发送一个SIGINT信号到外壳。外壳捕获该信号。然后发送SIGINT信号到这个前台进程组的每个进程。在默认情况下,结果是终止前台作业

    类似,输入ctrl-z会发送一个SIGTSTP信号到外壳,外壳捕获这个信号,并发送SIGTSTP信号给前台进程组的每个进程,在默认情况,结果是停止(挂起)前台作业(还是僵死的)

  • kill函数

    发送信号
    • 进程通过调用

      kill函数

      发送信号给其他进程,类似于

      bin/kill

int kill(pid_t pid, int sig);
           
- pid>0,发送信号sig给进程pid


 - pid<0,发送信号sig给进程组abs(pid)


 - 事例:kill(pid,SIGKILL)
           
  • alarm函数

    发送信号

进程可以通过调用

alarm函数

向它自己

SIGALRM信号

#include<unistd.h>

unsigned int alarm(unsigned int secs);

返回:前一次闹钟剩余的秒数。
           

alarm函数安排内核在

secs

秒内发送一个

SIGALRM信号

给调用进程

  • 如果

    secs=0

    那么不会调度闹钟,当然不会发送

    SIGALRM信号

  • 在任何情况,对

    alarm

    的调用会取消

    待处理(pending)

    的闹钟,并且会返回被取消的闹钟还剩余多少秒结束。如果没有

    pending

    的话,

    返回0

一个例子:

输出

unix> ./alarm
BEEP
BEEP
BEEP
BEEP
BEEP
BOOM!
//handler是一个自己定义的信号处理程序,通过signal函数捆绑。
           
8.5.3 接收信号

信号的处理时机是在从内核态切换到用户态时,会执行

do_signal()函数

来处理信号

当内核从一个异常处理程序返回,准备将控制传递给

进程p

时,它会检查进程p的未被阻塞的

待处理信号的集合(pening&~blocked)

  • 如果这个集合为空,内核将控制传递到p的逻辑控制流的下一条指令。
  • 如果非空,内核选择集合中某个

    信号k(通常是最小的k)

    ,并且强制p接收k。收到这个信号会触发进程某些行为。一旦进程完成行为,传递到p的逻辑控制流的下一条指令。
    • 每个信号类型都有一个预定义的默认类型,以下几种.
      • 进程终止
      • 进程终止并转储存器(

        dump core

        )
      • 进程停止直到被

        SIGCONT

        信号重启
      • 进程忽略该信号
    • 进程可以通过使用

      signal函数

      修改和信号相关联的默认行为。
      • SIGSTOP,SIGKILL

        是不能被修改的例外。
#include<signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum,sighandler_t handler);
           
  • signal函数

    通过下列三种方式之一改变和信号

    signum

    相关联的行为。
    • 如果

      handler

      SIG_IGN

      ,那么忽略类型为

      signum

      的信号
    • 如果

      handler

      SIG_DFL

      ,那么类型为

      signum

      的信号恢复为默认行为。
    • 否则,

      handler

      就是用户定义的函数地址,这个函数称为信号处理程序
      • 只要进程接收到一个类型为

        signum

        的信号,就会调用

        handler

      • 设置信号处理程序:把函数传递给

        signal

        改变信号的默认行为。
      • 调用信号处理程序,叫捕获信号
      • 执行信号处理程序,叫处理信号
  • 当处理程序执行它的

    return

    语句后,控制通常传递回控制流中进程被信号接收中断位置处的指令。
8.5.4 信号处理问题

当一个程序要捕获多个信号时,一些细微的问题就产生了。

  • 待处理信号

    被阻塞

    • Unix

      信号处理程序通常会阻塞 当前处理程序正在处理 的类型的待处理信号。
  • 待处理信号(被抛弃了)不会排队等待
    • 当有两个同类型信号都是待处理信号时,有一个会

      被抛弃

    • 关键思想:存在一个待处理的信号k仅仅表明至少一个一个信号k到达过。
  • 系统调用可以被中断(在某些

    unix系统

    会出现)
    • read,wait和accept

      这样的系统调用潜在的阻塞一段较长的时间,称为慢速系统调用。
      • 当处理程序捕获一个信号,被中断的慢速系统调用在信号处理程序返回后将不在继续,而是立即返回给用户一个错误条件,并将

        errno

        设置为

        EINTR

用一个后台

回收僵死子进程

的程序,前台读入做例子

  • 1.初始简单利用接收

    SIGCHLD信号

    回收,一次调用只回收一个。
    • 在调用的过程中,又有信号发送过来,但是被阻塞了。之后又被直接抛弃。
    • 如果不处理被阻塞和不会排队等待的问题。会有信号被抛弃。
    • 重要教训:不可以用信号对其他进程中发送的事件计数
    • handle1-code

  • 2.一次调用尽可能的多回收,保证在回收过程中,没有遗漏的信号。
    • handle2-code

  • 3.还存在一个问题,在前台中,某些unix系统(

    Solaris系统

    )的

    read

    被中断后不会自动重启,需要手动重启,

    Linux

    一般会自动重启。
    • 之前

      read

      模块

      code

  • 现在改为如果是

    errno==EINTR

    手动重启。
  • 或者使用

    Signal

    包装函数标准。
8.5.5 可移植的信号处理

不同系统之间,信号处理语义的差异(比如一个被中断的慢速系统调用是重启,还是永久放弃)是

Unix信号系统

的一个缺陷。

为了处理这个问题,

Posix

标准定义了

sigaction函数

,它允许

与Linux

Solaris

这样与

Posix

兼容的系统上的用户,明确指明他们想要的信号处理语义。

#include<signal.h>

int sigaction(int signum,stuct sigaction *act,struct sigaction *oldcat);
//若成功则为1,出错则为-1。
           

sigaction函数应用不广泛,它要求用户设置多个结构条目。

一个更简洁的方式,是定义一个包装函数,称为

Signal

,它调用

sigaction

  • 它的调用方式与

    signal函数

    的调用方式一样。
  • Signal

    包装函数设置了一个信号处理程序,其信号处理语义如下(设置标准):
    • 只有这个处理程序当前正在处理的那种类型被阻塞。
    • 和所有信号实现一样,信号不会排队等待。
    • 只要可能,被中断的系统调用会自动重启
    • 一旦设置了信号处理程序,它就会一直保持,直到

      Signal

      带着

      handler

      参数为

      SIG_IGN或者SIG_DFL

      被调用。
      • 在某些比较老的

        Unix系统

        ,信号处理程序被使用一次后,又回到默认行为。
8.5.6 显示地阻塞和取消阻塞信号

通过sigprocmask函数来操作。

#include<signal.h>

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
           
  • sigprocmask函数

    改变当前已阻塞信号的集合(8.5.1节描述的blocked位向量)。
    • 具体行为依赖how值
      • SIG_BLOCK

        :添加

        set

        中的信号到

        blocked

        中。
      • SIG_UNBLOCK

        : 从

        blocked

        删除

        set

        中的信号。
      • SIG_SETMASK

        :

        blocked=set

    • 如果

      oldset

      非空,

      block

      位向量以前的值会保存到

      oldset

      中。

还有以下函数操作

set

集合

#include<signal.h>

int sigemptyset(sigset_t *set);
//置空
int sigfillset(sigset_t *set);
//每个信号全部填入
int sigaddset(sigset_t *set,int signum);
//添加
int sigdelset(sigset_t *set,int signum);
//删除
//成功输出0,出错输出-1
int sigismember(const sigset_t *set,int signum);
//判断
//若signum是set的成员,输出1,不是输出0,出错输出-1。
           
8.5.7 同步流以避免讨厌的并发错误

如何编写读写相同存储位置的并发流程序的问题,困扰着数代计算机科学家。

  • 流可能交错 的数量是与指令数 量呈指数关系
  • 有些交错会产生正确结果,有些可能不会。

所谓

同步流

就是。以某种方式同步并发流,从而得到 最大的可行

交错的集合

,每个

交错集合

都能得到正确的结果。

如果发生以下情况,会出现同步错误。

  • 父进程执行

    fork函数

    ,内核调度新创建的子进程运行,而不是父进程。
  • 在父进程再次运行前,子进程已经终止,变成僵死进程,需要内核一个

    SIGCHLD

    信号给父进程
  • 父进程处理信号,调用

    deletejob.

  • 调用

    addjob

显然

deletejob

必须在

addjob

之后,不然添加进去的

job

永久存在。这就是同步错误。

这是一个称为竞争(

race

)的经典同步错误的示例。

  • main

    中的

    addjob

    和处理程序中调用deletejob之间存在竞争。
  • 必须

    addjob

    赢得进展,结果才是正确的,否则就是错误的。但是

    addjob

    不一定能赢,所以有可能错误。即为同步错误。
  • 因为内核的调度问题,这种错误十分难以被发现。难以调试。

8.6 非本地跳转

C语言提供一种用户级

异常控制流形式

,称为非

本地跳转(nonlocal jump)

  • 它将控制直接从一个函数转移到另一个当前正在执行的函数。不需要经过正常的调用-返回序列。
  • 非本地跳转是通过

    setjmp

    longjmp函数

    来提供。
#include<setjmp.h>

int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env,int savesigs);//信号处理程序使用
//参数savesigs若为非0则代表搁置的信号集合也会一块保存
           
  • setjmp函数

    env缓冲区

    保存当前调用环境,以供后面

    longjmp

    使用,

    并返回0

    • 调用环境包括程序计数器,栈指针,通用目的寄存器。

8.7 操作进程的工具

  • STRACE(痕迹)

    :打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。
    • 用-static编译

      ,能得到一个更干净,不带有大量共享库相关的输出的轨迹。
  • PS(Processes Status)

    : 列出当前系统的进程(包括僵死进程)
  • TOP

    (因为我们关注峰值的几个程序,所以叫

    TOP):

    打印当前进程使用的信息。
  • PMAP(rePort Memory map of A Process)

    : 查看进程的内存映像信息
  • /proc

    :一个虚拟文件系统,以

    ASCII

    文本格式输出大量内核数据结构。
    • 用户程序可以读取这些内容。
    • 比如,输入"

      cat /proc/loadavg

      ,观察

      Linux系统

      上当前的平均负载。

8.8 小结

  • 异常控制流(ECF)

    发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。
  • 在硬件层,异常是处理器中的事件出发的控制流中的突变。控制流传递给一个异常处理程序,该处理程序进行一些处理,然后返回控制被中断的控制流。
    • 四种不同类型

      的异常:

      中断,故障,终止和陷阱

      • 定时器芯片或磁盘控制器,设置了处理器芯片上的中断引脚时,中断会异步发生。返回到

        Inext

      • 一条指令的执行可能导致故障和终止同时出现。
        • 故障可能返回调用指令。
        • 终止不将控制返回。
      • 陷阱用于系统调用。结束后,返回

        Inext

  • 在操作系统层,内核用

    ECF

    提供进程的基本概念。进程给应用两个重要抽象:
    • 逻辑控制流

    • 私有地址空间

  • 在操作系统和应用程序接口处,有子进程,和信号。
  • 最后,C语言的非本地跳转 完成应用程序层面的异常处理。

第八章课后家庭作业

8.9

进程对 是否并发
AB NO
AC YES
AD YES
BC YES
BD YES
CD YES

8.10

A. 调用一次,返回两次:

 fork

  
B. 调用一次,从不返回:

 execve, longjmp

 
C. 调用一次,返回一次或者多次:

 setjmp
           

8.11

4行

8.12

8行

8.13

->x=2

 

->x=4->x=3

 

满足这种拓扑即可。
           

8.14

主进程只打印一行。
主进程的直接子进程会打印一行,子进程的子进程又打印一行。
所以是3行。
           

8.15

这里的子进程不是exit,而是return,说明两个子进程都要到回到main函数去打印那里的hello。所以是5行。

8.16

输出counter = 2,因为全局变量也是复制的,而不是共享的。

8.17

满足

Hello--->1->Bye

                  

       --->0---->2-->Bye

这种拓扑即可。
           

8.18

画一下进程图就可以知道。

所以ACE是可能的。

8.19

总共会输出2^n行。

8.20

int main

(int argc, char args[]) 

{ execve

("/bin/ls", args, environ); 

//没有错误处理,注意环境变量

return0; 

}


           

8.21

abc或bac,c在ab之后。

8.22

int mysystem(char *command)
 {
     int status;
     char *argv[4];
     char *a0 = "sh";
     char *a1 = "-c";
     if( fork()==0 ) /*子进程*/
     { 
         argv[0] = a0;
         argv[1] = a1;
         argv[2] = command;
         argv[3] = NULL;
         execve("/bin/sh", args, environ);
         return -1; //执行异常
     }
     else{ /*父进程*/
         if( wait(&status) > 0)
         {
             if(WIFEXITED(status) != 0)
                 return WEXITSTATUS(status);
             else return status;
         }
         else return -1; //wait异常
     }
 }
           

8.23

一个可能的原因是,在第一个信号发给父进程之后,父进程进入handler,并且阻塞了SIGUSR2,第二个信号依然可以发送,然而,之后的3
个信号便会被抛弃了。因为是连续发送,所以很可能是没等上下文切换,这5个信号就同时发送了。所以只有2个信号被接收。

           

8.24

#include "csapp.h"

 

#define N 2

int main() 

{  
int status, i; 

   pid_t pid;  
char errorInfo[128];  

/* Parent creates N children */
for

(i=0;i<N;i++)  
if

 ((pid = Fork()) == 0) 

/* Child */
exit(100+i); 

/* Parent reaps N children in no particular order */
while((pid = waitpid(-1, &status, 0)) > 0) { 

       if(WIFEXITED(status))  

          printf("child %d terminated normally with exit status=%d
",  pid, WEXITSTATUS(status)); 

       else if(WIFSIGNALED(status)) 

        { printf("child %d terminated by signal %d: ",pid, WTERMSIG(status) );  

           psignal(WTERMSIG(status), errorInfo); //psignal会打印sig的信息

        } 

    }  


  if(errno != ECHILD)  

        unix_error("waitpid error"); 
           exit(0); 

}

           

8.25

fgets的定义如下:

char *fgets(char *buf, int bufsize, FILE *stream);

参数:

*buf: 字符型指针,指向用来存储所得数据的地址。

bufsize: 整型数据,指明buf指向的字符数组的大小。

*stream: 文件结构体指针,将要读取的文件流。

这个应该是用alarm发送信号给自己,然后在信号处理程序里面做文章。

显然,在tfgets里一开始需要调用fgets。然而,因为五秒时间到了,fgets还没有返回,所以我们必须在处理程序里直接跳转到某个地方进行tfgets的NULL返回。这就需要用到非本地跳转。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>

sigjmp_buf env;
void tfgets_handler(int sig)
 {
     signal(SIGALRM, SIG_DFL);
     siglongjmp(env, 1);
 }

char *tfgets(char *buf, int bufsize, FILE *stream)
 {
     static const int TimeLimitSecs = 5;
     signal(SIGALRM, tfgets_handler)
     alarm(TimeLimitSecs);
     int rc = sigsetjmp(env, 1);
     if(rc == 0) return fgets(buf, bufsize, stream);
     else return NULL; //alarm,time out
 }
           

8.26

1)需要定义一个struct结构体数组,JID是1,那么就是数组的第一个。

结构体中有pid和状态。状态分空,运行,停止等等。

难点是JID和pid的互相查找,有点像hash。

如果不用hash,就是枚举数组寻找pid。所以,结构体数组大小要先设定(最多运行的jobs数目),不能太大,否则会增加复杂度。

2)可以写一个addjob和deletejob函数,以pid为参数。

当然,对于后台运行的进程,要注意竞争。

3)需要写handler函数,当子进程运行完毕或终止,需要用handler函数来回收。

还需要写一些handler来实现发送消息的功能,比如ctrl-z和ctrl-c。

4)jobs就是列出数组中状态不为空的进程。

可以写一个searchJID(pid_t pid)函数,根据某个pid来查询JID。
5)fg和bg倒是比较简单,给子进程发送那些消息,让那些消息重新execve。
           

教材学习中的问题及解决

以下是我和同伴互相解决对方的疑问:

  • 问题1: 如何消除竞争?
  • 问题1解决:可以在

    fork

    之前,阻塞

    SIGCHLD信号

    ,在调用

    addjob

    后取消阻塞。
    • 注意,子进程继承了阻塞,我们要小心地接触子进程中的阻塞。
    • 消除竞争的原则就是,让该赢得竞争的对象在任何情况下都能赢。
  • 问题2: 如何使用非本地跳转来规避正常的调用/返回栈规则。
  • 问题2解决: 非本地跳转通过setjmp和longjmp函数来提供。

上周考试错题总结

结对及互评

点评模板:

  • 博客中值得学习的或问题:
    • xxx
    • xxx
    • ...
  • 代码中值得学习的或问题:
    • xxx
    • xxx
    • ...
  • 其他

本周结对学习情况

-[20155318](http://www.cnblogs.com/lxy1997/)
- 结对照片
- 结对学习内容
    - 教材第八章内容
    - 
    - ...
           

其他(感悟、思考等,可选)

通过这一章的学习,进一步加深了对异常控制流的了解。

代码托管

(statistics.sh脚本的运行结果截图)

学习进度条

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 133/133 1/1 8/8
第三周 159/292 1/3 10/18
第五周 121/413 1/5 10/28
第七周 835/3005 2/7 10/38
第八周 1702/4777 1/8 10/48
第九周 1664/6441 3/11 10/58
第十一周 300/6741 3/14 10/68
第十三周 743/7484 2/16 10/78
第十四周 345/7829 1/17 10/88

尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。

耗时估计的公式

:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。

参考:软件工程软件的估计为什么这么难,软件工程 估计方法

  • 计划学习时间:15小时
  • 实际学习时间:10小时
  • 改进情况:

(有空多看看现代软件工程 课件

软件工程师能力自我评价表)

参考资料

  • 《深入理解计算机系统V3》学习指导
  • ...