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

什么是信号

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

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

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

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操作系统下不完全适用;

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

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

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

相关文章

基于Java EE平台项目管理系统的设计与实现(论文 + 源码)

【免费】基于javaEE平台的项目管理系统.zip资源-CSDN文库https://download.csdn.net/download/JW_559/89267688 基于Java EE平台项目管理系统的设计与实现 摘 要 随着社会信息化的发展&#xff0c;很多的社会管理问题也一并出现了根本性变化&#xff0c;项目公司的报表及文…

【YOLO】目标检测 YOLO框架之train.py参数含义及配置总结手册(全)

1.一直以来想写下基于YOLO开源框架的系列文章&#xff0c;该框架也是日常项目开发中常用的一款工具&#xff0c;最近刚好挤时间梳理、总结下这块儿的知识体系。 2.熟悉、梳理、总结下YOLO目标检测相关知识体系&#xff0c;之前实战配置时总是临时性检索些注释含义&#xff0c;但…

JVM组成之类加载器

类加载器&#xff08;ClassLoader&#xff09;&#xff1a;是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。 类加载器多数是有Java编写的&#xff0c;也有部分是c编写的&#xff0c;负责接收来自外部的二进制数据&#xff0c;然后执行JNI&#xff08;也就是本…

【Java】山外有山,类外还有类

【Java】山外有山&#xff0c;类外还有类 内部类是Java语言中的一种特性&#xff0c;它允许在另一个类中定义一个类。 内部类可以是静态的&#xff08;不依赖于外部类的实例&#xff09;&#xff0c;也可以是非静态的&#xff08;依赖于外部类的实例&#xff09;。 在本篇博…

在R的 RGui中,使用devtools 安装trajeR

创建于&#xff1a;2024.5.5 文章目录 1. 报错信息2. 尝试使用指定的清华镜像&#xff0c;没有解决3. 找到原因&#xff1a;官网把包删除了4. 尝试从网上下载&#xff0c;然后安装。没有成功5. 使用devtools安装5.1 尝试直接安装&#xff1a;install.packages("devtools&q…

【智能算法应用】混合粒子群算法求解CVRP问题

目录 1.算法原理2.数学模型3.结果展示4.参考文献5.代码获取 1.算法原理 【智能算法】粒子群算法&#xff08;PSO&#xff09;原理及实现 经典PSO算法用于连续空间优化问题&#xff0c;VRP问题为离散组合优化问题&#xff0c;涉及如何有效地分配一组车辆去访问多个客户点&…

OSEK的设计哲学与架构

1 前言 OSEK是为单核分布式嵌入式控制单元量身定制的实时系统&#xff0c;对事件驱动&#xff08;event driven&#xff09;的硬实时控制系统具有良好的适配性。OSEK没有强求不同软件模块间的完全兼容性&#xff0c;而是将重心放到了软件的可移植性上来。简单来说&#xff0c;与…

[报错解决]Communications link failure

报错 主机IDEA项目连接虚拟机的数据库报错。 主要报错信息有&#xff1a; com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received a…

智慧旅游引领未来风尚,科技助力旅行更精彩:科技的力量推动旅游业创新发展,为旅行者带来更加便捷、高效和智能的旅行服务

目录 一、引言 二、智慧旅游的概念与特点 &#xff08;一&#xff09;智慧旅游的概念 &#xff08;二&#xff09;智慧旅游的特点 三、科技推动旅游业创新发展 &#xff08;一&#xff09;大数据技术的应用 &#xff08;二&#xff09;人工智能技术的应用 &#xff08;…

Linux Ubuntu 开机自启动浏览器

终端输入命令&#xff1a;gnome-session-properties 打开启动设置 如果提示&#xff1a;Command ‘gnome-session-properties’ not found, but can be installed with: apt install gnome-startup-applications 则执行&#xff1a;apt install gnome-startup-applications安装…

一、写给Android开发者之harmony入门

一、创建新项目 对比 android-studio&#xff1a;ability类似安卓activity ability分为两种类型(Stage模型) UIAbility和Extensionability&#xff08;提供系统服务和后台任务&#xff09; 启动模式 1、 singleton启动模式&#xff1a;单例 2、 multiton启动模式&#xff1…

数据结构十:哈希表

本次将从概念上理解什么是哈希表&#xff0c;理论知识较多&#xff0c;满满干货&#xff0c;这也是面试笔试的一个重点区域。 目录 一、什么是哈希表 1.0 为什么会有哈希表&#xff1f; 1.1 哈希表的基本概念 1.2 基本思想 1.3 举例理解 1.4 存在的问题 1.5 总结 二、…

libcity笔记:参数设置与参数优先级

1 参数优先级 高优先级的参数会覆盖低优先级的同名参数 Libcity中的优先级顺序维&#xff1a; 命令行参数&#xff08;命令行python run_model.py时导入的&#xff09; > 用户定义配置文件&#xff08;命令行python run_model.py时由config_file导入的&#xff09; >…

javascript 练习 写一个简单 另类录入 电脑组装报价表 可打印

数据格式 &#xff08;1代表cpu、2代表主板、3代表内存、。。。&#xff09; 1i3 12100 630 2H610 480 3DDR4 3200 16G 220 4500G M.2 299 5300W电源 150 6小机箱 85 7GT 730G 4G 350 8WD 2T 399 9飞利浦 24Led 580 主代码 Html JS <!DOCTYPE html> <html lang&qu…

02_Java综述

目录 面向对象编程两种范式抽象OOP 三原则封装继承多态多态、封装与继承协同工作 面向对象编程 面向对象编程(Object-Oriented Programming&#xff0c;OOP)在Java中核心地位。几乎所有的Java程序至少在某种程度上都是面向对象的。OOP与java是密不可分的。下面说一下OOP的理论…

SSM+Vue酒店管理系统

SSMVue酒店管理系统&#xff0c;JavaWeb酒店管理系统&#xff0c;项目由maven工具管理依赖&#xff0c;数据库Mysql&#xff0c;一共19张表&#xff0c;前端用Vue写的管理端&#xff0c;功能丰富&#xff0c;需要可在最后位置联系我&#xff0c;可加购调试&#xff0c;讲解&…

自注意力架构大成者_Transformer(Pytorch 17)

1 模型简介 在上节比较了 卷积神经网络&#xff08;CNN&#xff09;、循环神经网络&#xff08;RNN&#xff09;和 自注意力&#xff08;self‐attention&#xff09;。值得注意的是&#xff0c; 自注意力同时具有并行计算和最短的最大路径长度这两个优势。因此&#xff0c;使…

Llama3本地部署与高效微调入门

前言 为了保持公司在AI&#xff08;人工智能&#xff09;开源大模型领域的地位&#xff0c;社交巨头Meta推出了旗下最新开源模型。当地时间4月18日&#xff0c;Meta在官网上宣布公布了旗下最新大模型Llama 3。目前&#xff0c;Llama 3已经开放了80亿&#xff08;8B&#xff09…

8086 汇编学习 Part 9

端口的读写 CPU 的邻居 CPU 内部的寄存器内存单元端口&#xff08;各种接口卡、网卡&#xff0c;显卡&#xff0c;主板上的接口芯片等&#xff09; 各种芯片工作时&#xff0c;都有一些寄存器由 CPU 读写从 CPU 角度&#xff0c;将各寄存器当端口&#xff0c;并统一编制CPU …

开源im即时通讯app源码系统/php即时聊天im源码/php+uniapp框架【终身使用】

摘要 随着开源文化的蓬勃发展&#xff0c;即时通讯(IM)系统作为现代通信不可或缺的一部分&#xff0c;其开源实现正变得越来越普遍。本文将深入探讨基于PHP的全开源即时通讯源码系统&#xff0c;并结合UniApp开源框架&#xff0c;从理论基础到代码实现&#xff0c;再到实际应用…