CSAPP 异常控制流 笔记

admin 2024年8月18日00:20:23评论6 views字数 4780阅读15分56秒阅读模式

应用是如何与操作系统进行交互的?

异常

异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。

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

理解的还是比较模糊,得调调源码看看细节。

类别

  • 中断
  • 陷阱
  • 故障
  • 终止

进程

关键抽象:

  • 一个独立的逻辑控制流,它提供一个假象,好像程序独占了处理器。
  • 一个私有的地址空间,它提供了一个假象,好像程序独占了内存系统。

进程实体

程序段、数据段、PCB 三部分组成了进程实体(进程映像)。

定义

传统定义:

  • 进程是程序的一次执行过程
  • 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
  • 进程是具有独立功能的程序在数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位

进程是进程实体的运行过程,是系统进行资源分配调度的一个独立单位。

一道程序在一个数据集上的一次执行过程。

PCB

  • 基本描述信息

    • 进程名(通常用文件名或命令名称表示)

    • 进程标识符 PID(唯一标识符)

    • 用户标识符 UID

    • 当前进程状态

  • 管理信息

    • 程序和数据的地址
    • I/O 操作相关参数
    • 进程通信信息
  • 控制信息

    • 现场信息(各种寄存器值,进程切换时这些运行情况都要保存到PCB中)
    • 调度参数
    • 同步、互斥信号量

组织形式

  • 链接方式:按照进程状态将 PCB 分为多个队列,操作系统持有指向各个队列的指针

  • 索引方式:根据进程状态的不同,建立几张索引表(底层是个啥),操作系统持有指向各个索引表的指针

特征

  • 动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的(最基本的特征)
  • 并发性:内存中有多个进程实体,各进程可并发执行
  • 独立性:进程是操作系统分配资源的基本单位,一个进程的程序和数据只能由该进程本身访问。
  • 异步性:各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题
  • 结构性:每个进程都配置一个 PCB。结构上来看,进程由程序段、数据段和PCB组成

状态

运行态:CPU、其他资源均满足

就绪态:已具备运行条件,只欠CPU

阻塞态:因等待某一事件而不能执行。CPU、其他资源均不满足。等待操作系统或其他进程唤醒。

创建态:操作系统为进程分配资源、初始化PCB

终止态:进程正在从系统中撤销,操作系统将回收进程所拥有的资源、撤销PCB

CSAPP 异常控制流 笔记

注意:

  • 不能由阻塞态直接转换为运行态

    申请的资源被分配,或等待时间发生了,只代表其他资源满足,此时进入就绪态,还要等CPU。

  • 不能由就绪态转换成阻塞态。

    因为进入阻塞态需要进程主动请求,必然需要在运行时才能发出请求。

逻辑控制流

并发流

私有地址空间

用户模式和内核模式

运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法时通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

上下文切换

上下文是由程序正确运行所需的状态组成的。

这个状态包括存放在内存中的程序的代码和数据,以及它的栈、寄存器、PC、环境变量和打开文件描述符的集合。
Golang 从 2009 年正式发布以来,依靠其极高运行速度和高效的开发效率,迅速占据市场份额。Golang 从语言级别支持并发,通过轻量级协程 Goroutine 来实现程序并发运行。

Goroutine 非常轻量,主要体现在以下两个方面:

上下文切换代价小: Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP…等寄存器的刷新;

内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;

Golang 程序中可以轻松支持10w 级别的 Goroutine 运行,而线程数量达到 1k 时,内存占用就已经达到 2G。

进程控制

程序员角度,可认为进程总是处于三种状态之一:

  • 运行:进程要么在 CPU 上执行,要么在等待被执行且最终会被内核调度。

  • 停止:进程的执行被挂起(suspended),且不会被调度,直到收到 SIGCONT 信号再次运行。

  • 终止:进程永远地停止了。

    三种原因可使得进程停止:

    • 收到一个信号,该信号的默认行为是终止进程。
    • 从主程序返回。
    • 调用 exit 函数。

主要功能:对系统中的所有进程实施有效的管理,包括创建新进程、撤销已有进程、实现进程状态转换。

原语:是一种特殊的程序,执行必须一气呵成,不可中断。用开、关中断实现的。

唤醒进程:

  • 从等待队列中移出

  • 修改 PCB 进程状态

  • 加入就绪队列

获取进程 ID

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

创建和终止进程

  • 建立 PCB,生成 pid
  • 初始化 PCB
  • 加入就绪队列
#inlcude <stdlib.h>

pid_t fork(void);
void exit(int status);

回收子进程

当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递个父进程,然后抛弃已终止进程,从此时开始,该进程才不存在。

一个终止了但还未被回收的进程成为僵死进程(zombie)。

僵死进程已经终止了,但内核仍保留着它的某些状态直到父进程回收它为止。

一个进程可通过调用 waitpid() 来等待它的子进程终止或者停止。

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int* statusp, int options);

(TODO:深入整理)

让进程休眠

  • 修改 PCB 中的进程状态
  • 现场保护
  • 将进程加入合适的等待队列
#include <unistd.h>

unsigned int sleep(unsigned int secs);

int pause(void); // 休眠至进程接收到一个信号

加载并运行程序

execve() 函数在当前进程的上下文加载并运行一个新程序。

fork() 函数则是在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。

#include <unistd.h>

int execve(const char* filename, const char** argv, const char** envp);

与 fork 一次调用两次返回不同,execve 调用一次并从不返回。

调度

一旦资源不足时就需要进行调度,比如现实中的十字路口,需要红绿灯来调度。

作业与进程调度的区别:作业是内外存的调度,进程是CPU与内存间的调度

性能指标

  • 周转时间和平均周转时间

    周转时间 = 完成时刻 - 提交作业时刻

    平均周转时间 = 总周转时间 / n

    加权平均周转算法 =

  • 响应时间

  • 评价调度性能的其他指标

    • 公平合理
    • 提高资源利用率
    • 吞吐量

作业调度

作业调度:按一定的策略从后备队列中选择一部分作业,为他们分配运行所必须的资源、创建进程的过程。

总的来说,都是一个作业执行结束后再开始调度。

提交状态:

后备状态:

执行状态:作业进入了内存

完成状态:

先来先服务算法(FCFS)

对长作业有利、短作业不利

短作业优先算法(SJF)

上一作业运行结束后,在就绪作业里选择时间最短的。

高响应比优先算法(HRN)

响应比 = (系统当前时间 - 作业提交时间) / 作业大小

优先选择响应比最大的作业

进程调度

从进程就绪队列中选一个进程,让其占用CPU运行。

时间片轮转算法(RR)

公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。

优点:公平、响应快,适用于分时操作系统。

缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度。

优先级算法(Priority)

调度时选择优先级最高的作业 / 进程。

优先级分配有静态和动态两种。

对于 I/O 繁忙和 CPU 繁忙的进程,应该赋予 I/O 繁忙进程更高的优先级,有利于提高并行程度。

多级反馈队列算法

时限调度算法

用于实时系统的调度。

交换调度

缓解内存紧张,将一部分就绪状态或阻塞状态进程调出到外存,需要的时候再调回来,即内外存交换。

设备调度

让哪个进程使用该设备。

死锁

根本原因:系统拥有的资源数量小于各进程对资源的需求总数。

死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进。

​ 至少有两个或两个以上的进程同时死锁,死锁进程一定处于阻塞态。

饥饿:可能只有一个进程发生饥饿。

死循环:可能只有一个进程发生死循环,死循环的进程也可能就绪。

死锁和饥饿是操作系统要解决的问题,死循环是程序员的事情。

如果系统中的所有进程存在一个可完成的执行序列P1,…,Pn,则称系统处于安全状态。

必要条件

  • 互斥:对必须互斥使用的资源的争夺才会导致死锁。
  • 不剥夺:进程保持的资源只能主动释放,不能被强行剥夺。
  • 请求与保持:保持着某些资源不放的同时,请求别的资源。
  • 环路等待:存在一种进程资源的循环等待链。循环等待未必死锁。

处理策略

预防:破坏死锁产生的四个必要条件

避免:避免系统进入不安全状态(银行家算法)

检测和解除:允许死锁发生,系统负责检测出死锁并解除。

忽略:鸵鸟算法

系统调用错误处理

参见 error.h,这里想说的还是对错误返回处理的封装。

信号

一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的时间。在 Linux 上支持了 30 中不同类型的信号。每个信号类型都对应于某种系统事件。

低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。

信号提供了一种机制,通知余户进程发生了这些异常。

发送

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

发送信号可以有如下两种原因:

  • 内核检测到一个系统事件,比如除零错误或者子进程终止。
  • 一个进程调用了 kill 函数,显示地要求内核发送一个信号给目的进程。

一个进程可以发送信号给它自己。

Unix 系统发送信号的机制

  • 进程组

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

    默认一个子进程和它父进程同属于一个进程组。

    #include <unistd.h>

    pid_t getpgrp(void); // 返回调用进程的进程组 ID

    int setpgid(pid_t pid, pid_t pgid); // 设置进程组成功返回 0,否则为 -1
  • /bin/kill 程序

    kill -9 1023# 杀掉 1023 进程
    kill -9 -1023# 杀掉 1023 进程组的每个进程
  • 从键盘发送

    CTRL + C / Z :终止 / 挂起

  • 用 kill 函数

    #include <sys/types.h>
    #include <signal.h>

    int kill(pid_t pid, int sig);
  • 用 alarm 函数发送

    进程可通过调用 alarm 函数向自己发送 SIGALRM 信号,网络编程中可拿来处理超时。

    #include <unistd.h>

    unsigned int alarm(unsigned int secs);

接收

当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个信号处理函数的用户层函数捕获这个信号。

处理

非本地跳转

操作进程的工具

  • strace

    打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。

    这是一个超级牛逼的工具,比如你想跟进 PHP 内核底层实现,这就能收获大量信息。

  • ps

    列出当前系统中的进程(包括僵尸进程)。

  • top

    打印出关于当前进程资源使用的信息。

  • pmap

    显示进程的内存映射。

  • /proc

    一个虚拟文件系统,以 ASCII 文本格式输出大量内核数据结构的内容(从这也能感受到 Linux 文件的重要性),用户程序可以读取这些内容。

    (TODO:补充详细结构及其作用)

FROM:wywwzjj

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年8月18日00:20:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CSAPP 异常控制流 笔记https://cn-sec.com/archives/3075479.html

发表评论

匿名网友 填写信息