【Linux】第八讲:Linux进程信号详解(一)_ 认识信号 | 产生信号

news2024/11/13 15:00:41

「前言」文章是关于Linux进程信号方面的知识,本文的内容是Linux进程信号第一讲,讲解会比较细,下面开始!

「归属专栏」Linux系统编程

「笔者」枫叶先生(fy)

「座右铭」前行路上修真我

 「枫叶先生有点文青病」

「每篇一句」 

人生天地间,忽如远行客。
——《乐府·青青陵上柏》

 

目录

一、认识信号

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号信号不支持捕捉,这是禁止的,这是一个管理员信号

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

--------------------- END ---------------------- 

「 作者 」 枫叶先生
「 更新 」 2023.4.4
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

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

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

相关文章

【Ambari】开启HDFS 的HA架构

之前搭建的Ambari可以查看之前的博客 接下来我们来看下HDFS 开启HA 开启HDFS 的HA架构 选择启动NN的HA 因为之前是3节点的所以一开始安装的时候 Ambari架构选择了让安装一个NameNode和一个SecendryNameNode。 点击启动NameNode HA 后跳出个界面 填写集群名 类似于之前第…

六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序

文章目录&#xff1a; 1. 插入排序2.希尔排序3.选择排序4.冒泡排序5.堆排序6.快速排序5.1 hoare版本(左右指针法)5.2 挖坑法5.2.1 递归5.2.2 非递归 5.3 前后指针法 1. 插入排序 步骤&#xff1a; 1.从第一个元素开始&#xff0c;该元素可以认为已经被排序 2.取下一个元素tem…

【社区图书馆】Spring Boot 3核心技术与最佳实践

文章目录 前言什么是Spring BootSpring Boot特性Spring Boot优缺点Spring Boot核心功能 记录和随想 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 什么是Spring Boot 使用Sprin…

Java基础(二十):泛型

Java基础系列文章 Java基础(一)&#xff1a;语言概述 Java基础(二)&#xff1a;原码、反码、补码及进制之间的运算 Java基础(三)&#xff1a;数据类型与进制 Java基础(四)&#xff1a;逻辑运算符和位运算符 Java基础(五)&#xff1a;流程控制语句 Java基础(六)&#xff1…

【SWAT水文模型】SWAT水文模型建立及应用第四期: 气象数据的准备(待更新)

SWAT水文模型建立及应用&#xff1a; 气象数据的准备 1 简介2 气象数据的准备&#xff08;传统气象站&#xff09;2.1 天气发生器各参数的计算2.2 降水及气温输入数据的准备 3 气象数据的准备&#xff08;中国区域高精度同化气象站CMADS&#xff09;参考 本博客主要介绍气象数据…

Git命令与在IDEA中配置Git

目录 Git常用命令 全局设置 1.设置用户信息 2.查看配置信息 3.获取Git仓库 工作区, 暂存区, 版本库的概念 本地仓库操作 远程仓库操作 分支操作 标签操作 IDEA配置Git 本地仓库操作 远程仓库操作 分支操作 Git常用命令 全局设置 1.设置用户信息 git config --g…

ESP32设备驱动-VEML7700光照度传感器驱动

VEML7700光照度传感器驱动 1、VEML7700介绍 文章目录 VEML7700光照度传感器驱动1、VEML7700介绍2、硬件准备3、软件准备4、驱动实现VEML7700 是一款高精度环境光数字 16 位分辨率传感器。 它包括一个高灵敏度光电二极管、一个低噪声放大器、一个 16 位 A/D 转换器,并支持一个…

Coursera—Andrew Ng机器学习—课程笔记 Lecture 1_Introduction and Basic Concepts 介绍和基本概念

1. 1欢迎 1.2 机器学习是什么 参考视频: 1 - 2 - What is Machine Learning_ (7 min).mkv 1.2.1 机器学习定义 • Arthur Samuel (1959). Machine Learning: Field of study that gives computers the ability to learn without being explicitly programmed. 机器学习…

基于COM组件实现C#调用C++类对象过程中的注意事项

目录 一、基于COM的调用原理二、注意事项如何在C ATL中有效添加方法与属性如何让C#调用C中的属性&#xff08;.idl中声明属性&#xff09;如何对变量类型进行转换C#如何获取C类中的参数变量 一、基于COM的调用原理 调用原理&#xff1a;首先基于C ATL模板类&#xff0c;实现需…

29次-CCF-第一题-田地丈量

1 题目链接 进去后点&#xff0c;模拟考试就可以看到题目了 AC证明&#xff1a; 2 分析 前言&#xff1a; 离谱&#xff0c;这个题考试的时候做了30min才写出来&#xff0c;但是代码还贼简单。 你说它难吧&#xff0c;代码这么简单&#xff0c;你说不难吧&#xff0c;我在这…

CSS布局之圣杯布局/双飞翼布局

&#x1f4dd;个人主页&#xff1a;爱吃炫迈 &#x1f48c;系列专栏&#xff1a;HTMLCSS &#x1f9d1;‍&#x1f4bb;座右铭&#xff1a;道阻且长&#xff0c;行则将至&#x1f497; 文章目录 圣杯布局HTML代码步骤CSS代码 双飞翼布局HTML代码步骤CSS代码 小结 圣杯布局 HTM…

延长无线传感器网络网络寿命的异构节点智能部署策略(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 无线传感器网络是通过大量分布的传感器节点作为终端来协同感知和自主地监测外部世界,以多跳、自组织或协作的方式进行通信和信息…

Android系统架构

Application层&#xff0c;也就是应用层&#xff0c;不仅包括通话短信联系人这种系统级的应用&#xff0c;还包括用户自己安装的一些第三方应用Framework层&#xff0c;这一层大部分用Java写的&#xff0c;包括系统服务和四大组件Library层&#xff0c;这一层大部分都是C/C写的…

C++调用matlab编译动态库方法及相关问题解决

目录 参考链接&#xff1a;1、C调用matlab代码的方法1.1、Library Compiler 方法1.1.1、功能1.1.2、参考链接 1.2、mex -setup方法1.2.1、功能参考链接&#xff1a; 1.3、coder 方法功能 1.4、Matlab engine 2、C 使用matlab编译动态库的传参方法3、matlab运行时 参考链接&…

《AI嵌入式系统技术与实践-基于树莓派RP2040和MicroPython》书籍介绍

图书封面及出版信息 该书由本博主编著&#xff0c;全书总字数约50万字&#xff0c;本书于2023年4月出版。 内容简介 本书基于以RP2040 MCU芯片为核心的树莓派Pico开发板硬件扩展接口&#xff0c;利用Pico硬件扩展接口和面包板设计典型硬件电路&#xff0c;并结合当今流行的微…

网络编程(TCP与UDP协议)

文章目录 1. 网络编程1.1 软件架构1.2 网络基础 2. 网络通信要素2.1 如何实现网络中的主机互相通信2.2 通信要素一&#xff1a;IP地址和域名2.2.1 IP地址2.2.2 域名 2.3 通信要素二&#xff1a;端口号2.4 通信要素三&#xff1a;网络通信协议 3. 传输层协议&#xff1a;TCP与UD…

如何快速查询Git的config配置文件的本地路径

如何快速查询Git的config配置文件的本地路径 命令作用git config --local --list --show-origin查看仓库级别 git 配置信息&#xff0c;并打印配置文件本地路径最高优先级&#xff08;仓库下&#xff09;git config --global --list --show-origin查看全局级别 git 配置信息&a…

2022年全国职业院校技能大赛网络系统管理赛项模块B:Windows部署(样题1)

2022年全国职业院校技能大赛 网络系统管理赛项 模块B&#xff1a;Windows部署 &#xff08;样题1&#xff09; 目录 一、 初始化环境 &#xff08;一&#xff09;默认账号及默认密码 二、项目任务描述 &#xff08;一&#xff09;基本配置 &#xff08;二&#xff09;拓…

C语言控制语句

文章目录 前言一、分支语句1.if语句2.if else else if3.switch语句 二、循环语句1.for循环2.while循环3.do-while循环 三、跳转语句1.break语句2.continue语句3.goto语句 四、嵌套语句1.if语句嵌套2.for语句嵌套3.while语句嵌套 总结 前言 C语言中的控制语句是非常重要的一个知…

JVM入门必备

1、JVM 的位置 2、JVM 的体系结构 JVM&#xff08;Java虚拟机&#xff09;是Java程序的运行环境&#xff0c;它对于Java平台的运行和跨平台特性的实现有着重要的作用。JVM的体系结构有以下几个部分&#xff1a; 类加载器&#xff08;ClassLoader&#xff09;&#xff1a;负责将…