【Linux】信号(一文学会,八千字好文深度讲解信号)

news2024/9/22 1:30:15

目录

1.信号的初步理解

2.信号处理

信号的产生  

信号的保存

前台进程和后台进程 

信号处理以及产生信号

对于信号的处理方式有三种

产生信号:

1.通过终端按键产生信号 

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

​编辑 3. 由软件条件产生信号

 4.硬件异常产生信号

除0

野指针

 3.核心转储

4.信号递达 未决 阻塞 和 忽略

pending表 block表 handler表

信号集 

函数接口介绍

信号处理

 可重入函数


1.信号的初步理解

首先生活中我们肯定使用过信号去传达信息,比如妈妈做好饭菜和你说吃饭了,那么你会立刻放下手里的工作,然后走到餐厅。

那你怎么证明linux是有信号的?

kill -l

查看linux中的所有信号

不难发现,linux的信号被分成 1-31(普通信号) 34-64(实时信号

 上面说去吃饭这个简单的过程就体现了信号的产生和处理

要知道,即使妈妈没有喊你吃饭,你也知道:如果她喊你,你会做出什么样的应答(去吃饭),所以程序员设计进程时早就设计好了对信号的识别能力,进程在没收到信号的时候,早就知道一个信号该被如何处理(就像你知道吃饭该去餐厅)

但是今天可能工作很紧张,必须要先完成才能吃饭,所以即使妈妈叫你,你也会暂时忽略,但是心里记住 饭好了 这件事,所以信号如果没被立即处理,进程需要有保存信号的能力

所以信号的产生对于进程是异步的

2.信号处理

信号的产生  

假设今天我运行起来一个进程,此时我想终止,按下ctrl c键

键盘中ctrl c,这个键盘输入产生一个硬件中断,被OS获取到解释成信号,发送给目标前台进程,前台进程收到信号,进而引起进程退出

计算机如何得知我从键盘中输入了数据?硬件中断

可以理解为CPU中有寄存器,当这个针脚为高电平的时候(表示外部有设备就绪)向寄存器写9

而OS维护了一张中断向量表

信号的保存

进程对接收到的信号一定要记录,那么怎么记录?先抽象化描述然后使用一定的结构体组织 ,使用0 1来描述普通信号,因为普通信号只要记录有无即可,实时信号稍微复杂,不在本文的讨论范围,用什么数据结构?位图!!!!!!!因为刚才说普通信号一共31个,那么32位bit的int刚好可以盛装!!!!!!!!!!这个位图保存在进程的task_struct中,比特位的位置——>信号的编号,比特位的0/1——>是否收到信号

 所以,所谓的发送信号本质是写入信号,直接修改特定进程的信号位图中特定比特位的0——>1即可

前台进程和后台进程 

linux的进程分为:前台进程,后台进程

前台进程是在终端中运行的命令,那么该终端就为进程的控制终端,一旦这个终端关闭,这个进程也随之消失

linux只允许一个进程作为前台进程运行,默认是bush,当你 ./a.out 的时候把该可执行程序以前台方式运行起来,再输入ls pwd等指令根本没效果,因为a.out不认识这些指令——也就是这个进程没有被设计对应ls/pwd等指令(信号)的处理方式

那么默认运行的前台进程bush,是被程序员设计好的,里面有对应各种诸如 ls  pwd 指令的处理方法,当你ls,这个指令会被OS获取解释成信号,发送给bush,他自己就会处理,最后在终端呈现处理结果,也就是你输入ls,自然会看到当前目录内容!

后台进程:也叫守护进程(Daemon),是运行在后台的一种特殊进程,不受终端控制它不需要终端的交互;Linux的大多数服务器就是使用守护进程实现的。无法被ctrl C 只能kill -9

这就解释了为什么大多数服务器是使用后台进程实现,如果是前台进程,一旦今天关闭终端,那么整个服务器就瘫痪了.......众所周知 服务器是万万不能关机的,也就是不能受终端控制,所以linux使用守护进程实现服务器

比如我们自己写代码运行起来变成进程,这个进程是前台进程,一旦Xshell关闭,那进程肯定会被终止

当然可以通过运行时加上&,让进程变成后台进程

实操:

  • 前台进程 

首先 我们运行一个程序

./cond

 此时这个进程在终端开始运行

while :;do ps -axj|head -1&&ps -axj|grep cond|grep -v grep;sleep 1;done

发现他确实在运行

  • 后台进程 

 现在运行一个后台进程 

./a.out&

但是当我^C的时候根本杀不掉,只能kill -9 PID

kill -9 18671

 

此时这个后台进程被终止

信号处理以及产生信号

对于信号的处理方式有三种

1.默认动作

2.忽略信号

3.用户自定义捕捉(用户可以自己设置对于某个信号的处理方式)

产生信号:

1.通过终端按键产生信号 

 这个系统调用是把收到的signum信号默认动作改为handler方法

实操:

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

void handler(int signo)
{
  cout<<"我收到了信号%d"<<signo<<endl;
}
int main()
{
  signal(2,handler);
  return 0;
}

运行起来 

发现什么都没有啊? 

一定要注意:我们这个函数只是改变了对信号2的默认动作,你得现有信号2 才能选择执行/不执行,或者执行什么动作,上面的代码都没有发送信号2 怎么可能看到现象?

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

void handler(int signo)
{
  cout<<"%d收到了信号"<<getpid()<<signo<<endl;
}
int main()
{
  signal(2,handler);
  while(true)
  {
    cout<<"hello"<<endl;
    sleep(1);
  }
  return 0;
}

为什么2号信号就是对应ctrl c??你可以试验一下把其他信号默认动作改成handler,然后ctrl c返现这个快捷键还是好使的

当然你也可以查看信号表

man 7 signal

 然后下翻

这个信号是键盘中断,对应的就是ctrl c

信号3是ctrl \ 对应也是退出进程

 那么既然可以改处理信号默认动作,是不是把所有信号都改掉,就没有任何信号可以用来终止进程?

当然不是,你能想到的大佬早就预判了,kill -9 是管理员信号,不能对他修改默认的动作


上面说的是通过键盘发信号,下面的系统调用也可以发信号

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

很简单,向pid进程发送sig 

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

void handler(int signo)
{
  cout<<"%d收到了信号"<<getpid()<<signo<<endl;
}
int main()
{
  signal(2,handler);
  int cnt=1;
  pid_t pid=getpid();
  while(cnt++)
  { 
    sleep(1);
    if(cnt==3)
    kill(pid,2); //给当前进程发送2号信号
    else if(cnt==5)
    kill(pid,9); //给当前进程发9号信号
    else 
    cout<<"hello"<<endl;
  }
  return 0;
}

 C语言的函数:

 谁调用raise,就给谁发信号

C语言函数: 

该函数的作用是异常终止一个进程,即该函数之后的代码不会再执行

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

void handler(int signo)
{
  cout<<"%d收到了信号"<<getpid()<<signo<<endl;
}
int main()
{
  signal(2,handler);
  int cnt=1;
  pid_t pid=getpid();
  while(true)
  {
    cout<<"hello"<<endl;
    if(cnt++==2)
     abort();
    sleep(1);
  }
  // while(cnt++)
  // { 
  //   sleep(1);
  //   if(cnt==3)
  //   kill(pid,2); //给当前进程发送2号信号
  //   else if(cnt==5)
  //   kill(pid,9); //给当前进程发9号信号
  //   else 
  //   cout<<"hello"<<endl;
  // }
  return 0;
}

 3. 由软件条件产生信号

当两个进程正在利用管道进行读写,此时把读端关闭,操作系统就会终止掉写进程(发送SIGPIPE信号)。这种情况称为软件条件产生信号

 所以SIGPIPE就是由软件条件产生的信号

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

所以SIGALRM信号是软件条件产生的信号

软件:闹钟,条件:一定秒数之后就会响

我们可以自己给自己设定闹钟,这闹钟会帮我们每隔一秒完成一个动作

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

void handler(int signo)
{
  cout<<getpid()<<"收到了信号"<<signo<<endl;
  alarm(1);
}

int main()
{
  signal(SIGALRM,handler);
  alarm(1); //1
  while(true)
  sleep(1);
  return 0;
}

我们还可以利用这个函数来验证:IO效率低下

 

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
#define LOG "log.txt"
void handler(int signo)
{
  cout<<getpid()<<"收到了信号"<<signo<<endl;
  alarm(1);
}

int main()
{
  umask(0);
  // signal(SIGALRM,handler);
  int cnt=1;
  alarm(1); //1
  int fd=open(LOG,O_CREAT|O_WRONLY|O_TRUNC,0666);
  while(cnt++)
  {
    write(fd,"hello",sizeof(char)*5);
    cout<<cnt<<endl;
  }
  return 0;
}

这个函数会在1s之后终止进程,而1s我们的IO次数是

 其实我们计算机1s的运行效率比这个高多了,但是io的效率就是很低

 其实OS的闹钟是用一个结构体维护起来的,看伪代码

 

 alarm函数的返回值很值得我们看一下

如果之前没有设定闹钟/之前的闹钟全部被唤醒,返回0

如果历史有还没到时间的闹钟,返回之前闹钟的剩余秒数

#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
#define LOG "log.txt"
void handler(int signo)
{
  cout<<getpid()<<"收到了信号"<<signo<<endl;
  alarm(1);
}

int main()
{
  int n1 = alarm(20); //设置一个20s的闹钟
  cout<<n1<<endl; //由于历史没有闹钟,所以n1=0
  
  sleep(15);
  //休息了15s之后被叫醒
  int n2 = alarm(30); //前一个闹钟还剩5,所以n2=5,

  cout<<n1<<" "<<n2<<endl;
  return 0;
}

 4.硬件异常产生信号

除0

#include <iostream>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
  int cnt=0;
  int num=1/cnt;
  cout<<num<<endl;
  return 0;
}

 发现除0之后能编过但是有告警(浮点数溢出)

内存中的一段代码被load到寄存器,CPU中有一个寄存器是状态寄存器,里面是比特位,用来记录本次计算是否有溢出问题,有则将对应位置比特位记为1

 CPU告诉OS某个程序有溢出问题

OS向进程PCB中发送信号:Floating point exception,该信号是8号信号

 所以除0的本质就是触发硬件异常(此时的硬件是CPU)

野指针

int* p = nullptr;
*p = 10;

在*p的时候,第一步不是写入,而是先进行虚拟到物理地址的转换

OS通过MMU(硬件)结合你要访问目标的虚拟地址,在页表中搜索

如果没有映射,那么MMU报错,如果发现有映射,但是没有访问权限,MMU也会直接报错

报错被软硬件管理者OS发现,向目标进程PCB发信号,从而终止进程

 

补充知识:

MMU是什么?

MMU(Memory Management Unit),即内存管理单元,被集成在CPU中,是现代CPU架构中不可或缺的一部分,MMU主要包含以下几个功能:

虚实地址翻译,访问权限控制,引申的物理内存管理 

 3.核心转储

是linux系统级别提供的一种能力,可以将一个进程在异常的时候,OS将核心代码部分进行核心转储,全部dump到磁盘,一般在当前进程运行的目录下形成core.pid(核心转储文件)这样的二进制文件

但是我们刚才演示那么多进程异常,却没有看见core.pid文件

这个文件确实在云服务器上看不到,因为云服务器默认关闭查看的功能

怎么证明?

ulimit -a

这个指令用于查看当前系统中特定资源对应的上限大小 

 看到这个文件显示大小是0,单位是块——>不允许当前系统形成core文件

那么我们怎么在云服务器上打开核心存储?

ulimit -c 10240

证明已经打开了核心转储文件

补充:

之前我们说ctrl c和ctrl \都是终止一个进程

但是怎么回事,这两个指令还有action的区别

一个是Term 一个是Core

 我们可以看到,Term:就是终止进程 Core:先核心转储之后再终止进程

#include <iostream>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
  while(1)
  {
    sleep(1);
    cout<<"hello"<<endl;
  }
  return 0;
}

 可以看到,Term信号是不会生成core文件的(前提是你打开了核心转储),但是Core信号会

后面的.2010就是当前进程的pid

自己随便验证一下就行

 那么核心转储有什么用?方便异常之后调试

我们写的代码默认是release,不支持调试

最需要在makefile的编译选项加上-g

	g++ -o $@ $^ -std=c++11 -g

然后运行起来刚才的程序 进入gdb调试

./test
gdb 3699  //gdb 你程序的pid
core-file core.3699 //gdb自动定位,无需自己定位问题——>事后调试

 他告诉我们是因为收到3号信号导致进程退出

为什么云服务器默认关闭核心转储文件?

生产环境的core dump一般是关闭的

嘶这个core dump有点熟悉啊(快去看进程等待的时候,我们提到过core dump但是没细讲进程等待)

status位图:

 我们可以写个试验代码

#include <iostream>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
  pid_t id=fork();
  if(id == 0)
  {
    int cnt=5;
    while(cnt--)
    {
      cout<<"我是子进程:"<<getpid()<<endl;
      sleep(1);
    }
    kill(getpid(),3);
  }
  int status=0;
  waitpid(id,&status,0);
  printf("我的子进程status中core dump:%d\n",(status>>7)&1);
  return 0;
}

 这份代码就是想让子进程打印,然后父进程阻塞式等待,然后向子进程发送3信号,注意:此时我们是打开了核心存储的,然后打印core dump

 关闭核心存储:

ulimit -c 0

4.信号递达 未决 阻塞 和 忽略

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

信号到产生到递达之间的状态:信号未决(pending)

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

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

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

pending表 block表 handler表

一个进程会维护三张表,如标题

这就是进程还没有收到信号就知道如果收到应该怎么做的原因——>进程是可以识别这个信号的

 block表:位图结构,比特位的位置表示哪个信号,比特位的内容表示是否对应信号被阻塞

pending表:位图结构,比特位的位置表示哪个信号,比特位的内容表示是否收到信号

handler表:函数指针数组:数组的下标表示信号的编号,数组特定的下标的内容 表示该信号的递达动作

 

我们发现 SID_DFL表示默认动作,SIG_IGN表示忽略信号

此外还能看到,如果定义了用户自定义方法,那么实现他

否则 每个信号有自己的默认动作(使用条件编译完成的) 

信号集 

信号集:sigset_t是位图结构,属于系统但是不属于语言,可以控制block和pending两张表,是一种数据类型

 

 所以不难看出,他是一个struct结构体

函数接口介绍

sigprocmask

 

其中oset是输出型参数,可以帮我们保存用该函数设置block表之前的block表状态 

读取 更改block表

注意:在使用sigset_t 类型的变量之前一定要对他初始化(sigemptyset/sigfillset)

  检测pending信号集

信号处理

 信号可以被立即处理吗?如果一个信号之前被block,当前解除block 对应信号会立即被抵达

但是,信号处理可以不是立即处理,而是在合适的时候,什么是合适的时候?进程从内核态切回到用户态

为什么?信号的产生时异常的 当前进程可能在做更重要的事情

用户态 执行你写的代码时进程所处的状态

内核态 .......OS代码时...

时间片:CPU分配给每个进程执行的时间段

什么时候从用户态->内核态

1.进程的时间片到了,需要切换,要执行进程切换的逻辑

2.系统调用

那么CPU怎么知道当前是用户态/内核态

 

 OS运行的本质 :在进程地址空间内运行,故无论进程如何切换[3,4G]不变——>看到OS内容和进程无关

所谓系统调用本质:如同调用动态库中的方法,在自己的地址空间中进行函数跳转并返回即可!!!!

那么我们不就可以随意访问OS的代码?为了解决这个问题 我们定义用户态和内核态

CPU中有CR3寄存器来表征正在运行的进程执行级别是用户态/内核态

谁来更改执行级别?用户肯定是没有权限的

所以,OS提供的所有系统调用内部都会在执行正式调用逻辑时修改执行级别!!!!

那么进程如何被调度?

1.OS是一个软件,是一个死循环

OS时钟(硬件)每隔很短时间向OS发送时间中断——>OS要执行对应的中断处理方法(检测当前进程的时间片)

2.进程被调度:就是时间片到了,然后将进程对应的上下文等进行保存并且切换,选择合适的进程

——这一系列的动作就是系统函数 schedule()

所以上面说到的 信号处理所谓合适的时间就是内核态切换到用户态的时候左信号检测并进行处理

 可重入函数

函数重入:不同执行流中同一个函数被重复进入

如果函数重入没有问题:可重入函数

否则:不可重入函数

之前学过的很多函数都是不可重入的,一般内部使用了链表,顺序表,STL容器,全局数据结构,调用标准IO库 都是不可重入的

 

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

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

相关文章

docker私有仓库harbor部署

1. harbor简介&#xff1a; Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器&#xff0c;通过添加一些企业必需的功能特性&#xff0c;例如安全、标识和管理等&#xff0c;扩展了开源Docker Distribution。作为一个企业级私有Registry服务器&#xff0c;Harbor提…

系列二、MongoDB的安装

一、传统方式安装 1.1、下载安装包 https://www.mongodb.com/try/download/community-kubernetes-operator 1.2、上传至opt目录并解压 tzr -xzvf mongodb-linux-x86_64-rhel70-5.0.18.tgz 1.3、移动mongodb安装包并重命名 mv mongodb-linux-x86_64-rhel70-5.0.18 /usr/local…

【Unity100个实用小技巧】屏幕后处理实现渐隐渐现

☀️博客主页&#xff1a;CSDN博客主页&#x1f4a8;本文由 我是小狼君 原创&#xff0c;首发于 CSDN&#x1f4a2;&#x1f525;学习专栏推荐&#xff1a;面试汇总❗️游戏框架专栏推荐&#xff1a;游戏实用框架专栏⛅️点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd;&#xf…

35 KVM管理设备-管理虚拟网卡

文章目录 35 KVM管理设备-管理虚拟网卡35.1 概述35.2 操作步骤 35 KVM管理设备-管理虚拟网卡 35.1 概述 虚拟网卡类型主要包含virtio-net、vhost-net、vhost-user等。用户在创建虚拟机后&#xff0c;可能会有挂载或者卸载虚拟网卡的需求。openEuler提供了网卡热插拔的功能&am…

springboot+vue宠物领养系统的设计与实现

随着国内经济的不断发展&#xff0c;人民收入水平的提高以及对于情感需求的日益增强&#xff0c;宠物饲养成为了一种流行趋势。宠物的增多不可避免地造成了流浪宠物的泛滥&#xff0c;它们大多来自被主人遗弃的动物或这些动物繁衍的后代。它们没有管束&#xff0c;游走在人类居…

自定义kong插件golang版本

开发一个go版本的kong 插件&#xff0c;通常需要以下3个步&#xff1a; 基于kong提供的go语言版本的PDK编写并且编译kong插件 --》 在kong的kong.conf文件中配置插件位置 --> 通过kong的admin api来启用插件 一、使用go语言编写kong插件 1、定义Config对象 type Config s…

嵌入式软件测试怎么实现自动化测试?

说起嵌入式软件测试&#xff0c;我们先快速了解一下嵌入式软件自身的特点。嵌入式软件具有实时性、内存空间有限、I/O通道少&#xff0c;而且要求功耗低、高可靠性&#xff0c;对成本也比较敏感&#xff0c;和硬件关联性强等特点。因此&#xff0c;嵌入式软件的测试与一般商用软…

【Vue】data属性,v-for,diff算法,composition API

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录 Vue3data属性插值语法修饰符v-modelv-for虚拟DOMdiff算法 响应式原理v-for中的keynextTick comp…

Vue3 小兔鲜:项目起步

Vue3 小兔鲜&#xff1a;项目起步 Date: May 31, 2023 创建项目并整理目录 npm init vuelatestsrc目录调整 需要补充创建以下文件&#xff1a; git 管理项目 基于create-vue 创建出来的项目默认没有初始化git仓库&#xff0c;需要我们手动初始化 执行命令井完成首次提交 …

通过脚本将本地文件上传到服务器 WinSCP

通过脚本将本地文件上传到服务器 准备工作 安装好WinSCP软件。服务器上配置了Java环境。 编写脚本 以下是一个使用WinSCP软件的脚本示例&#xff0c;用于将本地的JAR包上传到服务器并启动&#xff1a; echo offset WINSCP_PATH"C:\Program Files (x86)\WinSCP\WinSCP.…

中国人民大学与加拿大女王大学金融硕士项目——在职读金融硕士,没想到收获这么多

随着社会经济的快速发展&#xff0c;金融专业的报考越来越受欢迎。近些年来&#xff0c;市场对于金融专业的高端人才需求不断增加。工作多年的金融人或有计划跳槽到金融领域的群体&#xff0c;都想通过业余时间充电以增强在金融行业的竞争力。在职读金融硕士有用吗&#xff1f;…

基于Element的Select下拉选组件(支持快速全选)

Select下拉选增强 支持快速多选、tooltip等 示例图 1. quicklySelectAll: boolean 在多选模式下支持快速全选取消全选,默认开启 <template><div id"app"><div class"container"><el-form ref"formRef" :model"formD…

Framework开发环境搭建

Framework开发环境搭建 开启Android Framework之旅&#xff0c;一步步记录自己学习过程。 硬件配置 RAM&#xff1a;最低16GB&#xff0c;建议32GB&#xff0c;有条件64GB&#xff0c;内存越高&#xff0c;编译时间越短ROM&#xff1a;最低400GB&#xff0c;代码250GB构建15…

第八篇、基于Arduino uno,获取MAX30102心率传感器的心率信息——结果导向

0、结果 说明&#xff1a;先来看看串口调试助手显示的结果&#xff0c;第一个值是原始的IR值&#xff0c;第二个值是实时的心跳&#xff0c;第三个值是平均心跳&#xff0c;如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;MAX30102心率传感器的外观如下…

Linux:开机自动挂载磁盘出错 两种解决办法 /etc/fstab

假如在/etc/fstab文件中有一个错误的语句 但是我们假装不知道&#xff0c;直接给他重启 第一种 开机自动进入到一个这样的界面 在这我们直接输入root的密码 进入到root下 直接 vi /etc/fstab 先将错误的删除掉&#xff0c;等正常开机了再去做挂载 然后重启 正常进入系统 第二…

基于机智云物联网平台的智能种树小车

前言:针对目前人工种树效率低的问题&#xff0c;设计了一种全自助高效智能种树小车。介绍了装置的结构组成&#xff0c;剖析了装置的运动机理。通过SolidWorks三维软件对传动部件进行了结构设计。 利用蓝牙模块实现了种植过程的信息传递、发送命令等&#xff0c;物联网机智云模…

Fiddler+Proxifer 实现PC端软件的抓包

FIddlerProxifer 实现PC端软件的抓包 下载Proxifer https://www.proxifier.com/download/ Proxifier配置 默认是关闭HTTP协议的&#xff0c;使用需要配置开启&#xff0c;配置文件->高级->HTTP代理服务器->启用HTTP代理服务器支持 配置Proxifer代理服务器&#x…

4.数据结构期末复习之森林和最优二叉树(哈夫曼树)

1.森林的定义&#xff1a;m>0 颗互不相交的树的集合 //树和森林的区别 2.森林的遍历 for森林里的树(前序和后序遍历) 3.树转二叉树(兄弟连起来,去右线) 1.兄弟之间加线(树和树之间不加) 2.右孩子去线(只保留第一个结点的线) 3.顺时针调整45度位置变成二叉树 4.森林转二叉…

网络安全 2023 年为什么如此吃香?事实原来如此.....

前言 由于我国网络安全起步晚&#xff0c;所以现在网络安全工程师十分紧缺。俗话说:没有网络安全就没有国家安全 为什么选择网络安全&#xff1f; 十四五发展规划建议明确提出建设网络强国&#xff0c;全面加强网络安全保障体系和能力建设&#xff0c;加强网络文明建设&#x…

OpenGl之变换

目录 缩放 位移 齐次坐标(Homogeneous Coordinates) 旋转 缩放 对一个向量进行缩放(Scaling)就是对向量的长度进行缩放&#xff0c;而保持它的方向不变。由于我们进行的是2维或3维操作&#xff0c;我们可以分别定义一个有2或3个缩放变量的向量&#xff0c;每个变量缩放一个轴…