Linux信号的概念信号的产生

前言

我们前面已经对进程已做了介绍!知道进程具有独立性,但在运行起来后可能会"放飞自我",即不受控制的执行,这就会导致系统崩溃等问题,非常不利于管理。因此OS需要一种机制来协调和控制进程的运行,而进程信号就是这样的重要机制!

目录

一、信号的基本概念

1、生活中信号的例子

2、什么是信号?

3、31个普通信号

4、自定义信号处理行为

二、信号的产生

1、 kill指令

2、键盘产生信号

3、函数的接口调用

• kill 

• raise

• abort

4、软件条件

• 管道

• alarm

测试IO效率

5、异常

• 除 0 异常

• 野指针异常

三、其他问题

1、如何理解信号的发送?

• 信号的存储

• 信号的发送

2、核心转储

• Core和Trem的区别

• 核心转储的概念

• 核心转储的作用

• 云服务器默认关闭core dump的原因

• 解决历史遗留问题


一、信号的基本概念

1、生活中信号的例子

信号在我们的生活中是随处可见的!比如:

• 红绿灯 --> 红灯停,绿灯行

• 闹钟 --> 起床,该学习了

• 坤叫 --> 天要亮了

• 你考了30分你妈黑着脸 --> 你要被"奖励"了

•  .......

当我们在生活中,看到这些信号时,我们会下意识的想到相关的动作,这是因为我们认识这些并能够处理信号! 其实,OS中的进程也是一样的!当进程收到一个信号后,至少要做到两点:1、认识信号 2、能处理信号 

2、什么是信号?

信号是进程之间事件异步通知的一种方式!

这样直接说太过于官方了!不好理解!下面用直观的方式解释一下:

信号是Linux提供的一种向指定进程发送特定事件的方式!信号的产生是异步的,也就是收到信号的进程不知道具体什么时候就收到了,即信号和接收信号的进程是两个不相关的执行流!

OK,举个栗子:

比如说,你今天买了快递,但是你不知道他什么时候给你通知说到了!快递的发送是独立于你的生活的,当快递到了,快递员会通知你拿!这个栗子中,你就是进程,通知拿快递就是信号,你收到信号拿快递就是处理信号!你和通知你拿快递的消息是独立的,你也不知道啥时候会通知你,即异步的!

3、31个普通信号

Linux中有62个信号,我们可以用 kill -l 来查看:

其中, 1-31 是普通信号(收到信号后,选择合适的时间处理); 34-64 是实时信号(必须马上处理);

其中实时信号我们不介绍,他是用于一些实时的操作系统的,例如车载系统;而我们的一般开发用的操作系统都是分时操作系统,这也是普通信号的主要用武之地!

上面介绍了当进程收到信号后,需要对该信号做处理,处理的方式有三种:

1、忽略此信号

2、执行该信号的默认处理函数

3、执行自定义的处理函数,这种方式也称为信号的捕捉

忽略就是不管,当没看到!自定义后面解释!这里先介绍一下信号的默认处理动作:

可以用 man 7 signal 查看信号的默认动作:

其中TermIgnCoreStopCont都是信号处理的默认行为

  • Term:默认操作是终止进程
  • Ign:默认操作是忽略信号
  • Core:默认操作是终止进程并转储核心
  • Stop:默认操作是暂停进程
  • Cont:默认操作是,如果该进程当前已暂停,则继续该进程

以上5种,其中TermCore 都是终止进程,区别是Core会额外的进行 核心转储,这个后面介绍!我们再往下面翻,就会看到每个进程的描述:

4、自定义信号处理行为

默认行为了介绍了,忽略不管!那自定义行为如何处理呢?自定义信号的行为,是你当前的需求和系统默认的该信号的行为不符,此时你需要自定义该信号的行为!这个就和我们写的那个仿函数控制sort以及优先级队列的行为一样!

可以使用, signal 系统调用函数,来实现!

#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);

注意 typedef void (*sighandler_t)(int); 这是一个函数指针类型, 参数是一个int 类型的变量,其实就是收到的信号!

signal的第一个参数就是收到的信号,第二个就是该信号的行为函数,我们要想自定义该信号的行为,只需要自主实现handker函数即可!signal 的返回值也是sighandler_t类型,返回的是原先处理该信号函数的函数指针!

OK,我们用一个2号信号来演示:

2号信号SIGINT的默认行为是终止进程,和ctrl + c一样! 我们可以先来验证一下:

#include <iostream>
#include <unistd.h>int main()  
{while (true){std::cout << "pid: " << getpid() << std::endl;sleep(1);}return 0;
}

OK,下面我们不想让他按默认的终止进程来,我们想就让他打印一下他收到的信号是多少

#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;
}int main()  
{signal(SIGINT, handler);//自定义2号信号的行为while (true){std::cout << "pid: " << getpid() << std::endl;sleep(1);}return 0;
}

我们发现此时无论是给该进程发送2号信号,还是直接ctrl + c 都是不能准直进程的!而且上面你ctrl + c显示的是收到的是2号信号!

这里你可能会想,普通信号就31个,那我全部捕捉了,都让他不终止那信号岂不是没用了!

哈哈是的,这个问题设计OS的人也想到了,人家避免上述的情况发生,于是9号和19号信号设计成了不可自定义捕捉

小tips : 其实上面的所有信号都是宏!所以既可以使用数字,也可以使用对应的大写字符串!

二、信号的产生

OK,简单的介绍了信号的概念和三种处理方式后,我们来看看Linux中信号产生的5种方式!

1、 kill指令

可以在命令行,向指定的进程发送指定的信号!例如,kill -9 

2、键盘产生信号

例如,ctrl + c(2) ctrl + \(3)

int main()  
{while (true){std::cout << "pid: " << getpid() << std::endl;sleep(1);}return 0;
}

3、函数的接口调用

• kill 

作用:给指定进程发送指定的信号

参数

• pid : 收到信号进程的pid

• sig : 发送哪一个信号

返回值

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

OK,举个例子:让父进程休眠3秒,通过kill系统调用给子进程发送3号信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdio>void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;exit(1);
}int main()
{pid_t id = fork();if(id == 0){//child signal(3, handler);while (true){std::cout << "i am process" <<std::endl;sleep(1);}}//father sleep(3);kill(id, 3);return 0;
}

果然在,3秒后收到了3号信号!这里你可能会想kill 指令和 kill系统调用是啥关系呢其实kill指令的底层一定是调用了kill系统调用!

• raise

作用:给当前进程发指定的信号

  参数

• sig : 要发送的信号

返回值

成功,返回0; 失败,返回非0

我们让我们的进程先跑5秒,然后给自己发送9号信号,让OS杀掉当前进程!

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdio>void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;exit(1);
}int main()
{signal(SIGKILL, handler);//捕捉一下int cnt = 5;while (cnt--){std::cout << "hello world -> "  << cnt << std::endl;sleep(1);}raise(9);return 0;
}

小tips:kill(getpid(), xxx) == raise(xxx);

• abort

作用:给自己发送6号信号(SIGABRT)

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdio>
#include <cstdlib>void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;exit(1);
}int main()
{signal(SIGKILL, handler);int cnt = 5;while (cnt--){std::cout << "hello world -> "  << cnt << std::endl;sleep(1);}abort();return 0;
}

OK,这里6号信号的作用好像也是终止进程!是的,但是他会形成core文件,即核心转储!

4、软件条件

这个看字面可能很抽象!但是,其实我们以前就提到过!例如管道!

• 管道

前面介绍管道的时候,介绍过管道的一个特性:当管道的读端关闭,写端还在写,此时OS会想写写进程发送13号信号!这是我们以前介绍的!这里不再验证了!

• alarm

作用:设定一个闹钟,一定时间后,给当前进程发送14( SIGALRM)号信号

参数

• seconds : 表示设定多少秒的闹钟

返回值

• 如果提前醒了,上一个闹钟还有剩余的时间,返回上一次闹钟的剩余秒数

• 否则,返回0

OK,我们先来演示一下,正常的闹钟:

int main()
{alarm(5);int cnt = 1;while(true){std::cout << "second = " << cnt << std::endl;cnt++;sleep(1);}return 0;
}

我们可以自定义捕捉看看是不是收到了14号信号:

void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;exit(1);
}int main()
{signal(14, handler);//捕捉一下14号信号alarm(5);//设置一个5秒的闹钟int cnt = 1;while(true){std::cout << "second = " << cnt << std::endl;cnt++;sleep(1);}return 0;
}

下面我们来一个提前结束的:alarm(0)是取消闹钟

int main()
{alarm(5);sleep(1);int n = alarm(0);// alarm(0) 提前取消闹钟,返回值是上一次闹钟的剩余时间std::cout << "n = " << n << std::endl;return 0;
}

这里本来设了5秒的闹钟,1秒后取消了;就剩下了4秒!

注意:alarm设的闹钟只有有效一次!如果想要多次有效就需要,每次重置(自举)

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdio>
#include <cstdlib>void handler(int sig)
{alarm(1);std::cout << "get sig: " << sig << std::endl;
}int main()
{signal(14, handler);alarm(1);while (true){std::cout << "hello world" << std::endl;sleep(1);}return 0;
}

理解闹钟

我们一个进程可以定一个,也可以定好多的闹钟,同理OS中的其他进程也可以订个十个八个的,而OS中的进程很多,所以需要对闹钟进行做管理,如何做管理?先描述,在组织

闹钟的描述可以用一个结构体来描述,例如这样的:

struct _alarm
{time_t expired; // 未来要超时的时间 = seconds + 当前的时间pid_t pid;      // 哪一个进程设置的func_t fun;     // 默认行为//....
};

组织的话,因为闹钟很多所以,可以用小根堆组织,到之后只需要判断当前时间和超时时间即可!

测试IO效率

这个以前从来没有测试过,我们以前只是说,I/O本是和外设打交道,外设的效率很慢,所以I/O的效率慢!理论是这样!但是没有一个量化的认识!我们这里借着闹钟演示一下:

先来看看一秒,有I\O可以执行多少次:

int cnt = 1;void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;std::cout << "cnt: " << cnt << std::endl;exit(1);
}int main()
{signal(14, handler);alarm(1);while (true){std::cout << "cnt: " << cnt << std::endl;cnt++;}return 0;
}

我们发现是4万多次!再来看看没有I\O的:

int cnt = 1;void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;std::cout << "cnt: " << cnt << std::endl;exit(1);
}int main()
{signal(14, handler);alarm(1);while (true){cnt++;}return 0;
}

接近高了1W多倍!所以I/O是很费时间的!而我今天用的是云服务器,还要跨网络通信代价更大!

5、异常

最后一种产生的信号的方式是:异常,更准确说是硬件异常!例如,我们平时的代码遇到的:除0、野指针

• 除 0 异常

先来一段除0的代码看效果:

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

这里报的错误是浮点数异常,一般这种错误就是除0导致的!这里进程终止了,说明他收到了信号!这里浮点数异常是收到了 8 号信号 SIGFPE

我们可以用前面的自定义捕捉,证明一下收到的就是8号信号:

void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;
}int main()
{signal(8, handler);int n = 10;std::cout << n / 0 << std::endl;return 0;
}

确实验证了8号信号!但是我们看到这里一直死循环的打印!我只发生了一次除0错误啊!OK,我下面来解释一下原因:

我们所有的代码最后编译成二进制后,加载到内存都是由CPU来执行的(CPU执行又分为算术运算和逻辑运算)!所以上面的除0错误也是由CPU抛出的(CPU是硬件,也就是硬件异常)!现在的问题是CPU执行时他怎么知道当前的代码是正常还是异常呢?其实在CPU内部不只有正常运算的寄存器。还有一个状态寄存器eflag,在他内部有溢出标记位当执行时发生异常时将溢出标记位设为1即异常的状态,OS是软硬件资源的管理者,当OS检测到异常时就要解决这种硬件问题保证系统的安全,如何处理?向指定的进程发送对应的信号让其终止!如果是正常的情况发送信号当前进程就结束了,但是我们这里给把对应的信号给自定义捕捉了,也就是打印一句话而不是进程退出。当OS给该进程发送信号后该进程就要被切换下来,前面介绍了进程切换时需要保存硬件的上下文数据!保存在哪里?当前进程的PCB里面!而当前进程把8号给自定义了有没有退出,接下来还会被调度当再次调度时,他就会把上一次异常的数据又给CPU,CPU的溢出标记为又是1,OS又发送信号!这样就进入了死循环,也就是我们上面看到的一直打印!

• 野指针异常

野指针的异常包括越界、栈溢出等!

还是先来个例子看现象:

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

这里是段错误!其实他也是收到了信号,收到了11(SGISEGV) 号信号并终止了进程!我们也可以自己捕捉看看:

void handler(int sig)
{std::cout << "get sig: " << sig << std::endl;
}int main()
{signal(11,handler);int* ptr = nullptr;*ptr = 100;return 0;
}

这里看到了确实收到了11号信号!这里的一直循环的打印和上面的原理类似:

前面介绍过CPU在执行时全部使用的是虚拟地址,当他执行时根据页表转换成物理地址找到其数据和代码!如果CPU拿着虚拟地址去页表中依次查找的话有些慢。其实在CPU内部右两个专门负责虚拟地址转物理地址的寄存器:MMUCR3,其中CR3存的是当前页表的起始地址,MMU是当前的虚拟地址,所以MMU+CR3 = 当前虚拟地址对应的物理地址。那CPU又如何知道我指针越界访问了呢?CPU内部还有一个和上面的状态寄存器作用相似的一个页故障线性地址寄存器CR2专门存储的是当前虚拟地址是否非法!如果当前的地址转换过程中发现非法,直接将该虚拟地址放到CR2,当OS检测到之后就会发送11号信号!这里一直循环打印的原因和上面的一样!

OK,介绍玩这里我们就可以回答下面三个问题了: 

1、程序为什么会奔溃?

         • 原因是当前程序中有代码进行了非法访问或非法操作,导致OS向该进程发送了信号!

2、程序奔溃后默认是退出,可以不退出吗?

        • 可以!自定义捕捉异常。

3、为什么推荐异常收到信号后,终止进程?

        • 为了释放进程的上下文数据,包括溢出标记为等其他异常数据!

三、其他问题

1、如何理解信号的发送?

我们前面都在说发送信号。但是如何理解这个发送信号呢?OK,要谈清楚发送信号,我们呢就得先介绍一下信号的存储!

• 信号的存储

我们分时OS用的是普通信号,有31个[1,31],没有0。你说巧不巧我们每个进程的PCB内部都会有一个字段(uint32_t),专门描述信号的!也就是一个整型,采用的是位图的思想!

• 信号的发送

当进程收到对应的信号后,OS就会把对应进程的PCB中的信号字段对应的位置1!所以发送信号的本质就是,将指定进程PCB中的描述信号位图中的特定位置修改为1;而PCB属于内核数据结构,只有OS有权利修改,所以,真正发送信号的是操作系统!!

2、核心转储

• Core和Trem的区别

前面就介绍了,Core和Trem最终都是异常终止进程!但是它两你有啥区别呢?

• Trem : 单纯的异常终止!

• Core : 在异常终止时,会额外生成一个Core文件,一般用于时候debug!

  这个Core文件的内容就是,进程在收到信号异常终止时内存中核心的代码和数据!

OK ,先来一个验证一下:这里的11号信号就是Core

我们这里也没有形成Core文件啊!难道是环境的问题?这回还真是!我们当前的环境是云服务器!一般云服务器时把Core关掉的!

可以用ulimit -a 查看

我们可以用 ulimit -c size 打开

我们再来试一下:

这里面是乱码,就不再看了!如果你不想要了,你就把他关闭了!使用: ulimit -c 0

• 核心转储的概念

一些信号终止进程时,需要对该进程的核心数据进行在当前进程的目录下生成一个core.pid的文件,这个过程称作核心转储(Core Dump)

常见的信号有:3号 SIGQUIT4号 SIGILL5号 SIGTRAP6号 SIGABRT7号 SIGBUS8号 SIGFPE11号 SIGSEGV24号 SIGXCPU25号 SIGXFSZ31号 SIGSYS 都是可以产生核心转储文件的。

• 核心转储的作用

核心转储的主要作用是:调试!核心转储可以直接使用core-file corefile到出错的地方!也就是时候调试!

• 云服务器默认关闭core dump的原因

一般在云服务器上是部署项目的,一旦一个程序异常终止,就得立刻重启!当然这个工作是相关的监控脚本做的!如果默认打开了core dump,如果项目和上面的一样有问题,就会挂掉,但是自动脚本就会立即重启,这样就会生成大量的core文件,如果时间稍微一长可能磁盘就爆了!导致服务器宕机了!所以一般的服务器都是将core dump关掉的!

• 解决历史遗留问题

我们在进程控制中说过:子进程退出父进程要等待!其中status是获取子进程退出的信息的!他的低7位是退出信号,次低8位是退出码!但是我们当时留了一个问题就是,第8位,我们当时是说后面介绍!这里就可以介绍了:

其实第8位就是刚刚介绍的,是否核心转储,如果是核心转储,第8位就是1,否则就是0!

int main()
{pid_t id = fork();if(id == 0){//childint* ptr = nullptr;*ptr = 100;}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){printf("exit code: %d, exit sig: %d, core dump: %d\n", (status>>8)&0xff, (status&0x7f), (status >> 7)&1);}return 0;
}

注意:这里只有进程收到Core信号才会让core dump位,置1,Trem是不会让core dump位,置1的!

OK, 好兄弟本期分享就到这里,我是cp我们下期再见!

结束语:要使劲加油啊!

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

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

相关文章

PHP多城市多门店多端平台健身系统小程序源码

&#x1f3cb;️‍♀️&#x1f30d; 打造健身新纪元&#xff01;多城市多门店多端平台健身系统全解析 &#x1f3c3;‍♂️ &#x1f310; 开篇&#xff1a;跨越界限&#xff0c;健身无界 &#x1f30d; 在快节奏的现代生活中&#xff0c;健身已成为越来越多人追求健康与美好…

家纺四件套入驻亚马逊VC的四大优势——WAYLI威利跨境助力商家

亚马逊VC平台为供应商提供了一个庞大的客户基础。亚马逊作为全球最大的在线零售商之一&#xff0c;拥有数以亿计的活跃用户&#xff0c;这为家纺四件套品牌商家带来了前所未有的市场机遇&#xff0c;也深刻影响着消费者的购物体验。 以下是四大核心优势&#xff1a; 1.流量与曝…

AWS域名注册服务:为您的在线业务打下坚实基础

在如今的数字时代&#xff0c;域名是每个在线业务的基础。一个好的域名不仅可以提升品牌形象&#xff0c;还能为用户提供便捷的访问体验。亚马逊网络服务&#xff08;AWS&#xff09;提供了强大的域名注册服务&#xff0c;帮助企业轻松获取和管理域名。我们九河云将深入探讨AWS…

电影票购买管理系统-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设残哥 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目、 源…

米联客-FPGA程序设计Verilog语法入门篇连载-24 FPGA知识_认识FPGA中的状态机

软件版本&#xff1a;无 操作系统&#xff1a;WIN10 64bit 硬件平台&#xff1a;适用所有系列FPGA 板卡获取平台&#xff1a;https://milianke.tmall.com/ 登录“米联客”FPGA社区 http://www.uisrc.com 视频课程、答疑解惑&#xff01; 1概述 让FPGA电路&#xff0c;有序…

初识C++ · 智能指针

目录 前言&#xff1a; 1 智能指针的发展历史 2 unique_ptr和shared_ptr的基本使用 3 shared_ptr的模拟实现 4 有关定制删除器 前言&#xff1a; 智能指针的引入&#xff0c;我们得先从异常开始说起&#xff0c;异常面临的一个窘境是new了多个对象&#xff0c;抛异常了会…

厂家揭秘:劳保鞋里的防砸黑科技,这些材料你了解多少?

在工业生产的前沿阵地&#xff0c;安全生产始终是企业发展的基石&#xff0c;也是每一位劳动者的头等大事。在繁忙的生产线上&#xff0c;一双看似普通的劳保鞋&#xff0c;实则蕴含着保护我们双脚免受意外伤害的重要科技——防砸材料。今天&#xff0c;百华小编就来和大家盘点…

GitLab-CI/CD指南

由于公司没有运维&#xff0c;写go服务时各个环境编译部署还是略显麻烦&#xff0c;由于代码管理使用的是 gitlab&#xff0c;所以决定使用 gitlab 自带的 CI/CD 来做自动编译和部署&#xff0c;这样每次提交代码以后就可以自动部署到服务器上了。 gitlab 本身只有 CI/CD 的接…

STM32第十二节(中级篇):串口通信(第一节)——功能框图讲解

前言 我们在51单片机中就已经学习过了串口通信的相关知识点&#xff0c;那么我们现在在32单片机上进一步学习通信的原理。我们主要讲解串口功能框图以及串口初始化结构体以及固件库讲解。 STM32第十二节&#xff08;中级篇&#xff09;&#xff1a;串口通信&#xff08;第一节…

Python绘图入门:使用Matplotlib绘制柱状图

Python绘图入门&#xff1a;使用Matplotlib绘制柱状图 柱状图是一种常见的数据可视化方式&#xff0c;能够直观地展示不同类别之间的数据差异。在Python中&#xff0c;Matplotlib是一个非常强大且灵活的绘图库&#xff0c;它不仅能绘制简单的图表&#xff0c;还能创建复杂的多…

远程命令行控制SSH

第一次接触SSH是ROS小车作为服务端&#xff0c;通过ubuntu电脑客户端访问。因为机器人接键盘和屏幕操作起来不方便&#xff0c;所以使用SSH进行连接&#xff0c;方便对小车的操作。 1.服务端安装 打开终端查看ssh是否安装 sudo service ssh status 如果未安装 sudo apt upd…

【网络】私有IP和公网IP的转换——NAT技术

目录 引言 NAT工作机制​编辑 NAT技术的优缺点 优点 缺点 个人主页&#xff1a;东洛的克莱斯韦克-CSDN博客 引言 公网被子网掩码划分为层状结构&#xff0c;一个公网IP的机器又可以用很多私有IP搭建内网。在日常生活场景中用的都是私有IP&#xff0c;例如手机&#xff0c;…

目标检测算法,Yolov7本地化部署使用(一)

安全帽检测、口罩检测、行为检测、目标物体检测&#xff0c;随着深度学习和计算机视觉技术的不断发展&#xff0c;目标检测成为了研究热点之一。YOLOv7作为YOLO系列的新成员&#xff0c;以其高效和准确的性能受到了广泛关注。本文将介绍如何在本地部署并利用YOLOv7完成目标检测…

OpenCV图像处理——按最小外接矩形剪切图像

引言 在图像处理过程中&#xff0c;提取感兴趣区域&#xff08;ROI&#xff09;并在其上进行处理后&#xff0c;往往需要将处理后的结果映射回原图像。这一步通常涉及以下几个步骤&#xff1a; 找到最小外接矩形&#xff1a;使用 cv::boundingRect 或 cv::minAreaRect 提取感兴…

计算机毕业设计 助农产品采购平台 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

C# 中 Tuple 与 ValueTuples 之间的区别

在 C# 中&#xff0c;元组和值元组都用于在单个变量中存储多个值。但它们在语法、功能和性能方面存在一些关键差异。 一.Tuples(元组) 元组是一种引用类型&#xff0c;长期以来一直是 .NET 的一部分。它们是使用 System.Tuple 类创建的。 例子 using System; class Program…

07 STM32ADC

AD单通道和AD多通道 使用ADC可以对高电平和低电平之间的任意电压进行量化&#xff0c;最终用一个变量来表示&#xff0c;读取这个变量。所以ADC就是一个电压表&#xff0c;把引脚电压值测出来&#xff0c;放在一个变量里。 数字到模拟的桥梁&#xff0c;PWM来控制电机的速度&a…

springboot家校共育平台-计算机毕业设计源码54235

摘 要 采用高效的SpringBoot框架&#xff0c;家校共育平台为家长与教师提供了便捷的沟通渠道。该平台整合了丰富的教育资源&#xff0c;实现了家校之间的即时信息互通&#xff0c;从而助力协同教育。 为进一步方便用户访问和使用&#xff0c;平台与微信小程序进行了深度整合。家…

SPAW7000高精度功率分析记录仪,测试方案

测试目标 评估双电机四驱系统中前后电机的性能。 分析前后电机之间的实时联动情况。 测量并分析电机控制器的输入与输出功率。 计算功率转换效率和损耗。 验证电机系统的谐波特性。 测试设备 SPAW7000高精度功率分析记录仪&#xff1a;用于测量功率、电压、电流等参数&am…

【LeetCode Cookbook(C++ 描述)】一刷二叉树综合(上)

目录 LeetCode #226&#xff1a;Invert Binary Tree 翻转二叉树「遍历」「分而治之」广度优先搜索&#xff1a;层序遍历 LeetCode #101&#xff1a;Symmetric Tree 对称二叉树递归法迭代法 LeetCode #100&#xff1a;Same Tree 相同的树递归法迭代法 LeetCode #559&#xff1a;…