【Linux】深入理解进程信号机制:信号的产生、捕获与阻塞

52bc67966cad45eda96494d9b411954d.png

🎬 个人主页:谁在夜里看海.

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 时间不语,却回答了所有问题


目录

📚前言

📚一、信号的本质

📖1.异步通信

📖2.信号列表

📖3.信号发送的本质

📚二、信号的产生

📖1.终端按键

🔖core dump

📖2.系统调用

🔖kill()

🔖raise()

🔖abort()

🔖alarm()

📚三、信号的捕获

📖1.signal信号处理

🔖函数原型

🔖示例

📖2.sigaction信号处理

🔖函数原型

🔖示例 

📚四、信号的阻塞与未决

📖1.阻塞的本质

🔖未决信号

🔖阻塞信号

⚠️阻塞≠忽略

📖2.阻塞的应用

🔖函数原型

🔖示例

📚五、可重入函数

📖1.定义

🔖特性

🔖示例

📖2.volatile关键字

🔖-O1优化 


📚前言

上一章我们谈论了进程间的通信机制,由于不同进程之间有时需要相互协作,所以引入了进程间通信的概念,使得不同的进程可以共享同一份资源,完成数据的传输。进程间通信时是用户层面的通信,为什么这么说呢,因为在这个过程中,用户空间的进程通过特定的通信机制来传递数据或信息,而这些操作并不涉及操作系统的内核执行逻辑,通信的内容也是由用户自定义的。

然而系统层面也需要通信,比如一个进程执行异常时,操作系统必须对异常进程进行处理,那么有哪些处理方式呢?

✅①:直接“杀死”该进程,将进程从CPU运行队列中移除,但是存在隐患,比如文件还没关闭,资源还未释放,有内存泄漏的风险;

✅②:告知进程退出,让进程主动退出,这样可以保证资源被释放。

系统告知进程退出,不就是系统与进程间的通信嘛,这一类通信不需要用户层的参与,在操作系统底层完成,并且通信的内容也不由用户定义,而是由系统定义,这种通信方式就叫做信号

📚一、信号的本质

📖1.异步通信

在用户层的通信中,比如管道通信,我们可以明确通信何时发生,数据何时进行交换吗?答案是可以,管道通信是借助一个管道文件完成的,进程需要对管道文件进行打开和关闭操作,而这些操作在执行流中是确定的,也就是说,用户规定了进程在何时发送数据,何时接收数据,我们称这种通信为同步通信

然而在系统层的通信中,我们可以明确通信何时发生吗,就比如程序执行异常时,需要发送信号告知程序终止,我们能确定程序何时异常吗,并不能确定(如果能确定的话,我们就完全可以避免异常的发生了,就不会引发异常了),我们称这种通信为异步通信

📖2.信号列表

❓为什么操作系统需要与进程进行通信呢?

✅通常,进程和操作系统之间的通信发生在一些特殊情况下,而不是无缘无故的通信。这些特殊情况往往是有限的,比如异常、外部中断、资源不足等。针对这些情况,操作系统有相应的处理方法,例如遇到异常时,操作系统会终止进程;当外部中断发生时,操作系统会中断进程的执行并进行相应处理。因此,操作系统只需将所有可能出现的特殊情况及其对应的处理办法保存在一个集合中,当进程遇到某种情况时,操作系统就发送相应的信号进行处理,这个集合被称为信号列表。

通过kill -l命令可以查看系统定义的信号列表:

我们可以看到,每个信号都有一个编号以及宏定义名称,这些宏定义可以signal.h中找到。其中编号32号以下的信号是标准信号,它们用于处理进程中的常见情况,如进程终止、异常、退出等;32号以上的信号是是实时信号,通常由用户定义并用于更精细的进程控制。

📖3.信号发送的本质

在操作系统中,每一个信号都有一个对应的编号,这些编号的范围是1~64,所以在信号传输的过程中,我们并不需要传输具体的信号宏名称,只需要一个位图即可实现,比如传输SIGINT信号时,只需要将位图中第二位置为1,即可完成信号的传输。因此,每一个进程都会维护一个用于表示信号集的位图,每一位对应一个信号的状态。

理解了信号的本质,我们来谈谈信号的操作,即信号是如何产生的,进程对信号又是如何处理的

📚二、信号的产生

📖1.终端按键

其实,我们早就接触过信号了,平常我们写代码时,偶尔会遇到程序陷入死循环或卡住的情况,这个时候我们按住ctrl+c就可以退出进程,结束死循环,其实,按下ctrl+c的过程就是向进程发送一个SIGINT信号,用于通知进程中断当前操作,这就是第一种信号产生的方式:终端按键产生。

不止ctrl+c可以产生信号,其他按键也可以产生相应的信号,例如ctrl+\,用于发送SIGQUIT信号,该信号可以强制终止进程,这与SIGINT类似,但不同的是,SIGQUIT可以生成核心转储文件(core dump)以便于调试:

🔖core dump

Core dump(核心转储)是操作系统在进程崩溃时将该进程的内存内容保存到一个文件中的机制,能够为开发者提供有关崩溃时程序状态的详细信息。

⚠️触发core dump时,系统会向磁盘写入一个核心转储文件,用于保存程序崩溃的完整快照,但是并不是所有程序崩溃的场景都需要一个快照,因为这样会导致磁盘空间被大量占用,当磁盘空间被大量占用时,可能会引发比程序崩溃更严重的问题,所以默认情况下,禁用core dump文件

然而可以通过ulimit命令接触限制:

① 使用 ulimit -c 命令查看当前系统的核心转储限制。如果输出为 0,则表示禁用了核心转储。

② 通过 ulimit -c unlimited 命令可以取消核心转储大小限制,允许生成核心转储文件。

📖2.系统调用

🔖kill()

通常情况下,我们是通过系统调用 kill 来实现信号的发送的,例如:

kill -SIGINT (进程pid)
# 或者
kill -2 (进程pid)

这种方式等价与按键ctrl+c,都是向指定进程发送SIGINT信号。

除了在终端输入kill指令,我们还可以在函数内部调用kill()函数:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main() {pid_t pid = getpid();  // 获取当前进程IDprintf("Sending SIGINT signal to process %d\n", pid);// 向当前进程发送 SIGINT 信号if (kill(pid, SIGINT) == -1) {perror("Error sending signal");}// 除了向当前进行发送,还可以向其他进程发送 SIGINT 信号,只要知道它的pidreturn 0;
}

❓为什么是kill?

kill是“杀死”的意思,为什么发送信号的指令要取名为“杀死”呢?

✅这大概是因为,在信号被设计之初,其作用主要就是“杀死”一个进程,所以当时直接将信号发送取名为kill,到了后面,信号的种类越来越多,但是kill这个指令名称被保留了下来,沿用至今。但是不要因为指令叫做kill就认为它只能“杀死”进程,实际上它用于发送多种不同类型的信号。

除了kill()之外,其他一些与信号相关的系统调用和库函数也能触发信号的发送或控制信号的行为。 

🔖raise()

raise()函数用于向当前进程发送信号(自己给自己发信号):

#include <signal.h>
int raise(int sig);

sig表示要发送的信号。

🔖abort()

abort()函数也可以向当前进程发送信号,但与raise不同的是,它只能发送SIGABRT信号,因此函数内部不包含任何参数:

#include <stdio.h>
#include <stdlib.h>int main() {printf("Before abort\n");abort();  // 终止进程并生成核心转储printf("This line will never be executed.\n");return 0;
}
🔖alarm()

alarm()函数的作用是在指定时间之后发送一个SIGALRM信号:

#include <unistd.h>unsigned int alarm(unsigned int seconds);

参数

        seconds:设置定时器的时间,单位为秒,当 alarm() 被调用时,系统会在 seconds 秒后向当前进程发送 SIGALRM 信号。

返回值

        如果成功设置定时器,它返回剩余的时间。如果定时器已经存在,返回值是该定时器剩余的时间。

示例

#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>int main()
{alarm(5);while(1){cout<<"I am a process..."<<endl;sleep(1);}return 0;}

📚三、信号的捕获

进程对于信号的处理有以下三种方式:

忽略信号:进程可以选择忽略某个信号,即在接收该信号时,不执行任何操作;

处理信号:进程可以指定一个信号处理函数来处理指定信号;

默认处理:如果没有指定默认的信号处理函数,操作系统会采用该信号的默认行为。

📖1.signal信号处理

为了捕捉并处理信号,我们通常调用signal()函数,通过该函数,我们可以自定义信号处理行为。

🔖函数原型
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);

signum是我们要捕获的信号编号;

对于handler参数,我们有两种选项:

采用关键字:SIG_IGN,表示当前信号被忽略;SIG_DFL,表示采用默认处理方式;

传入自定义信号处理函数,函数内部带有一个int型参数,表示信号编号

🔖示例
#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>void sigcb(int sig)
{if(sig == 2)cout<<"catch a sig: "<<sig<<endl;
}int main()
{signal(2,sigcb); // 调用自定义的信号处理函数cout<<"This is a process,pid is: "<<getpid()<<endl;while(1);return 0;
}

此时通过ctrl+c发生SIGINT信号时程序执行自定义行为:

此时我们将signal的第二个参数修改成SIG_IGN,表示忽略当前信号:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>int main()
{signal(2,SIG_IGN);cout<<"This is a process,pid is: "<<getpid()<<endl;while(1);return 0;
}

此时输入ctrl+c,SIGINT信号被忽略,不执行任何行为:

📖2.sigaction信号处理

sigaction函数不仅可以指定信号处理程序,还可以设置额外的选项来控制信号处理的细节。

🔖函数原型
#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

① signum: 指定要操作的信号编号;

② act:指向一个struct sigaction 类型的结构体,用于定义信号处理行为,可设置为NULL;

③ oldact:保存之前的信号处理行为(结构体),可设置为NULL。

struct sigaction:

该结构体用于定义信号处理行为:

struct sigaction {void     (*sa_handler)(int);     // 信号处理函数,或以下宏之一:// SIG_DFL:使用默认行为// SIG_IGN:忽略信号void     (*sa_sigaction)(int, siginfo_t *, void *); // 备用信号处理函数sigset_t sa_mask;                // 在处理此信号时需要阻塞的其他信号集合int      sa_flags;               // 信号处理的标志void     (*sa_restorer)(void);   // 已废弃
};

结构体成员:

① sa_handler:指定信号处理函数(简单处理);

② sa_sigaction:用于处理更复杂信号信息(需要设置 SA_SIGINFO 标志);

③ sa_mask:在处理信号时需要阻塞的其他信号;

④ sa_flags:控制信号处理行为的选项。常用值:

        SA_RESTART:自动重启被信号中断的系统调用

        SA_SIGINFO:启用 sa_sigaction,传递更多信号信息

        SA_NOCLDWAIT:阻止僵尸进程的生成(用于 SIGCHLD

        SA_NODEFER:在处理信号时不自动阻塞该信号

🔖示例 

设置对SIGINT信号的处理行为:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>void sigcb(int sig)
{cout<<"catch a sig: "<<sig<<endl;
}int main()
{struct sigaction sa;sa.sa_handler = sigcb;sigemptyset(&sa.sa_mask);sa.sa_flags = 0;sigaction(2,&sa,nullptr);while(1);return 0;
}

 

📚四、信号的阻塞与未决

然而,对于进程来说,并不是所有信号都需要立即响应的,因为在信号到达时,进程需要立即停止当前执行流,去执行信号处理操作。例如,一个进程的部分执行内容是对一个大文件进行拷贝写入,如果在写入的途中中断了,就会导致文件不完整甚至损坏,所以进程希望写入操作执行完毕后再响应信号,此时就需要信号阻塞操作。

📖1.阻塞的本质

我们前面提到,信号发送的本质是将目标进程的信号集(位图)中相应位置的标志位置为1,实际上,进程要维护两个这样信号集,一个为阻塞信号集,一个为未决信号集

🔖未决信号

当系统向进程发送信号时,修改的就是未决信号集中的指定标志位,在未决信号是指已经由系统发送但进程因为某些原因还未处理的信号,一个信号要想被进程处理的第一个条件就是在未决信号集(Pending)中标志位为1;

🔖阻塞信号

阻塞信号(Block)就是进程希望延迟执行的信号(例如在文件写入完毕之后再执行SIGINT信号,此时就需要先将SIGINT信号阻塞,写入完毕后再取消阻塞),未决信号想要被进程执行还需要判断是否阻塞,即在阻塞信号集中的标志位是否为1,为1则阻塞:

所以一个信号想要被执行需要满足:

未决位为1,表示信号已产生;

阻塞位为0,表示信号不被阻塞

③ 其他条件,例如进程空闲,没有在执行其他信号...

⚠️阻塞≠忽略

信号被阻塞与信号被忽略是两个不同的概念:

信号被阻塞表示信号不会被进程接收,如果产生则处于未决状态(Pending);

信号被忽略是进程对信号的一种处理方式,表示进程接收信号时忽略该信号,不做处理;

📖2.阻塞的应用

🔖函数原型

sigprocmask() 是一个用于设置或获取当前进程阻塞信号集的系统调用。它可以阻塞、解除阻塞或替换进程的阻塞信号集。

#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

① how:指定对信号集的操作方式,可以取以下值:

        SIG_BLOCK:将 set 中的信号添加到当前阻塞信号集中;

        SIG_UNBLOCK:从当前阻塞信号集中移除 set 中的信号;

        SIG_SETMASK:将当前阻塞信号集替换set

② set:指向包含要阻塞或解除阻塞的信号集的指针。

③ oldset:存储之前的阻塞信号集,为NULL表示不作存储。

返回值:

成功返回 0;失败返回 -1,并设置errno。

除了上述函数,我们还需要一系列的信号集操作函数:

#include <signal.h>
// sigset_t set; // 调用下面函数前,需要先定义一个函数集变量
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置位,表示该信号集的有效信号包括系统支持的所有信号

③ sigaddset

向set指向的信号集中添加信号signo,即将signo对应的标志位置为1。

④ sigdelset

将set指向的信号集中的signo信号删除,即将signo对应的标志位置为0。

⑤ sigismember

判断set指向的信号集中是否存在信号signo,也可以理解成返回signo对应的标志位。

🔖示例

设置阻塞信号2与40,观察发生信号时的状态:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>void printsigset(sigset_t *set)
{for(int i=0;i<64;++i){if(sigismember(set,i))putchar('1');elseputchar('0');}cout<<endl;
}int main()
{cout<<"This is a process,pid is: "<<getpid()<<endl;sigset_t m,p;sigemptyset(&m);sigaddset(&m,2);sigaddset(&m,40);sigprocmask(SIG_BLOCK,&m,NULL);while(1){sigpending(&p);printsigset(&p);sleep(3);}return 0;
}

此时阻塞了信号2与40,并通过打印未决信号集,观察情况:

发现发送信号2和40后,未决信号集的第2位和第40位置为1,表示信号2和40为未决信号。

📚五、可重入函数

信号处理函数执行过程中可能被任意中断,因此信号处理函数需要使用可重入函数,以避免数据竞争、资源冲突和未定义行为。

📖1.定义

可重入函数是指函数在执行过程中可以被中断,并且在中断期间又可以安全地被调用,且两次调用互不干扰。

🔖特性

① 无全局变量的依赖: 函数内部不依赖于全局或静态变量,或者对这些变量的操作是线程安全的(原子性操作)

② 无静态数据的修改: 函数不能修改其内部的静态变量,因为多次调用可能导致状态不一致

③ 无不可重入函数调用: 函数只能调用其他可重入函数

④ 使用局部资源: 函数仅使用局部变量或参数,不会影响其他调用的状态。 

🔖示例

情况一:两个线程并发地执行以下代码,假设a是全局变量,初始为0,那么输出结果会是什么?

   void foo(){a=a+1;printf("%d ",a);}

✅答案:1 1 、1 2 、2 1 、2 2 都有可能

分析:

a=a+1并不是原子性操作,怎么理解呢?

原子性操作只有未执行执行完毕两种情况,而a=a+1的操作流程是:

①将a从全局数据区提取到寄存器中;②在寄存器中完成+1操作;③将a返回到全局数据区

线程之间是共享虚拟地址空间的(除了栈区,线程之间各自拥有一块栈区空间),所以它们在访问全局变量a的时候会产生竞争访问,它们可能同时将a提取到各自的寄存器中,也有可能进程1先提取,修改完毕返回后进程2才提取,此时进程2提取的就是已经经历过一次+1的a:

情况二:那如果此时a是局部变量呢?

✅答案:只会是 1 1。

因为线程之间各自拥有一块栈空间,a是局部变量的话,那么在两个线程之间各有一份,即线程会在各自的栈空间中提取a到寄存器并返回+1后的结果,不存在竞争访问问题,执行结果只会是1 1。

情况一的foo函数是不可重入函数,因为存在竞争访问;情况二的foo函数是可重入函数,因为没有使用全局变量(只存在于栈空间),不存在竞争访问。

📖2.volatile关键字

看下面代码:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>int flag = 0;
void handler(int sig)
{if(sig == 2){cout<<"change flag 0 to 1"<<endl;flag = 1;}
}int main()
{signal(2,handler);while(!flag);exit(0);
}

在标准情况下键入CTRL-C ,2号信号被捕捉,执行自定义动作,修改flag=1,while条件不满足,退出循 环,进程退出:

# makefile
test:volatile.cppg++ -o $@ $^ #标准情况.PHONY:clean
clean:rm -f test

但是在优化情况下,键入CTRL-C ,2号信号被捕捉,执行自定义动作,修改flag=1,但是while条件依旧满足,进程继续运行!

test:volatile.cppg++ -o $@ $^ -O1 #优化情况.PHONY:clean
clean:rm -f test

这是为什么呢?

🔖-O1优化 

-O1是编译器的一个优化级别,在当前级别下,编译器会对代码进行性能优化, 原本存储在全局数据区的变量flag被直接存储到了寄存器,signal函数调用中,对flag值的修改是在另一个寄存器中进行的,而while循环中对flag的判断还是在原先的寄存器中提取,导致signal函数中对flag的修改没有作用到while循环中,键入CTRL-C最终不会退出进程。

为了解决编译器优化带来的这一潜在问题,我们可以使用volatile关键字修饰flag的定义,表示flag不被优化,这样就保证了signal中对flag的修改可以同步到while循环中:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <signal.h>volatile int flag = 0; // volatile修饰
void handler(int sig)
{if(sig == 2){cout<<"change flag 0 to 1"<<endl;flag = 1;}
}int main()
{signal(2,handler);while(!flag);exit(0);
}

在优化情况下,键入CTRL-C ,2号信号被捕捉,执行自定义动作,修改flag=1,while条件不满足,退出循 环,进程退出:

test:volatile.cppg++ -o $@ $^ -O1 #优化情况.PHONY:clean
clean:rm -f test


以上就是【文件操作的艺术——从基础到精通】的全部内容,欢迎指正~ 

码文不易,还请多多关注支持,这是我持续创作的最大动力!  

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

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

相关文章

【西门子PLC.博途】——面向对象编程及输入输出映射FC块

当我们做面向对象编程的时候&#xff0c;需要用到输入输出的映射。这样建立的变量就能够被复用&#xff0c;从而最大化利用了我们建立的udt对象。 下面就来讲讲映射是什么。 从本质上来说&#xff0c;映射就是拿实际物理对象对应程序虚拟对象&#xff0c;假设程序对象是I0.0&…

WebRTC服务质量(04)- 重传机制(01) RTX NACK概述

WebRTC服务质量&#xff08;01&#xff09;- Qos概述 WebRTC服务质量&#xff08;02&#xff09;- RTP协议 WebRTC服务质量&#xff08;03&#xff09;- RTCP协议 WebRTC服务质量&#xff08;04&#xff09;- 重传机制&#xff08;01) RTX NACK概述 WebRTC服务质量&#xff08;…

MySQL索引的理解

MySQL与磁盘的交互 根据冯诺依曼结构体系&#xff0c;我们知道我们任何上层的应用想要去访问磁盘就必须要通过内存来访问&#xff0c;MySQL作为一款储存数据的服务&#xff0c;肯定是很多时间要用来访问磁盘。而大量访问磁盘一定会影响运行效率的在innoDB的存储引擎下为了减少…

spring集成ehcache

目录 一、前言二、使用ehcache注解方式2.1、配置2.2、使用示例三、直接读取xml方式3.1、配置3.2、使用示例3.3、结果说明一、前言 问题背景:一些业务场景中,一些数据往往是长期不变更,但数据使用较多。为了满足系统性能需要,可以借助本地缓存去使用,保证同一个JVM实例只保…

分布式全文检索引擎ElasticSearch-数据的写入存储底层原理

一、数据写入的核心流程 当向 ES 索引写入数据时&#xff0c;整体流程如下&#xff1a; 1、客户端发送写入请求 客户端向 ES 集群的任意节点&#xff08;称为协调节点&#xff0c;Coordinating Node&#xff09;发送一个写入请求&#xff0c;比如 index&#xff08;插入或更…

前端解析超图的iserver xml

前端解析超图的iserver xml const res await axios.get(url)const xmlDom new DOMParser().parseFromString(res.data, text/xml);// 获取versionconst version xmlDom.getElementsByTagNameNS(*, ServiceTypeVersion)[0].textContent// 获取layerconst layerDom xmlDom.ge…

Maven 生命周期

文章目录 Maven 生命周期- Clean 生命周期- Build 生命周期- Site 生命周期 Maven 生命周期 Maven 有以下三个标准的生命周期&#xff1a; Clean 生命周期&#xff1a; clean&#xff1a;删除目标目录中的编译输出文件。这通常是在构建之前执行的&#xff0c;以确保项目从一个…

Android Studio AI助手---Gemini

从金丝雀频道下载最新版 Android Studio&#xff0c;以利用所有这些新功能&#xff0c;并继续阅读以了解新增内容。 Gemini 现在可以编写、重构和记录 Android 代码 Gemini 不仅仅是提供指导。它可以编辑您的代码&#xff0c;帮助您快速从原型转向实现&#xff0c;实现常见的…

倒排单词C++

描述 编写程序&#xff0c;读入一行英文(只包含字母和空格&#xff0c;单词间以单个空格分隔)&#xff0c;将所有单词的顺序倒排并输出&#xff0c;依然以单个空格分隔。 输入描述 输入为一个字符串&#xff08;字符串长度至多为100&#xff09;。 输出描述 共一行&#xff0c;…

#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍04-盲SQL注入(Blind SQL Injection)

免责声明 本教程仅为合法的教学目的而准备&#xff0c;严禁用于任何形式的违法犯罪活动及其他商业行为&#xff0c;在使用本教程前&#xff0c;您应确保该行为符合当地的法律法规&#xff0c;继续阅读即表示您需自行承担所有操作的后果&#xff0c;如有异议&#xff0c;请立即停…

Scala 的迭代器

迭代器定义&#xff1a;迭代器不是一种集合&#xff0c;它是一种用于访问集合的方法。 迭代器需要通过集合对应的迭代器调用迭代器的方法来访问。 支持函数式编程风格&#xff0c;便于链式操作。 创建一个迭代器&#xff0c;相关代码如下&#xff1a; object Test {def mai…

Spring整合Redis基本操作步骤

Spring 整合 Redis 操作步骤总结 1. 添加依赖 首先&#xff0c;在 pom.xml 文件中添加必要的 Maven 依赖。Redis 相关的依赖包括 Spring Boot 的 Redis 启动器和 fastjson&#xff08;如果需要使用 Fastjson 作为序列化工具&#xff09;&#xff1a; <!-- Spring Boot Re…

底层理论基础(单片机)

计算机基础 IO逻辑 计算机系统中的高低电平逻辑1和0&#xff0c;数据在计算机中的存储、传输、运算都是以二进制形式进行的。 数据的传输通过总线真正传递的是电信号&#xff0c;高低电平&#xff08;0、1&#xff09;。运算在电路中进行&#xff0c;集成电路中运算。 计算机的…

深入浅出支持向量机(SVM)

1. 引言 支持向量机&#xff08;SVM, Support Vector Machine&#xff09;是一种常见的监督学习算法&#xff0c;广泛应用于分类、回归和异常检测等任务。自1990年代初期由Vapnik等人提出以来&#xff0c;SVM已成为机器学习领域的核心方法之一&#xff0c;尤其在模式识别、文本…

docker安装Redis、docker使用Redis、docker离线安装redis、Redis离线安装

服务器到期了&#xff0c;换了一个新的环境要重搭&#xff0c;就记录一下好了&#xff1a; -----docker在线安装Redis 拉取 Redis 镜像 docker pull redis:6.2 运行 Redis 容器 docker run --name redis -d redis docker run --name redis -d redis:6.2 映射端口 docker run -…

初探Java的双冒号运算符

什么是双冒号运算符 双冒号运算符 :: 是在 Java 8 中实现的一个新特性——方法引用。它能使用我们将方法作为一个参考传递到其他的方法中。它能将简化超级长的代码&#xff0c;提高代码可读性。 基本格式&#xff1a; ClassName::methodName双冒号运算符不会调用方法&#x…

B站bilibili视频转文字字幕下载方法

本文将讲述介绍一种使用本地工具如何快速的下载B站的字幕为本地文本文件的方法。 通常获取B站字幕需要在浏览器中安装第三方插件&#xff0c;通过插件获取字幕。随着大模型&#xff0c;生成式AI&#xff0c;ChatGPT的应用&#xff0c;B站也提供了AI小助手对视频的内容进行总结…

ROS+PX4+Gazebo仿真环境配置全流程解析

上一期文章介绍了我们即将发布的仿真平台&#xff0c;并提到后续需要在Ubuntu系统上进行PX4软件在环仿真。本期文章将为大家详细介绍如何配置Ubuntu环境以及安装ROS和PX4仿真环境。具体配置包括&#xff1a;Ubuntu 20.04 ROS Noetic PX4 Python3。 需要注意的是&#xff0c…

【操作系统】为什么需要架构裁剪?

为什么需要架构裁剪&#xff1f; 原因 减小核心大小提高架构初始化速度降低内存占用提高系统性能移除不需要的功能&#xff0c;增加安全性 裁剪方法 初始化配置设置功能模块化移除不需要的驱动底层 一般裁剪对象&#xff08;以操作系统为例&#xff09; 文件系统的支持网…

鸿蒙app封装 axios post请求失败问题

这个问题是我的一个疏忽大意&#xff0c;在这里记录一下。如果有相同问题的朋友&#xff0c;可以借鉴。 当我 ohpm install ohos/axios 后&#xff0c;进行简单post请求验证&#xff0c;可以请求成功。 然后&#xff0c;我对axios 进行了封装。对axios 添加请求拦截器/添加响…