【Linux】八、Linux进程信号详解(一)

news2024/12/23 14:23:17

目录

一、认识信号

1.1 生活中的信号

1.2 将1.1的概念迁移到进程

1.3 信号概念

1.4 查看系统定义信号列表

1.5 man 7 signal

1.6 解释1.2的代码样例

1.7 信号处理常见方式概览

二、产生信号

2.1 signal函数

2.2 通过终端按键产生信号

2.3 调用系统函数向进程发信号

2.3.1 kill函数

2.3.2 raise函数

2.3.3 abort函数

2.4 硬件异常产生信号

2.4.1 除0操作产生的异常 

2.4.2 空指针异常

2.5 由软件条件产生信号


一、认识信号

1.1 生活中的信号

日常生活中,常见的信号有:发令枪、红绿灯、消息提醒、电话铃声、闹钟等等,以快递为例。

  1. 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”;
  2. 识别快递包含了两个信息:1、认识:你是认识 “快递” 的  2、行为产生:“快递” 到来,你会产生相应的行为;
  3. 当 “快递” 到来,快递小哥给你打电话,但是你正在打游戏,需5min之后才能去 “取快递”。那么在在这5min之内,你并没有去取快递,但是你是知道有快递到来了。也就是 ”取快递” 的行为并不是一定要立即执行,可以理解成 “在合适的时候去取”;
  4. 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你 “记住了有一个快递要去取”;
  5. 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你的女朋友)3. 忽略快递(快递拿上来之后,放在一旁,继续开一把游戏);
  6. 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。

快递便可视为信号,对信号的处理大致分以下三步:

  1. 识别信号:你认识这个信号,信号到来你会产生相应的行为;
  2. 信号到来(即将处理信号):不一定立即处理信号,也可以在某个合适时间进行处理信号,但是必须要把这个信号记住;
  3. 拿到信号(处理信号):拿到信号后分三种行为 (1、默认动作:拿到信号后使用信号  2、忽略动作:拿到信号后忽略信号,什么事也不干  3、自定义动作:拿到信号去干别的事)

解释异步概念:

以生活例子为例:你在煮着面条。当你在煮面条时,你可以同时做其他事情,例如准备酱料或者切菜。你不需要一直站在炉子旁边等待面条煮熟,而是可以在面条煮熟之前做其他事情,这就是异步;

而同步则是:你必须等待面条煮熟了,你才能干其他事,这是同步。

以信号为例:假设你正在等待信号的到来。在同步中,你会等待信号到达后再继续下一个任务,这意味着你会阻塞其他任务直到信号到达。

另一方面,在异步中,你不会等待信号到达,这意味着在等待信号到来的时可以做其他任务。

1.2 将1.1的概念迁移到进程

首先知道,信号是给进程发送的,比如我们之前的 kill -9 ,给进程发送9号信号终止进程。

  • 进程是如何识别信号:也是 认识信号 + 行为动作(进程之所以能够认识信号,是因为程序员将对应的信号种类和逻辑已经写好了);
  • 进程收到信号时:进程不一定立即处理这个信号,进程可能干着其他更重要的事情;
  • 进程立即处理 或 不一定立即处理这个信号的时候,进程本身必须要有对信号的保存能力;
  • 进程处理信号的时候,一般有三个动作(默认、忽略、自定义),进程处理信号称为信号被捕捉。

进程本身必须要有对信号的保存能力,信号保存在哪里?答案是保存在进程的PCB里面,即task_struct。 

信号如何保存?是否收到了指定的信号,是否即两态:二进制表示非0即1,即用1代表收到了信号,0则代表没有收到信号

这种结构称为位图结构,之前有过相关解释,C++专栏也有

指定的信号在Linux是:

用 kill -l 命令可以察看系统定义的信号列表

kill -l 

其中 [1, 31] 号信号是普通信号,[34, 64] 号信号是实时信号(信号学习中,这里我们只学习普通信号) 

即普通信号就可以用32个比特位来表示,即四字节的整型,即在 task_struct 里面一定存在一个字段 unsigned int

struct task_struct
{
	//进程属性
    //.......
    
	unsigned int signal;

	//.......
}

这个字段可以表示所有的普通信号:

第一个比特位代表 1号信号,以此类推(位图结构)

如何理解信号的发送?也就是发送信号的本质

发送信号的本质就是:修改PCB中的信号位图,即task_struct 的信号位图,也就是上面图中所说的位图,比如发送1号信号,发送1号信号就是把信号位图的第一个比特位由0置1

而 task_struct 是内核维护的一种数据结构对象,所以 task_struct 的管理者是OS,只有OS才有权利修改 task_struct 里面的内容,所以以此推导:无论在未来学习多少种发送信号的方式,本质都是通过OS向目标进程发送信号(谁都没有权利修改OS内的数据结构,只有OS自己可以)

所以我们用户要操作信号,OS必须提供发送信号、处理信号的相关系统调用

 比如之前一直使用的 kill 命令,底层一定调用了对应的系统调用

以代码展示信号:

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

int main()
{
    while(true)
    {
        cout << "我是一个进程,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

该程序的运行结果就是死循环地进行打印,而对于死循环来说,常用方式就是使用 Ctrl+C 终止进程

为什么使用 Ctrl+C 后,该进程就终止了? 

实际上当用户按Ctrl+C时,这个键盘输入会产生一个硬中断,被操作系统获取并解释成信号(Ctrl+C被解释成2号信号),然后操作系统将2号信号发送给目标前台进程,当前台进程收到2号信号后就会退出

2号信号是:SIGINT

这里只是简单介绍,下面详细解释 

1.3 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断

1.4 查看系统定义信号列表

kill -l 命令可以察看系统定义的信号列表

kill -l

1.2 有过大概解释,这不介绍了,我们可以使用信号数字的编号,也可以直接使用宏定义,比如2号信号,我们可以使用 2 也可以使用 SIGINT

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signum.h 中找到,例如其中有定 义 #define SIGINT 2

查看 signum.h

1.5 man 7 signal

普通信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明,直接看文档即可

man 7 signal 查看:

普通信号的默认动作: 

1.6 解释1.2的代码样例

man 7 signal 查看二号信号的作用:

Interrupt from keyboard:从键盘获取中断
默认动作:Term
Term: Default action is to terminate the process.
翻译:默认操作是终止进程

 所以,到这里我们就知道为什么 2号信号可以终止进程

我们可以使用signal函数对2号信号进行捕捉,证明当我们按 Ctrl+C 时进程确实是收到了2号信号。使用 signal 函数时,我们需要传入两个参数,第一个是需要捕捉的信号编号,第二个是对捕捉信号的处理方法,该处理方法的参数是int,返回值是void

注:signal 函数这里是使用,详细下面才解释,这里只是演示

下面的代码中将2号信号进行了捕捉,当该进程运行起来后,若该进程收到了2号信号就会打印出收到信号的信号编号

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

void handler(int signo)
{
    cout << "捕捉到一个信号,该信号编号是:" << signo << endl;
}

int main()
{
    signal(2, handler);
    while(true)
    {
        cout << "我是一个进程,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

此时当该进程收到2号信号后,就会执行我们给出的handler方法,而不会像之前一样直接退出了,因为此时我们已经将2号信号的处理方式由默认改为了自定义了(默认行为:结束进程  -> 变成 自定义行为:handler方法)

进程是无法 Ctrl+C 结束进程,直接发送 9 号信号终止进程,kill -9 进程pid

注意: 

  1. Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的

1.7 信号处理常见方式概览

信号处理动作有以下三种,也就是上面概念所提到的

  1. 默认:执行该信号的默认处理动作;
  2. 忽略:忽略此信号;
  3. 自定义:提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号

二、产生信号

前面第一大点都是信号预备知识,这里第二大点讲的是信号产生

2.1 signal函数

对上面使用signal函数进行补充:signal函数的作用是用于处理信号(自定义行为),对信号进行捕捉 

man 2 signal 查看一下

signal

头文件:
 #include <signal.h>

函数原型:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:
    第一个参数signum:需要捕捉的信号编号
    第二个参数handler:对信号自定义的行为,对捕捉信号的处理方法(函数),handler是一个回调函数,该处理方法的参数是 int,返回值是void
    
    sighandler_t是一个函数指针

2.2 通过终端按键产生信号

测试代码

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

int main()
{
    while(true)
    {
        cout << "我是一个进程,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

该代码是一个死循环,前面已经说过可以使用 Ctrl+C 来终止进程,发送的信号是 2号信号 SIGINT

小提示:进程运行了,该进程就变成了前台进程(命令行在当前进程无效),bash就会变成后台进程,进程结束后,bash又会变回前台进程,命令行生效 

实际上除了按 Ctrl+C 之外,按 Ctrl+\ 也可以终止该进程

按 Ctrl+C 和按 Ctrl+\ 都可以终止进程,但是两者有什么区别?

Ctrl+C 发送的信号是2号信号 SIGINT,Ctrl+\ 发送的信号是3号信号 SIGQUIT

这两个信号的默认行为(Action)不一样:2号信号默认行为是 Term,3号信号默认行为是 Core

Term 上面解释过了,不解释了

Core 在终止进程的时候会进行一个动作,那就是核心转储

Default action is to terminate the process and dump core (see core(5)).

dump core:核心转储

什么是核心转储

Term 把进程终止了就不做其他工作了,核心转储Core 把进程终止后还做其他的工作

注意:在云服务器中,核心转储是默认被关掉的,我们需要打开才能观察到现象

以通过使用 ulimit -a 命令查看当前资源限制的设定

其中,第一行显示core文件的大小为0,即表示核心转储是被关闭的

我们可以通过 ulimit -c size命令来设置core文件的大小,即打开核心转储

 

core文件的大小设置完毕后,就相当于将核心转储功能打开了

再次运行上面的程序,Ctrl+\把进程终止(发送3号信号,默认动作Core),现象就会出现:core dumped

 

并且会在当前路径下生成一个core文件,该文件以一串数字为后缀,而这一串数字实际上就是发生这一次核心转储的进程的PID

 

 核心转储就是:当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据转储到磁盘中,也就是上面的文件

那么核心转储有什么用??

当我们的代码出错了,我们最关心的是我们的代码是什么原因出错的。如果我们的代码运行结束了,那么我们可以通过退出码来判断代码出错的原因,而如果一个代码是在运行过程中出错的,那么我们也要有办法判断代码是什么原因出错的

当我们的程序在运行过程中崩溃了,我们一般会通过调试来进行逐步查找程序崩溃的原因。而在某些特殊情况下,我们会用到核心转储,核心转储指的是操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及有关进程状态的其他信息转而存储到一个磁盘文件当中,这个磁盘文件也叫做核心转储文件(支持调试

可以使用gdb进行调试

为了方便演示,使用 2.4.2 的空指针例子演示,代码如下:

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

int main()
{
    while(true)
    {
         cout << "我是一个进程,正在运行中,pid: " << getpid() << endl;
        sleep(2);

        //空指针(野指针)
        int *p = nullptr;
        *p = 10;
    }
    return 0;
}

 注:Linux默认是release,调试需要增加 -g选项

 

运行结果

 

 使用gdb对当前可执行程序进行调试

gdb 可执行程序, 进入调试

然后直接使用 core-file 核心转储文件 命令加载 core文件,即可判断出该程序在终止时收到了11号信号,并且定位到了产生该错误的具体代码,错误信息也详细列出

core-file core.11467

 事后用调试器检查core文件以查清错误原因,这种调试方式叫做事后调试

这就是 Term 和 Core 的区别,Term是正常终止进程 

2.3 调用系统函数向进程发信号

2.3.1 kill函数

kill函数是一个系统调用,kill命令就是通过 kill函数来实现的

测试 kill命令 

使用kill命令向一个进程发送信号时,我们可以用 kill -信号编号 进程pid 的形式进行发送信号

kill函数的作用是:向目标进程发送指定信号

man 2 kill 查看:

函数:kill

头文件
#include <sys/types.h>
#include <signal.h>

函数原型:
int kill(pid_t pid, int sig);

参数
    第一个参数pid就是进程的pid
    第二个参数sig是信号的编号

返回值
发送成功,返回0,否则返回-1

使用 kill函数模拟 kill命令(mykill):

#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <string>
#include <unistd.h>
using namespace std;

//使用手册
static void Usage(const string& proc)
{
    cout << "\nUsage: " << proc << " pid signo\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    //把字符串转整型
    pid_t id = atoi(argv[1]);
    int signo = atoi(argv[2]);

    int n = kill(id, signo);
    if(n != 0)
    {
        perror("kill");
    }

    return 0;
}

另一个测试代码,死循环,test.cc

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

int main()
{
    while(true)
    {
        cout << "我是一个进程,pid: " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

选运行死循环测试代码,再使用 mykill杀掉死循环,这样就实现了一个 kill命令

2.3.2 raise函数

raise函数的作用是:给自己发送信号

man 3 raise 查看:

函数:raise

头文件
#include <signal.h>

函数原型
int raise(int sig);

参数:sig是要发送的信号编号

返回值
发送成功,则返回0,否则返回一个非零值

这个函数也可以通过 kill函数实现:kill(getpid(), sig) 

测试代码:

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

int main()
{
    int cnt = 0;
    while(true)
    {
        cout << "我是一个进程,pid: " << getpid() << " cnt: " << cnt++ << endl;
        sleep(1);
        //cnt>=5,就给自己发送3号信号
        if(cnt >= 5)
            raise(3);
    }

    return 0;
}

运行结果

2.3.3 abort函数

abort函数用于给自己发送指定信号:6号信号SIGABRT,6号信号的默认动作也是终止进程

man 3 abort 查看

  

函数:abort

头文件
#include <stdlib.h>

函数原型
void abort(void);

 这个函数也可以通过 kill函数实现:kill( getpid(),  SIGABRT 

 测试代码:

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

int main()
{
    int cnt = 0;
    while(true)
    {
        cout << "我是一个进程,pid: " << getpid() << " cnt: " << cnt++ << endl;
        sleep(1);
        //cnt>=5,就给自己发送指定信号
        if(cnt >= 5)
            abort();
    }

    return 0;
}

运行结果

 

abort函数总是会成功的,所以没有返回值

进程收到的大部分信号,默认动作都是终止进程

2.4 硬件异常产生信号

信号的产生,不一定非得用户显示发送,有些信号会在OS内部自动产生,比如硬件异常产生的信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号 

2.4.1 除0操作产生的异常 

比如进行进程除0操作,VS会直接报错终止进程

下面在g++下进行测试,测试代码如下

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

int main()
{
    while(true)
    {
        cout << "我在运行中..." << endl;
        sleep(1);

        //除0操作
        int a = 10;
        a /= 0;
    }
    
    return 0;
}

运行结果,编译警告不用理会

 为什么除0会终止进程??

因为进程收到了来自OS的信号,该信号是SIGFPE,8号信号

 

该信号的默认动作也是 Core,也是终止进程

 Floating point exception:浮点异常

 下面对该信号进行捕捉,捕捉后进行自定义动作

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

//自定义行为
void handler(int signo)
{
    cout << "捕捉到一个信号,该信号编号是:" << signo << endl;
}

int main()
{
    signal(8, handler);
    while(true)
    {
        cout << "我在运行中..." << endl;
        sleep(1);

        //除0操作
        int a = 10;
        a /= 0;
    }

    return 0;
}

 运行结果

 运行结果说明,进程确实收到了8信号,可是为什么一直对该信号一直捕获??OS为什么一直发送8号信号???

下面讲解对于除0的理解:

 在CPU中有很多的寄存器,例如eax,ebx,eip等等

CPU会将代码中的变量拿到寄存器中进行运算,如果有需要,运算结果需要返回

 进行对两个数进行算术运算时,我们是先将这两个操作数分别放到两个寄存器当中,然后进行算术运算并把结果写回寄存器当中

(这里就简单略过具体的寄存器,方便理解,图也是简略化)

CPU当中还有一组寄存器叫做状态寄存器,它可以用来标记当前指令执行结果的各种状态信息,如有无进位、有无溢出等等

除0操作,对于计算机来说,是除一个无穷小的数,得到的结果是无穷大的数,寄存器存不下这个数,这时候状态寄存器的溢出标志位由0置1,这时CPU就会发出运算异常的信号,OS就会识别到这个异常,OS就会给指定的进程发送这个信号,这个信号就是8号信号,CPU报的这个异常归属于硬件异常,OS检测到,由OS主动发送给目标进程

接下来解释为什么会一直死循环捕获到8号信号?? 

 代码在CPU中执行的时候,此时CPU内的寄存器的内容属于该进程的上下文数据,因为寄存器只有一份,代码没有运行完,当该进程的时间片到了,就会从CPU上切下去,该进程的上下文数据也被会保存,包括溢出标志位

进程被来回切换,就有无数次寄存器的内容被保存会恢复的过程,所以每次恢复的时候,OS都会识别到CPU内部的状态寄存器溢出标志位为1,OS就会给该进程发送8号信号

由于我们把该信号捕获了,执行自定义行为,该信号的默认行为就不会执行,每次恢复的时候,进程还没有被终止,OS依旧会识别到CPU内部的状态寄存器溢出标志位为1,就又发8号信号给该进程,以此往复,就陷入死循环,所以我们会看到8号信号一直被捕获

2.4.2 空指针异常

空指针(野指针)问题在程序中可能会遇到,空指针VS直接崩溃,终止程序,下面在g++下测试

测试代码:

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


int main()
{
    while(true)
    {
         cout << "我是一个进程,正在运行中,pid: " << getpid() << endl;
        sleep(2);

        //空指针(野指针)
        int *p = nullptr;
        *p = 10;
    }

    return 0;
}

运行结果

 

结果发现,空指针操作进程直接被终止了,为什么进程会被终止?

因为进程收到了来自OS的信号,该信号是11号信号 SIGSEGV

该信号的默认动作也是 Core,也是终止进程

Invalid memory reference:无效内存引用
Segmentation fault:段错误

 下面对该信号进行捕捉,捕捉后进行自定义动作

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

//自定义行为
void handler(int signo)
{
    cout << "捕捉到一个信号,该信号编号是:" << signo << endl;
}

int main()
{
    signal(11, handler);
    while(true)
    {
         cout << "我是一个进程,正在运行中,pid: " << getpid() << endl;
        sleep(2);

        //空指针(野指针)
        int *p = nullptr;
        *p = 10;
    }

    return 0;
}

运行结果 

 

 OS怎么知道空指针异常??

我们必须知道的是,当我们要访问一个物理内存时,一定要先经过页表的映射,将虚拟地址转换成物理地址,然后才能进行相应的访问操作

在从虚拟地址映射到物理地址的过程中,必须经过页表,页表上有一个硬件叫做MMU,MMU被集成在CPU里面,MMU用于计算映射关系,在MMU算出物理内存的映射关系之后,CPU可以直接进行物理内存访问

当我们要访问不属于我们的虚拟地址时(访问空地址,空指针的使用,空地址是不允许访问的),MMU在进行虚拟地址到物理地址的转换时就会出现错误,MMU就会出现异常,OS在这时也会识别这个异常,然后OS就会向目标进程发送11号信号SIGSEGV,该信号的默认动作就是终止进程

死循环捕捉信号,解释与上面除0的类似,不解释了

2.5 由软件条件产生信号

SIGPIPE信号实际上就是一种由软件条件产生的信号,当进程在使用管道进行通信时,读端进程将读端关闭而写端进程还在一直向管道写入数据,那么此时写端进程就会收到13号信号SIGPIPE 进而被操作系统终止

该信号的默认行为是 Term,也是终止进程,这个就解释到这

下面介绍 alarm函数 和 SIGALRM信号

 alarm 函数的作用是:设定一个闹钟,也就是告诉内核在 seconds秒之后给当前进程发 SIGALRM信号, 该信号的默认处理动作是终止当前进程

man 2 alarm 查看

函数:alarm

头文件:
#include <unistd.h>

函数原型:
unsigned int alarm(unsigned int seconds);

参数:传入一个时间,单位秒

返回值:
若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置
如果调用alarm函数前,进程没有设置闹钟,则返回值为0

 测试代码

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

int main()
{
    //1秒后才发送信号
    alarm(1);
    int cnt = 0;
    while(true)
    {
         cout << "cnt: " << cnt << endl;
         cnt++;
    }
    return 0;
}

运行结果

 

下面进行捕捉该信号

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

 int cnt = 0;
//自定义行为
void handler(int signo)
{
    cout << "捕捉到一个信号,该信号编号是:" << signo << endl;
    cout << "cnt: "<< cnt << endl;
    exit(1);//自定义行为,退出进程
}

int main()
{
    signal(14, handler);
    //1秒后才发送信号
    alarm(1);
   
    while(true)
    {
         cnt++;
    }
    return 0;
}

 运行结果

 

14号信号默认动作是 Term,也是终止进程

两次cnt实验结果数据级别相差较大,由此也证明了,与计算机单纯的计算相比较,计算机与外设进行IO时的速度是非常慢的

注意:9号信号不支持捕捉,这是禁止的,这是一个管理员信号

信号产生,完结,下一篇进入信号保存和处理

----------------我是分割线---------------

文章暂时就到这里,下篇即将更新

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

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

相关文章

前后端的身份认证【Node.js】

1. 前后端的身份认证 1.1 Web 开发模式 目前主流的 Web 开发模式有两种&#xff0c;分别是&#xff1a; &#xff08;1&#xff09;基于服务端渲染的传统 Web 开发模式 &#xff08;2&#xff09;基于前后端分离的新型 Web 开发模式 服务端渲染的传统 Web 开发模式 服务端渲染…

力扣面试题 08.06. 汉诺塔问题:思路分析+图文详解+代码实现

文章目录 第一部分&#xff1a;问题描述1.1 题目1.2 示例&#x1f340; 示例一&#x1f340; 示例二 1.3 提示 第二部分&#xff1a;思路分析第三部分&#xff1a;代码实现 第一部分&#xff1a;问题描述 1.1 题目 &#x1f3e0; 链接&#xff1a;面试题 08.06. 汉诺塔问题 -…

windows安装rabbitmq和环境erlang(最详细版,包括对应关系)

写在最前&#xff1a;不知何时起安装一个mq需要翻无数文章才能安上了&#xff0c;没有一个讲全的&#xff0c;这里写一个详细的教程。 删除旧版本对应关系: 1.在官方文档中找到RabbitMQ版本对应的Erlang版本重新下载安装包文档RabbitMQ Erlang Version Requirements — Rabbit…

大家副业都在做什么?csgo搬砖靠谱的副业推荐给你

从来没想过&#xff0c;以前只会玩CSGO的男孩子&#xff0c;现在居然能借助游戏赚到钱了&#xff01;甚至不需要什么专业的技巧&#xff0c;简简单单 在steam平台选择有利润的道具后&#xff0c;再上架到国内网易BUFF平台&#xff0c;赚取“信息差”差价而已&#xff01; 谁大…

itop-3568开发板驱动学习笔记(19)内核工作队列

《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记 文章目录 工作队列简介共享工作队列工作结构体初始化 work_struct调度工作队列函数共享工作队列实验 自定义工作队列创建工作队列函数调度和取消调度工作队列刷新工作队列函数删除工作队列函数 内核延时工作队列延时…

成功上岸字节35K,技术4面+HR面,耗时20天,真是不容易

这次字节的面试&#xff0c;给我的感触很深&#xff0c;意识到基础的重要性。一共经历了五轮面试&#xff1a;技术4面&#xff0b;HR面。 下面看正文 本人自动专业毕业&#xff0c;压抑了五个多月&#xff0c;终于鼓起勇气&#xff0c;去字节面试&#xff0c;下面是我的面试过…

kali利用airgeddon破解WiFi (详细安装和使用教程)

目录 前言 一&#xff0c;软件&硬件环境 二&#xff0c;前期配置 Airgeddon安装和调试 #自带 #安装方法一 #安装方法二 #注意 网卡的配置 #打开服务 #加载网卡 三&#xff0c;运行操作 #检查 #主菜单 #打开监听模式 #查看周围可以攻击的网络 #截取…

vue - - - - - vue3全局配置挂载axios

vue3配置axios 1. 安装axios2. 配置拦截器3. vue.config.js代理配置4. 将axios全局挂载4. 文件内使用 1. 安装axios yarn add axios 2. 配置拦截器 创建文件 /src/utils/request.js "use strict";import Vue from "vue"; import axios from "axios&…

从现在起,请你不要用ChatGPT再做这4件事了

ChatGPT已经火爆了快半年了吧&#xff0c;紧接着国内也开始推出了各种仿制品&#xff0c;我甚至一度怀疑&#xff0c;如果人家没有推出ChatGPT&#xff0c;这些仿制品会不会出现。而很多人也嗨皮得不行&#xff0c;搭着梯子爬过高墙&#xff0c;用ChatGPT做各种觉得新鲜的事。但…

电脑可以开机但是无法进入到桌面怎么办?

电脑可以开机但是无法进入到桌面怎么办&#xff1f;有用户的电脑可以正确启动&#xff0c;但是电脑启动之后&#xff0c;却无法进入到系统桌面&#xff0c;而且卡在加载系统的页面中&#xff0c;或者是出现错误代码蓝屏了。这些情况其实都可以通过U盘来重装一个系统&#xff0c…

第七回:如何使用GirdView Widget

文章目录 概念介绍使用方法示例代码经验总结 我们在上一章回中介绍了Image Widget,本章回中将介绍 GirdView这种Widget&#xff0c;闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 在Flutter中使用GirdView表示网格状的布局&#xff0c;类似日常办公中使用的Excel…

hashCode 如何计算?这一篇就够了!

介绍 hashCode 中文‘散列码’&#xff0c;存在的意义是加快查找速率&#xff0c;可以在常数时间内进行寻址操作。 存在意义 它被定义在 Object 中&#xff0c;而 Object 类是一切类的父类&#xff0c;所以所有的方法都具有这个方法。 Java 中 hashCode 计算方式如下&#x…

C2. Exam in BerSU (hard version)(思维 + 小数据范围)

Problem - C2 - Codeforces 简单版本和困难版本之间的唯一区别是约束。 如果你用Python写一个解决方案&#xff0c;那么最好用PyPy发送&#xff0c;以加快执行时间。 贝兰德州立大学的一场会议已经开始。许多学生正在参加考试。 波利格拉夫维奇要对N个学生进行考试。学生们将…

工业树莓派远程I/O控制套装—更高效、更灵活、更便捷

一、背景 在完整的生产过程中&#xff0c;许多传感器设备和执行设备不完全安装在同一位置&#xff0c;大多分散部署在各个生产环节中。如果采用本地控制的方式&#xff0c;就需要用到多个控制器&#xff0c;但是成本较高&#xff0c;且不利于管理&#xff0c;所以最理想的解决…

2.redis-持久化

01-Redis持久化 概述 Redis数据存储在缓存中&#xff0c;为了防止数据的意外丢失&#xff0c;确保数据安全性。所以&#xff0c;就有了redis持久化。 分类 RDB: Redis默认的持久化策略, 直接存储数据 AOF: 存储数据的操作过程. 02-RDB持久化之save指令 配置说明 # 设置rdb…

[pgrx开发postgresql数据库扩展]2.安装与开发环境的搭建

——前文再续&#xff0c;书接上一回。 前言 我上篇文章刚刚写完&#xff0c;pgx就全面改名为了 pgrx……&#xff0c;结果导致我都来不及把以前的文章改过来&#xff0c;所以以后遵循最新的命名方法。 pgrx的开发环境需求 pgrx目前仅支持在linux操作系统上进行开发&#xff…

Android 基于 Perfetto 抓取 trace

Perfetto 官方链接地址 https://github.com/google/perfetto/ 开启Android的trace跟踪服务 Perfetto 是基于 Android 的系统追踪服务&#xff0c; 这个配置在 Android11 之后是默认打开的&#xff0c;但是如果你是 Android 9 ( P ) 或者 10 ( Q ) &#xff0c;那么就需要手动设…

【备考2023年软考】选系统规划与管理师,还是信息系统项目管理师?

目录 一、系统规划与管理师介绍 二、信息系统项目管理师介绍 三、二者区别 四、适合什么人考 五、怎么备考 1.了解考试大纲 2.系统学习&#xff08;附带资料分享&#xff09; 3.多做题 4.总结复习 软考系统规划与管理师和信息系统项目管理师是软考中的两个比较热门的证…

CSS——js 动态改变原生 radio、switch 的选中样式

导航 1. radio1-1. 业务场景&#xff1a;1-2. 效果&#xff1a;1-3. 问题点&#xff1a;1-4. 解决方案&#xff1a;1-5. 代码&#xff1a;1-5-1. HTML1-5-2. JS1-5-3. html 内容排版的 css1-5-4. 实现 radio 效果的 css 2. switch2-1. 业务场景&#xff1a;2-2. 效果&#xff1…

Vue3+Typescript+Vitest单元测试环境+组件事件测试篇

上一节我们学会了组件测试的基础测试部分组件测试基础篇&#xff0c;这一节&#xff0c;我们学习一下深入测试组件的事件 在component中增加一个新的组件,名字就叫做Zmbutton2吧 import { defineComponent } from "vue";const ZmButton2 defineComponent({name: &…