🎬 胖咕噜的稞达鸭 :个人主页
🔥 个人专栏 : 《数据结构》《C++初阶高阶》
《Linux系统学习》
《算法入门》
⛺️技术的杠杆,撬动整个世界!

进程状态就是task_struct内的一个整数。
运行状态:进程在调度队列中,进程的状态都是running;
阻塞状态:等待某种设备或者资源就绪(硬件资源:键盘,显示器,网卡,磁盘,摄像头,话筒等)
1. 运行 && 阻塞 && 挂起
操作系统对软件硬件系统进行管理,对硬件资源进行管理,怎么管理的?(通过运行队列和设备队列)
运行队列:
软件打开的时候,会加载到内存,许多软件的代码和数据(这里我们通称进程)会按序形成一个队列,调度队列,运行队列,每一个task_struct都在一个运行队列中,处于运行状态。
struct XXX(所有操作系统的统称叫做PCB)
{
代码地址
数据地址
id
优先级
状态
...
struct xxx* next;//创建指针用于指向下一个数据
}
设备队列 :(struct device* devices)
在开机的时候。操作系统加载完成,就会对每一个设备构建一个struct device的结构体,如下:
struct device
{
int id;
int vender;
int status;
void* data;
struct device* next;//指针用来链接下一个设备
int type;
struct task_struct * wait_queue;//等待队列
//每一个设备都有对应的等待队列
}
然后每一个设备分别链接他们对应的硬件设备,这就构成了一个链表的结构,所以操作系统对设备硬件的管理转换为对链表的增删查改!
什么是阻塞?
假如说,我们打开一个软件,要使用键盘,操作系统就会检查键盘的状态,如果没有被摁下任何一个键,那么此时这个进程就处于阻塞状态;
操作系统对应这个软件(进程),由于键盘(硬件)没有被摁下任何一个键,所以此时,这个进程的状态就不活跃了,把这个软件的进程从CPU拿下来,并且从运行队列中移除,然后把PCB链入到特定设备的等待队列中,此时这个进程已经出了运行队列(调度队列),就不会被调度了,这个进程此时就处于阻塞状态。
如果我们此时又按下了键盘,此时进程是不知道键盘已经被摁下了,键盘摁下就属于硬件就绪了,操作系统作为硬件的管理者,要提前知道,此时操作系统就会查看对应的硬件设备的节点,struct device; int status;将状态设置为活跃,并且检测等待队列,struct task_struct * wait queue;发现指针不为空,就将该进程的状态设置为运行中,重新将该进程PCB链回到运行队列中。
最后被链回到运行队列的进程又会读取到该硬件设备的信息。
什么是挂起?
进程等于数据结构+自己的代码和数据;
如果内存资源严重不足,操作系统在磁盘中会有一个swap交换分区,现在内存中有几个进程处于阻塞状态,而且这几个进程对应的设备也没有就绪,已经不再运行了,不会被调度了,但是还在内存中占着空间,
此时操作系统会将这几个阻塞的进程置换到它们代码数据对应的磁盘swap分区上,只将进程的PCB存储下来,释放了内存空间。此时这几个进程的状态就叫做阻塞挂起。
如果再需要启用这几个进程,PCB会从磁盘中调用其代码和数据,重新换入内存形成完整进程,又会被重新调度和运行。
把该进程的代码和数据挂起挂到外设上,磁盘中的swap的分区上。
运行挂起:如果内存资源严重吃紧,操作系统也会将调度队列末端的几个PCB进程置换到磁盘的swap分区中,为内存节省空间。
怎么理解Linux中的数据结构是网状的?
一个PCB在内存中只有一份,但是可以属于多种数据结构。可以既是双链表中的节点,又是运行队列中,所以Linux中的很多数据结构是网状的。相当于List_head存储了task_struct在各个数据结构的位置信息。
Linux 通过list_head实现 "一个 PCB(task_struct)、多份链接信息",让进程同时挂靠多个功能数据结构,形成网状关联,既保证 PCB 唯一性,又满足多维度管理需求。
关键逻辑:list_head是 "多维度挂钩"
task_struct(PCB)本身只存一份核心信息(PID、状态等),不直接包含链表指针。- 其内部嵌入多个
list_head结构体(本质是双向链表的前后指针对),每个list_head对应一个数据结构。 - 比如一个
list_head用于挂入运行队列,另一个用于挂入设备等待队列,还有的用于进程组、调度类等链表。
网状结构的核心价值
- 避免数据冗余:无需为每个数据结构复制一份 PCB,仅通过
list_head关联,节省内存。 - 支持多维度管理:进程可同时被 CPU 调度(运行队列)、I/O 资源分配(设备队列)、进程关系维护(父子链表)等场景管理。
- 操作高效:通过
list_head提供的通用链表接口,能快速在任意关联结构中添加、删除、查找进程,无需修改 PCB 核心数据。
代码查看一下:
此时进程在等待键盘输入一个scanf值,如果键盘没有任何一个键被摁下,此时这个进程的PCB链接在设备队列对应的等待队列中,处于阻塞状态,不被调度了。
[keda@VM-0-4-centos lesson13]$ cat myprocess.cc
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("I am a process, pid: %d\n",getpid());
int x;
scanf("%d", &x);
// while(1)
// { printf("hello keda nice to meet you!\n");
// }
return 0;
}
[keda@VM-0-4-centos lesson13]$ gcc myprocess.cc -o myprocess
[keda@VM-0-4-centos lesson13]$ ./myprocess
I am a process, pid: 7215
此时进程在等待键盘输入一个scanf值,如果键盘没有任何一个键被摁下,此时这个进程的PCB链接在设备队列对应的等待队列中,处于阻塞状态:sleeping
2. Linux的进程状态
static const char *const task_state_array[] =
{
"R(running)",//运行状态
"S(sleeping)",//可中断休眠,浅睡眠;
"D(disk sleep)",//不可中断休眠,深度睡眠,
"T(stopped)",//用户键盘crtl+z,就是进程被暂停了
"t(tracing stop)",//被debug,断点:进程被暂停了,追踪
"X(dead)",//结束状态
"Z(zombie)",//僵尸进程,为了获取退出信息
};
进程状态就是task_struct内的一个整数。怎么理解:
当进程在运行的时候,struct task_struct中对应的数据是0 runnable;
大于0 表示进程停止; -1表示进程不再运行
被debug,断点:进程被暂停了,追踪
用户键盘crtl+z,就是进程被暂停了
"S(sleeping)",可中断休眠,浅睡眠;
"D(disk sleep)",不可中断休眠,深度睡眠,不可被杀掉,只能等D进程自己醒来,如果要杀掉这个进程,只能重启或者计算机断电。
如果需要这个进程写入100MB到磁盘,磁盘再写入100MB时,进程就会自动将自己的状态设置为S(sleeping),然后等到磁盘生成100MB数据了才会继续运行。如果此时操作系统OS在处理内存空间严重不足的情况下,处理了很多被挂起的进程,将挂起进程的代码和数据置换到磁盘中的swap分区,还是面临内存空间严重不足,就会将该状态为S的进程杀死以节省内存空间。
所以我们手机上的有些程序会闪退,也就是进程被操作系统杀死了。
磁盘在写好100MB,找不到该进程,就会丢弃到这个100MB的数据,此时会造成数据丢弃的问题。更危险的是:还没有提示会告诉操作系统,用户不知道。
所以要怎么解决:
以后涉及到进程要求磁盘写入数据的时候,进程自己的状态不能设置为S,要设置为D,进入深度睡眠,如果内存严重不足,操作系统要杀死这个D进程时,状态D不对任何操作系统的操作做出响应。这样当磁盘在写好数据之后向进程汇报,不至于找不到这个进程,也就不会造成数据的丢弃。
僵尸进程(zombie):
我们创建子进程的目的是为了让子进程完成某种事情的,结果相关的信息,父进程得知道。
子进程的信息存在哪里:task_struct,子进程在退出的时候,系统只保留了它的PCB进程控制块,丢弃了代码和数据,当我们想要知道它是因为什么原因退出的,就可以调用PCB进程控制块,task_struct查出来退出原因。
父进程通过wait()、waitpid()等系统调用,读取task_struct中保存的退出状态(如退出码、终止信号)。在此之前,这个进程就是处于僵尸进程中。
子进程退出后,内核会保留task_struct(PCB)的核心信息(含退出原因),释放代码、数据等资源,未被父进程调用wait()类函数读取状态前,它就是僵尸进程。
僵尸进程的本质是 "task_struct未被父进程回收",此时进程已无运行实体,仅残留 PCB 中的状态信息,等待父进程读取。
[keda@VM-0-4-centos lesson13]$ ./myprocess
我是父进程,我正在运行...
我是子进程,我正在运行:5
我是父进程,我正在运行...
我是子进程,我正在运行:4
我是父进程,我正在运行...
我是子进程,我正在运行:3
我是父进程,我正在运行...
我是子进程,我正在运行:2
我是父进程,我正在运行...
我是子进程,我正在运行:1
我是父进程,我正在运行...
我是父进程,我正在运行...
我是父进程,我正在运行...
我是父进程,我正在运行...
我是父进程,我正在运行...
^C
[keda@VM-0-4-centos lesson13]$ cat myprocess.cc
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int count = 5;
while(count)
{
printf("我是子进程,我正在运行:%d\n", count);
sleep(1);
count--;
}
}
else
{
while(1)
{
printf("我是父进程,我正在运行...\n");
sleep(1);
}
}
}
可以看到子进程结束运行,但是父进程还没有对子进程的PCBtask_struct进行回收,此时子进程处于僵尸状态,父进程一直不管不回收,不获取子进程的提出信息,Z就会一直存在!
这里就涉及到内存泄漏的问题。
所以除了new malloc会引起内存泄漏问题;僵尸进程也会引起内存泄漏问题。
进程退出了,内存泄漏问题还在不在?
进程一旦退出了,内存泄漏的问题就不存在了。我们使用的所有软件都是一旦启动就不会退出的,叫做常驻内存的进程。
关于内核结构申请:
如果子进程被回收了,它的task_struct被父进程读取到,就会free掉这个task_struct;后来新建一个子进程,就会新建一个task_struct。
孤儿进程
父子进程关系中,如果父进程先退出,子进程要被1号进程领养,这个被领养的进程(子进程),叫做孤儿进程。
我们将这个1号进程近似看作操作系统。
为什么要领养?
子进程进入僵尸状态后,就一定需要被领养,如果不领养,就会造成内存泄漏而无法解决的问题。新的父进程就会对这个子进程进行回收。
父进程的父进程就是bash,一旦父进程退出bash就会来回收。
孤儿进程被回收之后,就会成为后台进程,./cmd &
总结:
父进程提前退出,子进程后退出,进入Z之后,子进程就会成为孤儿进程,孤儿进程会被1号进程领养,就会被1号进程回收。
进程优先级切换调度
什么是优先级?
优先级的本质:是进程得到CPU资源的先后顺序。
为什么?
目标资源稀缺,导致要通过优先级确认谁先谁后的问题!
优先级:能得到这个资源,不过是先后的顺序;
权限:是否能得到这个资源。
怎么办?
优先级也是一种数字,int,task_struct;值越低,优先级越高,反之,优先级越低;基于时间片的分时操作系统。每一个进程都有自己的时间片,而且还要考虑一定的公平性,优先级可能变化,但是变化幅度不能太大。
[keda@VM-0-4-centos lesson14]$ cat myprocess.c
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("我是一个子进程,pid : %d,ppid: %d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//father
int cnt = 5;
while(cnt)
{
printf("我是一个父进程,pid: %d,ppid :%d\n",getpid(),getppid());
cnt--;
sleep(1);
}
}
return 0;
}
[keda@VM-0-4-centos lesson14]$ make
make: `myprocess' is up to date.
[keda@VM-0-4-centos lesson14]$ ./myprocess
我是一个父进程,pid: 30940,ppid :30599
我是一个子进程,pid : 30941,ppid: 30940
我是一个父进程,pid: 30940,ppid :30599
我是一个子进程,pid : 30941,ppid: 30940
我是一个父进程,pid: 30940,ppid :30599
我是一个子进程,pid : 30941,ppid: 30940
我是一个父进程,pid: 30940,ppid :30599
我是一个子进程,pid : 30941,ppid: 30940
我是一个子进程,pid : 30941,ppid: 30940
[keda@VM-0-4-centos lesson14]$ 我是一个子进程,pid : 30941,ppid: 1(此时父进程已经退出了)
我是一个子进程,pid : 30941,ppid: 1(这里的父进程是1)
我是一个子进程,pid : 30941,ppid: 1(就是说子进程被1号进程领养了)
我是一个子进程,pid : 30941,ppid: 1(这个子进程在被领养之前是孤儿进程)
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
我是一个子进程,pid : 30941,ppid: 1
问题:当我们想要访问一个文件的时候,系统是怎么知道我访问 文件的时候,是拥有者,所属组还是other?
我们访问一个(文件)进程,在操作系统中我们自己的拥有者,所属组和other在运行的时候会有一个UID(user id),在进程运行的时候这个UID会去匹配,符合拥有者就去调用拥有者的文件,所属组和other同理。
Linux访问任何资源,都是进程访问,进程就代表用户。
[keda@VM-0-4-centos lesson14]$ ps -al |head -1 && ps -al |grep myprocess
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1001 242660 21931 0 80 0 - 1054 hrtime pts/0 00:00:00 myprocess
PRI:进程的优先级,默认:80;
NI:进程优先级的修正数据,nice值,默认为0;
进程真实的优先级=PRI(默认)+NI
方法:(了解一下)
nice renice这样的命令。
Linux调整优先级的系统调用。
#include<sys/time.h>
#include<sys/resource.h>
int getpriority(int which,int who);
int setpriority(int which,int who,int prio);
nice的最小值是-20,最大是19。
PRI默认是80
进程真实的优先级=PRI(默认)+NI
Linux进程的优先级范围是【60,99】
虽然优先级我们可以手动调整,但是为了考虑公平性,进程的优先级手动可以调整的幅度有规定。
规定优先级用户可调整的幅度的目的:为了防止有些用户恶意将自己的优先级调整到最大,最先获取到资源,在一般的操作系统下,会导致优先级低的资源总是得不到CPU资源,进而导致:进程饥饿。
补充-竞争,独立,并行,并发
- 竞争性:系统的进程数目很多,但是CPU的资源只有少量,所以进程之间是具有竞争属性的,为了高效完成任务,更合理竞争相关资源,有了优先级;
- 独立性:多个进程运行,需要独享各种资源,多进程运行期间互不干扰;(任何一个进程异常不会影响其他进程)
进程=内核数据结构(task_struct)+代码和数据 - 并行:多个进程在多个CPU下分别,同时进行运行,这称为并行;
- 并发:多个进程在一个CPU采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发。
CPU通过高频切换,使得多个进程可以同时进程。
进程切换
1. 死循环进程是如何运行的?
一旦一个进程占有CPU,会把自己的代码跑完吗?
不会,操作系统规定一个进程运行的时候都有自己的时间片,时间片到了就不会再运行了。
死循环进程不会打死系统,不会一直占有CPU。
2.聊聊CPU,寄存器
当一个进程运行的时候,这个进程加载到内存,由CPU访问这个进程的代码和数据,一条一条访问代码和数据,这个时候就有寄存器这个概念。
CPU中存在很多个寄存器,寄存器就是CPU内部的临时空间;
寄存器不等于寄存器里面的数据。
3.如何进程切换
当我们有同学在大学二年级选择去当兵,在当兵之前就要在导员处申请保留学籍,然后当兵回来继续学业就要向导员申请恢复学籍。
此时学校就是CPU,导员可以近似于调度器,这个同学就是进程,学籍:进程运行的临时数据,CPU内部寄存器里面的内容(当前进程的上下文数据);
保留学籍:保存进程的上下文数据,CPU内寄存器里面的内容,保存起来;
恢复学籍:恢复进程的上下文数据,保存起来,恢复到CPU寄存器里面;
去当兵:进程被从CPU剥离下来。
进程切换,就是保存和恢复当前进程的硬件上下文的数据,即CPU内寄存器的内容。
问题:当前进程要把自己的进程硬件上下文数据,保存起来,保存到哪里了?
保存到进程的task_struct里面,每一个进程都有一个TSS(任务状态段)就是硬件上下文。
总结:如何切换CPU
CPU在运行进程的时候,这个进程的时间片到了,操作系统就会把这个进程切换下去,运行下一个进程,这个进程就会保存自己的上下文数据,等到切换到了这个进程,就会把它的上下文数据交给操作系统,继续运行进程。这个切换速度非常快。
CPU 运行进程时,当"时间片到期" "进程主动阻塞" "高优先级进程就绪"等情况发生时,操作系统的进程调度器会触发进程切换:
- 先保存当前进程的上下文数据(CPU 寄存器、PCB 关键信息等),让进程进入"就绪状态";
- 从就绪队列中选择下一个要运行的进程;
- 恢复该进程的上下文数据(加载到 CPU、恢复内存映射等),让其从之前暂停的位置继续运行;
- 整个切换过程在毫秒级 / 微秒级完成,用户完全感知不到,看起来就像多个进程在 "同时运行"。
Linux真实调度算法(O(1))调度算法
调度和切换共同构成了调度器。
一个CPU,一个运行队列,
调度器如何快速地挑选一个进程呢?
1.挑队列;2.挑进程。

总结:
我们用"找最高优先级进程→运行→移到过期数组→数组切换"的流程,简述O(1)调度器中 prio_array_t (含 nr_active 、 bitmap 、 queue )的工作过程:
- 找进程:
调度器看active数组的bitmap(位图),快速定位到"第一个非空的优先级队列"(比如优先级50的queue[50]),直接取该队列的头进程(O(1)时间)。 - 运行并更新计数:
进程被调度到CPU运行,active数组的nr_active(就绪进程总数)减1。 - 时间片用完:
进程时间片耗尽后,优先级降级,从active的queue旧优先级中移除,插入expired数组的queue新优先级]中;同时active的nr_active减1,expired的nr_active加1。 - 数组切换:
当active的nr_active变为0(无就绪进程),调度器直接交换active和expired的指针,让expired数组变为新的active数组,继续调度。
整个过程中, nr_active 负责快速判断数组是否为空, bitmap 负责快速定位非空队列, queue 负责按优先级存储进程------三者配合实现了"无论多少进程,都能O(1)时间完成调度"的核心目标。