标签:
异常即控制流的突变;事件即状态变化。
系统中可能的每种类型的异常都分配了一个惟一的非负整数的异常号(exception number)。这些号码中的某一些是由处理器的设计者分配的,其他号码是由操作系统内核的设计者分配的。前者如:除0项,缺页,存储器访违例,断点,算术溢出;后者的示例包括系统调用和来自外部I/O设备的信号。
在系统启动时,OS分配的初始化一张称为异常表的跳转表,使得表目k包含异常k的处理程序的地址。
如果控制从一个用户程序转移到内核,所有这些项目(item)都被压到内核栈中,而不是压到用户栈中。
异常处理程序运行在内核模式下,这意味着它们对所的资源都有完全的访问权限。
现代OS把这些突发改变(abrupt changes)称为异常,如中断。
异常的类别(中断、陷阱、故障和终止)
异步异常是由处理器外部的IO设备中的事件产生的,同步异常是执行一条指令的直接产物。
(1)中断
中断是异步的,是来自处理器外部的I/O设备的信号的结果。硬件中断的异常处理程序称为中断处理程序。
(2)陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
用户程序经常需要向内核请求服务,如读read,创建新进程fork,加载一个新程序execve,终止当前进程exit。从程序员角度看,系统调用和普通函数调用一样。实际他们实现不同。普通函数调用运行在user mode,用户模式限制了函数可以执行的指令类型,而且它们只能访问与调用函数相同的栈;系统调用发生在kernel mode,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
(3)故障
故障由错误情况引起,他可能能够被故障处理程序修正。
故障的一个经典示例是缺页异常。
(4)终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回应用程序。
Linux/IA32系统中的异常
(1)Linux/IA32故障和终止
除法错误 、一般保护故障、缺页、机器检查。
(2)Linux/IA32系统调用
查看系统调用:
Linux提供了160个系统调用,man syscalls查询;man 2 info 得到_syscall宏;可以直接调用任何“系统调用”。
系统调用的实现方法——在IA32中,系统调用通过一条陷阱指令提供:
int n;//n为异常号
所有的到Linux系统调用的参数都是通过寄存器传递的。惯例如下:
• %eax:包含系统调用号
• %ebx,%ecx,%edx,%esi,%edi,%ebp:包含最多六个任意参数
• %esp:栈指针,不能使用
进程:一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文中的。上下文(context)是由程序正确运行所需的状态组成的。这个状态包括存放在存储器中的程序的代码和数据,它的栈,它的通用目的寄存器的内容,它的程序计数器,环境变量,及打开文件描述符的集合。
进程提供给应用程序的关键抽象:
• 一个独立的逻辑控制流:独占的使用处理器
• 一个私有的地址空间:独占的使用存储器系统
!关键在于:进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。
并发流:一个逻辑流的执行在时间上与另一个流重叠,这两个流称为并发地运行。【与是否在同一处理器无关】
并发:多个流并地执行的一般现象叫做并发。
时间片:一个进程执行它的控制流的一部分的每一时间段。
注意:并发的思想与流运行的处理器核数和计算机数无关——在时间上重叠,就是并发的,即运行在同一个处理器上的。
并行流:两个流并发的运行在不同的处理机核或者计算机上。他们并行地运行,并行地执行。
空间地址:2的n次方个可能地址的集合(n位地址的机器上)。
一个进程为每个程序提供自己的私有地址空间。
模式位:处理器通常用某个控制寄存器中的一个模式位提供功能——限制一个应用可以执行的指令以及它可以访问的地址范围。
当设置了模式位时,进程就运行在内核模式中。可以执行指令集中的所有指令,访问系统中任何存储器位置。
当没有设置模式位时,进程就运行在用户模式中。进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。必须通过系统调用接口间接访问。
运行应用程序代码的进程初始是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常——中断,故障,或者陷入系统调用。
/proc文件系统允许用户模式进程访问内核数据结构的内容。它将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构。
上下文切换——一种异常控制流来实现多任务。上下文切换机制是建立在较低层异常机制上的。
上下文:内核重新启动一个被抢占的进程所需的状态。由一些对象的值组成:
通用目的寄存器、浮点寄存器程序计数器、用户栈、状态寄存器、内核栈、内核数据结构(页表、进程表、文件表)
调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定叫做调度,由内核中称为调度器的代码处理。
上下文切换机制
(1)保存当前进程的上下文
(2)恢复某个先前被抢占的进程被保存的上下文
(3)将控制传递给这个新恢复的进程。
详见附录A,上周已进行学习。
!注意:对于系统级函数,应用小写字母的基本名字来引用,不用大写。
每个进程都有一个唯一的正数进程ID(PID)。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回调用进程的PID
pid_t getppid(void); 返回父进程的PID(创建调用进程的进程)
进程三种状态:
(1)运行:进程要么在CPU上执行,要么等待被执行并最终会被内核调度。
(2)停止:被挂起且不会被调度
(3)终止:永远停止。原因:a.收到信号,默认行为为终止进程;b.从主程序返回;c.调用exit函数
#include <stdlib.h>
void exit(int status); 无返回值
exit函数以status退出状态来终止进程。
创建进程
父进程通过调用fork函数来创建一个新的运行子进程。fork函数定义如下:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回:子进程返回0,父进程返回子进程的PID。出错则为-1.
!fork函数只被调用一次,但是会返回两次:一次在调用进程(父进程)中(fork返回子进程的PID),一次在新创建的子进程中(fork返回0)。
!调用fork函数n次,产生2的n次方个进程。
进程终止后被保持,直到被它的父进程收回。
僵死进程:一个终止了但还未被回收的进程。
如果父进程没有来得及回收,内核会安排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.
等待集合的成员由参数pid确定:
如果pid>0:等待集合是一个单独子进程,进程ID等于pid
如果pid=-1:等待集合是由父进程所有的子进程组成
通过将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函数的简单版本。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回:成功则为子进程pid,出错则为-1
调用wait(&status)等价于waitpid(-1,&status,0)。
sleep函数
sleep函数使一个进程挂起一段指定的时间。
#include <unistd.h>
unsigned int sleep(unsigned int secs);
返回:还要休眠的秒数,如果到了返回0.
pause函数
#include <unistd.h>
int pause(void);
让调用函数休眠,直到该进程收到一个信号。
execve函数在当前进程的上下文中夹在并运行一个新程序。
#include <unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
返回:成功则不返回,失败则返回-1.
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。!execve函数调用一次,从不返回。
几个函数操作数组环境:
(1)getnev函数
#include <stdlib.h>
char *getenv(const char *name);
返回:若存在则为指向name的指针,无匹配则为null
getnev函数在环境数组中搜寻字符串"name=value",如果找到了就返回一个指向value的指针,否则返回null。
(2)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"添加到数组中。
Unix信号——更高层的软件形式的异常,允许进程中断其他进程。
传递一个信号到目的进程的两个步骤:发送信号和接收信号。
(1)发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。2个原因:
内核检测到一个系统事件
一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
(2)接收信号:当目的进程被内核强迫以某种方式对信号的发出做出反应时,目的进程就接受了信号。
!信号处理程序捕获信号的基本思想:
每个进程都只属于一个进程组。进程组是由一个正整数进程组ID来标识的。
Getpgrp函数返回当前进程的进程组ID
Setpgid函数改变自己或其他进程组
/bin/kill程序可以向另外的进程发送任意的信号,命令:
/bin/kill -n m
意思为发送信号n给进程或进程组m
Unix使用“作业”来表示为对一个命令行求值而创建的进程。在任何时候,至多有一个前台作业和0个或多个后台作业。
外壳为每个作业创建一个独立的进程组。进程组ID是取自作业中父进程中的一个。
进程通过调用kill函数发送信号给其他进程(包括自己)。
(1)如果pid>0,kill函数发送信号sig给进程pid。
(2)如果pid<0,kill函数发送信号sig给进程组中的每个进程。
进程可以通过调用alarm函数向它自己发送SIGALRM信号。
#include <unistd.h>
unsigned int alarm(unsigned int secs);
返回:前一次闹钟剩余的秒数,若以前没有设定闹钟则为0。
当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未被阻塞的待处理信号的集合。集合若为空,则内核将控制传递到p的逻辑控制流中的下一条指令;若非空,则选择集合中的信号k让p接收,出发进程的某种行为:
• 进程终止
• 进程终止并转储存储器
• 进程停止直到被SIGCONT信号重启。
• 进程忽略该信号。
信号处理-三种方法:
!注意:不可以用信号来对其他进程中发生的事件计数。
sigaction函数,signal函数(sigaction的一个包装函数)
Sigprocmask函数改变当前已阻塞信号的集合。具体的行为依赖于how值:
非本地跳转:用户级的异常控制流形式。通过setjmp和longjmp函数提供。
setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0。调用环境包括程序计数器,栈指针和通用目的寄存器。
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。
注意:
setjmp函数只被调用一次,但返回多次;
longjmp函数被调用一次,但从不返回。
体会:
通过这章学习了解了进程和并发、进程创建和控制的系统调用及函数使用、信号机制等。视频还没有全部看完,还需要再理解。
参考资料:《深入理解计算机系统》
标签:
原文地址:http://www.cnblogs.com/20135230pjy/p/4986533.html