进程一、进程基本概念
目录
一、冯诺依曼体系结构
借由冯诺依曼体系解释QQ聊天过程:编辑
二、操作系统(OS)
基本概念:
操作系统的管理:
建模:
操作系统管理资源的本质:
再谈C/C++中的库函数:
三、进程基本概念
什么是进程:
四、PCB -- task_struct
查看进程:
五、fork初识
fork函数的返回值:
问题:
一、冯诺依曼体系结构

上图就是冯诺依曼体系结构图,生活中的计算机包括笔记本和云服务器都遵循这个体系结构。现在我们记住冯诺依曼体系中红色箭头经过的流程后再看看计算机存储结构:
可以看到越靠近中央处理器(CPU)的存储速度越快,造价越高,体量也越小。根据木桶短板效应计算机的整体效率主要是被L6、L5这类速度明显不足的存储结构限制。
如果真是那样计算机的整体运行速度将被严重拉低,然而现实生活中计算机的速度并不慢,这是为什么?
再观察冯诺依曼体系我们可以看到CPU并不直接从输入端接受数据,而是从存储器(也就是内存,也叫做主存)接受数据。在冯诺依曼体系中,数据要想进入CPU处理必须先存入内存再经内存传送至CPU处理。
一个程序在磁盘被打开运行后会被存入内存,这时候计算机整体效率的短板就由本地磁盘提升为内存级别。使得计算机价格在能被大众接受的同时具备不错的运行效率,大大加快的计算机、网络等方面的发展。
冯诺依曼体系的规定,在数据层面CPU不与外设(输入输出设备)打交道,只和内存打交道。就一个程序而言,其要运行必须加载到内存中。
借由冯诺依曼体系解释QQ聊天过程:
一天张三登上QQ给李四发消息“hello”,设备从输入设备获取数据后加载到内存中,再经内存至CPU进行数据压缩、加密等处理,后传至网卡上传网络;李四设备的网卡接收到数据后同样需要将数据经过内存传入CPU进行解压解密等操作,最后传送到屏幕上点亮相应像素点。
二、操作系统(OS)
基本概念:
任何一台计算机都安装有操作系统(os)否则不能正常使用。一个操作系统分为两部分:内核跟其它程序(如库函数,shell程序等)
侠义上的操作系统,内核主要的四大功能:内存管理、文件管理、驱动管理、进程管理
讲一个生活中的小故事:
一家公司急需人手,张三将自己写好的简历投递给该公司。HR看到张三的简历后感慨这家伙是个人物便把张三的简历一把从简历堆中抽出来并打电话给张三通知来上班,张三就这么进入该公司。到了年终该公司老板看了眼公司各员工的KPI发现张三这家伙是个人物就通知其部门主管说给张三涨工资、评xx奖并调到更重要的岗位并要求在一定期限内完成XX任务,主管不敢怠慢转头就去员工系统里操作并通知张三相关事宜。
现在再提出一个颇有哲学意味的断论 “世界是一个不断重复进行决策、执行的循环”(当然别杠啊)公司老板发出决策,底下的人负责执行。并且决策者并不一定需要直接接触到执行者。
操作体系就使用这一套”哲学“逻辑:
老板是决策者,看成操作系统;张三是决策最终执行者,看成底层硬件;HR、主管介于老板和张三之间看成驱动程序。
之前讲过人是不擅长跟底层硬件打交道的,就像老板一个人很难对各个领域人数众多的员工进行好优评测。操作系统管理好底层硬件协助用户更好的操作,就像各部门主管管理并总结各自部门营收、人员贡献度等方面的信息并分析好后递交给老板,让老板更直观方便的做出决策。
之前我们使用各种库函数(如printf)、指令属于用户操作接口层次,其会去调用操作系统提供的系统调用接口再依次往下最终驱使底层硬件进行相关操作。
好了,假设公司营收有限需要进行裁员。那么请问老板是怎么决定要裁掉哪些员工呢?
很明显,就是在各部门的数据表里观测找到绩效最不好的几名员工。所以,老板是需要依靠手上的数据表来进行相关操作的。这么一说数据就很重要了,那么这些又该怎么存储呢?
没错,存储方式就是之前所学的数据结构!!(如老板手里的数据通过list、set、unordered_map等容器管理)在一个操作系统中,其核心就是数据结构!!操作系统不仅依靠数据结构来存储数据,更依靠其提高效率!
操作系统的管理:
上面说过,决策者不一定需要跟执行者相见,只要决策者手上具有执行者相关的数据就能进行人员选择与资源调度。在操作系统层面,操作系统并不需要直接跟底层硬件打交道,它只需要通过各种硬件驱动将各种信息收集好并依靠特定的数据结构来进行高效管理。那么,操作系统是怎么进行信息管理的呢?
操作系统会把各种资源用专门的数据结构进行存储,例如:
- 进程管理用 “进程控制块(PCB)” 存进程 ID、状态、占用资源等;
- 内存管理用 “页表”“段表” 记录内存分配、地址映射关系;
- 文件管理用 “文件控制块(FCB)” 存文件名、大小、存储位置等
实际上操作系统就是将各种资源描述好,再利用数据结构进行组织。在后续的C++语言中的类这一概念的提出就是为了更好的描述现实中的各种现象,STL就是为了更好的将描述内容组织起来方便开发者使用。这一过程也就是建模。
建模:
所谓建模就是将抽象的要求目的进行处理后能在应用层面具体化并能解决实际问题。要将抽象的需要具体化必须经过先描述、再组织实现相关功能这两个步骤。
比如员工要说服开辟新业务就需要先向老板描述新业务的过程、可行性和前景等方面,之后还需要组织起相关的人员部门合作实现达到新业务的要求。
操作系统管理资源的本质:
张三在求职的过程中是投递的简历,面试官决定应聘张三的原因在于张三的简历而不是张三这个具体的人;老板给张三加薪是因为在数据表中张三的KPI数据高,也不与张三本人有直接关系。没错,决策者不必与执行者见面,但必须拥有相关数据,并且相关决策的执行在某种程度上很大取决于这个数据(对比于张三的简历)。
操作系统管理资源的本质是通过存储的数据来间接管理各进程。
具体逻辑如下:
- 用数据记录状态:操作系统会为每个进程创建 “进程控制块(PCB)”,记录进程需要的资源(如内存大小、CPU 需求)、当前状态(就绪 / 运行 / 阻塞);同时用 “资源描述符” 记录硬件资源的使用情况(如内存是否空闲、设备是否被占用)。
- 基于数据做决策与调度:当需要分配资源时,操作系统不直接跟进程或硬件交互,而是读取 PCB 和资源描述符的数据,判断 “哪个进程优先”“哪些资源可用”,再通过数据更新实现管理(比如把 “内存空闲” 改为 “已分配给进程 A”,把进程 A 的状态从 “就绪” 改为 “运行”)。
再谈C/C++中的库函数:
在一个银行系统中为了保证银行财产安全以及对正常用户的服务,银行不会将内部组织架构暴露给外界,在加强外界防护的同时还会开设多个服务窗口供以服务用户已完成开户、存款、取款等操作。
对于一个操作系统而言把自己的内核暴露出去能被随意修改调用的话是一种十分危险的行为,所以操作系统都会把内核藏起来保护好,同时也会向外界留出一些“窗口”供开发者调用编写程序。C/C++中涉及 “和系统交互” 的库(如文件操作、网络通信库)本质上调用的就是操作系统预留出的各种接口。
三、进程基本概念
什么是进程:
在课本上,进程被描述为运行起来的程序,内存中的程序。站在操作系统内核角度进程又能被描述成担当分配系统资源的实体。
一个进程被加载到内存中并不是绝对唯一的,多个进程同时进行是常态,为了更好的管理就需要对各个进程做出更好的描述。
上面说过操作系统是通过管理相对应的数据来进行资源管理分配的,在操作系统中有专门的数据结构专门记录存储描述各个进程的信息--PCB,Linux下PCB就是task_struct数据结构。
所以,在Linux下的进程就能被说为:带有相关信息的内核数据结构task_struct(描述) + 自己的代码程序和数据(实体),也就是内核数据结构(PCB)+代码数据
在task_struct中不仅存储着进程进程中所有的信息,也内含着能找到对应进程的进程ID,保证能通过哈希映射直接找到对应进程。
这两者之间的关系也可以看成求职者与简历之间的关系,简历既包含了求职者所有的个人信息也充当着供HR挑选分配的角色。同时通过简历也必须能够找到求职者本身。
现实中经常有人说“双击启动程序”,其中的程序就是系统特定目录下的文件,双击这一动作就是将该文件启动进程。对于指令来说也是一样,指令本身也是文件,输入指令就是加载指令相关进程。

实际上PCB是不会直接进入CPU的,那么进程是怎么被控制的呢?
CPU中的寄存器除了存储进程产生的临时数据外,还充当着链接内存的桥梁:地址寄存器会记录内存中PCB的地址,数据寄存器将 PCB 中的关键信息(如目标进程的寄存器上下文、内存页表)读取到通用寄存器中,供内核程序(如调度器)分析或修改。
当代计算机会对每个进程分配一个时间片,当时间片执行完毕后该进程会自动退出,CPU开始处理下一个进程。
为了防止在中途退出导致进度丢失的情况,CPU会把中途暂时退出的进程的各项数据打包好写入PCB中,等下次再次处理时会就能直接恢复上一次的进度。
四、PCB -- task_struct
数据结构task_struct是PCB的一种,其属于Linux系统。其内部部分结构如下:
查看进程:
进程在创建的时候操作系统会给进程一个身份标识符PID,我们可以通过函数getpid()获得PID,并且一个进程是由父进程创建的(除了无根进程以外),getppid() 可以得到父进程的身份标识(PPID)


通过 ps ajx 指令(这里先认识一下)可以直观的看到进程的相关信息:
上面几张图中有个很疑惑的点--为什么进程的PID在不断改变而父进程的PPID却没有发生改变呢?
也不卖关子了,上面第一张图的右下角有个bash,bash是Linux下的命令行解释器。
bash是 Linux 系统中用户操作的 “核心入口”—— 既负责单条命令的即时执行,也支持复杂脚本的自动化运行。用户在终端手动创建的进程都由bash创建(还有其它进程创建方式),因为进程都是由父进程创建的,故用户在终端手动创建的进程都有同一个父亲--bash
/proc目录:
Linux下进程中的”身份标识“(PID)都会统一保存在 /proc 目录下,左侧的就是各进程的PID。如果想查看某进程的信息就需要打开相对于的文件。

再看看 cwd 目录我们会发现其中记录的是进程文件所在的目录下的内容:

每个进程进行的时候都有自己的cwd,cwd记录的是进程的路径。当没有指定时会默认为程序当前路径,如果指定就为指定路径。例如,进程执行open("log.txt", ...)时,实际打开的是[cwd路径]/log.txt 
好了,我们已经知道进程都是由父进程创建的,那么进程创建的过程又是什么样的呢?
五、fork初识
man手册查看fork函数:man fork

现在使用一下fork函数:

我们可以看到,使用fork函数后可以看到出现了两个进程,并且一个进程是另一个进程的父进程。(这里父进程的父进程的ppid跟之前不同是因为我重启了终端,系统重新分配了bash)
fork函数的作用是给当前进程实现“分流”,也就是以当前进程为父亲重新创建一个子进程。
fork函数的返回值:
| 进程类型 | 返回值 | 含义说明 |
|---|---|---|
| 父进程 | 子进程的 PID(大于 0 的整数) | 父进程通过该 PID 管理子进程(如wait) |
| 子进程 | 0 | 标识自身为子进程 |
| 调用失败 | -1 | 需通过perror等函数排查错误(如内存不足、进程数超限) |
当调用失败的时候只会返回一个-1;但当其调用成功后会有两个返回值,一个返回零标识子进程,另一个返回大于零的值(子进程的PID)给父进程。
问题:
1、为什么子进程要返回零而父进程返回子进程的PID?
现实生活中老子管理小子是很正常的现象,在fork分流后父进程依旧需要保存一份子进程的PID方便后续管理操作;而子进程返回零是因为在 Linux 中,PID 是唯一标识进程的数值,且所有有效进程的 PID 都大于零(0通常属于系统级的init进程,用户进程不会占用),标识起来很简洁。并且子进程是可以通过getppid函数来获取父进程PID,这就不需要返回父进程的PID。
因此,子进程用零作为返回值,既不会与自身的实际 PID冲突,又能明确传达 “我是子进程” 的身份。
2、fork函数是怎么做到返回两个返回值的?
之前说过Linux下几乎所有的进程都是由父进程创建的(无父的跟进程除外),fork函数创建的子进程在初始时,父子进程共享内存空间(代码段、数据段、堆、栈的物理内存);当任一进程修改内存数据时,系统才会为其复制一份内存页,避免了 fork 时直接复制整个内存空间的开销,大幅提升效率。
子进程创建的时候基本与父进程处于同一执行状态——都停留在fork函数处,之后fork函数分别给父子进程写入(return)返回值,此后父子进程拿着各自写入的返回值开始独立执行。这导致在代码上来看就像是返回两个返回值,实际上就是两个进程的独立进行。
