
往期《Linux系统编程》回顾:/------------ 入门基础 ------------/ 【Linux的前世今生】 【Linux的环境搭建】 【Linux基础 理论+命令】(上) 【Linux基础 理论+命令】(下) 【权限管理】 /------------ 开发工具 ------------/ 【软件包管理器 + 代码编辑器】 【编译器 + 自动化构建器】 【版本控制器 + 调试器】 【实战:倒计时 + 进度条】 /------------ 系统导论 ------------/ 【冯诺依曼体系结构 + 操作系统基本概述】
hi~ 小伙伴们大家好呀!(ノ≧∀≦)ノ✨ 前面铺垫了这么久,今天终于要正式开启《Linux 系统编程》的核心之旅啦!而咱们遇到的第一个 “拦路虎”,就是系统编程里的第一座大山🏔️ ——进程!(。•̀ᴗ-)✧🌟
先给大家剧透下今天的学习重点 ——【进程入门】,核心内容主要包括这两块:🎉٩(◕‿◕)۶🎉
进程的概念:将会从全新的视角认识进程 (包含对进程的顶级理解)(≧ω≦)ゞ进程的创建:学会创建进程 (做到知其所以然) 🔍( ̄^ ̄)ゞ
进程(Process):是操作系统进行资源分配和调度的基本单位。
想象一个
餐厅的厨房(操作系统):
一个菜谱(程序)可以同时被多个厨师使用,做出多份鱼香肉丝(多个进程)
每个厨师都有自己的工作台和原料,互不干扰。
看完前面关于进程的介绍,大家是不是觉得自己已经搞懂 “什么是进程” 了? 悄悄告诉你们,其实到这里,我们还没真正触及进程的本质。 前面聊的,只是大家通常认知中的进程 —— 算是 “形” 的层面,今天,咱们就来揭开进程 “神” 的面纱,看看它最核心的本质是什么。
从更底层的结构来看: 进程 = 内核数据结构对象(即进程控制块 PCB) + 程序自身的代码与数据
操作系统需要管理多个被加载到内存中的程序(也就是多个进程),其管理思路遵循 “先描述,再组织” 的核心原则:
struct task_struct)来 “描述” 每个进程的所有属性
task_struct 节点 以存储在磁盘中的可执行程序 cmd 为例,它运行成为进程的过程如下:
cmd 以文件的形式存储在磁盘里。当用户执行 ./cmd 命令时,操作系统会把 cmd 的代码和数据从磁盘加载到内存当中cmd 进程),把 CPU 资源分配给它,使得它的代码能够得以执行 值得注意的是,进程的所有属性,都可以直接或者间接地通过 task_struct 来获取,这也体现了 PCB 作为 “进程核心描述载体” 的重要作用。

PCB 即进程控制块(Process Control Block):是操作系统管理进程的关键数据结构,用于记录进程的各种信息,是进程存在的唯一标志。
PCB 包含的信息十分丰富,主要可分为以下几类:
进程 ID(PID),它是进程的唯一编号,在系统中起到区分不同进程的作用父进程 ID(PPID),用于表明该进程的创建者是谁,方便系统进行进程间关系的管理就绪状态(进程已准备好,等待 CPU 调度执行)运行状态(进程正在 CPU 上执行)阻塞状态(进程因等待某一事件,如:I/O 操作完成、信号量等而暂停执行)内存资源,记录进程占用内存的起始地址、大小等,以便操作系统进行内存分配和回收文件列表,记录进程当前打开的所有文件的信息,包括文件描述符、文件指针等,使得进程能够正确地对文件进行读写等操作进程的优先级,优先级高的进程在调度时会优先获得 CPU 资源进程的时间片,在分时操作系统中,每个进程被分配一定的 CPU 执行时间,即时间片
getpid:是一个常用的系统调用(system call),主要功能是获取当前进程的进程ID(PID)
task_struct)中的 PID 信息并返回 简单说:getpid 就是进程 “自我介绍” 的工具,让程序能知道自己在系统中的唯一编号。


ps:用于查看当前系统中进程状态的重要工具。 (注:ps命令其实有很多值得深入探讨的细节,但今天它并不是主角。关于ps的学习,我们会在后续的内容中逐步渗透 —— 如果一开始就把ps的所有相关知识一股脑列出来,初学的小伙伴很可能难以理解,而死记硬背的话,那就更是与 “融会贯通” 的学习理念背道而驰了) 今天我们就先学习会使用ps吧:
基本语法:ps [选项]
ps a ,就能看到不同用户在各个终端中运行的进程情况ps x 就可以把这些进程展示出来,方便对后台服务进行管理和监控
思考与探究:为什么执行
ps ajx | grep code.exe命令时,筛选出来的进程除了code.exe,总是还会有一个grep --color=auto code.exe呢?
ps ajx 会先列出系统中当时的进程信息,然后 grep code.exe 作为一个正在运行的命令进程,也会被 ps ajx 捕获到。
grep 命令本身在执行 “筛选含 code.exe 关键字的进程” 这个任务,所以它自己的进程信息也会被包含在 ps ajx 的输出里grep code.exe 筛选出来,于是就出现了除 code.exe 外,还能看到 grep --color=auto code.exe 的情况code.exe 进程,可进一步用 ps ajx | grep code.exe | grep -v grep(grep -v grep 用于排除含 grep 关键字的行)
注意事项:
ps命令所显示的进程状态是执行命令瞬间的状态 由于进程状态是动态变化的,如果需要持续监控进程状态,可以采用以下两种方式:
while true; do ps ajx | grep code.exe | grep -v grep; sleep 1; done ps ajx | grep code.exee | grep -v grep,持续输出与code.exe进程信息,便于观察其状态变化

/proc:是一个非常特殊的目录,它并非存储在磁盘上的普通文件系统,而是一个虚拟文件系统(procfs)
核心特点:
/proc 中的文件和目录并非实际存储在磁盘上,而是由内核在运行时根据系统状态动态创建和更新,当读取这些文件时,内核会实时生成内容/proc 不会占用实际的磁盘存储空间(可用 df /proc 查看,会发现其占用为 0)/proc 可以直接获取内核参数、硬件信息、进程状态等关键数据,是用户与内核交互的重要接口
/proc/[PID]/exe:指向进程可执行文件的符号链接。

/proc/[PID]/cwd: 是表示某个进程当前的工作目录的符号链接。



在 C 语言中,当使用
fopen函数创建文件且未指定完整路径(只给出文件名,比如fopen("hello.txt", "a");)时,文件会在执行程序所在路径下创建。 这可以从进程和当前工作目录(cwd,Current Working Directory)的角度来解释:
进程的当前工作目录(cwd)
/proc/[PID]/cwd 中([PID] 是进程的 ID) fopen 函数的路径解析机制
fopen 函数在处理文件路径时,会区分绝对路径和相对路径:
fopen 的第一个参数传入的是绝对路径(如:fopen("/home/user/hello.txt", "a");)时,操作系统会根据路径从根目录开始查找和访问文件, 比如在上述示例中,就会在 /home/user/ 目录下操作 hello.txt 文件fopen 的第一个参数传入的是相对路径(也就是只有文件名,没有从根目录开始的完整路径信息,如:fopen("hello.txt", "a");)时,进程会以当前工作目录为起点去解析这个相对路径在 Linux 系统中,进程的创建遵循 “父子继承” 原则—— 被创建的新进程则称为 “子进程”:
systemd,PID 通常为 1)外fork() 或 clone() 等系统调用创建的 值得注意的是,一个父进程并非只能创建一个子进程 —— 它可以通过多次调用 fork() 生成多个子进程,这些子进程共享同一个父进程。
正因为这种 “一个父进程可衍生多个子进程” 的特性,Linux 系统中的所有进程最终会形成一种树状层级结构(即 “进程树”):
systemd),它是所有进程的 “祖先”
-bash之所以是父进程,原因如下: 1. 终端与 shell 的关系
ls命令,bash 就会解析这个命令,并调用系统函数来列出当前目录下的文件和目录
2. 进程创建逻辑 当你在终端中运行一个可执行程序(图中的code.exe )时:
exec() 族函数来执行你要运行的程序简单来说:bash 作为你与系统交互的桥梁,在你运行程序时,它就充当了父进程的角色,负责创建并管理子进程的启动。
fork():用于创建新进程。
基本特性
代码、数据、堆栈等)想象一个
细胞(父进程)正在进行有丝分裂:
它们共享着相同的“基因”(代码),但从此以后是独立的生命体,可以各自对外界刺激做出不同的反应(执行不同的代码路径)
在这个比喻中:

通过上面的示例,我们能明白这样一个道理:
fork() 具有复制特性 —> 父进程和子进程会共享相同的代码
printf 语句的


关于系统调用fork的灵魂三问?
fork 会给父进程和子进程返回各自不同的返回值?fork会返回两次?if 和 else 分支看起来像是同时成立了?刚了解完系统调用fork,现在感觉头好大啊!第三个问题今天先放一放,之后会细说。
fork()给父进程和子进程返回不同值的设计,是 Linux 内核为了让父子进程能够明确区分自身身份并执行不同逻辑而专门设计的机制
fork() 的作用是创建子进程,而创建后父子进程会从同一代码位置继续执行。内核需要一种方式让两个进程知道 “自己是谁”—— 是原来的父进程,还是新创建的子进程。
因此,内核设计为: wait() 回收资源)fork() 会复制父进程的地址空间(包括:变量、代码等),子进程会获得与父进程完全相同的内存副本。
当 fork() 执行完成后: 总结:这种设计使得父子进程能通过简单的条件判断(if (id > 0) 或 if (id == 0))执行不同逻辑,是多进程编程中区分父子进程的关键机制。
fork函数之所以会 “返回两次”,是由它创建子进程的机制决定的,核心逻辑在于fork会复制出一个与父进程几乎完全相同的子进程,之后父子进程会各自独立执行

进程的独立性,说起来也好理解,举个例子就很清楚:
这就是进程独立性的直观体现,哪怕是父子进程,就算父进程意外退出,子进程也能继续运行,不会受到直接影响。
从深层原理来看,这种独立性源于两个关键机制:
task_struct结构体)管理每个进程,每个进程都有自己专属的 PCB,记录着进程的 ID、状态、资源占用等信息,这种独立的 “身份档案” 让操作系统能单独管理每个进程,彼此不会混淆正是这些机制,确保了进程之间互不干扰,即使某个进程出现问题,也不会轻易影响到其他进程的正常运行。

通过这个示例,我们能更直观地理解
pid_t id = fork();中返回值id为何会是不同的值了,核心原因是:
fork() 调用成功后,会创建一个与父进程独立的子进程id 变量会在父进程和子进程中各自拥有独立的副本,操作系统会为两个副本赋值不同的结果