news 2026/10/20 17:50:35

Linux进程状态详解:从R/S/D/T到僵尸与孤儿进程的排查指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux进程状态详解:从R/S/D/T到僵尸与孤儿进程的排查指南

1. 一张进程列表引发的掉发:STAT列背后是调度器的底层判断

1.1 状态位不是给运维看的装饰,而是内核的调度依据

在Linux下跑ps -l或者ps aux,STAT那一列永远是几个大写英文字母加符号的组合。R、S、D、T、Z、X,第一次看到的人很容易以为是某种运行等级编码,或者干脆忽略掉。但搞懂这一列,其实等于搞懂了"内核眼中的进程状态机"。

我们平时说"这个进程还活着""那个进程挂了",这是人话。内核不能这么模糊,它需要一个精确的字段来标记每个task_struct当前处于什么阶段,而这个字段会直接参与调度器的决策。换句话讲,状态位是给内核看的"排班表",不是给人看的装饰品。比如说,一个进程如果状态位是R,调度器就会把它放进可运行队列,参与CPU时间片分配;如果是S,调度器就知道它在等某个外部事件,暂时不用给它CPU;如果是D,调度器知道它正在内核态做不可打断的IO等待,不能把它拉出来重新调度。

这跟我们在电梯间看到的"维修中""停止服务"是同一个道理。电梯可以正常运行、可以暂停、可以在维修,维修状态下你按任何楼层按钮它都不理你,因为它的内部状态不允许被打断。Linux进程的状态位,本质上就是内核给每个进程贴的一张运行状态标签,背后是一整套状态迁移逻辑。

1.2 先把STAT列常用字母和辅助标记认全

看进程状态最常用的三条路:

  • ps -l:输出里STATE列显示进程状态
  • ps aux:STAT列显示状态加辅助符号
  • cat /proc/<pid>/status:里面有State: R (running)这样更详细的行

内核源码task_state_array[]里其实有六种状态字母:R(TASK_RUNNING)、S(TASK_INTERRUPTIBLE)、D(TASK_UNINTERRUPTIBLE)、T(TASK_STOPPED或TASK_TRACED)、Z(EXIT_ZOMBIE)、X(EXIT_DEAD)。还有I(TASK_REPORT_IDLE),那是内核线程或者idle任务常用的空闲状态,很多ps版本里也能看到。

除了主字母,STAT列还经常出现附带符号,这里也顺带说清楚:

辅助符号含义常见场景
+位于前台进程组你在终端前台跑的命令
s会话首进程通常是shell本身
l多线程进程内有多线程
<高优先级用nice -n -20启动
N低优先级用nice调低过
L内存页被锁定实时进程锁页,较少见

新手不需要把辅助符号全背下来,但要能看出主字母。因为后面排查问题时,状态主字母才是决定操作方案的依据。看到D你慌,看到Z你也慌,但看到S和T你可以安心忙别的。

2. R 和 S:绝大多数进程的"日常状态",别再误判成死机

2.1 R状态是排队等待CPU,不是独占CPU

R状态对应内核的TASK_RUNNING。很多教程说R代表"正在运行",这个说法不算错,但不够准确。准确讲,R代表这个进程处于可运行队列中,只要调度器给它机会,它就能立刻上CPU执行。

单核CPU同一时刻只能真正执行一个进程,但可运行队列里可能排着好几个R状态的进程。跑一个死循环程序,top里CPU占用可能接近100%,你再打开ps看那个进程,状态大概率就是R。但反过来看,并不是所有R状态的进程此刻都在CPU上跑,它可能只是排在队列里等调度器叫号。

举个例子,写个多进程死循环程序,四个核的机器上起二十个子进程全在跑死循环,你用top按CPU排序会发现每个进程在抢CPU,但ps里看,这二十个进程可能都在R状态。因为它们的共同点是"随时可以被调度",而不是"每个进程都占着一个核在跑"。

排查时遇到R可以不用太紧张:只要系统整体负载可控,R多不多不是关键。但如果R状态的进程数量远超CPU核心数,同时负载值很高,说明CPU已经过载,线程都在排队等时间片,这时候才该去分析是不是业务代码写死了循环、或者服务并发配置不合理。

2.2 S状态是可中断睡眠,kill它是有喊醒再处理的机会

S状态对应TASK_INTERRUPTIBLE,官方翻译叫"可中断睡眠"。这个状态形容得非常准确:进程正在等待某个事件,这个事件通常不是CPU,而是IO完成、锁释放、定时器到期、网络数据到达等等。在等待期间它不需要CPU,调度器自然也不会分配CPU给它。

它跟D的关键区别,是可以被打断。S状态下的进程收到信号,比如kill -15,内核可以把进程从等待队列里唤醒,让它先去处理信号。处理完信号,进程可以继续原来的等待,也可以按信号逻辑退出。

这也是为什么sleep 100这个命令启动后,你去ps看它是S状态,而不是T或者R。sleep做的本来就是定时等待,在等待间隔内它是"睡着的",不过这个睡眠可以被kill打断。你用kill -15给sleep发信号,它大概率立刻退出。如果它处在D状态,kill -15发过去一点反应都不会有。

2.3 为什么top里R很少、S一大片

很多人在八核机器上跑了一堆服务,打开top发现R状态寥寥无几,S状态倒是占满屏幕,于是怀疑自己是不是哪里搞错了。其实这是完全正常的状态分布。

系统里大多数进程都在等待:等待网络请求、等待磁盘IO返回、等待锁、等待定时器。这些等待统统体现为S。只有真正进入调度队列准备使用CPU的进程才表现为R。所以看到一个四核机器负载5、但R状态的进程就三五个,别的全是S,这完全可能。负载数字衡量的是"可运行进程加不可中断睡眠进程的总和",不是说负载5就有5个进程在抢CPU。

理解了R和S,很多"系统是不是卡死了"的疑问能省掉一半。一个进程停在S状态,别急着说它死锁或者卡死,先看看它在等什么;一个进程是R,也别急着夸它在干活,它也可能是在无脑空转。

3. D 和 T:一个打不过也骂不醒,一个还能救回来

3.1 D状态与内核IO的关系:为什么kill -9都无效

D代表TASK_UNINTERRUPTIBLE,翻译是"不可中断睡眠"。这个名字在运维圈子里比较出名,因为碰到D状态进程,很多人会先试kill -9,然后发现完全没用,接着开始怀疑系统是不是中毒了。

其实D状态不是真正意义上的"死了",而是进程正在内核态等待某个必须同步完成的IO操作。典型的触发场景是磁盘IO、NFS网络文件系统同步读写、内存回收等。在这些场景下,内核让进程进入D状态,目的是保证IO操作的一致性。进程此刻可能持有内核某些资源,或者正在操作系统内部的数据结构,如果允许外部信号把它打断,内核里可能留下竞态条件甚至损坏数据。

所以内核干脆把进程"锁"在这个等待里:除非IO操作完成,或者内核明确决定把等待改造成可中断,否则外部信号一概不响应。kill -9本质也是发一个不可忽略的信号,但它同样没办法切入这个内核等待流程。

遇到大量D状态进程,正确的排查方向不是"杀进程",而是先查IO是不是出了问题。建议按这个顺序查:

  1. iostat -x 1,看看磁盘util是不是接近100%,await是不是飙升
  2. df -h和mount,确认挂载的网络文件系统是否还通着
  3. dmesg -T | tail,检查有没有IO错误、NFS超时、设备重置的记录

如果确实是NFS远端失联导致的D状况,光杀本地进程解决不了,得先恢复远端服务,或者把挂载恢复。要是单块磁盘故障导致的大量D,那要做的是备份数据、换盘,而不是在进程层面折腾。

3.2 T状态:Ctrl+Z、调试器断点与SIGCONT恢复

T状态是TASK_STOPPED,代表进程被暂停执行。这个状态比D温柔很多,它是"暂停但没有死"。

最经典的触发方式就是你在终端下跑一个长任务,不耐烦了按下Ctrl+Z。这时shell会给前台进程发送SIGTSTP信号,进程进入T状态。你再用jobs命令能看到它,用fg把它拉回前台继续跑,或者用bg放到后台继续跑,本质都是发送SIGCONT信号让它恢复执行。

另一个常见来源是调试器。用gdb调试程序的时候,一旦打了断点并命中,进程会进入T状态,准确说是TASK_TRACED。ps里一般也显示T,含义是"被调试器追踪,停在了断点处"。

T状态还常被用来做进程分析:你怀疑一个进程行为异常,可以先kill -STOP <pid>把它冻结,然后cat /proc/<pid>/stack之类去分析它此刻的调用链,分析完再kill -CONT <pid>恢复运行。不至于直接杀进程,影响面小很多。

3.3 一张表区分R、S、D、T的状态转移方向

用表格把四个主状态的方向理一遍,方便对照:

状态内核宏进程在干什么能被信号打断吗怎么恢复/退出
RTASK_RUNNING可运行,排队等CPU能执行完或用信号终止
STASK_INTERRUPTIBLE等待IO/定时事件能事件到达或信号唤醒
DTASK_UNINTERRUPTIBLE内核态同步等待IO不能等IO完成,或内核恢复
TTASK_STOPPED/TRACED暂停,保留现场能SIGCONT恢复或SIGKILL终止

初学阶段最容易混淆的是S和D:一个能被打断,一个不能。记住"S是浅睡眠,喊能醒;D是深睡眠,喊不醒"就行。

4. 僵尸进程:进程的遗体还躺在PCB里等父进程认领

4.1 一个进程的"身后事"到底怎么办

先说结论:僵尸进程不是"还活着的怪进程",而是已经退出、但退出信息还没被父进程读取的进程。

子进程调用exit()或者从main函数return之后,内核会把它接走,清理它打开的文件描述符、释放它占用的内存、关闭它的信号处理逻辑。但有一个东西不会立刻清掉,就是task_struct,也就是进程控制块。为什么保留它?因为父进程需要知道两个信息:子进程是正常退出还是被信号杀死的,退出码是多少。这些信息就存在这个PCB里,等待父进程调用wait或waitpid来领取。

如果父进程一直没有调用wait,子进程的运行实体早就没了,但PCB还在,这个状态就是Z,僵尸。用生活化的方式理解:一个人已经办完离职手续走人了,但流程要求主管签个字确认交接,主管一直不去签,档案就一直挂在系统里,工号占着不释放。

僵尸进程几乎不消耗CPU和内存,它真正消耗的只有一个task_struct空间,数量少的时候基本没感觉。但它的存在意味着父进程没尽到回收义务,长期积累几百几千个僵尸,才能感觉到进程表空间被占用的问题。

4.2 写个C程序复现僵尸进程,并验证kill -9无效

文本讲再多不如自己复现一次。写个最简单的C程序,目录里创建一个文件演示:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("[child] pid=%d, 我现在退出\n", getpid()); exit(0); } printf("[parent] pid=%d, 子进程 pid=%d\n", getpid(), pid); sleep(60); printf("[parent] 终于睡醒了,开始回收\n"); wait(NULL); return 0; }

编译运行:

gcc -o zombie_demo zombie_demo.c ./zombie_demo

程序刚起来的60秒内,子进程已经exit(0)了,父进程还在sleep。此时打开另一个终端执行:

ps -eo pid,ppid,stat,comm | grep zombie_demo

会看到类似这样的输出:

12345 5678 S zombie_demo 12346 12345 Z zombie_demo

PID 12346是子进程,它的STAT列已经变成Z。此时你尝试:

sudo kill -9 12346

系统只会回一句kill: (12346) - No such process或者干脆静默失败。因为kill用的是进程PID,但僵尸进程已经没有可以被信号处理的可执行环境了。这不是权限问题,不是信号类型问题,是它已经死了,只是没有"注销户口"。

想看到这个"no such process"后的效果,可以等父进程60秒睡眠结束,wait(NULL)收回子进程退出码,那就看不到Z了。

4.3 收拾僵尸的三种可落地方案

对付僵尸,不能对着子进程使劲,得对着父进程或者代码逻辑使劲。

方案一:让父进程调用wait或waitpid。这是最正规的做法。代码里在fork之后、合适的位置等待子进程,获取退出状态。现实中开发者在编写守护进程或并发服务时,往往会在SIGCHLD信号处理函数里调用waitpid(-1, &status, WNOHANG)来批量回收。

方案二:直接忽略SIGCHLD信号。内核把SIGCHLD发给父进程,也是为了让父进程知道"你有个孩子退出了"。如果程序明确声明不关心子进程的退出状态,可以在初始化时写:

#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { // 显式忽略 SIGCHLD,子进程退出后由内核自动回收 signal(SIGCHLD, SIG_IGN); pid_t pid = fork(); if (pid == 0) { printf("[child] 我退出了\n"); exit(0); } sleep(5); return 0; }

设置SIG_IGN之后,内核会在子进程退出时直接释放它的PCB,不再保留成僵尸。这是很多高并发服务中常见的选择:反正不在乎退出码,进程也挺多,每个都wait会拖累性能,干脆让内核直接回收。

方案三:如果父进程已经是个坏掉的程序,根本不会处理信号也不会wait,那最直接的办法是结束父进程。父进程一死,僵尸子进程就会被init进程收养,也就是next进程(PID 1)。而init进程是祖传的"收尸大队",它会循环调用wait,把所有这些无人认领的僵尸进程逐一回收。所以某些场景下,你可以先找到僵尸进程的PPID,确认是哪个父进程没干活,把它干掉,僵尸就跟着消失了。

需要说明的是,方案三属于应急操作,生产环境最好还是让开发同事修代码。

5. 孤儿进程:父进程先跑路,init会来接盘

5.1 孤儿进程诞生后PPID为什么会变成1

僵尸是"子先死,父不收尸",孤儿正好相反:父进程先退出,子进程还在正常运行。

父进程挂了之后,子进程不能平白无故没有"长辈",内核会把它的PPID改成1,也就是让它挂到init进程名下。这个机制叫reparent,重新分配父进程。如果把进程比喻成小孩,父进程就是监护人,监护人突然没了,系统不会丢下小孩不管,而是立刻指派一个"集体监护机构"来接管。

这个接管好处很大:孤儿进程还能继续执行,因为它的父进程死了不等于它自己要退场。它原本持有的文件描述符、内存、打开的网络连接都不会因为父进程先走而立刻释放,只有继续跑的进程自己退出或不依赖父进程的那些资源,才会正常清理。

举例场景:你在Shell里用nohup ./myserver &启动一个后台服务,然后关掉终端。Shell退出后,服务进程的PPID会变成1。很多人以为关了终端服务就没了,其实只要服务本身没从Shell领到"终端退出时发SIGHUP"的逻辑,进程就继续活着,被init接管。这就是孤儿进程最常见的生产场景。

5.2 孤儿进程的自动回收机制与subreaper

init进程接管孤儿之后,不需要开发者操什么心。它内部会不断调用wait,所以孤儿最后退出时,由init负责回收,不会变成僵尸。

现代Linux上还有一个叫subreaper的机制,可以理解成"二级收尸队长":某个进程可以通过prctl(PR_SET_CHILD_SUBREAPER)把自己设置成子孙进程的reaper,这样它的子进程变成孤儿后,会被它接管而不是直接跳到init。容器场景里经常用到,因为容器里的1号进程往往想自己管理子进程生命周期。

大多数情况下,理解到"PPID变1,init自动回收"就够了。碰到孤儿的现实问题是:你本来想停掉一个后台任务,结果发现它爹已经死了,直接kill又舍不得,只好顺着进程线索一路找,最后发现它挂在PID 1下面运行。这时候不要慌,孤儿是可以正常杀掉的,因为它只是换了个父进程,主体还活着。

5.3 僵尸与孤儿对照表,面试题终于能串起来了

很多面试官喜欢一次问完僵尸进程和孤儿进程,因为这两个概念正好互补。整理成对照表,思路一下就清晰了:

对比项僵尸进程孤儿进程
产生顺序子进程先退出,父进程还在父进程先退出,子进程还在运行
状态Z,已经不再执行正常状态,继续执行
是否占用资源只剩PCB,不占用内存和CPU正常占用资源
谁能收尸父进程调用waitinit或subreaper自动接管并回收
能被kill吗不能,已无执行上下文能,和普通进程一样
危害程度多到一定数量会占满进程表几乎没有,除非它的父进程死因值得深究

理解到这个层面,再回头看标题里"从R/S/D/T到僵尸/孤儿"这条线,其实是一条完整的状态迁移链:R/S/D/T是一个进程活着时的不同状态,Z是退出瞬间等待父进程善后的状态,X是一闪而过的真正释放状态,而孤儿描述的是"父子关系断裂后的存活方式"。

6. 手把手观察五个状态:从命令到状态机的真实操作

6.1 制造R和S:忙循环 vs sleep

想要看着状态变化,动手跑几个命令最直观。

先制造R状态。在终端开一个死循环:

while :; do :; done

另开一个终端,找到它的进程号:

ps -eo pid,stat,comm | grep while

看到STAT是R,因为while :; do :; done这个bash子进程在空转,CPU每一刻都分给它执行。再数一下有多少个R状态的进程:

ps -eo stat | grep '^R'

四核机器上,跑一堆死循环,R的数量会持续在核心数附近徘徊,而不会无限膨胀,因为没抢到CPU时间片的死循环进程虽然状态是R,但依然需要排队。

再制造S状态,开一个sleep 100,然后查看:

sleep 100 & ps -eo pid,stat,comm | grep sleep

STAT显示S,或者S后面跟个符号。因为进程在定时器等待中,不消耗CPU。对比一下死循环进程和的sleep进程,能看到同样的生命周期里,等待和忙等的区别。

6.2 制造T:Ctrl+Z暂停与恢复

在终端跑一个耗时的循环,比如:

find / -name "*.conf"

按Ctrl+Z,然后查状态:

jobs -l

输出里括号内会显示已停止或者英文Stopped,进程号也在这里。再通过ps确认状态:

ps -o pid,stat,cmd -p <那个进程号>

STAT会显示T,因为SIGTSTP把进程暂停在终端作业状态。这时试着用fg把它带回前台:

fg

进程恢复执行。再查状态又变回R或S,视它此刻真正在忙还是在等IO而定。

如果想用命令方式制造T,可以:

kill -STOP <pid> ps -o pid,stat,cmd -p <pid> kill -CONT <pid>

这样T和恢复的流程完全掌握在自己手里,比Ctrl+Z更可控。

6.3 一个窗口里同时观察Z和孤儿进程的PPID变化

把前面两个C程序合并一下,一次观察两种状态:

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <signal.h> int main() { pid_t pid_b = fork(); // 这个子进程会变成孤儿 if (pid_b == 0) { // 子进程B:打印父进程PID,睡3秒后看父进程PID变了没 printf("[childB] 现在我的ppid=%d\n", getppid()); sleep(3); printf("[childB] 睡醒了,我的ppid=%d\n", getppid()); return 0; } pid_t pid_a = fork(); // 这个子进程会变成僵尸 if (pid_a == 0) { printf("[childA] 我直接退出,变成僵尸\n"); _exit(0); } printf("[parent] 我马上退场\n"); return 0; }

编译运行后,父进程是程序主体,会退场。子进程B会从打印的PPID里看到变化:刚开始是父进程PID,3秒后变成1。子进程A因为父进程已经不存在了,会被init接管,由init负责回收,所以它不会长期停留成Z状态,会很快被扫走。

想观察僵尸,就用4.2节的代码,让父进程占用较长的时间不回收子进程。想观察孤儿,就用5.2节的代码。两者对照着跑一遍,进程状态从R/S/D/T到Z/X这条完整链路基本就串起来了。

7. 关于这些状态,排障时我总结的几点经验

7.1 系统满载时R和负载的关系

排查线上问题,经常看到load average飙到四五十,但top里R状态的进程并没有想象中那么多。这很正常,因为负载计算把不可中断睡眠也算了进去,D状态进程同样会被计入负载。Linux的loadaverage统计的是"处于R或D状态的进程数量平均数",不是"正在使用CPU的进程数"。

所以光看负载高低没办法判断CPU是不是瓶颈,得配合top里的us/sy比例、vmstat里的r和b两列一起看。r列高说明CPU排队严重,b列高说明有进程卡在IO等待上,这两个的判断方向完全不同。

7.2 大量D状态时先看IO还是先杀进程

一次生产故障,某业务系统的进程全部卡在D状态,同事第一反应是kill -9,结果进程纹丝不动,最后重启宿主机才恢复。事后排查发现是NFS挂载点失联,网络包一直在重传,内核IO等待无法完成。

我的建议是:看到D状态大批量出现,第一件事是dmesg扫一眼有没有设备重连、IO error日志,然后iostat -x 1看设备utilization和await。如果确实因为远端IO失联,先把远端恢复或者umount挂载点。进程层面越是急着杀,越容易把系统搞得半死不活,因为那些进程可能持有文件系统锁、正在刷脏页,杀掉它们留下更深的文件系统元数据问题。

7.3 大量Z状态时该检查什么

僵尸进程零星一两个不用大惊小怪,比如某次ssh会话退出后留下的残留,过一段时间会被自动收走。但如果ps -eo stat里Z数量持续增长且不掉,重点查父进程:

ps -eo pid,ppid,stat,cmd | grep Z

找到僵尸进程的PPID,再查那个父进程在跑什么、是不是有异常退出或者没处理SIGCHLD。很多情况下是父进程本身被冻结了,或者代码里没写wait调用。生产环境如果出现批量僵尸,通常意味着某个服务的子进程逻辑有bug,比如fork了但不回收、或者sighandler写得太粗糙。

另一个容易踩的坑是:很多人看到僵尸进程出现,第一反应是杀父进程让它变成孤儿让init回收。对单机进程这招确实奏效,但在容器里,容器内的1号进程不一定做了wait回收。容器环境里的僵尸可能需要你自己写个辅助进程去做reaping,或者配合subreaper机制处理。

这些状态看着是概念题,实际上每一条都能映射到真实的运维事故里。能把R/S/D/T和僵尸孤儿串成一条线,再去读ps输出的时候,心里会踏实很多——每个字母背后都对应着一个正在发生的系统故事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 17:50:10

让“一切顺利”从许愿变成系统工程:可控变量与预案的力量

1. 先搞清楚“顺利”到底意味着什么1.1 顺利不是零失败&#xff0c;而是可控的波动我见过太多人把“一切顺利”理解成一条笔直的上升线&#xff1a;计划列得满满当当&#xff0c;每一步都指向成功&#xff0c;只要中间出任何一点岔子&#xff0c;就觉得“不顺利”了。但说实话&…

作者头像 李华
网站建设 2026/10/20 17:50:10

吃透Android系统架构:从Linux内核到Binder的完整解析

1. 从一张"分层蛋糕"说起&#xff1a;为什么每个Android开发者都应该吃透系统架构 刚接触Android开发那会儿&#xff0c;我最常干的事就是往项目里塞依赖、写Activity、跑模拟器&#xff0c;觉得"能跑起来"就算是会Android了。直到有一天&#xff0c;项目里…

作者头像 李华
网站建设 2026/10/20 17:49:47

微软开源长音频ASR与苹果AI胸针:端侧AI语音交互如何落地

01 导语&#xff1a;微软开源长音频 ASR、苹果 AI 胸针曝光&#xff0c;这波管线该细看 今天早上刷到两条科技新闻&#xff0c;一条来自软件巨头&#xff0c;一条来自消费电子巨头&#xff0c;信息密度都很高&#xff1a;微软开源了一个名为 VibeVoice-ASR 的语音识别模型&…

作者头像 李华
网站建设 2026/10/20 17:49:24

信创环境下Java大文件分块上传适配与性能调优实践

去年接手了一个内部管理系统向信创环境迁移的改造任务&#xff0c;业务上第一个卡住的功能就是大文件上传。业务场景本身不复杂&#xff0c;就是上传项目文档、培训视频、图纸压缩包这批东西&#xff0c;单文件最大能到2GB到4GB。放在以前的老环境里&#xff0c;改几个中间件参…

作者头像 李华
网站建设 2026/10/20 17:48:39

盲盒抽奖移动端系统开发:概率引擎、库存原子性与多端支付回调实战

简介&#xff1a;这是一套面向潮玩盲盒电商创业者的移动端商城系统源码&#xff0c;基于ThinkPHP框架开发&#xff0c;覆盖盲盒抽奖、一番赏、抽盒机等核心玩法&#xff0c;可同时适配H5、公众号与APP多端场景&#xff0c;适合具备PHP基础、希望快速搭建盲盒商城的开发者或运营…

作者头像 李华
网站建设 2026/10/20 17:48:33

SM2证书链验证失败?自建Python验证器绕过cryptography白名单

我最近在好几个国密改造项目里连续踩到同一个坑&#xff1a;证书链上只要出现一张SM2证书&#xff0c;Python的cryptography库就率先抛异常&#xff0c;代码里按官方文档写好的链验证逻辑根本走不到握手成功那一步。报错永远是那一句“Unknown signature algorithm OID”或者“…

作者头像 李华