文章目录
在操作系统中,进程(Process
) 是程序的一次动态执行过程,是系统进行资源分配和调度的基本单位。简单来说,当你运行一个程序(如打开浏览器、启动一个 Python
脚本),操作系统就会创建一个进程,并为其分配内存、CPU
时间等资源,使其能够执行
1.进程先前知识铺垫
1.1 冯诺依曼体系
如图所示,计算机底层的硬件都要遵守该体系,大致由四部分组成:
- 输入设备: 鼠标、键盘、摄像头、话筒、硬盘、网卡…
- 存储器: 简单来说就是内存
- 输出设备: 显示器、播放器硬件、磁盘、网卡…
- 中央处理器: 即
CPU
,包括运算器和控制器,对我们的数据进行计算任务(算数逻辑),以及流程进行控制
有的设备是纯输入、输出设备,有的既是输出也是输入设备
一个程序要运行,必须先从输入设备进入存储器,到 CPU
处理之后,再回到存储器,由输出设备让我们看到处理结果。该体系规定不能跳过存储器直接与 CPU
交互,简单来说就是所有设备都只能直接和内存打交道
🤔为什么必须按照这种流程?
CPU
的运算速度(纳秒级)远超外部存储设备(如硬盘、U
盘,毫秒级甚至秒级)。存储器的运算速度是适中的,如果没有存储器(尤其是内存)作为 “高速缓冲中转站”,CPU
会被外部设备的慢速拖垮 —— 每次运算都要等外部设备慢吞吞地传输数据,效率会下降几万甚至几十万倍
1.2 操作系统
操作系统是一款进行软硬件管理的软件,对下通过驱动对硬件进行控制计算的管理,对上提供统一的接口供代码调用
🤔为什么操作系统只提供接口用于调用呢?
操作系统里有各式各样的数据,为了保护数据安全,也为了能给用户提供安全稳定的服务,让所有访问操作系统的行为,通通转化为系统调用完成
🤯操作系统是如何进行管理的?
先说结论,总结起来就是 “先描述,再组织”
,以学校管理学生的入学档案数据为例,每个学生的个人信息就是一份档案,即 struct
结构体节点,将每个节点通过双向链表链接起来,即可以通过增删查改对数据进行管理,这就是操作系统的做法
2.进程
2.1 概念
既然了解了以上知识,那么进程应该是不难理解了,简单来说,进程就是一个已经加载到内存里的程序,叫做进程,也叫任务
每当有一个进程创建的时候,就会对应创建一个 PCB
(process control block
)对象,在 Linux
下 PCB
的具体实现是一个个的 task_struct
。PCB
是描述对象的属性的集合
🚩task_ struct内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程
- 状态: 任务状态,退出代码,退出信号等
- 优先级: 相对于其他进程的优先级
- 程序计数器: 程序中即将被执行的下一条指令的地址
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图
CPU
,寄存器] - I/O状态信息: 包括显示的
I/O
请求,分配给进程的I/O
设备和被进程使用的文件列表 - 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
- 其他信息
进程 = 内核
PCB
数据结构对象 + 你自己的代码和数据
2.2 查看进程
我们将以 process.cpp
这个循环输出的文件为例子查看进程状态(已配置 makefile
文件),下面提到的 PID
表示该进程的 ID
值,PPID
表示该进程的父进程(进程中创建进程后,实行创建的那个进程就叫父进程,被创建的那个就是子进程)
2.2.1 ps用户级工具获取
ps
是 Linux
系统中用于查看进程状态的命令,全称是 Process Status
。它可以显示当前系统中运行的进程信息,包括进程 ID(PID)
、运行状态、CPU
占用率、内存使用等
a
:显示所有用户的进程(包括其他用户的进程)j
:以作业控制格式显示(包含PPID
父进程ID
、PGID
进程组ID
、SID
会话ID
等字段)x
:显示不关联终端的进程(如后台运行的进程、服务进程等)
ps ajx | head -1 && ps ajx | grep ./process
表示查看进程,显示出第一行的信息,并截取 ./process
这一条进程,grep --color=auto ./process
由于 grep
需要高亮显示而创建的进程
可以看到该进程的 PID
为 20662
,父进程为 18495
再去查看对应的父进程,发现是一个名为 bash
的进程。实际上,bash
是当前登录的终端交互系统的进程,大多数命令其直接父进程通常都是当前的 bash
2.2.2 系统文件查看
根据进程的 PID
同样可以直接在系统目录 proc
下查找到对应进程
2.3 函数获取进程ID
getpid()
:用于获取当前调用进程的进程 ID(PID)
。这个 PID
常被用于生成唯一的临时文件名等场景
getppid()
:用于获取当前调用进程的父进程的进程 ID(PPID)
🔥值得注意的是: 使用这两个函数需要包含 <sys/types.h>
和 <unistd.h>
这两个头文件,这两个函数总是成功的,不会返回错误
2.4 创建进程
fork
是用于创建新进程的指令,相当于创建该进程的子进程
当一个进程(称为父进程)调用 fork
时,系统会创建一个新的进程(称为子进程),我们知道 进程 = 内核 PCB 数据结构对象 + 你自己的代码和数据
,子进程PCB还是会正常创建一个,但是自定义的数据代码就不一样了。举个例子,子进程和父进程相当于两个厨师,代码相当于菜谱,运行中的代码是不能被修改的,对于父子进程来说是共享的,就不需要再多一份浪费空间;数据相当于食材,虽然做的是同一道菜,但是肯定是两份食材,因为数据可能被修改,不可能共享同一份数据,子进程复制父进程的数据进行的是写时拷贝,只有需要修改的数据才会拿下来拷贝,避免多余的拷贝占用空间
返回值区分: 为什么要创建子进程?是为了让多个进程同时执行不同的事情,因此需要执行不同的代码块,通过 if-else
条件句实现。fork
调用会在父进程和子进程中都返回,但返回值不同。在父进程中,fork
返回子进程的 PID
;在子进程中,fork
返回 0
。通过判断 fork
的返回值,程序可以区分当前是在父进程还是子进程中执行
运行结果:
可以看出确实是有两个进程运行着,根据不同的分支的结果就能看出,那么我将提出三个细节上的问题
- 为什么fork要给子进程返回0,给父进程返回子进程PID?
子进程被创建后,其自身的 PID
对区分身份来说并非必需 —— 子进程若需要知道自己的 PID
,可通过 getpid()
系统调用获取。更重要的是,子进程需要一个明确的信号来识别自己是子进程,而 0
恰好是一个理想的特殊值,因此 0
只作为标识,而不是有效的 PID
父进程创建子进程后,通常需要对其进行后续管理(如等待子进程结束、发送信号、监控状态等),而 PID
是进程的唯一标识,父进程必须知道子进程的 PID
才能执行这些操作
- fork函数是如何做到返回两次的?
看到 fork
内部的函数结构,中间部分进行相应的子进程创建操作,在 return
操作前子进程就已经创建完毕了,既然 return
也是代码,那么就是共享的,父子进程就都会执行,即 fork
函数会有两次返回
- 父子进程谁先运行?
这个是无法确定的,由调度器的策略决定,调度器简单来说就是一个分配进程占据CPU运行效率的组件