第八章 异常控制流

8.1异常

  • 异常

异常是异常控制流的一种形式,由硬件和操作系统实现。简单来说,就是控制流中的突变。

  • 事件

即状态变化,与当前指令的执行可能直接相关,也可能没有关系。

  • 出现异常的处理方式:

1.处理器检测到有异常发生

2.通过异常表,进行间接过程调用,到达异常处理程序

3.完成处理后:①返回给当前指令②返回给下一条指令③终止

8.1.1异常处理

  • 异常号

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

  • 异常表

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

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

  • 异常类与过程调用的区别

1.处理器压入栈的返回地址,是当前指令地址或者下一条指令地址。

2.处理器也把一些额外的处理器状态压到栈里

3.如果控制一个用户程序到内核,所有项目都压到内核栈里。

4.异常处理程序运行在内核模式下,对所有的系统资源都有完全的访问权限。

8.1.2异常的类别

  • 中断

异步发生

来自处理器外部的I/O设备的信号的结果

返回下一条指令

  • 陷阱

同步发生

陷阱是有意的异常

是执行一条指令的结果

最重要的用途——系统调用

  • 故障

同步发生

由错误状况引起,可能能够被故障处理程序修正

结果要么重新执行指令(就是返回当前指令地址),要么终止

典型示例:缺页异常

  • 停止

同步发生

是不可恢复的致命错误造成的结果

通常是一些硬件错误

8.1.3Linux/IA32故障和终止

  • Linux/IA32故障和终止

除法错误/浮点异常 异常0 终止程序

一般保护故障/段故障 异常13 终止程序

缺页 异常14 返回当前地址

机器检查 异常18 终止程序

  • Linux/IA32系统调用

8.2进程

  • 进程提供给应用程序的关键抽象

个独立的逻辑控制流:独占的使用处理器

一个私有的地址空间:独占的使用存储器系统

8.2.1逻辑控制流

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

  • 进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。但是进程可以向每个程序提供一种假象,好像它在独占的使用处理器。

  • 示例

    异常处理程序、进程、信号处理程序、线程、Java进程

8.2.2并发流

  • 含义

一个逻辑流的执行在时间上与另一个流重叠。【与是否在同一处理器无关】这两个流并发的运行。

  • 并发:多个流并发的执行

  • 多任务:一个进程和其他进程轮流运行(也叫时间分片)

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

  • 并行

    两个流并发的运行在不同的处理机核或者计算机上。
    并行流并行的运行,并行的执行。

8.2.3私有地址空间

  • 进程为程序提供的假象,好像它独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读写的。

8.2.4用户模式和内核模式

用户模式和内核模式的区别在于用户的权限上,权限指的是对系统资源使用的权限。

具体的区别是有无模式位,有的话就是内核模式,可以执行指令集中的所有指令,访问系统中任何存储器位置;没有就是用户模式。

进程从用户模式变为内核模式的唯一方法是通过异常——中断,故障,或者陷入系统调用。

Linux的聪明机制——/proc文件系统,将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。

8.2.5上下切换

  • 上下文

    内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,包括通用目的寄存器,浮点寄存器,程序寄存器,用户栈,状态寄存器,内核栈,各种内核数据结构(页表,进程表,文件表)。

  • 调度

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

  • 上下文切换机制

1.保存当前进程的上下文

2.恢复某个先前被抢占的进程被保存的上下文

3.将控制传递给这个新恢复的进程。

  • 发生上下文切换的原因:

    内核代表用户执行系统调用时

    中断

8.3系统调用错误处理

  • Unix fork函数

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

8.4 进程控制

8.4.1获取进程ID

每个进程都有一个唯一的正数进程ID(PID)。

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);	返回调用进程的PID
pid_t getppid(void);	返回父进程的PID(创建调用进程的进程)

8.4.2创建和终止进程

  • 进程总是处于下面三种状态之一
    运行

    停止:被挂起且不会被调度

    终止:永远停止。原因:

    1.收到信号,默认行为为终止进程

    2.从主程序返回

    3.调用exit函数

  • 创建进程

父进程通过调用fork函数来创建一个新的运行子进程。fork函数定义如下:

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

pid_t fork(void);
fork函数只被调用一次,但是会返回两次:父进程返回子进程的PID,子进程返回0.如果失败返回-1.

在图8-15的代码示例为:

/* $begin fork */
#include "csapp.h"

int main() 
{
pid_t pid;
int x = 1;

pid = Fork(); //line:ecf:forkreturn
if (pid == 0) {  /* Child */
printf("child : x=%d\n", ++x); //line:ecf:childprint
exit(0);
}

/* Parent */
printf("parent: x=%d\n", --x); //line:ecf:parentprint
exit(0);
}
/* $end fork */

调用一次,返回两次
并发执行,内核能够以任何方式交替执行它们的逻辑控制流中的指令
相同和不同:

相同:用户栈、本地变量值、堆、全局变量值、代码

不同:私有地址空间

共享文件:子进程继承了父进程所有的打开文件。参考10.6节笔记。

调用fork函数n次,产生2的n次方个进程。

8.4.3回收子进程

进程终止后还要被父进程回收,否则处于僵死状态。

如果父进程没有来得及回收,内核会安排init进程来回收他们。init进程的PID为1.

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

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

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

1.判断等待集合的成员——pid

pid>0:等待集合是一个单独子进程,进程ID等于pid

pid=-1:等待集合是由父进程所有的子进程组成

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

3.检查已回收子进程的退出状态——status
在wait.h头文件中定义了解释status参数的几个宏:

WIFEXITED:如果子进程通过调用exit或一个返回正常终止,就返回真

WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态

WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么返回真

WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时才定义这个状态

WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么返回真

WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时才定义这个状态

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

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

5.wait函数
wait函数是waitpid函数的简单版本,wait(&status)等价于waitpid(-1,&status,0).

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

pid_t wait(int *status);
成功返回子进程pid,出错返回-1

8.4.4、让进程休眠

1.sleep函数
sleep函数使一个进程挂起一段指定的时间。定义如下:

#include <unistd.h>
unsigned int sleep(unsigned int secs);
返回值是剩下还要休眠的秒数,如果到了返回0.

2.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[]);

成功不返回,失败返回-1.
execve函数调用一次,从不返回。
filename:可执行目标文件
argv:参数列表
envp:环境列表

新程序开始时:

getnev函数
#include <stdlib.h>

char *getenv(const char *name);
若存在则为指向name的指针,无匹配是null
在环境数组中搜寻字符串"name=value",如果找到了就返回一个指向value的指针,否则返回null。

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,只有在overwrite非零时成立。

如果name不存在,setenv会将"name=newvalue"写进数组。

※fork函数和execve函数的区别

fork函数是创建新的子进程,是父进程的复制体,在新的子进程中运行相同的程序,父进程和子进程有相同的文件表,但是不同的PID

execve函数在当前进程的上下文中加载并运行一个新的程序,会覆盖当前进程的地址空间,但是没有创建一个新进程,有相同的PID,继承文件描述符。

8.5 信号

  • Unix信号

更高层的软件形式的异常允许进程中断其他进程。

8.5.1、信号术语

传递一个信号到目的进程的两个步骤:发送信号和接收信号。

发送信号的原因:

1.内核检测到一个系统事件
2.一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。
一个进程可以发送信号给它自己。

接收信号:

1.忽略

2.终止

3.执行信号处理程序,捕获信号

待处理信号:
只发出没有被接收的信号
任何时刻,一种类型至多只会有一个待处理信号,多的会被直接丢弃
一个进程可以选择性的阻塞接受某种信号,被阻塞仍可以被发送,但是不会被接收
一个待处理信号最多只能被接收一次。
pending:待处理信号集合
blocked:被阻塞信号集合。

8.5.2、发送信号——基于进程组

1.进程组
每个进程都只属于一个进程组。
进程组ID:正整数
一个子进程和他的父进程属于同一进程组。
查看进程组id:getpgrp
修改进程组:setpgid

2.用/bin/kill程序发送信号
/bin/kill程序可以向另外的进程发送任意的信号,格式是:

/bin/kill -n m
n是信号,m是进程或进程组
当n>0时,发送信号n到进程m

当n<0时,使信号|n|发送到进程组m中的所有进程。

4.用kill函数发送信号
进程通过调用kill函数发送信号给其他进程。

5.用alarm函数发送信号
进程可以通过调用alarm函数向它自己发送SIGALRM信号。

#include <unistd.h>
unsigned int alarm(unsigned int secs);
返回前一次闹钟剩余的秒数,若没有返回0.

6.接收信号

信号类型的预定义的默认行为:

进程终止

进程终止并转储存储器

进程停止直到被SIGCONT信号重启

进程忽略该信号

signal函数通过下列三种方法来改变和信号signum相关联的行为:

如果handler是SIG_IGN,那么忽略类型为signum的信号。

如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为。
否则,handler就是用户定义的函数的地址,这个函数称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序。

通过把狐狸程序的抵制传递到signal函数从而改变默认行为,称为设置信号处理程序。

调用信号处理程序称为捕获信号

执行信号处理程序称为处理信号

信号处理问题

  • 待处理信号被阻塞
  • 带处理信号不会排队等待
  • 系统调用可以被中断
  • 不可以用信号来对其他进程中发生的事件计数
    可移植的信号处理
    Signal包装函数设置了一个信号处理程序,其信号处理语义如下:

只有这个处理程序当前正在处理的那种类型的信号被阻塞。
和所有信号实现一样,信号不会排队等待。
只要可能,被中断的系统调用会自动重启。
一旦设置了信号处理程序,它就会移至保持,直到Signal带着handler参数为SIG_IGN或者SIG_DFL被调用。
显式地阻塞和取消阻塞信号 p517
sigprocmask函数改变当前已阻塞信号的集合,具体行为依赖于how值:

SIG_BLOCK:添加set中的信号到blocked中(blocked = blocked | set)
SIG_UNBLOCK:从blocked中删除set的信号(blocked = blocked &~ set)
SIG_SETMASK:blocked = set
如果oldset非空,blocked位向量以前的值会保存在oldset中。
非本地跳转
用户级的异常控制流形式,通过setjmp和longjmp函数提供。

setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0.

调用环境:程序计数器,栈指针,通用目的寄存器

longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。

setjmp函数只被调用一次,但返回多次;

longjmp函数被调用一次,但从不返回。

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

遇到问题

知识不是很系统,学起来很吃力,需要再花些时间吧。