Linux 进程概念
Linux 进程概念
- [Linux 进程概念](#Linux 进程概念)
-
- 一、进程基本概念
- [二、task_struct 结构体](#二、task_struct 结构体)
-
- [1. 内容分类](#1. 内容分类)
- [2. 进程组织方式](#2. 进程组织方式)
- [三、进程 ID 与系统调用](#三、进程 ID 与系统调用)
-
- [1. 获取进程 ID](#1. 获取进程 ID)
- [2. 查找父进程](#2. 查找父进程)
- 四、创建子进程:fork()
-
- [1. fork 原理](#1. fork 原理)
- [2. fork 行为](#2. fork 行为)
- [3. 返回值规则](#3. 返回值规则)
- [4. 示例:父子进程执行不同任务](#4. 示例:父子进程执行不同任务)
- 五、写时拷贝(Copy-On-Write)
- 六、查看与管理进程
-
- [1. 查看所有进程](#1. 查看所有进程)
- [2. 查看指定进程](#2. 查看指定进程)
- [3. 只看表头](#3. 只看表头)
- [4. 组合使用(过滤 grep 自身)](#4. 组合使用(过滤 grep 自身))
- [5. 杀死进程](#5. 杀死进程)
- [七、proc 文件系统](#七、proc 文件系统)
-
- [1 .关键文件属性](#1 .关键文件属性)
- 八、进程控制
-
- [1. 进程运行](#1. 进程运行)
- [2. 理解进程数据内核结构](#2. 理解进程数据内核结构)
- [3. 进程状态](#3. 进程状态)
- [4. 进程优先级](#4. 进程优先级)
- [5. 进程切换](#5. 进程切换)
- [6. 进程调度](#6. 进程调度)

一、进程基本概念
同一时刻,有很多可执行程序被加载到内存中,操作系统需要对这些加载到内存中的程序进行管理。

- 操作系统会构建
struct结构体来描述进程
c
struct task_struct {
代码地址, 数据地址, id, 优先级, 状态...
task_struct *next;
};

- 进程 = 内核数据结构对象(PCB) + 自己的代码和数据
内核中用于管理进程的结构体称为 PCB(Process Control Block)
在 Linux 下,PCB 具体叫做 task_struct
大致过程:
程序文件加载到内存 → 操作系统创建对应的 PCB → 将 PCB 链入进程队列 → CPU 调度时通过 PCB 找到对应代码和数据执行。
二、task_struct 结构体
1. 内容分类
- 标识符:唯一标识本进程,用于区分其他进程
- 状态:任务状态、退出代码、退出信号等
- 优先级:进程被调度的优先顺序
- 程序计数器:下一条即将执行指令的地址
- 内存指针:指向进程代码和数据
- 上下文数据:进程切换时保存的寄存器数据
- I/O 状态信息:I/O 请求、分配的设备、使用的文件列表等
2. 进程组织方式
Linux 内核使用双链表 管理所有进程的 task_struct。

程序运行起来后,就成为一个进程,对应的文件称为可执行文件 。
演示:

三、进程 ID 与系统调用
1. 获取进程 ID
c
pid_t getpid(void); // 返回调用进程自己的 PID
pid_t getppid(void); // 返回父进程 PID
查看手册:
bash
man 2 getpid

示例代码:
c
while(1)
{
printf("我是一个进程, PID:%d, 父进程PID:%d\n", getpid(), getppid());
sleep(1);
}

多次运行会发现:
- 每次 PID 都不同
- 父进程 PID 基本不变(通常是 bash)
2. 查找父进程
bash
ps axj | head -1; ps axj | grep 1757 | grep -v grep

知识点:
操作系统会为每个用户分配一个 bash(命令行解释器),我们在终端运行的程序,父进程大多都是 bash。
四、创建子进程:fork()
1. fork 原理
手册:

执行 fork() 之前只有一个执行流;
执行 fork() 之后变成两个执行流(父进程 + 子进程)。


2. fork 行为
- 父进程拷贝自身结构创建子进程
- 子进程与父进程共享代码和数据
- 子进程从 fork 之后开始执行
3. 返回值规则
- 创建成功:
- 给父进程 返回子进程 PID
- 给子进程 返回 0
- 创建失败:返回 -1
4. 示例:父子进程执行不同任务
c
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("当前进程pid:%d\n", getpid());
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
// 子进程
while(1)
{
printf("我是子进程, pid: %d, 父pid: %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
// 父进程
while(1)
{
printf("我是父进程, pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
结果:

五、写时拷贝(Copy-On-Write)
子进程创建后与父进程共享代码和数据 。
当父子任意一方试图修改数据时,操作系统会将被修改的数据单独拷贝一份,让修改方操作副本,不影响另一方。
这种机制就是写时拷贝。
验证示例:
c
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int a = 100;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
while(1)
{
int old = a;
a += 10;
printf("子进程:%d -> %d\n", old, a);
sleep(1);
}
}
else
{
while(1)
{
printf("父进程:a = %d\n", a);
sleep(1);
}
}
return 0;
}
运行后可观察到:
- 子进程修改
a,父进程的a不变 - 说明数据发生了写时拷贝

原理图:

六、查看与管理进程
1. 查看所有进程
bash
ps axj
2. 查看指定进程
bash
ps axj | grep 进程名
3. 只看表头
bash
ps axj | head -1
4. 组合使用(过滤 grep 自身)
bash
ps axj | head -1 && ps axj | grep myprocess | grep -v grep
5. 杀死进程
- 前台进程:
Ctrl + C - 根据 PID 杀死:
bash
kill -9 进程PID
七、proc 文件系统
/proc 是内存级文件系统,用于查看进程信息。每个数字目录对应一个 PID。
1 .关键文件属性

- exe:指向进程对应的可执行文件(绝对路径)
- cwd:current working directory,进程当前工作目录
CWD
示例 :我们在进行打开文件的时候可以进行用不带路径的文件名以写的操作进行,这时候文件就会创建到当前路径下。

c
int main()
{
whlie(1)
{
fopen("hello.txt,"a");//打开文件以追加的方式
printf("我是一个进程,我的pid是:%d\n",getpid());
sleep(1);
}
return 0;
}
PWD 指令
bash
pwd

- 修改进程的CWD
查看手册:
系统调用函数 chdir()

作用:更改一个进程的工作路径
比如我把当前进程工作路径改到/home/fj/桌面 。
这时候fopen创建文件的时候,默认就会在/home/fj/桌面路径下创建hello.txt

对于exe执行
如果这个exe文件给删除掉,那么我们现在运行的可执行程序还可以运行吗?
是可以被执行的,因为你的可执行程序被拷贝到内存了。
八、进程控制

1. 进程运行
一个CPU,一个调度队列。
这个队列不仅仅是CPU的调度队列,然后还是操作系统管理PCB的全局队列。
FIFO 调度算法
先进先出头部优先级比较高尾部优先级比较低

知识背景:现在我们就理解CPU在调度的时候,是依次获取这个队列的PCB,然后获取内存指针执行指向的代码进行执行
-
运行状态
只要你在这个CPU调度队列里面,那么进程的状态都是running -
阻塞状态
等待某种设备或者资源就绪 键盘,显示器,磁盘,摄像头,话筒....
操作系统要进行对各种硬件资源进行管理,就要进行先描述,在组织!!

所以操作系统内出来有运行队列,还要有设备队列

那么这俩个队列跟阻塞有什么关系呢?
在内核当中,每一个struct devices 结构体都要对应其中的设备,对于设备当我们在进行采取设备操作的时候,如:读磁盘,读网卡的时候,当设备没有准备好,或者正在等待资源,那么就会进行阻塞。
c
//描述设备的结构体
Struct device
{
int id;
Int vender;
Int status;
Void * data;
Int type;
Struct task_struct * wait_queue;
Struct device *next;
}
关系:
这个描述设备的结构体有一个等待队列,如果你的CPU在进行PCB对应的代码和数据,如果要进行访问设备,那么CPU就要去检查键盘的就绪状态或者资源是否满足,如果发现键盘并没有任何按键被按下,那么这个描述键盘的结构体就会标志这个键盘不是活跃的,那么操作系统就会发现,你这个进程无法获取数据,然后判断你这个进程无法被执行,所以操作系统,会把你这个进程从CPU的调度队列拿下来,然后放入到相对应硬件设备的结构体的等待队列当中。那么一旦把这个PCB链入到对应的没有准备好的设备中的等待队列中,那么CPU就不会调度这个队列了,因为不在运行队列了,所以这个进程永远不会被调度,那么这个进程就处于阻塞状态。
键盘按下、设备就绪后,进程本身无法感知,依靠操作系统完成调度切换:
- 键盘按下触发硬件中断,操作系统第一时间捕获设备就绪信号;
- 操作系统将设备标记为活跃,检查其等待队列,将队首 PCB 的状态修改,移入系统调度就绪队列;
- 进程此时仍不知设备就绪,等待 CPU 调度;
- CPU 调度到该 PCB 时,进程访问设备结构体,确认资源就绪后读取数据,继续执行后续代码。
所以从阻塞到运行状态,本质就是把PCB链入到运行队列中,就是运行状态了
经过上述描述我们得出:
状态的转换,就是说明PCB从一个链表链入到另一个链表中。
也就是在不同队列中进行流动,本质就是数据结构的增删查改
- 阻塞挂起
在磁盘中有一个分区叫做交换分区 swap

作用:当我们内存资源已经严重不足 了。那么操作系统要进行把阻塞队列中的PCB关联的代码和数据交换 到磁盘的swap分区 上。只保留PCB ,把数据和代码换出到磁盘上 ,这样就是获得内存的方式,当你PCB要进行运行的时候就会从磁盘的交换分区中进行获取代码和数据。
我们通常把这种情况称为:阻塞挂起
2. 理解进程数据内核结构
我们自己实现的双链表struct 描述:

linux实际上实现的双链表的格式:

- 把前指针和后指针封装成一个结构体

所以现在我们不需要,一个节点指向整一个链表的头部,而是通过这个链表中定义的结构体指向下一个链表(task_struct)中内部定义的结构体(list_head)。
问题:如果这个结构体不指向链表的头部,那么是拿不全链表中的属性的。所以我要访问链表中的所有属性该怎么做呢?
我们可以这样做:
c
(struct task_struct*)0 // 这个是0号地址存储了个结构体
&(struct task_struct*)0->links) //这样就会得到0到links的偏移量
之后:用指向这个link的结构体的地址-偏移量,此时这个指向的就是头部了。那么我们把它转成(struct task_struct*)就可以进行访问头部了!!
那么在进程中可不可以有多个list_head结构体呢?

一个进程结构体只有唯一一套属性信息,但内部定义了多个链表节点。
- 进程中可以链入到多个数据结构中,如链表,二叉树...
- 用一个节点链入就绪队列
- 用另一个节点链入设备等待队列
- 用其他节点链入定时器队列、亲缘关系树等
- 进程可同时归属多个数据结构,而不需要复制多份 PCB。
所以回到之前讲的进程运行和等待中:
这个PCB即在运行队列中,又在全局的双链表里面,
把这个PCB从运行队列断链,链入到阻塞队列中这个进程还要在全局队列中找到这个PCB。

3. 进程状态

- R:running 状态
我们执行一个while(1)的循环一直打印信息,然后再查看进程的状态

但是发现一个问题,为什么进程运行时候的状态是S呢 ??
因为:这个进程执行逻辑的时候的时间是很少的,然后因为printf打印数据的影响所以才会导致慢,所以这里可以认为这个进程不断的是在运行队列和阻塞队列进行切换。
总的来说:你执行的时候有%99的时候是在等待 ,然后%1的时间进行执行逻辑
如果我们把I\O输入输出的操作屏蔽掉之后
就可以得到R状态了

那么这个R后面的+是什么呢??
答案:前台进程 通常后面有 一个+号!!
如果你这个进程是后台进程 那么就没有这个+号!!
解决方法:进程变到后台运行那么你就要在执行程序后面 添加一个& ,让这个执行程序放到后台。
- s 阻塞状态/休眠状态
阻塞状态: 不可中断休眠
休眠状态:可中断休眠
bash
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int x;
scanf("%d",&x); //这时候就是阻塞,如果什么都没输入的情况下
}
运行之后:正在等待
所以这个阻塞状态我们称为S。
- T/t:暂停
我们用gdb进行对代码调试打断点就出现t 的状态!!
打完断点之后,我们进行运行,在断点处停下来了,然后这时候的状态就是t了。
所以t叫做为追踪状态,被debug了,断点:进程被暂停了。

如果用快捷键:ctrl + Z 进行暂停

- D深休眠状态
当我们操作系统内存资源严重不足的情况下,操作系统会有可能进行对一些进程杀掉。
举个例子:

当前有一个进程,这个进程的任务是要对磁盘进行写入100M的数据,然后进程不能离开,因为磁盘如果写入数据失败了,那么进程要第一时间获取然后反应给操作系统,然后让操作系统判断。所以进程就把自己的状态挂为S 等待状态,但是如果内存中资源严重不足,那么操作系统有可能会把这个S等待状态的进程给杀掉。那么之后磁盘写入失败反馈给进程的时候就找不到对应的进程了,这样就会导致磁盘就会丢失掉了这100MB的数据
所以你在执行程序的时候,手机会闪退是因为,操作系统压力太大了,然后把你进程给杀掉了!!!
D状态的作用 :那么这时候D状态就可以保证进程进行高IO的时候,就可以进行保证数据不丢失了。
- Z状态 (僵尸状态)
通常是:父进程创建子进程之后,让子进程退出。

结果:

Defunct 代表失效 ,Z代表僵尸
结果:
如果父进程一直不管,不回收,不获取进程的退出信息,那么Z会一直存在,这会导致内存泄漏问题!!服务器如果有内存泄漏,那么会出现很大的麻烦
slab 技术: 我们把死亡进程放入到一个维护的链表里面 叫做unuse,那么在下一次要进行创建进城的时候,就可以在这个链表进行获取task_struct。
- 孤儿进程
对于僵尸进程是父进程不进行回收子进程而导致,内存泄漏,但是如果父进程挂了呢?

我们可以看到,这里父进程的pid是22702 ,然后子进程的pid是22703 ,然后bash进程pid是21931
当父进程退出后,子进程的父进程变为1。
那么我们称1号进程领养的这个进程叫做孤儿进程
只要是被领养了,那么这个进程就会变成后台进程
-
问题1 :什么是1号进程
1号进程 :可以看成操作系统这个1号进程可以理解为,在你打开程序之前会帮你构建环境和初始化的操作等一系列操作

-
问题2 为什么要被领养
如果不领养,那么就没有人回进行管理了,那么就会导致内存泄漏!!!
4. 进程优先级
-
进程优先级是什么??
是进程资源的先后顺序
-
为什么
本质:目标资源稀缺,导致要通过优先级谁先谁后的问题!多个进程去竞争一块CPU,那么肯定会有优先级

- 怎么办?
优先级也是一种数字,int, task_struct
值越低,表明优先级越高,反之,优先级越低/
当前操作系统是基于事件片的分时操作系统
如:打饭的时候,规定30秒时间打完就走。这个操作系统必须考虑公平性。优先级可能变化,但是变化幅度不能太大
查看进程详细信息:
bash
ps -al

- 进程姓名
每一个进程都会有属于自己的姓名,这个姓名是用UID来进行表示。
可以通过指令 ls -ln 来进行查找
bash
ls -ln
小知识: 系统怎么知道我访问文件的时候,是拥有者,所属组,还是other?
因为访问文件的时候,是进程访问文件,然后进程就是代表用户,然后UID就是代表你,之后在进行判断这个文件你是拥有者还是所属组还是other...之后在进行操作。
那么这个进程的属性PRI NI 这俩个属性就是进程的优先级
-
PRI:进程优先级, 默认80
-
NI:进程优先级修正数据 nice值
-
真实的优先级=PRI+NI
如何修改优先级
直接用top指令 进入动态更新的进程信息界面
进入top界面之后输入r 然后输入进程PID 在输入对应调整的值就可以进行修改优先级了
bash
top

2154 进程修改之后:

用指令调整优先级:
bash
nice
nice -n 优先级数值 要运行的命令
renice
# renice 命令格式:renice 新NI值 -p 进程PID
renice 10 -p 2154
优先级的极值问题
-
最大值
'
renice -n 100 -p 2154
很显然这个nice值最大值是19
-
最小值

最小值为-20
Nice[-20,19]
默认是:80
所以Linux的进程优先级是
[60.99] 40个优先级。
如果调整优先级幅度大的话,那么就会导致优先级低的进程长时间得不到CPU资源,就会导致:进程饥饿
补充:
并发:在多个进程一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进。 来回切换,只要切换速度足够快,那么用户就感觉不到。

5. 进程切换
1:死循环进程如何运行的?
一旦一个进程占有CPU,会把自己的代码逻辑跑完吗?
不会!!!
-
时间片
每一个进程都会有自己的时间片,等事件片跑完之后,操作系统就会把这个进程从CPU剥离出来,让下一个PCB执行.
-
死循环进程,不会让系统卡,不会一直占有CPU!!
2: 聊聊CPU,寄存器

再进行访问PCB的代码和数据的时候,CPU中会有寄存器,比如:PC/EIP ebp/esp cs/ds/es...。进程在运行的时候,寄存器会进行保存你CPU中的代码数据的。
总之,我们可以理解为寄存器保存当前PCB进程的临时数据。CPU有一个pc 指针,会指向下一条指令。
基于以上的描述,我们可以得到几个结论
- 寄存器就是CPU内部的临时空间
- 寄存器--空间!=寄存器里面的数据---内容(这个内容就是可以进行切换的)
如何切换?
举例:
当兵保留学籍!!!回来之后,恢复学籍 ,保留学籍就是为了恢复,恢复之后就是执行之前还没执行的操作。
一次切换
- 学籍 --进程运行的临时数据 ,CPU内寄存器里面的内容(当前进程的上下文数据)
- 保留学籍 --保存进程的上下文
- 去当兵 --进程被从CPU玻璃下来
- 恢复学籍 --恢复进程上下文数据 ,保存起来,恢复到CPU内寄存器里
具体:

CPU要做切换的时候,进程A要保存上下文数据,也就是寄存器的内容,然后把进程A切走。之后在进行换到进程B,进程B把自己代码和数据放到寄存器中,覆盖上一次进程A的值,之后在执行。
总结:进程切换,最核心的,就是保存和恢复当前进程的硬件上下文的数据,就是CPU内的寄存器的内容!!
问题1 :保存在哪里呢??
保存到进程的task_struct 里面!!!----------->TSS字段:任务状态段(结构体) 通过这个任务段可以找到上下文数据。
在linux老内核中

tss这个字段的定义

这里定义的就是寄存器,如:es ,ss,ds 段地址...
6. 进程调度

属性:
Queue[140]
struct task_struct *queue[140] 指针数组
实时 VS 分时
-
实时
保证任务在严格的时间 deadline(截止时间)内完成,追求时间的确定性与可靠性,而非公平性或吞吐量。 -
分时
让多个用户 / 进程「公平、高效」地共享 CPU 资源,追求响应时间的公平性与系统吞吐量,而非绝对的时间确定性。

在这里不考虑这100个实时优先级
那么剩下40个就是分时优先级了,那么就和之前对应起来了
X-60+(140-40)。
查找优先级
操作系统依靠优先级进行从上往下遍历,那么就可以依靠优先级调度。 依次查找,如果不为空那么就取出当前数组下标的内容交给CPU进行运行。 宏观上 :我们先看优先级 !!!局部上:我采用队列的先进先出 ,因为会有优先级相同的进程进入运行调度队列中 ,那么后进入的就会构成一个队列然后进行先进先出。这就是FIFO调度算法!!
这个struct task_struct *queue[140] 指针指向的数据结构本质是一个哈希表,通过优先级来进行映射对应的位置!!!
但是这里又会有一个问题
如果我们所有进程的优先级都是最低的,那么在进行查找的时候就要进行一个一个的找,然后因为优先级最低,那么就是遍历完了这个数据结构。那么时间算法O(n)效率不高。
bitmap位图 查找

为什么是[5]?
Unsigned int bitmap[5] unsigned int 32个bit位 325=160
为什么不是4个? 32 4=128
为什么不是6个? 32*6-192
比特位的位置表示:queue[140], 哪一个slot
0000 0000
内容
位图:1 和 0 分别代表了存在 不存在
0001 0001 代表0号下标有对应的进程 ,和4号下标有对应的进程
所以调度器快速条选进程: 时间复杂度O(1) Linux内核大O(1)调度算法!!!
挑队列+挑进程
挑队列:查看bitmap ,查看对应的下标是否为1
挑进程:根据优先级进行挑选。
属性nr_active:
nr_active:代表整一个队列中有多少个进程
问题1:
如果我们在一个时间片中运行完一个优先级比较高的进程,那么这个进程在进入这个优先级队列中,那么应该怎么放,如果还是放到之前的优先级中的队列,那么这样的话,会导致后面的进程饥饿。
解决:

在linux老内核中:
有俩个队列,然后这个队列的配套数据与上面的队列的数据一摸一样。
c
//我们定义了一个rqueue,然后操作系统,通过struct rqueue_elem prio_array[2]定义了结构体数组
Struct rqueue_elem{
int nr_active;
Bitmap[5];
Queue[140];
}
分别对应的0号下标 的数组和对应了1号下标 的数组

在我们的rqueue内部定义了俩个指针
-
struct rqueue_elem *active=&prio_array[0] 指向了数组下标为0的开头
-
Struct rqueue_elem*expired=&prio_array[1] 指向了数组下标为1的开头

所以当我们调度一个进程的时候,把这个进程的时间片完之后,把这个进程从CPU中剥离下来 ,然后放到expired指向的队列中。
进程active_queue 越来越少
进程expired_queue 越来越多
如果active_queue 全部调度完了,那么系统还会有一个swap(&active,&expired); 进行指针交换,然后重复调度,所以我们就完成了linux内核当中的大O(1)调度算法!!!
如果新进程来了,那么是来expired队列还是,active队列
- 放在expired队列,那么这个进程就代表就绪状态,CPU不会调度
- 放在active队列,分时操作内核抢占,根据进程的优先级进行内核抢占资源
内核优先级抢占:
这里就体现了PRI默认优先级 和NI修正优先级的作用
因为这里我们CPU进行调度的时候,如果你直接修改PRI 如果改高了,那么你就要对这个PCB进程进行重新调整,链入到优先级相应的位置,但是这样的话会出现抢占资源的现象,产生进程饥饿,但是你要把它链入到过期队列里面,那么现在的进程还没执行就要放入到过期队列中,这样的话这个PCB比别的PCB少跑了一轮时间片 ,所以这样都不好,那么就是等这个PCB进程跑完之后,要链入到过期队列中进行等待的时候,进行用NI来进行修改优先级,然后在链入到合适的位置。
