大家好,这里是彩妙呀~

在上一篇博客,我们简单给我们的操作系统开了个小头并教了一点知识,接下来,彩妙将带着大家一起进入操作系统中的第一大关:进程概述以及其进程控制。
目录
[函数 getppid](#函数 getppid)
进程概念与其基本操作
对于进程的概念有很多版本,大多数教材或者书籍中,对于概念的定义是这样的:程序的一个执行实例,正在执行的程序等等。
但是,对于小白来说,这样子去讲进程会有点晦涩难懂。所以,我会聚例子,尽量来让你去了解什么是进程。
人们经常使用手机,而手机上就有许多的程序(应用)例如QQ、微信等等,所以你有没有想过,当你在玩手机的时候,后台(例如QQ、微信发送给你的信息)是怎么去管理这么多应用的?或者说,手机是怎么同时管理这么多的应用的?
我们先从单个进程来说
在这篇博客中,我们详细说明了冯诺依曼体系结构,了解过的小伙伴都知道,CPU只会在内存中工作。

如上图所示,当你去执行一个可执行程序时(.exe / cmd ==> 这类程序通常是静态的二进制文件,本质上是一堆指令以及其数据的集合,需要操作系统去激活他)磁盘会先把这个程序的代码和数据(后面会详细讲解代码与数据内存分布情况)提前拷贝到内存中(注意:凡是有数据流传输的,通常都是拷贝!)随后,内存才能开始执行这个程序。
其实操作系统也是可执行程序,它在系统启动时由引导加载程序从磁盘加载到内存中执行。
了解windows的小伙伴都知道,我们计算机有一些软件,其中的设置就有"开机自启动",而操作系统就是最先启动的那一批~
在操作系统中(后面简称OS ==> Operating System),在一般情况下,OS是要对多个被加载到内存中的程序进行管理(日常使用OS的时候,我们可以通过打开任务管理器,来查看当前正在进行的程序,会发现不止一个进程正在运行)。那么,OS又是怎么去管理这些进程的呢?
先描述,再组织!
什么是先描述,再组织呢?
由于不管是OS,c++还是其他的一些计算机相关的知识,都无法避免他们的 **"抽象化"。**而我们去学习的时候,为了更轻松理解这些知识,就提出这个概念。
举个例子:
当我们去求职时, 制作一份有关于你详细信息的简历
- 先描述 : 包括姓名,年龄的基本信息,对专业技能的详细信息等等进行一个汇总
- 再组织 :根据描述,制定精准策略------定制简历突出相关项目
- 这样投递公司后,公司就会对你有个基本认识,进而来安排你进行面试。
而在我们操作系统也一样,我们要去创建一个进程的时候,要先确定这个进程的属性(就像在windows中右键文件查看属性,里面会保存文件大小,创建时间等内容。)而后在让这个进程跑起来。而为了去描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块 ( Process Control Block)的创建(在后续博客中,对于其中详细内容不会仔细描述,但当遇到关键的地方,我会详细说明)。
那么对于PCB呢,在linux内核当中,通常就是以 struck task_struck来命名这个数据结构的。
PS:在OS中,进程控制块统称为PCB,而在Linux中才会以 struck task_struck来命名的。
cpp
进程所有的属性都可以直接或者间接通过PCB来查找的。
task_struct 是 Linux 内核的⼀种数据结构类型,它会被装载到RAM(内存)⾥并且包含着进程的信息。
struct task_struct
{
代码地址
数据地址
PID(进程标识符,是进程的唯一标识符,当内核创建进程的时候,OS自动回分配一个PID,这个值有且仅有一个)
优先级
.........
struct task_struck * next(用来连接下一个PCB的指针)
}
显而易见,每一个PCB都是一个链表节点,所以当我们去管理进程时,本质上是对程序链表的增删查改(这就回到了数据结构中的知识)而所谓的进程,就可以理解成:内核数据对象 + 自己代码和数据 = 进程
综上,我们就可以使用先描述,再组织来说明进程:先使用PCB来描述每一个进程的详细信息,在使用结构体(task_struct)来把这些PCB组织成链表,这样,我们对程序的控制与管理就会转变成对数据结构的增删查改。
进程加载到内存后,其核心是进程控制块(PCB),它如同求职者的简历------详细记录了进程的状态、资源需求和调度信息。CPU在调度时会优先检查PCB内容:若进程的资源需求(如内存、I/O)或状态(如是否就绪)不满足运行条件,系统会将其阻塞或挂起,类似HR因技能不符而淘汰简历。HR批量筛选简历后精准匹配岗位,CPU也基于调度算法(如优先级队列)从就绪队列中高效选择进程,从而确保系统资源合理分配。
在我们粗浅的认识:进程 = PCB + 代码和数据 后,彩妙将会一代码的方式带着大家来了解进程
进程实操
环境:采用vscode远程连接xshell云服务器(centos 系统)
先写一个简单且一直运行的程序:myprocess.c
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("I'm a process and I'm running\n");
sleep(1);
}
return 0;
}
使用gcc编译后,得到一个可执行文件myprocess,当我们运行这个文件,我们就得到了一个进程:

所以,我们过去所以执行的所有指令(例如ls 、cd等命令)、工具(gcc等)、还是自己写的程序(如刚刚所示),只要运行起来,都是进程!(我们所有跑/正在运行的程序,都是进程)。
回顾刚才我们介绍进程的内容:进程 = PCB + 自己的代码与数据。显而易见:PCB是OS给予程序,而代码与数据则是我们自己编辑好的。
那么,这么多进程,又怎么知道我们的进程是谁呢?
系统调用:getpid

如图:OS给予我们一个查看进程ID的方式:getpid()getppid(),当我们去使用getpid()这个函数时,OS会将PCB中PID这个字段拷贝到当前进程中,从而获取我们进程的ID号(后者等会再讲):
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("I'm a process and I'm running\n , my pid is %d\n", getpid());
sleep(1);
}
return 0;
}

这里的pid_t是OS所提供的类型,但本质上就是一个整形int。
只要我们可以获得PID,那么就可以说这个东西是一个进程。
接下来,我们该如何去查看当前OS中,我们进程有哪些呢?
linux命令:ps
使用man来查看ps命令:

名称
ps - 报告当前进程的快照。
概要
ps [选项]
描述
ps 显示当前活动进程的选择性信息。如果您希望重复更新所选进程及其显示信息,请使用 top 命令。
当我们要使用这个命令带来查看所有进程情况时,使用下面命令:
ps axj
效果如下:

但是,我们发现,直接使用这个命令来查看我们的进程,有点费眼睛。所以,我们可以使用一个管道来查询我们的进程:
ps axj | head -1 ; ps axj | grep myprocess 或者 ps axj | head -1 && ps axj | grep myprocess

我们又会发现:我们找到了我们的进程,但是每一个字段都是什么意思呢?
字段 说明 补充信息 PPID 父进程ID(Parent Process ID) 创建该进程的父进程ID,用于构建进程父子关系 PID 进程ID(Process ID) 进程的唯一标识符,系统中每个进程有唯一PID PGID 进程组ID(Process Group ID) 进程所属的进程组,用于进程组控制和信号传递 SID 会话ID(Session ID) 进程所属的会话,通常与终端会话关联 TTY 终端(Teletypewriter) 关联的终端设备,无终端时显示"?"(如守护进程) TPGID 终端进程组ID (Terminal Process Group ID) 与终端关联的进程组ID,控制终端读写权限 STAT 进程状态(Status) 状态码: R:运行中(可运行/队列等待) S:休眠中(等待条件/信号) D:不可中断睡眠(通常为IO) T:已停止(收到停止信号) Z:僵尸进程(终止未释放) UID 用户ID(User ID) 进程所属的用户ID,标识运行用户身份 TIME 运行时间(Time) 已使用CPU时间(时:分:秒),含用户态与内核态 COMMAND 命令(Command) 进程执行的可执行文件名或命令
当我们想要终止我们的进程,可以使用Ctrl + C 或者使用 kill -9 PID来结束或者杀死我们所运行的进程。
还有一种方式来查看进程:查看linux中内存级别的文件系统****/proc目录:

这里我重新运行了程序,所给出的PID为:1464079,而对应上述目录,可以看到一个目录文件夹:1464079。
我们打开这个文件夹:

我们发现:在文件中,会把进程属性详细呈现出来。由于文件数量过多,彩妙挑几个常见文件来讲解。
exe链接文件 :这个文件是一个链接文件,指向的是这个进程所对应的绝对路径与****文件名。
小知识:
当你在进程运行时删除exe链接文件所链接的文件时,进程可能不会崩溃。这是因为在你运行程序时,OS已经把程序拷贝到内存中了。我们删除掉的只不过是存于硬盘之中的文件,所以在大多数情况中,删除磁盘文件没有什么影响(但是大多数OS中是禁止这样做的)
而上面cwd链接文件(),则是存储了文件的当前路径。
当你使用**fopen()**之类的函数去使用进程来创建文件时(不带路径创建),则就是以该路径为起点创建文件。
进程会将当前路径记录至cwd文件下。我们也可以通过**chdir()**这个函数来切换cwd所记录的文件:chdir("你所要放文件的路径")
这里大家可以自行尝试修改一下,也可以自行联想一下无能所运行那些命令是怎么运行的(cd怎么改路径,touch怎么在该目录下创建文件等)。
函数 getppid
上述讲过getpid()函数,而getppid()与其几乎相同,区别则在于:前者是获得自己的PID,而后者是获得自己父亲的PID。(ppid:parent process ID)
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1)
{
printf("I'm a process and I'm running , my pid is %d ,my parent pid is %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}

在这里,如果我们不断去重新运行我们的程序,发现:pid会一直改变,而ppid不会改变。
那么,这个ppid对应的是谁呢?
ps axj | head -1 ; ps axj | grep 1456312
bash
root@hcss-ecs-d397:/proc/1464079# clear
root@hcss-ecs-d397:/proc/1464079# ps axj | head -1 ; ps axj | grep 1456312
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1455779 1456312 1456312 1456312 pts/6 1469570 Ss 998 0:00 /bin/bash --init-file /home/user/.vscode-server/cli/servers/Stable-fee1edb8d6d72a0ddff41e5f71a671c23ed924b9/server/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh
1456312 1456484 1456484 1456312 pts/6 1469570 T 998 0:00 ./myprocess -
1456312 1464079 1464079 1456312 pts/6 1469570 T 998 0:00 ./myprocess
1456312 1469570 1469570 1456312 pts/6 1469570 S+ 998 0:00 ./myprocess
1457418 1470471 1470470 1457418 pts/7 1470470 R+ 0 0:00 grep --color=auto 1456312
我们发现:我们所运行的程序,他的父进程是-bash(命令行解释器)
所以,我们发现:命令行解释器,自己本身也是一个进程!
知识点:
在每次登录云服务器时,OS会为用户启动一个交互式bash shell进程,作为用户与系统交互的界面。-bash提示符表示用户已成功登录并处于bash shell环境中,可以执行命令、运行脚本和管理服务器资源。这是Linux系统标准的用户登录体验,为用户提供了命令行操作的基础环境。
那么,我们程序能不能也有一个子进程呢?
系统调用:fork()

在使用代码来给大家演示之前,我们先谈谈子进程是怎么被创建的?
简单来说,子进程的创建就是父进程找操作系统 "帮忙造一个新进程" 的过程,核心就 4 步:
- 父进程 "提申请":告诉操作系统 "我要创建一个子进程",调用系统里专门的创建指令(比如 Linux 用 fork、Windows 用 CreateProcess)。
- 系统 "做检查 + 分身份":操作系统先看内存、CPU 这些资源够不够,够的话就给子进程分配一个唯一的 "身份证"(PID),并记好它的父进程是谁。
- 资源 "备齐":给子进程准备运行需要的资源 ------Linux 下会先和父进程共享内存(与父进程共享数据与代码),Windows 下则直接给子进程装要运行的程序,不共享父进程的资源。
- 子进程 "开工":操作系统把子进程加入 "待运行队列",等 CPU 有空了,子进程就开始执行代码。
补充一个小区别:Linux 里子进程默认接着父进程的代码跑,想换程序得额外操作;Windows 里创建时就指定要跑的程序,子进程直接启动这个程序就行。
了解了这些知识后,我们再看看fork()返回值是什么?
返回值说明 :调用成功时:父进程会拿到子进程的 PID(进程 ID) ,而子进程自己会拿到
0;调用失败时:父进程会拿到-1,此时不会创建任何子进程,操作系统还会把errno(错误码)设置好,用来告诉我们失败的原因。
这样,我们就可以利用返回值不同,来设计两个进程的代码啦!
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork(); // 调用fork,复制进程
if (pid > 0) {
// 父进程:pid是子进程的ID
printf("我是父进程,PID=%d,子进程PID=%d\n", getpid(), pid);
} else if (pid == 0) {
// 子进程:pid返回0
printf("我是子进程,PID=%d,父进程PID=%d\n", getpid(), getppid());
} else {
// 失败:pid=-1
perror("fork失败"); // perror会打印errno对应的错误描述
return 1;
}
return 0;
}
bash
user@hcss-ecs-d397:~/my_home/vscode-code/my_process$ ./myprocess
我是父进程,PID=1489670,子进程PID=1489671
我是子进程,PID=1489671,父进程PID=1489670
为什么fork()函数有两个不同的发返回值?
最主要的原因:fork()函数不是普通函数(不是执行代码块然后返回结果),而是父进程请求OS而夫指出的一个全新的子进程:
调用阶段 :只有父进程执行
fork()这行代码,向操作系统发起 "复制自身" 的请求;操作系统处理 :操作系统会复制父进程的核心资源(内存、文件描述符等,用写时复制优化避免浪费),生成一个和父进程几乎一模一样的子进程;
返回阶段 :复制完成后,父进程和子进程会同时从
fork()调用的下一行代码开始执行,操作系统会给这两个独立的进程各自分配一个返回值(方便区分谁为负进程,谁为子进程)。通俗理解:
就像你(父进程)去复印机(操作系统)那里说 "复制一个我":
复印机做完复制后,你(父进程)拿到了 "分身"(子进程)的编号(PID);
你的分身(子进程)拿到了一个 "0" 作为标记;
然后你和分身都从 "离开复印机" 这个动作(
fork()后的代码)开始做事,各自拿着自己的 "返回值",在外人看来,就像 "复制" 这个动作给了两个结果。
那么,为什么有父子进程这种关系呢?
简单说,父子进程的设计核心是为了让进程管理更有序、任务执行更高效、系统资源更安全,就像现实中 "团队分工 + 层级管理" 的逻辑 ------ 父进程是 "管理者",子进程是 "执行者"。
而其中必然会有通信过程:父进程数据传给子进程(本质上父子进程访问同一个数据段以及代码段,但两种通讯需要借助其他方式来进行,后续到进程通信会给大家详解),例如:父进程存有value :int i =100,传给子进程之后,子进程更改这个value,而父进程这个数据却不会有改变(体现了进程的独立性):
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork(); // 调用fork,复制进程
int i = 100;
while(1)
{
if (pid > 0) {
// 父进程:pid是子进程的ID
printf("我是父进程,PID=%d,子进程PID=%d , i=%d\n", getpid(), pid, i);
sleep(1); // 父进程休眠1秒,让子进程有机会执行
} else if (pid == 0) {
// 子进程:pid返回0
printf("我是子进程,PID=%d,父进程PID=%d , i=%d\n", getpid(), getppid(), i);
i++;
sleep(1); // 子进程休眠1秒,让父进程有机会执行
} else {
// 失败:pid=-1
perror("fork失败"); // perror会打印errno对应的错误描述
return 1;
}
}
return 0;
}
bash
我是父进程,PID=1495046,子进程PID=1495047 , i=100
我是子进程,PID=1495047,父进程PID=1495046 , i=100
我是父进程,PID=1495046,子进程PID=1495047 , i=100
我是子进程,PID=1495047,父进程PID=1495046 , i=101
我是父进程,PID=1495046,子进程PID=1495047 , i=100
我是子进程,PID=1495047,父进程PID=1495046 , i=102
我是子进程,PID=1495047,父进程PID=1495046 , i=103
我是父进程,PID=1495046,子进程PID=1495047 , i=100
我是子进程,PID=1495047,父进程PID=1495046 , i=104
本篇到这里就结束了,下一篇会带着大家来看进程的基本状态,喜欢文章的小伙伴可以关注一下彩妙,我们下一篇再见~


