CSAPP--第八章:异常处理

异常

程序在执行中,遇到突发的事件而转入内核模式处理。


CSAPP--第八章:异常处理_第1张图片
异常处理流程

通过异常基址寄存器,加上异常号(索引),可以访问唯一的异常处理程序。


CSAPP--第八章:异常处理_第2张图片
访问异常表

而由异常表,可以跳转到指定的异常处理程序。
CSAPP--第八章:异常处理_第3张图片
异常表
异常的种类

异常可以分为四类:
中断(interrupt)、系统调用(陷阱trap)、故障(fault)、终止(abort)
每种类型又包含了多个不同的具体异常。


CSAPP--第八章:异常处理_第4张图片
异常类型

异常处理中,有处理器设计师制定的异常,也有操作系统指定的异常。
如linux-86-64中,0-31为cpu指定异常,32-255为linux制定的异常。如下图:


CSAPP--第八章:异常处理_第5张图片
部分具体异常

进程和并发

进程(process):系统中某个正在运行的程序。
并发(concurrency):在时间宏观上,如果几个进程时间重复(同时运行),则称他们为并发进行。但是微观上,其所占用的时间片是不重复的,由操作系统进行调度分配。(单个处理器)
(并行(parallel)通常指多个cpu,在微观上时间也是同时进行。)
逻辑控制流:当有多个进程运行时,某进程的所占用所有时间片(可连续可不连续)称为逻辑控制流。
物理控制流cpu实际处理指令的顺序。
现代计算机中,采用了并发的方式运行,但宏观抽象上,每个进程具有连续性,内存独占性。


CSAPP--第八章:异常处理_第6张图片
三个进程的状态图

CSAPP--第八章:异常处理_第7张图片
单个进程运行的内存抽象图
进程切换

用户模式:进程占用时间片,正常运行时的模式,其只能访问自己被分配的内存空间及少量系统函数。
内核模式(超级用户模式):当时间片结束或进程发生异常事件,控制转移到操作系统,此时为内核模式,可以访问所有内存空间,和所有系统函数寄存器模式位也改变。
当一个进程由于时间片结束,或者异常事件,而转到另一个进程时,是由操作系统完成的。
操作系统采用上下文切换(context switch)的方式,转移当前进程各种寄存器状态等,然后储存在内核内存中,然后开始或恢复另一个进程。


CSAPP--第八章:异常处理_第8张图片
进程的控制转换

创建、终止进程
  • 几个头文件stdlib.h、unistd.h。linux:sys/wait等

  • pid_t fork()

    父进程通过调用fork()函数创建一个新的子进程,子进程的所有数据、指令、状态与父进程一样,在内存也分配了一个独立的空间。

    1:fork()函数调用一次,但是会返回两次,一次是在父进程中,返回子进程的pid;一次是在子进程中,返回0;

    2:子进程和父进程并发执行。

    3:相同但是独立的地址空间。当子进程创建时,其内存空间内完全复制父进程,但是两个是独立不干扰的。

    //画进程图对于分析父、子进程很有帮助,尤其在多次fork()时。

  • exit(status)

    进程终止。

回收子进程

当子进程已终止时,内核此时并没有立即将其清除,而是保持在已终止状态,直到父进程回收(reaped)它。另外,操作系统维护了一个init进程,pid=1,是所有进程的祖先。

一个终止了但还没回收的进程叫做僵死进程(zombie)。

  • pid_t waitpid( pid_t pid,int *statusp,int options )

    父进程通过调用这个函数,可以等待子进程终止。

    pid_t是int类型,statusp是指向退出状态的指针(即exit(status)中的status),options实现不同的waitpid。

    options:

    WNOHANG:

    WUNTRACED:

    WCONTINUED:

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

定义了几个宏来进行检查:WIFEXITED(status)、WEXITSTATUS(status)、WIFSIGNALED(status)、WTERMSIG(status)、WIFSTOPPED(status)、WSTOPSIG(status)、WIFCONTINUED(status)。

  • 错误条件

    如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。

    如果waitpid函数被一个信号中断,那么它返回-1,并且设置errbo 为EINTR。

  • pid_t wait(int *statusp);

    waitpid的简单版本,等于waitpid(-1,statusp,0)

    //-1代表所有子进程。

让进程休眠

  • unsigned int sleep(unsigned int ) //返回还要休眠的时间。

    可以进行简单包装,输出时间。

    int pause(void);//暂停进程。

加载并运行程序

include\inlcude

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

    //运行一个可执行程序filename,占有当前进程的地址空间。成功不返回,失败返回1

    当成功加载运行filename后,控制将传递给新进程的main函数,mian函数的原型为:

    int main(int argc , char *argv[] , char *envp[]) { ... }

    实验说明:

    在fun:main函数中,执行execve("myecho",NULL,NULL),然后确实也运行了myecho程序,但是在myecho:main中,此时为:

    int main(0,NULL,NULL){ ... }。

    可能是由于继承了原来进程的运行环境,所以也能正常运行,但是此时输出main的参数和环境变量都为NULL了。

  • char *getenv(const char *name);

    //返回指向环境变量envp[0],即name=value

  • int setenv(const char *name,const char *newvalue,int overwrite);

    //设置环境变量,成功返回0,错误返回-1;

  • void unsetenv(const char *name);

    //会删除envp[0]。

利用fork()、execve()实例

  • 如linux中的shell命令行工具

    其功能为:1:读命令行。2:解析命令行。

    在解析中提取命令行参数,如果参数argv[0]是内置命令,则直接运行。如果是可执行目标文件,则会fork()一个分支。

    然后由最后一个参数,判断是前端还是后端(&),选择shell是等待还是继续运行。

    实例参照CSAPP p527

信号

分两步,内核发送信号,进程接收信号:

发送信号
  • 进程组

    每个进程只属于一个进程组。

    pid_t getpgrp( void ); //返回当前进程的进程组号。

    int setpgid( pid_t pid , pid_t pgid ); //改变pid进程的进程组号。

    //①如果pid=0,则使用当前进程的pid号;②如果pgid=0,则将进程组的pid号改为pid。

    //通常进程组号,是父进程的pid号

  • 用 "/bin/kin"程序来发送信号

    如: /bin/kill -9 15213 //给15213进程组发送信号9(SIGKILL),-9代表发送到所有组内成员

  • 用kill函数发送信号

    int kill( pid_t pid , int sig ); //测试linux里是小写kill,包括很多其他类似函数。

    //①pid<0:发送信号给|pid|组的所有进程;②pid=0:发送信号给本进程组所有进程;

    //③0

    //?只能发送SIGKILL么?

  • 用alarm函数发送信号

    unsgined int alarm( unsigned int secs );

    alarm函数安排内核在secs秒后,发送一个SIGALRM信号给调用进程。

接收信号
  • 用signal函数来修改信号处理程序

    typedef void (*sighandler_t) (int);

    sighandler_t signal( int signum , sighandler_t handler )

    //signum为信号,handler为一个处理函数。详见CSAPP p532

  • 用sigprocmask函数来显式阻塞和接触阻塞信号

    blocked位信号:内核为每个进程在blocked位向量上,维护了被阻塞的信号集合。

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

    根据how的不同,而采取不同的方式,how=:

    • SIG_BLOCK:添加set里面的信号,到blocked中。

    • SIG_UNBLOCK:从blocked中,删除set里面的信号。

    • SIG_SETMASK:block=set。

    而之前的值,会保存在oldset中。

  • 处理set中的信号

    int sigemptyset(sigset_t *set); //将set初始化为空。

    int sigfillset(sigset_t *set); //将每个信号都添加到set中

    int sigaddset(sigset_t *set , int signum); //添加信号signum到set中

    int sigdelset(sigset_t *set , int signum); //从set中删除信号signum。出错返回-1,否则返0

    int sigismember( const sigset_t *set , int signum ); //signum是set成员返1,否则返0.

编写信号处理程序

  • 五个要点:

    ①处理程序尽量简单;②调用安全函数;③保存和释放errno;④保护对共享数据结构的访问;⑤使用volatile和sig_atomic_t来声明全局变量和标志。

  • 中间还有两节没学,等以后需要在学。

操作进程的工具:
  • linux中:

    strace:打印正在运行的程序,和他的子进程调用的,每个系统调用的轨迹。

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

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

    pmap:显示进程的内存映射。

    /proc:虚拟文件系统。以ASCII格式输出大量内核(数据结构)的内容。如:cat/proc/loadavg,输出平均负载。

你可能感兴趣的:(CSAPP--第八章:异常处理)