应用是如何与操作系统进行交互的?
异常
异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
理解的还是比较模糊,得调调源码看看细节。
类别
- 中断
- 陷阱
- 故障
- 终止
进程
关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像程序独占了处理器。
- 一个私有的地址空间,它提供了一个假象,好像程序独占了内存系统。
进程实体
程序段、数据段、PCB 三部分组成了进程实体(进程映像)。
定义
传统定义:
- 进程是程序的一次执行过程
- 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
- 进程是具有独立功能的程序在数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
一道程序在一个数据集上的一次执行过程。
PCB
基本描述信息
进程名(通常用文件名或命令名称表示)
进程标识符 PID(唯一标识符)
用户标识符 UID
当前进程状态
管理信息
- 程序和数据的地址
- I/O 操作相关参数
- 进程通信信息
控制信息
- 现场信息(各种寄存器值,进程切换时这些运行情况都要保存到PCB中)
- 调度参数
- 同步、互斥信号量
组织形式
链接方式:按照进程状态将 PCB 分为多个队列,操作系统持有指向各个队列的指针
索引方式:根据进程状态的不同,建立几张索引表(底层是个啥),操作系统持有指向各个索引表的指针
特征
- 动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的(最基本的特征)
- 并发性:内存中有多个进程实体,各进程可并发执行
- 独立性:进程是操作系统分配资源的基本单位,一个进程的程序和数据只能由该进程本身访问。
- 异步性:各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题
- 结构性:每个进程都配置一个 PCB。结构上来看,进程由程序段、数据段和PCB组成
状态
运行态:CPU、其他资源均满足
就绪态:已具备运行条件,只欠CPU
阻塞态:因等待某一事件而不能执行。CPU、其他资源均不满足。等待操作系统或其他进程唤醒。
创建态:操作系统为进程分配资源、初始化PCB
终止态:进程正在从系统中撤销,操作系统将回收进程所拥有的资源、撤销PCB
注意:
不能由阻塞态直接转换为运行态
申请的资源被分配,或等待时间发生了,只代表其他资源满足,此时进入就绪态,还要等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
|
创建和终止进程
- 建立 PCB,生成 pid
- 初始化 PCB
- 加入就绪队列
|
回收子进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递个父进程,然后抛弃已终止进程,从此时开始,该进程才不存在。
一个终止了但还未被回收的进程成为僵死进程(zombie)。
僵死进程已经终止了,但内核仍保留着它的某些状态直到父进程回收它为止。
一个进程可通过调用 waitpid()
来等待它的子进程终止或者停止。
|
(TODO:深入整理)
让进程休眠
- 修改 PCB 中的进程状态
- 现场保护
- 将进程加入合适的等待队列
|
加载并运行程序
execve()
函数在当前进程的上下文加载并运行一个新程序。
fork()
函数则是在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。
|
与 fork 一次调用两次返回不同,execve 调用一次并从不返回。
调度
一旦资源不足时就需要进行调度,比如现实中的十字路口,需要红绿灯来调度。
作业与进程调度的区别:作业是内外存的调度,进程是CPU与内存间的调度
性能指标
周转时间和平均周转时间
周转时间 = 完成时刻 - 提交作业时刻
平均周转时间 = 总周转时间 / n
加权平均周转算法 =
响应时间
评价调度性能的其他指标
- 公平合理
- 提高资源利用率
- 吞吐量
作业调度
作业调度:按一定的策略从后备队列中选择一部分作业,为他们分配运行所必须的资源、创建进程的过程。
总的来说,都是一个作业执行结束后再开始调度。
提交状态:
后备状态:
执行状态:作业进入了内存
完成状态:
先来先服务算法(FCFS)
对长作业有利、短作业不利
短作业优先算法(SJF)
上一作业运行结束后,在就绪作业里选择时间最短的。
高响应比优先算法(HRN)
响应比 = (系统当前时间 - 作业提交时间) / 作业大小
优先选择响应比最大的作业
进程调度
从进程就绪队列中选一个进程,让其占用CPU运行。
时间片轮转算法(RR)
公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。
优点:公平、响应快,适用于分时操作系统。
缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度。
优先级算法(Priority)
调度时选择优先级最高的作业 / 进程。
优先级分配有静态和动态两种。
对于 I/O 繁忙和 CPU 繁忙的进程,应该赋予 I/O 繁忙进程更高的优先级,有利于提高并行程度。
多级反馈队列算法
时限调度算法
用于实时系统的调度。
交换调度
缓解内存紧张,将一部分就绪状态或阻塞状态进程调出到外存,需要的时候再调回来,即内外存交换。
设备调度
让哪个进程使用该设备。
死锁
根本原因:系统拥有的资源数量小于各进程对资源的需求总数。
死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进。
至少有两个或两个以上的进程同时死锁,死锁进程一定处于阻塞态。
饥饿:可能只有一个进程发生饥饿。
死循环:可能只有一个进程发生死循环,死循环的进程也可能就绪。
死锁和饥饿是操作系统要解决的问题,死循环是程序员的事情。
如果系统中的所有进程存在一个可完成的执行序列P1,…,Pn,则称系统处于安全状态。
必要条件
- 互斥:对必须互斥使用的资源的争夺才会导致死锁。
- 不剥夺:进程保持的资源只能主动释放,不能被强行剥夺。
- 请求与保持:保持着某些资源不放的同时,请求别的资源。
- 环路等待:存在一种进程资源的循环等待链。循环等待未必死锁。
处理策略
预防:破坏死锁产生的四个必要条件
避免:避免系统进入不安全状态(银行家算法)
检测和解除:允许死锁发生,系统负责检测出死锁并解除。
忽略:鸵鸟算法
系统调用错误处理
参见 error.h
,这里想说的还是对错误返回处理的封装。
信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的时间。在 Linux 上支持了 30 中不同类型的信号。每个信号类型都对应于某种系统事件。
低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。
信号提供了一种机制,通知余户进程发生了这些异常。
发送
内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。
发送信号可以有如下两种原因:
- 内核检测到一个系统事件,比如除零错误或者子进程终止。
- 一个进程调用了 kill 函数,显示地要求内核发送一个信号给目的进程。
一个进程可以发送信号给它自己。
Unix 系统发送信号的机制
进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组 ID 来标识的。
默认一个子进程和它父进程同属于一个进程组。
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 函数
int kill(pid_t pid, int sig);用 alarm 函数发送
进程可通过调用 alarm 函数向自己发送 SIGALRM 信号,网络编程中可拿来处理超时。
unsigned int alarm(unsigned int secs);
接收
当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个信号处理函数的用户层函数捕获这个信号。
处理
非本地跳转
操作进程的工具
strace
打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。
这是一个超级牛逼的工具,比如你想跟进 PHP 内核底层实现,这就能收获大量信息。
ps
列出当前系统中的进程(包括僵尸进程)。
top
打印出关于当前进程资源使用的信息。
pmap
显示进程的内存映射。
/proc
一个虚拟文件系统,以 ASCII 文本格式输出大量内核数据结构的内容(从这也能感受到 Linux 文件的重要性),用户程序可以读取这些内容。
(TODO:补充详细结构及其作用)