深入理解计算机系统第8章 异常控制流之一异常与进程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010772289/article/details/85953511

第8章 异常控制流

8.1 异常

当处理器检测到有事件发生时,它会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类时间的操作系统子程序(异常处理程序)。

1. 异常处理

系统启动时,操作系统分配和初始化一张称为异常表的跳转表,使得条目k包含异常k的处理程序的地址。在运行时,处理器检测到发生了一个事件,并且确定了相应的异常号k。随后,处理器触发异常,通过异常表的条目k转到相应的处理程序。
异常表的起始地址放在异常表基址寄存器中。

2.异常类别

四类:中断、陷阱、故障、终止。

  • 中断: 中断时异步发生的,是来自处理器外部的I/O设备的信号的结果。

  • 陷阱:陷阱的最重要用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。普通函数运行在用户模式,它只能访问与调用函数相同的栈。系统调用运行在内核模式,内核模式允许系统调用执行指令,并访问定义在内核中的栈。

    每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。

  • 故障:是由错误情况引起,可能能被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序,若处理程序能够修正这个错误情况,它将控制返回给引起故障的指令,从而重新执行它,否则,处理程序返回到内核中的abort例程,终止引起故障的应用程序。

  • 终止:处理程序将控制返回给abort例程,该例程会终止这个应用程序。

8.2 进程

定义:一个执行中的程序的实例。

系统中每个程序都是运行在某个进程的上下文中的,包括存放在存储器的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

当用户向外壳输入一个可执行目标文件的名字,并运行一个程序时,外壳会创建一个新的进程,然后在这个新进程的上下文中来运行这个可执行目标文件。

1. 逻辑控制流

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

2. 并发流

一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发的执行。

多个流并发执行的一般现象称为并发。一个进程和其他进程轮流运行的概念称为多任务。

并行流是并发流的一个真子集。如果两个流并发地运行在不同的处理器核或者计算机上,那么它们称为并行流。它们并行的运行,且并行地执行。

3. 私有地址空间

在一台有n位地址的机器上,地址空间有2^n个可能地址的集合。

下图展示了一个x86 Linux进程的地址空间的组织结构。地址空间底部是保留给用户程序的,包括通常的文本、数据、堆和栈段。地址空间的顶部是保留给内核的,包括内核在代表进程执行指令时(比如,当应用程序执行一个系统调用时)使用的代码、数据和栈。
进程地址空间

4. 用户模式和内核模式

运行应用程序代码的进程初始时是在用户模式中。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

5. 上下文切换

操作系统内核通过上下文切换这种较高层形式的异常控制流来实现多任务。

内核为每一个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。包括通用目的存储器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,如描绘地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表

调度:在内核调度了一个新的进程运行后,它就抢占当前进程,并使用上下文切换的机制来将控制转移到新进程。上下文切换:1)保存当前进程的上下文,2)回复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
进程上下文切换的剖析

8.3 系统调用错误处理

Unix系统遇到错误时,它们典型的会返回-1,并设置全局整数变量errno来表示什么出错了。strerror函数返回一个文本串,描述了和某个errno值相关的错误。

if((pid=fork())<0) {
		fprintf(stderr, "fork error: %s\n", strerror(errno));
		exit(0);
}

8.4 进程控制

1. 获取进程ID

  • getpid: 返回调用进程的PID
  • getppid: 返回父进程的PID
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

2. 创建和终止进程

进程总处于以下三种状态:

  • 运行:要么在CPU上执行,要么等待被执行
  • 停止:进程被挂起,且不会被调度。当收到SIGSTOP, SIGTSTP, SIDTTIN或SIGTTOU信号时,进程就停止,知道它收到一个SIGCONT信号,进程再次开始运行.
  • 终止:进程永远停止。进程终止三种原因:1)收到一个信号,其默认行为是终止进程, 2)从主程序返回, 3)调用exit函数
#include <stdlib.h>

void exit(int status);

创建新进程:

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

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

子进程获得:

  • 与父进程用户级虚拟地址空间相同但独立的一份拷贝,包括文本、数据和bss段、堆以及用户栈。
  • 父进程打开文件描述符的相同拷贝。也就是,子进程可以读写父进程打开的任何文件。

fork函数:
被调用一次,返回两次: 父进程中返回子进程PID, 子进程中返回0.
根据返回值可分辨程序是在父进程还是子进程中执行。

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,如果WHOHANG,则为0,其他错误返回-1.

等待集合:

  • pid > 0: 等待集合就是进程id等于PID的子进程.
  • pid = -1: 等待集合是由父进程所有的子进程组成.

默认情况(options=0):waitpid返回导致waitpid返回的已终止子进程的PID,并将这个已终止的子进程从系统中去除.

  • waitpid挂起调用进程,直到它的等待集合中一个子进程终止。
  • 如果等待集合中一个子进程在刚调用时,就已经终止,那么waitpid就立即返回.

修改默认行为(通过设置options):

  • WNOHANG: 若等待集合中任何子进程都没有终止,那么久立即返回(返回值为0).
  • WUNTRACED: 挂起调用进程的执行,知道等待集合中一个进程变成已终止或被停止。返回的PID为导致返回的已终止或被停止子进程的PID.
  • WNOHANG|WUNTRACED: 立即返回,若等待集合中没有任何子进程被停止或已终止,那么返回0,否则返回被停止或终止子进程的PID.

检查已回收子进程的退出状态:
status参数:存放导致返回的子进程的状态信息. wait.h头文件定义了解释status参数的几个宏:

  • WIFEXITED
  • WEXITSTATUS
  • WIFSIGNALED
  • WTERMSIG
  • WIFSTOPPED
  • WSTOPSIG

错误:
若调用进程没有子进程,那么waitpid返回-1,并设置errno为ECHILD.若waitpid被一个信号中断,那么它返回-1,并设置errno为EINTR.

wait函数: 是waitpid的简单版本

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

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

调用wait(&status)等价于调用waitpid(-1,&status,0).

waitpid示例:
15行,父进程用waitpid作为while循环的测试条件,等待它所有子进程终止。因第一个参数是-1,所以对waitpid的调用会阻塞,直到任一个子进程终止.
在每个子进程终止时,对waitpid的调用会返回,返回值为该子进程的非零PID.
当回收所有子进程后,再调用waitpid就返回-1,并设置errno为ECHILD.
使用waitpid函数不按照特定的顺序回收僵死子进程
使用waitpid按照创建子进程的顺序来回收这些僵死子进程

4. 让进程休眠

sleep: 将一个进程挂起一段指定的时间

#include <unistd.h>

unsigned int sleep(unsigned int secs);  返回:还要休眠的秒数

若请求时间已到了,sleep返回0,否则返回还剩下的要休眠的秒数.

pause: 让调用函数休眠,知道该进程收到一个信号.

#include <unistd.h>

int pause(void);  返回:总返回-1

5. 加载并运行程序

execve: 在当前进程上下文中加载并运行一个新程序.

#include <unistd.h>

int execve(const char *filename, const char *argv[], const char *envp[]);   若成功则不返回,若错误,则返回-1

execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp.
argv:只想一个以null结尾的指针数组, argv[0]是可执行目标文件的名字.
在这里插入图片描述
unix操作环境数组:

#include <stdlib.h>

char *getenv(const char *name);  返回:若存在则为指向name的指针,否则返回NULL

int setenv(const char *name, const char *newvalue, int overwrite); 返回:若成功返回0,若错误返回-1

void unsetenv(const char *name);

getenv:在环境数组中搜索字符串"name=value",若存在,它就返回一个指向value的指针,否则它就返回NULL.
unsetenv: 若环境数组包含一个"name=value"的字符串,则删除它.
setenv:用newvalue代替oldvalue,但只有overwrite非零时才会这样.

猜你喜欢

转载自blog.csdn.net/u010772289/article/details/85953511