当前位置: 首页 > news >正文

【Linux】信号

祝大家新年快乐啦!!!新的一年,第一篇文章我们来谈谈Linux中的信号

目录

一、引入

二、系统内置的信号

三、前台进程和后台进程

四、signal函数

五、信号的产生

5.1 通过终端按键产生信号

5.2 调用系统函数向进程发信号

5.2.1 kill

5.2.2 raise

5.2.3 abort

5.3 由软件条件产生的信号

5.3.1 alarm

5.4 硬件异常产生的信号

六、核心转储

6.1 是什么核心转储

6.2 核心转储文件的产生

6.3 核心转储文件的使用举例

6.4 waitpid中status参数的core dump标志位

七、信号的保存

7.1 和信号相关的一些概念

7.2 信号在内核中的存储

7.3 sigset_t(信号集)

7.4 信号集操作函数

7.4.1 sigprocmask

7.4.2 sigpending

八、信号的处理

8.1 信号处理的原理

8.1.1 用户态和内核态

8.1.2 信号的捕捉

8.1.3 pending位图置0的时机

8.2 sigaction


一、引入

在生活中我们处处都可以遇到信号,比如红绿灯、闹钟、鸡叫或者是女朋友/男朋友的脸色

这些事务都有一个共性,当我们遇到它们时都会做出相应的行动

那我们为什么会做出对应的动作呢?这是因为曾经有人或事“培养”过自己。即便我们现在没有遇到这些信号,我们也知道该怎么处理它

所以我们可以认识并处理一个信号,进程也不例外,进程在没有收到信号的时候,其实它早就已经能够知道一个信号该怎么被处理了,比如kill -9

这就说明程序员当初在设计进程时,就内置了对信号的处理,当进程每收到一个信号时都会执行早已内置的代码了

但是信号的产生时间是不确定的,比如我们在处理一件重要的事情时突然响起了外卖小哥打来的电话,对于这种情况我们会先将手上的事情处理完再去取外卖

进程也不例外,其运行与收到信号具有异步性,所以在收到信号时,并不一定会直接去处理,我们将从进程收到信号一直到处理信号的这个时间段称为时间窗口,但是过了这段时间窗口进程需要对信号进行处理,这就意味着进程必须具有保存信号的能力

对于进程对信号做怎样的处理,主要有三种情况:默认动作、忽略信号、用户自定义动作

综上所述,我们对信号的讲解主要有三个部分:信号产生、信号保存、信号处理

二、系统内置的信号

我们在Linux中可以使用kill -l指令来查看系统中提供的所有信号:

我们可以看到系统中有1-31/34-64一共62种信号,该批信号分为两种:1-31编号的信号是普通信号,会进行详细的讲解;34-64编号的信号是实时信号,这部分信号我们不做重点介绍

这些普通信号在C语言中实际上是被定义的宏:

/* Signals.  */
#define	SIGHUP		1	/* Hangup (POSIX).  */
#define	SIGINT		2	/* Interrupt (ANSI).  */
#define	SIGQUIT		3	/* Quit (POSIX).  */
#define	SIGILL		4	/* Illegal instruction (ANSI).  */
#define	SIGTRAP		5	/* Trace trap (POSIX).  */
#define	SIGABRT		6	/* Abort (ANSI).  */
#define	SIGIOT		6	/* IOT trap (4.2 BSD).  */
#define	SIGBUS		7	/* BUS error (4.2 BSD).  */
#define	SIGFPE		8	/* Floating-point exception (ANSI).  */
#define	SIGKILL		9	/* Kill, unblockable (POSIX).  */
#define	SIGUSR1		10	/* User-defined signal 1 (POSIX).  */
#define	SIGSEGV		11	/* Segmentation violation (ANSI).  */
#define	SIGUSR2		12	/* User-defined signal 2 (POSIX).  */
#define	SIGPIPE		13	/* Broken pipe (POSIX).  */
#define	SIGALRM		14	/* Alarm clock (POSIX).  */
#define	SIGTERM		15	/* Termination (ANSI).  */
#define	SIGSTKFLT	16	/* Stack fault.  */
#define	SIGCLD		SIGCHLD	/* Same as SIGCHLD (System V).  */
#define	SIGCHLD		17	/* Child status has changed (POSIX).  */
#define	SIGCONT		18	/* Continue (POSIX).  */
#define	SIGSTOP		19	/* Stop, unblockable (POSIX).  */
#define	SIGTSTP		20	/* Keyboard stop (POSIX).  */
#define	SIGTTIN		21	/* Background read from tty (POSIX).  */
#define	SIGTTOU		22	/* Background write to tty (POSIX).  */
#define	SIGURG		23	/* Urgent condition on socket (4.2 BSD).  */
#define	SIGXCPU		24	/* CPU limit exceeded (4.2 BSD).  */
#define	SIGXFSZ		25	/* File size limit exceeded (4.2 BSD).  */
#define	SIGVTALRM	26	/* Virtual alarm clock (4.2 BSD).  */
#define	SIGPROF		27	/* Profiling alarm clock (4.2 BSD).  */
#define	SIGWINCH	28	/* Window size change (4.3 BSD, Sun).  */
#define	SIGPOLL		SIGIO	/* Pollable event occurred (System V).  */
#define	SIGIO		29	/* I/O now possible (4.2 BSD).  */
#define	SIGPWR		30	/* Power failure restart (System V).  */
#define SIGSYS		31	/* Bad system call.  */
#define SIGUNUSED	31

那进程要具有保存信号的能力,那该怎么保存呢?首先我们要知道一个进程是否有收到信号,以及收到了什么样的信号

对于判断一个进程是否收到了信号,这个很简单我们用一个数字表示即可:0(表示没收到)和1(表示收到了)

对于一个进程收到了什么样的信号,我们仔细观察一下普通信号的个数,一共有31个,那我们用位图表示即可,一个整型一共有32bit位,我们将每个位都用0和1表示是否收到了对应的信号即可

所以在进程的PCB中一定存在一个对应的位图结构来存储收到的信号

三、前台进程和后台进程

进程的种类有很多种,下面我们主要来说说前台和后台进程

在Linux中,我们可以使用./来将一个进程跑起来,在这个进程运行时,我们对终端输入任何指令都是没有效果的,但我们如果按下ctrl+c,会直接中断这个进程,例如:

#include<iostream>
#include<unistd.h>int main()
{while(1){std::cout << "我是一个进程,pid:" << getpid() << std::endl;sleep(1);}return 0;
}

这样的进程被称为前台进程,一般情况下前台进程只有一个,所以当我们运行起一个前台进程后,此时bash进程会退居到后台,这时我们输入的指令只能传给当前的前台进程,导致了没有任何效果,但是当我们输入ctrl+c后,会终止所有的前台进程

当然我们可以在运行进程的指令后加一个&

这样子,我们调起的进程会在后台运行:

此时我们输入指令是有效的,但是按下ctrl+c后该进程并不会终止

如果我们想终止一个在后台运行的进程就需要用到kill指令了:

四、signal函数

该函数应当在信号处理中进行讲解,但是为了方便我们实践的验证操作,在这里先进行介绍:

如果我们想让某个进程收到某个信号时不执行系统的默认动作,而是执行自定义动作,这时我们可以用到signal函数:

可以看到该函数有两个参数:sidnum和handler

● sidnum:传入信号编号,当进程再一次收到该信号时,会对应执行handler方法

● handler:传入void(*)(int)类型的函数地址,当收到sidnum信号时执行该方法

signal函数的三种常用用法如下:

  1. signal(signum, SIG_IGN):将信号signum的处理方式设置为忽略,即当接收到该信号时,程序不会做任何响应。
  2. signal(signum, SIG_DFL):将信号signum的处理方式恢复为默认,即当接收到该信号时,操作系统会按照默认操作处理该信号。
  3. signal(signum, handler):将信号signum的处理方式设置为由handler函数处理,即当接收到该信号时,会调用handler函数进行相应的处理。

下面是实例演示:

#include<iostream>
#include<unistd.h>
#include<signal.h>void handler(int signum)//handler函数被signal调用时,会传入收到的信号编号
{std::cout<<"received signal:"<<signum<<std::endl;
}int main()
{signal(2,handler);//将2号信号对应方法该为handler方法while(1){std::cout << "我是一个进程,pid:" << getpid() << std::endl;sleep(1);}return 0;
}

ctrl+c本质上就是对前台进程发送2号信号,所以在使用ctrl+c时,进程并没有终止,而是执行了handler函数中对应的方法

那这样子的话,我们将所有的普通信号对应方法都用signal函数改为handler方法,那这个进程会不会变成一个无法杀死的进程?

我们来试试看:

#include<iostream>
#include<unistd.h>
#include<signal.h>void handler(int signum)//handler函数被signal调用时,会传入收到的信号编号
{std::cout<<"received signal:"<<signum<<std::endl;
}int main()
{for(int i=1;i<=31;++i)//将所有的普通信号对应方法改为handler方法{signal(i,handler);}while(1){std::cout << "我是一个进程,pid:" << getpid() << std::endl;sleep(1);}return 0;
}

但事实并非我们所料,当我们向进程传入9号信号时,这个进程还是被终止了

原因是,操作系统的设计者早就考虑到该情况了,规定9号信号对应的方法不能被修改

五、信号的产生

5.1 通过终端按键产生信号

用户输入命令,在Shell下启动一个前台进程

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

这个信号产生的过程是有键盘的输入产生的,作为信号的来源之一

5.2 调用系统函数向进程发信号

5.2.1 kill

我们可以使用kill函数来向对应的进程传递信号:

该函数有两个参数:

pid:传入要发送信号的进程的PID

sig:传入要发送的信号编号

 kill函数的返回值为0表示成功,-1表示失败,并设置errno来指示错误的原因

下面我们来实操一下,利用main函数的两个形参来获取pid和sig(不熟悉的同学可以看到这里:【Linux】环境变量_linux 环境变量-CSDN博客):

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<string>
#include<cstring>
#include<cerrno>void User(std::string proc)
{std::cout<<"\t Useage:\n\t";std::cout<<proc<<" 信号编号 目标进程PID"<<std::endl;
}int main(int argc, char* argv[])
{if(argc!=3)//输入格式有误{User(argv[0]);//打印用户手册exit(1);}int sig=atoi(argv[1]);int target_id=atoi(argv[2]);int n=kill(target_id,sig);//调用kill来对目标进程发送信号if(n!=0)//失败打印错误码并退出{std::cout<<errno<<":"<<strerror(errno)<<std::endl;exit(2);}return 0;
}

运行效果: 

5.2.2 raise

调用该函数可以向调用它的进程发一个信号:

其中,sig参数表示要发送的信号编号

raise函数会向当前进程发送指定的信号,并返回一个非零值表示成功,返回0表示失败

下面是使用举例:

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<string>
#include<cstring>
#include<cerrno>void myhandler(int signo)
{std::cout<<"get a signal:"<<signo<<std::endl;
}int main()
{signal(2,myhandler);//修改2号信号对应方法while(1){raise(2);//自己向自己发送2号信号sleep(1);}return 0;
}

5.2.3 abort

该函数可以向调用它的进程发送6号信号,最后终止进程

下面是实例演示:

#include<iostream>
#include<unistd.h>
#include<signal.h>void myhandler(int signo)
{std::cout<<"get a signal:"<<signo<<std::endl;
}int main()
{signal(6,myhandler);//修改6号信号对应方法while(1){std::cout<<"begin"<<std::endl;abort();//自己向自己发送6号信号std::cout<<"end"<<std::endl;}return 0;
}

 

5.3 由软件条件产生的信号

我们在进程间通信的管道一期中说到过:当管道的读端被关闭时,进程再向管道写入数据就变成了一件无意义的事情,所以直接会向子进程传递13号信号(SIGPIPE)将写入进程杀掉(原文地址:【Linux】进程间通信——管道_linux任务间通信-CSDN博客)

SIGPIPE就是一种由软件条件产生的信号

下面再来介绍:alarm函数以及SIGALRM信号

5.3.1 alarm

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

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数

下面我们来进行实例演示:

#include<iostream>
#include<unistd.h>
#include<signal.h>
void myhandler(int signo)
{std::cout<<"get a signal:"<<signo<<std::endl;exit(14);
}int main()
{signal(SIGALRM,myhandler);//修改SIGALRM信号对应方法alarm(5);//5秒后向进程发送SIGALRM信号int count=0;while(1){std::cout<<count++<<std::endl;sleep(1);}return 0;
}

运行效果: 

 

下面我们来看一下,函数的返回值仍然是不是以前设定的闹钟时间还余下的秒数:

#include<iostream>
#include<unistd.h>
#include<signal.h>void myhandler(int signo)
{std::cout<<"get a signal:"<<signo<<std::endl;int n=alarm(5);//收到信号后,重置alarm函数std::cout<<"n:"<< n<<std::endl;
}int main()
{signal(SIGALRM,myhandler);//修改SIGALRM信号对应方法alarm(5);//5秒后向进程发送SIGALRM信号while(1){sleep(1);}return 0;
}

在该进程运行时,我们向该进程发送14号信号: 

可以看到,每次发送信号过后,alarm返回的值都是以前设定的闹钟时间还余下的秒数

另外将alarm的传入参数seconds的值置为0,表示取消以前设定的闹钟

下面来试试看:

#include<iostream>
#include<unistd.h>
#include<signal.h>void myhandler(int signo)
{std::cout<<"get a signal:"<<signo<<std::endl;int n=alarm(0);//取消以前设定的闹钟std::cout<<"n:"<< n<<std::endl;
}int main()
{std::cout<<"pid:"<<getpid()<<std::endl;signal(SIGALRM,myhandler);//修改SIGALRM信号对应方法alarm(10);//10秒后向进程发送SIGALRM信号int count=0;while(1){std::cout<<count++<<std::endl;sleep(1);}return 0;
}

我们在该进程运行的第6秒后向该进程发送了一个14号信号,收到该信号后自定义的方法会重置取消之前的alarm,最终导致之前设定的alarm过了10秒后也不会发送信号:

5.4 硬件异常产生的信号

我们来看到下面的代码:

#include<iostream>int main()
{int a=9;a/=0;std::cout<<"divide end..."<<std::endl;return 0;
}

运行结果:

我们可以看到在a除以0时,该进程收到信号终止了

这是由于a/0的结果会在cpu中计算出来并存储在寄存器当中,但在cpu的内部有一个状态寄存器,该寄存器会存储cpu近期计算结果是否出错溢出,当OS检查到状态寄存器有错误时,会立即向该进程发送8号信号,从而进程终止了

下面我们修改一下一下8号信号对应的默认动作来验证一下:

#include<iostream>
#include<signal.h>void myhandler(int signo)
{std::cout<<"进程确实是收到了:"<<signo<<"号信号"<<std::endl;
}int main()
{signal(SIGFPE,myhandler);int a=9;a/=0;std::cout<<"divide end..."<<std::endl;return 0;
}

 运行结果:

可是奇怪的是,为什么该进程会一直输出myhandler中对应的语句?

原因是每当该进程被OS调度时,OS都会检查到对应的状态寄存器出了问题,这时会再向该进程发送8号信号,导致了会一直执行myhandler方法

在C语言中还有一种常见的硬件异常产生的信号:野指针问题

#include<iostream>int main()
{int* p=nullptr;*p=100;std::cout<<"野指针问题 ..."<<std::endl;return 0;
}

上面野指针的崩溃原因在于:在cpu按照指针的所指向的地址寻址时,有个硬件叫做MMU,该硬件会将虚拟地址对应页表转换为物理地址,在转化的过程中如果虚拟地址在页表没有对应的物理地址将会直接MMU硬件报错,如果找到了对应的物理地址,但是没有所需要的操作权限MMU也会直接报错,OS检查到后会向该进程发送11号(SIGSEGV)信号

所以这是野指针造成进程崩溃的根本原因

六、核心转储

6.1 是什么核心转储

核心转储(Core Dump)是指在程序发生崩溃或异常终止时,操作系统将程序的内存状态和各种调试信息保存到一个二进制文件中的过程。这个文件被称为核心转储文件或核心文件(一般文件名为:core.pid)。

核心转储文件记录了程序在崩溃时的内存映像,包括程序的当前状态、堆栈跟踪、寄存器状态等。它可以提供有关程序崩溃原因和状态的详细信息,对于调试和分析程序错误非常有用。

通过核心转储文件,我们可以使用调试器(例如gdb)来还原崩溃发生时的环境,并查看程序在崩溃前的状态。调试器可以使用核心转储文件来定位错误发生的位置,分析调用栈,查找内存溢出、访问越界和其他错误的原因。这对于调试复杂的程序和确定造成崩溃的原因非常有帮助。

需要注意的是,核心转储文件可能会包含敏感信息,因此在共享或发布时需要格外小心。同时,要使用核心转储文件进行调试,通常需要使用适当的调试工具和符号文件,以还原程序的调试符号信息和源代码行号等。

6.2 核心转储文件的产生

虚拟机下一般是可以看到核心转储文件的,但是如果是在云服务器下该功能一般是被关闭的,我们可以使用ulimit -a指令来查看Linux下各种类型文件大小的配置:

其中第一个就是核心转储文件的大小配置信息:为0字节

下面我们用ulimit -c指令将其设为需要的大小:

下面我们模拟进程异常退出的场景,看看会不会产生核心转储文件:

#include<iostream>
#include<unistd.h>
#include<signal.h>int main()
{while(1){std::cout<<"模拟异常退出中,pid:"<<getpid()<<std::endl;sleep(1);}return 0;
}

下面当进程再在运行时,我们对应信号表中的信号向其发送不一样的信号,看看会发生什么: 

先向其发送1号信号,但是并没有产生核心存储文件:

先向其发送2号信号,但是也没有产生核心存储文件:

再来试试三号信号,这时产生了核心转储文件:

总结一下上面的规律,我们可以发现:只有Action为Core的信号终止的进程才能产生核心转储文件

6.3 核心转储文件的使用举例

我们打开一个核心转储文件来看看:

发现其存储的全都是二进制乱码,所以该文件绝对不会是这样让我们看的

我们可以使用gdb调试工具来查看核心转储文件,但是前提是该文件的形成进程是在debug环境下运行的,否则我们无法获取到调试信息

下面我们来举例使用:

#include<iostream>int main()
{int* p=nullptr;*p=100;//野指针std::cout<<"野指针问题 ..."<<std::endl;return 0;
}

我们现在运行上面代码形成的进程(在debug环境下进行):

可以看到该进程终止了并形成了一个核心转储文件 

现在我们使用gdb来调试试试看(对于gdb使用不熟悉的同学可以看到这里:【Linux】工具(5)——gdb_linux make debug-CSDN博客):

在进入gdb调试后,我们输入core-file指令,后面接上我们要查看的核心转储文件名

这样子就可以快速定位到程序出错的地方了,该调试方法被称为:事后调试

那为什么核心转储这么方便,在云服务器中默认是被关闭的呢?

这是因为云服务器属于生产环境,在生产环境中一般是运行着为用户提供服务的程序的,但不排除进程有挂掉的风险,一旦挂掉就必须尽快重启维持服务,这样在不断挂掉和重启的情况下,会产生大量的核心转储文件,会浪费大量的空间,所以导致了在云服务器中该功能是默认被关闭的

6.4 waitpid中status参数的core dump标志位

我们现在回到之前进程控制一期博客(【Linux】进程控制-CSDN博客)中遗留下的问题:

waitpid函数中输出型参数status的core dump标志位表示的就是,该进程异常终止后有没有形成核心转储文件(1为形成了,0表示没形成)

来段代码验证一下:

#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{int id=fork();if(id==0){std::cout<<"野指针问题 ..."<<std::endl;std::cout<<"野指针问题 ..."<<std::endl;int* p=nullptr;*p=100;//野指针std::cout<<"野指针问题 ..."<<std::endl;std::cout<<"野指针问题 ..."<<std::endl;exit(0);}int status=0;waitpid(id,&status,0);std::cout<<"exit code:"<<((status>>8)&0xFF)<<std::endl;std::cout<<"exit signal:"<<(status&0x7F)<<std::endl;std::cout<<"exit code:"<<((status>>7)&0x1)<<std::endl;return 0;
}

运行效果:

七、信号的保存

7.1 和信号相关的一些概念

我们先来介绍一些概念:

● 实际执行信号的处理动作称为信号递达(Delivery)

● 信号从产生到递达之间的状态,称为信号未决(Pending)

● 进程可以选择阻塞 (Block )某个信号

● 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

● 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

7.2 信号在内核中的存储

在Linux中信号在PCB中存储结构有三种:

pending表:位图结构(uint32_t)。每个比特位的位置,表示哪一个信号,每个比特位的内容,代表是否收到该信号

block表:位图结构(uint32_t)。每个比特位的位置,表示哪一个信号,每个比特位的内容,代表是否对应的信号该被阻塞
handler表:函数指针数组(void (*sighandler_t) (int) )。该数组的下标,表示信号编号,数组的特定下标的内容,表示该信号的递达动作

这三种结构决定了信号产生后是怎么被进程保存,并决定每种信号是否会被阻塞,以及其递达动作

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?

在Linux中是这样处理的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本期不讨论实时信号。

7.3 sigset_t(信号集)

从上面的存储方式来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

sigset_t的定义如下:

# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct{unsigned long int __val[_SIGSET_NWORDS];} __sigset_t;typedef __sigset_t sigset_t;

我们可以看到该结构就是一个数组构成的位图

7.4 信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储,从使用者的角度是不必过于关心的,我们只能调用以下函数来操作sigset_ t变量:

#include <signal.h>int sigemptyset (sigset_t *set);
//作用:清空信号集。参数:指向要操作的信号集的指针。返回:如果成功清空信号集,返回0;失败返回-1。int sigfillset (sigset_t *set);
//作用:将所有信号添加到信号集。参数:指向要操作的信号集的指针。返回:如果成功将所有信号添加到信号集,返回0;失败返回-1。int sigaddset (sigset_t *set, int signo);
//作用:向信号集中添加指定的信号。参数:指向要操作的信号集的指针,以及要添加的信号编号。返回:如果成功将信号添加到信号集,返回0;失败返回-1。int sigdelset (sigset_t *set, int signo);
//作用:从信号集中删除指定的信号。参数:指向要操作的信号集的指针,以及要删除的信号编号。返回:如果成功从信号集中删除信号,返回0;失败返回-1。int sigismember (const sigset_t *set, int signo);
//作用:检测指定的信号是否在信号集中。参数:指向要操作的信号集的指针,以及要检测的信号编号。返回:如果指定的信号在信号集中,返回1;如果不在,返回0;如果出错,返回-1。

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。

注意:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

7.4.1 sigprocmask

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

  • 参数how指定了信号屏蔽字的操作方式,可以是以下三个值之一:

    • SIG_BLOCK:将set中的信号添加到进程的当前信号屏蔽字中。
    • SIG_UNBLOCK:将set中的信号从进程的当前信号屏蔽字中移除。
    • SIG_SETMASK:将进程的当前信号屏蔽字设置为set中的值。
  • 参数set指向要设置的新信号屏蔽字的信号集。

  • 参数oset是一个可选参数,指向一个用于存储原始信号屏蔽字的信号集。如果不为NULL,则将进程的当前信号屏蔽字存储到oset中。

返回值:如果成功,返回0;如果出错,返回-1。

我们举例使用一下:

#include<iostream>
#include<unistd.h>
#include<signal.h>void showBlock(sigset_t *set)
{int signo=1;for(;signo<=31;++signo){if(sigismember(set,signo))//通过sigismember函数查找signo信号是否在set信号集中std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;
}int main()
{sigset_t set,oset;//定义信号集sigemptyset(&set);//初始化设置信号集sigemptyset(&oset);//初始化旧的信号集,用来接收sigprocmask函数返回的老信号集sigaddset(&set,2);//向新信号集中增加2号信号sigprocmask(SIG_SETMASK,&set,&oset);//将阻塞信号集全部设置为set信号集while(1){showBlock(&oset);//打印旧的信号集sleep(1);}return 0;
}

运行效果: 

我们可以看到在按下ctrl+c后该进程阻塞了该信号,并没有递达

7.4.2 sigpending

该函数可以查看进程当前的pending表

该函数接收一个指向sigset_t类型的输出参数型set,并将当前进程接收到的信号集存储在该参数中

函数的返回值为0表示成功,-1表示失败,并设置errno以指示错误的原因

下面来演示一下使用:

#include<iostream>
#include<unistd.h>
#include<signal.h>void showBlock(sigset_t *set)
{int signo=1;std::cout<<"现在pending表中存储的信号集为:";for(;signo<=31;++signo){if(sigismember(set,signo))//通过sigismember函数查找signo信号是否在set信号集中std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;
}void handler(int signo)
{std::cout<<"收到了2号信号"<<std::endl;
}int main()
{sigset_t set,oset,pending;//定义信号集sigemptyset(&set);//初始化设置信号集sigemptyset(&oset);//初始化旧的信号集,用来接收sigprocmask函数返回的老信号集sigemptyset(&pending);//初始化pending信号集,用来接收sigpending函数返回的信号集sigaddset(&set,2);//向新信号集中增加2号信号sigprocmask(SIG_SETMASK,&set,&oset);//将阻塞信号集全部设置为set信号集signal(2,handler);//自定义2号信号方法int count=0;while(1){sigpending(&pending);//接受pending信号集showBlock(&pending);//打印pending信号集sleep(1);if(count++==5){std::cout<<"2号信号阻塞解除"<<std::endl;sigprocmask(SIG_SETMASK,&oset,&set);//将阻塞信号集全部恢复为oset信号集}}return 0;
}

八、信号的处理

我们在引入的时候说过:信号的产生是异步的,当前进程可能正在做更重要的事情,所以在收到信号时,并不一定会直接去处理,需要等到合适的时候再处理

💡那什么时候是合适的时候呢?

当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的检测与处理

8.1 信号处理的原理

8.1.1 用户态和内核态

我们来看到32位下的进程地址空间:

可以看到在4GB内存的情况下,0-3GB是属于用户空间的,3-4GB是属于内核空间的

我们在之前博客中讲解的都是用户空间下的进程地址空间(不熟悉的同学可以看到这里:【Linux】进程地址空间-CSDN博客),下面我们要仔细说说内核空间的进程地址空间

在所有的进程运行起来都有其自己的内核空间和用户空间,其分别对应着两种不一样的页表,指向不一样的物理空间:

因为进程所执行的功能不一样,所以每个进程的用户级别空间是独一无二的;但是在同一个OS之下,所有进程3-4GB的内核空间都是一样的,也都可以看到同一张内核级页表,这就意味着所以的进程可以通过统一的窗口看到同一个OS!

那这样看来,每个进程度可以看到OS的进程空间,那OS运行的本质都是在各各不同进程之间的相同的内核进程地址空间中运行的!

我们每一次在自己的代码中使用系统调用函数,该进程都会到内核空间的地址当中进行函数跳转

综上所述,简而言之:当进程在跑用户进程空间中的代码时,该进程就处于用户态;当进程在跑内核进程空间中的代码时,该进程就处于内核态

💡那OS是如何区分进程在跑哪部分的代码的呢?

这是在CPU中有个名为CR3的寄存器,该寄存器有多个存储状态:3表示正在运行的进程执行的级别是用户态,0表示正在运行的进程执行的级别是内核态

💡那每个进程都有内核空间的话,岂不是每个进程都可以对OS进行访问了吗?

并不是的,只有当进程处于内核态时才能对内核空间进行访问

💡那谁来更改进程所处的状态级别呢?

我们如果想要直接访问OS的内核空间是无法做到的,但是OS提供的所有的系统调用,内部在正式执行调用逻辑的时候,会去修改执行级别!

8.1.2 信号的捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

下图就是OS信号捕捉的全过程:

💡为什么要返回用户态再执行信号的自定义方法呢?难道是内核态的进程无法访问用户进程地址空间吗?

并不是这样的,内核态的进程权限是比用户态高的,这是为了防止用户在自定义函数中进行一些非法操作

 💡OS在检测信号时,如果有多个信号需要处理,此时OS会全部处理完吗?

并不会,OS在检测信号时,有多个信号需要处理,每次只处理一个,剩下没有处理完的信号会轮到下一次

8.1.3 pending位图置0的时机

我们在sigpending函数的实际举例中可以发现,当OS处理完一个信号后会将其pending结构中对应的位图置0,那到底是在OS调用自定义函数之前就置0,还是在调用完自定义函数之后置0呢?

我们来段代码验证一下:

#include<iostream>
#include<unistd.h>
#include<signal.h>void showBlock(sigset_t *set)
{int signo=1;for(;signo<=31;++signo){if(sigismember(set,signo))//通过sigismember函数查找signo信号是否在set信号集中std::cout<<"1";elsestd::cout<<"0";}std::cout<<std::endl;
}void handler(int signo)
{sigset_t pending;sigemptyset(&pending);//初始化pending信号集,用来接收sigpending函数返回的信号集sigpending(&pending);//接受pending信号集std::cout<<"收到了2号信号,在handler函数内,pending表中存储的信号集为:";showBlock(&pending);//打印pending信号集
}int main()
{sigset_t pending;sigemptyset(&pending);//初始化pending信号集,用来接收sigpending函数返回的信号集signal(2,handler);//自定义2号信号方法while(1){std::cout<<"在handler函数外,pending表中存储的信号集为:";sigpending(&pending);//接受pending信号集showBlock(&pending);//打印pending信号集sleep(1);}return 0;
}

我们可以看到, OS调用自定义函数之前就将pending对应的位图置0了

8.2 sigaction

sigaction函数的功能与signal函数类似,但是多了一些更强大的细节:

我们可以看到该函数有三个参数:

● signum:传入要设置信号的编号。

● act:一个指向struct sigaction结构的指针,用于指定对传入信号处理的方式。struct sigaction结构定义如下:

struct sigaction
{void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void);
};

该结构体中我们重点要注意的是sa_handler和sa_mask参数,其他参数与实时信号有关我们可以默认置0。sa_handler接收要传入void(*)(int)类型的函数地址,当收到sidnum信号时执行该方法;sa_mask接收要在执行sa_handler方法时屏蔽的信号集(在未执行完sa_handler方法时,即使sa_mask信号集中的信号时会阻塞不进行处理)

● oldact(输出型参数):一个指向struct sigaction结构的指针,用于保存之前的信号处理方式的信息。

下面是使用演示:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>void showBlock(sigset_t *set)
{int signo = 1;for (; signo <= 31; ++signo){if (sigismember(set, signo)) // 通过sigismember函数查找signo信号是否在set信号集中std::cout << "1";elsestd::cout << "0";}std::cout << std::endl;
}void handler(int signo)
{sigset_t pending;int count = 20;while (count--){sigemptyset(&pending); // 初始化pending信号集,用来接收sigpending函数返回的信号集sigpending(&pending);  // 接受pending信号集std::cout << "执行2号信号对应方法,pending表中存储的信号集:";showBlock(&pending); // 打印pending信号集sleep(1);}
}int main()
{struct sigaction set, oset;memset(&set, 0, sizeof(struct sigaction));memset(&oset, 0, sizeof(struct sigaction));set.sa_handler = handler;sigemptyset(&set.sa_mask);sigaddset(&set.sa_mask, 2); // 在handler方法执行时屏蔽2号信号sigaddset(&set.sa_mask, 3); // 在handler方法执行时屏蔽3号信号sigaddset(&set.sa_mask, 4); // 在handler方法执行时屏蔽4号信号sigaction(1, &set, &oset);  // 自定义1号信号方法while (1){std::cout << "pid:" << getpid() << std::endl;sleep(1);}return 0;
}

我们可以看到在1号信号执行对应方法时,我们向该进程发送2/3/4在set.sa_mask信号集中被屏蔽的信号时,是无法做出反应的


本期博客到这里就结束了哦,内容较多,如有纰漏还请各位指出呀

最后祝大家新年快乐!万事胜意~

相关文章:

【Linux】信号

祝大家新年快乐啦&#xff01;&#xff01;&#xff01;新的一年&#xff0c;第一篇文章我们来谈谈Linux中的信号 目录 一、引入 二、系统内置的信号 三、前台进程和后台进程 四、signal函数 五、信号的产生 5.1 通过终端按键产生信号 5.2 调用系统函数向进程发信号 5…...

[NISACTF 2022]easyssrf

它提示我们输入 那我们输入file:///flag file:// 访问本地文件系统 它提醒我们输file:///fl4g 它提醒我们输ha1x1ux1u.php 看到代码stristr($file, “file”)当我们输入file它会提示我们输了 啥意思可以前面加个/ 也可以通过read读取 思路都是前面加/不等于flag绕过 filephp://…...

在Linux系统中设置全局HTTP代理的步骤与技巧

在Linux系统中&#xff0c;设置全局HTTP代理可以方便我们统一管理和控制网络请求。这不仅可以帮助我们加速网络访问&#xff0c;还可以在某些情况下绕过网络限制或实现匿名上网。下面&#xff0c;我将为你详细介绍在Linux系统中设置全局HTTP代理的步骤与技巧。 步骤一&#xf…...

即席查询框架怎么选?

怎么理解即席查询 即席查询&#xff08;Ad Hoc&#xff09;是用户根据自己的需求&#xff0c;灵活的选择查询条件&#xff0c;系统能够根据用户的选择生成相应的统计报表。即席查询与普通应用查询最大的不同是普通的应用查询是定制开发的&#xff0c;而即席查询是由用户自定义查…...

【C语言】实现双向链表

目录 &#xff08;一&#xff09;头文件 &#xff08;二&#xff09; 功能实现 &#xff08;1&#xff09;初始化 &#xff08;2&#xff09;打印链表 &#xff08;3&#xff09; 头插与头删 &#xff08;4&#xff09;尾插与尾删 &#xff08;5&#xff09;指定位置之后…...

Python操作MySQL基础

除了使用图形化工具以外&#xff0c;我们也可以使用编程语言来执行SQL从而操作数据库。在Python中&#xff0c;使用第三方库: pymysql来完成对MySQL数据库的操作。 安装第三方库pymysql 使用命令行,进入cmd&#xff0c;输入命令pip install pymysql. 创建到MySQL的数据库连接…...

【数学建模】【2024年】【第40届】【MCM/ICM】【E题 财产保险的可持续性】【解题思路】

一、题目 &#xff08;一&#xff09; 赛题原文 2024 ICM Problem E: Sustainability of Property Insurance Extreme-weather events are becoming a crisis for property owners and insurers. The world has endured “more than $1 trillion in damages from more than …...

SpringCloud--Eureka注册中心服务搭建注册以及服务发现

注意springboot以及springcloud版本&#xff0c;可能有莫名其妙的错误&#xff0c;这里使用的是springboot-2.6.13&#xff0c;springcloud-2021.0.5 一&#xff0c;Eureka-Server搭建&#xff1a; 1.创建项目&#xff1a;引入依赖 <dependency><groupId>org.sp…...

ansible shell模块 可以用来使用shell 命令 支持管道符 shell 模块和 command 模块的区别

这里写目录标题 说明shell模块用法shell 模块和 command 模块的区别 说明 shell模块可以在远程主机上调用shell解释器运行命令&#xff0c;支持shell的各种功能&#xff0c;例如管道等 shell模块用法 ansible slave -m shell -a cat /etc/passwd | grep root # 可以使用管道…...

qss的使用

参考&#xff1a;qss样式表笔记大全(二)&#xff1a;可设置样式的窗口部件列表&#xff08;上&#xff09;&#xff08;持续更新示例&#xff09;_51CTO博客_qss样式...

archlinux 使用 electron-ssr 代理 socks5

提前下载好 pacman 包 https://github.com/shadowsocksrr/electron-ssr/releases/download/v0.2.7/electron-ssr-0.2.7.pacman 首先要有 yay 和 aur 源&#xff0c;这个可以参考我之前的博客 虚拟机内使用 archinstall 安装 arch linux 2024.01.01 安装依赖 yay 安装的&#…...

macos安装local模式spark

文章目录 配置说明安装hadoop安装Spark测试安装成功 配置说明 Scala - 3.18 Spark - 3.5.0 Hadoop - 3.3.6 安装hadoop 从这里下载相应版本的hadoop下载后解压&#xff0c;配置系统环境变量 > sudo vim /etc/profile添加以下两行 export HADOOP_HOME/Users/collinsliu/…...

机器学习算法之支持向量机(SVM)

SVM恐怕大家即使不熟悉&#xff0c;也听说过这个大名吧&#xff0c;这一节我们就介绍这相爱相杀一段内容。 前言&#xff1a;在介绍一个新内容之SVM前&#xff0c;我们不觉映入眼帘的问题是为什么要引入SVM&#xff1f;吃的香&#xff0c;睡的着的情况下&#xff0c;肯定不会是…...

线性判别分析(LDA)

一、说明 LDA 是一种监督降维和分类技术。其主要目的是查找最能分隔数据集中两个或多个类的特征的线性组合。LDA 的主要目标是找到一个较低维度的子空间&#xff0c;该子空间可以最大限度地区分不同类别&#xff0c;同时保留与歧视相关的信息。 LDA 是受监督的&#xff0c;这意…...

Vue 前置导航

Vue 前置导航&#xff08;Vue Front Navigation&#xff09;是一种在 Vue.js 框架中实现导航功能的常见方式。它通常用于构建单页应用程序&#xff08;Single Page Application&#xff09;&#xff0c;通过在页面顶部或侧边栏显示导航菜单&#xff0c;使用户能够轻松切换到不同…...

串行通信,并行通信,波特率,全双工,半双工,单工等通信概念

串行通信&#xff1a; 只使用一根线来进行数据发送或者是接收&#xff0c;串行通信传输数据是一位一位进行传输 并行通信&#xff1a; 使用多跟线进行数据的发送和接收&#xff0c;并行通信可以一次传输多个数据位 波特率&#xff1a; 每秒传输数据的位数&#xff0c;决定…...

鸿蒙系统进一步学习(一):学习资料总结,少走弯路

随着鸿蒙Next的计划越来越近&#xff0c;笔者之前的鸿蒙系统扫盲系列中&#xff0c;有很多朋友给我留言&#xff0c;不同的角度的问了一些问题&#xff0c;我明显感觉到一点&#xff0c;那就是许多人参与鸿蒙开发&#xff0c;但是又不知道从哪里下手&#xff0c;因为资料太多&a…...

异步复位同步释放原则

复位信号有一个非常重要的原则&#xff0c;叫作异步复位同步释放原则。异步复位指一个寄存器的复位信号随时可以复位&#xff0c;不必考虑该寄存器的时钟信号正处在哪个相位上。同步释放是指一个寄存器的复位信号从复位态回到释放态的时机&#xff0c;必须与该寄存器的时钟信号…...

M1 Mac使用SquareLine-Studio进行LVGL开发

背景 使用Gui-Guider开发遇到一些问题&#xff0c;比如组件不全。使用LVGL官方的设计软件开发 延续上一篇使用的基本环境。 LVGL项目 新建项目 选择Arduino的项目&#xff0c;设定好分辨率及颜色。 设计UI 导出代码 Export -> Create Template Project 导出文件如图…...

web3知识体系汇总

web3.0知识体系 1.行业发展 2. web3的特点&#xff1a; 1、统一身份认证系统 2、数据确权与授权 3、隐私保护与抗审查 4、去中心化运行 Web3.0思维技术思维✖金融思维✖社群思维✖产业思维”&#xff0c;才能从容理解未来Web3.0时代的大趋势。 3.技术栈 Web3.jsSolidit…...

服务器与电脑的区别?

目录 一、什么是服务器 二、什么是电脑 三、服务器和电脑的区别 一、什么是服务器 服务器是指一种专门提供计算和存储资源、运行特定软件服务的物理或虚拟计算机。服务器主要用于接受和处理来自客户端&#xff08;如个人电脑、手机等&#xff09;的请求&#xff0c;并向客户…...

结束 代码随想录 链表章节(下一张

环形链表II 首先&#xff0c;先判断有没有环&#xff0c;像物理相对速度一样 只要 相对速度为1 那么快指针绝对会在环里追上慢指针&#xff0c;最后x 和z 的距离其实最后两个index总会相遇&#xff0c;相遇的点就是入口 class Solution { public:ListNode *detectCycle(List…...

re:从0开始的CSS学习之路 6. 字体相关属性

1. 字体相关属性 font-size 字体大小 font-family 字体的系列&#xff08;字体簇&#xff09; 可以设置多个字体&#xff0c;每个字体之间以逗号隔开 设置多个字体的目的是为了用户尽可能的支持字体 网页字体的五大类&#xff1a; serif 衬线字体 sans-serif 非衬线字体 monos…...

FPGA(基于xilinx)中PCIe介绍以及IP核XDMA的使用

Xilinx中PCIe简介以及IP核XDMA的使用 例如&#xff1a;第一章 PCIe简介以及IP核的使用 文章目录 Xilinx中PCIe简介以及IP核XDMA的使用一、PCIe总线概述1.PCIe 总线架构2.PCIe 不同版本的性能指标及带宽计算3.PCIe 接口信号 二、XDMA1.XDMA 与其它 PCIe IP 的区别2.XDMA简介 三…...

docker 运行jar包 指定配置文件

要在Docker中运行JAR包并指定配置文件&#xff0c;你可以创建一个Dockerfile来定义你的容器环境&#xff0c;并在其中指定如何运行JAR包和配置文件。下面是一个简单的例子&#xff0c;展示了如何在Dockerfile中设置这些配置&#xff1a; 第一步&#xff1a;创建 Dockerfile文件…...

‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序

遇到 vue-cli-service 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 的错误时&#xff0c;通常意味着Vue CLI没有被正确安装或配置在项目中。这可能是因为node_modules目录缺失了必要的包&#xff0c;或者局部安装的Vue CLI没有被正确设置到系统的PATH环境…...

第9讲用户信息修改实现

用户信息修改实现 后端修改用户昵称&#xff1a; /*** 更新用户昵称* param wxUserInfo* param token* return*/ RequestMapping("/updateNickName") public R updateNickName(RequestBody WxUserInfo wxUserInfo,RequestHeader String token){if(StringUtil.isNot…...

powershell 离线安装Posh-SSH

PowerShell 离线安装 Posh-SSH 模块&#xff0c;可以按照以下步骤进行&#xff1a; 从可靠的来源获取 Posh-SSH 模块的 NuGet 安装包&#xff08;.nupkg 文件&#xff09;。确保该安装包是最新版本&#xff0c;并且与你的 PowerShell 版本兼容。将 NuGet 安装包复制到你的计算…...

linux系统下vscode portable版本的c++/Cmake环境搭建002:使用 VSIX 安装VSCODE插件(暂记)

使用 VSIX 安装VSCODE插件 在 Visual Studio Code (VSCode) 中&#xff0c;你可以通过以下步骤离线安装插件&#xff1a; 获取插件的 VSIX 文件&#xff1a; 在一个联网环境中&#xff0c;访问 Visual Studio Code Marketplace&#xff0c;搜索并找到你想要的插件。 比如&am…...

PHP特性知识点总结

如果想观感更好看到图片,可以去我的gitbook或者github去看 github:https://github.com/kakaandhanhan/cybersecurity_knowledge_book-gitbook.22kaka.fun gitbook:http://22kaka.fun description: 专门出的关于php的特性比较,后面好像也有java的特性。 🏀 PHP特性知识点…...

数据库基本操作

一.DDL&#xff08;Data Definition Language&#xff09; 数据定义语言&#xff0c;该语言包括以下部分&#xff1a; 对数据库的常用操作对表结构的常用操作修改表结构 不涉及数据 不区分大小写 二.对数据库的常用操作 1.查看所有的数据库 show databases; 2.创建数据…...

【51单片机】矩阵键盘(江科大)

6.1矩阵键盘 矩阵键盘&#xff1a; 在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式 采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态 1.数码管扫描(输出扫描) 原理:显示第1位→显示第2位→显示第3位→ …… ,然后快速循环这个过程,最终实现所…...

Go语言教学(一)起源

目录 一.Go语言来源 二.Go语言应用 一.Go语言来源 Go语言&#xff0c;又称Golang&#xff0c;是Google公司于2009年11月正式对外公开的一门编程语言。它是一门静态强类型、编译型的语言&#xff0c;其语法与C相近&#xff0c;但在功能上有所改进和增加&#xff0c;如内存安全…...

口腔助手|口腔挂号预约小程序|基于微信小程序的口腔门诊预约系统的设计与实现(源码+数据库+文档)

口腔小程序目录 目录 基于微信小程序的口腔门诊预约系统的设计与实现 一、前言 二、系统功能设计 三、系统实现 1、小程序前台界面实现 2、后台管理员模块实现 四、数据库设计 1、实体ER图 2、具体的表设计如下所示&#xff1a; 五、核心代码 六、论文参考 七、最新…...

ChatGPT高效提问—prompt常见用法(续篇三)

ChatGPT高效提问—prompt常见用法&#xff08;续篇三&#xff09; 1.1 多选项 ​ 多选项技术为模型提供了一个清晰的问题或任务&#xff0c;并附带一组预先定义的潜在答案。这种方法在生成仅限于特定选项集的文本方面表现出色&#xff0c;适用于问答、文本补全和其他任务。利…...

IAR报错:Error[Pa045]: function “halUartInit“ has no prototype

在IAR工程.c文件末尾添加一个自己的函数&#xff0c;出现了报错Error[Pa045]: function "halUartInit" has no prototype 意思是没有在开头添加函数声明&#xff0c;即void halUartInit(void); 这个问题我们在keil中不会遇到&#xff0c;这是因为IAR编译器规则的一…...

C++三剑客之std::optional(一) : 使用详解

相关文章系列 C三剑客之std::optional(一) : 使用详解 C三剑客之std::any(一) : 使用 C之std::tuple(一) : 使用精讲(全) C三剑客之std::variant(一) : 使用 C三剑客之std::variant(二)&#xff1a;深入剖析 目录 1.概述 2.构建方式 2.1.默认构造 2.2.移动构造 2.3.拷贝构…...

网络安全漏洞管理十大度量指标

当前&#xff0c;网络安全漏洞所带来的风险及产生的后果&#xff0c;影响到网络空间乃至现实世界的方方面面&#xff0c;通信、金融、能源、电力、铁路、医院、水务、航空、制造业等行业各类勒索、数据泄露、供应链、钓鱼等网络安全攻击事件层出不穷。因此&#xff0c;加强对漏…...

Swift Combine 发布者订阅者操作者 从入门到精通二

Combine 系列 Swift Combine 从入门到精通一 1. Combine核心概念 你只需要了解几个核心概念&#xff0c;就能使用好 Combine&#xff0c;但理解它们非常重要。 这些概念中的每一个都通过通用协议反映在框架中&#xff0c;以将概念转化为预期的功能。 这些核心概念是&#x…...

python 笔记:shapely(形状篇)

主要是点&#xff08;point&#xff09;、线&#xff08;linestring&#xff09;、面&#xff08;surface&#xff09; 1 基本方法和属性 object.area 返回对象的面积&#xff08;浮点数&#xff09; object.bounds 返回一个&#xff08;minx, miny, maxx, maxy&#xff09;元…...

开源的JS动画框架库介绍

开源的JS动画框架库介绍 在现代网页设计中&#xff0c;动画已经成为提升用户体验的重要手段。它们不仅能够吸引用户的注意力&#xff0c;还能够帮助用户更好地理解和导航网站。JavaScript 动画框架库提供了一套丰富的动画效果&#xff0c;让开发者能够轻松地实现复杂的…...

MATLAB实现随机森林回归算法

随机森林回归是一种基于集成学习的机器学习算法&#xff0c;它通过组合多个决策树来进行回归任务。随机森林的基本思想是通过构建多个决策树&#xff0c;并将它们的预测结果进行平均或投票来提高模型的准确性和鲁棒性。 以下是随机森林回归的主要特点和步骤&#xff1a; 决策树…...

时间序列预测——BiGRU模型

时间序列预测——BiGRU模型 时间序列预测是指根据历史数据的模式来预测未来时间点的值或趋势的过程。在深度学习领域&#xff0c;循环神经网络&#xff08;Recurrent Neural Networks, RNNs&#xff09;是常用于时间序列预测的模型之一。在RNNs的基础上&#xff0c;GRU&#x…...

django中实现数据库操作

在Django中&#xff0c;数据库操作通常通过Django的ORM&#xff08;Object-Relational Mapping&#xff09;来实现。ORM允许你使用Python类来表示数据库表&#xff0c;并可以使用Python语法来查询和操作数据库。 以下是在Django中实现数据库操作的基本步骤&#xff1a; 一&am…...

使用 FFmpeg 将视频转换为 GIF 动画的技巧

使用 FFmpeg 将视频转换为 GIF 动画 FFmpeg 可以将视频转换为 GIF 动画&#xff0c;方法如下&#xff1a; 1. 准备工作 确保您已经安装了 FFmpeg。 熟悉 FFmpeg 的命令行使用。 了解 GIF 动画的基本知识。 2. 基本命令 ffmpeg -i input.mp4 output.gif 3. 参数说明 -i in…...

2024春晚纸牌魔术原理----环形链表的约瑟夫问题

一.题目及剖析 https://www.nowcoder.com/practice/41c399fdb6004b31a6cbb047c641ed8a?tabnote 这道题涉及到数学原理,有一般公式,但我们先不用公式,看看如何用链表模拟出这一过程 二.思路引入 思路很简单,就试创建一个单向循环链表,然后模拟报数,删去对应的节点 三.代码引…...

HCIA-HarmonyOS设备开发认证V2.0-轻量系统内核内存管理-静态内存

目录 一、内存管理二、静态内存2.1、静态内存运行机制2.2、静态内存开发流程2.3、静态内存接口2.4、实例2.5、代码分析&#xff08;待续...&#xff09;坚持就有收货 一、内存管理 内存管理模块管理系统的内存资源&#xff0c;它是操作系统的核心模块之一&#xff0c;主要包括…...

什么是vite,如何使用

参考&#xff1a; 主要&#xff1a;由一次业务项目落地 Vite 的经历&#xff0c;我重新理解了 Vite 预构建 vite官方文档 为什么有人说 vite 快&#xff0c;有人却说 vite 慢&#xff1f; 深入理解Vite核心原理 面向未来的前端构建工具-vite 聊一聊 Vite 的预构建和二次预构建 …...

基于大语言模型的AI Agents

代理&#xff08;Agent&#xff09;指能自主感知环境并采取行动实现目标的智能体。基于大语言模型&#xff08;LLM&#xff09;的 AI Agent 利用 LLM 进行记忆检索、决策推理和行动顺序选择等&#xff0c;把Agent的智能程度提升到了新的高度。LLM驱动的Agent具体是怎么做的呢&a…...

23种设计模式之抽象工厂模式

目录 什么是抽象工厂模式 基本结构 基本实现步骤 实现代码&#xff08;有注释&#xff09; 应用场景 简单工厂、工厂方法、抽象工厂的区别 什么是抽象工厂模式 抽象工厂模式也是一种创建型设计模式&#xff0c;提供了一系列相关或相互依赖对象的接口&#xff0c;而无需…...