跳到主要内容Linux 进程核心概念与机制详解 | 极客日志C算法
Linux 进程核心概念与机制详解
综述由AI生成Linux 进程的核心概念,包括冯诺依曼体系结构、操作系统定义及目的。详细阐述了进程控制块(PCB)、struct task_struct 结构体及其作用。讲解了进程的基本操作如查看进程、fork 创建子进程,以及进程状态(运行、阻塞、僵尸等)。深入分析了进程优先级(PRI、NI)、竞争与并发概念、上下文切换机制和 O(1) 调度队列。此外还涵盖了环境变量概念、命令及特点,最后探讨了进程虚拟地址空间与分页机制的作用。
灭霸17K 浏览 1、冯诺依曼体系结构
- 输入设备:键盘,鼠标,网卡,磁盘等。
- 输出设备:显示器,网卡,磁盘等。
- 存储器:即内存。
- CPU:简单来说,是中央处理器 (运算器 + 控制器)。
注意:
- 输入输出设备的传输效率低,但是,让输入输出的设备的传输效率变高,成本太高,所以出现内存,即效率与成本之间的平衡,才普及了电脑。
- 程序的运行需要 CPU,而CPU 只能访问内存,所以程序必须加载到内存中。
- 数据流动的本质:多台冯诺依曼体系结构的交互。
2、操作系统 (Operating System)
2.1 基本概念
操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell 程序等等)
2.2 目的
操作系统,是一款进行 软硬件管理 的软件。管理:先描述 (类),再组织 (数据结构)。
system call (系统调用),驱动程序,都是为了屏蔽底层细节,外部实现统一。安全且方便。
- 系统调用封装内核 → 对应用程序统一。
- 驱动程序封装硬件 → 对操作系统统一。
3、Linux 的进程
3.1 基本概念
3.1.1 PCB
PCB(Process Control Block),进程控制块,一种类型,Linux中的 PCB 为:struct task_struct。
3.1.2 struct task_struct
内容分类(后续会详细介绍)
- 标识符 (PID):描述本进程的唯一标识符,用于区分其他进程。
- 状态:任务状态,包括退出代码、退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,以及与其他进程共享的内存块指针。
- 上下文数据:进程执行时处理器的寄存器中的数据(例如:CPU 寄存器状态)。
- I/O 状态信息:包括未完成的 I/O 请求、分配给进程的 I/O 设备,以及进程使用的文件列表。
- 记账信息:可能包括处理器占用时间、时钟周期总和、时间限制、计账号等。
- 其他信息:与进程相关的其他数据。
在 Linux 内核中,所有进程均通过 struct task_struct 结构体描述,并以双向链表的形式 (即队列) 组织和管理。
3.1.3 进程的定义
进程 = 内核数据结构对象 (PCB)+ 代码和数据
3.2 基本操作
3.2.1 查看进程
- /proc 是一个虚拟文件系统,提供内核和进程信息的实时访问。
- 每个进程的信息存储在 /proc/[PID]/ 目录下,例如:
ls /proc/1/ # 查看 PID=1 的进程信息(通常是 init/systemd)
top
top -p PID1,PID2,PID3
top -u username
- k → 结束指定 PID 的进程(输入 PID 后回车)。
- M → 按内存占用排序。
- P → 按 CPU 占用排序(默认)。
- q → 退出 top。
- 可以配合 grep 进行搜索。
- ;和&&可以同时执行多条命令。
- 命令本身也是进程。
- 通过系统调用,获取进程标识符(PID & PPID)
- getpid():获取当前进程的 PID。
- getppid():获取当前进程的父进程 PPID。
#include <stdio.h>
#include <unistd.h>
int main() {
printf("PID: %d\n", getpid());
printf("PPID: %d\n", getppid());
return 0;
}
3.2.2 fork
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
int ret = fork();
printf("hello proc : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
- 创建成功,两个返回值,对父进程返回子进程的 PID,对子进程返回 0。因为父:子 = 1:N,父进程需要区分子进程,而子进程能通过 PPID 找到父进程。所以可以 if,让父子进程执行不同的语句。创建失败,返回**-1**。
- fork() 创建子进程后,父子进程从 fork() 返回处继续执行。注意:子进程不会执行 fork() 之前的代码。
- 当父子进程尝试修改数据,会发生写时拷贝(减少创建子进程的时间,减少内存浪费),重新拷贝一份数据。所以父子进程独立运行。
3.3 进程状态
3.3.1 操作系统的进程状态
- 运行:PCB 对象在调度队列中,正在运行 (运行) 或准备运行 (创建 + 就绪)。
- 阻塞:等待某种设备或资源就绪,PCB 对象进入设备队列或资源队列。
- 挂起:内存不足,将进程的代码和数据放到磁盘中,但 PCB 仍然保留在内存中(或部分信息保留)。进程原先是运行状态,就是就绪挂起,进程原先是阻塞状态,就是阻塞挂起。
-
一个 CPU,一个调度队列
-
PCB 对象,可以同时在不同的数据结构中,即可以在不同的队列中。
-
进程的状态,就是PCB 对象在不同队列之间的流动,本质是数据结构的增删改查。
-
阻塞是进程的 正常状态(因等待资源主动暂停),而 饥饿是 异常现象(可能是一直阻塞,或进程可能无需等待资源,但因调度问题无法运行等)
3.3.2 Linux 的进程状态
static const char *const task_state_array[] = {
"R (running)",
"S (sleeping)",
"D (disk sleep)",
"T (stopped)",
"t (tracing stop)",
"X (dead)",
"Z (zombie)",
};
- R:运行中或就绪(进程一创建,就进入就绪状态)。
- S:可中断休眠(浅睡眠,一种阻塞),能被操作系统杀死。
- D:不可中断休眠(深睡眠,一种阻塞),不能被操作系统杀死。
- T:暂停,如:Ctrl+z。
- t:暂停,如:debug 的断点。
- X:死亡,进程结束。
- Z:僵尸,子进程退出,父进程需要获取子进程退出前的信息(即子进程 PCB 对象里面的信息,其指向的代码和数据已被释放。可选),并释放子进程的 PCB 对象(必要),如果父进程没有"回收"子进程,那么子进程被称为"僵尸进程",其PCB 对象将会一直存在,造成内存泄漏。如果父进程先结束,其子进程称为"孤儿进程",会被 1 号进程"领养",当子进程退出时,会被 1 号进程回收资源,不会成为"僵尸进程"。
- 注意:如果子进程已经变成僵尸进程,这个时候父进程没有回收,并且父进程退出了,这个时候,僵尸子进程 (已终止) 不会变成孤儿进程 (仍在运行),但会被内核'过继'给 init 进程(或 systemd,PID=1),最终由 1 号进程自动回收。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t id = fork();
if (id == 0) {
printf("child pid:>%d, ppid:>%d\n", getpid(), getppid());
return 0;
} else {
printf("parent pid:>%d, ppid:>%d\n", getpid(), getppid());
sleep(10);
}
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t id = fork();
if (id == 0) {
printf("child pid:>%d, ppid:>%d\n", getpid(), getppid());
sleep(10);
return 0;
} else {
printf("parent pid:>%d, ppid:>%d\n", getpid(), getppid());
}
return 0;
}
3.4 进程优先级
3.4.1 基本概念
- 优先级是一种数字,值越低,优先级越高。
- 优先级,能得到某种资源(只是先后问题),权限,能否得到某种资源。
- Linux,基于时间片的分时操作系统,要考虑公平性,所以优先级变化不大。
3.4.2 PRI&&NI
- PRI:进程的优先级,默认 80。
- NI:nice 值,进程优先级的修正数据,默认 0。范围是**[-20,19]**。
- 进程真实的优先级 PRI= 80 + NI。所以优先级的范围是**[60,99]**。保证公平性。
- NI 的存在 是为了在 灵活性(用户态调整)和 稳定性(内核控制)之间取得平衡。
3.4.3 竞争&&独立&&并行&&并发
- 竞争:系统中进程数量远多于 CPU 资源(如单核 CPU 只能同时运行 1 个进程),因此进程之间需要竞争 CPU 时间片、内存、I/O 等资源。通过 优先级(Priority) 或 调度算法(如时间片轮转)来合理分配资源,确保高优先级或关键任务能优先执行。
- 独立:每个进程拥有独立的地址空间、文件描述符、寄存器状态等资源,一个进程崩溃不会直接影响其他进程。
- 并行:多个进程在 多个 CPU/核心上真正同时运行(物理层面的同时执行)。
- 并发:多个进程在 单个 CPU 上通过快速切换(时间片轮转) 模拟'同时运行'的效果(逻辑层面的交替执行)。
3.5 进程切换
CPU 上下文切换(Context Switch),实际上是任务切换,或CPU 寄存器的切换。
- 保存现场:
当多任务操作系统决定切换到另一个任务时,首先将当前运行任务的CPU 寄存器状态完整保存到该任务的私有堆栈中。
- 恢复现场:
从待运行任务的堆栈中加载其之前保存的寄存器状态到 CPU。
- 切换执行:
CPU 开始执行新任务的指令流。
- 进程在一个时间片内占用 CPU,不会一直占用。
- 进程切换的本质:保存和恢复 进程硬件上下文的数据(即 CPU 寄存器的状态)。
3.6 Linux2.6 内核进程 O(1) 调度队列
- 对于active 队列,先看nr_active,有没有进程,再通过bitmap[5],按照优先级,快速定位队列,最后挑队首的进程,执行。
- 进程执行完一个时间片,进入expired 队列(防止高优先级进程执行完一个时间片,又插队)。当active 队列为空时,swap(&active,&expired),交换两个指针,继续调度 active 队列。
- 新来一个进程,如果放到 expired 队列,就是就绪状态,如果放到 active 队列,也是就绪状态,但是"插队"了。
- active中的进程,如果能直接更改 PRI,就需要对队列进行修改,麻烦且影响公平性 (造成进程饥饿),所以有NI(nice 值),执行完一个时间片后,进入 expired 队列时,再更新优先级,按新的优先级插入。
4、Linux 的环境变量
4.1 基本概念
环境变量是操作系统中用于指定运行环境参数的键值对 (KEY=VALUE)。
KEY是环境变量的名字,VALUE是环境变量的内容。
4.2 常见的环境变量
4.3 环境变量的相关命令
4.3.1 查看环境变量
- env:显示当前进程所有的环境变量。
- echo $环境变量名字:显示环境变量的内容。
- set:显示当前进程所有的变量。如:直接 i=10 或 i,定义本地变量 i。
int main(int argc,char* argv[],char* env[]),argv是命令行输入的命令字符串数组(以空格为分隔符,将命令分成若干个字符串,数组以 NULL 结尾),argc是argv 数组元素的个数,env是该进程 环境变量的字符串数组(环境变量放在字符串里,数组以 NULL 结尾)。
- getenv(),在当前进程,根据环境变量的名字,获取环境变量的内容。
- 全局变量 environ(环境变量字符串数组,数组以 NULL 结尾),必须先extern char声明,再使用。
4.3.2 修改环境变量
- 环境变量名=$环境变量名:内容,给环境变量加内容。如:PATH=$PATH:/home/Lzc/test。
- export 变量名="值",新增环境变量。
以上关闭终端,重新登录,就会失效。想要永久生效,就要更改配置文件 (/.bashrc 或/.bash_profile),因为 bash 每次都是拷贝配置文件的内容。
4.3.3 删除环境变量
- unset 变量名:清除变量,本地变量和环境变量都可以。
4.4 环境变量的特点
- 新创建的子进程会继承父进程的环境变量(全局性)。进程相互独立,所以环境变量也独立,互不影响。
- 本地变量不会被新创建的子进程继承。
5、Linux 的进程虚拟地址空间
5.1 程序地址空间
5.2 问题抛出
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int g_val = 0;
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 0;
} else if (id == 0) {
g_val = 100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
} else {
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
return 0;
}
- 该地址绝对不是物理地址!
- 在 Linux 系统下,这种地址称为虚拟地址。
- 我们用 C/C++语言看到的地址都是虚拟地址,物理地址对用户完全不可见,由操作系统统一管理。
5.3 进程虚拟地址空间和分页机制
所以,程序地址空间,准确来说是,进程虚拟地址空间。
struct task_struct {
struct mm_struct *mm;
struct mm_struct *active_mm;
};
struct mm_struct {
struct vm_area_struct *mmap;
struct rb_root mm_rb;
unsigned long task_size;
unsigned long start_code, end_code;
unsigned long start_data, end_data;
unsigned long start_brk, brk;
unsigned long start_stack;
unsigned long arg_start, arg_end;
unsigned long env_start, env_end;
};
struct vm_area_struct {
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm;
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops;
unsigned long vm_pgoff;
struct file *vm_file;
void *vm_private_data;
atomic_long_t swap_readahead_info;
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy;
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;
一个进程,一个页表,进行虚拟地址和物理地址的映射。
5.4 虚拟地址空间和分页机制的作用
- 将地址,"无序"变"有序"。
- 地址转化的过程中,可以对操作进行合法判定,进而保护物理内存(根据权限)。
- 让进程管理和内存管理在一定程度上解耦合。
5.5 拓展
- 可以不加载代码和数据到物理内存,只有 struct task_struct,struct mm_struct,页表,需要访问时,'缺页中断',再加载。所以创建进程,先有 struct task_struct,struct mm_struct 等,再有代码和数据。
- 当物理内存不足时,对于阻塞的进程,通过页表换出物理地址 (释放内存),变为阻塞挂起,腾出内存空间。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online