Linux 进程基础(三):进程是什么、进程的创建与查看
摘要:
目录
- ➡️〇、导入:OS 的管理对象与管理“起点”
- 1. OS管理的对象是谁?
- 2. OS 何时开始管理对象:以U盘插入为例
- (1)硬件插入与检测:物理连接≠立即管理
- (2)驱动程序加载:管理的核心起点🟢
- (3)资源分配与逻辑映射:从物理设备到可操作对象
- 3. 类比理解:学校管理 🆚 OS管理
- 一、💻进程是什么?
- 1. 概念上:正在执行的程序
- 2. 实际上:担当分配资源的实体
- 二、进程管理的基石:描述进程
- 1. PCB:进程控制块
- 2. Linux:task_struct
- (1)核心字段解析
- (2)双链表结构简析
- 三、如何查看进程?
- 1. 查看进程信息的方法
- 2. 区分 cwd 与 pwd
- 四、如何创建进程?
- 1. 命令行中直接启动进程:bash
- 1)bash 是一个进程,且被内核统一管理
- 🤔bash 的 Parent Process 是用户态吗?登录进程何时启动?
- 2)bash 是内核与用户之间的 “交互中介”,但并非唯一中介
- 🤔系统调用是操作系统内部的代码吗?
- 🤔开发人员写的代码直接使用系统调用函数,算 “直接调用” 吗?bash 在其中起什么作用?
- 🤔为什么登录程序和 bash 都要 “先创建子进程再替换”?
- 2. 通过代码创建进程:fork
- fork函数特殊的返回值机制
- 进程的独立性与写时拷贝 (Copy-on-Write, COW)
➡️〇、导入:OS 的管理对象与管理“起点”
OS怎么算管理了对象👉管理的对象是谁?什么时候开始管理的?例如, U盘插入到电脑里就被管理起来了吗?
OS 对 对象的管理是一个涉及硬件检测、驱动加载、资源分配的复杂过程。以下将以U盘插入为例拆解具体流程:
1. OS管理的对象是谁?
OS管理的对象可分为硬件资源和软件资源两大类,具体包括:
- 硬件资源:
- 存储设备(硬盘、U盘、SSD等)
- 输入输出设备(键盘、鼠标、显示器、打印机等)
- 计算资源(CPU、内存)
- 网络设备(网卡、路由器等)
- 软件资源:
- 进程 与线程(程序运行实例)
- 文件系统(文件、目录、权限)
- 内存空间(虚拟内存、物理内存分配)
- 网络连接(Socket、端口)
2. OS 何时开始管理对象:以U盘插入为例
(1)硬件插入与检测:物理连接≠立即管理
- 动作起点:U盘插入USB接口。
- 系统反应:USB控制器检测到接口电压变化(硬件层面的物理信号)。控制器向CPU发送中断请求(IRQ),告知“有设备接入”。
此时OS仅知道“有硬件插入”,但尚未开始真正管理,因为:
- 不知道设备类型(U盘、键盘或其他)。
- 无法操作设备(无驱动程序、未分配资源)。
(2)驱动程序加载:管理的核心起点🟢
- 步骤1:设备识别
- U盘通过USB接口发送设备描述符(包含厂商ID、产品ID等信息)。
- OS根据描述符查找对应的驱动程序(Windows在注册表/驱动库,Linux在
/lib/modules
)。
- 步骤2:驱动初始化
- 加载驱动程序到内存,执行初始化函数(如注册设备操作接口)。
- 驱动向OS注册设备类型(如“存储设备”),并声明可支持的操作(读写、格式化等)。
驱动加载完成时,OS才真正开始管理设备
- 此时可:
- 通过驱动控制设备硬件(如发送指令读取U盘存储介质)。
- 为设备分配系统资源(如内存缓冲区、中断号)。
(3)资源分配与逻辑映射:从物理设备到可操作对象
- 步骤1:分配设备标识符
- Windows分配盘符(如F:),Linux创建设备文件(如
/dev/sdb
)。 - 建立设备与文件系统的映射关系(如FAT32、NTFS文件系统)。
- Windows分配盘符(如F:),Linux创建设备文件(如
- 步骤2:文件系统挂载
- 扫描U盘文件系统结构(目录、文件元数据)。
- 为用户提供可视化接口(如资源管理器中显示U盘图标)。
- 用户可见结果:此时U盘可被读写操作,即OS完成了从“物理设备”到“可管理资源”的转换。
- 非管理阶段:插入瞬间至驱动加载前,OS仅知道设备物理存在,但无法执行任何操作(类似“看到人但不知道身份”)。
- 管理开始阶段:驱动程序加载并初始化后,OS获得设备控制权,可进行:
- 状态监控(如U盘剩余空间、读写状态)。
- 资源调度(如分配CPU时间片处理U盘读写请求)。
- 安全管理(如权限控制、病毒扫描)。
3. 类比理解:学校管理 🆚 OS管理
场景 | 学校管理类比 | OS设备管理对应逻辑 |
---|---|---|
物理进入 | 人员进入校园区域 | U盘插入USB接口 |
身份识别 | 核查证件(学生证、工作证) | 读取设备描述符(厂商ID等) |
注册登记 | 录入系统(学生档案、员工表) | 驱动程序加载并注册设备 |
开始管理 | 纳入考勤、权限管理 | 分配盘符、文件系统挂载 |
可操作控制 | 安排课程、工作任务 | 读写文件、格式化等操作 |
PS.特殊情况说明
- 无驱动程序时:若OS找不到U盘驱动(如老旧设备或自定义硬件),则无法管理,设备显示为“未知设备”(类似学校里未登记的访客,无法使用校园系统)。
- 热插拔与冷启动管理差异:
- U盘(热插拔):插入时动态加载驱动。
- 内置硬盘(非热插拔):系统启动时(BIOS阶段)已检测并加载驱动,管理起点为系统启动完成时。
一、💻进程是什么?
1. 概念上:正在执行的程序
- e.g. 打开电脑上的浏览器,浏览器这个程序开始运行,它就成为了一个进程;
- e.g. 启动音乐播放器播放歌曲,音乐播放器程序也变成了一个进程。
它们都处于运行状态。
2. 实际上:担当分配资源的实体
计算机的资源就像是城市里的各种基础设施,如道路、电力、水源等。进程在运行时,需要获取 CPU 的计算资源来执行代码,需要占用内存空间来存储数据和程序指令,还可能需要访问磁盘、网络等其他资源。OS 就像城市的管理者,会根据进程的需求,合理地分配这些资源,让各个进程都能有序运行。
进程(Process) 与 程序(Program) 的区别:
- 程序:它是一组静态的指令集合,就好比是一本菜谱,静静地躺在计算机的磁盘上,不会主动做任何事情。🌰 下载的游戏安装包、办公软件的安装文件……
- 进程:当程序被加载到内存中并开始执行时,它就变成了进程。进程是动态的,具有生命周期,会经历创建、运行、暂停、终止等阶段。就像根据菜谱开始烹饪美食的过程,从准备食材、烹饪到最终上菜,这一系列动态的操作就是进程。
二、进程管理的基石:描述进程
想要管理进程,得先把进程是什么、有哪些特点搞清楚。就像要管理好一群人,得先了解每个人的情况一样,只有描述清楚进程,后续管理操作才能顺利进行。
在计算机系统运行时,当我们一边用音频播放器听歌,一边运行游戏客户端打游戏,同时还打开电子书阅读软件看小说、视频播放程序刷剧时,这些应用程序对应的进程都会同时被加载到内存空间里运行。虽然从简单概念来说,程序加载就是把数据从硬盘等外存复制到内存,但实际的进程管理远比这复杂得多。
多个进程会同时运行,操作系统必须有一套完善的管理方法。 首先,系统需要详细记录每个进程的关键信息:
- 进程标识符就像是每个进程的“身份证号”,能唯一确定一个进程;
- 进程的运行状态则反映了它当前的情况,比如正在运行、准备运行或是因为等待资源而暂停;
- 此外,进程的优先级、占用了哪些系统资源等,也都是进程的重要属性。
这些信息合起来,就构成了对单个进程的完整描述。
对多进程进行统一集中的管理,就好比图书馆里有很多图书卡片,如果卡片散乱摆放,管理员找书就很困难。OS需要一种特殊的数据结构,把这些分散的进程信息整合起来。进程控制块 (Process Control Block,PCB) 就是这个关键 “工具”。PCB 里存储了进程标识符、运行状态、资源使用情况、程序下一步执行位置(程序计数器)等重要数据。通过链表、队列等数据结构,系统能把所有进程的 PCB 串联起来,形成一个进程管理 “目录”。这样一来,操作系统就能轻松地对进程进行创建、调度、终止等全流程管理,保障整个系统稳定高效地运行。
1. PCB:进程控制块
- PCB(Process Control Block,进程控制块),可以理解为记录进程的“个人档案📄”的专属结构体
struct
,包含了进程的所有属性合集,用于记录和管理进程的各种信息。 - 注意:即使可执行程序文件被删除了,正在运行的进程依然可以继续运行,因为进程运行时依赖的是加载到「内存」中的代码和数据,而不是磁盘上的程序文件。
2. Linux:task_struct
Linux 系统中的 PCB 「具体表现」为 task_struct。task_struct
(双链表) 就是用于描述进程的结构体 struct
。当系统创建一个进程时,就会在内存中创建一个 task_struct
实例,里面包含了进程的各种重要信息。下面我们来详细看看它的核心字段:
(1)核心字段解析
- 标识符:
pid
(Process identity,进程标识符):就像每个人都有独一无二的身份证号,每个进程都有自己的pid
,用于在系统中唯一标识这个进程。ppid
(Parent Process identity):表示该进程的 Parent 进程的pid
。在 Linux 中,通过命令行启动的进程,它们的 parent process 通常都是bash
。🌰 在终端中输入ls
命令查看文件列表,这个ls
进程就是bash
进程的 child process。
- 状态:用于表示进程当前所处的状态,例如运行态(正在使用 CPU 执行)、就绪态(准备好运行,等待 CPU 分配时间片)、阻塞态(因为等待某个资源,如 I/O 操作完成而暂停)。
- 优先级:决定了进程在竞争 CPU 资源时的优先程度,优先级高的进程更容易获得 CPU 时间片来执行。
- 程序计数器:记录了进程下一条要执行的指令在内存中的地址。
- 内存指针:指向进程所占用的内存空间,方便操作系统管理进程的内存资源。
- 上下文数据:保存了进程在运行过程中的各种中间数据和寄存器状态,当进程暂停或恢复运行时,这些数据可以保证进程能够继续正确执行。
- I/O 状态信息:记录了进程进行输入输出操作(如读写文件、网络通信)的相关状态。
- 记账信息:用于统计进程占用 CPU 时间、内存使用量等资源使用情况,方便操作系统进行资源管理和计费(在一些商业系统中)。
- other:还可能包含其他一些与进程相关的信息。
(2)双链表结构简析
如上图:
在 Linux 中,task_struct
就像一个带多个 “魔术扣” 的文件夹(比如图中的 p1),每个魔术扣就是 list_head
(如 run_list
、wait_list
)。这些魔术扣的特点是:
- 只扣住同类文件夹:同一个魔术扣只能连接状态相同的进程(比如 run_list 魔术扣只连接 “可运行” 的进程);
- 不存文件夹完整地址:魔术扣本身只记录前后相邻魔术扣的位置,但通过 “魔术算法” 能反向推算出自己所属文件夹的位置。
当进程状态变化时(比如从 “运行” 变 “等待”),内核只需:
- 将该进程的
run_list
魔术扣从ready_queue
链上摘下; - 把
wait_list
魔术扣挂到wait_queue
链上。
如何挂载到链表?
当内核需要将进程加入某个队列时:
- 找到目标链表的头节点(例如调度器的就绪队列)。
- 修改指针:将进程的run_list标签插入到链表中。
// 伪代码:将进程添加到就绪队列struct task_struct *p = ...; // 要添加的进程struct list_head *ready_queue = ...; // 就绪队列头// 将进程的run_list标签插入到就绪队列list_add(&p->run_list, ready_queue);// 等价于以下指针操作:
p->run_list.next = ready_queue->next;
p->run_list.prev = ready_queue;
ready_queue->next->prev = &p->run_list;
ready_queue->next = &p->run_list;
- 链表操作只涉及
list_head
的指针,不影响task_struct
的其他部分。 - 同一个进程可以通过不同的
list_head
同时存在于多个链表中。
三、如何查看进程?
1. 查看进程信息的方法
在 Linux 系统中,我们可以通过多种方式查看进程的相关信息📋:
/proc
(系统文件):这是一个非常神奇的虚拟文件系统,它里面存储了系统中所有进程的相关信息。- 每个进程在
/proc
目录下都有一个以其pid
命名的子目录,比如pid
为1000
的进程,它的信息就存储在/proc/1000
目录下。 - 在这个目录中,我们可以找到进程的各种信息文件,如记录进程状态的
status
文件、记录进程命令行参数的cmdline
文件等。
- 每个进程在
getpid
(function):在编程中,如果我们想获取当前进程的pid
,可以使用getpid
函数。例如在 C 语言中,只需要调用getpid()
函数,它就会返回当前进程的pid
。
2. 区分 cwd 与 pwd
cwd
(Current Working Directory,当前工作目录):- 它是进程的一个系统属性,表示 进程当前的工作目录 。
- 在 Linux 中,我们可以通过
/proc/{进程ID}/cwd
这个符号链接查看进程的cwd
。 - 在编程中(如 C/C++),可以使用
getcwd()
函数获取当前进程的cwd
。
pwd
(Print Working Directory,打印工作目录):- 这是 Linux/Unix 系统的内置命令,用于在终端中显示当前所在的工作目录的绝对路径。
- 关键点:默认情况下,程序启动的路径就是当前所在路径(即
cwd
)。不过我们可以通过chdir
函数来更改进程的工作目录 (cwd
)。
四、如何创建进程?
在 Linux 系统中,创建进程主要有以下两种方式:
1. 命令行中直接启动进程:bash
这是最常见的手动启动方式。当我们在终端中输入一个命令,如ls
。从本质上讲,启动进程的过程就是创建进程的过程。命令行启动的进程都是 bash
的 Child Process。
⚠注意:bash 并非所有用户进程的 Parent Process,而是 “用户通过 bash 交互而启动的进程” 的 Parent Process。
1)bash 是一个进程,且被内核统一管理
- bash 本质上是一个用户态应用程序,当用户登录 Linux 系统(如通过终端、SSH)时,系统会启动一个 bash 进程(进程类型为interactive shell)。
- bash 是一个进程,由内核的进程管理模块统一管控:内核会为 bash 分配 PID(进程 ID)、管理其生命周期(创建、运行、暂停、终止)、调度 CPU 时间片,并通过进程控制块(PCB)记录其状态(如运行态、就绪态)。
- bash 的 Parent Process 通常是登录相关进程(如sshd、getty等),而 bash 自身会作为 “用户通过它启动的命令 / 程序” 的 parent 进程(通过fork()系统调用创建子进程)。
🤔bash 的 Parent Process 是用户态吗?登录进程何时启动?
🟢首先明确:所有由用户直接交互的进程(包括 bash 及其 Parent Process)都属于用户态。
bash 的 Parent Process (如 sshd、getty)本质是 “用户态的服务程序”,它们不在内核里,而是运行在用户空间(和 bash、ls 等程序一样,只是功能是处理登录)。
登录相关进程(如 sshd)通常是 “守护进程”(后台服务),随操作系统启动而启动,之后一直驻留在内存中等待触发(比如等待用户的 SSH 连接请求)。它们由系统的初始化进程(如 systemd)创建,属于用户态的 “系统服务”,但并非内核的一部分。
2)bash 是内核与用户之间的 “交互中介”,但并非唯一中介
用户无法直接调用系统调用 (需要通过程序代码触发),而 bash 的核心作用就是作为用户与内核之间的 “翻译官” 和 “协调者”:
用户通过终端输入命令(如ls、mkdir),bash 首先解析命令(识别命令名、参数、管道等):
对于简单命令(如ls),bash 会通过fork()创建一个子进程,再通过execve()系统调用加载/bin/ls程序替换子进程;程序运行过程中(如ls需要读取目录),会通过自身代码中的系统调用(如getdents())向内核发起请求,内核处理后将结果返回给程序,最终通过 bash 传递给用户。
对于内置命令(如cd、export),bash 会直接执行(无需创建子进程),必要时仍通过系统调用与内核交互(如cd需要调用chdir()系统调用切换目录)。
🤔系统调用是操作系统内部的代码吗?
🟢是的,系统调用的实现代码属于内核的一部分,封装在操作系统内核中。
从代码角度看,系统调用是内核源码里的一组函数(比如处理文件操作的sys_open、进程创建的sys_fork),这些函数直接操作硬件资源,属于内核空间的代码。
用户态程序(包括 bash、程序员写的代码)无法直接访问内核代码,只能通过 “系统调用接口”(一种约定的触发方式,比如特定的 CPU 指令)间接调用。可以理解为:内核在自己的 “地盘”(内核空间)提供了一组 “办事窗口”(系统调用),用户态程序只能通过窗口递请求,不能直接闯进内核的 “办公室”。
🤔开发人员写的代码直接使用系统调用函数,算 “直接调用” 吗?bash 在其中起什么作用?
🟢算 “直接调用”(相对于通过 bash 间接触发而言),且此时 bash 通常不参与工作。
-
开发人员写的代码(比如 C 程序)中调用的
open()
、fork()
等函数,本质是通过 glibc 等库封装的 “系统调用接口”,最终会触发内核的系统调用(比如sys_open
)。这个过程不需要 bash 参与,程序直接与内核交互。- 举例:你写了一个 C 程序
test.c
,里面有pid_t pid = fork();
,编译后运行./test
。此时:若你通过 bash 输入./test
启动程序,bash 的作用仅为 “创建子进程并替换为 test 程序”(启动它);
- 举例:你写了一个 C 程序
-
程序运行时,
fork()
直接触发内核的系统调用,整个过程 bash 已经 “脱手”,只是等待程序结束后继续接受命令。
核心区别:bash 是 “命令行交互中介”,而程序中的系统调用是 “代码级直接交互”,两者场景不同。
🤔为什么登录程序和 bash 都要 “先创建子进程再替换”?
🟢这是 Unix-like 系统的经典设计,核心目的是保持源进程的持续存在,避免被新程序 “覆盖”。
- 对于登录程序(如 sshd):
sshd 的作用是 “持续监听 SSH 连接”,如果它直接 “变身” 为 bash,那么处理完一个用户的连接后,sshd 本身就消失了,无法再处理其他用户的新连接。- 所以设计上:
①sshd (Parent)
先通过fork()
创建一个 “临时副本”(子进程 -sshd (Child)
),自己继续监听新连接;
②这个临时副本再通过execve()
“变身” 为 bash,专门服务当前用户。
③这样既不影响 sshd 的持续工作,又能为用户提供 bash 交互。
- 所以设计上:
- 对于 bash 处理外部命令(如
ls
):
bash 的核心作用是 “持续等待用户输入命令”,如果它直接 “变身” 为ls
,那么ls
执行完后,bash 就消失了,用户无法再输入下一个命令。- 所以设计上:
①bash (Parent)
先通过fork()
创建一个 “自己的副本”(子进程 -bash (Child)
),自己继续等待用户输入;
②这个副本再通过execve()
“变身” 为 ls,执行完后自动退出。
③这样 bash 始终保持存在,用户可以连续输入命令(ls
、cd
、mkdir
等)。
- 所以设计上:
2. 通过代码创建进程:fork
在编程中,我们可以使用 fork
函数来创建新的进程。
fork函数特殊的返回值机制
- 成功时:它会返回两个值,一个是当前进程(Parent Process)的
pid
,另一个是新创建的 Child Process 的pid
。- 为什么会有两个返回值呢? 这是因为在创建子进程的过程中,系统会执行一系列操作:
- 首先找到 Parent Process 的 PCB 对象。
- 然后使用
malloc
分配内存创建子进程的task_struct
。 - 接着根据 Parent Process 的 PCB 初始化子进程的 PCB。
- 让子进程的 PCB 指向 Parent Process 的代码数据。
- 最后将子进程放入调度队列等待执行。
- 在这个过程结束后,
fork
函数会返回。当 Parent Process 被调度执行时,fork
返回子进程的pid
;当子进程被调度执行时,fork
返回0
。实际上,操作系统是通过寄存器巧妙地实现了返回值返回两次。 - 而且,
fork
之后,Parent Process 和子进程共享代码,但这些代码是只读的,不能被修改。
- 为什么会有两个返回值呢? 这是因为在创建子进程的过程中,系统会执行一系列操作:
- 失败时:
fork
函数会返回-1
,通常是因为系统资源不足(如内存不足)等原因导致创建进程失败。
进程的独立性与写时拷贝 (Copy-on-Write, COW)
- 这里还有一个重要的概念 —— 进程的独立性:虽然父进程和子进程共享代码,但为了保证每个进程的独立性和数据安全,各个进程需要拥有自己私有的数据。
- Linux 采用了写时拷贝(Copy-on-Write, COW) 技术来实现这一点:
- 当某个进程试图修改共享的数据时,系统才会为其复制一份数据,让该进程在自己的副本上进行修改。
- 例如,我们通过
fork
创建进程,并用一个变量接受fork
的返回值,就会发现同一个变量在父进程和子进程中具有不同的数值,甚至通过调试可以看到这个变量的地址是相同的。 - 但实际上,这个地址并不是物理地址,而是虚拟地址,操作系统会通过地址映射机制将其转换为不同的物理地址,从而保证每个进程的数据独立。
END