linux之进程信号(初识信号,信号的产生)

目录

  • 引入
  • 一、初识信号(信号预备知识)
    • 1.生活中的信号
    • 2.Linux中的信号
    • 3.信号+进程得出的初步结论
  • 二、信号的产生
    • 1.通过终端输入产生信号
    • 拓展: 硬件中断
    • 2.调用系统函数向进程发信号
    • 3.硬件异常产生信号
    • 4.软件条件产生信号
    • 拓展: 核心转储技术
    • 总结一下:

引入

一、初识信号(信号预备知识)

1.生活中的信号

  • 当你在打着永杰无间的时候,你的外卖到了,外卖员给你打电话去取,而你此时正在打游戏,你心里想着这把打完了去取,过了一会,你打完了这把游戏,你想起来你还有外卖没取,你就马上去取了
  • 信号弹
  • 我们根据红绿灯的颜色过马路
  • 上课铃声响了,我们取上课
  1. 你怎么认识这些信号?
    有人教过我,我就记住了 -> 1.识别信号 2.知道信号的处理方法

  2. 即使我们现在没有信号产生,我们也知道信号产生之后,我该干什么?

  3. 信号产生了之后,我们可能不会立即处理这个信号,我们可能有更重要的事情要做,但是我们必须要把信号产生这个信息保存下来,在合适的时候去处理

2.Linux中的信号

概念:Linux信号通常由操作系统或其他进程发送给目标进程,可以用于多种目的,例如中断进程、终止进程或请求进程执行某个特定操作。本质是一种通信机制。

用kill -l命令可以察看系统定义的信号列表

在这里插入图片描述
可以看到全是大写的,因为linux是用c语言写的,所以就是c语言中的,所以我们也可以使用数字也可以使用以上宏都是一个意思
一共有62个信号,因为32、33不存在,其中,1-31是普通信号,也是本文重点讲解的, 34-64属于实时信号,优先级比较高,立即处理,本文不做讲解。

3.信号+进程得出的初步结论

所以进程信号?

  1. 进程必须识别并能够处理信号 ,信号即使没有产生。也要具备处理信号的能力,怎么做到呢?信号的处理能力,是操作系统给进程内置的功能的一部分

  2. 进程即使没有收到信号,也能知道哪些信号该如何处理

  3. 当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,会在合适的时候区里这个信号 处理动作:1.默认动作 2.忽略 3.自定义动作

  4. 一个进程必须当信号产生,到信号开始处理,就会有一定的时间窗口,这段窗口,进程需要保存哪些信号已经发送了的能力

所以信号要经历: 信号产生 -> 信号保存 -> 信号处理 三个阶段

ctrl + c 为啥能杀死我们的前台进程呢?

  • linux中,一次登录中,一般会配上一个bash,每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程。
  • 当我们用键盘输入ctrl + c 快捷键时候,会被解释成2号信号然后发给前台进程
  • 启动进程的时候 加上 & 表示以后台进程的方式启动
  • 所以,当我们以后台进程状态运行时,我们用键盘输入ctrl + c 的时候进程收不到信号,也就不会终止进程,我们只能使用kill命令杀死该进程
    在这里插入图片描述

二、信号的产生

通过以上对linux信号的简单了解后,我们再来看一下信号是如何产生的,以下是信号产生的几种方式

1.通过终端输入产生信号

比如ctrl + c 就是2号信号SIGINT,我们如何来验证呢?
介绍一个函数

SIGNAL(2)                                                                      Linux Programmer's Manual                                                                      SIGNAL(2)

NAME
       signal - ANSI C signal handling

SYNOPSIS
       #include <signal.h>

       typedef void (*sighandler_t)(int);  //函数指针

       sighandler_t signal(int signum, sighandler_t handler); 
       //表示当我们收到signum信号后,处理动作为handler方法
       //,涉及信号处理,此处先简单讲解

实验一:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
//num:收到了哪个信号
void handler(int num)
{
    cout << num << " handler ..." << endl;
    //exit(1);
}
int main()
{
    signal(SIGINT, handler);//只需设置一次,以后都有效
    while(1)
    {
        cout << "i am a process, mypid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
可以看到输入的ctrl + c被转换成2号信号


其实还有一些快捷键也可以表示为信号,比如ctrl + \ 表示3号信号即SIGQUIT
实验二:
code:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int num)
{
    cout << "signal: " <<  num << " handler ..." << endl;
    //exit(1);
}
int main()
{
    signal(SIGINT, handler);
    signal(3, handler);

    while(1)
    {
        cout << "i am a process, mypid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
我们可以发现,2号和3号信号的处理动作都是同一个函数,这也就是为handler函数要有一个参数为int,就是为了标识是哪个信号正在处理。


但是有些信号不会使用signal定义的处理动作,比如19号信号SIGSTOP
实验三:
code:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int num)
{
    cout << "signal: " <<  num << " handler ..." << endl;
    //exit(1);
}
int main()
{
    signal(SIGINT, handler);
    //signal(3, handler);
    signal(19, handler);
    while(1)
    {
        cout << "i am a process, mypid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
可以看到ctrl + z 并没有调用自定义的signal方法,仍然使进程停止了,这是为什么呢?想知后续请继续往下看!


实验四:
对所有信号都实验signal捕捉

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int num)
{
    cout << "signal: " <<  num << " handler ..." << endl;
    //exit(1);
}
int main()
{
    //signal(SIGINT, handler);
    //signal(3, handler);
    //signal(19, handler);

    for(int i = 1; i <= 31; ++i)
    {
        signal(i, handler);

    }
    while(1)
    {
        cout << "i am a process, mypid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
可以看到1-8都被捕捉了,而9号信号没有。我们继续重启进程,继续向后发信号。
在这里插入图片描述
在这里插入图片描述
可以看到10-18都被捕捉了,19不会,也就是之前做的实验三。
继续重启进程,发信号,可以发现都被捕捉了(这里不做演示了)。

所以只有9号和19号信号不会被signal捕捉,这是为什么呢?
可以发现这俩一个是杀死进程,一个是停止进程,都跟进程的执行有关,你觉得操作系统为啥不让所有信号被捕捉呢?很简单,操作系统又不是傻子,万一有个病毒软件什么的,它把所有信号都捕捉了,那它不就永远都杀不掉了嘛。


拓展: 硬件中断

看了上面的,你有什么疑问嘛?
键盘数据是如何传入给内核的?,ctrl + c 又是如何变成信号交给进程呢?
接下来就要谈谈硬件了

首先,进程是由操作系统管理的,所以键盘被按下,肯定是操作系统先知道?那么操作系统又是如何知道键盘上有数据了????
在这里插入图片描述

  1. 操作系统会在启动的时候加载进内存
  2. linux下一切皆文件,每个文件都有自己的缓冲区,即struct file结构体
  3. os怎么知道键盘上有数据?os会对外设进行轮询监测嘛?os系统没这么蠢,系统中这么多设备,操作系统这样的化效率太低了
  4. cpu和键盘通过主板连接在一起,cpu上有很多针脚,和各种设备间接或直接连接着,当键盘中有数据产生的时候,键盘会给cpu发送硬件中断,将cpu特定的针脚发送高低电频,即充放电,然后cpu感受到了。
  5. cpu中的寄存器如何存储数据呢?也就是上述充放电的过程
  6. os内核中有一个中断向量表,就和文件描述符表向上,中断向量表中存储各个设备的读写方法
  7. 当键盘中有数据产生,给cpu发送硬件中断,cpu让操作系统执行中断相量表中的方法去读取键盘中的数据
  8. 当键盘中的数据是一般数据的时候,会被读入缓冲区中,当是快捷键比如ctrl + c 这样的,会被解释成信号由操作系统发送给进程

那么以上所讲硬件中断和我们讲的信号有什么联系嘛?
我们学习的信号,就是利用软件的方式,对进程模拟硬件中断

再谈缓冲区

  1. 平常我们输入命令的时候,我们可以看到我们自己的输入,其实是操作系统将键盘输入缓冲区中的数据拷贝到显示器输出缓冲区中去了,我们就可以看到我们输入的内容了,而linux中输入密码的时候我们看不到密码,也就是os没有把此时的输入缓冲区中的内容拷贝到输出缓冲区中去
  2. 当我们以后台进程的方式启动进程时,我们即使隔一段时间才输入一个完整的命令,也可以正常执行,因为我们我们看到的很长时间才输入完整的命令实在显示器缓冲区中,而输入缓冲区中的数据时连续完整的,所以可以正常执行

在这里插入图片描述

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

在这里插入图片描述
可以看到,kill系统调用的作用是发送一个信号给指定进程,就不就是和我们命令行中的kill命令一样的嘛,其实我们命令行中的kill命令底层就是调用的kill系统调用。
那么我们直接来做一个kill命令

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <cstdio>
#include <signal.h>
using namespace std;
static void Usage(const string& cmd)
{
    cout << "\n\r " << cmd << " signo process_pid " << endl << endl;
}
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int sig = stoi(argv[1]);
    pid_t id = stoi(argv[2]);
    int ret = kill(id, sig);
    if(ret == -1)
    {
        perror("kill");
        exit(2);
    }
    return 0;
}

在这里插入图片描述


在这里插入图片描述
raise作用:发送一个信号给调用者,实际上就是kill的封装,就是kill(getpid(), sig).


在这里插入图片描述
abort作用: 给调用者发送6号信号(SIGABRT,实际也是对kill的封装,但是他有一些特性,直接看实验

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <cstdio>
#include <signal.h>
using namespace std;
// static void Usage(const string& cmd)
// {
//     cout << "\n\r " << cmd << " signo process " << endl << endl;
// }
void handler(int signum)
{
    cout << "get a sig : " << signum << endl;
}
int main(int argc, char* argv[])
{
    // if(argc != 3)
    // {
    //     Usage(argv[0]);
    //     exit(1);
    // }
    // int sig = stoi(argv[1]);
    // pid_t id = stoi(argv[2]);
    // int ret = kill(id, sig);
    // if(ret == -1)
    // {
    //     perror("kill");
    //     exit(2);
    // }
    signal(6, handler);
    for(int i = 0; i < 3; ++i)
    {
        cout << "i am a process pid: " << getpid() << endl;
        sleep(1);
    }
    abort();

    return 0;
}

在这里插入图片描述
可以看到,即使signal捕捉了6号信号,进程也执行了handler方法,但进程最终还是终止了,所以abort内部采用一定的手段使进程强制停止了。

3.硬件异常产生信号

1.除0错误
code:

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

void handler(int signum)
{
    cout << "get a sig : " << signum << endl;
}
int main()
{
    // signal(SIGFPE, handler);
    cout << "zero before" << endl;
    sleep(1);
    int a = 10;
    cout << 10 / 0 << endl;;
    cout << "zero after" << endl;
    return 0;
}

实验现象:
在这里插入图片描述
放开注释后,自定义捕捉SIGFPE信号

实验现象:
在这里插入图片描述
可以看到我们捕捉了8号信号(SIGFPE),所以发生除0错误后,会收到8号信号,但是我们一直死循环重复捕捉,这是为什么呢?

  • 我们从硬件角度来分析,cpu内有一套寄存器,保存着进程的上下文数据,其中一些特殊的寄存器比如eip/pc记录着进程执行的代码情况。
  • 最重要的是有一个状态寄存器,他就是一个位图结构,用来记录进程的运行状态。每一个比特位代表不同的状态。
  • 当发生除0错误的时候,状态寄存器中溢出标记位由0变1,发生异常。
  • 而操作系统是硬件的管理者,它发现cpu中状态寄存器发生异常,就会给进程发送特定的信号,而进程本身无法处理这个异常,即使捕捉了信号, 操作系统就会不断的发信号。

2.野指针异常
code:

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

void handler(int signum)
{
    cout << "get a sig : " << signum << endl;
}

int main()
{
	//signal(SIGSEGV, handler);
    cout << "before" << endl;
    sleep(1);
    int *p = nullptr;
    *p = 30;
    cout << "after" << endl;
    return 0;
}

实验现象:
在这里插入图片描述
放开注释,实验现象:
在这里插入图片描述
可以看到,发生野指针的时候,也会不断收到信号。
我们再从底层的角度解释一下:

  • 发生野指针的本质是访问无效的空间,由进程地址空间可以知道,我们语言上访问的空间其实是虚拟地址,实际上我们要通过页表进行虚拟地址到物理地址的转化,而这一过程os系统不会参与,会有页表和mmu(memory manager unit)即内存管理单元(cpu中硬件),当转化失败的时候mmu会报错。
  • os系统是硬件的管理者,它识别到mmu的报错,而进程无法修正错误,即使捕捉了信号,就不断给进程发信号

总结一下:
当我们出现异常的时候,操作系统为啥选择给进程发信号,然后去执行特定的代码,而不是直接干掉进程呢?
其实向我们发信号是为了让进程知道进程出现了什么异常,而且让进程有一定的缓存时间,去处理日志信息等,如果操作系统直接把进程干掉了,就可能处理不了这些情况了。

补充:
上述过程一个进程导致cpu中的寄存器发生了异常,也就是硬件发生了异常,为啥其他进程还能正常运行呢?

  • cpu中的寄存器只有一套,存储的是进程的上下文信息,当发生进程切换的时候,进程会带走它的上下文数据,然后其他进程将它之前访问的上下文数据放进寄存器,上一个进程带来的寄存器异常已经被带走了,而新来的进程上下文数据并没有异常,所以不会影响其他进程的运行。
  • 所以信号的存在不是让我们解决问题,操作系统都解决不了,你让进程自己解决码?
  • 信号的存在是为了让我们知道程序异常的原因,并且给我们一定的缓冲时间,用来打印日志信息等等。

4.软件条件产生信号

比如我们写管道的时候,当读端关闭的时候,写端还有继续写入,此时操作系统就会向写端进程发送SIGPIPE信号,然后终止进程。
在这里插入图片描述
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
返回值代表上次设定闹钟的剩余时间,比如你定一个闹钟30分钟后响,20分钟后你就行了,此时你查看时间闹钟还有10分钟才响。

实验:如果我们自定义处理闹钟的动作,进行一些日志打印等操作,然后再在捕捉函数内定一个闹钟,不就做到了不断做日志打印的业务了嘛?
code:

#include <signal.h>
using namespace std;
void work()
{
    cout << "print a log..." << endl;
}
void handler(int signum)
{
    cout << "get a sig : " << signum << endl;
    work();
    alarm(5);
}
int main()
{
    signal(SIGALRM, handler);
    alarm(5);
    while(1)
    {
        cout << "i am a process pid : " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

实验现象:
在这里插入图片描述

返回值验证:可以看到上面实验现象每次闹钟剩余时间都是0,因为我们没有利用其他手段让闹钟提前苏醒,我们使用kill命令来验证
实验现象:
在这里插入图片描述
补充:

  • 操作系统内部有很多进程,每个进程都可以设置闹钟,那么操作系统内部就有很多闹钟了,操作系统就要管理它们,管理的本质是先描述再组织,用特定的结构体来描述闹钟,比如 struct alarm.
  • struct alarm中需要存储设置这个alarm的进程,以及什么时候响,一般用当前时间戳+设计多久响来表示
  • 那么操作系统用什么组织alarm呢?很明显可以用我们常见的数据结构小堆来管理,堆顶存最早响的闹钟,os只需要查看堆顶的时间,如果超时了,给struct alarm中存的进程pid发送信号,然后将堆顶的数据删除,继续查看堆顶,重复以上动作,直到堆顶没有超时。

拓展: 核心转储技术

在这里插入图片描述
之前讲进程退出状态的时候,正常退出时次第8为为退出码,低8位为0,
收到信号退出的时候,次第0位无意义,低7位表示退出信号,其中第8个比特位之前没有讲,其实是core dump(核心转储)标识。

什么是核心转储?
将进程异常退出时的状态保存下来形成一个core.pid文件,放在当前路径下(磁盘)可以配合gdb进行时候调试,一会做实验。

一般在项目上线后是关闭的,为什么?
一般服务即使出现异常挂掉了,会有一定的手段自动重启,如果一些程序员写的代码太水,一上线就挂掉,不断自动重启,不断形成core dump文件,把磁盘写满,挂的就是操作系统了,那问题就严重了。

并不是所以的信号都会使进程挂掉后形成核心转储,通过man 7 signal可以查看
在这里插入图片描述
其中term表示只终止进程,core表示终止进程并形成core dump核心转储文件。
ign表示忽略动作,stop表示停止,这俩现在不详细讲。

实验验证:

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


int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 500;
        while(cnt)
        {
            cout << "i am child process, pid: " << getpid() << "cnt: " << cnt-- << endl;
            sleep(1);
        }
        exit(0);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    cout << " child quit info, rid: " << rid << " exit code: " << 
    ((status >> 8) & 0xff) << " exit signal: " << 
    (status & 0x7f) << " core dump: " << (status >> 7 & 1) << endl;
    return 0;
}

默认情况下core dump是关闭的,ulimit -a可以查看
在这里插入图片描述
此时实验现象:
在这里插入图片描述
在这里插入图片描述
可以看到此时无论发送term信号还是core信号都不会形成core dump。

当我们把核心转储打开
在这里插入图片描述
开始重做实验:
在这里插入图片描述
在这里插入图片描述
可以看到当我们使用core信号时,形成了核心转储文件,并且很大。

那么我们如何将他和gdb配合使用呢?
code:

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


int main()
{
    int a = 10;
    a /= 0;
    cout << "a: " << a << endl;
    return 0;
}

在这里插入图片描述
当我们直接运行的时候,可以看到报错多了一段core dumped,并形成了core.pid文件
在这里插入图片描述
直接开始gdb
在这里插入图片描述
可以看到直接定位到了异常的位置

总结一下:

  1. 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
    OS是进程的管理者

  2. 信号的处理是否是立即处理的?
    普通在合适的时候进行处理,但是对于实时信号来说,必须立马处理。

  3. 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
    可以发现普通信号一共有31个,一个int有32个比特位,不觉得很巧嘛?每一个比特位代表着一个信号,0和1表示着是否收到该信号,其实信号就是用类似这样的结构保存的,也就是位图,但是可以发现只能存0和1那就只能表示是否收到该信号,而不能知道收到该信号的数量的,实际上内核也是这样的。
    而实时信号需要立即处理,所以发几个信号就要处理几个,所以就不能用位图结构来存储了,而是用一个队列(先到先处理)。

  4. 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
    必须知道。

  5. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
    与其说os向进程发信号,不如说向进程的pcb发信号,信号在pcb中用位图方式存储,那么os向进程发信号就是修改进程pcb中的位图,将对应的信号比特位由0置1.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

24-25-1-单片机开卷部分习题和评分标准

依据相关规定试卷必须按评分标准进行批改。 给分一定是宽松的&#xff0c;能给分一定给&#xff0c;如有疑问也可以向学院教务办申请查卷。 一部分学生期末成绩由于紧张或其他原因导致分数过低&#xff0c;也是非常非常遗憾的。 个人也是非常抱歉的。 开卷考试 简答题 第一…

电动汽车V2G技术Matlab/Simulink仿真模型

今天给大家更新关于V2G技术的仿真&#xff0c;不是研究这个方向的&#xff0c;可能会对这个名称比较陌生&#xff0c;那么&#xff0c;什么是“V2G”&#xff1f; V2G全称&#xff1a;Vehicle-to-Grid&#xff0c;即车网互动&#xff0c;利用电动汽车特有的储能功能与电网“双…

统计学习算法——决策树

内容来自B站Up主&#xff1a;风中摇曳的小萝卜https://www.bilibili.com/video/BV1ar4y137GD&#xff0c;仅为个人学习所用。 问题引入 有15位客户向某银行申请贷款&#xff0c;下面是他们的一些基本信息&#xff0c;类别列表示是否通过贷款申请&#xff0c;是表示通过贷款申…

Pytorch导出onnx模型并在C++环境中调用(含python和C++工程)

Pytorch导出onnx模型并在C环境中调用&#xff08;含python和C工程&#xff09; 工程下载链接&#xff1a;Pytorch导出onnx模型并在C环境中调用&#xff08;python和C工程&#xff09; 机器学习多层感知机MLP的Pytorch实现-以表格数据为例-含数据集和PyCharm工程中简单介绍了在…

Uniapp判断设备是安卓还是 iOS,并调用不同的方法

在 UniApp 中&#xff0c;可以通过 uni.getSystemInfoSync() 方法来获取设备信息&#xff0c;然后根据系统类型判断当前设备是安卓还是 iOS&#xff0c;并调用不同的方法。 示例代码 export default {onLoad() {this.checkPlatform();},methods: {checkPlatform() {// 获取系…

VMWare虚拟机+Ubuntu24.04+ROS2Jazzy版本安装——踩坑及爬坑过程

VMWare安装 VMWare安装参考VMWare安装&#xff0c;WMWare workstation从17版本以后就面向个人用户免费开放了&#xff0c;所以在安装的最后只要勾选“用于个人”这个选项&#xff0c;就无需再输入激活码等&#xff0c;非常方便。 WMWare workstation17的获取地址&#xff1a;通…

【Golang 面试题】每日 3 题(三十一)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

分布式数据存储基础与HDFS操作实践(副本)

以下为作者本人撰写的报告&#xff0c;步骤略有繁琐&#xff0c;不建议作为参考内容&#xff0c;可以适当浏览&#xff0c;进一步理解。 一、实验目的 1、理解分布式文件系统的基本概念和工作原理。 2、掌握Hadoop分布式文件系统&#xff08;HDFS&#xff09;的基本操作。 …

《OpenCV》——模版匹配

文章目录 OpenCV——模版匹配简介模版匹配使用场景OpenCV 中模板匹配的函数参数 OpenCV——模版匹配实例导入所需库读取图片并处理图片对模版图片进行处理进行模版匹配显示模版匹配的结果注意事项 OpenCV——模版匹配简介 OpenCV 是一个非常强大的计算机视觉库&#xff0c;其中…

迅翼SwiftWing | ROS 固定翼开源仿真平台正式发布!

经过前期内测调试&#xff0c;ROS固定翼开源仿真平台今日正式上线&#xff01;现平台除适配PX4ROS环境外&#xff0c;也已实现APROS环境下的单机飞行控制仿真适配。欢迎大家通过文末链接查看项目地址以及具体使用手册。 1 平台简介 ROS固定翼仿真平台旨在实现固定翼无人机决策…

基于深度学习的视觉检测小项目(十二) 使用线条边框和渐变颜色美化界面

到目前为止&#xff0c;已经建立起了基本的项目架构&#xff0c;样式表体系也初步具备&#xff0c;但是与成品的界面相比&#xff0c;还是差点什么。 我的界面效果图&#xff1a; 优秀demo的界面截图&#xff1a; 是的&#xff0c;我的界面太“平” 了&#xff0c;没有立体感&…

MySQL(高级特性篇) 06 章——索引的数据结构

一、为什么使用索引 索引是存储引擎用于快速找到数据记录的一种数据结构&#xff0c;就好比一本教科书的目录部分&#xff0c;通过目录找到对应文章的页码&#xff0c;便可快速定位到需要的文章。MySQL中也是一样的道理&#xff0c;进行数据查找时&#xff0c;首先查看查询条件…

Springboot + vue 图书管理系统

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e; &#x1f496;&#x1f4d5;&#x1f389;&#x1f525; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论 &#x1f525;&#x1f525;&…

2025年01月15日Github流行趋势

1. 项目名称&#xff1a;tabby - 项目地址url&#xff1a;https://github.com/TabbyML/tabby - 项目语言&#xff1a;Rust - 历史star数&#xff1a;25764 - 今日star数&#xff1a;1032 - 项目维护者&#xff1a;wsxiaoys, apps/autofix-ci, icycodes, liangfung, boxbeam - 项…

详解数据增强中的平移shft操作

Shift 平移是指在数据增强&#xff08;data augmentation&#xff09;过程中&#xff0c;通过对输入图像或目标进行位置偏移&#xff08;平移&#xff09;&#xff0c;让目标在图像中呈现出不同的位置。Shift 平移的目的是增加训练数据的多样性&#xff0c;从而提高模型对目标在…

Linux:地址空间(续)与进程控制

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《Linux&#xff1a;地址空间与进程控制》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 如果本篇文章对你有帮助&#xff0c;还请各位点点赞&#xff0…

RabbitMQ(三)

RabbitMQ中的各模式及其用法 工作队列模式一、生产者代码1、封装工具类2、编写代码3、发送消息效果 二、消费者代码1、编写代码2、运行效果 发布订阅模式一、生产者代码二、消费者代码1、消费者1号2、消费者2号 三、运行效果四、小结 路由模式一、生产者代码二、消费者代码1、消…

ssh,samba,tftp,nfs服务安装和配置

前提准备 sudo ufw disable sudo ufw status sudo apt update ssh服务 sudo apt-get install openssh-server sudo apt-get install openssh-client sudo apt-get install ssh echo "PasswordAuthentication yes" >> /etc/ssh/ssh_config //配置ssh客户…

.NetCore 使用 NPOI 读取带有图片的excel数据

在.NetCore使用NPOI插件进行批量导入时&#xff0c;获取Excel中的所有的图片数据&#xff0c;存到集合中。 1.定义类PictureData 代码如下&#xff1a; public class PictureData { public byte[] Data { get; set; } } 2.数据集引用 using NPOI.XSSF.UserModel; usin…

MAC上安装Octave

1. 当前最新版Octave是9.3版本&#xff0c;需要把mac os系统升级到14版本&#xff08;本人之前的版本是10版本&#xff09; https://wiki.octave.org/Octave_for_macOS octave的历史版本参考此文档&#xff1a;Octave for macOS (outdated) - Octavehttps://wiki.octave.org/Oc…