专栏链接:《C++学习》、《Linux学习》
文章目录
- 前置知识
- 1.操作系统中的进程状态和Linux中的进程状态
- 👍2.偏移量+起始地址 == &目标地址
- 👍3.正式开始剖析!操作系统内核里面的数据结构
- 那么为什么操作系统要这么做? 为什么要把内部数据结构设计成这样?
- 进程状态
- 1.R运行状态
- 补充——前台进程后台进程
- 2.阻塞状态
- 2.1S可中断睡眠状态
- 2.2D不可中断睡眠状态
- 2.3T停止态
- 2.4t跟踪停止态
- 3.挂起状态
- 👍僵尸进程和孤儿进程
- 1.僵尸状态和僵尸进程
- 2.孤儿进程
前置知识
1.操作系统中的进程状态和Linux中的进程状态
操作系统中的进程状态
Linux中的进程状态
结论一:操作系统中的进程状态和Linux中的进程状态 个状态之间的联系以及他们之间担任的角色都存在的大同小异的效果
而在本章节我们重点要学习linux中的进程状态
linux进程状态
| 名称 | 具体含义 | 场景 |
|---|---|---|
| R(TAST_RUNNING)运行态 | 合并就绪和运行态。要么进程在cpu上执行命令,要么在排队等待cpu分配时间片 | 比如死循环程序会一直处于该状态,用 ps 命令查看可能显示 R+(前台运行)或 R(后台运行)。 |
| S(TAST_INTERRUPTIBLE)可中断睡眠态 | 因等待某个事件挂起 | 比如键盘输入、等待网络传输数据、在比如文本编辑器。系统中大多数进程平时就处于这个状态。使用kill命令可以唤醒或者终止这个进程 |
| D(TAST_UNINTERRUPTIBLE)不可中断睡眠态 | 也叫磁盘睡眠态。进程同样处于睡眠态 | 大多数是在等在磁盘I/o等关键操作完成。核心特点是操作系统也无法杀死他,kill -9命令也无法杀死例如向磁盘写入重要数据时候可能就处于该状态 |
| T(TAST_STOPPED)停止态 | 进程收到SIGSTOP信号后会被暂停执行,直到收到SIGCONT信号才会回复运行 | 比如终端执行kill -SIGSTOP 进程号 就可以让目标进入该状态,然后用 kill -SIGCONT 进程号 可恢复 |
| t(tracing stop)跟踪停止态 | 和 T 状态类似,但专门用于调试场景。当进程被 gdb 等调试工具跟踪时,遇到断点就会进入该状态。 | 此状态下进程不能用 SIGCONT 信号唤醒,只能通过调试工具的操作(如继续执行指令)恢复运行。 |
| X(TASK_DEAD-EXIT_DEAD) 死亡态 | 进程生命周期的最后阶段,代表进程已完全终止,正在释放最后的资源 | 这个状态非常短暂,用 ps 等命令几乎无法捕捉到,资源清理完成后进程就会彻底从系统中消失。 |
| Z(TAST_DEAD-EXIT_ZOMBIE)僵尸态 | 进程执行完毕,但是父进程没有调用wait()\waitpid()等函数回收它退出的状态信息,此时代码和数据已经释放但是 PCB还保留着 | 僵尸进程无法用kill命令删除,只能通过回收父进程或者重启系统来清理 |
👍2.偏移量+起始地址 == &目标地址
偏移量存在的必要性见3.
请看下面代码
#include<stdio.h>#include<stddef.h>// offsetof宏依赖的头文件// 定义测试结构体(包含不同类型成员,体现内存对齐)typedefstruct{inta;// 偏移0字节(int占4字节)charb;// 偏移4字节(内存对齐补3字节)doublec;// 偏移8字节(double占8字节)longlongd;// 偏移16字节(重点验证的成员)}obj;intmain(){// 1. 定义并初始化结构体变量obj test_obj={10,'x',3.14,10086};// 2. 计算成员d的偏移量size_toffset_d=offsetof(obj,d);printf("==== 核心验证:偏移量 + 起始地址 = 成员地址 ====\n");printf("成员d的偏移量:%lu 字节\n",offset_d);// 3. 基础验证:结构体真实地址 + 偏移量 = 成员d地址// 转为char*按字节偏移,再转回long long*指向成员dlonglong*calc_d_addr=(longlong*)((char*)&test_obj+offset_d);// 直接取成员d的地址longlong*real_d_addr=&test_obj.d;// 打印地址并验证一致性printf("结构体起始地址:%p\n",&test_obj);printf("计算出的d地址:%p\n",calc_d_addr);printf("直接取的d地址:%p\n",real_d_addr);printf("地址是否一致:%s\n\n",calc_d_addr==real_d_addr?"✅ 一致":"❌ 不一致");// 4. 补充验证:(struct obj*)0 方式的偏移量计算printf("==== 补充验证:0基地址的偏移量计算 ====\n");// 以0为结构体起始地址,取成员d的地址(等价于偏移量)longlong*zero_base_d_addr=&((obj*)0)->d;printf("以0为起始地址的d地址:%p\n",zero_base_d_addr);printf("偏移量是否等于0基地址的d地址:%s\n\n",(unsignedlong)zero_base_d_addr==offset_d?"✅ 是":"❌ 否");// 5. 额外验证:地址取值正确性(确保地址指向的是目标成员)printf("==== 取值验证 ====\n");printf("计算地址取值:%lld\n",*calc_d_addr);printf("直接取d的值:%lld\n",test_obj.d);return0;}运行结果
====核心验证:偏移量 + 起始地址=成员地址====成员d的偏移量:16 字节 结构体起始地址:0x7ffd35feafe0 计算出的d地址:0x7ffd35feaff0 直接取的d地址:0x7ffd35feaff0 地址是否一致:✅ 一致====补充验证:0基地址的偏移量计算====以0为起始地址的d地址:0x10 偏移量是否等于0基地址的d地址:✅ 是====取值验证====计算地址取值:10086 直接取d的值:10086👍3.正式开始剖析!操作系统内核里面的数据结构
structtast_struct{//进程其他属性/* …………………… */structlist_headlink;structlist_headqueue_link;structlist_headhash;/* 其他结构……………… */};Linux内核0.11代码
那么为什么操作系统要这么做? 为什么要把内部数据结构设计成这样?
全局进程链表(list_head link):管「所有存活进程」,支持遍历全量进程(如 ps 命令);
调度队列链表(queue_link):只管「待调度的 R 态进程」,供 CPU 调度器快速选进程。
多 list_head 嵌入结构体,进程可同时加入多个链表,零侵入扩展;
container_of 反推结构体地址,链表只存通用节点,不丢业务数据;
链表增删改查 O (1),调度 / 遍历效率最优。
另外 通过偏移量准确定位其他属性
通过list_node节点内存地址+成员偏移量反向计算出task_struct首地址,进而访问其他属性
这样实现的妙处在于 一个tast_struct 就可以既属于双链表又可以属于调度队列,未来还可以属于任何结构
进程状态
1.R运行状态
合并就绪和运行态。要么进程在cpu上执行命令,要么在排队等待cpu分配时间片
比如死循环程序会一直处于该状态,用 ps 命令查看可能显示 R+(前台运行)或 R(后台运行)。
创建状态
凡是在调度队列里的都属于创建状态,创建状态是运行状态的过渡。创建状态结合cpu==运行状态
补充——前台进程后台进程
想这些状态后面带+号的都表示是前台进程
不带加号的都表示后台进程
- 1.前台进程的概念
能从键盘中读取数据的拥有键盘文件的——叫做前台进程
结论无论前台程序还是后台程序都可以向显示器打印。
在Linux中有且只有一个前台进程就是我们使用的键盘 - 2.为什么要存在前台/后台进程
前台进程读取数据,后台进行运行程序 可以大大提高效率。
2.阻塞状态
2.1S可中断睡眠状态
进程因为等待某种资源,但是资源没有就绪。CPU就会让当前进程阻塞,直到资源就绪。
因为等待导致不被调度叫做阻塞。
比如键盘输入、等待网络传输数据、在比如文本编辑器。系统中大多数进程平时就处于这个状态。使用kill命令可以唤醒或者终止这个进程
代码实例 下面我们用scanf函数 等待键盘输入但是我们就是不输入
1#include<stdio.h>2#include<unistd.h>34intmain()5{6printf("PID:%d\n",getpid());7inta;8scanf("请输入数值:%d",&a);9return0;10}这里的S就表明是一个阻塞状态的进程
[root@VM-0-12-centos~]# ps-ajx|head-1&&ps-ajx|grep30367PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND31934303673036731934pts/130367S+10010:00./proc.exe28635305613056028635pts/230560S+00:00grep--color=auto3036731933319343193431934pts/130367Ss10010:00-bash最佳实践——实例——
我们s/canf时候 进程会等待我们从键盘手动输入数据。此时为了防止这个进程一直占用内存。就把这个调度队列放到了其所对应的硬件进程中。 (什么时候从键盘中读取到数据 再什么时候回来)
结论:进程是否阻塞就看tast-struct被放在哪个队列中
2.2D不可中断睡眠状态
也叫磁盘睡眠态。进程同样处于睡眠态 大多数是在等在磁盘I/o等关键操作完成。核心特点是操作系统也无法杀死他,kill -9命令也无法杀死 例如向磁盘写入重要数据时候可能就处于该状态。
2.3T停止态
进程收到SIGSTOP信号后会被暂停执行,直到收到SIGCONT信号才会回复运行 比如终端执行kill -SIGSTOP 进程号 就可以让目标进入该状态,然后用 kill -SIGCONT 进程号 可恢复
停止让后台程序获取数据
停止态存在的必要性:当发生越权、非法操作的时候、操作系统并不希望杀死进程。就可以将其变为停止态
2.4t跟踪停止态
和 T 状态类似,但专门用于调试场景。当进程被 gdb 等调试工具跟踪时,遇到断点就会进入该状态。 此状态下进程不能用 SIGCONT 信号唤醒,只能通过调试工具的操作(如继续执行指令)恢复运行。
遇到断点就暂停——调试视角
遇到断点就t——系统视角
3.挂起状态
当内存资源严重不足的时候
就将阻塞等待的进程 与swap分区进行换入和换出
这个过程叫做挂起(分为阻塞挂起和运行挂起)
挂起的本质是用时间换空间
结论:swap分区一般不能太大,最佳实践为内存/2或者=内存。
如果swap太大 那么就会过渡依赖swap分区,导致系统速度大幅度降低。
👍僵尸进程和孤儿进程
1.僵尸状态和僵尸进程
什么是僵尸状态
在子进程需要退出的时候,代码和数据已经销毁。但是tast_struct还需要维持一段时间。这个时候就需要父进程来读取。
如果这个时候父进程没有读取 那么就可能造成内存泄漏 此时的状态就是僵尸状态
1#include<stdio.h>2#include<unistd.h>3#include<sys/wait.h>45intmain(){6// 1. 创建子进程7pid_tchild_pid=fork();89if(child_pid==-1){// fork失败10perror("fork error");11return1;12}1314if(child_pid==0){// 子进程逻辑15printf("子进程(PID: %d):我要退出了,变成僵尸进程\n",getpid());16// 子进程立即退出(执行完这行就结束)17return0;18}else{// 父进程逻辑19printf("父进程(PID: %d):我不回收子进程(PID: %d),让它变成僵尸\n",getpid(),child_pid);20// 父进程进入死循环(不退出、不调用wait/waitpid回收子进程)21while(1){22sleep(1);// 每秒休眠,避免CPU占满,方便查看状态23printf("父进程:子进程还没被回收...\n");24}25// 注释:如果想回收子进程,取消下面一行注释即可消除僵尸态26// wait(NULL); // 回收子进程退出状态27}2829return0;30}父进程(PID:8262):我不回收子进程(PID:8263),让它变成僵尸 子进程(PID:8263):我要退出了,变成僵尸进程 父进程:子进程还没被回收... 父进程:子进程还没被回收... 父进程:子进程还没被回收...可以看到这里的状态已经变为Z+ 表示是一个前台僵尸进程
PPIDPID PGID SID TTY TPGID STATUIDTIME COMMAND82628263826231541pts/08262Z+10010:00[proc.exe]<defunct>316748298829731674pts/38297S+00:00grep--color=auto8263- 避免僵尸进程的措施
- wait(NULL)回收子进程退出状态
结论:一般情况下就算存在内存泄漏对我们的程序计算机影响也不是很大,最害怕的是一个死循环 它们不退出。我们现在几乎所有的软件 都是死循环。它们也叫做常驻进程
2.孤儿进程
僵尸进程是 子进程退出 父进程不管
孤儿进程是 父进程直接退出 子进程不退出
这个时候没了父进程 子进程就需要一个新的父进程 被领养
此时这个子进程就叫做孤儿进程
- 为什么要被领养?
必须领养:未来会退出保证系统回收
避免内存泄漏。否则一直为僵尸i进程
结论:一个进程变成孤儿进程 默认会变成后台程序
代码
#include<stdio.h>#include<unistd.h>#include<sys/wait.h>intmain(){// 1. 创建子进程pid_tchild_pid=fork();if(child_pid==-1){// fork失败处理perror("fork error");return1;}if(child_pid==0){// 子进程逻辑printf("【子进程】PID: %d | 父进程PID: %d\n",getpid(),getppid());// 子进程休眠10秒(足够父进程退出,变成孤儿进程)sleep(10);// 休眠后再次打印父进程PID(此时已被PID=1接管)printf("【子进程】休眠后 → 父进程PID: %d(已被init/systemd接管)\n",getppid());// 子进程执行完退出return0;}else{// 父进程逻辑printf("【父进程】PID: %d | 创建的子进程PID: %d\n",getpid(),child_pid);printf("【父进程】我要退出了,子进程即将变成孤儿进程\n");// 父进程主动退出(不等待子进程,让子进程失去父进程)return0;}}运行结果
【父进程】PID:10873|创建的子进程PID:10874【父进程】我要退出了,子进程即将变成孤儿进程 【子进程】PID:10874|父进程PID:10873[ljy@VM-0-12-centos12-18]$ 【子进程】休眠后 → 父进程PID:1(已被init/systemd接管)