【Linux】信号的保存

信号的小细节真的很多~

文章目录

  • 前言
  • 一、信号的保存
  • 总结


前言

首先我们先引出一个新的概念,叫核心转储。linux系统提供了一种能力,操作系统可以将一个进程在异常的时候将核心代码部分进行核心转储,将内存中进程的相关数据全部dump到磁盘中,一般这个文件会在当前进程的运行目录下,形成core.pid这样的二进制文件。当然如果我们使用的是云服务器的话,这个核心转储功能是默认关闭的,但是我们可以通过命令将这个功能打开:

使用命令:ulimit -a   查看当前系统中特定资源对应的上限

 而我们圈出的core file size就是核心转储的功能,默认为0就是关闭状态,想要打开使用选项:

ulimit -c 10240就是将核心转储文件的大小设置为10240.

下面我们直接用一些信号发送给正常的代码使之异常退出看是否有核心转储文件生成:

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout<<"我们的进程确实收到了"<<signo<<"号信号"<<endl;
    exit(1);
}

int main()
{
    while (true)
    {
        cout<<"我是一个正常的进程,正在模拟某种异常:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

 下面我们将程序运行起来然后发信号:

 为什么没有生成核心转储文件呢?下面我们试试其他信号:

 为什么8号信号就可以呢?还记得我们上一篇讲过信号的方式有term和core,term是终止,那么core是什么呢?其实term就是终止,以core终止会先进行核心转储,然后再终止进程。下面我们看一下刚刚的核心转储文件:

 里面全是二进制乱码,也就是说这个文件不是给我们看的是给计算机看的,下面我们说一下核心转储有什么用:其实是为了在异常后方便进行调试。

首先将代码修改一下:

 然后我们将makefile中的命令调成可调试状态:

 只需要在g++指令后面加上-g选项,下面我们重新运行一下代码:

 有了核心转储文件后下面我们用gdb进入调试模式:

然后我们直接输入指令:core-file +核心转储文件

这个时候gdb自动帮我们找到了报错的代码行数以及原因,这就验证了我们刚刚说的核心转储文件可以帮助我们在产生异常后方便进行调试,这种方案叫事后调试。当然为什么核心转储功能这么好用云服务器却要默认关闭呢?因为这个文件所占内存很大,一旦有多个出错每次都生成这样的核心转储文件那么服务器很容易挂掉,所以默认不支持打开核心转储功能,下面我们用指令将核心转储功能关闭:

我们只需要用指令:ulimit -c 0即可关闭:

 下面我们讲一下系统如何识别核心转储的打开或关闭:

 还记得我们之前讲的位图吗?我们的core标志位就在中间的那个比特位,如果这个位置的二进制为1则说明开启了核心转储功能,否则就是没有开始,要验证也很简单,只需要让子进程出现异常让父进程去接收,下面我们演示一下:

int main()
{
    pid_t id = fork();
    if (id==0)
    {
        cout<<"野指针问题 ....here"<<endl;
        cout<<"野指针问题 ....here"<<endl;
        cout<<"野指针问题 ....here"<<endl;
        int* p = nullptr;
        *p = 100;   //对空指针进行解引用
        cout<<"野指针问题........"<<endl;
        cout<<"野指针问题 ....here"<<endl;
        cout<<"野指针问题 ....here"<<endl;
        exit(0);
    }
    int status = 0;
    waitpid(id,&status,0);
    cout<<"exit code: "<<((status>>8)&0xFF)<<endl;
    cout<<"exit signal: "<<(status&0x7F)<<endl;
    cout<<"core dump flag: "<<((status>>7)&0x1)<<endl;
    return 0;
}

 以上代码能在退出后给我们打印返回值,退出信号以及core标志位:

 下面我们将核心转储重新打开我们再运行一下程序:

当我们重新运行程序后发现core dump的标志位变成了1也就是核心转储功能被打开了,并且我们成功拿到了核心转储文件:

以上就是核心转储的所有知识,下面我们进入信号的保存。 


一、信号的保存和处理

阻塞信号

实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
注意:之前的捕捉信号操作也被称为信号递达。
下面我们讲解一下信号在内核中的表示:

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号 产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
下面我们解释一下上面所表达的意思:
pending表:位图结构。比特位的位置表示哪一个信号,比特位的内容代表是否收到该信号。
比如:000010   第二个二进制位为1代表这是某个信号,整体的大小代表了是否收到该信号
block表:位图结构。比特位的位置表示哪一个信号,比特位的内容代表是否对应的信号被阻塞。
handler表:函数指针数组,如下图:

这个指针数组里存放的函数指针就是上图这样的,该数组的下标表示信号编号,数组的特定下标的内容表示该信号的递达动作。

下面我们看看信号递达的动作,比如执行和忽略:

int main()
{

    signal(2,SIG_DFL);
    while (true)
    {
        sleep(1);
    }
    return 0;    
}

 首先DFL的意思是默认,默认就是执行了意思就是说遇到2号信号就执行,下面我们运行一下:

 键盘输入ctrl+c后就执行了2号信号,下面我们查看SIG_DFL的宏:

 通过函数定义我们看到这个宏就是用函数指针实现的,下面我们试试忽略信号:

int main()
{

    signal(2,SIG_IGN);
    while (true)
    {
        sleep(1);
    }
    return 0;    
}

IGN是ignore的缩写,就是忽略的意思,就是说我们遇到2号信号就忽略:

代码运行后确实将2号信号忽略了,然后我们用ctrl + \退出程序。

下面我们认识一下sigset_t:

从上图来看,每个信号只有一个bit的未决标志,01,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效无效状态,在阻塞信号集中有效无效的含义是该信号是否被阻塞,而在未决信号集中有效”无效的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的屏蔽应该理解为阻塞而不是忽略。

 

认识了信号集后我们学习一下如何用信号集操作函数:

sigset_t 类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做任何解释, 比如用 printf 直接打印 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(const sigset_t *set, int signo);
函数 sigemptyset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 清零 , 表示该信号集不包含任何有效信号。
函数 sigfifillset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 置位 , 表示该信号集的有效信号包括系统支持的所有信号。
注意 , 在使用 sigset_ t 类型的变量之前 , 一定要调用 sigemptyset sigfifillset 做初始化 , 使信号集处于确定的状态。初始化sigset_t 变量之后就可以在调用 sigaddset sigdelset 在该信号集中添加或删除某种有效信号
以上这四个函数都是成功返回 0, 出错返回 -1 sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1
下面我们认识一下sigprocmask这个系统调用函数:
调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字 ( 阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果osetset都是非空指针,则先将原来的信号屏蔽字备份到oset,然后根据sethow参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值:

 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

下面我们演示一下对2号信号进行屏蔽:

void showBlock(sigset_t *oset)
{
    int signo = 1;
    for (;signo<=31;signo++)
    {
        if (sigismember(oset,signo))
        {
            cout<<"1";
        }
        else 
        {
            cout<<"0";
        }
    }
    cout<<endl;
}
int main()
{
    //只是在用户层面上进行设置
    sigset_t set,oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set,2);
    //设置进入进程,谁调用,设置谁
    sigprocmask(SIG_SETMASK,&set,&oset); //1.2号信号没有反应2.我们看到老的信号屏蔽字block位图是全0
    while (true)
    {
        showBlock(&oset);
        sleep(1);
    }
    return 0;
}

下面我们先讲解一下代码,首先进入main函数我们创建两个对象,因为创建的对象或变量是在栈中存放,所以我们只是在用户层面上进行设置。然后我们先将新的信号集和旧的信号集初始化一下,初始化完成后将2号信号添加到新的信号集上去,这样就相当于屏蔽了2号信号。然后我们设置信号屏蔽字为set所指向的值就是谁调用这个进程谁就将二号信号屏蔽了,然后这个函数返回值返回老的信号屏蔽字,但是由于我们已经将信号屏蔽字初始化了所以老的信号屏蔽字block位图全是0.然后我们写一个死循环去显示老的信号屏蔽字的位图有哪些信号被设置了有哪些信号没有被设置,在这个函数中我们知道一共有31个信号,并且需要判断当前这个信号是否在老的信号集里,如果是就打印1如果不是就打印0,sigismember能判断signo这个信号是否在老的信号集里。

下面我们运行起来:

 通过结果我们可以知道与我们所想的是一样的,下面我们修改一下代码将信号屏蔽字取消屏蔽:

int main()
{
    //只是在用户层面上进行设置
    sigset_t set,oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set,2);
    //设置进入进程,谁调用,设置谁
    sigprocmask(SIG_SETMASK,&set,&oset); //1.2号信号没有反应2.我们看到老的信号屏蔽字block位图是全0
    int cnt = 1;
    while (true)
    {
        showBlock(&oset);
        sleep(1);
        cnt++;
        if (cnt==10)
        {
            sigprocmask(SIG_SETMASK,&oset,&set);
        }
    }
    return 0;
}

我们设置一个计数器让计数器等于10的时候将进程的信号集恢复为oset也就是取消屏蔽2号信号,现象就是一开始我们按ctrl+c是没有反应的,但是到cnt==10的时候2号屏蔽字恢复直接就终止程序了,下面我们看看现象:

 我们可以看到现象与我们想的完全一样。

下面我们在认识一个新的接口:

 sigpending函数是获取set的pending表,也就是说可以知道哪些信号是未决的,下面我们看看返回值:

 如果成功则返回0如果失败返回-1,下面我们用函数重新写一下上面的代码并且引入新现象:

#include <iostream>
#include <signal.h>
#include <assert.h>
#include <unistd.h>
using namespace std;
static void PrintPending(const sigset_t &pending)
{
    for (int signo=1;signo<=31;signo++)
    {
        //sigsimember可以判断signo信号是否在pending中存在
        if (sigismember(&pending,signo))
        {
            cout<<"1";
        }
        else 
        {
            cout<<"0";
        }
    }
    cout<<endl;
}
int main()
{
    //1.屏蔽2号信号
    sigset_t set,oset;
    // 1.1初始化
    sigemptyset(&set);
    sigemptyset(&oset);
    // 1.2将2号信号添加到set中
    sigaddset(&set,2);
    // 1.3将新的信号屏蔽字设置至进程
    sigprocmask(SIG_BLOCK,&set,&oset);
    //2. while获取进程的pending信号集合,并以01打印
    while (true)
    {
        // 2.1 先获取pending信号集
        sigset_t pending;
        //初始化pending
        sigemptyset(&pending);
        int n = sigpending(&pending);
        assert(n==0);
        (void)n; //保证在release模式下不会出现编译时的warning
        // 2.2 打印,方便我们查看
        PrintPending(pending);
        //2.3休眠一下
        sleep(1);
    }
    return 0; 
}

这段代码的现象是:因为有block和pending位图,当我们将某个信号block后这个信号就不会被递答了,然后我们给进程发送这个信号,一旦发送那么这个信号在pending表中的比特位就会被修改为1,然后我们就可以看到pending表中2号信号位由0变1的过程。

上面的代码与前面那个演示代码非常相似,并且该注释的我们都注释了,下面我们直接运行起来看看现象:

 现象和我们说的一样,当然我们输入ctrl+c也是一样的。因为我们将2号信号进行了屏蔽,即使我们发送了2号信号但是2号信号不会被递达,只能留在pending位图里,下面我们让代码在10秒后恢复被屏蔽的信号并且必须看到pending位图从1变成0:

  //2. while获取进程的pending信号集合,并以01打印
    int cnt = 0;
    while (true)
    {
        // 2.1 先获取pending信号集
        sigset_t pending;
        //初始化pending
        sigemptyset(&pending);
        int n = sigpending(&pending);
        assert(n==0);
        (void)n; //保证在release模式下不会出现编译时的warning
        // 2.2 打印,方便我们查看
        PrintPending(pending);
        //2.3休眠一下
        sleep(1);
        //2.4 10s之后,恢复对所有信号的block动作
        if (++cnt==10)
        {
            cout<<"解除对2号信号的屏蔽"<<endl;   //先打印
            sigprocmask(SIG_SETMASK,&oset,nullptr);
        }
    }

由于我们在倒计时结束后已经将之前老的信号屏蔽字设置为进程,所以这次我们不需要老的信号屏蔽字了直接设为nullptr即可。下面我们看一下现象:

 为什么与我们预期的不一样呢,我们想要看到的pending表呢?这是因为2号信号由阻塞状态修改为解除屏蔽后2号信号直接终止进程了,所以我们是看不到现象的,要看到现象我们需要对2号信号进行捕捉:

static void handler(int signo)
{
     cout<<"拦截到"<<signo<<"信号"<<endl;
}

signal(2,handler);
    while (true)
    {
        // 2.1 先获取pending信号集
        sigset_t pending;
        //初始化pending
        sigemptyset(&pending);
        int n = sigpending(&pending);
        assert(n==0);
        (void)n; //保证在release模式下不会出现编译时的warning
        // 2.2 打印,方便我们查看
        PrintPending(pending);
        //2.3休眠一下
        sleep(1);
        //2.4 10s之后,恢复对所有信号的block动作
        if (++cnt==10)
        {
            cout<<"解除对2号信号的屏蔽"<<endl;   //先打印
            sigprocmask(SIG_SETMASK,&oset,nullptr);
        }
    }

下面我们将程序运行起来:

 可以看到这次的现象就与我们预期的现象一样了,一开始将2号信号进行了阻塞,然后当我们发送2号信号的时候信号保存在pending表中,等10s后解除2号信号的屏蔽了然后我们立即捕捉这个信号,然后循环继续打印pending表,此时2号信号已经递达所以2号信号的位置由1变成0.


总结

以上就是信号的保存的内容,现在我们已经学会了信号的产生,信号的保存,下一篇文章我们将详细介绍信号的处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/18929.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

写文章的软件-一款写作文章的软件

写文章的软件 写文章的软件是一种工具&#xff0c;可以帮助用户轻松快速地创作高质量的文章。该软件一般包括以下几个主要功能&#xff1a; 写作界面&#xff1a;提供简洁、美观的写作界面&#xff0c;让用户专注于文章创作&#xff0c;同时可以进行排版、字体、颜色等调整。 …

函数(1)

文章目录 目录1. 函数是什么2. 库函数3. 自定义函数4. 函数的参数4.1 实际参数&#xff08;实参&#xff09;4.2 形式参数&#xff08;形参&#xff09; 5. 函数的调用5.1 传值调用5.2 传址调用5.3 练习 附&#xff1a; 目录 函数是什么库函数自定义函数函数的参数函数的调用函…

消息队列中的事务消息

大家好&#xff0c;我是易安&#xff01;今天我们谈一谈消息队列中的事务消息这个话题。 一说起事务&#xff0c;你可能自然会联想到数据库。我们日常使用事务的场景&#xff0c;绝大部分都是在操作数据库的时候。像MySQL、Oracle这些主流的关系型数据库&#xff0c;也都提供了…

电阻阻值读取方法、电容容值的读取方法

电阻、电容的数值读取方法 文章目录 电阻、电容的数值读取方法前言1、电阻读数1.1 贴片电阻1.2.直插色环电阻 2、电容读数2.1 电容单位换算2.2 电容读数方法 前言 现在随着电子产品的不断升级优化&#xff0c;做到体积越来越小了&#xff0c;以前发现还是用得很多直插电阻和一…

GDD471A001 PLC / DCS维护日志

​ GDD471A001 PLC / DCS维护日志 PLC维护日志 PLC/DCS 维护日志将帮助您跟踪过去的故障、解决方案、零件更换。如果以后再次出现同样的问题&#xff0c;跟踪日志将帮助您立即解决。 您的控制系统的可靠性可以通过参考维护日志来确定。 使用 PLC/DCS 维护日志可以识别频繁出…

WGCNA | 不止一个组的WGCNA怎么分析嘞!?~(一)(共识网络分析-第一步-数据整理)

1写在前面 最近又是忙碌的一米&#xff0c;做不完的手术&#xff0c;收不完的病人&#xff0c;前途堪忧&#xff0c;收入更是不堪入目。&#x1f972; 把之前的WGCNA教程再补一补吧&#xff0c;之前介绍的是雌性鼠的表型数据分析&#xff0c;只有一组&#xff0c;相对简单。&am…

炫技亮点 任务编排使用CompletableFuture优化业务流程

文章目录 背景CompletableFuture简介使用场景如何编排任务步骤场景一 多个任务串行执行场景二 多个步骤并行执行场景三 一个串行步骤后两个并行步骤场景四 一个步骤依赖两个并行步骤场景五 一个步骤依赖多个并行步骤同时完成场景六 一个任务依赖多个任务的任意一个完成结果 其他…

全景丨0基础学习VR全景制作,平台篇第19章:热点功能-文本

大家好&#xff0c;欢迎观看蛙色VR官方——后台使用系列课程&#xff01; 功能说明 应用场景 热点&#xff0c;指在全景作品中添加各种类型图标的按钮&#xff0c;引导用户通过按钮产生更多的交互&#xff0c;增加用户的多元化体验。 文本热点&#xff0c;即点击热点后会弹出…

【二叉搜索树】

1 二叉搜索树概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树 &#xff0c;或者是具有以下性质的二叉树 : 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值 它的左…

COCO数据集相关知识介绍

&#x1f468;‍&#x1f4bb;个人简介&#xff1a; 深度学习图像领域工作者 &#x1f389;总结链接&#xff1a; 链接中主要是个人工作的总结&#xff0c;每个链接都是一些常用demo&#xff0c;代码直接复制运行即可。包括&#xff1a; &am…

Java 8 Time 关于java.time包中你可能不知道的使用细节

目录 前言一、时区与时间1. 世界标准时&#xff1a;UTC、GMT、UT2. 地区时&#xff1a;Asia/Shanghai、UTC83. 时区&#xff1a;ZoneId、TimeZone4. 时间偏移量&#xff1a;ZoneOffset5. 时区简称&#xff1a;CTT、PRC 二、主要时间类1. 重要时间接口&#xff1a;Temporal2. 时…

腾讯云轻量16核32G28M带宽服务器CPU流量性能测评

腾讯云轻量16核32G28M服务器3468元15个月&#xff0c;折合每月231元&#xff0c;28M公网带宽下载速度峰值可达3584KB/s&#xff0c;折合3.5M/秒&#xff0c;系统盘为380GB SSD盘&#xff0c;6000GB月流量&#xff0c;折合每天200GB流量。腾讯云百科来详细说下腾讯云轻量应用服务…

13 | visual studio与Qt的结合

1 前提 Qt 5.15.2 visual studio 2019 vsaddin 2.8 2 具体操作 2.1 visual studio tool 2.1.1 下载 https://visualstudio.microsoft.com/zh-hans/downloads/2.1.2 安装 开发

10_Uboot启动流程_2

目录 _main函数详解 board_init_f函数详解 relocate_code函数详解 relocate_vectors函数详解 board_init_r 函数详解 _main函数详解 在上一章得知会执行_main函数_main函数定义在文件arch/arm/lib/crt0.S 中,函数内容如下: 第76行,设置sp指针为CONFIG_SYS_INIT_SP_ADDR,也…

信息抽取与命名实体识别:从原理到实现

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

君子签:助力高校毕业、就业协议电子签,打通就业最后一公里

据介绍&#xff0c;2023届全国普通高校毕业生规模预计达1158万人&#xff0c;同比增加82万人。毕业季即将来临&#xff0c;全国各大高校毕业、就业材料签署压力大&#xff0c;盖章需求激增&#xff0c;如何快捷、高效地处理各类毕业、就业材料签署问题呢&#xff1f; 在教育部…

2023 年第八届数维杯数学建模挑战赛 赛题浅析

为了更好地让大家本次数维杯比赛选题&#xff0c;我将对本次比赛的题目进行简要浅析。本次比赛的选题中&#xff0c;研究生、本科组请从A、B题中任选一个 完成答卷&#xff0c;专科组请从B、C题中任选一个完成答卷。这也暗示了本次比赛的难度为A>B>C 选题人数初步估计也…

解决APP抓包问题「网络安全」

1.前言 在日常渗透过程中我们经常会遇到瓶颈无处下手&#xff0c;这个时候如果攻击者从APP进行突破&#xff0c;往往会有很多惊喜。但是目前市场上的APP都会为防止别人恶意盗取和恶意篡改进行一些保护措施&#xff0c;比如模拟器检测、root检测、APK加固、代码混淆、代码反调试…

程序员痛心流涕自述:“因为把自己代码给了别人,我亲手断送了自己的前程”

在求职的过程中&#xff0c;一般都会有投递简历、笔试、面试以及背调的环节&#xff0c;而在这几个环节中折戟沉沙的人也着实不少。 不少人觉得&#xff0c;在求职时简历需要优化&#xff0c;背调不能有瞒报、捏造的情况&#xff0c;而笔试面试则是纯纯的要靠硬实力。 虽然说…

Springboot +Flowable,服务任务ServiceTask执行的三种方式(二)

一.简介 ServiceTask 从名字上看就是服务任务&#xff0c;它的图标是像下面这样&#xff0c;截图如下&#xff1a; ServiceTask 一般由系统自动完成&#xff0c;当流程走到这一步的时候&#xff0c;不会自动停下来&#xff0c;而是会去执行我们提前在 ServiceTask 中配置好的…