Linux系统编程——理解系统内核中的信号捕获

目录

一、sigaction()

使用

信号捕捉技巧

 二、可重入函数

三、volatile关键字 

四、SIGCHLD信号


在信号这一篇中我们已经学习到了一种信号捕捉的调用接口:signal(),为了深入理解操作系统内核中的信号捕获机制,我们今天再来看一个接口:sigaction()

一、sigaction()

 sigaction 一个系统调用,用于修改和/或查询信号的处理方式。它提供了对信号处理函数更精细的控制,相较于旧的 signal 函数来说更为强大和灵活。

1、int signum:该参数需要传入指定的进程信号,表示要捕捉的信号

2、const struct sigaction *act:该参数与函数同名,是一个结构体指针如图

1、void (*sa_handler)(int);,此成员的含义其实就是 自定义处理信号的函数指针;
2、void (*sa_sigcation)(int, siginfo_t *, void *);此成员也是一个函数指针. 但是这个函数的意义是用来 处理实时信号的, 不考虑分析. (siginfo_t 是表示实时信号的结构体)
3、sigset_t sa_mask;, 此成员是一个信号集, 这个信号集有什么用呢?我们在使用时解释
4int sa_flags;, 此成员包含着系统提供的一些选项, 本篇文章使用中都设置为0
5、void (*sa_restorer)(void);, 很明显 此成员也是一个函数指针. 但我们暂时不考虑他的意义.

也就是说我们暂时将该接口的第二个参数简单理解为一个结构体指针,并且结构体里有一个成员是用来自定义处理信号的。即该参数的作用就是将指定信号的处理动作改为传入的struct sigaction的内容。

3、struct sigaction *oldact,第三个参数类似sigprocmask()接口中的第三个参数一样,都是输出型参数,且其作用是获取 此次修改信号 struct sigaction之前的原本的 struct sigaction ,如果传入的是空指针,则不获取。 

使用

我们实现一个捕捉2号信号的程序,然后让他按照我们自定义的处理动作执行,且将自定义的处理动作设置为死循环,也就是让进程在收到2号信号后一直执行该信号的处理动作。

#include<iostream>
#include<unistd.h>
#include<signal.h>
using std::cout;
using std::endl;void handler(int signum)
{cout<<"I catch a signal::"<<signum<<endl;sigset_t pending;while(true){sigpending(&pending);int sig=1;for(sig=1;sig<=31;sig++){//利用死循环打印未决信号集if(sigismember(&pending,sig)){cout<<"1";}else{cout<<"0";}}cout<<endl;sleep(1);}
}
int main()
{struct sigaction act,oact;//实例化出两个结构体对象作为参数act.sa_handler=handler;//初始化自定义处理动作act.sa_flags=0;//先设置为0sigemptyset(&act.sa_mask);//初始化sigaction(2,&act,&oact);//捕捉2号信号while(true){cout<<"I am a process!My pid::"<<getpid()<<endl;sleep(1);}return 0;
}

       可以看到在我们第一次发送了2号信号后,其打印出来的未决信号集全是0,这是因为在没有捕捉到2号信号前,该进程的未决信号集全为0,捕捉之后,第二个位置应该为1,然后开始处理自定义动作前将1又置为0,表示开始处理自定义动作,此时就一直处于死循环中,即一直在执行自定义动作。

       当我们第二次发送2号信号时,它的未决信号集的对应位置就变为了1,后面再发送2号信号时,该信号都会被拦截下来导致后面的2号信号一直处于未决状态。但是发送其他信号进程又会处理其他信号。

那么我们要是想要在处理2号信号的同时还要将其他信号拦截呢?这时候就与 sa_mask 相关了,顾名思义了就是信号屏蔽字。 

 struct sigaction结构体的sa_mask 成员的意义是, 添加进程在处理捕捉到的信号时对其他信号的阻塞. 如果需要添加对其他信号的阻塞, 那么就可以继续在 sa_mask 中添加其他信号.

不过, 这样做有什么意义呢?

这样做可以 防止用户自定义处理信号时, 嵌套式的发送其他信号并捕捉处理.

如果 用户的自定义处理信号方法内部, 还会发送其他信号, 并且用户还对其进行了捕捉. 那么 信号的处理就无止尽了. 这种情况是不允许发生的.

所以 可以通过使用 sa_mask 来进行对其他信号的拦截阻塞.

信号捕捉技巧

为了避免捕捉不同的信号并做处理时,编写不同的处理函数太过于麻烦,我们可以考虑通过传入相同的函数指针实现对不同信号的不同处理。

 当我们定义完指定信号的处理函数之后, 我们可以再定义一个 handlerAll(int signo) 函数, 并使用 switch 语句, 将不同的 signo 分别处理.

此时, 我们在使用 signal() 或者 sigaction() 捕捉信号时, 就只需要统一传入 handlerAll 的函数指针就可以了.

这是一种 解耦技巧

 二、可重入函数

可重入函数(Reentrant Function)是指可以在多线程环境中安全使用的函数,即这个函数可以被多个线程同时调用而不会导致数据错误或不一致。

下面用个例子解释一下

一个进程中, 存在一个全局的单链表结构. 并且此时需要执行一个节点的头插操作:

此时需要让该结点的指向下一个节点的指针指向头节点,再将自己变为头节点

node1->next = head;
head = node1;

如果在刚执行完第一步之后, 进程因为硬件中断或者其他原因 陷入内核.

陷入内核之后需要回到用户态继续执行, 切换回用户态时 进程会检测未决信号, 如果此时刚好存在一个信号未决, 且此信号自定义处理.并且, 自定义处理函数中 也存在一个新节点头插操作:

 

此时又会执行node2节点的头插 ,执行完后node2节点暂时就成为了新的头节点

接着进程返回用户态去执行剩下的代码,即 head=node1 ,

导致的结果就是node2节点最终找不到了,这样就造成了 内存泄漏 

是因为 单链表的头插函数, 被不同的控制流程调用了, 并且是在第一次调用还没返回时就再次进入该函数, 这个行为称为 重入

而 像例子中这个单链表头插函数, 访问的是一个全局链表, 所以可能因为重入而造成数据错乱, 这样的函数 被称为 不可重入函数, 即此函数不能重入, 重入可能会发生错误

反之, 如果一个函数即使重入, 也不会发生任何错误(一般之访问函数自己的局部变量、参数), 这样的函数就可以被称为 可重入函数. 因为每个函数自己的局部变量是独立于此次函数调用的, 再多少次调用相同的函数, 也不会对之前调用函数的局部变量有什么影响.

如果一个函数符合以下条件之一,则称为不可重入函数

  1. 调用了malloc和free,因为 malloc 也是用全局链表来管理堆的。
  2. 调用了标准I/O库函数, 标准I/O库的很多实现都以不可重入的方式使用全局数据结构

三、volatile关键字 

       在之前学习C/C++的时候,我们就已经接触过这个关键字了,它的作用是防止编译器对该关键字修饰的变量的优化,确保每次访问这个变量时都直接从内存中读取,而不是使用可能存在的寄存器中的缓存值。这是因为在某些情况下,变量的值可能会被外部因素改变,如硬件中断、多线程环境下的其他线程等。

接下来我们用一个例子分析一下该关键字

下面的程序是先定义一个全局变量flag,以该全局变量作为触发条件,当为0时,一直处于死循环状态。当为1时程序正常结束。在main()中不对flag做修改,只有在捕获到2号信号的时候,在自定义的处理函数中才会对flag做出修改。

#include <stdio.h>
#include <signal.h>int flags = 0;void handler(int signo) {printf("获取到一个信号,信号的编号是: %d\n", signo);flags = 1;printf("我修改了flags: 0->1\n");
}int main() {signal(2, handler);while (!flags);// 未接收到信号时, flags 为 0, !flags 恒为真, 所以会死循环printf("此进程正常退出\n");return 0;
}

 可以看到在发送了2号信号后程序正常结束。

  • 虽然 2信号的自定义处理函数 会对flags作出修改, 但是这个函数的执行是未知的. 即 不确定进程是否会收到2信号 进而执行此函数.
  • 那么对编译器来说, 就有可能对 flags 做出优化.
  • 我们知道, 进程再判断数据时, 是CPU在访问数据, 而CPU访问数据时 会将数据从内存中拿到寄存器中. 然后再根据寄存器中的数据进行处理.
  • 在此例中, 就是 while(!flags); 判断时, CPU会从内存中拿出数据进行判断. 当flags从0变为1时, 是内存中的数据发生了变化, CPU也会从内存中拿到新的数据进行判断
  • 而 此例中编译器可以确定一定会执行的代码中, flags是不会被修改的. 那么 编译器就可能针对flags做出优化:
  • 由于编译器认为进程不会修改 flags, 那么在 while(!flags); 判断时, CPU读取到flags为0 并存放在寄存器中之后, 为了节省资源 在之后的判断中 CPU 不会再从内存中读取数据, 而是直接根据寄存器中存储的数据进行判断. 
  • 这就会造成 即使处理信号时将 flags 改为了1, 在进行 while(!flags);判断时, CPU依旧会只根据寄存器中存储的0 来进程判断, 这就会造成 进程不会正常退出

我们可以在编译是使用 -02 选项让编译器做出这样的优化 

可以看到即使是flag改成了1,程序依然不会停止

接着我们使用关键字修饰这个变量 volatile int flag=0 ,再查看结果

可以看到已经没有优化了。

四、SIGCHLD信号

我们之前讲到过,在子进程退出的时候,是需要让父进程接收退出信息的,否则子进程会进入僵尸状态,所以我们介绍了有关于进程等待的函数,让父进程主动去询问子进程是否退出,事实上,子进程退出的时候是会通知父进程的,只不过父进程会忽略而已。

在子进程退出的时候,子进程会向父进程发送一个信号,即 SIGCHID

下面我们在父进程中捕捉这个信号看看情况:

#include<iostream>
#include<cstdlib>
#include<signal.h>
#include<unistd.h>
using std::cout;
using std::endl;
using std::cerr;
void handler(int signum)
{cout<<"Child process has exited,mypid::"<<getpid()<<"Signal num::"<<signum<<endl;
}
int main()
{signal(SIGCHLD,handler);pid_t id=fork();if(id<0){cerr<<"fork error!"<<endl;exit(1);}else if(id==0){while(true){cout<<"I am child process,mypid::"<<getpid()<<endl;sleep(2);}exit(0);}while(true){cout<<"I am parent process!my pid::"<<getpid()<<endl;sleep(2);}return 0;}

17号信号即 SIGCHID信号 ,且默认动作是Ign(忽略)

 但是对于该信号,内容中说明 子进程暂停或终止,即在子进程暂停或终止的时候都会发送该信号给父进程。我们做个测试,首先需要知道暂停信号是19号,继续信号是18号信号

可以看到无论是暂停、继续、还是终止子进程的时候,其都会向父进程发送该信号。

那么我们知道了这个信号又有什么用处呢?

 在介绍进程等待时 提到过,waitpid()接口会等待子进程退出, 而等待的动作是主动去询问子进程是否退出.


现在我们清楚了子进程在退出的时候会发送SIGCHID信号给父进程,那我们让父进程可以通过捕捉该信号去等待子进程。

#include<iostream>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<signal.h>
using std::cout;
using std::endl;
using std::cerr;
void Childprofree(int signo)
{assert(signo==SIGCHLD);pid_t id=waitpid(-1,nullptr,0);if(id>0){cout<<"Waiting success!pid::"<<getpid()<<"child process id::"<<id<<endl;}}int main()
{signal(SIGCHLD,Childprofree);pid_t id=fork();if(id<0){cout<<"Perror fork!"<<endl;exit(0);}else if(id==0){while(true){cout<<"I am child process!my pid::"<<getpid()<<endl;sleep(2);}exit(0);}while(true){cout<<"I am parent process!my pid::"<<getpid()<<endl;sleep(2);}return 0;
}


上面的程序只针对单进程的情况,如果是多个进程的情况下就会有一些问题

 我们利用循环创建10个子进程,这10个子进程在打印完十次之后自动退出。

#include<iostream>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<signal.h>
using std::cout;
using std::endl;
using std::cerr;
void Childprofree(int signo)
{assert(signo==SIGCHLD);pid_t id=waitpid(-1,nullptr,0);if(id>0){cout<<"Waiting success!pid::"<<getpid()<<"child process id::"<<id<<endl;}}int main()
{signal(SIGCHLD,Childprofree);for(int i=0;i<10;i++){pid_t id=fork();if(id<0){cout<<"Perror fork!"<<endl;exit(0);}else if(id==0){int cnt=10;while(cnt){cout<<"I am child process!my pid::"<<getpid()<<"cnt::"<<cnt--<<endl;sleep(2);}cout<<"Child process being Z!"<<endl;exit(0);}}while(true){cout<<"I am parent process!my pid::"<<getpid()<<endl;sleep(2);}return 0;
}

我们在右边窗口使用 while :;do ps ajx|head -1&&ps ajx|grep mydwait|grep -v grep;sleep 2;done命令循环查看进程情况

运行结果如图

 可以看到等到子进程推出的时候,按道理是所有的子进程都退出都被父进程回收掉,但是只有左边红色框里这几个进程退出了,右边红色框里还有几个没有被回收掉,一直处于僵尸状态。

      在Linux中,每个进程都有一个信号集,用来表示该进程当前接收到的、尚未处理的信号。这个集合有一个重要特点:它是基于信号类型的,而不是基于信号的数量。这意味着对于同一类型的信号(例如多个 SIGCHLD),操作系统不会为每个信号单独排队,而是只会记录该类型信号至少发生过一次。换句话说,如果多个 SIGCHLD 信号几乎同时到达,操作系统会将它们合并成一个信号,并只传递给父进程一次。

因此,如果大量的子进程几乎在同一时间结束,父进程可能只接收到一个 SIGCHLD 信号,而实际上有多个子进程已经终止。这就导致了父进程可能没有机会处理所有终止的子进程,从而留下僵尸进程。

那么怎么修改处理信号的方式呢?我们将回收子进程的部分设置为 利用死循环回收,在没有子进程的情况下跳出循环。

一旦有收到子进程的退出信号后,这个外部的函数就会进入死循环,不断等待释放需要退出的子进程,直到没有子进程需要释放了就退出。事实上这个改动就跟收到信号没有关系了,单纯利用循环不断等待需要被释放的子进程。

 

void freeChild(int signo) {assert(signo == SIGCHLD);while(true) {pid_t id = waitpid(-1, nullptr, 0);if (id > 0) {cout << "父进程等待子进程成功, child pid: " << id << endl;}else {cout << "等待结束" << endl;break;}}
}

 

 可以看到最后所有的进程都退出了。


但是新的问题又出现了,一旦有子进程不退出的话,父进程就不会再运行了,因为我们设置的waitpid()的第三个参数为0是阻塞式等待,所以会一直处在外部的自定义处理函数中,不会回到main()函数

观察下面的情况,我们让一部分子进程循环5次后退出,一部分循环30次后退出

#include <cassert>
#include <cstdlib>
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using std::cout;
using std::endl;void freeChild(int signo) {assert(signo == SIGCHLD);while (true) {pid_t id = waitpid(-1, nullptr, 0);if (id > 0) {cout << "父进程等待子进程成功, child pid: " << id << endl;}else {cout << "等待结束" << endl;break;}}
}int main() {signal(SIGCHLD, freeChild);for (int i = 0; i < 10; i++) {pid_t id = fork();if (id == 0) {// 子进程int cnt = 0;if(i < 6)cnt = 5;        // 前6个子进程 5就退出elsecnt = 30;       // 后4个子进程 30退出while (cnt) {cout << "我是子进程, pid: " << getpid() << ", cnt: " << cnt-- << endl;sleep(1);}cout << "子进程退出, 进入僵尸状态" << endl;exit(0);}}// 父进程while (true) {cout << "我是父进程, pid: " << getpid() << endl;sleep(1);}return 0;
}

 

可以看到在所有进程退出之前父进程代码并没有运行,我们只需要将 waitpid()的第三个参数改为 WNOHANG 就可以了表示 非阻塞式等待。 


下面是最终版代码

 

#include<iostream>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<signal.h>
using std::cout;
using std::endl;
using std::cerr;
void Childprofree(int signo)
{assert(signo==SIGCHLD);while(true){pid_t id=waitpid(-1,nullptr,WNOHANG);if(id>0){cout<<"Waiting success!pid::"<<getpid()<<"child process id::"<<id<<endl;}else{cout<<"Wating end!"<<endl;break;}}}int main()
{signal(SIGCHLD,Childprofree);for(int i=0;i<10;i++){pid_t id=fork();if(id<0){cout<<"Perror fork!"<<endl;exit(0);}else if(id==0){int cnt = 0;if(i < 6)cnt = 5;        elsecnt = 30;     while(cnt){cout<<"I am child process!my pid::"<<getpid()<<"cnt::"<<cnt--<<endl;sleep(2);}cout<<"Child process being Z!"<<endl;exit(0);}}while(true){cout<<"I am parent process!my pid::"<<getpid()<<endl;sleep(2);}return 0;
}

 

 

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

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

相关文章

IEC104 协议 | 规约帧格式 / 规约调试

注&#xff1a;本文为 “ IEC104 协议” 相关文章合辑。 未整理去重&#xff0c;如有内容异常请看原文。 图片清晰度限于引文原状。 从零开始理解 IEC104 协议之一 ——104 规约帧格式 洪城小电工 IP 属地&#xff1a;江西 2020.06.10 00:30:54 前言 本文根据相关标准、本…

WPS如何快速将数字金额批量转换成中文大写金额,其实非常简单

大家好&#xff0c;我是小鱼。 在日常的工作中经常会遇到需要使用金额大写的情况&#xff0c;比如说签订业务合同时一般都会标注大写金额&#xff0c;这样是为了安全和防止串改。但是很多人也许不太熟悉金额大写的方法和习惯&#xff0c;其它没有关系&#xff0c;我们在用WPS制…

针对超大规模病理图像分析!华中科技大学提出医学图像分割模型,提高干燥综合征诊断准确性

口干、眼干、皮肤干&#xff0c;每天伴有不明原因的肌肉酸痛和全身乏力&#xff0c;如果以上症状你「中招」了&#xff0c;除了考虑冬季天气干燥外&#xff0c;还应该警惕一种常见却总是被我们忽视的疾病——干燥综合征 (Sjgren’s Syndrome, SS)。 干燥综合征是以外分泌腺高度…

本地部署 LLaMA-Factory

本地部署 LLaMA-Factory 1. 本地部署 LLaMA-Factory2. 下载模型3. 微调模型3-1. 下载数据集3-2. 配置参数3-3. 启动微调3-4. 模型评估3-5. 模型对话 1. 本地部署 LLaMA-Factory 下载代码&#xff0c; git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Facto…

[创业之路-199]:《华为战略管理法-DSTE实战体系》- 3 - 价值转移理论与利润区理论

目录 一、价值转移理论 1.1. 什么是价值&#xff1f; 1.2. 什么价值创造 &#xff08;1&#xff09;、定义 &#xff08;2&#xff09;、影响价值创造的因素 &#xff08;3&#xff09;、价值创造的三个过程 &#xff08;4&#xff09;、价值创造的实践 &#xff08;5&…

ASP.NET |日常开发中定时任务详解

ASP.NET &#xff5c;日常开发中定时任务详解 前言一、定时任务的概念与用途1.1 定义1.2 应用场景 二、在ASP.NET中实现定时任务的方式2.1 使用System.Timers.Timer2.2 使用Quartz.NET 三、定时任务的部署与管理3.1 部署考虑因素3.2 管理与监控 结束语优质源码分享 ASP.NET &am…

【unity】【游戏开发】Unity项目一运行就蓝屏报Watch Dog Timeout

【背景】 由于是蓝屏所以没法截屏&#xff0c;总之今天遇到了一开Unity&#xff0c;过一阵就蓝屏的情况&#xff0c;报Watch Dog Timeout。 【分析】 通过任务管理器查看&#xff0c;发现Unity占用率100%&#xff0c;再观察Unity内部&#xff0c;每次右下角出现一个Global I…

如何从 0 到 1 ,打造全新一代分布式数据架构

导读&#xff1a;本文从 DIKW&#xff08;数据、信息、知识、智慧&#xff09; 模型视角出发&#xff0c;探讨数字世界中数据的重要性问题。接着站在业务视角&#xff0c;讨论了在不断满足业务诉求&#xff08;特别是 AI 需求&#xff09;的过程中&#xff0c;数据系统是如何一…

java全栈day20--Web后端实战(Mybatis基础2)

一、Mybatis基础 1.1辅助配置 配置 SQL 提示。 默认在 mybatis 中编写 SQL 语句是不识别的。可以做如下配置&#xff1a; 现在就有sql提示了 新的问题 产生原因&#xff1a; Idea 和数据库没有建立连接&#xff0c;不识别表信息 解决方式&#xff1a;在 Idea 中配置 MySQL 数…

深度学习每周学习总结J9(Inception V3 算法实战与解析 - 天气识别)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 目录 0. 总结Inception V1 简介Inception V3 简介1. 设置GPU2. 导入数据及处理部分3. 划分数据集4. 模型构建部分5. 设置超参数&#xff1…

重温设计模式--中介者模式

中介者模式介绍 定义&#xff1a;中介者模式是一种行为设计模式&#xff0c;它通过引入一个中介者对象来封装一系列对象之间的交互。中介者使得各个对象之间不需要显式地相互引用&#xff0c;从而降低了它们之间的耦合度&#xff0c;并且可以更方便地对它们的交互进行管理和协调…

【开源库 | xlsxio】C/C++读写.xlsx文件,xlsxio 在 Linux(Ubuntu18.04)的编译、交叉编译

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a; 2024-12-20 …

NACA四位数字翼型

NACA四位数字翼型&#xff0c;以NACA 2412为例 第一位数字2 —相对弯度 第二位数字4 —相对弯度所有位置&#xff08;单位化后的&#xff09; 最末两位数字12 —相对厚度 所有NACA四位数字翼型的&#xff08;相对厚度所在的位置&#xff09;

DataX与DataX-Web安装与使用

DataX github地址&#xff1a;DataX/introduction.md at master alibaba/DataX GitHub 环境准备 Linux环境系统 JDK&#xff08;1.8及其以上版本&#xff0c;推荐1.8&#xff09; Python&#xff08;2或者3都可以&#xff09; Apache Maven 3.x&#xff08;源码编译安装…

电子应用设计方案69:智能护眼台灯系统设计

智能护眼台灯系统设计 一、引言 随着人们对眼睛健康的重视&#xff0c;智能护眼台灯成为了越来越多人的选择。本设计方案旨在打造一款功能丰富、护眼效果显著且智能便捷的台灯系统。 二、系统概述 1. 系统目标 - 提供无频闪、无蓝光危害的均匀柔和光线&#xff0c;保护眼睛。…

cesium 常见的 entity 列表

Cesium 是一个用于创建3D地球和地图的开源JavaScript库。它允许开发者在Web浏览器中展示地理空间数据,并且支持多种类型的空间实体(entities)。 Entities是Cesium中用于表示地面上或空中的对象的一种高层次、易于使用的接口。它们可以用来表示点、线、多边形、模型等,并且可…

在Visual Studio 2022中配置C++计算机视觉库Opencv

本文主要介绍下载OpenCV库以及在Visual Studio 2022中配置、编译C计算机视觉库OpenCv的方法 1.Opencv库安装 ​ 首先&#xff0c;我们需要安装OpenCV库&#xff0c;作为一个开源库&#xff0c;我们可以直接在其官网下载Releases - OpenCV&#xff0c;如果官网下载过慢&#x…

【Java基础面试题035】什么是Java泛型的上下界限定符?

回答重点 Java泛型的上下界限定符用于对泛型类型参数进行范围限制&#xff0c;主要有上界限定符和下届限定符。 1&#xff09;上界限定符 (? extends T)&#xff1a; 定义&#xff1a;通配符?的类型必须是T或者T的子类&#xff0c;保证集合元素一定是T或者T的子类作用&…

WPF+MVVM案例实战与特效(四十七)-实现一个路径绘图的自定义按钮控件

文章目录 1、案例效果2、创建自定义 PathButton 控件1、定义 PathButton 类2、设计样式与控件模板3、代码解释3、控件使用4、直接在 XAML 中绑定命令3、源代码获取4、总结1、案例效果 2、创建自定义 PathButton 控件 1、定义 PathButton 类 首先,我们需要创建一个新的类 Pat…

共模电感的工作原理

共模电感也称为共模扼流线圈&#xff0c;是一种抑制共模干扰的器件&#xff0c;它是由两个尺寸相同&#xff0c;匝数相同的线圈对称地绕制在同一个铁氧体环形磁芯上&#xff0c;形成的一个四端器件。当共模电流流过共模电感时&#xff0c;磁芯上的两个线圈产生的磁通相互叠加&a…