Liunx系统编程:进程信号的概念及产生方式

目录

一. 进程信号概述

1.1 生活中的信号

1.2 进程信号

1.3 信号的查看

二. 信号发送的本质

三. 信号产生的四种方式

3.1 按键产生信号

3.2 通过系统接口发送信号

3.2.1 kill -- 向指定进程发送信号

3.2.2 raise -- 当自身发送信号

3.2.3 abort -- 向自身发送进程终止信号 

3.3 软件条件产生信号

3.3.1 管道通信读端关闭

3.3.2 时钟问题

3.4 硬件异常产生信号

3.4.1 除0错误

3.4.2 野指针、越界访问问题

四. Core dump问题

4.1 什么是core dump

4.2 进程退出状态中的core dump标记位

4.3 用core文件调试代码

五. 总结


一. 进程信号概述

1.1 生活中的信号

在生活中,信号作为信息通知的一种手段无处不在,我们看到信号后,需要根据信号的种类,来对信号做出特定的反应。如:红绿灯、起床闹钟、警铃、电话铃声等,都属于信号的范畴。

我们能对信号做出相应的反应,是因为我们能够识别信号,并且知道对应的处理动作。按照日常经常,对于生活中的信号,做出以下的特性总结:

  1. 我们能够认识识别某种信号。
  2. 即使没有出现特定信号,我们也知道信号发出之后应当做出什么反应。
  3.  我们可能暂时不对信号进行处理,等待合适时机再处理。
  4. 如果暂时不处理信号,我们需要暂时记住信号,等待处理。

1.2 进程信号

进程信号与生活中的信号,都是用于信息通知的。我们可以这样理解进程信号:进程信号是一种信息通知机制,由用户或操作系统发送给特定的进程,通知进程发生了某事件,进程可以稍后处理。

进程对于信号的处理,与生活中的信号处理策略本质相同,可归为以下三类:

  1. 按照默认方式处理。(红灯停绿灯行,铃响后马上起床等)
  2. 自定义处理方式。(铃响后再睡十分钟回笼觉然后起床)
  3. 忽视信号,即不做任何相应。(铃响后让它一直响,继续在床上睡觉)

同样,我们也可以推断出进程信号应用的特性如下:

  1. 进程有识别信号的能力。(每个信号都有对应的编号用于进程识别)
  2. 进程有默认处理信号的方法。(由程序员写的代码决定默认处理方法)
  3. 进程可以对信号进行延后处理,或根本不处理。
  4. 用户可以自定义对于特定信号的处理方式。(signal函数)
  5. 信号的产生和进程的运行是异步的,即:产生信号时,进程可能在忙自己的事情,延后对信号的处理,而不是立马处理。

1.3 信号的查看

在Linux系统中,通过kill -l指令,可以查看系统中所有内置信号的编号与对应名称,如图1.1所示。

其中2号信号SIGINT,就对应我们经常使用的 Ctrl + C 终止进程。

图1.1 信号列表的查看

观察图1.1,我们发现,信号的最高编号为64,没有0号信号,没有32、33号信号,Liunx总共定义有62个信号。其中:

  • 1 ~ 31:普通信号。
  • 34 ~ 64:实时信号。

普通信号用于OS通过正常的运行调度队列调度的进程,即:进程轮番拿到CPU上去运行,每个进程每次在CPU上运行特定的时间片长度,然后切换运行进程。 

实时信号只用于极少数的生产环境,这是OS要求要对某个进程运行完毕才可以从CPU上拿下,且进程不应在调度队列中等待调度,而是应当立马调度运行。如:车载系统接收到刹车指令,不可以等待其他进程运行,也必须一次完成运行。

通过man 7 signal指令,下翻查找,我们可以看到每种信号的默认处理方式

  • Term -- 进程终止,不发生核心转储。
  • Core -- 进程终止,发生核心转储。 
  • Ign -- 子进程暂停运行或终止。
  • Cont -- 继续运行进程。
  • Stop -- 暂停运行进程。
图1.2 信号的默认处理方式

二. 信号发送的本质

要理解信号发送的本质,首先要理解进程如何保存信号。对信号进行保存,需要记录:a.哪一种信号,b.特定信号是否产生

这让我们很容易想到位图这种数据结构,位图中通过对特定比特位设置0/1值,来表示某件事情是否发生或某对象是否存在。进程PCB中存有位图这种数据结构来保存信号,每种信号都有其对应的编号,每个编号对应位图中的一个bit位,1表示产生并保存了某信号,0表示没有保存某信号。

如果进程收到了某信号但暂时不对信号做出响应,那么就应当将位图中对应的bit位由1置0,等待进程做出了响应之后,在置回0。

总结信号发送的本质为:OS向目标进程写信号 -> OS改变进程中记录信号状态的位图中的特点bit位 -> 完成信号发送过程

Crtl + C 组合键终止进程最终也是改变进程中位图的bit位值,键盘中特定槽位的按键按下,键盘驱动就会将按键按下的信息传给OS,OS就会将按键信息转换为信号,然后发送给进程。

三. 信号产生的四种方式

3.1 按键产生信号

通过组合键的方式,可以向进程发送信号,如:

  • Ctrl + c:向前台进程发送2号信号SIGINT。
  • Ctrl + \:向前台进程发送3号信号SIGQUIT。

其中2号信号的默认处理方式为进程终止,不进行核心转储,3号信号的默认处理方式为终止进程,进程核心转储。(本文后面会对核心转储进行讲解)

如代码和图3.1所示,运行一个间隔1s输出进程pid的进程,通过Ctrl + C组合键和Ctrl + \组合键向这个死循环进程发送信号,进程都终止掉了,即使没有运行到最后的代码。

代码3.1:死循环打印进程pid

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

int main()
{
    while(true)
    {
        std::cout << "This is a process, pid:" << getpid() << std::endl;
        sleep(1);
    }

    return 0;
}
图3.1 代码3.1运行结果

3.2 通过系统接口发送信号

3.2.1 kill -- 向指定进程发送信号

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

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

函数功能:向特定进程发送指定信号

返回值:发送成功返回0,失败发挥-1

编写信号发送程序的源文件kill.cc如代码3.2所示,编译生成可执行文件取名为kill,在命令行参数中输入 ./kill [id] [sig],即可向指定进程发送特定信号。如图3.2所示,通过kill向死循环进程发送8号信号,进程被终止掉了。

8号信号SIGFPE:浮点数错误,常发生于 x/0 的情况(除0错误)

代码3.2:信号发送kill.cc

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

int main(int argc, char** argv)
{
    // 发送信号,必须要有三个命令行参数
    if(argc != 3)
    {
        std::cout << "参数个数错误" << std::endl;
        exit(1);
    }

    int id = atoi(argv[1]);    // 获取结束信号进程的id
    int sig = atoi(argv[2]);   // 获取信号编号

    kill(id, sig);    // 发信号

    return 0;
}
图3.2 通过kill发送信号

3.2.2 raise -- 当自身发送信号

函数原型:int raise(int sig)

头文件:#include<signal.h>

函数功能:向自身发送指定编号的信号

返回值:发送成功返回0,失败发挥-1

代码3.3在通过fork创建子进程,在子进程中循环5次后,通过raise向子进程本身发送8号信号,父进程阻塞等待子进程退出,接收子进程的返回信号并输出到屏幕,代码运行结果见图3.3。

代码3.3:raise向自身发信号

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

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if(id == 0)
    {
        // 子进程代码
        int count = 0;
        while(true)
        {
            std::cout << "[Child process]# [" << getpid() << \
                    "], count:" << ++count << std::endl;

            if(count == 5)
            {
                raise(SIGFPE);    //向吱声发送8号信号
            }

            sleep(1);
        }

        exit(0);
    }

    int status = 0;
    pid_t n = waitpid(id, &status, 0);

    std::cout << "signal:" << (status & 0x7F) << std::endl;

    return 0;
}
图3.3 代码3.3的运行结果

3.2.3 abort -- 向自身发送进程终止信号 

函数原型:void abort()

头文件:#include <stdlib.h>

函数功能:向自身发送6号终止信号SIGABRT

代码3.3:abort向自身发送进程终止信号

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

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if(id == 0)
    {
        // 子进程代码
        while(true)
        {
            std::cout << "[Child process]# pid:" << getpid() << ", ppid:" << getppid() << std::endl;
            abort();   // 向子进程自身发送进程终止信号
        }

        exit(0);
    }

    int status = 0;
    pid_t n = waitpid(id, &status, 0);

    std::cout << "signal:" << (status & 0x7F) << std::endl;

    return 0;
}

3.3 软件条件产生信号

软件条件产生信号,就是OS检测到某种软件条件触发或者不满足,由OS向指定进程发送信号。如:a. 管道通信中,读段关闭,写端进程退出   b.时钟问题

3.3.1 管道通信读端关闭

管道通信,是一种提供访问控制的进程间通信方式,一端进程负责写数据(写端),另一端进程负责读数据(读端),如果读端关闭,那么写端就不再有意义,OS会强制终止写端进程。OS终止写端进程的方法就是发送13号信号SIGPIPE。

代码3.4对上述现象进行复现,a. 通过fork创建子进程 -> b. 父进程读数据,子进程写数据 -> c. 父子进程进行一段时间的通信 -> d.关闭父进程读端管道 -> e.父进程阻塞等到子进程退出,输出退出状态信息。运行代码,可见子进程因接收到了13号SIGPIPE信号而终止退出。

代码3.4:管道通信中因软件条件产生信号

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

int main()
{
    // 1.创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);   // 管道创建

    if(n == -1)
    {
        perror("pipe");
        exit(1);
    }

    // 2.创建子进程
    pid_t id = fork();   

    // 让子进程写数据, 父进程读数据
    if(id == 0)
    {
        // 子进程发送数据
        // 3. 关闭不需要的fd
        close(pipefd[0]);

        // 4. 写数据
        // char send_buffer[1024] = {0};
        std::string message = "[Child process]# Send message to you";

        while(true)
        {
            // 子进程间隔一秒发送数据
            ssize_t sz = write(pipefd[1], message.c_str(), message.size());
            if(sz > 0) sleep(1);
            else break;
        }

        close(pipefd[1]);
        exit(1);
    }

    // 父进程读取数据
    // 3. 关闭不需要的fd
    close(pipefd[1]);

    // 4. 开始读取数据
    char read_buffer[1024] = {0};
    int count = 0;

    while(true)
    {
        ssize_t sz = read(pipefd[0], read_buffer, 1024);
        if(sz > 0)
        {
            read_buffer[sz] = '\0';
            std::cout << "[" << getpid() << "]" << read_buffer << ", count:" << ++count << std::endl;

            if(count == 5)
            {
                // 关掉读端
                // 终止读取死循环
                close(pipefd[0]);
                break;
            }
        }
        else break;
    }

    // 5. 父进程阻塞等待子进程退出
    int status = 0;
    waitpid(id, &status, 0);

    std::cout << "child process exit, write end, exit signal:" \
    << (status & 0x7F) << std::endl;

    return 0;
}
图3.4 代码3.4的运行结果

3.3.2 时钟问题

首先要了解一下时钟接口函数alarm。

alarm函数:

函数原型:unsigned int alarm(unsigned int second)

头文件:#include<unistd.h>

功能:调用时钟函数second秒后,向进程发送14号SIGALRM信号,在倒计时期间进程继续运行不受影响。

返回值:如果之前有alarm函数被调用,返回上一个时钟倒计时还有几秒,如果之前没有alarm函数被调用,返回0。

代码3.5为算力评估函数,通过时钟函数,自定义处理14号SIGALRM信号的方法,计算1s内CPU能够进行多少次的++操作,在自定义信号处理方法中输出。

代码3.5:通过alarm函数进行算力评估

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

uint64_t count = 0;

void handler(int sig)   // 自定义信号处理函数
{
    std::cout << "count end, final count:" << count << std::endl;
    exit(0);
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1);   // 倒计时1秒
    while(true) ++count;
    return 0;
}
图3.5 代码3.5的运行结果

3.4 硬件异常产生信号

硬件异常,被硬件以某种方式记录(通常为寄存器中的状态标记信息),由OS检测到,并产生信号发送给进程。比如:除0错误被CPU检查并记录,OS检测到并产生SIGFPE信号发送给进程,再比如野指针、越界访问内存的问题,MMU检测到了非法访问,由硬件记录异常状态,OS检测到后发送SIGSEGV信号给进程。

Linux中越界访问、野指针等问题通常都被记录为段错误Segmentation fault,对应11号SIGSEGV信号。

3.4.1 除0错误

代码3.6自定义了对SIGFPE信号的处理方法handler_signal,出现异常程序不会退出终止,在主函数中,先人为写出除0错误,然后while(true)死循环。

按照我们的一般认知,进程在接收到信号后只会执行一次对应的自定义处理动作,但是,如图3.6所示,handler_signal函数在不断地被运行,这是为什么?更加奇怪的是,如果将代码中发生/0错误的部分注释掉,通过命令行kill -8的方法,人为向进程发送8号信号,handler_signal就只会被执行一次!

代码3.6:除0错误引发硬件异常

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

void handler_signal(int sig)
{
    std::cout << "recieve a signal:" << sig << std::endl;
    sleep(1);
}

int main()
{
    signal(SIGFPE, handler_signal);
    int a = 10;
    int b = a/0;
    while(true) {}
    return 0;
}
图3.6 代码3.6运行结果

自定义handler_signal后,除零错误产生的现象:

  • 如果代码本身发生除0错误,handler_signal被一次次执行。
  • 如果人为通过kill -8发送信号,handler_signal只被执行一次。 

产生这种现象的原因就在于,代码本身产生的除0错误为硬件异常,而人为kill -8发送信号不经由硬件,直接被OS处理后将信号发送给进程。

我们可以以如下的方式理解硬件产生的除0异常和上面的奇怪现象:

  • 计算是由硬件(CPU)来完成的,CPU会赋值进行溢出检查。
  • CPU的寄存器中会记录有状态信息,其中就包括溢出标志位,如果出现除0错误或其他溢出问题,CPU寄存器中的溢出标志位就会被置1,OS会在计算完成时对CPU寄存器中的状态信息进行检测,如果检测到溢出标记位为1,OS就会向进程发送8号SIGFPE信号。
  • 进程接收到了8号异常信号后,并不一定会退出(如果自定义了处理函数)。
  • 若进程不退出,OS就会对CPU寄存器中的状态信息进行轮巡检测,溢出标记在除零错误发生后并没有被置回0,OS就会不断检测到异常信息,进而不断向进程发送SIGFPE信号,进而一直在调用handler_signal处理信号。
  • 通过kill -8 发送信号,是一种纯软件行为,并没有经过硬件,没有设置过CPU中的标记位,在CPU轮巡检测的过程中也就不会被不断检测出异常。

3.4.2 野指针、越界访问问题

当某个进程需要访问物理内存资源时,需要先拿到进程地址空间的虚拟地址,再通过 页表 + MMU(Memery Manager Unit,内存管理模块) 映射到物理内存的方式来访问。

我们获取认为页表映射是OS所进行的软件行为,其实不是,试想一下,通过页表映射物理内存是进程运行过程中大量、高频执行的操作,如果有OS来进行这一操作,就严重延缓了计算机设备的整机效率。

实际的情况是:OS将页表和虚拟地址信息,以二进制的方式写入到硬件中去,通过虚拟地址 + 页表 映射物理内存的方法,是由硬件来完成的,那么出现越界、野指针等问题,也理应属于硬件异常。

我们可以按照以下方式理解野指针、越界访问的问题:

  • 进程必须要通过地址来访问物理内存,而现代计算机使用的都是虚拟地址。
  • 虚拟地址需要通过 页表 + MMU(Memery Manager Unit,硬件)来完成虚拟地址到实际地址的转换。
  • 在不考虑编译器优化的情况下,如果出现野指针、越界访问等问题,MMU一定会报错。
  • 页表 + MMU映射物理内存地址是硬件层面的操作,出错当然也属于硬件异常,硬件会对相应的寄存器标志位进行设置,供OS进行轮巡检测。

出现野指针、越界访问等问题在Linux中属于段错误,OS会向进程发送11号SIGSEGV信号。

代码3.7模拟了野指针问题产生的硬件异常行为,代码运行结果与3.6一致,都是不间断执行信号处理函数signal_handler,因为OS在对硬件寄存器中的信息进行轮巡检测。

代码3.7:野指针问题引发硬件异常

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

void handler_signal(int sig)
{
    std::cout << "recieve a signal:" << sig << std::endl;
    sleep(1);
}

int main()
{
    signal(SIGSEGV, handler_signal);
    int* pa = nullptr;
    *pa = 10;
    while(true) {}
    return 0;
}
图3.7 代码3.7的运行结果

四. Core dump问题

4.1 什么是core dump

通过man 7 signal,查看每种信号对应的默认处理方法,如果为Core,那么默认会产生Core文件,OS会将当前进程内存中的核心数据全部保存到Core文件中,Core文件是用来调试的。

如:3号SIGQUIT信号、8号SIGFPE信号、11号SIGSEGV信号,默认触发响应都会产生Core文件,而2号SIGINT、9号SIGKILL信号,默认不会产生Core文件。

一般而言,生产环境、云服务器的core dump功能都是关闭的,如果想要在响应信号的时候产生core文件,还必须保证core dump功能打开。

  • ulimit -a 指令:可用于查看core dump功能是否打开,如果输出结果的第一行core file size为0,那么表示core dump功能没有打开。
  • ulimit -c [size] 指令 -- 通过指定file文件的大小(非0),来打开core file功能。
图4.1 查看和开启core dump

4.2 进程退出状态中的core dump标记位

通过wait/waitpid函数,可以实现父进程对子进程的等待,wait/waitpid函数都可以接收int*类型的输出型参数,用于记录子进程的退出状态,我们称之为int status,status在进程正常退出和异常退出的状态下,如图4.2所示根据退出状态和status比特位的划分,对应子进程退出状态。

图4.2 进程退出状态status的比特位使用情况

代码4.1创建进行死循环的子进程,并且waitpid阻塞等待子进程退出,输出信号编号和core dump标记位,在代码运行起来后,通过在命令行中向子进程发送3号信号(kill -3 [pid]),强制子进程终止并在进程终止后通过ll指令查看当前路径下的文件及属性,可以看到生成的core文件。

代码4.1:子进程信号编号和core dump标志的获取

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

int main()
{
    // 1. 创建子进程
    pid_t id = fork();   
    if(id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if(id == 0)
    {
        // 2. 子进程代码
        while(true)
        {
            std::cout << "[Child process]# pid:" << getpid() << ", getppid:" << getppid() << std::endl;
            sleep(1);
        }

        exit(0);
    }

    // 3. 父进程阻塞等待子进程,并获取退出状态
    int status = 0;
    waitpid(id, &status, 0);

    //4. 检查子进程退出状态
    if(WIFEXITED(status))
    {
        std::cout << "子进程正常终止" << std::endl;
        std::cout << "exit code: " << WEXITSTATUS(status) << std::endl;
    }
    else
    {
        std::cout << "子进程被信号所杀" << std::endl;
        std::cout << "signal:" << (status & 0x7F) << ", core dump:" << ((status >> 7) & 1) << std::endl;
    }

    return 0;
}
图4.3 代码4.1的运行结果

4.3 用core文件调试代码

Linux下的gcc/g++编译器默认采用Release版本发布可执行程序,若要对代码进行调试,就需要Debug版本的可执行程序,这就需要在使用gcc/g++编译源文件的时候添加-g选项。

代码4.2人为制造除0错误,这样就会有8号信号发给进程,8号信号默认会产生core文件。编译代码,生成Debug版本的可执行程序。使用gdb启动调试之后,在命令行中输入指令core-file [core文件名],就会显示出出现异常的行数,以及其他的错误信息。

gdb指令:core-file [core文件名] -- 引入core文件调试代码,定位出错位置,查看错误信息

代码4.2:出现异常并生成Core文件的测试代码

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

int main()
{
    int a = 10;
    int b = a/0;

    sleep(1);
    std::cout << "run here" << std::endl;

    return 0;
}
图4.4 代码4.3的运行和调试结果

五. 总结

  • 进程信号用于向进程发送信息,通知进场某件事情的发生,进程咱叔不响应信号,等待合适的时机再进行响应。
  • 进程处理信号的方式和分为三种:a. 按照默认方式响应  b.按照用户自定义的方式响应  c.直接忽略信号,不做处理。
  • 如果进程暂时不响应信号,则需要对进行进行暂时保存。信号保存需要存储信号编号以及它是否产生,Linux中使用位图保存信号,而位图存于每个进程的PCB中。
  • 发送信号的本质,就是更改位图中对应的二进制位。
  • 信号可以通过四种方式产生:1.按键  2.系统接口调用  3.软件行为  4.硬件异常
  • 如果某种信号的默认处理方式会产生core文件,而且系统的Core dump功能处于开启状态,那么进程在接收到这些信号后会生成core文件,core文件用于对代码的调试,可以帮助快速定代码中出现异常的位置以及错误信息。

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

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

相关文章

使用 Elasticsearch 轻松进行中文文本分类

本文记录下使用 Elasticsearch 进行文本分类&#xff0c;当我第一次偶然发现 Elasticsearch 时&#xff0c;就被它的易用性、速度和配置选项所吸引。每次使用 Elasticsearch&#xff0c;我都能找到一种更为简单的方法来解决我一贯通过传统的自然语言处理 (NLP) 工具和技术来解决…

基于Python的HTTP代理爬虫开发初探

前言 随着互联网的发展&#xff0c;爬虫技术已经成为了信息采集、数据分析的重要手段。然而在进行爬虫开发的过程中&#xff0c;由于个人或机构的目的不同&#xff0c;也会面临一些访问限制或者防护措施。这时候&#xff0c;使用HTTP代理爬虫可以有效地解决这些问题&#xff0…

麦肯锡发布《2023科技趋势展望报告》,生成式AI、下一代软件开发成为趋势,软件测试如何贴合趋势?

近日&#xff0c;麦肯锡公司发布了《2023科技趋势展望报告》。报告列出了15个趋势&#xff0c;并把他们分为5大类&#xff0c;人工智能革命、构建数字未来、计算和连接的前沿、尖端工程技术和可持续发展。 类别一&#xff1a;人工智能革命 生成式AI 生成型人工智能标志着人工智…

元宇宙电商—NFG系统:区块链技术助力商品确权。

在国内&#xff0c;以“数字藏品”之名崛起以来&#xff0c;其与NFT的对比就从未停歇。从上链模式到数据主权&#xff0c;从炒作需求到实际应用&#xff0c;从售卖形式到价值属性&#xff0c;在各种抽丝剥茧般的比较中&#xff0c;围绕两者孰优孰劣的讨论不绝于耳。 NFT的每一…

机器学习知识点总结:什么是EM(最大期望值算法)

什么是EM(最大期望值算法) 在现实生活中&#xff0c;苹果百分百是苹果&#xff0c;梨百分白是梨。 生活中还有很多事物是概率分布&#xff0c;比如有多少人结了婚&#xff0c;又有多少人有工作&#xff0c; 如果我们想要调查人群中吸大麻者的比例呢&#xff1f;敏感问题很难得…

React如何配置env环境变量

React版本&#xff1a; "react": "^18.2.0" 1、在package.json平级目录下创建.env文件 2、在‘.env’文件里配置环境变量 【1】PUBLIC_URL 描述&#xff1a;编译时文件的base-href 官方描述&#xff1a; // We use PUBLIC_URL environment variable …

解决C#报“MSB3088 未能读取状态文件*.csprojAssemblyReference.cache“问题

今天在使用vscode软件C#插件&#xff0c;编译.cs文件时&#xff0c;发现如下warning: 图(1) C#报cache没有更新 出现该warning的原因&#xff1a;当前.cs文件修改了&#xff0c;但是其缓存文件*.csprojAssemblyReference.cache没有更新&#xff0c;需要重新清理一下工程&#x…

clickhouse-监控配置

一、概述 监控是运维的一大利器&#xff0c;要想运维好clickhouse,首先就要对其进行监控&#xff0c;clickhouse有几种监控数据的方式&#xff0c;一种是系统本身监控&#xff0c;一种是通过exporter来监控&#xff0c;下面分别描述一下 二、系统自带监控 我下面会对监控做一…

三角形添加数--夏令营

题目 tips&#xff1a; 1.本题不要求正三角形输出&#xff0c;只要输出左下三角即可 2.这种输入三角形的&#xff0c;都是可以理解为左下三角形的模型&#xff0c;然后去写f[i][j]f[i-1][j]f[i-1][j1]&#xff0c;写行列 3.还有双重for循环输入输出三角形&#xff0c;注意第二…

linux 搭建 nexus maven私服

目录 环境&#xff1a; 下载 访问百度网盘链接 官网下载 部署 &#xff1a; 进入目录&#xff0c;创建文件夹,进入文件夹 将安装包放入nexus文件夹&#xff0c;并解压​编辑 启动 nexus,并查看状态.​编辑 更改 nexus 端口为7020,并重新启动&#xff0c;访问虚拟机7020…

【Spring专题】Spring之Bean的生命周期源码解析——阶段二(二)(IOC之属性填充/依赖注入)

目录 前言阅读准备阅读指引阅读建议 课程内容一、依赖注入方式&#xff08;前置知识&#xff09;1.1 手动注入1.2 自动注入1.2.1 XML的autowire自动注入1.2.1.1 byType&#xff1a;按照类型进行注入1.2.1.2 byName&#xff1a;按照名称进行注入1.2.1.3 constructor&#xff1a;…

如何解决使用npm出现Cannot find module ‘XXX\node_modules\npm\bin\npm-cli.js’错误

遇到问题&#xff1a;用npm下载组件时出现Cannot find module ‘D&#xff1a;software\node_modules\npm\bin\npm-cli.js’ 问题&#xff0c;导致下载组件不能完成。 解决方法&#xff1a;下载缺少的npm文件即可解决放到指定node_modules目录下即可解决。 分析问题&#xff1…

【自创】关于前端js的“嵌套地狱”的遍历算法

欢迎大家关注我的CSDN账号 欢迎大家关注我的哔哩哔哩账号&#xff1a;卢淼儿的个人空间-卢淼儿个人主页-哔哩哔哩视频 此saas系统我会在9月2号之前&#xff0c;在csdn及哔哩哔哩上发布成套系列教学视频。敬请期待&#xff01;&#xff01;&#xff01; 首先看图 这是我们要解…

Unity进阶–通过PhotonServer实现联网登录注册功能(客户端)–PhotonServer(三)

文章目录 Unity进阶–通过PhotonServer实现联网登录注册功能(客户端)–PhotonServer(三)前情提要客户端部分 Unity进阶–通过PhotonServer实现联网登录注册功能(客户端)–PhotonServer(三) 前情提要 单例泛型类 using System.Collections; using System.Collections.Generic; …

探索高级UI、源码解析与性能优化,了解开源框架及Flutter,助力Java和Kotlin筑基,揭秘NDK的魅力!

课程链接&#xff1a; 链接: https://pan.baidu.com/s/13cR0Ip6lzgFoz0rcmgYGZA?pwdy7hp 提取码: y7hp 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 --来自百度网盘超级会员v4的分享 课程介绍&#xff1a; &#x1f4da;【01】Java筑基&#xff1a;全方位指…

政务、商务数据资源有效共享:让数据上“链”,记录每一个存储过程!

数据上链是目前“区块链”最常见的场景。因为链上所有参与方都分享了统一的事实来源&#xff0c;所有人都可以即时获得最新的信息&#xff0c;数据可用不可见。因此&#xff0c;不同参与方之间的协作效率得以大幅提高。同时&#xff0c;因为区块链上的数据难以篡改&#xff0c;…

SpringBoot的配置文件(properties与yml)

文章目录 1. 配置文件的作用2. 配置文件格式3. 配置文件的使用方法3.1. properties配置文件3.1.1. 基本语法和使用3.1.2. properties优缺点分析 3.2. yml配置文件3.2.1. 基本语法与使用3.2.2. yml中单双引号问题3.2.3. yml配置不同类型的数据类型及null3.2.4. 配置对象3.2.5. 配…

js闭包用法以及和bind的结合使用

bind用法 let info { name: "xuhaitao", age: 36 }function haitao() {console.log(this);}let fun haitao.bind(info)fun();haitao(); 控制台打印: 闭包用法: function xiaoMing() {let v 1;function jia() {v;console.log(v);}function getV() {console.log(…

通过微软Azure调用GPT的接口API-兼容平替OpenAI官方的注意事项

众所周知&#xff0c;我们是访问不通OpenAI官方服务的&#xff0c;但是我们可以自己通过代理或者使用第三方代理访问接口 现在新出台的规定禁止使用境外的AI大模型接口对境内客户使用&#xff0c;所以我们需要使用国内的大模型接口 国内的效果真的很差&#xff0c;现在如果想使…

perl下载与安装教程【工具使用】

Perl是一个高阶程式语言&#xff0c;由 Larry Wall和其他许多人所写&#xff0c;融合了许多语言的特性。它主要是由无所不在的 C语言&#xff0c;其次由 sed、awk&#xff0c;UNIX shell 和至少十数种其他的工具和语言所演化而来。Perl对 process、档案&#xff0c;和文字有很强…