信息安全系统设计基础第十一周学习总结

**学习目标 **

  1. 了解异常及其种类
  2. 理解进程和并发的概念
  3. 掌握进程创建和控制的系统调用及函数使用:fork,exec,wait,waitpid,exit,getpid,getppid,sleep,pause,setenv,unsetenv,
  4. 理解数组指针、指针数组、函数指针、指针函数的区别
  5. 理解信号机制:kill,alarm,signal,sigaction
  6. 掌握管道和I/O重定向:pipe, dup, dup2

Chapter 11:异常控制流

前言

从给处理器加电开始,直到断电为止,程序计数器假设一个值的序列

                a0,a1,,,,,,an-1

其中,每个ak是某个相应的指令Ik的地址。每次从ak到ak+1的过度称为控制转移。这样的控制转移序列叫做处理器的控制流

异常控制流(ECF)

  现代系统通过使控制流发生突变来对系统状态变化做出反应。一般而言的这些突变就称为异常控制流。

学习ECF的原因

  • ECF是操作系统用来实现I/O、进程和虚拟存储器的基本机制。
  • 应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务。
  • 操作系统为应用程序提供了强大的ECF机制,用来创建新进程、以及终止当前进程。
  • ECF是计算机系统中实现并发的基本机制。
  • 它将帮助你理解软件异常如何工作。

8.1 异常

  1. 异常就是控制流中的突变,用来响应处理器状态中的某些变化。
  2. 根据异常的事件的类型,会发生以下三种情况中的一种:

    • 处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的命令。
    • 处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。
    • 处理程序终止被中断的程序。
  3. 在系统启动时(当计算机重启或者加电时),操作系统分配和初始化一张称为异常表的跳转表,使得条目k包含异常k的处理程序的地址。下图为异常表的格式: 信息安全系统设计基础第十一周学习总结_第1张图片

  4. 在运行时,处理器检测到发生了一个事件,并且确定了相应的异常号k。随后,处理器触发异常,方法是执行间接过程调用,通过一场表的条目k转到相应的处理程序。

  5. 异常表的起始地址放在一个叫做异常表基址寄存器里。
  6. 异常类似于过程调用,但又有一些不同之处:

    • 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。然而,根据异常的类型,返回地址要么是当前指令,要么就是下一条指令。
    • 处理器也把一些额外的处理器状态压到栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。
    • 如果控制从一个用户程序转移到内核,那么所有这些项目都被压倒内核栈中,而不是压到用户栈中。
    • 异常处理程序运行在内核模式下,这意味着他们对所有的系统资源都有完全的访问权限。

      一旦硬件出发了异常,剩下的工作就是由异常处理程序在软件中完成。

  7. 异常可以分为:

    - 中断:异步发生,是来自处理器外部的I/O设备的信号的结果。总是返回到下一条指令。
    - 陷阱:同步,来自有意的异常,总是返回到下一条指令。
    - 故障:同步,来自潜在可恢复的错误,可能返回到当前指令。
    - 终止:同步,来自不可恢复的错误,通常是一些硬件错误;不会返回。
    
  8. 陷阱最重要的用途是在用户程序是在用户程序和内核之提供一个像过程一样的接口,叫做** 系统调用**。

  9. 每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。下图为一些常见的linux系统调用。 信息安全系统设计基础第十一周学习总结_第2张图片

8.2 进程

进程的经典定义就是一个执行中的程序的实例。 - 进程提供给应用程序的关键抽象:

   一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器。

   一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用存储器系统。
  • 如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,而这些值,这个PC值的序列叫做逻辑控制流,或者简称逻辑流
  • 进程是轮流使用处理器的。每个进程执行它的一部分,然后被抢占(暂时挂起),然后轮到其他进程。
  • 一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发的运行
  • 多个流并发的执行的一般现象称为并发;一个进程和其他进程轮流进行的概念成为多任务;一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
  • 注意:并发的思想与流运行的处理器核数或者计算机数无关。
  • 处理器通常使用某个控制寄存器模式位来提供这种功能的,该寄存器描述了当前进程享有的特权。当设置了模式位时,就进入内核模式(也称超级用户模式),否则,进程就运行在用户模式中。即不允许执行特权指令。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的故障。
  • 进程从用户模式变为内核模式的唯一方法是通过诸如中断,故障或者陷入系统调用这样的异常。上下文切换 :

    用它这个较高层形式的异常控制流来实现多任务。
    
    内核为每个进程维持一个上下文。在进程执行的某些时刻,内核可以决定抢占当前进程。并重新开始一个先前被抢占的进程,这种决定就叫做***调度***,是由内核中称为调度器的代码处理的。
    
  • 1):保存当前进程的上下文。
  • 2):恢复某个先前被抢占的进程被保存的上下文。
  • 3):将控制传递给这个新恢复的进程。

8.3 系统调用错误处理

  • 错误报告函数:

     `void  unix_error(char  *msg)
      {
           fprintf(stderr,"%s:  %s\n", msg, strerror(errno));
           exit(0);
      }
    

    `

  • 通过使用错误处理包装函数,我们可以更进一步的简化我们的代码。包装函数调用基本函数,检查错误,如果有问题就终止。比如,下面是fork函数的错误处理包装函数:

     `pid_t  Fork(void)
      {
         pid_t pid;
    
         if ((pid = fork())< 0)
              unix_error("Fork error");
         return pid;
      }
    

    给定这个包装函数,我们对fork的调用就缩减为1行:

     `pid = Fork();`
    

8.4 进程控制

获取进程ID

  • 每个进程都有一个唯一的正数进程ID(PID),getpid函数返回调用进程的PID,getppid函数返回它的父进程PID(创建调用进程的进程)。
  • getpid 和getppid函数返回一个类型为pid_t的整数值,在Linux系统上它在types,h中被定义为int。

进程处于的三种状态:运行、停止、终止。

进程会因为三种原因终止:

    1):收到一个信号,该信号的默认行为是终止进程
    2):从主程序返回
    3):调用exit函数。
  • 父进程通过调用fork函数来创建子进程,而子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的PID.
  • fork函数的特点就是:fork函数纸杯调用一次,而返回两次。
  • 在父进程中,fork返回子进程的PID。在子进程中,fork返回0.

回收子进程 当一个进程由于某种原因终止时,内核并不马上清除,而是把它传递给父进程,当父进程回收完子进程时,抛弃,然后该进程就不存在了。 一个终止了但还未被回收的进程称为僵死进程,而内核就会安排init进程来回收他们。init进程的PID为1,并且是在系统初始化时由内核创建的。11/21/2015 9:54:13 PM

一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。

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

  pid_t  waitpid(pid_t  pid, int  *status, int options);

              返回:如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1      `

判定等待集合的成员

等待集合的成员是由参数pid来确定的:

- 若pid<0,那么等待集合就是一个单独的子进程,它的进程ID等于pid。
- 若pid =0,那么等待集合就是由父进程所有的子进程组成的。

可以通过将options设置为常量WNOHANG WUNTRACED(释义详见:P496) 的各种组合,修改默认行为。 错误条件 如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR.

**wait函数**是waitpid函数的简单版本:
调用wait(&status)等价于调用waitpid(-1,&status, 0)。
  • sleep函数讲一个进程挂起一段指定的时间
  • pause函数让调用函数休眠,知道该进程收到一个信号。
  • execve函数在当前进程的上下文中加在并运行一个新程序。

    `#inclue <unistd.h>
    
     int execve(const  char  *filename, const  char  *argv[],
                const  char  *envp[]);
    
                                                  如果成功,则不返回;如果错误,则返回-1.`
    

execve函数调用一次并从不返回!!!

外壳Unix是一个交互型的应用级程序,他代表用户运行其他程序。最早的外壳是sh程序,后出现了一些变种,比如csh、tcsh、ksh和bash。外壳执行一系列的读/求值步骤,然后终止。该步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

8.5 信号

在外壳命令行上输入

man 7 signal 得到如下列表:

信息安全系统设计基础第十一周学习总结_第3张图片

传送一个信号到目的进程是由两种不同的步骤组成的:

 - 发送信号
 - 接收信号:进程可以忽略这个信号,终止或者通过执行一个称为 ** 信号处理程序 ** 的用户层函数捕获这个信号。

发送信号的前提或者是原因:

 - 内核检测到一个系统事件,比如被零除错误或者子进程终止。
 - 一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。

待处理信号:只发出而没有被接收的信号。一种类型至多只会有一个待处理信号,最多只能被接收一次。

进程组

  • 每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。
  • getgrp函数返回当前进程的进程组ID。
  • 默认地,子进程和父进程同属于一个进程组。一个进程可以通过setpgid函数来改变自己或者其他进程的进程组。

用/bin/kill程序发送信号

/bin/kill/程序可以向另外的进程发送任意的信号。(一个为负的PID会导致信号被发送到进程组PID中的每个进程。)

外壳为每个作业创建一个独立的进程组。典型地,进程组ID是取自作业中父进程中的一个。。

alarm函数 - 进程可以通过alarm函数向他自己发送SIGALRM信号。 - alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。返回待处理闹钟的秒数。

接收信号

每个信号类型都有一个预定义的默认行为,是下面的几种:

  • 进程终止,如SIGKILL
  • 进程终止并转储存储器,。
  • 进程停止直到被SIGCONT信号重启。
  • 进程忽略该信号,如SIGCHLD.

进程可以通过使用signal函数修改和信号相关联的默认行为。唯一的例外就是SIGSTOP和SIGKILL,他们的默认行为是不能被改变的。

11/22/2015 4:16:19 PM

信号处理问题

  • 待处理信号被阻塞。
  • 待处理信号不会排队等待
  • 系统调用可以被中断

得到的教训是:不可以用信号来对其他进程中发生的事件计数

如何解决?

  • 为了编写可移植的信号处理代码,我们必须考虑系统调用过早返回的可能性,然后当它发生时手动重启他们。

可移植的信号处理

不同系统之间,信号处理语义的差异,是Unix信号处理的一个缺陷。为了处理这个问题,Posix标准定义了sigaction函数,它允许像linux和solaris这样的与posix兼容的系统上的用户,明确语义。

Signal包装函数设置了一个信号处理程序,其信号处理语义如下:

  • 只有这个处理程序当前正在处理的那种类型的信号被阻塞。
  • 和所有信号实现一样,信号不会排队等待。
  • 只要可能,被中断的系统调用会自动重启。
  • 一旦设置了信号处理程序,他就会一直保持,知道signal带着handler参数为SIGIGN或者SIGDFL被调用。

显式的阻塞和取消阻塞信号

  • 应用程序可以使用sigprocmask函数显式的阻塞和取消阻塞信号:

    `#include <signal.h>
    
     int sigprocmask(int how, const sigset_t  *set, sigset_t  *oldset);
     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 `
    
  • sigprocmask函数改变当前以阻塞信号的集合。具体的行为依赖于how的值:

    • SIG_BLOCK: 添加set中的信号到blocked中(blocked=blocked | set)。
    • SIG_UNBLOCK:从blocked中删除set中的信号(blocked=blocked&~set)。
    • SIG_SETMASK:blocked = set.

8.6 非本地跳转

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

  • setjmp函数env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.调用环境包括程序计数器、栈指针和通用目的寄存器。(の被调用一次,但返回多次。
  • longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retvall。(被调用一次,但从不返回。

重要应用:

- 允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的。
- 使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。

C语言和Java中的软件异常: - try语句中的catch子句看作类似于setjmp函数。 - throw语句就类似于longjmp函数。

8.7 操作进程的工具

  • STRACE:打印一个正在运行的程序和他的子进程调用的每个系统调用的轨迹。
  • PS:列出当前系统中的进程(包括僵死进程)。
  • TOP:打印出关于当前进程资源使用的信息。
  • PMAP:显示进程的存储器映射。

8.8 小结

  • ECF发生在计算机系统中的各个层次,是计算机系统中提供并发的基本机制。
  • 在硬件层,异常是由处理器中的事件触发的控制流中的突变。
  • 在操作系统层,内核用ECF提供进程的基本概念。进程提供给应用两个重要的抽象:

    • 逻辑控制流
    • 私有地址空间
  • 在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待他们的子进程停止或终止,运行新的程序,以及捕获来自其他进程的信号。

  • 在应用层,C程序可以使用非本地跳转来规避正常的调用/返回栈规则,并且直接从一个函数分支到另一个函数。

心得体会

本周的学习内容相比第十章有点多,但是大部分都是代码所以,你只要搞懂代码,我觉得就是对大篇幅文字的很好理解,文字写的很令人易懂,所以课后的题目可以自己做一点点了,总体自我感觉还是蛮好,8000多的字都是自己一个个敲上去的,没有半点粘贴。所以心里很满足。要继续这样下去。

参考文献

  • 《深入理解计算机系统》
  • 自己的上课笔记有关父进程和子进程的说法。
  • 实验楼的平台。检验代码。

 

你可能感兴趣的:(信息安全系统设计基础第十一周学习总结)