CSAPP3e-学习笔记-第八章


判断题:异常分为四种类别:中断(interrupt)、陷阱(trap)、故障(fault)、终止 (abort),四类异常都是同步异常。(错)

CSAPP3e 8.1.2 异常的类别

异常可以分为四类:中断 (interrupt) 、陷阱 (trap) 、故障 (fault) 和终止 (abort) 。下面的表对这些类别的属性做了小结。

异常的类别

判断题:进程是一个执行中程序的实例,每个进程都运行在某个进程的上下文(context) 中。(对)

CSAPP3e 8.2 进程

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

进程提供给应用程序两个关键抽象:

  • 逻辑控制流

    • PC(程序计数器)值的序列叫做逻辑控制流。
    • 每个程序似乎独占地使用 CPU
    • 通过 OS 内核的上下文切换机制提供
  • 私有地址空间

    • 每个程序似乎独占地使用内存系统
    • OS 内核的虚拟内存机制提供

判断题:上下文(context)是由程序正确运行所需要的状态组成,包括:内存中的程序的代码和数据、栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。(对)

CSAPP3e 8.2.5 上下文切换

  • 进程由常驻内存的操作系统代码块(称为内核)管理,内核不是一个单独的进程,而是作为现有进程的一部分运行。

  • 操作系统内核使用上下文切换实现多任务。上下文包含进程的状态:通用目的寄存器、浮点寄存器、程序计数器、用户栈、内核栈、各种内核数据结构。

  • 调度、调度器、上下文切换过程:

    上下文切换

判断题:父进程和新创建的子进程具有相同的 PID 和 PGID。(错)

判断题:fork 函数调用一次返回两次,一次返回到父进程,一次返回到新创建的子进程。(对)

CSAPP3e 8.4.2 创建和终止进程

从程序员的角度,我们可以认为进程总是处于下面三种状态之一

  • 运行(Running)
    进程要么在 CPU 上执行,要么在等待被执行且最终会被操作系统内核调度。
  • 停止/暂停/挂起(Stopped/Paused/Hanged)
    进程的执行被挂起且不会被调度,直到收到新的信号。
  • 终止(Terminated)
    进程永远地停止了,但仍占资源

创建进程

父进程通过调用 fork 函数创建一个新的运行的子进程。

1
int fork(void);
  • 被调用一次,却返回两次
    • 子进程返回 0,父进程返回子进程的 PID
  • 新创建的子进程几乎但不完全与父进程相同
    • 子进程得到与父进程虚拟地址空间相同的(但是独立的)一份副本(代码、数据段、堆、共享库以及用户栈)
    • 子进程获得与父进程任何打开文件描述符相同的副本
    • stdout 文件在父、子进程是相同的
    • 子进程有不同于父进程的 PID
  • 并发执行
    • 不能预测父进程与子进程 的执行顺序

CSAPP3e 8.5.2 发送信号: 进程组

每个进程只属于一个进程组,进程组是由一个正整数进程组 ID 来标识的(PGID)。


多选题:每个信号类型都有一个预定义的默认行为,分别是(A B C D)
A. 进程终止
B. 进程终止并转储内存
C. 进程停止(挂起)直到被 SIGCONT 信号重启
D. 进程忽略该信号

CSAPP3e 8.5 Linux 信号

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

  • 类似于异常和中断
  • 从内核发送到(有时是在另一个进程的请求下)一个进程
  • 信号类型是用小整数 ID 来标识的(1-30)
  • 信号中唯一的信息是它的 ID 和它的到达

CSAPP3e 8.5.3 接收信号

每个信号类型都有一个预定义默认行为, 是下面中的一种:

  • 进程终止
  • 进程终止并转储内存
  • 进程停止(挂起)直到被 SIGCONT 信号重启
  • 进程忽略该信号

进程可以通过 signal 函数修改和信号相关联的默认行为,其中 SIGSTOPSIGKILL 的默认行为不能修改。


多选题:进程调用 int kill(pid_t pid, int sig) 函数发送信号给其他进程,以下说法正确的是(A B C)
A. $pid>0$:kill 函数发送信号号码 sig 给进程 pid。
B. $pid=0$:kill 函数发送信号 sig 给调用进程所在进程组中的每个进程。
C. $pid<0$:kill 函数发送信号 sig 给进程组 $| pid |$(pid 的绝对值)中的每个进程。
D. 以上说法都不正确

CSAPP3e 8.5.2 发送信号: 进程组

进程可以通过调用 kill 函数发送信号给其他进程(包括自己)。

1
int kill(pid_t pid, int sig) // 若成功则返回 0,若错误则返回 -1。

pid 取值有三种情况:

pid 结果
$pid>0$ kill 函数发送信号号码 sig 给进程 pid。
$pid=0$ kill 函数发送信号 sig 给调用进程所在进程组中的每个进程。
$pid<0$ kill 函数发送信号 sig 给进程组$

多选题:发送信号的原因:(A B)
A. 内核检测到一个系统事件,比如除零错误或子进程终止。
B. 一个进程调用了 kill 函数,显式地要求内核发送一个信号给目的进程。
C. 进程创建子进程。

CSAPP3e 8.5.1 信号术语:发送信号

内核通过更新目的进程上下文中的某个状态,发送 (递送)一个信号给目的进程。

发送信号可以是如下原因之一:

  • 内核检测到一个系统事件如除零错误(SIGFPE)或者子进程 终止(SIGCHLD)。
  • 一个进程调用了 kill 系统调用,显式地请求内核发送一 个信号到目的进程。
    • 一个进程可以发送信号给它自己。

多选题:signal 函数接受两个参数:信号值和函数指针,可以通过下列哪些方法(A B C)来改变和信号 signum 相关联的行为。
A. 如果 handler 是 SIG_IGN,那么忽略类型为 signum 的信号。
B. 如果 handler 是 SIG_DFL,那么类型为 signum 的信号行为恢复为默认行为。
C. 如果 hanlder 是用户定义的函数的地址,这个函数就被称为信号处理程序。

CSAPP3e 8.5.3 接收信号

可以使用 signal 函数修改和信号 signum 相关联的默认行为:

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

sighandler_t signal(int signum, sighandler_t handler);
//返回:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR(不设置errno)。

handler 的不同取值:

  • SIG_IGN: 忽略类型为 signum 的信号
  • SIG_DFL: 类型为 signum 的信号行为恢复为默认行为
  • 否则,handler 就是用户定义的函数的地址,这个函数称为信号处理程序
    • 只要进程接收到类型为 signum 的信号就会调用信号处理程序
    • 将处理程序的地址传递到 signal 函数从而改变默认行为,这叫作设置信号处理程序
    • 调用信号处理程序称为捕获信号
    • 执行信号处理程序称为处理信号
    • 当处理程序执行 return 时,控制会传递到控制流中被信号接收所中断的指令处

单选题:以下对Linux 系统提供的监控和操作进程的工具说法错误的是(C):
A. $TOP$:打印关于当前进程资源使用的信息。
B. $STRACE$:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。
C. $PS$:列出当前系统中的进程,不包括僵死进程。
D. $/proc$:一个虚拟文件系统,以 ASCII 文本格式输出大量内核数据结构的内容,用户程序可以读取这些内容。

CSAPP3e 8.4.3 回收子进程

为什么回收?–与fork创建相反!

  • 当进程终止时,它仍然消耗系统资源

    • Examples: Exit status, various OS tables(占用内存)
  • 称为僵死进程( “僵尸 zombie”进程)

  • 僵死进程占用内存资源、打开的 IO 资源

回收 (Reaping)

  • 父进程执行回收(using wait or waitpid)
  • 内核删掉僵死子进程、从系统中删除掉它的所有痕迹

父进程不回收子进程的后果:

  • 如果父进程没有回收它的僵死子进程就终止了,内核安排 init-养父 进程去回收它们(init 进程 PID 为 1,系统启动时创建,不会终止,是所有进程的祖先)
  • 长时间运行的进程应当主动回收它们的僵死子进程

ps 命令:列出当前系统中的进程

1
2
3
4
5
6
7
8
/* ps 命令示例 */

linux> ps
PID TTY TIME CMD
6585 ttyp9 00:00:00 tcsh
6639 ttyp9 00:00:03 forks
6640 ttyp9 00:00:00 forks <defunct>
6641 ttyp9 00:00:00 ps

ps 命令显示的子进程标记为 “defunct”,即僵死进程。


CSAPP3e 练习题8.4 P520

考虑下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() { 
int status;
pid_t pid;
printf("Hello\n");
pid = Fork();
printf("%d\n", !pid);
if (pid != 0) {
if (waitpid(-1, &status, 0) > 0) {
if (WIFEXITED(status) != 0)
printf("%d\n", WEXITSTATUS(status));
}
}
printf("Bye\n");
exit(2);
}

A. 这个程序会产生多少输出行?
B. 这些输出行的一种可能的顺序是什么?


A. 打印输出 6 行
B. 可能的输出:Hello, 1, 0, Bye, 2, Bye

CSAPP3e 8.4.3 回收子进程 >> 与子进程同步:wait/waitpid

wait 函数

父进程通过 wait/waitpid 函数回收子进程:

  • 调用 wait(&status) 等价于调用 waitpid(-1, &status, 0)
1
2
3
4
5
#include <sys/types.h> 
#include <sys/wait.h>

pid_t wait(int *statusp);
// 返回:如果成功,则为子进程的 PID, 如果出错,则为 -1。
  • 挂起当前进程的执行直到它的一个子进程终止。

  • 返回已终止子进程(可能很多,是个集合)的 pid。

  • child_status != NULL,则在该指针指向的整型量中写入关于终止原因和退出状态的信息。

    • wait.h 头文件中定义的宏来检查(具体作用查看):

      • WEXITSTATUS 是一个检验子进程退出的正常还是非正常和返回值的宏。

        WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

        WEXITSTATUS(status)WIFEXITED 返回非零值时,可以用这个宏来提取子进程的返回值,如果子进程调用 exit(5) 退出,WEXITSTATUS(status) 就会返回 5;如果子进程调用 exit(7)WEXITSTATUS(status) 就会返回 7。

        请注意,如果进程不是正常退出的,也就是说,WIFEXITED 返回 0,这个值就毫无意义。

  • 子进程完成结束的顺序是任意的(没有固定的顺序)。

  • 可用宏函数 WIFEXITEDWEXITSTATUS 获取进程的退出状态信息。

waitpid 函数

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

pid_t waitpid(pid_t pid, int *statusp, int options);
// 返回:如果成功,则为子进程的 PID, 如果 WNOHANG, 则为 0, 如果其他错误,则为 —1。
  1. 默认情况下(当 options = 0 时), waitpid 挂起调用进程的执行,直到它的等待集合(wait set) 中的一个子进程终止。
  2. 如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么 waitpid 就立即返回。

在上面这两种情况中,waitpid 返回导致 waitpid 返回的已终止子进程的 PID。此时,已终止的子进程已经被回收,内核会从系统中删除掉它的所有痕迹。

参数说明:

  1. 判定等待集合的成员

    pid 成员
    $pid=-1$ 等待集合是由父进程所有的子进程组成的。
    $pid>0$ 等待集合是一个单独的子进程,它的进程 ID 等于 pid。
  2. 修改默认行为

    通过设置 Options 来修改默认行为(默认Options = 0,等待子进程终止):

    options 行为
    WNOHANG 如果等待集合的任何子进程没有终止,就立即返回 0。
    父进程可继续其他工作。
    WUNTRACED 挂起当前进程,直到等待集合中的任一进程终止或停止,则返回其 pid。
    用于检查。
    WCONTINUED 挂起当前进程,直到等待集合中的任一进程从运行到终止一个停止的进程收到 SIGCONT 而恢复运行。
    `WNOHANG WUNTRACED`组合

CSAPP3e-学习笔记-第八章
https://luoyuy.top/posts/68b85099e992/
作者
LuoYu-Ying
发布于
2023年2月20日
许可协议