不落辰

知不可乎骤得,托遗响于悲风

0%

csapp-8-异常控制流

Exceptional Control Flow:
Exceptions and Processes

  • 异常
    异常控制流类型:从硬件到软件都有,Execption、Context Switching、Signal、NonLocal Jump。
    异常处理流程、异常四种类型:中断、故障、陷阱、终止
  • 进程
    进程 —> 不同进程需上下文切换 —-> 进入内核模式 —-> 依赖于 异常的机制
    进程调度流程 –> 0123
    并发概念
    如何从用户模式到内核模式 –> 通过异常 控制传递给处理程序时,会设置模式位。
  • 进程控制
    API:getpid、fork、waitpid、wait、sleep、execve、fork和execve区别
    SHELL:fork and execve

Exceptional Control Flow(ECF)

  • Processors do only one thing

    • 从给(处理器)processors加电开始,直到断电,CPU只是读取并执行一序列的指令,每次执行一条指令。
  • 程序计数器假设一个值的序列:a0,a1,…,an-1。其中,每个ak是某个相应指令Ik的地址。

  • 控制转移(control transfer):每次从ak到ak+1的过渡称为控制转移。

  • 控制流(control flow):这样的控制转移序列叫做处理器的控制流。

  • 突变:最简单的控制流是一个“平滑”序列,即Ik和Ik+1在内存中相邻。当Ik和Ik+1不相邻时,即为突变。

  • 控制流突变的机制

    • 跳转:jumps and branches
    • 调用及返回:call and return
    • 这两种机制是对由程序变量表示的内部程序状态中的变化作出反应。即 react to changes in program state
  • 但是这两个机制无法对系统状态的变化作出反应。difficult to react changes in system state

    • 这些系统状态不是被内部程序变量捕获的,且也不一定要和程序的执行相关。如
    • 从磁盘或者网络适配器来的数据
    • 除0
    • 键盘的ctrl + c
    • 硬件定时器定期产生信号
  • 异常控制流(Exceptional Control Flow,ECF):现代系统使控制流发生突变来对上述这类情况(也即system state)作出反应,这种突变一般称为异常控制流。

  • 异常控制流存在于计算机系统的各个层次

    • 硬件层(low level mechanisms)
      • Exceptions 异常
        • Changes in control flow in response to a system event
        • 通过硬件和os协作实现
    • 以下是Higher level mechanisms
    • 操作系统层
      • Process context switch 进程上下文切换
        • 内核通过上下文切换将控制从一个用户进程转移到另一个用户进程。
        • 通过os和硬件协作实现
    • 应用层
      • Signal 信号
        • 一个进程可以发送信号到另一个进程,而接收者会将控制转移到它的一个信号处理程序。
        • os实现
      • Nonlocal jumps:setjump() and longjump()
        • 通过setjump() and longjump()回避通常的栈规则。
        • C runtime library实现

Exceptions 异常

  • 异常:控制流中的突变,用来响应处理器状态中的某些变化。

    • 异常控制流的一种形式
    • 由硬件和os实现。
  • 异常流程

    • 任何情况下,当处理器检测到有事件发生时,他就会通过一张跳转表:异常表(exceptional table),进行一个间接过程调用(异常),控制转移到异常处理程序(exception handler)。当异常处理程序完成后,根据引起异常的事件的类型,将控制返回。关于返回,如下
      • 处理程序将控制返回给当前指令I_curr,即当实现发生时正在执行的指令
      • return to I_next
      • 处理程序终止被中断的程序
    • 关键词:事件发生、异常表、控制转移、异常处理程序、控制返回。
    • 其中异常中的事件
      • 可能和当前指令的执行相关:如虚拟内存却也,算术溢出,除0
      • 也可能无关:如系统定时器产生一个信号或者IO请求完成
  • 异常exception类似于过程调用call,但也有重要的不同之处。

      1. ret addr
      • call:在跳转到目标过程前,会先将返回地址push入栈。
      • exception:根据异常的类型,处理成序结束后返回到当前指令/下一条指令。
      1. exception:处理器会把额外的处理器状态压入栈中,处理程序返回时,重新执行被中断的程序需要这些指令。
      1. exception:完全运行在内核模式下,也即它们对所有的系统资源都有完全访问权限
  • 一旦硬件触发了异常,剩下的工作就是由异常处理成须在软件中完成.

  • 异常处理完之后如何返回:

    • 在处理成须处理完事件之后,它通过执行一条特殊的从中断返回的指令可选地返回到被中断的程序,
      • 该指令将适当的状态弹回到处理器的控制和数据寄存器中,
      • 如果异常中断的是一个用户程序,就将状态恢复为用户模式
      • 然后将控股之返回给被中断的程序。

Exception tables

  • 每种类型的异常都分配了一个唯一的非负整数的异常号。
    • 一些号码由处理器的设计者分配:除0、缺页、内存访问违例、断点、算数运算溢出
    • 一些由操作系统内核的设计者分配:系统调用以及来自外部IO设备的信号

  • 异常表中存储的是异常号k以及相应的code for exception handler k的地址
  • 当每次异常k发生时,会调用异常处理程序k。(处理器通过异常表的表目k 转到相应的处理程序)
  • 如何生成exception handler k的地址

Cateories Of Exception

  • 中断(interrupt)、陷阱(trap)、故障(fault)、终止(abort)
  • 异步:中断
  • 同步:陷阱、故障、终止。这类指令称之为故障指令
    • 由执行一条指令而造成的事件所引发的异常。

异常示例

  • 除法错误:除0,Linux shell报告为 Floating Exception
  • 一般保护故障:许多原因。通常是因为一个程序引用了一个未定义的虚拟内存区域,或者因为程序试图写只读的文本段。报告为Segmentation fault
  • 缺页:异常处理程序将适当的磁盘上的虚拟内存的一个页面映射到物理内存的一个页面。然后重新执行这条产生故障的指令。
  • 机器检查:致命硬件错误。机器检查处理程序曾布返回控制给应用程序

Asychronous Exceptions 异步异常

Interrupts 中断
  • 中断
    • 异步发生(硬件中断不是由任何一条专门的指令造成的,从这个意义上来说它是异步的)
    • 是来自外部IO设备的信号的结果(Indicated by setting the processor’s interrupt pin)
    • 返回到下一条指令
    • 硬件中断的异常处理程序称为中断处理程序
  • 中断处理
    • 中断处理的结果:程序继续执行,就好像没发生过中断一样。
  • Examples:
    • Timer interrupt
      • Every few ms, an external timer chip triggers an interrupt
      • Used by the kernel to take back control from user programs
    • I/O interrupt from external device
      • Hitting Ctrl-C at the keyboard
      • Arrival of a packet from a network
      • Arrival of data from a disk

Synchronous Exceptions 同步异常

Trap 陷阱 -> System Calls 系统调用
  • 陷阱

    • 有意的异常
    • 返回到下一条指令
    • Examples:system calls,breakpoint traps,special instructions
    • 陷阱最重要的用途:在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用 system call
    • 陷阱的异常处理程序称为陷阱处理程序
  • syscall n:当用户程序想要请求内核的服务n时,可以执行这条指令。

    • 陷阱指令:syscall
    • 执行syscall导致一个到异常处理程序的陷阱
    • 这个处理程序解析参数,并调用适当的内核程序。
  • syscall 是 64位系统 中断 的指令

  • int0x80 是 32位系统 中断 的指令
    https://cloud.tencent.com/developer/article/1492374
    https://blog.csdn.net/weixin_43363675/article/details/117944212

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

  • 系统级函数:C中的syscall可以直接调用任何系统调用,但我们没必要,因为C库提供了包装函数。我们将系统调用及其相关的包装函数,称为系统级函数。

  • C中的syscall库函数 #include<sys/syscall>

    • 调用系统调用,该系统调用的汇编语言接口具有指定的带指定参数的数字
    • 例如:当调用C库中没有包装函数的系统调用时,使用syscall()非常有用。
      • 如有一个函数gettid()可以得到线程的真正PID,但glibc并没有实现该函数,只能通过Linux的系统调用syscall来获取
      • ```c++
        tid = syscall(SYS_gettid);
        printf(“tid:%d\n”,tid);
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        121
        122
        123
        124
        125
        126
        127
        128
        129
        130
        131
        132
        133
        134
        135
        136
        137
        138
        139
        140
        141
        142
        143
        144
        145
        146
        147
        148
        149
        150
        151
        152
        153
        154
        155
        156
        157
        158
        159
        160
        161
        162
        163
        164
        165
        166
        167
        168
        169
        170
        171
        172
        173
        174
        175
        176
        177
        178
        179
        180
        181
        182
        183
        184
        185
        186
        187
        188
        189
        190
        191
        192
        193
        194
        195
        196
        197
        198
        199
        200
        201
        202
        203
        204
        205
        206
        207
        208
        209
        210
        211
        212
        213
        214
        215
        216
        217
        218
        219
        220
        221
        222
        223
        224
        225
        226
        227
        228
        229
        230
        231
        232
        233
        234
        235
        236
        237
        238
        239
        240
        241
        242
        243
        244
          - syscall()在进行系统调用之前保存CPU寄存器,从系统调用返回时恢复寄存器,如果发生错误,将系统调用返回的任何错误代码存储在errno


        ##### Fault 故障

        - **故障**
        - 不是故意的,由错误情况引起,可能被故障处理程序修正。
        - **可能返回到当前指令,也有可能终止**。
        - 修正了则返回到当前指令,重新执行。
        - 没修正则返回到内核的abort例程。
        ![](csapp-8-异常控制流1/2022-07-27-21-50-51.png)

        - 经典例子:**Page Fault 缺页异常**
        - 当指令引用一个虚拟地址,而与该地址相对应的物理页面不在内存中,因此必须从磁盘中取出时,就会发生故障。
        - 缺页处理程序从磁盘加载适当的页面,然后将控制返回给引起故障的指令。
        - 当指令再次执行时,相应的物理页面已经驻留在内存中了,指令就可以没有故障地运行完成了。
        - ![](csapp-8-异常控制流1/2022-07-27-22-10-55.png)
        - 例子2:一般保护故障![](csapp-8-异常控制流1/2022-07-27-22-11-19.png)

        ##### Abort 终止
        - 不是故意的,且不可修正的。是致命错误造成的结果,通常是一些硬件错误。
        ![](csapp-8-异常控制流1/2022-07-27-22-07-40.png)



        ## Processes 进程

        - 异常是允许os内核提供进程概念的基本构造块。
        - 经典定义:**一个执行中程序的实例**。
        - 系统中的每个程序都运行在某个进程的**上下文中(context)**。
        - **上下文**是由程序正确运行所需的**状态**组成的。具体见下文
        <!-- - 存放在内存中的程序的代码和数据
        - 栈
        - 通用目的寄存器的内容
        - 程序计数器
        - 环境变量
        - 打开的文件描述符等 -->
        - 如何在shell中运行可执行文件
        - 每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行文件。
        - 应用程序也能够创建新进程,并在这个新进程的上下文中运行它们自己的代码或其他应用程序。


        - **os如何实现进程的细节**
        - **上学再学**

        - 先关注**进程提供给应用程序的关键抽象**
        - **Logical control flow 一个独立的逻辑控制流**
        - 它提供一个假象,好像我们的程序**独占地使用处理器(CPU)**。
        - 通过内核提供的名为**上下文切换(context switching)**的机制实现
        - 上下文切换:是地址空间和寄存器的变化。
        - **Private address space 一个私有的地址空间**
        - 它提供一个家乡,好像我们的程序**独占地使用内存系统**。(每个运行的程序都有自己的上下文,并且看不到其他运行中程序的上下文)
        - 通过内核提供的名为**虚拟内存(virtual memory)**的机制实现
        ![](csapp-8-异常控制流1/2022-08-06-18-22-05.png)


        ### 单核cpu 多进程
        ![](csapp-8-异常控制流1/2022-08-06-18-56-44.png)
        - 单核cpu并发地执行多进程 -- Single processor executes multiple processes concurrently
        - 多个进程交织在一起 -- Process execusions interleaved (multitasking)
        - 进程地址由虚拟内存机制管理 -- Address spaces managed by virtual memory system (later in course)
        - 没有执行的进程的寄存器值保存在主存里 -- Register values for nonexecuting processes saved in memory

        - **进程调度流程**
        - **0.** Schedule next process for execution -- 决定重新开始的进程
        - **调度 Schedule**:在进程执行的某些时刻,内核可以决定抢占当前线程,并重新开始一个先前被抢占了的进程,这种决策即为调度。
        - 由内核中的调度器(scheduler)处理
        - **1.** 保存当前进程的上下文
        - **2.** 加载该进程保存的寄存器值到cpu中,并切换到该进程的进程地址。(这就是所谓的 **上下文切换(context switch),恢复某个先前被抢占的进程被保存的上下文**)
        - **3.** 将控制传递给这个新恢复的进程。
        ![](csapp-8-异常控制流1/2022-08-06-18-36-35.png)
        ![](csapp-8-异常控制流1/2022-08-06-18-41-40.png)
        - 下面又写了一遍,感觉挺重要的。


        ### 多核cpu 多进程
        ![](csapp-8-异常控制流1/2022-08-06-19-07-25.png)
        - Multicore processors
        - Multiple CPUs on single chip
        - Share main memory (and some of the caches) 共享主存和L3缓存
        - Each can execute a separate process
        - Scheduling of processors onto cores done by kernel(进程间的切换由cpu调度。切换流程同上)


        ### Concurrent Process 并发进程
        - Each Progess is logical control flow -- 逻辑控制流
        - Two Processes run concurrentlay (are concurrent) if their flows overlap in time --- 如果两个进程在时间上是重叠的(宏观),那么就程这两个进程并发的运行
        - **并发流 concurrent flow**:一个逻辑流在时间上与另一个流重叠。
        - Otherwise , they are sequential --- 否则,就称他们是连续的
        - Examples (running on signle core)
        - (相互)并发:A和B,A和C
        - 连续:B和C
        ![](csapp-8-异常控制流1/2022-08-06-19-23-17.png)
        - **关键**:进程轮流使用处理器。每个进程执行它的流的一部分,然后被抢占(preempted)(即暂时挂起,从这里可以看出挂起的时候不占用CPU),然后轮到其他进程。对于一个运行在这些进程之一的上下文中的程序,他看上去像是在独占的使用处理器。

        - **并发 concurrency**:多个流并发执行的一般现象。
        - **多任务 multitasking**:一个进程和其他进程轮流运行
        - **时间片 time slice**:一个进程执行它的控制流的一部分的每一时间段。因此,多任务也叫时间分片 time slicing
        - **并行流 parallel flow**:两个流并发地运行在不同的处理器核或计算机上。他们并行地运行(running in parallel),且并行地执行(parallel execution)


        ### 私有地址空间
        - 上面所说的假象之一:**private address space**
        - 进程为每个程序提供他自己的**私有地址空间**。(n位机器的地址空间:0,1,...,(2^n)-1。)
        - 什么叫做地址空间私有:一般,和这个空间中某个地址相关联的那个内存字节是不能被其他进程读或者写,
        - 每个私有地址空间相关联的内存的内容不同,但这些私有空间有相同的通用结构。(一般各个这样的地址空间互相不可见,空间中用户代码看不见内核部分的内存)
        ![](csapp-8-异常控制流1/2022-08-06-20-06-24.png)


        ### 用户模式 内核模式

        - **用户模式、内核模式**
        - 为了进程抽象,cpu提供的一种机制,来**限制一个应用可以执行的命令和可访问的地址空间范围**。
        - **模式位 mode bit**
        - 控制寄存器中的模式位描述了**进程当前享有的特权**
        - **内核模式 / 超级用户模式**
        - 设置了模式位
        - 一个运行在内核模式的进程可以执行指令集中的**任何指令**
        - 并且可以访问系统中的**任何内存位置**。
        - **用户模式**
        - 用户模式中的进程不允许执行特权指令,如停止处理器,改变模式为,发起IO操作
        - 不允许用户模式中的进程直接引用地址空间中内核区的代码和数据。(否则会导致致命的保护故障);用户程序必须通过系统调用接口间接地访问内核代码和数据。
        - **用户模式变为内核模式**
        - **唯一方法**
        - **通过异常**:中断、故障、陷阱(系统调用)
        - 异常发生时,控制传递到异常处理程序,
        - **处理器设置模式位,用户模式变为内核模式。**
        - 如前所说,异常处理程序完全运行在内核模式中
        - 返回用户程序时,处理器将内核模式切换为用户莫斯

        - ***上下文切换Context Switching** 需要进入内核模式。也就需要依赖异常机制。



        ### Context Switch 上下文切换

        #### 上下文 context

        - 上下文切换是为了调度进程,而不是为了进入内核模式。进入内核模式是通过异常机制。
        - os内核通过**上下文切换**的机制,来实现multitasking多任务。
        - **上下文切换 Context Switch**机制建立在**异常 Exception**之上
        - 上下文是内核重新启动一个被抢占的进程所需的状态,由一些对象的值组成。包括如下
        - 进程上下文采用进程PCB表示。在Linux中,PCB就是task_struct
        - 通用目的寄存器
        - 浮点寄存器
        - 程序计数器
        - 用户栈
        - 状态寄存器:new,ready,running,waiting,terminated.
        - 内核栈
        - 以及各种内核数据结构
        - 描述地址空间的页表
        - 包含当前进程信息的进程表
        - 包含进程已打开文件信息的文件表



        #### 调度、上下文切换

        - **进程调度流程**
        - **0.** Schedule next process for execution -- 决定重新开始的进程
        - **调度 Schedule**:在进程执行的某些时刻,内核可以决定抢占当前线程,并重新开始一个先前被抢占了的进程,这种决策即为调度。
        - 由内核中的调度器(scheduler)处理
        - **1.** 保存当前进程(旧)的上下文
        - 内核将旧进程状态保存进其PCB。
        - **2.** 加载要调度的进程的上下文~~保存的寄存器值~~到cpu中,并切换到该进程的进程地址。(这就是所谓的 **上下文切换(context switch),恢复某个先前被抢占的进程被保存的上下文**)
        - **3.** 将控制传递给这个新恢复的进程。
        ![](csapp-8-异常控制流1/2022-08-06-18-36-35.png)
        ![](csapp-8-异常控制流1/2022-08-06-18-41-40.png)



        - **Processes are managed by a shared chunk of memory-resident OS code called kernal** --- 进程们是由一块被常驻在主存中的、被称为内核的操作系统代码 来管理的。
        - **Important:the kernal is not a separate process , but rather runs as part of some existing process**.
        - 内核不是一个独立的进程,而是作为于每个进程的一部分。(等待被调用?)
        - Control flow passes from one process to another via a context switch
        ![](csapp-8-异常控制流1/2022-08-06-21-55-50.png)


        #### 上下文切换情况

        - 单核多进程还有意义吗?我觉得下面这个情况就能说明有意义。

        - 内核代表用户执行**系统调用时,可能会发生上下文切换**。如果系统调用因为等待某个事件而发生**阻塞**,那么**内核**可以让当前**进程休眠**,**切换到另一个进程**。(所以阻塞了就不会占cpu了,因为内核会执行上下文切换到另一个进程)如下
        - read系统调用:进程A调用read,那么内核可以选择执行上下文切换,运行另一个进程B,而不是等待磁盘数据到达。
        - sleep系统调用:显示地请求让调用进程休眠。
        - 一般来说,即使系统调用没阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。

        - **中断也会引发上下文切换**
        - 比如定时器中断(1ms/10ms),每次定时器中断时,内核就能判定当前进程运行了足够长时间,好切换到下一个进程。

        - **例子**
        ![](csapp-8-异常控制流1/2022-08-06-22-36-01.png)
        ![](csapp-8-异常控制流1/2022-08-06-22-39-34.png)


        - 上下文切换的时间是**纯粹的开销**,因为在切换时系统并没有做任何有用工作。
        - 典型速度为**几ms**
        - 上下文切换的速度因机器不同而不同,它依赖于内存速度、必须复制的寄存器数量、是否有特殊指令。


        #### 内核 躺着的代码?
        ------
        - [Linux 内核的操作系统是不是得一直运行着? - 高鹏的回答 - 知乎](https://www.zhihu.com/question/23561375/answer/25345790)
        - **虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存**。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

        - 内核就是躺着等待别人调用的代码,只有通过异常才可以调用
        - 如何调用:进程通过中断故障陷阱这样的异常 改变模式位 用户模式变为内核模式 然后再请求内核提供的服务(系统调用之类的)。
        - **小疑问**
        - 为什么要把内核地址设置在每个进程的高位?
        - 每个虚拟进程地址中的内核区地址我觉得应当都是指向同一块物理内存
        - 这里的东西在虚拟内存那一章应该挺详细。
        - 是为了用户代码可以切换到内核模式调用系统调用,或者说进入异常处理程序。
        - 我认为再说简单一点就是用户代码可以知道内核代码的地址位置,这样就可以找到内核代码。
        -------

        ## Process Control


        ### System Call Error Handling
        - unix系统级函数error时,通常返回-1,并且设置全局变量errno。
        - You must check the return status of every system-level function
        - **包装函数**
        ```c
        void unix_error(char *msg) /* Unix-style error */
        {
        fprintf(stderr, "%s: %s\n", msg, strerror(errno));
        exit(0);
        }

        if ((pid = fork()) < 0)
        unix_error("fork error");


        pid_t Fork()
        {
        pid_t pid;
        if( ( pid=fork() ) < 0)
        {
        unix_error("Fork error");
        }
        return pid;
        }

        pid = Fork();

进程控制

getpid

1
2
3
4
5
#include<sys/types.h>   //  typedef int pid_t
#include<unistd.h> // 声明getpid、getppid

pid_t getpid(); // 返回当前进程id
pid_t getppid(); // 返回父进程id

进程状态

  • 进程状态
  • 运行 RUNNING
    • 进程要吗在CPU上执行,要么在等待被执行且最终会被内核调度
  • 停止 Stopped
    • 进程的执行被挂起(suspended),且不会被执行。
    • 当收到信号 SIGSTOP、SIGTSTP、SIGTIN、SIGTTOU时,进程停止,且保持停止知道受到一个SIGCONT信号,这个时刻,进程再次开始运行。
  • 终止 Terminated
    • 进程永远地停止了。以下三个原因
    • 收到一个信号,该信号的默认行为是终止进程
    • 从main中return
    • 调用exit函数

fork 创建进程 / exit销毁进程

  • exit

    1
    2
    3
    4
    5
    6
    #include<stdlib.h>
    void exit(int status);
    // 1. 以status退出状态来终止进程。
    // 2. Convention: normal return status is 0, nonzero on error
    // 3. Another way to explicitly set the exit status is to return an integer value from the main routine
    // 4. exit is called once but never returns.
  • fork

    1
    2
    3
    #include<sys/types.h>
    #include<unistd.h>
    pid_t fork(void);
    • Child returns 0
    • Parent returns Child’s pid
    • fork:called once , but returns twice
    • 子进程和父进程几乎完全相同。
      • Child得到与Parent相同但独立 (identical but separate) 用户级虚拟地址空间副本
        • 包括代码和数据段、堆、共享库、用户栈
      • Child获得与父进程打开的文件描述符副本(open file descriptors)
      • Child和Parent的PID不同。
  • 进程图,拓扑排序。

Reaping Child Process 回收子进程

  • man wait
    1
    2
    3
    4
    5
    6
    NOTES
    A child that terminates, but has not been waited for becomes a "zombie". **The kernel maintains a minimal set of information about the zombie process** (PID, termination
    status, resource usage information) **in order to allow the parent to later perform a wait to obtain information about the child**. As long as a zombie is not removed from
    the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes. If a parent
    process terminates, then its "zombie" children (if any) are adopted by init(1), (or by the nearest "subreaper" process as defined through the use of the prctl(2)
    PR_SET_CHILD_SUBREAPER operation); **init(1) automatically performs a wait to remove the zombies**.
  • 僵死进程 zombie:当一个进程由于某种原因终止,内核并不是立即把他从系统中清除。相反,进程被保持在一种已终止的状态,直到被它的父进程回收一个终止了但还未被回收的进程称为僵死进程

  • Reaping 回收

    • 当父进程回收已终止的子进程时(通过wait or waitpid)
    • 内核将子进程的退出状态(exit status)传递给父进程,
    • 然后抛弃已终止的进程,(kernel then deletes zombie child process)
    • 从此时开始,该进程就不存在了。
  • 如果父进程没有回收其子进程?

    • 长时间运行的程序(如shell)即使僵死进程并没有运行,也在消耗内存资源。
    • 父进程已终止,子进程没终止:没被回收的子进程称为孤儿进程。内核安排init进程(init process ;pid=1)成为孤儿进程的养父。内核安排init进程去回收这些孤儿进程。
    • 父进程没终止,子进程终止,父进程终止了也没去回收子进程:内核安排init进程去回收这些孤儿进程
    • 因此,长时间不停的父进程需要显示地wait/waitpid回收子进程。

waitpid

  • DESCRIPTION
    • All of these system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state
    • change is considered to be:
      • the child terminated;
        • In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a “zombie” state (see NOTES below).
      • the child was stopped by a signal;
      • or the child was resumed by a signal.
原型
1
2
3
4
5
#include<sys/types.h>
#include<sys/wait.h> // WNOHANG 一类option
pid_t waitpid(pid_t pid, int *statusp, int options);
// func : 等待调用进程的子进程终止或停止
// return val: 回收到的子进程pid
  • 阅读功能步骤
      1. 默认情况下,即 options=0,waitpid挂起当前进程(挂起不占cpu)
      • 是否挂起(阻塞等待)由options决定
      1. 直到它的等待集合(wait set)的一个子进程终止。
      • wait set 由 pid 决定
      • 若wait set集合中的一个进程在刚调用的时刻就已经终止了,那么waitpid就立刻返回
      • 若wait set集合中没有已经停止的,就阻塞等待。
      1. 将返回的子进程的状态信息存入statusp
      • statusp = null ?
pid – 判定等待集合的成员
  • pid:判定等待集合的成员
  • pid>0:一个单独的pid子进程
  • pid=-1:本进程作为父进程的所有子进程
  • 其他还有,略。
options – 是否挂起
  • 默认行为 0

    • 挂起调用进程,直到有子进程终止。
    • return 已终止的子进程。
  • WNOHANG

    • 等待集合中的任何子进程都还没终止,则立刻return 0。
  • WUNTRACED

    • 挂起当前进程,直到等待集合中的一个进程变成已终止或者被停止。(比起默认只监听终止,这个还监听了停止(一般是被signal (ztrl+z)SIGTSTP停止,终止一般是 exit,或者被信号如(ctrl+c)SIGINT杀死)
    • return的pid为导致返回的已终止或被停止子进程的pid。
  • WCONTINUED

    • 挂起调用进程的执行,
      • 直到等待集合中一个正在运行的进程终止 或
      • 等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。
  • WNOHANG | WUNTRACED

    • 立即返回,如果等待集合中的子进程都没被停止或终止,则return 0
    • 如果有一个停止或终止,则return 该子进程pid。
statusp – 检查已回收子进程的退出状态
  • WIFEXITED and WEXITSTATUs
    1
    2
    3
    4
    5
    6
    7
    if(WIFEXITED(status)){                  //  true  <--->  child process return  /  exit
    printf("child %d terminated normally with exit status %d\n",pid,WEXITSTATUS(status));
    // return the exist status of a normally exit process 8位
    // This macro should be employed only if WIFEXITED returned true.
    }else{
    printf("child %d terminated abnormally\n",pid);
    }
  • WIFSIGNALED and WTERMSIG
    1
    2
    3
    4
    5
    // returns true if the child process was terminated by a signal.
    if(WIFSIGNALED(status)){
    // returns the number of the signal that caused the child process to terminate. This macro should be employed only if WIFSIGNALED returned true.
    int signal = WTERMSIG(status)
    }
  • WIFSTOPPED

    1
    2
    3
    4
    5
    6
    // returns  true  if  the  child  process  was stopped by delivery of a signal; this is possible only if the call was done using WUNTRACED or when the child is being traced (see ptrace(2)).

    if(WIFSTOPPED(status)){
    int signal = WSTOPSIG(status);
    // returns the number of the signal which caused the child to stop. This macro should be employed only if WIFSTOPPED returned true.
    }
  • WIFCONTINUED

    1
    2
    3
    if(WIFCONTINUED(status)){
    printf("continue\n");
    }

return

  • RETURN VALUE
    • wait():
      • on success, returns the process ID of the terminated child;
      • on error, -1 is returned.
    • waitpid():
      • on success, returns the process ID of the child whose state has changed;
      • if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned.
      • On error, -1 is returned.
错误条件
  • waitpid默认阻塞HANG情况下

    • return -1 && errno = ECHILD:调用进程没有子进程剩余了,都被回收玩了。
    • return -1 && errno = EINTR:waitpid函数被一个信号中断。
  • waitpid(WNOHANG)情况下

    • WNOHANG =-1是error ,=0是集合里剩余的所有进程还没有一个结束的

wait

1
2
3
#include<sys/wait.h>
#include<sys/types.h>
pid_t wait(int *statusp);
  • wait(&status) <===> wait(-1,&status,0);

  • waitpid 回收子进程 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    // 1. fork
    // 2. waitpid
    // 3. errno
    #include<unistd.h>
    #include<sys/wait.h>
    #include<sys/types.h>
    #include<stdio.h>
    #include<sys/errno.h>
    #include<stdlib.h>

    const int N = 5;

    int main()
    {

    for(int i=0;i<N;++i) // N个子进程
    {
    if(fork()==0){
    exit(i); // exit status
    }
    }

    int status,pid;
    // 当前进程 **挂起** 等待子进程集合中有结束的进程
    // 不会占用cpu。内核会调度到其他不阻塞进程。
    while( (pid = waitpid(-1,&status,0)) > 0 ){
    if(WIFEXITED(status)){ // true <---> child process return / exit
    printf("child %d terminated normally with exit status %d\n",pid,WEXITSTATUS(status)); // return the exist status of a normally exit process
    }else{
    printf("child %d terminated abnormally\n",pid);
    }
    }


    // 检验是否回收完所有子进程
    // waitpid -- return -1 & errno = ECHILD
    // 只有当 waitpid 的选项是 挂起的时候才可以这样检查errno
    // 如果waitpid的选项是 WNOHANG,那么就不能这样检查,因为不阻塞,返回,会有没结束的子进程. 此时errno应该 = success
    if(errno == ECHILD){
    printf("all child processes is reaped by parent\n");
    }else{
    printf("waitpid error");
    }
    return 0;
    }

    shc@shc-virtual-machine:~/code/csapp_try/process$ ./waitpid1
    child 110852 terminated normally with exit status 0
    child 110853 terminated normally with exit status 1
    child 110854 terminated normally with exit status 2
    child 110855 terminated normally with exit status 3
    child 110856 terminated normally with exit status 4
    all child processes is reaped by parent



    ERRORS
    ECHILD (for wait()) The calling process does not have any unwaited-for children.

    ECHILD (for waitpid() or waitid()) The process specified by pid (waitpid()) or idtype and id (waitid()) does not exist or is not a child of the calling process. (This
    can happen for one's own child if the action for SIGCHLD is set to SIG_IGN. See also the Linux Notes section about threads.)

    EINTR WNOHANG was not set and an unblocked signal or a SIGCHLD was caught; see signal(7).

    EINVAL The options argument was invalid.

sleep

  • sleep
    • 休眠secs秒
      1
      2
      #include<unistd.h>
      unsigned int sleep(unsigned int secs);
    • return value:
      • 时间已到,返回0
      • 时间未到,返回剩下的要休眠的秒数(sleep信号可能会被信号中断)
  • pause

    • 让调用进程休眠,直到该进程收到一个信号。
      1
      2
      #include<unistd.h>
      int pause(void);
    • return -1
  • snooze函数,同sleep,就是加一条打印

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    #include<unistd.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<signal.h>

    unsigned int snooze(unsigned int secs)
    {
    int t_left = sleep(secs);
    printf("Slept for %d of %d secs\n",secs-t_left,secs);
    return t_left;
    }


    // 信号处理程序(处理SIGNIT)。功能:捕获信号后,将控制返回给snooze函数。(本来对于SIGINT的默认行为是终止进程,现在变成返回控制给用户程序 如P532图8-31)
    // 处理SIGINT信号,将控制返回给中断的语句的下一条语句
    void sigint_handler(int sig)
    {
    printf("prog catch %d\n",sig);
    return ;
    }

    int main(int argc,char *argv[],char *envp[])
    {
    if(argc!=2){
    printf("Usage : ./signal_snooze secs\n");
    return 0;
    }
    // signal
    signal(SIGINT,sigint_handler);
    snooze(atoi(argv[1]));

    return 0;
    }

    shc@shc-virtual-machine:~/code/csapp_try/process$ ./signal_snooze 10
    ^Cprog catch 2
    Slept for 3 of 10 secs

execve

1
2
#include<unistd.h>
int execve(const char *filename,const char *argv[],const char *envp[]);
  • 功能:在当前进程的上下文中加载并运行一个新程序

  • Called once and never returns,除非发生错误,会返回。

  • Loads and runs in the current process

    • filename:
      • Executable file filename,Can be object file or script file
    • argv:指向一个指针数组
      • argv[0] = 可执行文件名
      • 剩余的是参数
    • envp:指向一个指针数组
      • 每个指针指向一个环境变量字符串
      • name = value
  • 程序是一堆代码和数据;程序可以作为目标文件存在于磁盘上,或者作为段存在于地址空间中。进程是执行中程序的一个具体的实例;程序总是运行在某个进程的上下文中。

  • fork和execve区别

    • fork新创建了一个进程,这个新的进程的用户级虚拟地址空间是父进程的复制品,在这个新进程里,运行相同的程序。
    • execve在当前进程的上下文中加载并运行一个新的程序。它会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然具有相同的pid,并继承了调用execve函数时已打开的所有文件描述符。

SHELL

  • fork and execve












https://www.bilibili.com/video/BV1iW411d7hd?p=15&t=2894.1


https://www.bilibili.com/video/BV1iW411d7hd?p=15&t=3985.4