【Linux】-- 进程信号(认识、应用)

news2024/12/28 5:57:32

目录

信号初识

生活中的信号

Linux中的信号

产生信号

核心转储

软件条件产生信号的样例

系统调用接口

raise

kill

abort 

闹钟

硬件异常产生信号

如何理解除0错误

如何理解野指针 / 越界错误

总结


信号初识

(信号和信号量是两个东西)

生活中的信号

  • 红绿灯
  • 狼烟
  • 闹钟
  • 转向灯
  1. 认识这些信号:记住对应场景下的信号+后续需要执行的”动作“。
  2. 我们通过大脑,能够识别这个信号!
  3. 如果特定信号没有产生,但是我们依旧知道应该如何处理这个信号。
  4. 我在收到这个信号的时候,可能不会立即处理这个信号!
  5. 信号本身,在我们无法立即被处理的时候,也—定要先被临时的记住!

Linux中的信号

        本质就是一种通知机制,用户 / 操作系统通过发送一定的信号,通知进程,某些事情已经发生,可以在后续进行处理。

1:进程 <-> 信号

a、进程要处理信号,必须具备信号“识别”的能力(看到 + 处理动作)

b、进程对于信号的“识别”

        操作系统也是程序员写的,一个程序员在写进程如何实现的时候,是需要考虑到信号如何识别处理。

c、信号产生是随机的,进程可能正在忙自己的事情。所以,信号的后续处理,可能不是立即处理。

        如同,不知道外卖到达的准确时间。而你又在忙,于是就算到达时也会延迟取外卖。

d、信号会临时的记录下对应的信号,方便后续进行处理。

        延迟取外卖,就必须临时的几下这件事请,不能忘记。

e、进程处理信号的时间,合适的时候。

        方便取外卖的时候,前去取。

g、一般而言,信号的产生相对于进程而言是异步的。

        异步:不等待,你做你的事情,他做他的事情。外卖小哥我们的单,继续送他手上的单,主要干的事请是互相不干扰的。

        同步:等待,两方的事情是互相干扰的。开会时,然一个人去拿一样东西,然后此时会议暂停等待此人将东西拿回,再继续开会。

2:信号如何产生

        使用CTRL + c将一个死循环的进程终止运行。

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

int main()
{
    while(true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }
    return 0;
}

        CTRL + c本质:向进程发送2号信号

        此时进程接收到了2号信号,进程就做了和原始行为不一样的行为。说明其一定处理了此信号。

信号处理的常见方式:

1. 默认(进程自带的,程序员写好的逻辑)

        每一个信号都有其的默认处理动作。如:收到此信号,进程就直接终止。(听见闹钟,默认该起床了)

2. 忽略(也是信号的一种处理方式)

        一个信号被进程接收,但是由于一些原因,处理为忽略。如:进程将曾经记住的信号忘掉。(听见闹钟,但是太困选择了忽略)

3. 自定义动作(捕捉信号)

        (听见闹钟,不是起床,而是写作业)

融会贯通的理解

        现在开始,对于信号要有一个新的认识,不能说CTRL + c可以用于终止,记住就完了。而是进一步理解,是因为我们发了一个2号信号,然后进程收到2号信号,由于进程已经由程序员写好的逻辑 -- 默认信号处理方式,即终止自己。

        这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

3:常见信号

          kill -l 查看操作系统中的信号。

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

4:如何理解组合键变成信号

信号处理的常见方式:

        键盘的工作方式是通过:中断方式进行的。

中断:

        如:一位正在上课的老师,忽然校长来到教师将老师叫走了。此时老师停止上课,处理紧急事情,处理完才回来继续上课。

        即:一件事情打断了一个进程正在做的事情,使得该进程先执行此事情再接着执行前面的事情。这种方式就是中断。

·        我们在按键盘的时候,一但按下键盘,键盘有着对应的位置,有对应的编号(通过键盘驱动进行识别),发出信号可以让操作系统识别,所以当然操作系统也能够识别组合键(CTRL + c)。

5:如何理解信号被进程保存

        有[1,32]多个信号。传入的信号是可能是一到多个的,进程需要对传入的信号进行分析、处理。于是对于进程便有两个关键的问题:

  • 哪个信号?
  • 是否产生该信号?

        对此,进程必须具有保存信号的相关数据结构 —— 位图。

以unisgned int的位图进行标识:

  • 对应bit位位数表示哪一个信号
  • 对应bit位01表示信号是否产生

        PCB内部保存了信号位图字段。

如何理解信号发送的本质:

        信号位图是在task_struct中 -> task_struct在内核数据结构中 -> 只有操作系统有权利发送信号(因为内核数据结构属于操作系统)

        可以理解为:产生信号的方式有很多很多种,但是所有信号的发送全是操作系统去发的。

(只有操作系统有资格去更改,task_struct中的内容字段)

发送信号的本质:操作系统向目标进程写信号,操作系统直接修改PCB中的指定位图结构,完成 “发送” 信号的过程。

产生信号

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
对特定的信号进行捕捉。

参数:

        signum:传入的信号

  • signum值会自动,传参数到我们自定义捕捉函数handler。
  • 参数可传数字,可传宏。
signal(2, catchSig);
signal(SIGINT, catchSig); //更好,能见名知意

        handler:用户自行完成的自定义捕捉函数

  • 回调函数,通过回调的方式,修改对应的信号捕捉方法。

返回值:是一个函数指针

        我们设置了一个新的方法,其返回的是老的方法,错误返回SIG_ERR。

路径:/usr/include/bits/signum.h

想将默认信号变为自定义捕捉,不想做信号在进程内部的默认行为,而是执行我们的自定义行为。

理解:

        所有的行为都是代码,计算机世界里无非就是代码 + 数据。行为就可以无限的具体化为代码(函数、类中成员方法)。

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

void catchSig(int signum)
{
    std::cout << "进程捕捉到一个信号,正在处理中:" << signum << "Pid:" << getpid() << std::endl;
}

int main()
{
    signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个

    while(true)
    {
        std::cout << "我是一个进程,我正在运行……。Pid:" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

        此时我们会发现无论这么CTRL + c都终止不了了。以前,对于2号信号的默认动作就是终止进程,现在我们将2号的处理动作改了,于是对应的进程就不退出了。 

        只有当信号产生的时候signal函数中的cathSig才会被调用 —— signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作。

        后续的进程将signal掉完后还能跑,需要当前进程运行着。因为产生信号与进程代码执行是异步的(无法知晓信号产生的时间)。所以一般需要用信号的场景都是后需有很多的逻辑(死循环 / 长时间运行的代码),使得可以有充足的时间执行signal函数

Note:

        并不代表signal函数写到哪都可以,一般signal都是写在最前面。其实我们是提前注册了方法,我们并未掉用该方法罢了。用以防止信号的随机产生。

        这就像:学校的留校察看警告,并不是立马开除你。并且在这之后的犯事并不是默认的请家长,而是新的处理方式开除,并且是需要提前准备此方案。

        如果想终止,可以CRTL + \,发送3号信号。

核心转储

复习:

        在进程等待中,学到:

        pid_t waitpid(pid_t pid, int *status, int options);

        输出型参数status并不是按照整数来整体使用的。而是按照比特位的方式,将32比特位进行划分,对于应用只需要学习低16位。

        次8位是进程的退出状态,低8位的第1位是core dump标志。剩下的7位是终止信号。

        其中,当时用不到的core dump就是代表是否核心转储。

        一般而言,云服务器(生产环境)的核心转储功能是被关闭的,可以进一步确认ulimit -a 查看当前环境当中的相关的资源配置。(虚拟机默认是打开的)

        将云服务器的核心转储打开,其中有对应的选项:

        比如:ulimit -c 10240 ,core file的size改为10240。

Note:

        这个打开,仅仅是在当前会话中打开了自己的,我们如果将终端关掉也就恢复到最开始。

        此时再使用Action为Core的信号就发生了变化。

        并且,还多了一个临时文件。

        内存里,进程运行时的重要数据

  • Trem:就是简单的终止。
  • Core:终止,但是会发生核心转储。

        此处已经发生了核心转储,所以在gdb中就没有必要逐行Debug了。直接core-file 核心转储文件即可。

Core引用场景:

        代码调试的时候,遇见Core报错的时候,可以将核心转储打开。在不想定位的时候,让其先跑一次,形成Core文件,再用gdb中core-file一下,直接定位出错的位置。

验证进程等待中的core dump标记位

(此处的验证是要保证核心转储是打开的)

        此处我们写一个除数为0的浮点数错误,会产生8号信号。

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


int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if(id == 0)
    {
        // 子进程
        sleep(1);
        int a = 100;
        a /= 0;  // 会产生8号信号
    }

    int status = 0;
    waitpid(id, &status, 0);
    std::cout <<  " exit sig: " << (status & 0x7F) << " is core: " << ((status >> 7) & 1) << std::endl;
    return 0;
}

#问:为什么生产环境一般都是要关闭core dump?

         因为核心转储是会往磁盘中dump的。而大公司有大机房,里面部署了自己的服务,这个服务如果挂掉了,通过人力重启成本高。所以一般会有监控服务,定期去检测服务是否出问题,出问题会有重启对应的软件,对挂掉的服务自动重启。如果一运行就挂,就会重启,然后循环往复,磁盘中就会有大量core dump文件。

        操作系统给子进产生了SIGPIPE信号,因为管道本身没有人读了,我们再写入没有意义。所以操作系统发现了这个问题,就直接产生发送了这个信号。管道是软件,因为其通过文件在内存级的实现 —— 这个时候就叫软件条件不满足。

软件条件产生信号的样例

#问:管道,读端不过不读,并且还关闭。但是写端一直在写,会发生什么?

        操作系统会自动终止对应的写端进程,通过发送信号的方式,(13)SIGPIPE。

  1. 创建匿名管道。
  2. 让父进程进行读取,子进程进行写入。
  3. 父子进程可以通讯一段时间。
  4. 让父进程关闭读端,并waitpid(),子进程只要一直写入即可。
  5. 子进程退出,父进程waitpid()拿到子进程的退出status。
  6. 提取退出信号。
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{

    // 1. 创建匿名管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if(n == -1)
        perror("pipe");

    // 2. 让父进程进行读取,子进程进行写入
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork()");
        exit(1);
    }
    else if(id == 0)
    {
        // 子进程
        close(pipefd[0]);

        // 3.数据写入管道 - 父子进程可以通讯一段时间
        std::string buffer("hello world\n"); 
        while(true)
        {
            sleep(1);
            write(pipefd[1], buffer.c_str(), buffer.size());
        }
        exit(1);        
    }

    // 父进程
    close(pipefd[1]);
    sleep(3);

    // 3.管道数据读取 - 父子进程可以通讯一段时间 
    char* father_buffer[1024];
    memset(father_buffer, 0, 1024);
    read(pipefd[0], father_buffer, 1023);
    printf("%s", father_buffer);

    // 4. 让父进程关闭读端,并waitpid(),子进程只要一直写入即可。
    close(pipefd[0]);
    int status = 0;
    // 5. 子进程退出,父进程waitpid()拿到子进程的退出status
    waitpid(id, &status, 0);

    // 6. 提取退出信号
    std::cout << "退出码:" << (status & 0x7f) << std::endl;
    return 0;
}

系统调用接口

raise

#include <signal.h>

int raise(int sig);
向当前进程产生信号。

参数:

       sig:产生的信号

返回值:

  • 成功时返回0
  • 失败时返回非0。
#include <iostream>
#include <signal.h>
#include <unistd.h>

int main()
{
    sleep(1);
    raise(6);
    return 0;
}

kill

#include <signal.h>

int kill(pid_t pid, int sig);

向指定进程产生信号。
 

参数:

        pid:指定进程的pid

        sig:产生的信号

返回值:

  • 成功返回0。
  • 失败返回-1。
#include <iostream>
#include <signal.h>
#include <unistd.h>

int main()
{
    sleep(1);
    kill(getpid(), 6);
    return 0;
}

模拟实现一个命令行的kill命令 

        需要模拟实现,即kill -6 pid,为进一步简易的实现,我们将其简化为:./mykill 6 pidz则运用main函数的命令行参数即可:

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

static void Usage(std::string proc)
{
    std::cout << "Usage:\r\n\t" << proc << " signumber processid" << std::endl;
}


int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    kill(atoi(argv[2]), atoi(argv[1]));
    return 0;
}

        利用sleep先在bash中形成一个运行的进程,再使用我们所写的kill即可。 

abort 

#include <stdlib.h>

void abort(void);
向当前进程直接产生6号信号。

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

int main()
{
    sleep(1);
    abort();
    return 0;
}

融会贯通的理解:

        abort( ); == kill( getpid( ), 6 ); == raise( 6 ); 

#问:如何理解系统调用接口

        用户调用系统接口 -> 执行操作系统对应的系统调用代码 -> 操作系统提取参数 / 设置特定的数值 -> 操作系统向目标进程写信号 -> 修改对应进程的信号标记位 -> 进程后续会处理信号 -> 执行对应的处理动作。

闹钟

#include <unistd.h>

unsigned alarm(unsigned seconds);

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

参数:

        seconds:几秒后给当前进程发SIGALRM信号。

返回值:

        这个函数的返回值是0 或者是以前设定的闹钟时间还余下的秒数。返回上一个闹钟剩余的秒数。
  • 打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
#include <iostream>
#include <unistd.h>

int main()
{
    // 验证1s之内, 我们一共会进行多少次count++
    alarm(1);
    int count = 0;
    while(true)
    {
        std::cout << "cout: " << count++ << std::endl;
    }
    return 0;
}

#问:此处142135看似多,但是对于计算机还是太少了,如此少的原因?

        首先,此处有IO的cout(外设传输)。其次,此处用的是云服务器,是需要网络发送(长距离传输)。(cout与网络发送默认是阻塞式的IO,也就是说阻塞不发完,无法进行下一步)

单纯的计算云服务器的算力

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

uint64_t count = 0;

void catchSig(int signum)
{
    std::cout << "final cout:" << count << std::endl;
}

int main()
{
    alarm(1);
    signal(SIGALRM, catchSig);

    while(true) count++;
    return 0;
}

        设置了一个闹钟,这个闹钟一旦触发,就自动移除了。并且,我们将本应终止信号,进行了自定义动作 —— 只会cout并不会终止,于是count++会一直执行。

        所以,我们如果想终止其可以使用CTRL + \

        因为,闹钟一旦触发,就自动移除了。所以如果想实现一个,每隔一段时间就有一个闹钟触发,可以在自定义中再写一个闹钟。即:不用自己写死循环的方式,让程序能以异步的方式,周期性的执行某些任务。

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

int count = 0;

void catchSig(int signum)
{
    std::cout << count << std::endl;
    alarm(1);
}

int main()
{
    alarm(1);
    signal(SIGALRM ,catchSig);
    while(true) ++count;
    return 0;
}

 

        在此,利用闹钟实现了一个定时器功能。让进程定期的去做一些事情,比如:每隔一段时间打印count值、每隔一段时间打印日志、每隔一段时间打印使用者。

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

typedef std::function<void ()> func;
std::vector<func> callbacks;

int count = 0;

// 展示count的值
void showCount()
{
    std::cout << "final count : " << count << std::endl;
}

// 打印日志
void showLog()
{
    std::cout << "这个是日志功能" << std::endl;
}

// 展示当前用户
void logUser()
{
    if (fork() == 0)
    {
        execl("/usr/bin/who", "wko", nullptr);
        exit(1);
    }
    wait(nullptr);
}

// 定时器功能
void catchSig(int signum)
{
    for (auto &f : callbacks)
    {
        f();
    }
    std::cout << "--------------------------------" << std::endl;
    alarm(1);
}

int main()
{

    signal(SIGALRM, catchSig);

    alarm(1);

    callbacks.push_back(showCount);
    callbacks.push_back(showLog);
    callbacks.push_back(logUser);
    while (true)
        count++;

    return 0;
}

融会贯通的理解:

        我们知道,中断是操作系统的核心技术之一。即此处就可以利用闹钟写一些定时任务,每次事件到了就会去做。

        比如我们长时间连一个网站,链接定期长时间内不访问,就直接需要你重新进行登陆了,或者从新链接。

硬件异常产生信号

如何理解除0错误

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

void handler(int signum)
{
    sleep(1);
    std::cout << "获得了一个信号: " << signum << std::endl;
    // exit(1);
}

int main()
{

    signal(SIGFPE, handler);

    int a = 100;
    a /= 0;

    while(true) sleep(1);
}

理解除0错误:

1. 进行计算的是硬件CPU。

2. CPU内部是有寄存器的(状态寄存器),有对应的状态标记位,溢出标记位,操作系统会自动进行计算完毕后的检测,如果溢出标记位是1,即操作系统里面识别到有溢出问题。

        状态寄存器(位图):不进行数值保存,就保存本次计算的计算状态,有对应的状态标记位。

融会贯通的理解:

        计算完后将数据写内存里,操作系统在写入之前先进行检测,检测状态寄存器内对应的:溢出标记位是0 / 1、等。

        如果溢出位为1,操作系统里面识别到有溢出问题(确定溢出方:此时CPU内所有的数据,都是当前正在运行的进程的上下文数据),立即只要找到当前谁在运行并提取PID(因为CPU内部有一个寄存器,其中数据是通过指针指向了进程的PCB),操作系统完成发送信号的过程,进程会在合适的时候,进行处理。

#问:一旦出现硬件异常,进程一定会退出吗?

        并不是的,一般默认情况是退出,但是我们即便不退出,我们也做不了什么。因为进程状态位图,属于操作系统,我们没有进行修改。

#问:为什么会死循环?

        我们将信号进行了捕捉,并将处理动作改成,不是终止进程。这么来说我们也处理了信号,但是寄存器中的异常是一直没有被解决的。

        这个状态寄存器也是当前进程的上下文数据,出异常进程是不退的,不退于是操作系统就正常调度,于是就切下去,并且将上下文数据也一同带走了。切换回来操作系统立马发现有异常 —— 于是,操作系统不断发送8号信号。

        由于状态寄存器属于操作系统级别,我们没有操作的权限,没有办法解决,于是最好的方法就是进程终止 —— 上下文数据自动释放。

如何理解野指针 / 越界错误

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

void handler(int signum)
{
    sleep(1);
    std::cout << "获得了一个信号: " << signum << std::endl;
    // exit(1);
}

int main()
{

    signal(SIGSEGV, handler);

    int *p = nullptr;
    *p = 100;

    while(true) sleep(1);
}

理解野指针 / 越界错误: 

1. 野指针 / 越界都必须通过地址,找到目标位置。

2. 我们语言上的地址,全部都是虚拟地址。

3. 将虚拟地址转换成物理地址。

        将虚拟地址转换成物理地址,需要查页表。

查页表:

        并不是我们所学的二叉树、哈希的方式去查找的位置,软件逻辑的方式搜索即便再快也太慢了,因为虚拟地址到物理地址的转换是最高频的,所以此处用的是硬件。

4. 查页表:页表 + MMU(Memory Manager Unit,硬件)。

拓展:

        基于外设的寄存器有一种编程 —— 串口式编程。就是根据协议,根据所对应的外设寄存器,向外部发送消息 / 指令。命令、数据,给予对应的寄存器,让其去执行(驱动程序驱动外设的基本原理)。

5. 野指针 / 越界 -> 非法地址 -> MMU转换的时候,一定会报错。

Note:

        硬件异常也能产生信号(软硬件互相结合产生信号)—— 给目标进程发信号

总结

  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

                OS是进程的管理者。

  • 信号的处理是否是立即处理的?

                在合适的时候。

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

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

相关文章

java web命令学习笔记

本博文源于笔者自学java web&#xff0c;博文包含了jsp脚本代码、jsp声明、jsp表达式、注释、jsp指令等内容说明&#xff0c;并书写对应的案例。即可观看&#xff0c;也可收藏。 文章目录1.jsp脚本代码2.JSP声明3.jsp表达式4.注释5.JSP指令5.1 page指令5.2 include指令1.jsp脚本…

《霍格沃茨之遗》席卷Steam周榜 哈迷们出手了!

自上上周起&#xff0c;《霍格沃茨之遗》就打败了长期霸榜王Steam Deck&#xff0c;夺得了Steam周榜冠军&#xff0c;果然魔法的力量不容小觑。 而在最新公开的周榜&#xff08;2 月 6 日~2 月 12 日&#xff09;中&#xff0c;《霍格沃茨之遗》更是强势席卷榜单&#xff0c;四…

​网易游戏实时 HTAP 计费风控平台建设

本文整理自网易互娱资深工程师, Flink Contributor, CDC Contributor 林佳&#xff0c;在 FFA 实时风控专场的分享。本篇内容主要分为五个部分&#xff1a; 实时风控业务会话会话关联的 Flink 实现HTAP 风控平台建设提升风控结果数据能效发展历程与展望未来 众所周知&#xff…

【GlobalMapper精品教程】045:空间操作(2)——相交(Intersect)

GlobalMapper提供的空间分析(操作)的方法有:交集、并集、单并集、差异、对称差集、相交、重叠、接触、包含、等于、内部、分离等,本文主要讲述相交工具的使用。 文章目录 一、实验数据二、符号化设置三、相交运算四、结果展示五、心灵感悟一、实验数据 加载配套实验数据(…

初始C++(三):引用

文章目录一.引用的概念二.引用的使用1.引用作为输出型参数2. 引用作为函数返回值3.const引用三.引用的一些小问题四.引用和指针五.引用和指针的区别一.引用的概念 引用的作用是给一个已经存在的变量取别名&#xff0c;编译器不会为引用变量开空间&#xff0c;引用变量和被他引…

一般人我劝你不要自学软件测试!!!

本人5年测试经验&#xff0c;在学测试之前对电脑的认知也就只限于上个网&#xff0c;玩个办公软件。这里不能跑题&#xff0c;我为啥说&#xff1a;自学软件测试&#xff0c;一般人我还是劝你算了吧&#xff1f;因为我就是那个一般人&#xff01; 软件测试基础真的很简单&…

九龙证券|“春季躁动”行情要来?1月新增投资者数大增

新增投资者数量在上一年12月触及多年新低后&#xff0c;2023年1月份开端呈现反弹。 在新增投资者数量之外&#xff0c;近段时刻以来&#xff0c;包含A股商场股票成交额、北向资金净买入额、两融资金规划及成交额在内多个商场目标也呈现回暖的特征&#xff0c;目前A股商场交投氛…

SQL数据查询——连接查询

文章目录一、等值和非等值连接查询1.等值连接查询2.非等值连接查询二、自连接与非自连接三、内连接与外连接1.内连接2.外连接使用左外连接还是右外连接&#xff1f;满外连接四、UNION 的使用使用 UNION 还是 UNION ALL &#xff1f;五、各种形式集合关系的SQL实现六、语法差异注…

动态SQL使用【JavaEE】

动态SQL使用 1. if 标签 判断一个参数是否有值&#xff0c;如果没值&#xff0c;那么就会隐藏 if 中的 sql 语法&#xff1a; <if test"username!null">username#{username} </if>表达式&#xff1a;username 的参数是否为空 如果结果为 true&#xff0c…

minio前后端分离上传视频/上传大文件——前后端分离断点续传minio分片上传实现

&#x1f340;&#x1f340;&#x1f340;&#x1f340;分布式文件系统-minio&#xff1a; 第一章&#xff1a;分布式文件系统介绍与minio介绍与使用&#xff08;附minio java client 使用&#xff09;第二章&#xff1a;minio&前后端分离上传视频/上传大文件——前后端分离…

网络协议(六):网络层

网络协议系列文章 网络协议(一)&#xff1a;基本概念、计算机之间的连接方式 网络协议(二)&#xff1a;MAC地址、IP地址、子网掩码、子网和超网 网络协议(三)&#xff1a;路由器原理及数据包传输过程 网络协议(四)&#xff1a;网络分类、ISP、上网方式、公网私网、NAT 网络…

【Spark分布式内存计算框架——Spark Core】10. Spark 内核调度(中)

8.3 Spark Shuffle 首先回顾MapReduce框架中Shuffle过程&#xff0c;整体流程图如下 Spark在DAG调度阶段会将一个Job划分为多个Stage&#xff0c;上游Stage做map工作&#xff0c;下游Stage做reduce工作&#xff0c;其本质上还是MapReduce计算框架。Shuffle是连接map和reduce之…

全国空气质量排行,云贵川和西藏新疆等地空气质量更好

哈喽&#xff0c;大家好&#xff0c;春节刚刚过去&#xff0c;不知道大家是不是都开始进入工作状态了呢&#xff1f;春节期间&#xff0c;允许燃放烟花爆竹的地区的朋友们不知道都去欣赏烟花表演没有&#xff1f;其他地区的朋友们相比烟花表演可能更关心燃放烟花爆竹造成的环境…

浅谈前端设计模式:策略模式和状态模式的异同点

一、策略模式 策略模式是定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。 而且策略模式是重构小能力&#xff0c;特别适合拆分“胖逻辑”。 这个定义乍一看会有点懵&#xff0c;不过通过下面的例子就能慢慢理解它的意思。 先来看一个真实场景 某次活动要做…

测试2年还拿实习生的薪资打发我,你后悔去吧····

20年7月大学毕业&#xff0c;学的计算机科学专业。因为考研之后&#xff0c;秋招结束了。没什么更多的岗位选择&#xff0c;就想找个工作先干着&#xff0c;然后亲戚在一家大厂公司上班说要招测试&#xff0c;所以就来做测试了。 虽然都是属于计算机大类&#xff0c;但自己专业…

记一次 .NET 某游戏网站 CPU爆高分析

一&#xff1a;背景 1. 讲故事 这段时间经常有朋友微信上问我这个真实案例分析连载怎么不往下续了&#xff0c;关注我的朋友应该知道&#xff0c;我近二个月在研究 SQLSERVER&#xff0c;也写了十多篇文章&#xff0c;为什么要研究这东西呢&#xff1f; 是因为在 dump 中发现…

零入门kubernetes网络实战-13->同一宿主机上的两个网络命名空间通信方案

《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本篇文章主要是想模拟一下&#xff0c;在同一个宿主机上&#xff0c;多个网络命名空间之间如何通信&#xff1f; 有哪些可以采取的方案。 可能存在的方案…

【GD32F427开发板试用】6. 定时器运用之精确定时1s

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;hehung 之前发帖 【GD32F427开发板试用】1. 串口实现scanf输入控制LED 【GD32F427开发板试用】2. RT-Thread标准版移植 【GD32F427开发板试用…

MySQL:连explain的type类型都没搞清楚,怎敢说精通SQL优化?

我们在使用SQL语句查询表数据时&#xff0c;提前用explain进行语句分析是一个非常好的习惯。通过explain输出sql的详细执行信息&#xff0c;就可以针对性的进行sql优化。 今天我们来分析一下&#xff0c;在explain中11种不同type代表的含义以及其应用场景。 1&#xff0c;sys…

如何在dom节点上使用fixed定位?实现基于父节点而不是浏览器的滚动定位?一眼看懂,简单明了。

文章目录一、想要实现的功能场景二、父相子绝方式实现dom节点内元素滚动定位2.1、父相子绝代码2.2、实现效果三、iframe中使用fixed方式实现3.1、fixed代码3.2、实现效果四、总结一、想要实现的功能场景 想在一个node节点中实现滚动&#xff0c;但是我的文本要基于这个元素在滚…