异常
当处理器检测到有事件发生时,他就会通过一张叫做异常表的跳转表,进行一个间接的过程调用,转到专门用于处理这类事件的异常处理程序。
当异常处理程序完成处理后,根据引起异常事件的类型,会发以下情况:
- 将控制返回给当前指令
- 将控制返回给下一条指令
- 终止程序
异常的类别
类别 | 原因 | 异步同步 | |
---|---|---|---|
中断 | 来自IO设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 潜在的可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
中断: IO设备,例如网络适配器、磁盘控制器,通过向处理器芯片的一个引脚发信号,并将异常号放到系统总线上,来触发中断。
陷阱和系统调用:陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
用户程序经常向内核请求服务,比如读文件(read)、创建一个新的进程(fork)、加载一个新程序(execve)等。处理器提供了 syscall n
指令,执行这条指令会触发一个异常处理程序的陷阱,异常处理程序就可以取调用适当的内核程序,用户可以通过调用这个指令来完成对内核服务的请求。
故障:一个经典的故障示例是 缺页异常
,当指令引用一个虚拟地址,而该地址对应的物理页面不在内存中,因此必须从磁盘中取出,这就会发生故障。
缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令,当指令再次执行时就可以正常完成了。
终止:终止是不可恢复的致命错误,终止处理程序从不将控制返回给应用程序,而将控制返回给一个abort例程,该例程会终止这个应用程序。
进程控制
1. 获取进程ID
每个进程都有一个唯一正数进程ID。
pid_t getpid(void);//返回调用进程的id
pid_t getppid(void);//返回他的父进程的id(创建调用进程的进程)
2. 创建和终止进程
进程总是处于下面三种状态之一:
运行:进程正在执行,或进程等待被调度执行。
停止:进程被挂起,当进程收到 SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOUT
信号时,进程就会被挂起,不会被调度执行。直到接收到 SIGCONT
信号。
终止:进程结束了。产生进程结束可能有这下几种情况。1)收到一个信号,信号的默认行为时终止进程;2)从主程序返回;3)调用exit函数
void exit(int status);//exit函数以status退出状态来终止进程
pid_t fork(void);//父进程通过fork函数,创建一个新的运行的子进程
fork函数调用一次,却返回两次。在父进程中,fork返回子进程的进程id。在子进程中,fork返回0。如果fork出错,则返回-1。
fork创建的子进程和父进程拥有相同的地址空间,这意味着用户栈、堆、data和code区都是相同的。但要注意的是,fork创建子进程时使用copy on write,所以在修改共享数据时会拷贝到自己的私有地址空间,不会影响其他进程。
3. 回收子进程
一个终止了但还未被回收的进程称为 僵死进程 zombie
。
当一个进程终止了,内核并没有立即把它从系统中清除,而是保持在一种已终止的状态中,直到被他的父进程回收。
默认情况下options=0,调用waitpid会挂起调用的进程,直到其等待集合中一个进程终止。(pid > 0,等待集合中就是该指定的pid对应的进程;pid=-1,等待集合由父进程所有的子进程组成)
waitpid返回终止进程的id,此时,已终止的进程已被回收,内核会从系统中删除掉它的所有痕迹
//statusp不为null时,在waitpid返回时,它记录了终止进程的终止状态
pid_t waitpid(pid_t pid, int *statusp, int options);
pid_t wait(int *statusp);//等同于 waitpid(-1, &statusp, 0)
如果父进程终止了,内核会安排
init
进程来回收它僵死的子进程。所以仅对于那些可能长时间不会终止的父进程,如shell或服务器。我们才需要考虑回收它们的僵死的子进程。
4. 让进程休眠
//若请求休眠的时间已到,sleep返回0。否则,返回还剩下要休眠的时间。(sleep函数可能被一个信号中断而过早返回)
unsigned int sleep(unsigned int secs);
//休眠,直到接受到一个唤醒信号。(类似Java中的park和unpark)
int pause(void);
5. 加载并运行程序
//若执行成功则不返回,执行出错返回-1。
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函数在当前进程上下文中加载并运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的pid,并且继承了调用execve函数时已打开的所有文件描述符。
信号
Unix系统提供了大量向进程发送信号的机制,所有这些机制都是基于 进程组
这个概念的。
进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。
默认的,一个子进程和它的父进程同属于一个进程组。
//返回调用进程的进程组ID
pid_t getpgrp(void);
//若成功返回0,若错误返回-1(若pid为0,那么使用当前进程的pid。若pgid是0,那么就用pid作为pgid)
int setpgid(pid_t pid, pid_t pgid);
1. 发送信号
- 用
/bin/kill
程序发送信号
/bin/kill -9 12345 #发送SIGKILL信号给进程12345
/bin/kill -9 -3245 #发送SIGKILL信号给进程组3245中的每个进程
一个为负的PID会导致信号被发送到进程组PID中的每个进程
- 用键盘发送信号
ctrl + c #使内核发送一个SIGINT信号到前台进程组中的每个进程(默认情况下终止前台作业)
ctrl + z #使内核发送一个SIGTSTP信号到前台进程组中的每个进程(默认情况下挂起前台作业)
- 用kill函数发送信号
//成功返回0,出错返回-1(pid>0,发送sig到该PID对应的进程。pid=0,发送sig到调用该函数的进程所在进程组的每个进程)
int kill(pid_t pid, int sig);
- 用alarm函数发送信号
//安排内核在secs秒后发送一个SIGALARM信号给调用进程。
//返回前一次闹钟剩余秒数,若以前没有设定闹钟,则为0。
//对alarm的调用将取消任何待处理的闹钟。
unsigned int alarm(unsigned int secs);
2. 接受信号
当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未阻塞的待处理信号集合。若集合非空,那么内核会选择集合中的某个信号,让进程p接受处理这个信号。
每个信号类型都有相关联的默认行为:
SIGKILL 终止接受信号的进程
SIGCHLD 忽略这个信号
进程可以通过使用signal函数修改和信号相关联的默认行为,唯一的例外是 SIGSTOP和SIGKILL
,它们的默认行为不能更改。
typedof void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
信号处理程序可以被其他信号处理程序中断(一个信号处理程序执行到一半,CPU可能调度执行其他信号处理程序。也就是说信号处理程序的执行是并发的)
未处理的信号是不排队的,不可以用信号来对其他进程中发生的事件计数。