Linux学习笔记(六)--Linux进程概念
前言:
冯诺依曼体系:
核心定义:冯·诺依曼体系结构,也称为普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的计算机设计概念。它由美籍匈牙利数学家约翰·冯·诺依曼及其团队在1945年的一份报告中首次明确提出。
冯诺依曼体系结构组成:冯诺依曼体系结构主要由五大核心部分组成,下面为大家一一介绍它们.
运算器:负责执行所有的算术运算和逻辑运算(核心部件:算术逻辑单元)
控制器:是整个计算机的指挥中心。它从存储器中读取指令,进行分析,然后发出控制信号,协调其他所有部件有序工作。(与运算器的关系:运算器和控制器共同构成了现代计算机的中央处理器。)
存储器:用于存储程序和数据。程序是指令的集合,数据是程序处理的对象。(内存)
输入设备:将外部的原始数据、程序或命令输入到计算机中。(例子:键盘、鼠标、扫描仪等。)
输出设备:将计算机处理后的结果以人们能够理解的形式呈现出来。(例子:显示器、打印机、音响等。)
运算器和存储器的集合称为CPU.
这五大部件通过总线(系统总线和IO总线)连接在一起,进行数据和指令的传输。
冯诺依曼体系结构示例图:
两用户通过QQ聊天整个数据是如何流动的(忽略网络情况)?
两个用户分别是两个冯诺依曼体系结构
首先用户1通过键盘输入信息,键盘将输入的字符转为数字信号,这些数据通过系统总线被存储到用户1的存储器(内存中),如何通过CPU处理信息后返回到内存中,然后处理好的数据通过总线发送到网卡中,再由用户2的网卡接收数据包,数据包通过总线传输到用户2的存储器中,再由用户B的CPU解包信息,将解包后的文字信息返回到存储器中,再从存储器通过总线发送到输出设备(显示器)上.这就是两台计算机在冯·诺依曼体系结构下,协同完成一次即时通讯任务的经典过程。
那么为什么需要存储器呢?为什么不能直接通过传递信息给CPU来直接发送信息?
核心答案就是存储器解决了计算机内部不同部件之间巨大的“速度差”和“时序差”问题,是保证高效、有序工作的关键。我们可以从上面的图中看到输入设备(如键盘)和输出设备(如显示器、网卡)都不直接与CPU相连,它们的所有数据交换都必须通过存储器这个中间环节。其中外设的运算速度远不及CPU,如果让CPU直接处理键盘输入或网络数据,它99.999%的时间都在无所事事地“空等”,效率极其低下。所以这个时候就需要我们存储器这个角色派上用场,它是这两个部件的高速缓存区,存储器的速度介于两者之间。外部设备可以慢慢地把数据写入存储器,攒够一批后,再通知CPU来快速处理。
操作系统:
概念:任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:内核(进程管理,内存管理,文件管理,驱动管理)和其他程序(例如函数库,shell程序等等)
目的:
(1)将复杂的硬件操作细节隐藏起来,提供简单统一的接口(如“打开文件”、“打印文档”),让用户和程序员无需关心底层硬件具体如何实现。
(2)作为资源管理器,公平、高效地分配和调度有限的硬件资源(如CPU时间、内存空间、磁盘空间、I/O设备)给多个程序和用户。
(3)防止多个同时运行的程序相互干扰,保护系统进程和用户数据不被非法访问或破坏。
计算机层状体系结构:
下面是一张计算机层状体系结构的图片:
计算机层状体系结构从上至下可以分为三层,它们分别是用户部分,系统软件部份,硬件部分.各层的功能以及组件的功能如下:
用户层:这是所有操作的起点和终点。用户可以是正在使用计算机的人,也可以是另一个正在请求服务的系统。用户不直接与硬件打交道,而是通过下面的层层接口来使用计算机。
用户操作接口层:包含 “shell 外壳”(提供命令行交互界面)、“lib(库)”(提供函数等资源支持)、“部分指令”,负责接收并初步处理用户操作请求,为后续与系统调用交互做准备。
系统调用接口层:作为用户操作接口与操作系统之间的桥梁,将用户或上层的请求转换为操作系统能识别的调用形式,实现请求的传递。系统调用就像是一家公司对外的“服务窗口”,普通员工(应用程序)不能直接进入仓库(硬件)或指挥经理(操作系统),必须通过填写标准化申请表(系统调用)来提出正式请求。
操作系统层:承担 “内存管理”(分配、回收内存等)、“进程管理”(进程的创建、调度、终止等)、“文件管理”(文件的存储、读写、组织等)、“驱动管理”(管理各类设备驱动程序)等核心系统管理功能,是系统软件的核心,对硬件资源进行抽象和管理。
驱动程序层:包含 “网卡驱动”“硬盘驱动”“其他驱动”,负责将操作系统的请求转换为硬件设备能理解的命令,实现硬件设备的具体控制,是操作系统与底层硬件之间的 “翻译官”。
底层硬件层:由 “网卡”“硬盘”“其他(如 CPU、内存芯片等)” 组成,是计算机系统的物理基础,执行最底层的硬件操作,如数据的物理存储、网络数据的收发等。
接口与操作系统的关系:接口是操作系统对外提供的服务窗口和边界,是用户和应用程序访问操作系统核心功能的唯一安全通道。
进程:在上述的计算机层状体系结构图中,我们可以将一个程序比作一个菜谱,它是一堆静态的指令和数据.而进程就像一个厨师来按照食谱炒菜的这一个过程,它是一个动态的执行过程,包含了所用的食材(数据)、厨房灶具(CPU、内存等资源)、以及炒菜的每个步骤(执行流程)。
定义:进程是计算机中的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度的基本单位。(它是“执行中的程序”:程序是静态的、存储在磁盘上的文件;而进程是动态的、被加载到内存中并正在执行的程序实例。它是“资源分配的单位”:操作系统以进程为单位来分配CPU时间、内存空间、文件句柄等系统资源。)
为什么需要进程?因为为了让我们可以一次性执行多个程序,保证每个程序都有独立的内存空间不会互相干扰,所以操作系统通过创建一个个独立的进程,来实现并发执行、资源隔离和公平调度。
进程控制块(PCB(process control block))
任何一个进程,在加载到内存中的时候,形成真正的进程时,操作系统要先创建描述进程的结构体对象,这个结构体对象就叫做PCB(进程控制块).这是操作系统管理和控制进程的核心数据结构。每个进程都有一个唯一的PCB。操作系统通过PCB来感知进程的存在。在Linux操作系统下的PCB叫做task_struct.
PCB记录了:
进程标识符(PID):唯一的身份证号码。
进程状态:运行/等待/就绪
优先级: 相对于其他进程的优先级。
程序计数器(PC):下一条要执行的指令的地址。
CPU寄存器:进程被切换时,需要保存当前的运行现场(如累加器的值),以便下次能接着执行。
内存管理信息:记录了程序代码、数据、堆栈在内存中的位置(基址、界限寄存器等)。这实现了进程间的内存隔离。
I/O状态信息:进程所占用的I/O设备列表、打开的文件列表等。
进程 = 内核PCB数据结构对象 + 自己的代码和数据
Linux如何组织进程?
进程关系组织:进程树:Linux 中的所有进程构成了一棵树形结构,这棵树的根是 init
进程(PID 1),通常是 systemd
或 init
。
组织方式:每个 task_struct
中都包含指向其亲属的指针:
(1)parent
:指向父进程的 task_struct
。
(2)children
:链表头,链接所有子进程的 task_struct
。
(3)sibling
:链表节点,用于将本进程链接到兄弟进程的链表中。
如何查看:使用 pstree
命令可以清晰地看到这棵进程树。
目的:(1)资源继承:子进程继承父进程的某些属性(如打开的文件描述符)。(2)进程管理:父进程可以通过 wait()
系统调用来等待和获取子进程的退出状态,防止其成为“僵尸进程”。
所有进程的全局组织:任务列表:内核需要一种方式来跟踪所有的进程,这是通过一个名为 init_task
的双向循环链表实现的。
组织方式:每个 task_struct
都包含一个 tasks
链表节点(struct list_head tasks
)。所有的 task_struct
通过这个节点连接成一个巨大的循环链表。(init_task
是这个链表的头节点(一个特殊的“0号进程”)。)
目的:(1)全局遍历:当需要遍历系统中的所有进程时(例如 ps
命令或 killall
命令的实现),内核会遍历这个任务列表。(2)进程查找:通过 PID 查找进程时,也可以辅助遍历(虽然有更高效的哈希表优化)。
高效的 PID 到进程的查找:PID 哈希表:通过 PID(一个数字)快速找到对应的 task_struct
是一个常见操作。如果每次都遍历任务列表,效率极低。
组织方式:内核维护了多个 PID 哈希表。(这是一个哈希数据结构,将 PID 作为键,将对应的 task_struct
地址作为值。当需要根据 PID 查找进程时,内核计算 PID 的哈希值,直接定位到对应的哈希桶,从而极快地找到目标 task_struct
。)
目的:实现 O(1) 时间复杂度的 PID 查找,极大提升性能(例如,发送信号 kill(pid)
时就需要这个操作)。
查看进程信息:进程的信息可以通过 /proc 系统文件夹查看,大多数进程信息同样可以使用top和ps这些用户级工具来获取
(1)ps
功能:显示当前时刻的进程状态。它提供的是执行命令那一刻的快照。
常用组合:ps aux
:最常用,显示所有用户的所有进程详细信息。
a
:显示所有用户的进程。u
:以用户为主的格式显示。x
:显示没有控制终端的进程(通常是后台进程、守护进程)。
关键列:
USER:进程所有者。PID:进程ID。%CPU:CPU占用百分比。%MEM:内存占用百分比。VSZ:虚拟内存大小。RSS:常驻内存集大小(实际物理内存)。TTY:进程所在的终端。?表示与终端无关。STAT:进程状态(非常重要,见下文解释)。START:进程启动时间。TIME:进程占用CPU的总时间。COMMAND:启动进程的命令。ps -ef:以完整格式列表显示所有进程。-e:显示所有进程。-f:显示完整格式(UID, PID, PPID等)。
STAT状态码解析:
R(Running):正在运行或可运行(在运行队列中)。S(Sleeping):睡眠中,可被中断(等待事件完成)。D(Disk Sleep):不可中断的睡眠(通常是在等待I/O,不能用kill杀死)。T(Stopped):已停止(通常由信号 SIGSTOPCtrl+Z导致)。Z(Zombie):僵尸进程(进程已终止,但父进程尚未回收其资源)。<:高优先级。N:低优先级。s:会话首进程(session leader)。l:多线程进程。+:位于前台进程组。
初始fork--通过系统调用创建进程
基本功能:fork()
通过复制调用进程(父进程)创建一个新的子进程。子进程是父进程的副本,拥有相同的代码、数据段、堆栈、文件描述符等(但实际采用写时拷贝技术优化性能)。调用一次,返回两次:父进程中返回子进程的PID(>0)。子进程中返回 0
。失败时返回 -1
(如进程数达到上限)。
为什么fork要给子进程返回0,给父进程返回子进程的PID?
原因:为了明确区分父子进程的执行逻辑,并提供一种简单可靠的进程控制机制。
子进程返回 0
:子进程需要知道自己是被 fork()
创建的,且可以通过返回值 0
明确识别自己的身份。这样,子进程可以执行与父进程不同的代码逻辑(例如调用 exec()
加载新程序)。
父进程返回子进程的PID:父进程需要知道子进程的PID,以便后续管理(如通过 wait()
回收资源、发送信号等)。如果返回 0
,父进程将无法区分自己是父进程还是子进程。
一个函数是如何做到返回两次的?
因为在fork中,并不是函数本身真的返回了两次,而是因为 fork
调用后,系统创建了一个与原进程几乎完全相同的子进程,两个进程(父进程和子进程)会各自从 fork
调用处继续执行,并分别得到不同的返回值。
写时拷贝:写时拷贝是 Linux 内核中用于优化进程创建和内存管理的关键技术。它通过延迟内存复制来提高性能,减少不必要的开销。
作用:当调用 fork()
创建子进程时,传统方式会立即复制父进程的所有内存,但这会导致大量无用的内存拷贝,而写时拷贝优化了这一过程,具体分为三个部分:(1)父子进程共享相同的内存页(代码段、数据段、堆栈等)。(2)仅当某一进程尝试修改内存时,内核才真正复制该页,使父子进程拥有独立的副本。(3)如果子进程不修改内存,则永远不会复制,节省时间和内存。
写时拷贝工作流程:
(1) fork()
调用时
内核仅复制进程的页表(指向相同物理内存),并标记所有内存页为 COW(只读)。
父子进程的代码、数据、堆栈等仍然共享同一份物理内存。
(2) 写操作触发时
当父进程或子进程尝试写入某个内存页时,CPU 会触发页错误。
内核检测到该页是 COW 页,于是:(1)分配新的物理内存页。(2)复制旧页内容到新页。(3)更新进程的页表,使其指向新页。(4)恢复进程执行,此时写入操作在新页上进行,不影响另一个进程。
#include <unistd.h>
#include <stdio.h>int main() {int x = 42; // 父子进程共享该变量(COW)pid_t pid = fork();if (pid == 0) {// 子进程尝试修改 xx = 100; // 触发 COW,内核复制该页printf("Child: x = %d\n", x); // 输出 100} else {// 父进程的 x 仍然是 42printf("Parent: x = %d\n", x); // 输出 42}return 0;
}
基本用法:
#include <unistd.h>
#include <stdio.h>int main() {pid_t pid = fork();if (pid == -1) {perror("fork failed");return 1;} else if (pid == 0) {printf("Child process (PID: %d)\n", getpid());} else {printf("Parent process (PID: %d), Child PID: %d\n", getpid(), pid);}return 0;
}
fork 之后通常要用if进行分流
关键特性:
写时复制:子进程共享父进程的内存空间,仅当任一进程尝试修改内存时,内核才会复制相关部分,减少开销。
独立运行:子进程与父进程并发执行,调度顺序由内核决定。
继承资源:子进程继承父进程的文件描述符、信号处理方式、环境变量等。