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

异常

相关概念:

异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。
异常就是控制流中的突变,用来相应处理器状态中的某些变化。
当处理器状态中发生一个重要的变化时,处理器正在执行某个当前指令Icurr。
在处理器中,状态被编码为不同的位和信号。
状态变化称为事件,事件可能和当前指令的执行直接相关。
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况中的一种:
①处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。
②处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。
③处理程序终止被中断的程序。

1.异常是异常控制流的一种形式,一部分由由硬件实现,一部分由操作系统实现。由于系统的不同而有所不同。

异常就是控制流的突变

2.异常的剖析:

img

3.异常处理

系统为每种类型的异常分配的唯一的非负整数异常号

img

系统启动时操作系统就会初始化一张称为异常表的跳转表,使得条目k包含异常k的处理程序的地址

img

异常号是到异常表中的索引,异常表的起始地址放在异常表基址寄存器的特殊CPU寄存器里。

4.异常的类别

分为四种:中断、陷阱、故障和终止

img

(1)中断
  • 异步发生
  • 来自处理器外部的I/O设备的信号的结果
  • 将控制返回给下一条指令
(2)陷阱和系统调用
  • 陷阱是有意的异常
  • 是执行一条指令的结果
  • 最重要的用途:在用户和内核间提供一个像过程一样的接口,叫系统调用
(3)故障
  • 由错误状况引起,可能能够被故障处理程序修正
  • 故障发生时,处理器将控制转移给故障处理程序,如果能够修正,返回引起故障的指令,重新执行指令,否则返回abort例程,终止
  • 典型示例:缺页异常,如图:

img

(4)终止
  • 是不可恢复的致命错误造成的结果
  • 通常是一些硬件错误
  • 终止示例:将控制返回abort例程,如图:

img

Linux/IA32系统中的异常

高达256种异常类型,如图8:

img

(1)Linux/IA32故障和终止
  • 除法错误/浮点异常:异常0,选择中止程序
  • 一般保护故障/段故障:异常13,选择中止程序
  • 缺页:异常14,重新执行产生故障的指令
  • 机器检查:异常18,不返回控制给应用程序
(2)Linux/IA32系统调用

系统调用示例,如图9所示:

img

进程

在操作系统层:逻辑控制流,私有地址空间,多任务,并发,并行,上下文,上下文切换,调度。

进程就是一个执行中的程序实例。系统中的每个程序都是运行在某个进程的上下文中的。

进程提供给应用程序的关键抽象:a)一个独立的逻辑控制流 ;b)一个私有的地址空间

1.逻辑控制流

一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令,这个PC值的序列就叫做逻辑控制流,或者简称逻辑流

  • 示例:三进程系统,如图所示

img

进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。对于运行在改程序上下文的其他程序,它看上去在独占的使用处理器。

2.并发流

并发流:并发流一个逻辑流的执行在时间上与另一个流重叠

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

多任务:多个进程并发叫做多任务。

并行:并发流在不同的cpu或计算机上。

3.私有地址空间

一个进程为每个程序提供它自己的私有地址空间。

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。

  • 进程地址空间,如图11所示

img

4.用户模式和内核模式

需要限制一个应用可以执行的指令以及可访问的地址空间范围来实现进程抽象,通过特定控制寄存器的一个模式位来提供这种机制。设置了模式位时,进程运行在内核模式中,进程可以执行任何指令和访问任何存储器位置。没设置模式位时,进程运行在用户模式中,进程不允许执行特权指令和访问地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。

用户程序的进程初始是在用户模式中的,必须通过中断、故障或陷入系统调用这样的异常来变为内核模式。

Linux的聪明机制——/proc文件系统,包含内核数据结构的内容的可读形式,运行用户模式进程访问。

5.上下文切换
  • 上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。
  • 上下文切换机制:
  • (1)保存当前进程的上下文;
  • (2)恢复某个先前被抢占的进程被保存的上下文;
  • (3)将控制传递给这个新恢复的进程
  • 调度:内核中的调度器实现调度。
  • 当内核代表用户执行上下文切换时,可能会发生上下文切换。
  • 如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。
  • 示例:进程A与B之间上下文切换,如图12所示

img

系统调用错误处理

  • 在Linux中,可以使用 man syscalls 查看全部系统调用的列表。
  • 系统级函数遇到错误时,通常返回-1,并设置全局变量 errno 。

进程控制

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

#include <unistd.h>

pid_t getpid(void); /*返回调用进程的PID*/
pid_t getppid(void);    /*返回它的父进程的PID(创建调用进程的进程)*/

2.创建和终止进程

进程的三种状态——运行、停止和终止。

  • 运行:要么在CPU上执行,要么在等待被执行,且最终被内核调度。
  • 停止:进程的执行被挂起,且不会被调度。收到 SIGSTOP 、 SIGTSTP 、 SIDTTIN 、 SIGTTOU 信号,进程停止,收到 SIGCONT 信号,进程再次开始运行。
  • 终止:永远停止。原因可能是:收到终止进程的信号,从主程序返回,调用 exit 函数。
创建进程

父进程通过调用fork创建一个新的运行子进程:父进程与子进程有相同(但是独立的)地址空间,有相同的文件藐视符集合。

fork函数定义如下:

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

pid_t fork(void);

fork 函数调用一次,返回两次;父子进程是并发运行的,不能假设它们的执行顺序;两个进程的初始地址空间相同,但是是相互独立的;它们还共享打开的文件。因为有相同的程序代码,所以如果调用 fork 三次,就会有八个进程。

3.回收子进程
  • 当一个进程终止时,内核并不立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。
  • 僵死进程:一个终止了但是还未被回收的进程称为僵死进程。父进程回收终止的子进程时,内核将子进程退出状态传给父进程,然后抛弃该进程。如果回收前父进程已经终止,那么僵死进程由 init 进程回收。

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

waitpid函数的定义如下:

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

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

(1)判断等待集合的成员

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

  • 如果pid>0:等待集合是一个单独子进程,进程ID等于pid
  • 如果pid=-1:等待集合是由父进程所有的子进程组成
(2)修改默认行为

将options设置为常量WNOHANG和WUNTRACED的各种组合,修改默认行为:

img

(3)检查已回收子进程的退出状态——status

img

(4)让进程休眠

sleep函数使一个进程挂起一段指定的时间。

#include <unistd.h>

unsigned int sleep(unsigned int secs);

pause函数让调用函数休眠

#include <unistd.h>

int pause(void);

(5)加载并运行程序
  • filename:可执行目标文件
  • argv:参数列表
  • envp:环境列表

img

img

信号

在操作系统和应用程序之间:进程之间传送信号

一种更高层次的软件形式的异常,称为unix信号,它允许进程中断其他进程。

img

1.信号术语
(1)发送信号

发送信号:/bin/kill , kill函数,键盘,alarm函数

  • 用kill函数发送信号:发送SIGKILL信号
  • 用alarm函数发送信号:发送SOGALARM信号
(2)接收信号

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

img

2.发送信号
(1)进程组
  • 每个进程都只属于一个进程组。
  • 进程组是由一个正整数进程组ID来标识的。
  • getpgrp函数返回当前进程组id
  • setpgid函数修改自己或其他进程组
(2)用/bin/kill程序发送信号

/bin/kill程序可以向另外的进程发送任意的信号,比如 /bin/kill -9 15213

即为:发送信号9给进程15213

(3)从键盘发送信号

在任何时刻,至多只有一个前台作业和0个或多个后台作业。外壳为每个作业创建一个独立的进程组,一个作业对应一个进程组。

img

(4)用kill函数发送信号

发送SIGKILL信号

(5)用alarm函数发送信号

发送SOGALARM信号

3.接收信号

当内核从一个异常处理程序返回,准备将控制传递给进程P时,他会检查进程P的未被阻塞的处理信号的集合。如果这个集合为空,那么内核将控制传递到P的逻辑控制流中的下一条指令;如果集合是非空的,那么内核选择集合中的某个信号K(通常是最小的K0,并且强制P接收信号K。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回P的逻辑控制流中的下一条指令。

每个信号类型都有一个预定的默认行为:
  • 进程终止
  • 进程终止并转储存储器
  • 进程停止直到被SIGCONT型号重启
  • 进程忽略该信号
4.信号处理问题

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

  • 待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。
  • 待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为K的信号传送到一个目的进程,而由于目的进程当前正在执行信号K的处理程序,所以信号K时阻塞的,那么第二和信号就简单地被简单的丢弃,他不会排队等待。
  • 系统调用可以被中断。像read、wait和accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误的条件,并将errno设置为EINTR。
5.可移植的信号处理

Signal包装函数设置的信号处理程序的信号处理语义:

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

img

7.同步流以避免讨厌的并发错误

以某种方式同步并交流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。

非本地跳转

c语言中,用户级的异常控制流形式,通过setjmp和longjmp函数提供。

setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0。sig—函数是setjmp和longjmp函数的可以被信号处理程序使用的版本。

操作进程的工具

Linux系统提供了大量的监控和操作进程的有用工具:

STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。对于好奇的的工具。用-StatiC编译你的程序,能传到一个更干净的、不带学生而言,这是一个令人着迷有大量与共享库相关的输出的轨迹。

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

TOP:打印出关于当前进程资源使用的信息。

PMAP:显示进程的存储器映射。proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数数据结构的内容,用户程序可 cat 2 / proc / load avg” , 观察在Linux系统上的平均负载。

代码分析

exec1.c

  • 功能:

    • execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件
  • 注意:

    • 如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中
    • exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话
  • 运行结果:

exec2.c

  • 功能:

    • 同exec1.c
  • 注意:

    • exec2与exec1的区别:
      • exevp函数的第一个参数,exec1 -> s,exec2 -> arglist[0]
      • 由定义可得这两个等价,故运行结果是相同的
  • 运行结果:

exec3.c

  • 功能:

    • 函数中execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名
    • 执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……
  • 注意:

    • 最后一个参数必须用空指针(NULL)作结束
  • 运行结果:

env

  • 功能:
    • getenv函数:
      • 获得环境变量值的函数,参数是环境变量名name。
      • 例如”HOME”或者”PATH”。如果环境变量存在,那么getenv函数会返回环境变量值,即value的首地址。
      • 如果环境变量不存在,那么getenv函数返回NULL
    • setenv函数:
      • 修改或添加环境变量的函数
      • 如果name在环境中不存在则添加该变量。
  • 注意:
    • setenv函数不必在environment list中增加一个新的entry。
    • 当overwrite为0, 则不必改动entry的指向。
    • 当overwrite非0, 则直接使该entry指向name=value。
    • 每个程序都有一个环境表,它是一个字符指针数组,其中每个指针包含一个以NULL结尾的C字符串的地址。全局变量environ则包含了该指针数组的地址。

argv

  • 分析:

    • 头文件argv.h,下面的函数的功能是把命令行字符串转化为以NULL结尾的参数数组
    int makeargv(const char *s, const char *delimiters, char ***argvp);
    
    • 由于argtest.c中有如下代码:
     if (argc != 2) 
    {
    fprintf(stderr, "Usage: %s string
    ", argv[0]);
     return 1;
    }
    
    • 输入命令的个数必须为2,否则不显示正确结果。

fifo

  • 功能:

    • FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中
    • FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
    • FIFO往往都是多个写进程,一个读进程

producer.c

  • 功能:
    • testmf.c代码中调用了mkfifo函数
      mkfifo(FIFO_NAME, 0777);//依据FIFO_NAME创建fifo文件,0777依次是相应权限
  • 注意:
    • mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取

forkdemo1.c

  • 功能:

    • 先打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0
  • 运行结果:

forkdemo2.c

  • 功能:

    • 调用两次fork,一共产生四个子进程,所以会打印四个aftre输出
  • 运行结果:

forkdemo3.c

  • 功能:

    • fork产生子进程,父进程返回子进程pid,不为0,输出父进程。
    • 子进程返回0,输出子进程。
  • 运行结果:

forkdemo4.c

  • 功能:

    • 先打印进程pid,fork创建子进程,父进程返回子进程pid,输出parent,休眠十秒;子进程返回0,所以输出child等。
  • 运行结果:

forkgdb.c

  • 功能:

    • 父进程打印是先打印两句,休眠一秒,打印一句,子进程先打印,然后休眠一秒,接着打印。
    • 两个线程是并发的,可看到在一个线程休眠,另一个线程执行,并且线程之间相互独立互不干扰。
  • 运行结果:

waitdemo1.c

  • 功能:

    • 如有子进程,则终止子进程,成功返回子进程pid。
  • 运行结果:

waitdemo2.c

  • 运行结果:

testbuf1.c

  • 运行结果:

testbuf2.c

  • 运行结果:

  • fflush(stdout) == ‘ ’

testbuf3.c

  • 功能:

    • 将内容格式化输出到标准错误、输出流中
  • 运行结果:

testpid.c

  • 功能:

    • 输出当前进程pid和当前进程的父进程的pid
  • 运行结果:

testpp.c

  • 运行结果:

testsystem.c

  • 运行结果:

psh1.c

  • 功能:
    • 输入要执行的指令,回车表示输入结束,输入的每个参数对应到函数中,再调用对应的指令
  • 运行结果:

psh2.c

  • 运行结果:

总结

本周学习的内容大多数集中在第八章《异常控制流》,也涉猎了一部分第十章《系统级I/O》的内容,内容多且不易吸收学习。阅读完CSAPP后,我学写了相关的视频。但是,最后发现对于一些概念,还是不甚理解,如跳转等,还需要继续专研。不过学习了异常控制流并结合系统级I/O相关知识,对于系统如何正常运转的理解又加深了不少却是实情,希望自己能将没能摸透的概念继续深入理解。

代码托管

代码托管链接

代码行数截图

代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
目标 5000行 30篇 400小时
第一周 0/0 1/2 15/30
第二周 56 /56 2/3 15/45
第三周 89/145 1/4 20/65
第五周 500/645 1/5 20/85
第六周 150/795 1/6 20/105
第七周 124/919 1/7 20/125
第八周 0/919 1/8 15/140
第九周 98/1017 1/9 15/155
第十周 448/1465 1/10 20/175
第十一 634/2099 1/11 25/200

参考资料

  • 《深入理解计算机系统》学习指导
原文地址:https://www.cnblogs.com/xxy745214935/p/6107305.html