【Linux】信号之信号的保存和处理详解
🤖个人主页:晚风相伴-CSDN博客
💖如果觉得内容对你有帮助的话,还请给博主一键三连(点赞💜、收藏🧡、关注💚)吧
🙏如果内容有误或者有写的不好的地方的话,还望指出,谢谢!!!
让我们共同进步
目录
前言
✨信号的阻塞
🔥信号的常见概念
🔥在内核中的表示
❔sigset_t类型
🔥信号集操作函数
sigprocmask
sigpending
示例代码
✨信号的捕捉
🔥理解信号捕捉的流程
🔥sigaction
❔可重入函数编辑
❔volatile
前言
信号的时间线
在上一篇《信号之信号的产生》中已将信号产生讲明白了,本篇就来讲讲信号的保存和处理吧。
✨信号的阻塞
🔥信号的常见概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
🔥在内核中的表示
信号在内核中的表示示意图
每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针数组,里面保存的是函数的地址表示处理动作。信号产生时,操作系统在进程控制块(PCB)中设置该信号的未决标志(由0->1),直到信号递达后才清理该标志位(由1->0)。
例如在上面的图中
- SIGHUP信号没有阻塞也没有产生,当它递达时执行默认的处理动作。
- SIGINT产生了,但是Block位图中是1,表示它正在被阻塞着,所以暂时不能递达,虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作在解除阻塞之前。
- SIGQUIT信号没有产生,但是Block位图中是1,所以一旦产生了SIGQUIT信号将会被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这个信号产生了多次,Linux操作系统该如何处理呢?
对于常规信号而言[1-31],操作系统会将它们在递达之前产生多次只计一次。也就是说,即使这个信号在阻塞期间产生了多次,当阻塞被解除并且信号能够传递给进程时,进程只会收到一个这样的信号。
对于实时信号而言[34-64],它们在递达之前产生多次是可以排队的,即多个相同的实时信号在阻塞期间产生会依次放在一个队列里,当阻塞被解除时,它们会按照产生的顺序依次递达给进程。
❔sigset_t类型
每个信号就是由一个比特位来表示其状态,非0即1。从上图可以看出,在阻塞位图和未决位图中都是这样表示的,所以它们可以用相同的数据类型sigset_t来进行存储,sigset_t称为信号集。对于这个类型内部如何存储这些比特位的则依赖于系统实现,用户并不需要关心,这个类型可以表示每个信号“有效”或“无效”的状态,在阻塞位图(阻塞信号集)中“有效”或“无效”就表示该信号是否被阻塞,而在未决位图(未决信号集)中“有效”或“无效”就表示该信号是否产生。
注:阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。
🔥信号集操作函数
可以调用以下函数对sigset_t类型进行操作
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(sigset_t *set, int signo);
- sigemptyset函数:初始化set所指向的信号集,使其中所有的信号对应的比特位清0,表示该信号集不包含任何有效信号。
- sigfillset函数:初始化set所指向的信号集,使其中所有的信号对应的比特位置1,表示该信号集包含所有有效信号
- sigaddset函数:在该信号集中添加某个有效信号
- sigdelset函数:在该信号集中删除某个有效信号
- sigismember函数:判断一个信号集的有效信号中是否包括了某个信号
前4个函数的返回值都是成功返回0,失败返回-1。最后一个是若包含则返回1,若不包含则返回0,失败返回-1。
sigprocmask
该函数用于读取或者更改进程的信号屏蔽字(阻塞信号集)
如果oldset是非空指针,则读取进程通过oldset参数传出的当前信号屏蔽字。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。
下面是how参数的可选项
SIG_BLOCK 将set指向的信号集添加到当前进程的信号屏蔽字中 SIG_UNBLOCK 将set指向的信号集从当前进程的信号屏蔽字中移除 SIG_SETMASK 设置当前信号屏蔽字为set所指向的值 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending
该函数用于读取当前进程的未决信号集
返回值:成功成功返回0,失败返回-1
示例代码
将2号信号阻塞掉,并且不断的获取并打印当前进程的pending信号集,如果我们向该进程发送一个2号信号,我们就可以看到在pending信号集中一个比特位由0->1。
#include <iostream>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;void handle(int signum)
{cout << "捕捉到信号: " << signum << endl;
}static void printPending(sigset_t &pending)
{for (int i = 1; i <= 31; i++){if (sigismember(&pending, i))cout << "1";elsecout << "0";}cout << endl;
}int main()
{//捕捉2号信号,不让其退出signal(2, handle);// 1、定义信号集对象sigset_t bset, obset;sigset_t pending;// 2、初始化sigemptyset(&bset);sigemptyset(&obset);sigemptyset(&pending);// 3、添加要屏蔽的信号sigaddset(&bset, 2 /*SIGINT*/);// 4、设置set到内核中对应的进程内部int n = sigprocmask(SIG_BLOCK, &bset, &obset);assert(n == 0);(void)n;cout << "block 2号信号成功……, pid: " << getpid() << endl;// 5、重复打印当前进程的pending信号集int count = 0;while (true){// 获取pending信号集sigpending(&pending);// 打印信号集printPending(pending);sleep(1);count++;//count == 20时解除对2号信号的阻塞if (count == 20){n = sigprocmask(SIG_SETMASK, &obset, nullptr);assert(n == 0);(void)n;cout << "解除2号信号的block成功" << endl;}}return 0;
}
结果演示
如果我们将所有的信号都进行阻塞掉,我们是不是就写了一个不会被异常或者用户杀掉的进程?
这真的可以吗?用下面的代码进行验证一下
static void printPending(sigset_t &pending)
{for (int i = 1; i <= 31; i++){if (sigismember(&pending, i))cout << "1";elsecout << "0";}cout << endl;
}static void blockSig(int sig)
{sigset_t bset;sigemptyset(&bset);sigaddset(&bset, sig);int n = sigprocmask(SIG_BLOCK, &bset, nullptr);assert(n == 0);(void)n;
}int main()
{cout << "pid: " << getpid() << endl;for (int sig = 1; sig <= 31; sig++){blockSig(sig);}sigset_t pending;while (true){// 获取pending信号集sigpending(&pending);// 打印信号集printPending(pending);sleep(1);}return 0;
}
mykill.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>using namespace std;void Usage(string proc)
{cout << "Usage:\r\n\t" << proc << " processid" << endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[1]);exit(1);}int processid = atoi(argv[1]);for (int i = 1; i <= 31; i++){if (i == 9 || i == 19)continue;kill(processid, i);cout << "kill -" << i << endl;sleep(1);}kill(processid, 9);return 0;
}
结果演示
其实在上面的代码中是屏蔽了9号信号和19号信号的,如果你将9号信号放开,这个进程立马就会被终止掉,所以将所有信号阻塞掉,还是可以将这个进程终止的——用9号信号。
19号信号是中止进程
✨信号的捕捉
将所有的信号都捕捉,那么这个进程是不是就不会被异常或者用户杀掉了呢
示例代码
void catchSig(int signum)
{cout << "获取一个信号: " << signum << endl;
}int main()
{for (int i = 1; i <= 31; i++)signal(i, catchSig);while (true)sleep(1);return 0;
}
结果演示
可见虽然我们自定义捕捉了所有信号,但是9号信号还是有用的,不会失效。
🔥理解信号捕捉的流程
信号产生之后,信号不是被立即处理而是在合适的时候进行处理。
内核态和用户态的概念
当一个进程执行系统调用而陷入内核代码中执行时,则称其处于内核态
当一个进程在执行用户自己的代码时,则称其处于用户态
怎么理解这个合适的时候呢
我们都知道信号的相关数据字段都是保存在进程PCB内部的,而PCB是属于内核的范畴,所以要检测这个信号是否未决,是否产生,一定是在内核状态进行检测的;而进程大部分时间是在执行你写的代码,而这所处的状态叫做用户态。所以当从内核态返回到用户态时,就会对信号进行检测和处理。这就叫做合适的时候。
如何理解用户态和内核态之间的相互转换
在进程的地址空间中0-3GB是用户区,3-4GB是内核区。
- 当进程处于用户态时,在地址空间中的用户区的代码和数据,通过用户级的页表映射到物理内存中,CPU就能拿到代码和数据进行执行
- 当进程处于内核态时,在地址空间中的内核区的代码和数据,用户内核级的页表映射到物理内存中,CPU就能拿到代码和数据进行执行
注意:内核级页表可以被每个进程所看到,因为操作系统只有一个,每个进程通过内核级页表映射到物理内存的同一块区域。
地址空间中的内核区和用户区之间的转换是通过系统调用和上下文切换来实现的。
例如,你在你的代码中使用了系统调用接口open,它就会直接在地址空间中由用户区跳转到内核区,然后再内核区通过内核级页表将代码和数据映射到物理内存上。
注:CPU可以通过寄存器里的值就能知道当前进程是处于用户态还是内核态
当然内核也是可以执行用户的代码的,但是内核不愿意,也不想这样干,因为如果这样干很可能导致内核中的数据和代码被用户给修改了。
信号捕捉的流程图:
假设用户自定义的信号处理函数sighandler,进程当前正在执行用户的代码,这时执行到某条指令时发生了中断,用户态切换到了内核态并保存当前进程的上下文。内核态将中断处理完后需要返回到用户态,在返回之前先检查一下是否有信号需要递达, 如有则要对信号进行处理,如果信号的处理动作是用户自定义的, 那么内核态就要返回到用户态执行sighandler函数(sighandler和主函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程),sighandler函数处理完后,进程又再次返回到内核态,如果没有新的信号需要递达了,那么就需要从内核态返回到用户态并且恢复上下文继续从主逻辑向下执行。
可以简化成下面这幅图
🔥sigaction
sigaction函数可以读取和修改与指定信号相关联的处理动作。和signal作用类似
参数:
- sig:指定信号的编号
- 若act指针非空,则根据act修改该信号的处理动作。
- 若oact指针非空,则通过oact传出该信号原来的处理动作
返回值:成功则返回0,失败则返回-1。
说明:
sa_handler是一个回调函数,被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本篇的代码把sa_flags设为0,sa_sigaction是实时信号的处理函数,这里不做讨论。
示例代码
static void printPending(sigset_t &pending)
{for (int i = 1; i <= 31; i++){if (sigismember(&pending, i))cout << "1";elsecout << "0";}cout << endl;
}void handler(int signum)
{cout << "捕捉到一个信号: " << signum << endl;int count = 20;sigset_t pending;while (true){// 获取pending信号集sigpending(&pending);// 打印信号集printPending(pending);count--;if (!count)break;sleep(1);}
}int main()
{cout << "pid: " << getpid() << endl;struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);sigaddset(&act.sa_mask, 6);act.sa_handler = handler;sigaction(2, &act, &oact);cout << "default action: " << (int)(oact.sa_handler) << endl;while (true)sleep(1);
}
结果演示
❔可重入函数

例如在图中的循环单链表
在主函数中调用insert函数向一个链表的头节点插入节点node1,插入操作分为两步,刚做完第一步时,因为硬件中断使进程切换到内核,当内核态会到用户态之前检查到有信号需要处理,于是切换到自定义的信号处理函数,信号处理函数中也调用了insert函数向同一个链表的头节点中插入node2,插入操作的两步都完成了之后,信号处理函数返回到内核态,再次返回到用户态后就从主函数调用的insert函数中继续向下执行,先前只做完了第一步就被打断了,现在继续做插入操作的第二步,但是在自定义的信号处理函数中已经插入了node2了,node2作为新的头结点,node1在完成剩下的一步时,就会导致node2节点丢失,导致内存泄漏等问题。
所以一个函数在一个特定的时间段内被多个执行流重复进入,如果该函数被重复进入不会导致问题那么就叫做可重入函数,否则就叫做不可重入函数
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准的I/O库的很多实现都以不可重入的方式使用全局数据结构。
❔volatile
示例代码
int flag = 0;void changeFlag(int signum)
{(void)signum;cout << "change flag: " << flag;flag = 1;cout << "->" << flag << endl;
}int main()
{signal(2, changeFlag);while (!flag);cout << "进程正常退出后: " << flag << endl;return 0;
}
没给编译器加优化前
结果演示
给编译器加了优化后
结果演示
导致这样的结果的原因:
- 在没加优化之前,CPU是正常从我们的内存中读取flag的值的,在flag由0->1后,CPU读取到了1,就直接让进程终止掉了。
- 在加了优化之后,CPU在看了main函数中没有对flag进行修改的相关语句,所以CPU就不在从内存中读取flag的值了,而是从自己的寄存器中读取比如说edx寄存器, 所以CPU一直读取到的flag是0,也就不会让进程终止掉了。
加了优化并且加了volatitle后
结果演示
volatitle作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
相关文章:

【Linux】信号之信号的保存和处理详解
🤖个人主页:晚风相伴-CSDN博客 💖如果觉得内容对你有帮助的话,还请给博主一键三连(点赞💜、收藏🧡、关注💚)吧 🙏如果内容有误或者有写的不好的地方的话&…...

基于Django的图书管理系统
文章目录 前言一、页面展示1.登录2.前端页面3.后端页面 二、项目上传(1)导入数据库(2)导入项目(3)数据库密码修改(4)进入网站 总结 前言 本网站调用Django编写了图书管理网站&#…...

js实现元素根据鼠标滚轮滚动向左右上下滑动着从模糊到清楚显示出来
html代码 <div ref{test} id"animatedElement" className"not-animated"> <div style{{width:"100px",height:"50px",backgroundColor:"red"}}> </div> </div> JS代码 const te…...

yocto学习
bitbake命令单独编译u-boot: $ bitbake -c compile -f u-boot-imx $ bitbake -c deploy -f u-boot-imx //部署编译生成的u-boot镜像到deploy bitbake命令单独编译kernel: bitbake -c compile -f linux-imx //编译内核 bitbake -c deploy -f linux-imx /…...

【IC设计】牛客网-序列检测习题总结
文章目录 状态机基础知识VL25 输入序列连续的序列检测VL26 含有无关项的序列检测VL27 不重叠序列检测VL28 输入序列不连续的序列检测参考资料 状态机基础知识 VL25 输入序列连续的序列检测 timescale 1ns/1ns module sequence_detect(input clk,input rst_n,input a,output re…...

python爬虫登录到海康相机管理页面
简述 1.最近接到个任务是在管理页面更改相机的某个参数,下载官方的sdk貌似没有提供这个接口,所以只能自己写爬虫登录发请求了。 1.主要步骤 1.1 发送get请求获取到salt,sessionID,challenge等信息 http://admin:123456192.168.…...

9.Docker网络
文章目录 1、Docker网络简介2、常用基本命令3、网络模式对比举例3.1、bridge模式3.2、host模式3.3、none模式3.4、container模式3.5、自定义网络 1、Docker网络简介 作用: 容器间的互联和通信以及端口映射容器IP变动时候可以通过服务名直接进行网络通信而不受到影…...

Windows VS2022 C语言使用 sqlite3.dll 访问 SQLite数据库
今天接到一个学生C语言访问SQLite数据库的的需求: 第一步,SQLite Download Page下载 sqlite3.dll 库 下载解压,发现只有两个文件: 于是使用x64 Native Tools Command Prompt 终端 生成 sqlite3.lib 和 sqlite3.exp文件 LIB -def:sqlite3.def -out:sqlite3.lib -machin…...

java库和包的概念
在Java中,"库"和"包"是两个不同的概念,但它们之间存在着密切的关联。 库(Library) 定义:库是一组已经编写好的代码和资源,用于解决特定的问题或提供特定的功能。它可以包含一个或多个…...

mysql内存结构
一:逻辑存储结构:表空间->段->区->页->行、 表空间:一个mysql实例对应多个表空间,用于存储记录,索引等数据。 段:分为数据段,索引段,回滚段。innoDB是索引组织表&…...

Python | Leetcode Python题解之第111题二叉树的最小深度
题目: 题解: class Solution:def minDepth(self, root: TreeNode) -> int:if not root:return 0que collections.deque([(root, 1)])while que:node, depth que.popleft()if not node.left and not node.right:return depthif node.left:que.appen…...

c++二进制输出
输入一个数,输出n个数,数可以是0或1;输入:4输出:0010;提示:本题要用到rand(),srand(time(0));代码如下:#include<bits/stdc.h> #include<windows.h> using namespace s…...

5. C++网络编程-UDP协议的实现
UDP是无连接的。 UDP Server网络编程基本步骤 创建socket,指定使用UDP协议将socket与地址和端口绑定使用recv/send接收/发送数据 由于UDP是无连接的,直接侦听就行使用close关闭连接 这个UDP接收数据的时候用的API是recvfrom,发送数据是sendto 客户端 …...

Altium Designer 中键拖动,滚轮缩放,并修改缩放速度
我的版本是AD19,其他版本应该都一样。 滚轮缩放 首先,要用滚轮缩放,先要调整一下AD 设置,打开Preferences,在Mouse Wheel Configuration 里,把Zoom Main Window 后面Ctrl 上的对勾取消掉,再把…...

python从入门到精通04
一、编写英文月份词典 month_num int(input("请输入您想要查询的月份:")) month_list [January,February,March,April,May,June,July,August,September,October,November,December] print("您查询的月份单词是:", month_list[mon…...

tomcat三级指导
版本 ./catalina.sh linux version.bat win 1.确认是否使用了tomcat管理后台 我们先找到配置文件:tomcat主目录下/conf/server.xml 可以查看到连接端口,默认为8080 然后查看manager-gui管理页面配置文件,是否设置了用户登录 配置文件…...

不知道是该怎么引用多个函数片段?具体示例如代码
🏆本文收录于「Bug调优」专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&…...

P3128 [USACO15DEC] Max Flow P题解(树上差分,最近公共祖先,图论)
前言: 题目链接:P3128 [USACO15DEC] Max Flow P - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 讲解: 这一题含金量真算高的,包含了建树(用了图论的知识),求最近公共祖先(倍增法…...

在Linux上面部署ELK
注明:一下的软件需要自己准备 一、准备环境: 1.两台elasticsearch主机4G内存 2.两台elasticsearch配置主机名node1和node2(可以省略) #vim /etc/hostname #reboot 3. 两台elasticsearch配置hosts文件 #vim /etc/hosts 192.168.1.1 node1 192…...

Langchain-Chatchat的markdownHeaderTextSplitter使用
文章目录 背景排查步骤官方issue排查测试正常对话测试官方默认知识库Debug排查vscode配置launch.json命令行自动启动condadebug知识库搜索测试更换ChineseRecursiveTextSplitter分词器 结论 关于markdownHeaderTextSplitter的探索标准的markdown测试集Langchain区分head1和head…...

掩码生成蒸馏——知识蒸馏
摘要 https://arxiv.org/pdf/2205.01529 知识蒸馏已成功应用于各种任务。当前的蒸馏算法通常通过模仿教师的输出来提高学生的性能。本文表明,教师还可以通过指导学生的特征恢复来提高学生的表示能力。从这一观点出发,我们提出了掩码生成蒸馏(…...

【C#实战】Newtonsoft.Json基类子类解析
情景再现 假设你有如下类: public class Item {public int Id;public string Name; }public class Weapon: Item {public int CurrentAmmo; }public class Inventory {public List<Item> Items; } 其中你序列化的是Inventory类,Items列表里混杂着…...

表达式求值的相关语法知识(C语言)
目录 整型提升 整型提升的意义 整型提升规则 整型提升实例 算术转换 赋值转换 操作符的属性 C语言的语法并不能保证表达式的执行路径唯一!!! 问题表达式 整型提升 C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这…...

开发中遇到Electron自定义窗口的问题
开发中遇到Electron自定义窗口的问题 使用VUE3 Electron 开发一个音乐软件,自定义导航栏的放大、缩小和关闭。 其中使用ipcRenderer进行联系Electron,进行放大、缩小和关闭操作。 遇到问题 遇到__dirname is not defined in ES module scope //在V…...

c# sqlite使用
安装包 使用 const string strconn "Data Sourcedata.db"; using (SQLiteConnection conn new SQLiteConnection(strconn)) {conn.Open();var cmd conn.CreateCommand();cmd.CommandText "select 1";var obj cmd.ExecuteScalar();MessageBox.Show(ob…...

39、Flink 的窗口剔除器(Evictors)详解
Evictors Flink 的窗口模型允许在 WindowAssigner 和 Trigger 之外指定可选的 Evictor,通过 evictor(...) 方法传入 Evictor。 Evictor 可以在 trigger 触发后、调用窗口函数之前或之后从窗口中删除元素, Evictor 接口提供了两个方法实现此功能&#x…...

Flutter 中的 DefaultTabController 小部件:全面指南
Flutter 中的 DefaultTabController 小部件:全面指南 在Flutter中,DefaultTabController是一个用于管理Tab控制器的widget,它允许你控制Tab视图的初始索引和动态更新。这个组件在实现具有可滚动标签页的界面时非常有用,例如在设置…...

C++技能进阶指南——多态语法剖析
前言:多态是面向对象的三大特性之一。顾名思义, 多态就是多种状态。 那么是什么的多种状态呢? 这里的可能有很多。比如我们去买火车票, 有普通票, 学生票; 又比如我们去旅游, 有儿童票ÿ…...

Linux内存管理--系列文章肆
一、引子 上篇文章介绍了目标文件,也就是讲到编译过程中的汇编这个阶段。本篇要讲目标文件怎么变成一个可执行文件的,介绍编译过程中的链接。 链接主要分为两种,静态链接和动态链接。它们本质上的区别,是在程序的编译和运行过程中…...

kali下载zsteg和stegpy
1.kali下载zsteg 从 GitHub 上克隆zsteg到kali git clone https://github.com/zed-0xff/zsteg 切换目录 cd zsteg 用于安装名为 zsteg 的 Ruby Gem 包 gem install zsteg 2.kali下载stegpy 下载网站内的stegpy-master压缩包GitCode - 开发者的代码家园 并拉到kali中 切换到s…...