信号,信号列表,信号产生方式,信号处理方式

news2024/12/27 15:14:37

什么是信号

信号在我们的生活中非常常见;如红绿灯,下课铃,游戏团战信号,这些都是信号;信号用来提示接收信号者行动,但接收信号的人接收到信号会进行一系列的行为,完成某个动作;这就是信号;

接下来我们通过生活中的信号来辅助理解信号:

我们具备识别信号的能力,例如红绿灯,小孩本来不知道红绿灯的意思,大人们教会了小朋友红灯行绿灯行,小朋友们才能识别红绿灯信号;

1.操作系统可以识别我们的信号,执行一些列行为;

信号提示的行为我们可能不会立刻执行,例如下课铃响了,老师说再多讲几分钟,等会在下课,说明老师把下课的信号存储了,等课讲完了,才会处理信号提示的行为,让我们下课;

2.信号可以被暂时忽略,操作系统可以先执行它正在调度的工作,之后再去处理此信号;(至于什么时候去处理,就得看调度器的调度了)

信号的发出是随机的,就比如王者荣耀打团的时候,我们并不知道什么时候队友会发信号,我们什么时候该发起进攻;

3.信号产生是随机的,对于进程是异步的;

我们处理信号的行为是多样的;就比如在上课的时候,你的外卖到了,你可以选择忽略外卖到了的信号,继续听课,听完课之后再去拿;也可以直接去拿(因为你实在是太饿了);或者这个外卖不是你的你是帮别人拿的;这几种不同的行为也是计算机中处理信号的几种不同行为

4.信号的处理方式是多样的(下面详细讲解);

理解信号

信号是由谁发出的呢?信号是怎么产生的呢?我们先抛出这样的问题,然后慢慢理解;

通过场景来理解信号产生与执行信号的过程

我们带入这样一个场景,当我们写了一份死循环代码,我们使用ctrl+c组合键来终止进程;这个过程发生了什么?

初步的理解:

我们可以这么理解操作系统发出了终止信号,让进程终止了;

底层的理解:

1.首先我们在键盘上按了ctrl+c的按键,使得操作产生系统中断(键盘工作方式),使得操作系统接收到这个信息;

2.操作系统将此信息转换为信号(产生信号)发送给正在运行的进程,如何发送呢?进程有它的pcb结构体,pcb结构体中存储着保存信号的数据结构(位图),操作系统通过修改位图上的标志位,写信号给进程;信号发送成功就是进程pcb数据结构被修改成功;

3.在操作系统运行时会不断检查进程的信号,这个速度非常快(也是调度器来操作),当检查到某个进程的新为终止的时候,就会进行终止操作使得此进程被终止;

这就是信号产生与发送信号最后执行信号的过程;我们再来看看进程pcb中的位图可以存储哪些信号;

信号列表

通过kill -l查看信号列表

我们可以看到有62个信号(没有第32,33);

其中1至31是普通信号,我们现在学习的信号,而34至64是实时信号,他们的要求更高,当操作系统发送给进程时会立即执行此信号,场景(智能汽车的自动避障与刹车);

man 7手册

我们可以通过man 7 signal来查看这些信号的细节:

gpt:

man 7 这节主要包含了关于各种杂项的信息,比如惯例与协议、文件格式以及一些杂项的说明文档。

  • man-pages(手册页的概述和说明)
  • ascii(ASCII 字符集)
  • environ(环境变量)
  • filesystems(文件系统相关)
  • ip(IP 协议相关)
  • socket(套接字编程接口)
  • signal(信号处理)
  • standards(标准与规范)

 产生信号方式

通过终端键盘产生信号

通过键盘产生信号ctrl+c实质是发送了SIGINT 2操作,ctrl+\是发送SIGQUIT 2操作,他们的默认功能都是终止进程;

先介绍一个接口signal

signal接口

这个系统接口可以捕捉我们的信号,对信号行为自定义;参数1是要捕捉的信号的宏例如SIGINT或者2;参数2传递的是一个回调函数,来自定义我们的信号行为,不再进行默认操作; 

示例:

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

void catchsig(int signalnum)
{
    cout<<"catch signalnum is: "<<signalnum<<endl;
    exit(1);//读取信号后退出
}

int main()
{
    signal(SIGINT,catchsig);
    signal(SIGQUIT,catchsig);
    while(1)
    {
        cout<<getpid()<<": 我正在运行"<<endl;
        sleep(1);
    }
    return 0;
}

这是一份死循环;我们运行它并使用ctrl+c和ctrl+\终止进程;

所以ctrl+c与ctrl+\ 确实是分别产生了2和3信号;

另外我们还可以设置core文件的大小进行核心转储;

core dump核心转储

这实际上是一种debug的方式,通过接收信号默认行为为core的信号来将进程的debug数据存储到当前目录中;默认行为为core的信号一般都是进程产生了bug;而核心转储功能一般是在生产环境(就是编写代码的环境)下才会被打开的;如果我们发送默认行为为core的信号没有创建出core file文件的话就是核心转储功能没有被打开我们可以使用ulimit -a来查看;

使用ulimit -c (大小)设置core文件大小打开核心转储功能;

之后我们运行ctrl+\这是SIGQUIT 3命令默认行为是进行core dump会生成core file文件

我们在gdb中就可以查看此信息;

核心转储默认不打开,因为为了保护用户的数据安全,还有就是占用内存空间问题;

回顾一下

在我们前面进程等待那块waitpid的status参数:

通过系统调用产生信号

我们在bash上直接输入kill 命令是封装了系统调用发送信号的;

示例:

 其实kill命令是封装了kill接口:

kill接口

这就是向进程pid发送sig号指令;

raise函数

向自己发送sig号指令;

abort函数 

和exit函数相同,都是发送信号使得当前进程退出;发送6号SIGABRT信号给自己;

通过软件条件产生信号

软件产生信号,就是由于软件的某些行为产生某些条件从而发出信号;接下来我们举两个例子辅助理解;

匿名管道的13号信号

在我们前面学习匿名管道的时候,当读端关闭,写端还在写的时候,写端就会被终止;这种情况就是写端在读端关闭时,写端没读端发出的信号杀死了;

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

int main()
{
    int pipefd[2];
    int ret_pipe = pipe(pipefd);
    assert(ret_pipe != -1);
    (void)ret_pipe;
    pid_t pid = fork();
    if (pid == 0) // 子进程
    {
        close(pipefd[0]);
        int i=0;
        while (true) // 死循环 永远不会退出
        {
            cout << getpid() << ": 我是子进程,我正在发消息" << i++ << endl;
            int tem = 1;
            write(pipefd[1], &tem, sizeof(tem));
            sleep(1);
        }
        exit(1);
    }
    close(pipefd[1]); // 关闭写管道
    for (int i = 0; i < 5; i++)
    {
        cout << getpid() << ": 父进程进程正在运行" << endl;
        sleep(1);
    }
    close(pipefd[0]);
    cout << "父进程读管道关闭成功" << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << getpid() << ": 父进程正在运行" << endl;
        sleep(1);
    }

    wait(nullptr);
    return 0;
}

可以看到子进程本来在死循环一直在向管道中写数据,但是由于父进程的读管道被关闭了,所以子进程直接退出了;这就是软件上管道读端关闭条件满足,会发信号给写端的进程,使得写端退出;发的信号是13号信号;

 我们改写一下代码捕捉13号信号;

void hander(int signum) 自定义行为
{
    cout<<"catch signal : "<<signum<<endl;
}

int main()
{
    int pipefd[2];
    int ret_pipe = pipe(pipefd);
    assert(ret_pipe != -1);
    (void)ret_pipe;
    pid_t pid = fork();
    if (pid == 0) // 子进程
    {
        close(pipefd[0]);

        signal(13,hander); 增加一个捕捉信号

        。。。。。。
    }
    close(pipefd[1]); // 关闭写管道
    。。。。。
}

可以看到: 

我们的程序变成了死循环代码,因为我们子进程一直在向管道中写数据,但是因为读端被关闭了,所以向写端发送了信号,而写端捕捉了这个信号,处理器在处理的时候发现了这个错误,但这个错误一直没有被修改所以这个信号就一直重复的发送;这就是软件上的发送信号;

我的思考:我认为这里的死循环其实也已经算是硬件异常产生的信号了,应该是操作系统检查到某个硬件的操作一直是错误的所以就会不断的向进程发送信号导致死循环;

alarm时钟信号

alarm接口可以在seconds秒后发送一个时钟信号给当前进程;

alarm的默认行为是忽略

就是说我们可以通过alarm设置一个闹钟,当seconds秒后会自动做一些事情,这常用来做一些自动的周期性的功能;比如我们手机的屏幕显示,当我们无操作30秒时,手机自动熄屏;用户端登录的自动退出;这些都可以使用alarm来进行周期性的工作;

下面我们通过代码来辅助理解:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <ctime>
#include <functional>
#include<vector>
using namespace std;

// typedef void(*func)();
typedef function<void()> func;

vector<func> task_table;

void handler(int signum)
{
    cout << "catch signal: " << signum << endl;
    for(auto& func:task_table)
    {
        func();
    }
    alarm(1);
}
void log()
{
    time_t curtime = time(nullptr);
    cout << asctime(localtime(&curtime)) << "打印日志信息" << endl;
}

void exam()
{
    cout<<"检查一下程序"<<endl;
}

void load()
{
    task_table.push_back(log);
    task_table.push_back(exam);
}

int main()
{
    alarm(1);
    load();//加载任务列表
    signal(14, handler);
    int count=0;
    while (true)
    {
        cout << getpid() << ": 正在运行中"<<count++ << endl;
        sleep(1);
    }
    return 0;
}

现象: 

 

所以这就是软件层面上发送信号给进程从而完成某些任务; 

拓展:

计算出一秒内cpu可以完成多少次++:

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

static int count=0;

void handler(int signum)
{
    cout<<"count="<<count<<endl;
    alarm(1);
}

int main()
{
    alarm(1);
    signal(14, handler);
    while (true)
    {
        count++;
    }
    return 0;
}

通过硬件异常产生信号

什么是硬件异常呢?就是硬件发生了错误嘛,这个错误会被操作系统检查出来,检查出错误了的话,就会发送信号给我们的进程,告诉进程你这样的行为会导致我们的硬件出现错误,所以你得修改的你的代码;

我们直接据一个例子:除0错误

我们先写一份除0错误的代码并捕捉除0错误的信号:

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

void handler (int signum)
{
    cout<<"catch signal : "<<signum<<endl;
    sleep(1);
}

int main()
{
    pid_t pid = fork();
    assert(pid != -1);
    if (pid == 0) // 子进程
    {
        signal(SIGFPE,handler);
        int a = 10;
        a /= 0;
    }
    wait(nullptr);
    // int status=0;
    // waitpid(pid,&status,0);
    // cout<<WTERMSIG(status)<<endl;
    return 0;
}

我们可以看到我们的程序结果是进入了死循环:

这是为什么呢?其实前面在软件产生信号的管道发送信号方法的最后我们也说了;

因为处理我们数据的是硬件。这里进行除0的是我们的cpu,cpu进行除0操作后,我们的寄存器中存储了这次计算的状态;而这次除0的结果是错误的,状态也就被标识成了错误,所以操作系统对这个寄存器进行检查时就会发现这个错误,从而向进程发送信号;而我们的进程又捕捉了这个信号,此信号的行为被改变了,本应该退出进程变成了打印,并没有解决这个错误,所以进程没有退出,而进程没有退出继续运行,os在调度的时候不断检查进程信号位图,位图中依旧有这个信号,会继续运行信号的自定义行为,从而产生死循环;

梳理总结一下:

1.除0操作由cpu进行,cpu是硬件;

2.cpu除0后将除0后的数据放入cpu的寄存器中,而寄存器中有标记位供os来判断计算结果是否正确;

3.os检查出结果错误之后产生信号发送给进程;

4.进程没有解决硬件的异常错误使得进程进入死循环;

这就是由硬件异常而产生的信号;

我们可以通过一个坐标来说明信号的状态:

信号状态

 接下来我们解释一下信号一些状态名词例如:

信号保存:信号产生后由操作系统发送给进程,操作系统将信号写入进程pcb的位图中;

信号未决:当信号被保存之后,信号还未抵达内核由操作系统处理的时候;

信号抵达:当进程中信号的数据结构进入内核时就是抵达,忽略,默认,捕捉是抵达后对信号的三种处理方式;

阻塞:当信号被保存后,通过阻塞使得信号无法抵达的操作

信号保存

信号到底是如何被保存的呢,我们前面只知道信号是被保存在进程pcb的位图中,但是具体是什么样的呢?

信号在pcb中的结构

这三个表又是一一对应的,pending信号集,block屏蔽字(也可以叫信号机),handler方法表;它们从1到31位置代表的就是我们31个普通信号的位置;

信号产生后由操作系统写入pending信号集中,再对block屏蔽字进行检查,如果此屏蔽字bit位为1则代表信号被屏蔽无需抵达,如果信号屏蔽字bit位为0即代表此信号没有被屏蔽需要检查handler方法表,如果方法表相应信号位置不为空即执行相应方法; 

sigset_t封装类型

操作系统对我们的信号数据结构的类型进行了封装,将其封装为了sigset_t类型;

所以sigset_t  pending 与 sigset_t  block,它们是这样的数据类型;这样的封装,使得用户无法直接对pending与block进行位操作,如果相对其进行位操作,需要通过os给出的接口:

对sigset_t进行操作的接口:

sigemptyset:将set每个bit位置为0;

sigfillset:将set bit全置为1; 

sigaddset:向set中添加signum号信号;

sigdelset:从set删除signum号信号;

sigismember:检查signum是否再set中存在;

sigprocmask

此接口的第一个参数是对屏蔽字block进行how操作,how选项就和我们再open函数中的设置选项一般由宏来控制:

这就是man手册中记载的how的三个选项的宏 ;

第二个参数sigset_t类型的set,这个set是由我们自己设置的,我们可以是用前面的接口来设置这个set;

而how的三个宏函数需要和我们的这个第二个参数配合:

SIG_BLOCK :  block=block|set;

SIG_UNBLOCK :  block=block&~set;

SIG_SETMASK :  block=set;

形成这样的操作;

第三个参数是输出型参数,将原有的block屏蔽字输出到oset中;

返回值:操作正确返回0操作出现错误返回-1;

sigpending

这个接口是用来将pending信号集输出到我们的第二个参数set中;

返回值:操作正确返回0操作出现错误返回-1;

有了这些接口我们接下来就可以用代码来证明我们的信号的保存了:

实践证明信号的保存

我们写下面这份代码

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

void handler(int signum)
{
     cout<<getpid()<<": catch signal "<<signum<<endl;
}

void handler1()
{
    sigset_t pending;
    sigpending(&pending);
    for (int i = 31; i > 0; i--)
    {
        int ans = sigismember(&pending, i);
        cout << ans;
    }
    cout << endl;
    sleep(1);
}

int main()
{
    sigset_t set;
    sigemptyset(&set);           // 初始化将要替换block的set
    // for (int i = 31; i > 0; i--) // 打印一下初始化的set
    // {
    //     int ans = sigismember(&set, i);
    //     cout << ans;
    // }
    // cout << endl;
    for (int signum = 1; signum <= 31; signum++)
    {
        // signal(signum,handler);//将1到31号信号全部捕捉发现9号信号无法被捕捉

        sigaddset(&set, signum);     // 添加信号进入set
        // for (int i = 31; i > 0; i--) // 打印一下修改的set
        // {
        //     int ans = sigismember(&set, i);
        //     cout << ans;
        // }
        // cout << endl;
        sigprocmask(SIG_SETMASK, &set, nullptr); // 成功将信号屏蔽
    }

    while (true) // 让进程死循环防止退出
    {
        handler1();
    }

    return 0;
}

我们首先调用signal捕捉1到31全部的信号我们然后再运行这个bash脚本,对此进程发送1到31的信号;

i=1
id=$(pidof block)
while [ $i -le 31 ]
do
    if [ $i -eq 9 -o $i -eq 19 ]
    then
        let i++
        continue
    fi
    kill -$i $id
    echo "kill -$i $id"
    let i++
    sleep 1
done

我们可以得出这样的效果:

 

 

 同理:如果捕捉所有信号,进程也无法被终止,除9与19信号;9与19无法被捕捉和屏蔽

上面就是信号的保存内容; 

信号处理方式

 默认处理:

忽略:

信号抵达后(被os读取之后存储在某个地方),os暂时先不处理,os先处理其他任务;

自定义行为:

这就是我们上面进行了那么多次使用signal函数进行的catch捕捉信号;然后自定义函数,回调函数给signal,进行我们定义函数的行为; 

信号处理的时间

 我们知道了这些处理方式,我们下面详细的讲解一下它们;

我们知道信号要被处理,需要在合适的时候;那究竟什么时候是合适的时候呢?

当cpu执行状态从内核态返回用户态的时候;

那什么是用户态和内核态呢?我们先看这张图片:

我们知道了代码在内存中的结构,接下来听我好好分析;

我们进程中用户自己编写的代码数据被加载到内存中,而进程中的内核部分会被统一加载到操作系统进程中,这是为了方便操作系统进行管理;操作系统其实也是个进程的,cpu一次只能处理一条指令,那么它加载的进程也肯定只有一个;所以通过每个进程都拥有内核区,加载了操作系统的功能,那么每个进程都能被操作系统所管理;所以此时我们代码的执行会成为这个样子:

信号处理的过程

 

补充:

内核态与用户态的转换

1.自定义方法因为是我们用户所编写的所以一定会回到我们的内核态执行(内核态虽然能执行用户代码,但为了安全不会在内核态中执行),如果是在内核态执行我们的自定义方法,恶意方法中有指针会获取我们内核数据从而产生危害等行为;所以操作系统十分严格,用户的代码只能在用户态执行,那怎么我们是用户态还是内核态呢?

2.cpu中有一个不可见的寄存器CR3,这个寄存器中使用2个bit位来标识当前代码执行所处的状态,如果状态位内核态即可访问操作系统中的数据,如果为用户态及只能访问自己当前的进程空间;

 3.由此我们也可以清楚的知道内核其实也是在所有进程的上下文数据中运行的

信号一次只能处理一个

我们的信号在被处理时会屏蔽信号表设置屏蔽字,使得操作系统无法检查到其他信号,避免其他信号对当前信号处理的影响;

用下面这段代码证明:

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

// 证明一个信号在被执行的时候它的信号集会被屏蔽接os无法去处理任何其他信号
void showpending()
{
    sigset_t pending;
    sigpending(&pending);
    cout << "信号集: ";
    for (int i = 31; i > 0; i--)
    {
        int ans = sigismember(&pending, i);
        cout << ans;
    }
    cout << endl;
}

void handler(int signum)
{
    cout << getpid() << ": catch signal " << signum << endl;
    for (int i = 0; i < 5; i++)
    {
        showpending();
        sleep(1);
    }
}

int main()
{
    sigset_t set;
    signal(2, handler);
    showpending();
    while (true)
    {
    }
    return 0;
}

 现象:

 

sigaction

它的第一个参数是用来传递信号的号码的;

第二个参数是一个结构体,用来设置这个信号的相关内容:

我们可以通过传递一个被我们自己改写好的sigaction对象来对signum信号内容进行处理;

 第三个参数和sigprocmask的第三个参数一样都是输出型参数,用来输出我们旧的sigaction对象;

下面的内容是对线程的铺垫: 

可重入函数

当发生这样的场景时:

上面的场景代表我们的函数不能多次重复进入,叫做不可重入函数;当某个函数被调用还没完全执行完成的时候,就被中断,调用其他函数时这就叫做重入;

只有当函数中的变量是局部的,不是在堆区这样的所有函数都可以访问的空间时,这样的函数才可以被重入,才不会发生混乱;

一般数据在堆区或者全局变量这样的情况是不可重入的;

 

volatile

在我们编写好代码后,由于效率问题,编译器一定会对我们的某些操作进行优化,而这些优化可能会影响我们原有的逻辑思维导致,出现错误,这类错误的发现也是最难的;就像我们c++之前将的拷贝构造函数的优化,会省略几步拷贝;这样的就是编译器的优化,不同编译器优化程度不同,编译器也可以自动的设置优化程度,而有时候我们为了避免编译器的优化对我们的代码逻辑产生影响就可以使用这个volatile关键字,告诉编译器不要优化这个数据的处理,保持内存可见性;

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

int count=0;

void handler(int signum)
{
    cout<<getpid()<<": catch signal "<<signum<<" change count->1"<<endl;
    count=1;
}

int main()
{
    signal(2,handler);
    while(count==0){}
    cout<<"成功退出"<<endl;
    return 0;
}

上面的逻辑是正常的;但是当我们提高编译优化级别:

g++加上 -o2选项;

优化级别:

gdp:

在 GCC(GNU Compiler Collection)中,除了 `-O2` 之外,还有更高级别的优化选项。这些选项用于告诉编译器在生成目标代码时进行更深层次的优化,以提高程序的性能或者减小生成的代码的大小。下面是一些常见的优化级别:

1. **-O1**:基本优化级别,会进行一些基本的优化,例如去除未使用的代码、简化表达式等。

2. **-O2**:更高级别的优化,会进行更多的优化,例如循环展开、函数内联等。这是默认的优化级别。

3. **-O3**:最高级别的优化,会进行更加激进的优化,例如向量化、更深层次的循环优化等。但是有时候 `-O3` 会导致编译时间增加,并且可能会引入一些不可预测的行为。

4. **-Os**:优化生成的代码大小。这个选项会尝试减小生成的目标代码的大小,以牺牲一些性能为代价。

5. **-Ofast**:在 `-O3` 的基础上进一步启用一些不严格的优化,例如允许忽略 IEEE 浮点数标准,以提高性能。但是由于牺牲了一些精度和安全性,使用 `-Ofast` 需要谨慎。

6. **-Og**:用于开发和调试阶段的优化级别,会生成容易调试的目标代码,同时保留大部分的优化。

这些优化级别可以根据具体的需求进行选择,通常在进行性能测试和调试时会尝试不同的优化级别来找到最优的性能和代码大小折中。

优化后:

原因: 

 

 将全局数据count增加啊volatile修饰,保持内存可见性;

volatile int count=0

 

SIGCHLD信号

这个信号一般出现在子进程运行结束后会向父进程发送这个信号告诉父进程,我们当前的进程运行完毕了;

我们可以使用这个特点来回收子进程:

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

void handler(int signum)
{
    pid_t pid;
    while ((pid = waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout <<"wait success : "<<pid << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    // signal(17,handler);
    for (int i = 0; i < 5; i++) // 创建5个进程
    {
        pid_t pid = fork();
        if (pid == 0)
        {
            sleep(10);
            exit(0);
        }
    }
    for (int i = 0; i < 12; i++)
    {
        cout << "父进程正在运行 " << i << endl;
        sleep(1);
    }
    return 0;
}

回收成功: 

还有一种linux特有的方式但在其他unix操作系统下不完全适用的方式直接:

signal(17,SIG_IGN) 

直接使用此代码,这个忽略信号在被显示调用的时候会使得增加回收僵尸进程的功能;而相较于默认的忽略多了回收的功能;此方法为linux特有的方式但在其他unix操作系统下不完全适用;

以上就是本篇的全部内容; 

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

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

相关文章

qt学习篇---界面按键关联(信号和槽)

目录 1.qt基础 2.做一个界面 创建project UI界面设计 信号和槽 1.控件改名字 2.什么是信号和槽 3.怎么关联信号和槽 自动关联 手动关联 1.qt基础 qt可移植性强&#xff0c;不久会用到MCU。很有意义学习 2.做一个界面 创建project 不要中文路径 选择QWidget .pro文件…

字符串函数与字符函数运用(1)

字符串与字符函数介绍1 前言一、字符分类函数字符函数练习 二、字符函数转换1.引入库2.代码改进 字符串函数strlen函数strcpy 结尾 前言 字符串函数大概有以下这几种 strcpy、strcat 、strcmp、strncpy、strncat、strncmp、strstr、strtok、strerror 这些函数可以很好的解决你…

数据结构的队列(c语言版)

一.队列的概念 1.队列的定义 队列是一种常见的数据结构&#xff0c;它遵循先进先出的原则。类似于现实生活中排队的场景&#xff0c;最先进入队列的元素首先被处理&#xff0c;而最后进入队列的元素则要等到前面的元素都被处理完后才能被处理。 在队列中&#xff0c;元素只能…

什么,你的EasyExcel导出一万条数据就OOM了?

前言 前段时间在做一个导出的功能&#xff0c;本以为是平平无奇的一个功能。就用公司内部的一个导出工具类三下五除二就写完了&#xff0c;做法是直接查全量数据&#xff0c;然后直接往Excel里写。一开始没多少数据也没什么问题&#xff0c;但是当数据量逐渐多了起来后&#x…

【OpenCV • c++】图像平滑处理(2) —— 方框滤波 | 盒滤波 | 源码分析

文章目录 前言一、方框滤波代码演示 二、源码分析 前言 前文我们了解了什么是图像平滑处理、图像滤波、邻域算子与线性邻域滤波、以及如何使用方框滤波&#xff0c;本文我们来分析一下方框滤波的源码。 一、方框滤波 void boxFilter(InputArray src, OutputArray dst, int ddep…

面试常见 | 项目上没有亮点,如何包装?

很多技术人在公司用的老技术&#xff0c;而且很多都是搬业务代码且做枯燥乏味的CRUD&#xff0c;在面试提交简历或做自我介绍的时候并不突出&#xff0c;这种情况&#xff0c;如何破局&#xff1f; 首先不管你做的啥项目&#xff0c;全世界不可能只有你自己在做&#xff0c;比…

Android Handler用法

Android Handler用法 为什么要设计Handler机制&#xff1f;Handler的用法1、创建Handler2、Handler通信2.1 sendMessage 方式2.2 post 方式 Handler常用方法1、延时执行2、周期执行 HandlerThread用法主线程-创建Handler子线程-创建Handler FAQMessage是如何创建主线程中Looper…

Agent AI智能体的未来

未来社会中的智能使者&#xff1a;Agent AI智能体的可能性与挑战 随着科技的迅速进步&#xff0c;人工智能已深入我们生活的各个领域&#xff0c;而Agent AI智能体作为与人工智能紧密相关的一个分支&#xff0c;其未来发展无疑是值得期待的。Agent AI智能体&#xff0c;或称为…

JAVA Coding 规范

Coding 规范 文章目录 Coding 规范一.文件规范1.1 声明1.2 缩进1.3 空行1.4 空格1.5 对齐1.6 小括号1.7 花括号1.8 代码长度 二.命名规范2.1 命名总则2.2 命名空间2.3 类与接口2.4 方法命名2.5 属性命名2.6 常量命名2.7 变量命名 三.语句规范3.1 语句总则3.2 循环语句3.3 Switc…

【note3】linux驱动基础,

文章目录 1.互斥锁和自旋锁选择&#xff1a;自旋锁&#xff08;开销少&#xff09;的自旋时间和被锁住的代码执行时间成正比关系2.linux错误码&#xff1a;64位错误指针指向内核空间最后一页&#xff0c;对于 1.互斥锁和自旋锁选择&#xff1a;自旋锁&#xff08;开销少&#x…

QT:核心控件-QWidget

文章目录 控件enableobjectNamegeometrysetWindowTitleopacitycursorFonttooltipstyleSheet 控件 什么是控件&#xff1f; 如上所示&#xff0c;就是控件&#xff0c;而本篇要做的就是对于这些控件挑选一些比较有用的常用的进行讲解分析 在QT的右侧&#xff0c;会有对应的空间…

【面试经典 150 | 分治】将有序数组转换为二叉搜索树

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;中序遍历递归建树 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等…

如何有效地合并和分类多个文件目录?

在现代办公环境中&#xff0c;文件管理和组织是至关重要的。随着科技的发展&#xff0c;我们不再仅仅依赖于纸质文件&#xff0c;而是更多地使用电子设备来存储和管理信息。这种转变带来了一些新的挑战&#xff0c;其中之一就是如何有效地合并和分类多个目录文件。一&#xff0…

Sortable 拖拽行实现el-table表格顺序号完整例子,vue 实现表格拖拽行顺序号完整例子

npm install sortable<template><vxe-modalref"modalRef"v-model"showModal"title"详情"width"70vw"height"60vh"class"his"transfer><el-table ref"tableRef" :data"tableData&q…

树莓派5用docker运行Ollama3

书接上回&#xff0c;树莓派5使用1panel安装 Ollama 点击终端就可以进入容器 输入以下代码 ollama run llama3Llama3 是市场推崇的版本。您的 树莓派5上必须至少有 4.7GB 的可用空间&#xff0c;因此用树莓派玩机器学习就必须配置大容量的固态硬盘。用1panel部署网络下载速度…

4G远程温湿度传感器在农业中的应用—福建蜂窝物联网科技有限公司

解决方案 农业四情监测预警解决方案 农业四情指的是田间的虫情、作物的苗情、气候的灾情和土壤墒情。“四情”监测预警系统的组成包括管式土壤墒情监测站、虫情测报灯、气象站、农情监测摄像机&#xff0c;可实时监测基地状况,可以提高监测的效率和准确性&#xff0c;为农业生…

“Unite“ > MacOS下很不错的网站转应用App的工具

前言 前不久在浏览mac论坛&#xff0c;无意了解到一款非常好的工具&#xff0c;可以将网站转换为app&#xff0c;考虑到我们现在的主要应用都从本地客户端转成web形式使用&#xff0c;但基于本能的使用习惯&#xff0c;还是希望有个快捷的访问信息&#xff0c;这个应用非常适合…

202012青少年软件编程(Python)等级考试试卷(一级)

第 1 题 【单选题】 运行下方代码段&#xff0c;输出是6&#xff0c;则输入的可能是&#xff08; &#xff09;。 a eval(input())print(a)A :8%2 B :8/2 C :3*2 D :3**2 正确答案:C 试题解析: 第 2 题 【单选题】 关于Python变量&#xff0c;下列叙述正确的是&#x…

Offer必备算法33_DFS爆搜深搜回溯剪枝_八道力扣题详解(由易到难)

目录 ①力扣784. 字母大小写全排列 解析代码1_path是全局变量 解析代码2_path是函数参数 ②力扣526. 优美的排列 解析代码 ③力扣51. N 皇后 解析代码 ④力扣36. 有效的数独 解析代码 ⑤力扣37. 解数独 解析代码 ⑥力扣79. 单词搜索 解析代码 ⑦力扣1219. 黄金矿…

[嵌入式系统-58]:RT-Thread-内核:线程间通信,邮箱mailbox、消息队列MsgQueue、信号Signal

目录 线程间通信 1. 邮箱 1.1 邮箱的工作机制 1.2 邮箱控制块 1.3 邮箱的管理方式 &#xff08;1&#xff09;创建和删除邮箱 &#xff08;2&#xff09;初始化和脱离邮箱 &#xff08;3&#xff09;发送邮件 &#xff08;4&#xff09;等待方式发送邮件 &#xff08…