

前引:当你在 Linux 桌面双击浏览器图标,或在终端执行python script.py时,一个肉眼不可见的 "转变" 正在发生:原本静静躺在硬盘里的程序文件,突然拥有了 "生命"------ 它开始占用内存空间、争抢 CPU 资源、与其他程序交互。这个 "活起来的程序",就是操作系统最核心的抽象概念进程!
首先我们介绍了冯诺依曼体系结构 ,为平衡输入设备和CPU性能高度差异,让内存 来平衡二者,所有任务都需要经过内存被且CPU处理!接着学了(上)给用户提供系统接口、规范用户行为,(下)管理底层软硬件,提供良好运行环境的操作系统 !接着学习软硬件执行行为出现的进程 (操作系统在内存中管理各种任务所描绘的内核PCB数据结构对象和任务本身的代码数据),接着学了创建子进程的fork函数、 几种常见进程状态 ,从进程的产生到进程的执行完成,调度算法 和优先级让每一个运行进程都在高效有序的执行着!
目录
[(2)fork 函数](#(2)fork 函数)
【一】冯诺依曼体系结构
冯诺依曼体系结构属于计算机设计的理论框架
我们使用的笔记本、台式电脑,包括服务器等都严格的遵循冯诺依曼体系结构:

现在我们来介绍上图的各种硬件:
输入设备:比如键盘、鼠标等
存储器:就是内存
运算器+控制器:中央处理器(CPU)
输出设备:显示器等
在这里需要强调:从输入到输出,数据无法直接跳过存储器(内存)
因为输入设备比如硬盘等有一个特点:空间大、计算慢、读写远远不及CPU
而CPU内存很小但是计算超级快
这也就导致:如果去掉中间的内存,导致CPU虽可以快速计算处理输入设备上的数据,但是受输入设备性能影响,导致整体效率显著降低,因此便出现了内存,来平衡二者!内存空间一般在16GB左右大于CPU,但是运行速度上远大于输入设备,因此如果提前把一些数据放到内存,再通过CPU计算,保证每次内存中都有一些任务,那就可以做到持续计算、输入设备也持续工作!因此程序的输入输出离不开内存!
例如:你正在和远方的朋友互相发送信息,那么数据流如下分析
A键盘输入(输入设备)->内存->CPU处理->输出运算结果->内存->A的网卡作为输出设备
A的输出网卡作为B的输入设备->B的内存->CPU->内存->B显示器
(如果涉及到文件,可能会将磁盘作为输入设备!)
【二】操作系统
(1)什么是操作系统
操作系统是一个进行管理(软件+硬件)的****软件,例如:管理底层的驱动程序和底层硬件

(2)为什么要有操作系统
难道我们实行哪个程序、硬件不能自己完成吗?
(1)底层是驱动程序+各种硬件,很明显用户无法直接操作各种硬件,因此帮用户管理各种软硬 件资源
(2)提供我们的体验,给用户提供一个稳定、高效、安全的运行环境(总不能游戏玩着就黑屏)
(3)如何理解"管理"
就好比银行,我们都是通过窗口和工作人员完成各种操作,不能进入银行自己操作电脑!
操作系统规范了我们用户的行为,给我们一个"系统调用接口",允许我们通过接口间接的和软硬件交互!
什么是系统调用接口?在开发⻆度,操作系统对外会表现为⼀个整体,但是会暴露⾃⼰的部分接⼝,供上层开发使⽤, 这部分由操作系统提供的接⼝,叫做系统调⽤
系统调⽤在使⽤上,功能⽐较基础,对⽤⼾的要求相对也⽐较⾼,所以,有⼼的开发者可以对部 分系统调⽤进⾏适度封装,从⽽形成库,有了库,就很有利于更上层⽤⼾或者开发者进⾏⼆次开发,所有用户都是通过系统调用接口(函数)来施行命令!
用户如何完成系统接口调用?
用户无需关心内核细节,只需通过
shell
或库
发起操作;shell/库
专注于 "用户需求→系统调用的转换";内核专注于 "系统资源的底层管理"例如:(shell和库通过再次封装系统调用接口或者转换命令来完成系统调用接口!)
shell外壳
(如 Linux 的 bash):解析终端命令,转化为系统调用请求lib(库)
(如 C 标准库libc
):封装系统调用,为开发者提供更易用的函数接口部分指令
:直接与系统调用对接的简易工具(如echo
等内置命令)
【三】进程理解
在上面我们学习了操作系统对上给用户提供系统接口,对下管理各种软硬件资源,例如:

现在我们要学习它是如何实现"管理"的?
在学习之前,需要引入一个故事同时也是为了后面的复习:
例如校长和每个学生,校长通过下达命令给各下一级管理员,再由管理员作为执行者对学生完成任务,再将数据一级一级反馈上去。整个过程由校长发出命令开始,拿到底层提交的数据结束。校长不需要去了解每个学生长什么样子,在干什么,它只需要数据即可完成,因此在操作系统中类似:
操作系统->校长
驱动程序->辅导员
底层硬件->学生
由驱动程序提供底层硬件的数据完成和操作系统的交互
这样操作系统就完成了对每个硬件的管理而非直接操控对硬件
操作系统管理的是属性而非是每个硬件具体的数据(比如大小、代码等)
每个任务应该具有属性+数据,操作系统作为高效的管理者,管理的是每个正在执行任务的属性,因为只有正在运行的任务才会加载到内存,而数据可以放在属性里面通过指针指向,快速找到这个数据本身,对具体数据的管理没有意义!
操作系统是如何管理属性的?
在每个执行任务被加载到内存中时,会提前被操作系统建一个struct 填充这个任务的各种属性(因为操作系统是用C语言写的),而这个struct我们称之为 PCB,Linux 操作系统下的 PCB 叫: task_struct,多个任务再建多个PCB通过数据结构连接进行管理。即先描述 (PCB)再组织(任务执行):
什么是进程?
任务被加载到内存中执行这个说法只是进程的一部分!
进程(PCB+(数据+代码...))= 内核PCB数据结构对象+任务自身数据+代码
对进程的管理变成了对数据结构的增删查找,真是高效!
【四】如何查看进程
注意:需要正在内存运行的任务才会显示!
ps命令**(静态查看)**
a
:显示所有用户的进程u
:显示用户信息x
:显示没有控制终端的进程-e
:显示所有进程(等同于 - A)-f
:显示完整格式的进程信息
top命令**(动态查看)**
· 按 q
退出
· 按 P
按 CPU 使用率排序
· 按 M
按内存使用率排序
这样虽然可以查看所有进程信息,但是我们需要查看某个具体的进程信息呢?
(1)ps -f | head -1 ; ps aux | grew -w "具体的运行任务名"(2)ps -p PID
原理都是利用 ps 获取进程,再利用命令、管道多次筛选,组合各种命令,比如 grew 可以查找!
(1)标识符
标识符类似学生学号!代表了每一个进程,在 task struct 结构中,存在两个成员:PID和PPID
PID:进程唯一标识符
PPID:进程prev的唯一标识符
- PID 变化:因为它是内核为每个新进程动态分配的唯一 ID,随进程的创建 / 销毁而动态更新
- PPID 稳定 :因为父进程通常是长期运行的稳定进程(如 shell、
systemd
),其 PID 很少变化,因此子进程的 PPID 也保持稳定
通过头文件:**#include<sys/types.h>**调两个函数分别可以获取父子标识符getpid()
getppid()
例如:
(2)fork 函数
fork函数是什么?
fork()
是一个系统调用 ,用来**创建一个新的进程,**需要头文件:
#include <unistd.h>
fork函数是如何创建进程的?
fork 是一个系统调用,由当前正在运行的进程调用,这个调用它的进程被它称为父进程
fork函数内部大概原理:
根据父进程创建一个子进程,子进程PCB结构成员参数大部分仿照父进程,生成唯一PID标识符。(即不是一个函数本身返回两次,而是父子进程各自从fork返回)
子进程和父进程共享代码,数据则是由子进程在访问时可以直接访问父进程的,子进程如果要修改数据会直接在内存创建与父进程一样的进行修改(写时拷贝)(写的时候拷贝数据)原理如图:
因此这个函数会有两个返回值:
返回类型是
pid_t
,这是一个整数类型(通常是int
或long
),用来表示进程 ID
fork()
一次调用,会产生两次返回(父进程一次,子进程一次),加上出错的情况,共有三种可能:
返回值 含义 出现场景 大于 0 父进程中返回,值是子进程的 PID fork 成功 等于 0 子进程中返回 fork 成功 -1 创建失败 fork 失败(如内存不足、进程数超限等)
fork()
之后,父子进程是独立的调度单位(每个任务都是独立运行的进程)fork()
产生两个进程,各自独立调度,所以 "父进程走 else、子进程走 if" 是并发执行的分支,不是顺序执行!- 操作系统的调度器决定谁先运行,不一定父进程先执行
为什么要父进程返回的是子进程的PID,子进程返回0?
父进程对子进程的运用可能更多,父进程必须知道子进程的PID,子进程找父进程使用很少
例如:多个儿子只有一个父亲,但是儿子多了,你命令谁就需要有一个区分!
函数是如何做到返回两次的?
在父进程的返回位置,内核将子进程的 PID写入父进程的返回寄存器
在子进程的返回位置,内核将 0 写入子进程的返回寄存器
因此我们需要根据返回值来判断函数执行结果,它具有两次返回值!
例如:
可以看到父进程的 getpid()是27913,子进程的getppid是27913,二者满足父子关系!
【五】进程状态
我们可以通过 ps -ef | head -1 ; ps -aux | grew Hello(任务关键字) 查看(**+**代表正在前端运行)

每个进程都有自己的状态,如图:

(1)运行状态
**运行状态:**处于正在被CPU执行的进程或者已经准备让CPU执行的进程,状态码 R
理解:每个进程都有自己的 task struct ,它们受操作系统控制,需要被运行的任务就会有序的形成运行队列,按照队列顺序依次执行,且每个进程的运行时间也是有限制的(不然while(1)就导致整个系统崩了!),如图:

(2)阻塞状态
**阻塞状态:**当任务缺少外设执行条件(比如麦克风缺少外部声音),这时无法正常运行,此时就 会形成待队列放在内存里面,满足运行条件后根据数据结构的增删前往运行队列逐一 运行
理解:如下图所示

(3)挂起状态
**挂起状态:**进程是由PCB数据结构对象和代码数据组成,进程不处于运行状态而又待在内存里 面,代码数据会占用大量的内存空间,只需要根据 task_struct 里面的指针找到这个数 据、代码。那它本身的数据、代码就可以放回磁盘,内存中只放PCB结构对象,需要 即调即可
理解:如下图所示

(4)深度睡眠
**深度睡眠状态:**进程正在执行长时间的任务,按理来说只要是在做任务那应该是运行态,但是如 果一直保持运行,这个进程就在内存中干等着耗费资源,这时操作系统无法 终止这个进程,于是就把它放在深度睡眠状态,执行完毕再处于运行状态被结束
状态码 D
例如:某个1GB写进磁盘的进程,由于写入的时段很长,进程等着也是耗费资源,于是就放在了 深度睡眠状态,待整个写入过程结束,这个进程才会结束

(5)暂停状态
**暂停状态:**暂停状态特别像阻塞状态,它可能是缺某个资源暂时停止或者操作系统下指令临时停止
状态码T或者t ,这个我们简单了解即可!比如调试遇到断点就停下来!
(6)僵尸状态
僵尸状态:子进程退出的时候,如果没有父进程主动回收信息,子进程会一直处于Z状态 (僵尸状 态),那么子进程的 task struct 资源便一直无法被释放(持续占用内存),此时操作 系统也无法直接杀掉,需要父进程来回收子进程
(7)孤儿状态
**孤儿状态:**如果父进程释放,子进程还未释放,那么这个子进程的父进程会改为1号进程(操作系 统),由操作系统充当子进程的父进程,否则资源一直得不到释放,是很严重的内存 泄漏
【六】进程优先级
(1)何为优先级
进程的优先级也就是进程的执行顺序,因为运行的进程有一个排队等待的过程,所有优先级决定了进程的执行快慢:
所以进程之间是有竞争关系的,每个进程在运行队列都有一个排队的过程,而操作系统正是通过排队来提高进程任务的效率,如果因为排队混乱,各个进程任务都被延迟!进程的优先级= PRI(固定80)+NI(nice),即优先级最终的范围是【60~99】
(2)优先级的调整
我们可以调整进程的优先级,更改 nice ,但是这样不现实,操作系统本就是作为管理员来保证设备运行是稳定的,如果人为强制干预,那么就失去了原有的"安全、高效",因此进程的优先级nice修改范围是【-20~19】,值越小,进程执行速度越快,例如:
例如:(1)进入root权限,输入top
界面中关键列说明:
PID
:进程 ID;NI
:nice 值(静态优先级);PR
:动态优先级(内核计算后的值,普通进程 PR=120+NI)(2)在界面先输入 r 键,输入目标进程的 PID(如 2063),按回车
(3)再输入目标的新优先值nice,例如19,完成优先设计
(3)由优先级调整运行队列
在上面我们已经知道了什么是优先级,以及如何用 root 权限调整优先级,下面我们学习在调整之后操作系统是如何根据优先级调配运行队列的:
(1)在运行队列中通过2个指针数组(task struct * running / waiting)分别代表两个进程队列:
- 实时进程队列:为每个实时优先级(1~99)单独创建一个队列(共 99 个)
- 普通进程队列:为普通优先级(100~139)也创建了 40 个队列
(2)调度算法:每个指针数组各自有自己的一个镜像指针数组(与自己完全一样),共有4个指针数组每个指针数组采用哈希桶的方式根据优先级计算下标来存放每个运行进程的位置,如果其中一个指针数组根据优先级位图判断已执行完当前哈希桶存储的所有进程,就会采用swap交换的方式换到自己的镜像指针数组,这样循环交换执行,提高效率!根据优先级执行运行队列
例如:

(4)并发与寄存器
**并发:**多个进程在一个CPU下采用进程切换的模式,在一段时间内,多个任务被推进执行
"并发"的原理我们并不难理解,但是各位有这样一个问题:如果某个进程优先级特别高呢?
那是不是当这个优先级高的进程运行完毕之后,继续运行立刻因为优先级高,再次直接执行?
答案:理想是这样的,但是因为运行队列采用的有两种镜像指针数组形成的哈希桶存储,当这个进程执行完毕,如果它要再次执行,就会去另一个镜像的哈希桶里面,下一轮再按优先级执行!
**寄存器:**寄存器我们在C语言的函数栈帧里面讲过,它属于指针
系统是如何知道我们当前的进程执行到哪里了?通过CPU寄存器eip等大量寄存器!
(1)每次记录当前指令执行的下一个位置,例如:函数跳转、条件语句判断等.....
(2)通过寄存器来让外部拿到进程执行结果,就像C的函数栈帧销毁一样
(3)保存大量的使用频率高的数据,多次复用,提高效率。进程从CPU结束的时候,会通过寄存 器保存带走大量数据,既能提供高度复用,又可以拿回处理结果