csapp第八章笔记

csapp第八章 异常控制流

8.1 异常

异常就是控制流中的突变,用来相应处理器状态中的某些变化。

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

系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。其中一些号码是处理器的设计者分配的,其他的是操作系统内核的设计者分配的。

检测到异常后,通过异常表间接调用。

异常表的起始地址放在一个叫做异常表基址寄存器的特殊cpu寄存器中。

要注意的几点是:

  1. 除了返回地址,处理器状态也会被压到栈里,x86会将包含当前条件吗的EFLAGS寄存器和其他内容压入栈中

  2. 如果控制从用户程序转移到内核,这些所有项目都会被压入内核栈中

    异常的种类

类别 原因 异步/同步 返回行为
中断 来自I/O设备的信号 异步 总是返回到下一条指令
陷阱 有意的异常 同步 总是返回到下一条指令
故障 潜在可恢复的错误 同步 可能返回到当前指令
终止 不可恢复的错误 同步 不回返回

处理器提供了“syscall n”指令,当用户请求服务时使用。

内核中的abort例程会终止故障的程序。

8.2 进程

进程提供了两个关键抽象:

  1. 一个独立的逻辑控制流,好像程序独占地使用处理器
  2. 一个私有的地址空间,好像程序独占地使用内存系统

处理器通常是用某个控制寄存器中的一个模式位(mode bit)来提供这种功能的,该寄存器描述了进程当前享有的特权。一旦设置了模式位,则进程就运行在内核模式中。

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。该过程为:

异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。当它返回用户程序代码时,再变为用户模式。

8.4 进程控制

获取进程ID

1
2
3
4
5
#include<sys/types.h>
#include<unistd.h>

pid_t getpid(void); //返回调用进程的PID
pid_t getppid(void); //返回它的父进程的PID

pid_t = int

创建和终止进程

当收到SIGSTOP、SIGTSTP、SIGTTIN或者SIGTTOU信号时,进程就停止,并且保存直到收到SIGCONT信号,再开始运行

1
2
3
#include<stdlib.h>

void exit(int status);

创建子进程

1
2
3
4
#include<sys/types.h>
#include<unistd.h>

pid_t fork(void); //返回:子进程返回0,父进程返回子进程的PID,如果出错,则为-1

example:

1
2
3
4
5
6
7
8
9
10
11
12
int main (){
pid_t pid;
int x = 1;

pid = Fork();
if (pid == 0){
printf("child : x=%d\n",++x);
exit(0);
}

printf("parent : x=%d\n",--x);
}

结果为:

1
2
parent : x=0
child : x=2

这个例子有几个点:

  • 调用一次,返回两次
    • fork函数一次返回到父进程,一次返回到新创建的子进程
  • 并发执行
    • 父进程和子进程是并发运行的独立程序。内核能够以任意方式交替运行它们的逻辑控制流中的指令。
  • 相同但是独立的地址空间
    • 地址空间是相同的,例如相同的本地变量值、堆、全局变量值和代码
    • 但父进程与子进程对x所做的改变都是独立的
  • 共享文件

回收子进程

1
2
3
4
#include<sys/types.h>
#include<sys/wait.h>

pid_t waitpid(pid_t pid, int *statusp, int options); //如果成功,返回子进程的PID,如果WNOHANG,则返回0,其他情况-1

在默认情况下(当options=0时),waitpid挂起调用进程的执行(这里说的是调用该函数的进程,纠结了我一会),直到它等待集合中的一个子进程终止。

  1. 判定等待集合的成员

    等待集合的成员是由参数pid来确定的:

    • 如果pid>0,那么等待集合就是一个单独的子进程,它的id等于pid
    • 如果pid=-1,那么等待集合就是由父进程所有的子进程组成的
  2. 修改默认行为

    可以用|来组合

    • WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用
    • WUNTRACED:挂起调用进程的执行,直到等待集合中的进程变成已终止或者被停止。返回的PID为导致返回的子进程的PID。
    • WCONTINUED:挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行
  3. 检查已回收子进程的退出状态

https://blog.csdn.net/yiyi__baby/article/details/45539993 这篇blog有些许补充

加载并运行程序

1
2
3
#include<unistd.h>

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

记录一下老生常谈的问题

main函数有3个参数

  • argc,给出argv[]数组中非空指针的数量
  • argv,指向argv[]数组中的第一个条目
  • envp,指向envp[]数组中的第一个条目

Linux中给了几个函数来操作环境数组:

1
2
3
4
5
6
7
#include <stdlib.h>

char *getenv(const char *name); //若存在则返回指向value的指针,若无匹配的,则返回NULL

int setenv(const char *name, const char *newvalue,int overwrite); //若成功返回0,失败返回-1

void unsetenv(const char *name)

8.5 信号

信号就是小消息,它通知进程系统中发生了一个某种类型的事件。

一个发出而没有被接收的信号叫做待处理信号(pending signal)。在任何时候,一种类型至多只会有一个待处理信号。再来的直接被丢弃。

进程可以自行选择阻塞某种信号,信号被阻塞后依然可以被发送,但除非进程取消阻塞,否则一直不会被接收。

内核为每个进程在pending位向量中维护着待处理信号的集合,在blocked位向量中维护着被阻塞的信号集合。传送就设置,接收就清除。

发送信号

1
2
#include <unistd.h>
pid_t getpgrp(void); //返回调用进程的进程组PID

一个子进程和它的父进程都属于一个进程组,一个进程可以通过使用setpgid函数来改变自己或其他进程的进程组

1
2
3
#include <unistd.h>

int setpgid(pid_t pid, pig_t pgid);

如果进程15213是调用进程,那么setpgid(0,0)则会创建一个新的进程组,进程组ID为15213,并把进程15213加入到这个新的进程组中。

还可以用/bin/kill发送信号(Linux终端)

用kill函数发送信号:

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid,int sig); //成功返回0,若错误返回-1如果pid>0:kill函数发送sig给进程pid

如果pid=0:kill函数发送信号给调用进程所在进程组中的每个进程

如果pid<0:kill函数发送信号给进程组|PID|(PID的绝对值中的每个进程)

用alarm发送信号:

1
2
3
#include <unistd.h>

unsigned int alarm(unsigned int secs); //返回前一次闹钟剩余的描述,若没有设置闹钟,则返回0

接收信号

当内核把进程p从内核模式切换到用户模式时,会检查p的未被阻塞的待处理信号的集合(pending & ~blocked)。

每个信号都有默认行为,但是可以通过signal函数修改这种行为,但是,SIGSTOP和SIGKILL不能被修改。

1
2
3
4
#include <signal.h>
typedef void (*sighandler_t)(int);

sighander_t signal(int signum, sighander_t handler); //若成功返回指向前次处理程序的指针,若失败返回SIG_ERR
  • 如果handler是SIG_IGN,那么忽略的类型为signum的信号
  • 如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为
  • 否则,handler就是用户定义的函数的地址,这个函数被称为信号处理程序,只要进程就收到一个类型为signum的信号,就会调用这个程序。通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序。调用信号处理程序被称为捕获信号。执行信号处理程序被称为处理信号

阻塞和解除阻塞信号

应用程序可以使用sigprocmask函数和它的辅助函数,明确地阻塞和解除阻塞选定的信号。

1
2
3
4
5
6
7
8
9
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
//成功返回0,否则-1
int sigismember(const sigset_t *set, int signum); //若signum是set地成员则为1,不是则为0,出错返回-1

how的值可以为:

  • SIG_BLOCK:把set中的信号添加到blocked中(blocked=blocked | set)。

  • SIG_UNBLOCK:把blocked中删除set中的信号(blocked=blocked &~set)。

  • SIG_SETMASK:block=set

各个函数的作用:

sigemptyset初始化set集合为空。

sigfillset函数把每个信号都添加到set中。

sigaddset函数把signum添加到set。

之后就是关于并发编程的了,看的模模糊糊的,准备看完12章再来复习一下吧。