信号的产生、处理

news2025/1/6 23:08:37

一、信号的概念

信号是linux系统提供的一种,向指定进程发送特定事件的方式。收到信号的进程,要对信号做识别和处理。信号的产生是异步的,进程在工作过程中随时可能收到信号。

信号的种类分为以下这么多种(用指令kill -l查看)

其中1-31号信号是普通信号,32-64号信号是实时信号(暂且不关注)。

二、信号的产生

1、通过kill命令,向指定的进程发送指定信号

通过代码验证:利用 man 2 signal查看接受信号的系统调用:

其中handler是一个函数指针,返回值为void,参数为int,int表示的是接收的是几号信号;该系统调用是将信号进行自定义处理,也就是捕捉信号,捕捉之后的信号一般都不会进行自己的默认处理(有例外)。下面用代码验证kill命令向进程发送的信号:

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

void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    exit(1);//异常退出

}

int main()
{

    sighandler_t n=signal(4,handler);
    while(1)
    {
        cout<<"I am working! pid:"<<getpid()<<endl;
        sleep(1);
    }

    return 0;
}

可以看到接受4号信号后,实现了自定义处理。

2、键盘产生信号

Ctrl + c 产生2号信号;Ctrl + \ 产生3号信号;Ctrl + z 产生20号信号

只需修改sighandler_t n=signal(2,handler);中的2为3 、20即可自定义处理不同的信号。

3、系统调用产生信号

①kill

kill(pid,signal) pid表示给指定的进程发送。signal表示发送几号信号。

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

void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    //exit(1);//表示异常退出

}

int main()
{

    sighandler_t n=signal(2,handler);
    int cnt=10;
    while(cnt--)
    {
        cout<<"I am working! pid:"<<getpid()<<endl;
        sleep(1);
    }
    kill(getpid(),2);
    //等价于 raise(2)
    //abort();//发送6号信号
    

    return 0;
}

②raise

raise(int signal) 表示向本进程发送signal号信号,代码如上

③abort

abort发送6号信号。

注意:

①为了防止有程序对所有信号都自定义捕捉而恶意不退出,有9号信号不允许自定义捕捉。

②如何理解信号的发送?

真正发送信号的是OS

信号在进程的task_struct结构体中其实是一个32位int位图的形式,这就是为什么普通信号有31个,且从1开始而不是从0开始。发送信号的过程其实是OS在修改指定进程pcb中的信号的指定位图。因为只有OS有这个权限去修改进程的内核结构对象,所以只有OS能发送信号。

4、软件条件

①在管道学习中,已知当管道读端关闭之后,写端如果还在写,则会发送13号信号SIGPIPE让写端进程退出。这是一种软件条件。(写端写入操作的条件不满足)

②闹钟:设定几秒后收到一个14号SIGALRM信号

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

void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    exit(1);//表示异常退出

}

int main()
{

    sighandler_t n=signal(14,handler);
    unsigned int ret=alarm(5);
    while(1)
    {
        cout<<"I am working! pid:"<<getpid()<<endl;
        sleep(1);
    }
    /*int cnt=5;
    while(cnt--)
    {
        cout<<"I am working! pid:"<<getpid()<<endl;
        sleep(1);
    }*/
    //kill(getpid(),9);
    


    //raise(2);
    //abort();//发送6号信号

    return 0;
}

OS在对一些超时的程序,会设定一个闹钟,超出这个时间之后就会向指定进程发送14信号终止。

时间:通过时间戳(整数)来比较。

操作系统要对闹钟做管理:先描述再组织

闹钟结构体struct alarm

{

    time_t expired;//未来的超时时间=seconds+Now();

    pid_t pid;

}

 对于闹钟结构体,采用最大堆最小堆的组织方式,高效组织。这样以未来的超时时间作为基准进行堆排序,只要最近的超时的闹钟未超时,后面的闹钟就都不会超时。

闹钟的返回值表示的是上一个闹钟的剩余时间。

alarm(0):表示的含义是取消闹钟,返回上一个闹钟的剩余时间。

5、异常

①除0异常

当系统有除0异常时,OS会发送8号信号SIGFPE使进程退出,当运行以下代码时,系统会一直打印:

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

void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    //exit(1);//表示异常退出

}
int main()
{
    sighandler_t n=signal(8,handler);
    int a=4;
    int b=a/0;
    return 0;
}

②野指针异常

当系统有野指针异常时,OS会发送11号信号SIGSEGV使进程退出,当运行以下代码时,系统会一直打印

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

void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    //exit(1);//表示异常退出

}

int main()
{
    sighandler_t n=signal(11,handler);
    int * p=nullptr;
    *p=100;
    return 0;
}

③产生异常的原理

a、对于除0操作

计算都要通过CPU计算,CPU内部有一个寄存器eflag,内部有一个溢出标记位,当有除0操作时,CPU是将其转换为加法,而除0操作会一直加,该硬件就会出错,然后将错误传递给OS,OS接受到这种错误之后就会处理,处理方式就是向目标进程发送信号。

当我们自定义处理信号之后,为什么会一直打印?寄存器只有一套,但是寄存器里面的数据是属于每一个进程的;当我们自定义信号处理后进程不退出后,随着进程的切换调度,寄存器先是被其他进程使用,而当本错误进程又切换到运行队列中时,寄存器会恢复上下文,此时发现又出错,又交给OS,然后就会再次发送一次信号,所以就又会打印。

所以推荐接受信号后退出进程,否则就会一直卡在错误代码(这里是除0的代码)。退出进程后,寄存器的错误数据就会清空,进程自己的异常自己承受,不用让OS承担。

b、对于野指针操作

在CPU内部有一个硬件MMU,MMU将页表的虚拟地址转换为物理地址;CR3寄存器存储的是页表的地址,MMU+CR3得到物理地址;当给定指针p指向空的时候,又对其进行赋值,MMU+CR3转换的时候,接收到的是一个错误的虚拟地址,会将错误的虚拟地址放进CR2;此时OS发现了CR2内有错误的地址,就向进程发信号了。

三、信号的处理(基础)

1、默认处理与忽略

使用 man signal 7指令查看所有信号的默认处理方式

默认处理方式为终止进程的有Core Term两种,这两种的区别:

Term:异常终止;

Core:异常终止,但会形成一个debug文件。

对于云服务器来说,默认形成debug文件的功能是关闭的;

ulimit -a查看被关闭的功能;ulimit -c 10240打开core file功能,当系统错误时会形成debug文件(core文件),这里面存储的是进程退出前运行的信息,是进程退出的时候的镜像数据。

centos系统的core dump形成的文件,文件名是以进程id为结尾的,这可能会导致无数的core文件,因为每次运行的进程id可能不同;而ubuntu系统对该bug做了修正,每次都会覆盖。

为什么云服务器要默认关闭core file(核心转储)功能?当云服务器挂了之后,有一些监控程序会让该服务器自动重启,而如果一个程序每次重启就报错,就会形成大量core 文件,会将磁盘占满。

core文件如何使用:当程序出错退出时,在makefile 文件中如果带有一个 -g选项,那么后续gdb的时候输入core-file core 就可以定位哪一行出错。core是协助我们进行debug的文件,进行事后调试。

代码验证core dump标志位

core dump标志位为1说明形成了core文件。

代码:

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

void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    //exit(1);//表示异常退出

}
int Sum(int start,int end)
{
    int sum=0;
    for(int i=start;i<=end;i++)
    {
        sum /=0;
        sum+=i;
    }
    return sum;
}

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        //child process
        int total=Sum(0,100);
        exit(0);
    }

    //father process
    int status;
    pid_t rid=waitpid(id,&status,0);
    if(rid==id)
    {
        //等待成功
        printf("exit code: %d,exit sig: %d,core dump:%d\n",(status>>8)&0xff,(status&0x7f),(status>>7)&0x1);
    }


    return 0;

}

/*int main()
{
    sighandler_t n=signal(11,handler);
    int * p=nullptr;
    *p=100;
    return 0;
}*/


/*int main()
{
    sighandler_t n=signal(8,handler);
    int a=4;
    int b=a/0;
    return 0;
}*/


/*int main()
{

    sighandler_t n=signal(14,handler);
    unsigned int ret=alarm(5);
    while(1)
    {
        cout<<"I am working! pid:"<<getpid()<<endl;
        sleep(1);
    }
    /*int cnt=5;
    while(cnt--)
    {
        cout<<"I am working! pid:"<<getpid()<<endl;
        sleep(1);
    }
    //kill(getpid(),9);
    


    //raise(2);
    //abort();//发送6号信号

    return 0;
}*/


结果:

2、自定义

利用signal系统调用,实现自定义处理,具体代码在前面。

四、信号的保存

1、一些新概念 

实际执行信号的处理动作称为信号递达。

信号从产生到递达之间的状态,叫做信号未决。(临时在PCB中保存而未被处理)

有的信号会被进程阻塞。阻塞一个信号,那么对应的信号一旦产生,将永不递达,一直未决,直到主动接触阻塞。阻塞描述的是信号要不要被递达的特点。

注:①一个信号是否处于阻塞状态,和它是否未决并没有关系。②阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2、信号保存的深入理解

(1)pending表(未决信号集)

一张位图(32位整数位图,但只用了31位),其中比特位的位置代表信号编号,比特位的内容代表信号是否收到(0 ->未收到,1->收到)。

(2)handler表

是一个函数指针数组。普通信号是1-31号数字编号,这些编号对应的handler数组的下标,OS为每一个进程都维护了这样一个handler表,例如收到2号信号就那种2号信号去索引信号处理方法。

在系统调用signal(2,handler)中,我们能够实现对2号信号的自定义捕捉的原理,其实是拿着2号信号的编号,将自己写的自定义函数指针填进2号信号对应的handler数组中的处理方法,而原默认处理方法就被覆盖了。

(3)block表

一张位图,和pending类型完全一样,其中比特位的位置代表信号编号,比特位的内容代表信号是否阻塞(0 ->未阻塞,1->阻塞)

两张位图+函数指针数组,实现了让进程识别信号。

五、信号集操作

sigset_t : linux系给用户提供的一个用户级的数据类型,但禁止用户手动设置该位图的值,而是提供了一系列信号机操作函数。

1、sigprocmask:对block位图进行修改的系统调用。

其中how:

其中old_set表示返回修改之前的位图。

返回值0成功,返回值-1失败;

2、sigpending:获取当前进程的pending位图

只用来获取,改变是通过产生信号的那5种方式。其中set是输出型参数。

3、代码验证

屏蔽2号信号;向2号信号不断发送,再获取pending信号集,就能看到由0变1的变化。再解除对2号信号的屏蔽,看到由1到0的变化。

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

void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    //exit(1);//表示异常退出

    //验证pending位图清0的操作是在handler处理前还是处理后
    cout<<"-------------------"<<endl;
    sigset_t pending_bit;
    sigpending(&pending_bit);
    PrintPending(pending_bit);

    cout<<"-------------------"<<endl;

}
void PrintPending(sigset_t& pending_bit)
{
    for(int i=31;i>=1;i--)
    {
        if(sigismember(&pending_bit,i))
        {
            cout<<1;
        }
        else cout<<0;
    }
    cout<<endl;
}


int main()
{
    sighandler_t n=signal(2,handler);
    sigset_t new_bit;
    sigset_t old_bit;
    sigemptyset(&new_bit);
    sigemptyset(&old_bit);

    sigaddset(&new_bit,2);
    //屏蔽2号信号
    sigprocmask(SIG_BLOCK,&new_bit,&old_bit);
    int cnt=20;
    while(1)
    {
        sigset_t pending_bit;
        sigpending(&pending_bit);
        cout<<"pid:"<<getpid()<<"pending:";
        PrintPending(pending_bit);
        cnt--;
        if(cnt==0)
        {
            //解除对2号信号的屏蔽
            cout<<"解除对2号信号的屏蔽: ";
             sigprocmask(SIG_SETMASK,&old_bit,&new_bit);

        }
        sleep(1);
    }
    return 0;
}

解除屏蔽,一般会立即处理当前被解除屏蔽且被pending的信号。

pending位图对应的信号在被递达之前清0。

六、信号的处理(底层理解)

三种处理方式:

    signal(2,SIG_IGN);//忽略处理
    signal(2,SIG_DFL);//默认处理
    signal(2,handler);//自定义捕捉

1、信号处理的时机和信号捕捉

信号可能不会被立即处理,而是在合适的时候处理;合适的时候指的是进程从内核态返回到用户态的时候。

当OS从内核态准备切换到用户态之前,先处理当前进程中可以递达的信号,查看信号的handler指针数组;如果是SIG_DFL,大部分就直接退出进程了;如果是SIG_IGN,那么就直接忽略;而如果是自定义捕捉,就要回到用户态执行信号处理函数(而不是回到主控制流程)。

OS不能在内核态直接执行信号处理函数,因为该函数内可能会对OS内核做修改,这会影响到OS的安全。所以该信号处理函数只能在用户态下执行。由于信号处理函数和main函数是两个执行流,不能切换,所以OS要继续回到内核态,然后再返回到用户态往下执行。

信号捕捉的过程:要经历4次状态的切换。

2、内核态和用户态

(1)深入理解地址空间——内核空间

对于每个进程,其【3,4】GB的地址空间是内核空间,其实就是OS;除了用户级页表之外,还有一个内核级页表,因为OS在电脑启动时就被加载到内存中了,是第一个软件,内核级页表指向的就是内存中的OS。这就是为什么,无论进程如何切换,我们都能找到OS。

我们进程访问OS,就是在本身的虚拟地址空间中访问的,和访问库函数(在共享区)和本身写的函数(在正文代码区)一样;区别是,因为OS要保证自己的安全,用户在地址空间内访问OS时,只能通过系统调用。

而内核级页表只需维护一份就够了,也就是说所有的进程的内核空间都是一样的,内核级页表在系统中只存在一份。

(2)键盘输入数据的过程

当键盘上有按键被按,则会通过硬件中断的方式触发CPU中断;每种设备(包括磁盘、网卡等)在事前已经被分配了中断号;当CPU触发中断时,其实是该设备对应的中断号被写入CPU寄存器中;OS在启动时已经构建了一张函数指针表,CPU则会根据中断号索引到函数指针表中的函数,键盘的中断号对应的函数就是将键盘中的数据读到内存中。

这套机制类似信号,其实是先有硬件中断,然后信号是根据这种机制用软件模仿出来的。

(3)如何理解OS正常运行?

OS是如何运行的:

操作系统的本质是一个死循环+时钟中断不断调度系统任务

如何理解系统调用:

系统提供了一张函数指针数组,只要找到特定数组下标的方法,就能执行系统调用了。具体的如何实现系统调用,如下图:

(4)内核态和用户态

也就是说,CPU保证了什么时候可以访问内核空间的地址。

注意:OS由用户态进入内核态并不一定是进行了系统调用,系统在调度时,当某个进程的时间片到时,会从调度队列中剥离下来从而从用户态进入内核态,该过程是可能发生在代码的任意位置的,所以在代码执行的任一位置都可能发生;而该进程再次被调度时则是从内核态回到用户态的过程,该过程会检测信号。

3、信号在处理时默认屏蔽本信号

另一种处理信号的方式:sigaction

其中与该函数同名的结构体结构:

代码如下:

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

//sigaction的使用 signal mask的用处
//当前如果正在处理2号信号,默认2号信号会被屏蔽
//对2号信号处理完成的时候,会自动解除对2号信号的屏蔽 避免递归处理

void PrintPending(sigset_t& pending_bit)
{
    for(int i=31;i>=1;i--)
    {
        if(sigismember(&pending_bit,i))
        {
            cout<<1;
        }
        else cout<<0;
    }
    cout<<endl;
}
void handler(int signal)
{
    cout<<"receive signal:"<<signal<<endl;
    //sleep(100);
    while(1)
    {
        sigset_t pending;
        sigpending(&pending);
        PrintPending(pending);
        sleep(1);
    }
    exit(1);
}

int main()
{
    struct sigaction act,oact;
    sigemptyset(&act.sa_mask);//设置处理2号信号时对2号屏蔽的同时,对其他信号也进行屏蔽
    sigaddset(&act.sa_mask,3);
    //有很多信号无法被屏蔽 
    act.sa_handler=handler;

    sigaction(2,&act,&oact);
    while(1)
    {
        cout<<"I am a process,pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

代码结果:

从结果中可以看出,在sa_mask中加入3时,处理2号信号的过程中3号信号也被屏蔽了。

当前如果正在处理2号信号,默认2号信号会被屏蔽;对2号信号处理完成的时候,会自动解除对2号信号的屏蔽,这是为了避免递归处理。

七、可重入函数与volatile

1、可重入函数

在执行头插时,有两行代码;在执行第一行代码时发生信号捕捉(OS从用户态进入内核态的方式不止系统调用一种);而信号捕捉内部也有头插,这样最后得到的结果中,信号捕捉内部头插的节点则会丢失;这种函数称为不可重入函数。

STL的绝大多数函数,都是不可重入函数。函数是否可重入是函数的特点,对于不可重入函数,在多线程时就需要注意;可重入函数一般只会使用内部的局部变量而不会使用全局变量。

2、volatile

代码:

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

int gflag=0;
//volatile int gflag=0;

void changedata(int signo)
{
    cout<<"receive signo:"<<signo<<endl;
    gflag=1;

}

int main()
{
    signal(2,changedata);
    while(!gflag);//while不要其他代码
    cout<<"process exit normal"<<endl;
    return 0;
}

正常编译运行后结果:

但是,编译器知道在main函数执行流中没有对 gflag做修改

CPU有两种运算:算数运算和逻辑运算

对于while(!gflag)语句,其实是CPU不断对gflag做检测,而gflag是在内存中的数据,那么CPU进行判断的话就需要不断的将gflag加载到CPU中

但编译器是可以优化编译的, -O查看所有优化级别

将优化级别改为-O1,就会出现下列问题,即发送2号信号,修改gflag之后进程仍然无法退出。

这是因为优化之后,编译器发现main函数执行流没有对gflag做修改,那么直接将gflag的值放到寄存器中;修改gflag的值只是修改内存中的gflag,而对寄存器中的值没有影响。这是寄存器隐藏了内存中的真实值

这是编译器的优化导致的问题。如何保持内存的可见性?利用volatile关键字。改完之后运行结果:

八、SIGCHILD信号

子进程退出时,会给父进程发送sigchild信号。sigchild的默认处理是ignore。

验证sigchild的存在:

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


//子进程退出时会给父进程发sigchild信号

void DoOtherthing()
{
    cout<<"I am doing other thing"<<endl;
}
void notice(int signo)
{
    cout<<"get signal: "<<signo<<" pid: "<<getpid()<<endl;
    pid_t rid=waitpid(-1,nullptr,0);//waitpid的第一个参数为-1表示等待任意一个子进程
    if(rid>0)
    {
        cout<<"wait child success,pid:"<<rid<<endl;
    }
}

int main()
{
    signal(SIGCHLD,notice);
    pid_t id=fork();
    if(id==0)
    {
        //child process
        cout<<"I am child process,pid:"<<getpid()<<endl;
        sleep(5);
        exit(1);
    }
    //父进程
    while(1)
    {
        DoOtherthing();
        sleep(1);
    }
    return 0;
}

代码结果:

但以上在信号捕捉中进程等待的问题是:

如果有多个子进程,这些子进程同时退出,那么以上代码只会等待一次,只会回收一个子进程;所以要将代码改成while循环的形式不断回收;

如果有多个子进程,有些进程退出,有些进程永远不退出。此时waitpid就会阻塞,但由于现在是在信号捕捉的逻辑中,那么就会一直停在这而不会去执行main函数中的主逻辑。此时应该选择非阻塞方式等待。代码改成下面的形式:

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

// 子进程退出时会给父进程发sigchild信号

void DoOtherthing()
{
    cout << "I am doing other thing" << endl;
}
void notice(int signo)
{
    cout << "get signal: " << signo << " pid: " << getpid() << endl;
    while (1)
    {
        pid_t rid = waitpid(-1, nullptr, WNOHANG); // waitpid的第一个参数为-1表示等待任意一个子进程
        if (rid > 0)
        {
            cout << "wait child success,pid:" << rid << endl;
        }
        else if(rid<0)
        {
            cout << "wait child success done" << endl;

            break; // rid<0说明等待失败,父进程已经没有子进程等待了
        }
        else{
            cout << "wait child success done" << endl;

            break; // 非阻塞等待方式

        }
    }
}

int main()
{
    signal(SIGCHLD, notice);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // child process
            cout << "I am child process,pid:" << getpid() << endl;
            sleep(1);
            exit(1);
        }
    }

    // 父进程
    while (1)
    {
        DoOtherthing();
        sleep(1);
    }
    return 0;
}

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

等待的目的:①获取子进程的退出信息②使子进程不再是僵尸进程

如果不关心子进程的退出信息,则可以直接设置对SIGCHILD信号的处理动作为SIG_IGN,这是最简单的方式。系统默认的IGN和用户设置的SIG_IGN是不一样的

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

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

相关文章

Node.js应用程序遇到了内存溢出的问题

vue 项目 跑起来&#xff0c;一直报错&#xff0c;内存溢出 在 文件node_modules 里 .bin > vue-cli-service.cmd 在依赖包这个文件第一行加上这个 node --max-old-space-size102400 "%~dp0\..\vue\cli-service\bin\vue-cli-service.js" %* node --max-old-s…

openGauss与GaussDB系统架构对比

openGauss与GaussDB系统架构对比 系统架构对比openGauss架构GaussDB架构 GaussDB集群管理组件 系统架构对比 openGauss架构 openGauss是集中式数据库系统&#xff0c;业务数据存储在单个物理节点上&#xff0c;数据访问任务被推送到服务节点执行&#xff0c;通过服务器的高并…

深入理解计算机系统—虚拟内存(一)

一个系统中的进程是与其他进程共享 CPU 和主存资源的。然而&#xff0c;共享主存会形成特殊的挑战。随着对 CPU 需求的增长&#xff0c;进程以某种合理的平滑方式慢了下来。但是如果太多的进程需要太多的内存&#xff0c;那么它们中的一些就根本无法运行。 为了更加有效地管理内…

九、Vue 事件处理器

文章目录 前言一、基础事件绑定:v-on 指令二、方法调用:组织有序的交互逻辑三、事件修饰符阻止冒泡与默认事件捕获与自身触发单次触发与鼠标按键区分四、按键修饰符前言 在 Vue.js 的交互世界里,事件处理器起着举足轻重的作用,它让页面从静态展示迈向动态交互,精准捕捉用户…

Quartus In-System Sources and Probes Editor 的使用说明

文章目录 前言使用说明参考资料 前言 Quartus 提供了 In-System Sources and Probes Editor 调试工具&#xff0c;通过 JTAG 接口使用该工具可以驱动和采样内部节点的逻辑值。即通过 Sources 功能来驱动 FPGA 内部信号&#xff0c;通过 Probes 功能来探测内部节点的逻辑值。在…

springboot整合Quartz实现定时任务

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1.核心概念2.基础实现2.1引入依赖2.2创建具体逻辑类2.3配置类 总结 前言 在项目中我们会有许多要进行定时执行逻辑的业务场景&#xff0c;比如定期生成日报、定…

阿里云 ECS 服务器绑定多个公网IP

阿里云 ECS 服务器绑定多个公网IP 一、弹性公网IP绑定ECS服务器 单台ECS一般只能直接绑定一个弹性公网IP&#xff0c;但是可以绑定多张弹性网卡&#xff0c;如果把弹性公网IP绑定到弹性网卡上&#xff0c;那么单台ECS就能间接绑定多个弹性公网IP。但有的服务器系统镜像可能不…

线性代数考研笔记

行列式 背景 分子行列式&#xff1a;求哪个未知数&#xff0c;就把b1&#xff0c;b2放在对应的位置 分母行列式&#xff1a;系数对应写即可 全排列与逆序数 1 3 2&#xff1a;逆序数为1 奇排列 1 2 3&#xff1a;逆序数为0 偶排列 将 1 3 2 只需将3 2交换1次就可以还原原…

LLM - 使用 LLaMA-Factory 部署大模型 HTTP 多模态服务 (4)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/144881432 大模型的 HTTP 服务&#xff0c;通过网络接口&#xff0c;提供 AI 模型功能的服务&#xff0c;允许通过发送 HTTP 请求&#xff0c;交互…

数据库知识汇总2

一. 范式 定义&#xff1a;范式是符合某一种级别的关系模式的集合。 关系数据库中的关系必须满足一定的要求。满足不同程度要求的为不同范式&#xff1b; 一个低一级范式的关系模式&#xff0c;通过模式分解&#xff08;schema decomposition&#xff09;可以转换为若干个高一…

TP 钱包插件版本的使用

目前 TokenPocket 的几个平台中&#xff0c;以 ios 和 安卓版本最为常见&#xff0c;其实很少有人知道&#xff0c;浏览器上有一个插件版本的 Tp, 用电脑多的话&#xff0c;这也是一个挺好的选择。 最新版本现在支持Chrome、Brave 浏览器、Edge&#xff08;Firefox及Opera正在…

反向传播算法的偏置更新步骤

偏置的更新步骤 假设我们有一个三层神经网络&#xff08;输入层、隐藏层和输出层&#xff09;&#xff0c;并且每层的激活函数为 sigmoid 函数。我们需要更新隐藏层和输出层的偏置。以下是详细的步骤&#xff1a; 1. 计算误差项&#xff08;Error Term&#xff09; 输出层的…

(二)当人工智能是一个函数,函数形式怎么选择?ChatGPT的函数又是什么?

在上一篇文章中&#xff0c;我们通过二次函数的例子&#xff0c;讲解了如何训练人工智能。今天&#xff0c;让我们进一步探讨&#xff1a;面对不同的实际问题&#xff0c;应该如何选择合适的函数形式&#xff1f; 一、广告推荐系统中的函数选择 1. 业务目标 想象一下&#x…

Vue3 中的插槽

Vue3 中插槽的使用&#xff0c;插槽是 Vue 中的一个特别特性&#xff0c;插槽就是模版内容。例如<h1>标题 1</h1>标题 1 就是插槽&#xff0c;Vue 是无法识别模板内容的&#xff0c;只能通过属性进行传递。Slot 主要包括默认、具名和作用域。Slot开发起来难度不大&…

单元测试3.0+ @RunWith(JMockit.class)+mock+injectable+Expectations

Jmockit使用笔记_基本功能使用Tested_Injectable_Mocked_Expectations_jmockit.class-CSDN博客 静态变量直接赋值就好&#xff0c;没必要mock了 测试框架Jmockit集合junit使用 RunWith(JMockit.class) 写在测试案例类上的注解 Tested 在测试案例中,写在我们要测试的类上…

靶机系列|VULNHUB|DC-3

描述 DC-3 是另一个专门建造的易受攻击实验室&#xff0c;旨在获得渗透测试领域的经验。 与之前的 DC 版本一样&#xff0c;这个版本的设计考虑到了初学者&#xff0c;尽管这一次只有一个标志、一个入口点&#xff0c;根本没有线索。 必须具备 Linux 技能和熟悉 Linux 命令行…

sqlserver sql转HTMM邮件发送

通过sql的形式&#xff0c;把表内数据通过邮件的形式发送出去 declare title varchar(100) DECLARE stat_date CHAR(10),create_time datetime SET stat_dateCONVERT(char(10),GETDATE(),120) SET create_timeDATEADD(MINUTE,-20,GETDATE()) DECLARE xml NVARCHAR (max) DECLAR…

MCU芯片是什么意思_有哪些作用?

MCU(Microcontroller Unit)芯片&#xff0c;即微控制单元&#xff0c;是一种集成了中央处理器(CPU)、存储器(ROM、RAM)以及各种外设接口(如输入输出引脚、定时器、串口等)的集成电路芯片。它通过超大规模集成电路技术&#xff0c;将具有数据处理能力的中央处理器、随机存储器、…

朱姆沃尔特隐身战舰:从失败到威慑

前言 "朱姆沃尔特"号驱逐舰是美国海军雄心勃勃的项目&#xff0c;旨在重塑未来海战。它融合了隐身、自动化和强大火力&#xff0c;然而由于技术问题和预算超支&#xff0c;原计划建造32艘的目标被大幅缩减&#xff0c;最终只建造了三艘。该舰的设计特点包括“穿浪逆船…

【虚拟机】VMware 16图文安装和配置 AlmaLinux OS 9.5 教程

准备工作 下载AlmaLinux ISO文件&#xff1a;从AlmaLinux官方网站&#xff08;https://almalinux.org/&#xff09;下载最新版本的ISO文件。 安装VMware Workstation&#xff1a;确保您的计算机上已安装VMware Workstation。&#xff08;注&#xff1a;我这边使用的是VMware16…