Linux进程信号详解【下】

news2024/9/23 23:35:20

🌎 Linux进程信号详【下】


文章目录:

Linux信号详解

    核心转储

    信号保存
      信号的三种状态
      信号集操作函数
        sigset_t类型接口
        sigprocmask接口
        sigpending接口
        使用场景及理解

    信号处理
      信号处理时间
      信号处理流程
      捕捉信号的其他方式

    可重入函数

    volatile关键字

    SIGCHLD信号


🚀核心转储

  在进程等待这一章节,有一张图我没有详细解释:

在这里插入图片描述
  当时在 进程等待 这一章节里我们并没有详细说明 Core dump标志,而我们通过man手册查看signal,会发现大部分的信号的作用都是 终止进程,而终止进程的动作却又分为 CoreTermtermination) 两个动作。

在这里插入图片描述

  那么它们两个有什么区别呢?实际上,在云服务器上默认将进程core退出,进行了特殊的设定,默认core是关闭的

查看core功能

  通过使用 ulimit -a 命令查看系统中的core 文件打开情况:

在这里插入图片描述

打开core功能

  要打开core功能使用 ulimit -c core_size 命令打开core dump,其中 core_size 表示指定core文件大小

在这里插入图片描述
  这个时候就开起了Linux的 Core dump 功能。当没有开起core dump功能时运行下面代码,会正常给出报错:

#include <iostream>
#include <unistd.h>
#include <signal.h>

int main()
{
    int a = 10;
    a /= 0;// SIGFPE信号

    while(true) sleep(1);
    return 0;
}

在这里插入图片描述

  未打开core dump功能时,正常报错,当打开core dump时:

在这里插入图片描述

  使用了core dump报错则会生成一个 core 文件,当然这个文件在不同的系统表现形式可能不同,在 ubuntu 下文件名为 core。而在 centos 下文件名为 core.pid 后面跟一串数字,这串数字是报错进程的进程pid。

  core文件的内容的实际上是 将进程在内存中保存的核心数据(与调试有关)转储到磁盘中形成的core文件【core dump:核心转储】。这样,当进程退出的时候我们就可以通过core定位到进程为什么退出,以及执行到哪步代码退出的。所以,core文件的作用就是帮助我们调试

  core文件可辅助调试,比如还拿上面那段除零错误代码,并且打开核心转储,生成core文件,进入gdb,使用core-file core 命令,即可查看进程出错原因:

在这里插入图片描述

  这种辅助调试被称为 事后调试方案,我们使用man 手册查看的signal手册中的所有信号只要执行动作为core都可以打开core dump进行事后调试。

  而我们云服务器中核心转储功能是默认关闭的,是为了防止未知的core dump一直进行,不断生成core文件,从而使服务器资源被占满。把core的大小设置为0即可关闭core dump功能。如果用户打开该功能忘记关闭了其实也不用太过担心,因为重启时core dump会默认关闭。


🚀信号保存

✈️信号的三种状态
  • 实际执行信号的处理动作称为 信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为 信号未决(Pending)
  • 进程可以选择 阻塞 (Block ) 某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

  其中信号递达有三个处理动作(默认执行、忽略、自定义),这个在【上】中提到过。信号未决表示信号已经写入到进程当中,但是并未处理。信号阻塞 也叫做 信号屏蔽,跟pending位图一样,会提供一个带有屏蔽数的位图,当屏蔽比特位为1则表示信号屏蔽。

在这里插入图片描述

  在上一篇说过,信号写入的位置在进程中,所以pending位图和block位图也都在task_struct中:

task_struct 
{
	unsigned int pending;//未决位图
	unsigned int block;//阻塞位图
	//...
}

  那么,如果一个信号被阻塞(屏蔽),那么这个信号将永远不会被递达,除非解除阻塞

阻塞和忽略的区别

  忽略是信号递达的一种执行动作,阻塞仅仅是不让对应的信号进行递达。形象一点理解:忽略是boss上已读不回,阻塞是根本就没看你的简历。

  经过上面的学习,我们知道task_struct 中有两张位图表,实际上在task_struct 中还有一张表:

在这里插入图片描述

  前31个信号则有31种默认处理方法,而这些默认处理方法则是调用对应的函数接口,图中SIG_DFL以及SIG_IGN等宏,都为函数指针数组的数组下标。

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

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


✈️信号集操作函数
🚩sigset_t类型接口

  有了sigset_t类型我是是否可以直接操作进程中的信号位图呢?答案是否定的,其属于内核数据结构,并不会让用户直接访问,为了支持用户访问一些位图结构,操作系统给我们提供了系统调用接口。

  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清零,表示 该信号集不包含 任何有效信号
  • 函数 sigfillset 初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号
  • 注意:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是 成功返回0,出错返回-1sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。


🚩sigprocmask接口

sigprocmask函数原型:

int sigpricmask(int how, const sigset_t *set, sigset_t *oset);
  • 作用:可读取或更改进程的信号屏蔽字(阻塞信号集)。
  • 返回值:若成功则为0,若出错则为-1。
  • set与oset指针:如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
  • how参数:用来指示更改或读取进程信号屏蔽字的动作,通常使用以下几种选项:
    在这里插入图片描述

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


🚩sigpending接口

函数原型:

int sigpending(sigset_t *set);//set为输出型参数
  • 作用:获取当前进程的pending位图。
  • 返回值:成功返回0,否则返回-1。
  • set参数:读取当前进程的未决信号,通过set传出给用户。

🚩使用场景及理解

  通过前面的学习,我们知道了信号的写入是在进程当中的,task_struct 中有三张表,分别是 未决位图,阻塞位图,对应信号的默认处理动作。我们之前也学习了 signal 接口,可以自定义信号捕捉动作,对应第三张表。而sigprocmask和sigpending接口分别对应第二和第一张表:

在这里插入图片描述

  三个接口分别控制三张表,为了更好理解这些接口,准备应用以下场景:

1、屏蔽2号信号。2、给目标进程发送2号信号。3、获取pending位图。4、打印所有pending位图中的信号。

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <signal.h>

void PrintSig(sigset_t &pending)
{
    std::cout << "Pending bitmap:";
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))//检测pending位图中的信号是否存在
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

int main()
{
    //1.屏蔽2号信号
    sigset_t block, oblock;//栈上定义的变量可能为随机值
    sigemptyset(&block);// 清空信号集
    sigemptyset(&oblock);
    sigaddset(&block, 2);// 此时并未将2号信号写入到pcb block位图中,而是在栈中
    
    // 开始屏蔽2号信号,即设置进入内核中
    int x = sigprocmask(SIG_SETMASK, &block, &oblock);
    (void)x;// 取消无返回值接收报警
    std::cout << "block 2 signal sucess" << std::endl;

    while(true)
    {  
        //2.获取进程的pending位图
        sigset_t pending;
        sigemptyset(&pending);
        x = sigpending(&pending);
        assert(x == 0);

        //3.打印pending位图中收到信号
        PrintSig(pending);
        
        sleep(1);
    }

    return 0;
}

在这里插入图片描述
注意有一些信号是不能被用户屏蔽的,9号信号19号信号 时无法被屏蔽的,而18号信号会做出特殊处理,如果手动屏蔽 18号信号 可能会释放出其他被屏蔽信号

在前面代码的基础上,我们想要将2号信号最后递达处理:

#include <iostream>
#include <unistd.h>
#include <sys/type.h>
#include <sys/wait.h>
#include <assert.h>
#include <signal.h>

void PrintSig(sigset_t &pending)
{
    std::cout << "Pending bitmap:";
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))//检测pending位图中的信号是否存在
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

int main()
{
    //1.屏蔽2号信号
    sigset_t block, oblock;//栈上定义的变量可能为随机值
    sigemptyset(&block);// 清空信号集
    sigemptyset(&oblock);
    sigaddset(&block, 2);// 此时并未将2号信号写入到pcb block位图中,而是在栈中

    std::cout << "proc pid: " << getpid() << std::endl;
    
    // 开始屏蔽2号信号,即设置进入内核中
    int x = sigprocmask(SIG_SETMASK, &block, &oblock);
    (void)x;// 取消无返回值接收报警
    std::cout << "block 2 signal sucess..." << std::endl;

    int cnt = 0;
    while(true)
    {  
        //2.获取进程的pending位图
        sigset_t pending;
        sigemptyset(&pending);
        x = sigpending(&pending);
        assert(x == 0);

        //3.打印pending位图中收到信号
        PrintSig(pending);
        cnt++;

        //4.解除2号信号的屏蔽
        if(cnt == 10)
        {
            std::cout << "unblock 2 signal sucess..." << std::endl;
            x = sigprocmask(SIG_UNBLOCK, &block, &oblock);//解除屏蔽,2号信号则会立刻递达(执行)
            assert(n == 0);
        }
        
        sleep(1);
    }

    return 0;
}

在这里插入图片描述

  10次以后,解除block位图,pending位图中2号信号立即开始递达,进程立马终止。为了不让进程立马终止,我们对2号信号进行自定义捕捉:

在这里插入图片描述

在这里插入图片描述

  这个时候的动作就变为了自定义捕捉,并且在信号解除阻塞时,pending位图会立马清零,然后再进行递达处理


🚀信号处理

✈️信号处理时间

  我们知道,用户设置的信号需要再内核中进行处理,而信号的处理时间是在 进程从内核态切换回用户态 时被处理。

  我们信号处理一般遵循下面这张表:

在这里插入图片描述
  单看这张图可能你一时半会不能很好理解,我以一个系统调用为例解释说明:

   一个程序在正常的执行自己的代码,但是突然收到一个系统调用,这时就会陷入内核执行系统调用,而执行完系统调用时并不会立即返回用户态,而是对block bitmap和pending bitmap进行遍历检测:
  如果没有信号则返回用户态。如果信号为忽略或者默认执行,那么无外乎终止或者暂停信号,则把进程杀死或者将进程的状态设置为暂停状态,并且放入等待队列中。
  如果信号为自定义捕捉,那么在内核中检测到信号需要自定义捕捉,则会切换回用户态执行捕捉函数,但是这时并不会在用户态就结束了。而是返回内核态从上次被中断的地方继续向下执行,最后再返回用户态。

  我们都知道,内核中拥有进程的代码和数据,那么这时你可能就会有疑问了:既然进程拥有我们的代码,为何还要从内核态转换为用户态再执行自定义捕捉函数呢??

  实际上,因为自定义捕捉是由用户来写的,而内核并不知道你这个用户究竟是不是病毒,会不会危害OS的安全,所以对用户默认是有害的,这样,不在内核中执行自定义捕捉,到用户态执行,就算崩溃了也会减少对操作系统的影响。

  那么信号的捕捉,可以简化为下面这张图:

在这里插入图片描述


✈️信号处理流程

  为了更好的理解信号在操作系统中从产生到执行的过程,我们有必要深入理解 用户态内核态 这两个概念。

  要理解信号的处理流程,还得从进程地址空间说起,在操作系统中,进程地址空间分为用户空间([0-3GB])和 内核空间([3-4GB])。我们知道,电脑开机时最先加载到物理内存的软件是操作系统,而进程要与操作系统产生联系,这就需要用到进程的内核空间,内核空间和操作系统由 内核级页表 进行映射

在这里插入图片描述

  操作系统中页表可分为 用户级页表内核级页表,在此之前我们所提到的页表皆是用户级页表,内核级页表用来映射OS和进程的,这样进程就可以调用操作系统的系统调用。注意,这两个页表其实是一个页表,只不过是根据其特性进行的划分

  而 系统调用的本质是 函数指针数组。而我们把这个 数组的下标 称为 系统调用号,我们使用系统调用或者访问系统数据,其实还是在进程地址空间内跳转的。

在这里插入图片描述

  而操作系统中存在许多进程,而每个进程都有自己的代码和数据,所以每个进程都拥有自己的用户级页表。而操作系统对进程来说只有一份,所以 操作系统中内核级页表也只有一个。也就是说,每个进程的地址空间0-3GB(用户级)都不一样,3-4GB(内核级)都一样,所以每个进程都可以调用系统调用

在这里插入图片描述

  以上的过程意味着,在操作系统当中,无论进程如何切换,总是能找到操作系统。所以我们所访问操作系统,其实是通过进程地址空间的3-4GB来访问OS的。

  那么操作系统又是如何运行的呢?我们前面说过,硬件中断的问题,键盘通过硬件中断被CPU的针脚识别从而调用中断向量表对应的中断方法,不过这是硬件层面。而Linux信号技术,本身就是 通过软件的方式来模拟硬件中断

  而在OS中,每隔非常短的时间,就给CPU发送中断,CPU就需要通过中断向量表不断的处理中断,这种高频间隔中断被称为 操作系统的周期时钟中断。而操作系统就是一个死循环,在不断接收外部的其他硬件中断。

在这里插入图片描述

  而我们所用的系统调用实际上也被封装过,比如我们调用read接口,则会把 系统调用号 保存到寄存器里,然后陷入内核,根据read中保存的中断方法地址,从而去执行对应的方法。说白了就是通过数组下标 调用数组元素。

  还有一个至关重要的问题,既然进程地址空间中3-4GB的空间可以直接访问OS,那么为什么我们还需要陷入内核调用呢?前面我们说过,操作系统对用户默认是不信任的,如果用户写了一段病毒,来访问3-4GB的地址空间,那不就危险了?所以,OS就必须要区分当前用户的运行模式,也就是 用户态 和 内核态。

  在CPU中有一个寄存器叫做 CS寄存器,CS寄存器最后两位比特位表示 进程的权限标志位为0表示内核态,为3表示用户态。当然具体的情况要很复杂,如果感兴趣还请自行查阅资料。


✈️捕捉信号的其他方式

  除了signal 自定义捕捉以外,Linux还提供了一种其他自定义捕捉方法:sigaction函数

在这里插入图片描述

  • 功能sigaction函数可以读取和修改与指定信号相关联的处理动作
  • 返回值调用成功则返回0,出错则返回- 1
  • signo参数signo是指定信号的编号
  • act 和 oact参数若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

在这里插入图片描述

  其中结构体中第二个成员处理的是实时信号,不需要管,第四个成员设置为0即可,第五个成员也不需要管。所以我们使用这个结构体只需要把第一个参数与第三个参数设置好即可。

  为什么会有mask参数?实际上,当某个信号的处理函数被调用时,内核自动将当前信号加入到进程的信号屏蔽字。而当我们处理完信号之后,该信号也会从阻塞状态解除。OS这么做的目的是禁止一个信号被嵌套捕捉,只允许一个信号进行串行处理。我们做个实验验证一下:

#include <iostream>
#include <signal.h>
#include <unistd.h>

void Print(sigset_t &pending)
{
    std::cout << "curr process pending: ";
    for(int sig = 31; sig >= 1; sig--)
    {
        if(sigismember(&pending, sig)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

void handler(int signo)
{
    std::cout << "signo: " << signo << std::endl; 

    //不断获取当前进程pending信号集合
    sigset_t pending;
    sigemptyset(&pending);
    while(1)
    {
        sigpending(&pending);
        Print(pending);
    }
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);// 有什么用?

    sigaction(2, &act, &oact);

    while(1) sleep(1);
    return 0;
}

在这里插入图片描述

  我们不断对2号信号进行自定义捕捉并且无间断的执行,这时我们无论怎么按Ctrl-C 都毫无相应,因为此时当前进程正在处理2号信号,2号信号被屏蔽,故别的进程无法使用2号信号。

  上面代码中还有一个疑问的点,sigaction函数照这样看来不是和signal函数没两样吗?为什会更复杂?实际上,sa_mask参数可以额外屏蔽其他信号。使用时可将需要额外屏蔽的信号设置到函数当中。


🚀可重入函数

  可重入函数与链表相关,如果数据结构还没学过的建议看一看链表。这里只是简单认识一下,具体过程将会在线程篇详细解读。

在这里插入图片描述

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。

  简单来说,就是在head节点后插入一个新节点,但是在插入过程中需要从用户态转内核态,而前面说了,进程在内核态的时候会顺便检查信号,这时刚好收到信号,执行自定义捕捉,而自定义捕捉也是在head后插入一个节点。handler完成后,main函数依旧在刚才插入那步,最后head = p,使得头结点指向第一个被插入的节点,而自定义捕捉方法内插入的节点就会丢失

  • 像上例这样,insert函数被不同的控制流程调用, 有可能在 第一次调用还没返回时就再次进入该函数,这称为 重入insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为 可重入(Reentrant) 函数

🚀volatile关键字

  如果你学过C语言或者C++,那么你一定听说过volatile关键字,但是你可能并不能记得它的具体作用。

#include <stdio.h>
#include <signal.h>

int g_flag = 0;

void changeflag()
{
    g_flag = 1;
    printf("将g_flag, 从%d->%d\n", g_flag, 1);
}

int main()
{
    signal(2, changeflag);

    while(!g_flag);// 有时候编译器会对其进行自动优化,全局变量原因

    printf("process quit normal!\n");
    return 0;
}

在这里插入图片描述

  编译时带上 -O 选项,表示优化程度,其中gcc编译器分为4个优化级别,分别是 O0, O1, O2, O3 其中 O0 表示编译时不带任何优化。

  现在的编译器可能会对一些地方进行优化,但是有时候我们并不想让其被优化,比如全局变量g_flag,现代编译器,为了优化代码,因为编译器认为全局变量访问概率大,大概率会把g_flag放置到寄存器当中,每次需要访问g_flag时,只需要从寄存器内取即可,但是今天我们需要修改g_flag的值,修改的却是内存中的g_flag的值,而保存在寄存器中的g_flag却不曾改变。

  我们对全局变量使用了volatile关键字,这样,无论编译器怎么优化,都不会影响g_flag的值了:

在这里插入图片描述
在这里插入图片描述
  这样,在怎么优化,都不会把我们预期动作改变了。


🚀SIGCHLD信号

  我们在学习进程的时候曾经说过,僵尸进程出现的原因是父进程没有回收子进程,实际上 子进程在终止时会发送 SIGCHLD 信号给父进程,而该 信号的默认处理动作是忽略,父进程可以对该信号进行自定义捕捉,这样就没必要浪费资源对子进程进行等待了。

在这里插入图片描述

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

void handler(int signo)
{
    // v1
    if(signo == SIGCHLD)
    {
        pid_t rid = waitpid(-1, nullptr, 0);
        if(rid > 0)
        {
            std::cout << "wait child sucess: " << rid << std::endl; 
        }
    }
    std::cout << "wait child sucess done" << std::endl;
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if(id == 0)
    {
        // child
        int cnt = 5;
        while(cnt --)
        {
            std::cout << "I am child process: " << getpid() << std::endl;
            sleep(1);
        }
        std::cout << "child process died" << std::endl;
        exit(0);
    }
    // father 
    while(1) sleep(1);
    return 0;
}

  我们对SIGCHLD信号做了捕捉回调,一旦子进程退出就回收子进程。

在这里插入图片描述

  这样,通过信号处理就不需要父进程在将资源用在监视子进程是否退出这件事上。但是这种代码却是一种错误的代码。

  我们说过,pending位图如果收到同一个信号多次,只会记录一次,那么如果有个场景是多个子进程在同时运行,最后子进程都结束了,发送了多次的SIGCHLD信号,但是pending位图只记录一次,所以这个时候我们只能处理一个子进程,剩下的子进程会变为僵尸。

在这里插入图片描述

  把子进程回收改为如上图所示,就解决了所有问题,无论是100个子进程退出,还是100个只有50个子进程退出,这样就都可正常将子进程回收了。


  文章到此结束,如果这篇文章对您有帮助的话还望三连~~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

会员信息管理系统-计算机毕业设计源码38258

目 录 摘要 1 绪论 1.1 研究背景 1.2 研究意义 1.3开发技术 1.3.1 Spring Boot框架 1.3.2 Java语言 1.3.3 MySQL数据库 1.4论文结构与章节安排 2系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 登录流程 2.2.2数据删除流程 2.3 系统功能分析 2.4 系统用例分析…

我的世界!

每位冒险家在《我的世界》中的出生点都各不相同&#xff0c; 有的出生在桦木森林&#xff0c;有的出生在草原&#xff0c; 还有的出生在临近海洋的沙滩。 这些环境叫做生物群系&#xff0c;也常被称为生态系统。 在《我的世界》中的不同生物群系具有不同的地域特色—— 不…

TDS传感器

目录 一、实物图 二、原理图 引脚定义 模块特性 三 、简介 四、注意事项 源文件下载 可访问底部联系方式也可前往电子校园网官网搜索关键词 关键词&#xff1a; TDS传感器 一、实物图 二、原理图 引脚定义 …

稳定、低成本、兼容性强的无线串口通信选择-适用于多场景的高性能无线串口模块

LoRa610Pro是思为无线的一款无线串口通讯模块采用了先进的LoRa扩频调制跳频技术&#xff0c;高效的接收灵敏度&#xff0c;具有超强的抗干扰性&#xff0c;还增强了通信的穿透能力和距离&#xff0c;相较于传统的FSK和GFSK产品有明显的优势。 高效的接收灵敏度 由于采用了LoRa…

学习记录day19——数据结构 查找算法

概念 在给定数据元素的某个值&#xff0c;在查找表中确定一个其关键字等于给定值的数据元素的操作&#xff0c;叫做查找 查找的分类 顺序查找:将待查找数据&#xff0c;进行全部遍历一遍&#xff0c;直到找到要查找的元素 折半查找:每次都去除一半的查找范围的查找方式&#x…

【C++高阶】哈希之美:探索位图与布隆过滤器的应用之旅

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;模拟实现unordered 的奥秘 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀哈希应用 &#x1f4…

PyQt5 + selenium,自动票务工具,演唱会门票,学习使用

PyQt5 selenium&#xff1b;在damai工具的基础上加入了UI界面&#xff0c;并将应用做了打包工作&#xff0c;主要是方便不会/不想折腾环境的用户使用&#xff0c;抢票的核心代码来自由于原作者不再维护&#xff0c;自己修改了部分代码。 安装教程 解压安装包到任意位置&…

U盘损坏无法访问?解锁两大高效数据恢复秘籍

U盘损坏之痛&#xff1a;数据失联的困境 在日常生活中&#xff0c;U盘作为数据交换与存储的重要工具&#xff0c;其便捷性无可替代。然而&#xff0c;当U盘遭遇损坏&#xff0c;无法被计算机正常访问时&#xff0c;存储在其中的宝贵数据仿佛一夜之间变得遥不可及&#xff0c;这…

关键词查找【Boyer-Moore 算法】

1、【Boyer-Moore 算法】 【算法】哪种算法有分数复杂度&#xff1f;- BoyerMoore字符串匹配_哔哩哔哩_bilibili BM算法的精华就在于BM(text, pattern),也就是BM算法当不匹配的时候一次性可以跳过不止一个字符。即它不需要对被搜索的字符串中的字符进行逐一比较&#xff0c;而…

云盘高速检测的秘密:密封圈外观检测全解析!

密封圈是一种用于填塞、隔离或密封两个相互连接部件之间空隙的圆形密封装置。密封圈通常由橡胶、塑料、金属等材料制成&#xff0c;具有弹性并能在压力作用下填充间隙&#xff0c;防止液体、气体或固体物质泄漏。 密封圈可根据具体应用选择不同材料&#xff0c;如橡胶密封圈适…

UDP网口(3)逻辑组包(下)

文章目录 1.ARP应答验证2.UDP实现思路3.UDP接收验证4.UDP发送验证5.总结与思考6.传送门 1.ARP应答验证 创建一个ARP应答工程&#xff0c;当PC发出ARP请求的时候&#xff0c;手动按下板卡指定按键&#xff0c;将会响应ARP应答。以此验证phy芯片的配置正常&#xff0c;硬件链路正…

node+mysql实现(账户密码,阿里云短信验证,QQ邮箱注册登录,短信验证密码重置,邮箱密码重置)之注册,登录密码重置总篇

node+mysql实现账户登录 注意效果图项目插件代码参数说明短信验证模块邮箱验证模块注册方式登录方式密码重置前端页面部分登录页面账户登录页面(login.html)短信验证登录页面(smsLogin.html)邮箱登录页面(emailLogin.html)注册部分页面短信验证注册页面(register.html)邮…

Profinet 转 EtherCAT 主站网关

一、功能概述 1.1 设备简介 本产品是 PN(Profinet)和 ECAT(EtherCAT)网关&#xff0c;通过数据映射方式工作。 本产品在 PN 侧作为 PN IO 从站&#xff0c;接西门子 PLC 的 Profinet 口&#xff1b;在 ECAT 侧 做为 ECAT 主站&#xff0c;接 ECAT 从站&#xff0c;如伺服驱…

懒人精灵安卓版纯本地离线文字识别插件

目的 懒人精灵是一款可以模拟鼠标和键盘操作的自动化工具。它可以帮助用户自动完成一些重复的、繁琐的任务&#xff0c;节省大量人工操作的时间。懒人精灵也包含图色功能&#xff0c;识别屏幕上的图像&#xff0c;根据图像的变化自动执行相应的操作。本篇文章主要讲解下更优秀的…

nacos2.x作为配置中心和服务注册和发现以及springcloud使用

目录 一、nacos是什么 二、windows下安装配置nacos 1、准备 2、安装nacos 3、配置nacos 4、启动并且访问nacos 三、springcloud使用nacos作为配置中心 四、springcloud使用nacos进行服务注册与发现 五、springcloud使用nacos进行服务消费 六、nacos的一些高级配置 1…

IP地址申请HTTPS证书

申请IP地址的HTTPS证书是一个相对简单但需要仔细操作的过程。选择合适的CA机构&#xff0c;明确所需证书类型&#xff0c;按照规定步骤提交申请并验证信息&#xff0c;最后正确安装和部署&#xff0c;即可实现通过IP地址访问的安全HTTPS连接。 下面是具体的申请流程&#xff0…

云盘高速视觉检测机,如何提高螺丝件的检测效率?

螺纹螺丝钉是一种常见的螺纹结构紧固件&#xff0c;通常由金属制成&#xff0c;具有螺旋状的螺纹结构。这种螺丝钉旨在通过旋入螺纹孔或材料中&#xff0c;实现可靠的固定连接。 螺纹螺丝钉具有螺旋状的螺纹结构&#xff0c;使其能够轻松旋入金属或其他硬质材料。主要用于金属…

Spring Boot 引入 Guava Retry 实现重试机制

为什么要用重试机制 在如今的系统开发中&#xff0c;为了保证接口调用的稳定性和数据的一致性常常会引入许多第三方的库。就拿缓存和数据库一致性这个问题来说&#xff0c;就有很多的实现方案&#xff0c;如先更新数据库再删除缓存、先更新缓存再更新数据库&#xff0c;又或者…

江苏省发改委副主任钱海云一行莅临我司调研指导

近日&#xff0c;江苏省发改委副主任钱海云、支援合作处副处长卢桐、调研员鲁培和一行&#xff0c;在江宁开发区管委会及市、区发改委有关负责人陪同下&#xff0c;莅临南京天洑软件有限公司走访调研。天洑软件总工程师郭阳博士携管理层参与本次调研活动。 在参观过程中&#x…

【C++】选择结构- 嵌套if语句

嵌套if语句的语法格式&#xff1a; if(条件1) { if(条件1满足后判断是否满足此条件) {条件2满足后执行的操作} else {条件2不满足执行的操作} } 下面是一个实例 #include<iostream> using namespace std;int main4() {/*提示用户输入一个高考分数&#xff0c;根据分…