【Linux】信号(上)

news2024/11/23 21:22:22

文章目录

  • 📕 信号入门
    • 生活角度的信号
    • 技术角度的信号
  • 📕 信号产生
    • 认识 signal 函数
    • 键盘产生信号
    • 通过系统调用产生信号
    • 软件条件产生信号
    • 硬件异常产生信号
  • 📕 核心转储
  • 📕 信号保存
    • 信号集函数
  • 📕 信号处理
    • 用户态与内核态
    • 处理信号

📕 信号入门

生活角度的信号

下面一个网购的过程,有利于我们理解信号。

  • 我在网上买了很多件商品,在等待不同商品快递的到来。但即便快递没有到来,我也知道快递来临时,我该怎么处理快递。也就是我能“识别快递”。

  • 当快递员到了你楼下,我也收到快递到来的通知,但是我正在打游戏,需5min之后才能去取快递。那么在在这5min之内,我并没有去取快递,但是我是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。

  • 在收到通知,再到我拿到快递期间,是有一个时间窗口的,在这段时间,我并没有拿到快递,但是我知道有一个快递已经来了。本质上是我“记住了有一个快递要去取”。

  • 当我时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,我要送给我的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。

  • 快递到来的整个过程,对我来讲是异步的,我不能准确断定快递员什么时候给我打电话。

技术角度的信号

当我们按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程。前台进程因为收到信号,进而引起进程退出

将生活例子和 Ctrl-C 信号处理过程相结合,实际上就可以理解技术角度的信号。( 进程就是 “我”,操作系统就是快递员,信号就是快递。

一个进程正在运行,突然操作系统给进程发送了一个信号(体现异步,进程并不关心信号何时产生),进程在接收这个信号之前,就已经认识这个信号,并且知道该如何处理这个信号了(执行默认动作、执行自定义动作、忽略),所以,收到信号之后,就按照规定好的处理方式来处理这个信号(不一定立即处理,因为可能进程在执行优先级更高的事情)。
不难理解,在信号产生到信号处理之间,有一段时间窗口。可是,进程不一定只收到一个信号,这就需要将信号保存起来,同样地,信号保存也是——先描述、再组织

如下,使用 kill -l 可以查看各种信号, 31 号后面的是实时信号。实时信号要求产生之后立马处理,所以不需要保存下来!1-31 号信号是普通信号,要保存对应信号是否产生

请添加图片描述

根据上述特点:1. 只需要考虑 1-31 号信号。2. 只需要保存信号有无。 3. 先描述,再组织。不难想到,可以用位图的数据结构来表示信号!!毫无疑问,这个位图是存在于 pcb 中的

假设,pcb 中存在一个 uint32_t signals; 这就是描述信号的位图结构。四个字节,32位,00000000 00000000 000000000 000000000 最高一位无意义,只需要用到低 31 位即可。比特位的位置,表示信号的编号(1号位置表示1号信号);比特位的内容,表示是否收到信号当进程收到操作系统的信号,就将位图对应位置的 0 改成 1,就表示该信号产生了

所以,发送信号的本质,实际上是直接修改特定进程的信号位图的特定比特位,从 0 变成 1。并且, pcb 是内核数据结构,只有操作系统可以进行修改,那么无论以何种方式产生信号,最后都要由操作系统进行“发送”。

📕 信号产生

认识 signal 函数

请添加图片描述

signal 可以改变指定信号的执行动作, SIGINT代表的是2号信号,下面代码中的 signal(SIGINT,handler1); 就是将 2 号信号的执行动作,改为 handle1()。

#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<cerrno>
#include<cstring>

using namespace std;

void handle1(int signum)
{
    cout<<"I get a signal:"<<signum<<endl;
}


void test1()
{
    signal(SIGINT, handle1);

    while (true)
    {
        cout << "我是一个正在运行的进程,pid:" << getpid() << endl;
        sleep(1);
    }
}

int main(int argc,char* argv[])
{
    test1();
    return 0;
}

如下运行结果,原本进程收到二号进程,应该杀死前端进程,现在变成了执行自定义动作。此外,Ctrl+C 也是一样的,发送二号进程。

signal(SIGINT,handler1); 并没有调用 handler1 方法,而是在执行**用户动作的自定义捕捉,**仅仅是改变了 2 号信号的执行动作。向该进程发送 2 号信号之后,进程才会调用 handler1 方法!

此外,在执行自定义的动作时,要求操作系统切换到用户态来执行!如果是在核心态,那么可以执行任何操作,假如自定义动作里由一些恶意代码,就会导致无法挽回的错误!!

请添加图片描述

当然,可以给所有信号设置同一个处理动作,如下。

#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<cerrno>
#include<cstring>

using namespace std;

void handler1(int signum)
{
    cout<<"I get a signal:"<<signum<<endl;
}


void test1()
{
    for(int i=1;i<=31;++i)
    {
        signal(i,handler1);
    }

    while (true)
    {
        cout << "我是一个正在运行的进程,pid:" << getpid() << endl;
        sleep(1);
    }
}

int main(int argc,char* argv[])
{
    test1();
    return 0;
}

通过下图运行结果可以看出,9 号信号是无法被替换的。因为 9 号信号是可以杀死任何进程,如果 9 号信号也被替换执行动作,那么就可以由此设计 bug ,让一个进程永远无法被杀死!

请添加图片描述

键盘产生信号

例如快捷键 Ctrl+C,产生 2 号信号;Ctrl+ / 产生 3 号信号……

由计组方面的知识,我们不难理解操作系统读取键盘的数据过程,首先,按下键盘,产生中断号,根据中断号在中断向量表中找到对应的函数指针,调用函数,读取键盘的数据。例如,操作系统读取到 Ctrl+C 之后,会将其解释成信号。

此外,还可以通过 kill 指令产生信号。

kill -X pid ,可以向指定进程(pid) 发送指定信号(X)。

在命令行通过 kill 指令,可以向目标进程发送信号。上面的运行结果已经有所展示。

通过系统调用产生信号

kill 系统调用

请添加图片描述
如上是 kill 系统调用,第一个参数是进程号,第二个参数是发送的信号编号

下面代码是对 kill 系统调用的一个实践,要求在命令行输入 ./mysignal 信号编号 进程号 就可以对目标进程发送相应的信号(生成的可执行文件是 mysignal ,所以 ./mysignal )。

#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<cerrno>
#include<cstring>

using namespace std;

void Manual(char* str)
{
    cout<<"Manual"<<endl;
    cout<<str<<"信号编号 目标进程"<<endl;
}

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

    int signum=atoi(argv[1]);
    int procpid=atoi(argv[2]);

    //cout<<procpid<<" "<<signum<<endl;

    int n=kill(procpid,signum);
    if(n != 0)
    {
        cout<<errno<<":"<<strerror(errno)<<endl;
        exit(-1);
    }

    return 0;
}


如下是执行结果。

请添加图片描述

raise 系统调用

请添加图片描述
这个系统调用,会向调用它的进程发送信号!参数是几,就发送几号信号!

abort 系统调用

请添加图片描述

调用 abort() 可以对当前进程发送指定信号(六号)!如下是测试代码以及运行结果:

#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<cerrno>
#include<cstring>

using namespace std;

void myhandler(int signo)
{
    cout << "get a signal: " << signo <<endl<<"n:"<<count<< endl;
}


int main(int argc,char* argv[])
{
    signal(SIGABRT,myhandler);

    while(true)
    {
        cout<<"test start"<<endl;
        sleep(1);
        abort();
        cout<<"test end"<<endl;
    }
    return 0;
}

根据结果我们可以看出, “test end” 并没有打印出来,也就是说——即使我们对 6 号信号设置自定义动作,但是它执行完自定义动作之后,依然会让当前进程退出!!

请添加图片描述

软件条件产生信号

软件条件,就是字面意思,即软件方面的条件。例如,在使用管道进程进程间通信的时候,如果读端关闭,那么写端就无法向管道写入数据,保持读端开启,这就是软件条件

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号(14号信号), 该信号的默认处理动作是终止当前进程。

如下,可以测试一秒钟能执行多少次 count++ ,以此来测试计算机的算力!

#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<cerrno>
#include<cstring>

using namespace std;

void myhandler(int signo)
{
    cout<<"the result is:"<<count<<endl;
    exit(1);
}


int main(int argc,char* argv[])
{
    cout<<"pid:"<<getpid()<<endl;
    signal(SIGALRM,myhandler);

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

如下是运行结果:

请添加图片描述

此外,如果闹钟还没有响,进程又收到了闹钟信号,会如何呢?用下面的代码进行测试!

#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<cerrno>
#include<cstring>

using namespace std;

void myhandler(int signo)
{
    cout << "get a signal: " << signo <<endl<<;
    int n = alarm(10);
    cout << "return: " << n << endl;
}

int main(int argc,char* argv[])
{
    cout<<"pid:"<<getpid()<<endl;
    signal(SIGALRM,myhandler);

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

如下是运行结果,其实,alarm() 的返回值,是上一个闹钟还剩下的时间!

请添加图片描述

硬件异常产生信号

写 C/C++ 程序的时候,遇到野指针、除0等操作,进程就会崩溃,它们崩溃的原因,在这里就可以得到解释!

以除0为例,如下,CPU 中有一个状态寄存器,当本次计算出现溢出问题,状态寄存器中的溢出标志位置为1,进而CPU这个硬件本身发生异常,然后操作系统识别到了硬件异常,向引起硬件异常的进程发送SIGFPE信号!(CPU中保存了当前正在运行的进程的pcb,所以可以找到它)

注意,SIGFPE信号(8号信号)是由于溢出问题产生的,不同的问题会产生不同的信号,要关注的重点不是进程退出,而是收到了几号信号,因为可以根据信号来判断进程是由于什么原因退出的!!

请添加图片描述

如下代码和运行结果,为 SIGFPE 设置自定义动作,可是它却重复打印,进程没有退出。

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

using namespace std;

void handler(int signo)
{
  cout<<"我的进程收到了:"<<signo<<"信号导致崩溃"<<endl;
}


void test1()
{
  signal(SIGFPE,handler);
  int a=10;
  a/=0;
  cout<<"10 div 0 here"<<endl;
}

int main()
{
  test1(); 

  return 0;
}

请添加图片描述

重复打印的原因是:当前进程执行除0的代码,CPU的溢出标志位置为1,出现异常。而因为进程没有退出,状态标志位属于进程的上下文,所以无论这个进程被怎么调度,状态标志位始终是异常的,并且进程也没有退出,也没有去改变溢出标志位,所以操作系统就一直检测到硬件异常!一直发送信号。

进程没有退出的原因是,我们设置了自定义捕捉。在 handler里面调用 exit 就可以退出。


执行下列代码,会出现野指针异常。

#include<iostream>
using namespace std;

int main()
{
    int* p=nullptr;
    //p=100;
    *p=100;
	return 0;
}

但是其本质却并不简单。首先,指针变量 p 里面保存的是进程地址空间里的地址,也就是虚拟地址。当执行 *p=100; 这个代码的时候,不是直接向 p 这个地址的空间写入内容,而是要先完成虚拟地址到物理地址的转换!这需要 MMU 这个硬件的协助!!

页表除了有虚拟地址到物理地址的映射关系,还有访问权限(当然还有其他标志位),这表示虚拟地址对映射到的物理地址有哪些权限(rwx)。

那么,当执行 *p = 100; 先执行虚拟地址到物理地址的转化,如果操作系统通过 MMU 在页表中没有找到 p 保存的虚拟地址到物理地址的映射关系,那么 MMU 硬件报错。如果查找到了对应的映射关系,但是,并没有写的权限,那么也无法执行这行代码,所以MMU 报错! MMU 硬件报错,会被操作系统识别到,操作系统找到当前进程的 pcb,然后对它发送信号!

请添加图片描述

📕 核心转储

操作系统可以在进程收到异常的时候,将核心代码部分进行核心转储,将内存中进程的关键数据全部 dump 到磁盘中。这个功能在云服务器上是默认关闭的,需要手动打开。

如下,凡是 Action 为 Term 的,则仅仅终止进程; Action 为 Core 的,会先进行核心转储,再终止进程。

请添加图片描述

首先可以使用 ulimit -a 指令查看一些信息,如下 core file size 默认设置为0。

在这里插入图片描述

使用 ulimit -c 指令修改 core file size 的大小,设为 1024。

在这里插入图片描述
如下,是对核心转储功能的验证!

在这里插入图片描述

当然了,如何使用核心转储生成的文件才是重点。如下,生成调试文件 mysignal(g++ 指令最后要加上 -g)。运行出错,使用 gdb 调试 mysignal,在 gdb 界面 使用 core-file [filename] 指令,就可以定位错误点!

请添加图片描述

此外,之前所说的进程等待,等待得到的数据里,有一个 core dump 标志位,就和核心转储有关。如果生成了核心转储文件, core dump 标志位就为1,否则为0。

在这里插入图片描述

📕 信号保存

下面是信号其他相关常见概念。

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

如下,在操作系统的 pcb 中,会维护三张表。

pending 表,位图结构,比特位的位置表示哪一个信号,比特位的内容表示是否收到该信号(1为收到,0为未收到)。
bloc 表,位图结构,比特位的位置表示哪一个信号,比特位的内容,代表该信号是否被阻塞。
handler 表,本质上是一个函数指针数组,存储的是一个个指针。该数组下标表示信号编号,特定下标的内容,表示该信号的递达动作(默认、忽略、自定义)。

所以,这也就可以解释,为什么在没有收到信号的时候,就已经知道要对每个信号做什么动作了!就是因为 handler 表的存在!

请添加图片描述

当然了,信号递达的自定义动作已经通过 signal 函数了解了,默认动作自然不必多说。如果想要观察到忽略的动作,以2号信号为例,可以 signal(2,SIG_IGN); 这表示将2号信号忽略,这样进程收到 2 号信号就不会做任何动作。

信号集函数

由于 block 和 pending 都是位图结构,所以有一个专门的类型 —— sigset_t 来描述。可以实例化出两个 sigset_t 的对象 s1、s2 ,分别控制 block 表 和 pending 表,控制 block 表的叫做信号屏蔽字,控制 pending 表的叫做 pending信号集

但是,光有类型还不够,也要匹配对应的方法。所以,信号集函数应运而生。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置为1,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

请添加图片描述

如果 oldset 是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。如果set是非空指针,则根据 set 更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

how 参数作用
SIG_BLCOKset 包含了我们希望添加到当前信号屏蔽字的信号,相当于 mask = mask 按位或 set
SIG_UNBLOCKset 包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于 mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为 set 所指向的值,相当于 mask = set

如下代码,用来测试 sigprocmask 可以设置 block 表。

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

using namespace std;

void showBlock(sigset_t* oset)
{
  int signo=1;
  for(;signo<=31;signo++)
  {
    if(sigismember(oset,signo)) cout<<"1";
    else cout<<"0";
  }
  cout<<endl;
}


int main()
{
   // 只是在用户层面进行设置
 sigset_t set,oset;
 sigemptyset(&set);
 sigemptyset(&oset);
 sigaddset(&set,2);  // 向 set 添加 2 号信号

 // 设置进入进程,谁调用,设置谁
 sigprocmask(SIG_SETMASK,&set,&oset);
 int cnt=0;
 while(true)
 {
    showBlock(&oset);
    sleep(1);
    cnt++;
    if(cnt == 5)
    {
      cout<<"recover block"<<endl;
      sigprocmask(SIG_SETMASK,&oset,&set);
      showBlock(&set);
      exit(1);
    }
 }
  return 0;
}

一开始调用 sigprocmask 将 block 表设置为屏蔽了 2 号信号,所以在键盘按下 Ctr l+c 的时候,2号信号被阻塞,进程没有反应。后来重新设置 block 表才得以收到2号信号。

请添加图片描述

sigpending

该函数可以得到当前进程的 pending信号集。set 是输出型参数。

请添加图片描述

📕 信号处理

当进程收到一个信号,并不一定立即处理它,而是在合适的时候处理信号,因为信号的产生是异步的,当前进程可能在做更重要的事情!

当进程从内核态切换为用户态的时候,进程会在操作系统的指导下,完成信号的检测与处理!

用户态与内核态

  • 用户态:执行自己写的代码,进程所处的状态。
  • 内核态:执行操作系统的代码,进程所处的状态。

在进程地址空间中(以32位为例),并不是 4G 的虚拟地址都存储的用户代码和数据,有 1G 的内核空间,这个空间存储的数据就是操作系统的代码。当然了,也是通过页表映射到物理内存中,映射这片区域的页表就叫做内核页表

  • 所有进程的 [0,3] GB 地址空间是不同的,所以每一个进程都拥有自己的用户级页表。
  • 所有进程的 [3,4] GB 地址空间是相同的,所有进程可以看到同一张内核级页表,所有进程可以通过统一的窗口,看到同一个 OS !
  • OS 运行的本质,其实都是在进程地址空间运行的!
  • 所以,所谓的系统调用 ,其本质就如同调用动态库中的方法,在自己的地址空间进行函数跳转并返回即可!

请添加图片描述

但是,如果不对此加以限制,就会造成用户的代码,可以随意访问操作系统的代码和数据(因为在同一个进程地址空间中,可以随意跳转),这是不可以的!所以就有了用户态和内核态!想要访问OS的代码和数据,进程就必须处于内核态!

也就是说,内核态可以访问任意代码和数据,用户态只可以访问用户的代码和数据。并且,**当前进程处于什么状态,是在 CPU 的寄存器上保存的!**例如,当前代码要进行系统调用,CPU 会先查看特定寄存器的状态,确认是否处于内核态,再进行后续动作!

那么现在面临一个问题:如何更改进程的状态? 用户肯定不能直接更改,这样无法确保安全性。实际上,操作系统提供的所有系统调用,其内部在正式调用执行逻辑的时候,都会先修改进程的状态为内核态!

处理信号

如下,是信号捕捉的完整过程,以执行自定义方法为例。

  1. 进程执行系统调用,切换到内核态。
  2. 系统调用完成,检测信号,如果收到信号并且处理动作是 SIG_DEL 或者 SIG_IGN ,即默认或者忽略,那么对应的动作都可以在由操作系统在内核态直接完成。关键是处理自定义方法!
  3. 内核态的进程,当然可以执行用户写的代码。但是,处于安全性的考虑,执行自定义方法必须要切换到用户态!这是为了防止 handler 里有对操作系统进行修改或者其他操作的代码,这在内核态是可以直接执行的;并且,如果是用户态执行,可以查到是哪个用户,如果有什么错误也可以溯源。
  4. 执行完 hander ,不可以直接跳回进程。只能再陷入内核。
  5. 最后通过 sys_sigreturn() 返回用户态。

请添加图片描述

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

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

相关文章

如何通过桥接模式重构代码?

文章目录 什么是桥接模式&#xff1f;UML结构图通用代码实现适用场景案例场景分析⽤⼀坨坨代码实现桥接模式重构代码代码实现⽀付类型桥接抽象类⽀付类型的实现定义⽀付模式接⼝测试 总结 同类的业务、同样的功能&#xff0c;怎么就你能写出来那么多if else。 很多时候你写出来…

【Web服务器】Tomcat的部署

文章目录 前言一、Tomcat 的概念1. Tomcat 核心组件1.1 什么是 servlet1.2 什么是 JSP 2. Tomcat 功能组件结构2.1 Container 结构分析 3. Tomcat 请求过程4. 配置文件4.1 安装目录4.2 conf 子目录 二、Tomcat 服务部署1. 下载并安装 JDK1.1 关闭防火墙&#xff0c;将安装 Tomc…

Milvus Lite 已交卷!轻量版 Milvus,主打就是一个轻便、无负担

想要体验世界上最快的向量数据库&#xff1f;缺少专业的工程师团队作为支撑&#xff1f;Milvus 安装环境受限&#xff1f; 别担心&#xff0c;轻量版 Milvus 来啦&#xff01; 在正式介绍 Milvus Lite 之前&#xff0c;先简单回顾一下 Milvus。Milvus 是一款开源的向量数据库&a…

logstash启动时默认连接本机节点elasticsearch问题

背景 今天在排查处理一个logstash读取kafka数据写入到hdfs的问题时候&#xff0c;发现在启动日志中多了个 logstash.outputs.elasticsearch 连接的地址是localhost:9200 部分日志如下&#xff1a; 排查过程说明 1、首先确认 logstash 启动的配置文件中的 output 配置&#x…

SonarQube Data Center Edition 10.0 Crack

使用{SonarQube}为团队和企业提供干净的代码 SonarQube Data Center Edition为开发团队提供深度集成到企业环境中的代码质量和安全解决方案;使您能够一致且可靠地部署干净的代码。 灵活性和治理&#xff1a;完美的企业代码质量工具 与您的企业环境深度集成 自我管理&#xff0…

【软件测试】软件测试的基本概念和开发模型

1. 前言 在进行软件测试的学习之前,我们要了解软件测试一些基本概念. 这些基本概念将帮助我们更加明确工作的目标以及软件测试到底要做什么. 2. 软件测试的基本概念 软件测试的基本概念有3个,分别是需求,测试用例和BUG. 2.1 需求 这里的需求还可以分为 用户需求和软件需求,用户…

【博客646】将消息队列放在Prometheus前以提高可靠性并不总是“好主意“

将队列放在Prometheus前以提高可靠性并不总是"好主意" 为了防止突发流量&#xff0c;而在prometheus前加上消息队列以达到削峰填谷的目的 架构如下&#xff1a; 应用程序将指标推送到某种形式的队列&#xff08;通常是 Kafka&#xff09;&#xff0c;暴露器二进制…

超详细IDEA创建Maven项目

文章目录 一、Maven概述二、创建Maven项目三、Maven项目简单介绍3.1 标准化的项目结构3.2 标准化的构建流程3.3 方便的依赖管理 一、Maven概述 Maven是一个专门用于管理和构建Java项目的工具。我们之所以要使用Maven&#xff0c;是因为Maven可以为我们提供一套标准化的项目结构…

Python单元测试框架《python 自动化框架 pytest》

​ 编辑切换为居中 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; Pytest 简介 pytest 是python 的一种单元测试框架&#xff0c;不python 自带的unittest 测试框架类似&#xff0c;但是比 unittest 框架使用起来更简洁&#xff0c;效率更高。根据pyt…

智警杯半决赛知识点

hive&#xff1a; 启动hadoop 启动本地连接&#xff0c;连接外网 主机就是外网IP 1.修改云主机host文件&#xff0c;添加内网ip&#xff0c;对应映射名为hadoop000&#xff0c;实现云主机自身使用root用户ssh访问hadoop000免密登录 改host文件 vim/etc/hosts 比赛时会提…

【论文阅读】(2023.05.10-2023.06.03)论文阅读简单记录和汇总

(2023.05.10-2023.06.08)论文阅读简单记录和汇总 2023/05/10&#xff1a;今天状态&#xff0c;复阳大残&#xff0c;下午淋了点雨吹了点风&#xff0c;直接躺了四个小时还是头晕- -应该是阳了没跑了。 2023/06/03&#xff1a;前两周出差复阳&#xff0c;这两周调整作息把自己又…

第十三届蓝桥杯c++b组国赛题解(还在持续更新中...)

第十三届蓝桥杯Cb组国赛真题pdf自取 试题A&#xff1a;2022 解题思路&#xff1a; 有2022个物品&#xff0c;它们的编号分别是1到2022&#xff0c;它们的价值分别等于它们的编号。也就是说&#xff0c;有2022种物品&#xff0c;物品价值等于物品编号。 从2022个物品种选取10个…

虹科案例 | 虹科Dimetix激光测距传感器可实现无接触式监测采矿设备健康状况!

Part.01 行业挑战 采矿业机器的环境条件通常非常恶劣&#xff0c;损坏的设备会导致生产力和利润下降。虹科虹科Dimetix激光测距传感器为采矿设备和危险区域机器的机器健康监测提供了非接触式解决方案。 采矿操作中的处理设备通常包括分离、混合和粉碎矿石的机械设备。许多重型…

客户线上反馈:从信息搜集到疑难 bug 排查全流程经验分享

写在前面&#xff1a;本文是我在前端团队的第三次分享&#xff0c;应该很少会有开发者写客户反馈处理流程以及 bug 排查的心得技巧&#xff0c;全文比较长&#xff0c;写了一个多星期大概1W多字&#xff08;也是我曾经2年工作的总结&#xff09;&#xff0c;如果你有耐心阅读&a…

因数据库字段可存储长度设置的小了,已达到设置字符类型的存储上限,导致数据存储时报错 及 常用的数据类型

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 使用MySQL数据库&#xff0c;因数据库字段可存储长度设置的小了&#xff0c;已达到设置字符类型的存储上限&#xff0c;导致数据存储时报错 问题描述 提示&#xff1a;这里描述项目中遇到的问题&am…

2天刷完这套八股文,offer到手啦?

前言 大家好&#xff0c;最近有不少小伙伴在后台留言&#xff0c;得准备面试了&#xff0c;又不知道从何下手&#xff01;为了帮大家节约时间&#xff0c;特意准备了一份面试相关的资料&#xff0c;内容非常的全面&#xff0c;真的可以好好补一补&#xff0c;希望大家在都能拿…

我的GIT练习Three

目录 前言 GIT安装教程 Git作者 GIT优点 GIT缺点 为什么要使用 Git GIT练习Three 总结 前言 Git 是一个分布式版本控制及源代码管理工具;Git 可以为你的项目保存若干快照&#xff0c;以此来对整个项目进行版本管理 GIT安装教程 点击进入查看教程&#xff1a;点击进入 G…

(深入浅出)详解虚拟内存

概述 我们都知道一个进程是与其他进程共享CPU和内存资源的。正因如此&#xff0c;操作系统需要有一套完善的内存管理机制才能防止进程之间内存泄漏的问题。 为了更加有效地管理内存并减少出错&#xff0c;现代操作系统提供了一种对主存的抽象概念&#xff0c;即是虚拟内存&am…

从未尝试过的 7 种处理 CSS 的方法

啊&#xff0c;Web 开发的世界——您可以从学习 HTML、JavaScript&#xff0c;当然还有 CSS 等基础知识开始。 但是一旦你在现实世界中构建应用程序&#xff0c;纯 CSS 就不再适用了。 挣扎是真的。 因此&#xff0c;今天我们将深入探讨使用 NextJS 为 React 应用程序编写 CSS…

NLP与GPT联合碰撞:大模型与小模型联合发力

目录标题 NLP是什么东西&#xff1f;Al大小模型联合发力 NLP是自然语言处理&#xff0c;而GPT是自然语言生成模型。 它们的联合碰撞结果是大模型与小模型联合发力&#xff0c;是因为大模型可以提供更好的语言理解和生成能力&#xff0c;而小模型则可以更快地进行推理和预测。 …