linux(信号量)

news2024/11/18 7:36:08

信号量

    • 几个基本概念
      • 临界资源
      • 临界区
      • 原子性
      • 互斥
    • 信号量
      • 后台进程
      • 前台进程
    • 信号
      • 储存信号
      • 处理信号(信号捕捉)
    • 发送信号
      • 1、键盘产生:
      • 2、系统调用接口发送信号
      • 3、由软件条件产生信号
      • 4、硬件异常发送信号
    • 内核中的信号量
      • **信号量在内核中的数据结构**
      • **信号集操作函数**
    • 信号的检查和处理
    • 可重入函数
    • volatile关键字

几个基本概念

临界资源

临界资源:被多个进程能够看到的资源

如果没有对临界资源进行任何保护,对于临界资源的访问,双方进程在进行访问的时候,就都是乱序的,可能会因为读写交叉而导致的各种乱码、废弃数据、访问控制访问的问题

临界区

临界区:对多个进程而言,访问临界资源的代码

我们写的进程的代码中,有大量的代码,只有一部分代码,会访问临界资源

原子性

原子性:一件事情要么不做,要么做完了,没有中间状态

互斥

互斥:任何时刻,只允许一个进程,访问临界资源

信号量

信号量的本质就是计数器,且这个计数器的操作是原子性

信号量对应的操作:

​ 申请资源:P操作

​ 释放资源:V操作

共享内存不具有访问控制,但可以通过信号量进行对资源的保护

共享内存

shmget	//创建
shmctl	//删除
shmat	//关联
shmdt	//去关联

消息队列

msgget
msgctl
msgsnd
msgrcv

信号量

semget
semctl
semop	+1 P	//申请资源
semop	-1 V	//释放资源

查看

ipcs -m/-q/-s	//共享内存/消息队列/信号量

删除

ipcrm -m/-q/-s	//共享内存/消息队列/信号量

共享内存、消息队列、信号量的生命周期都是随内核(操作系统)的

管道文件的生命周期都是随进程的

对于进程来讲,即便信号还没有产生,进程已经具有识别和处理这个信号的能力了。

后台进程

./myproc &

后台进程运行时,可以使用bash进程,后台进程不能使用ctrl+c终止,前台进程可以使用ctrl+c终止

jobs			//查看后台进程

fg 作业号	  	  //把后台进程提到前台

动画

前台进程

./myproc //前台任务,运行时不能使用bash进程

kill -l			//查看信号

man 7 'singal' 	//查看信号详细信息

image-20230109210753827

其中131为普通信号,3464为实时信号

可以同时运行一个前台进程和若干个后台进程

信号

因为信号产生是异步的(信号随时都有可能产生),当信号产生的时候,对应的进程可能正在做更重要的事情,我们进程可以暂时不处理这个信号,进程暂时不处理信号,就需要先将信号先储存起来

储存信号

那么信号如何储存?

使用位图记录信号量(在进程的task_struct中)

1、有没有产生【比特位的内容1/0】

2、什么信号产生【比特位的位置】

我们要对信号进行存储就需要对进程的task_struct中记录信号量的位图进行修改,而task_struct是在内核空间中的,那么只有os可以对task_struct做修改,无论信号如何产生,都是os帮我们进行设置的

处理信号(信号捕捉)

处理信号有三种动作

1、默认动作

2、忽略

3、自定义动作

sighandler_t signal(int signum, sighandler_t handler);	//设置信号的自定义方法
void handler(int signum)
{
  cout<<"捕捉到"<<signum<<"号信号"<<endl;

}
int main()
{
  signal(SIGINT,handler);
  while(1)
  {
    cout<<"hello linux"<<endl;
    sleep(1);
  }
  return 0;
}

image-20230110103435953

ctrl+c就是给前台进程发送2号信号(终止自己)

注意:

1、无法对9号和19号信号设置自定义动作,忽略,阻塞

2、6号信号虽然可以设置自定义动作,但执行完自定义动作后依旧会执行默认动作

发送信号

用户层产生信号方式

1、键盘产生:

ctrl+c:发送2号信号

ctrl+\:发送3号信号

我们在之前说过,无论信号如何产生,都是由os来发送的,本质上发送信号就是修改task_struct中的位图

2、系统调用接口发送信号

int kill(pid_t pid,int sig)	//给任意进程发送任意信号
void handler(int signum)
{
cout<<"捕捉到"<<signum<<"号信号"<<endl;
exit(1);
}
int main()
{
signal(SIGINT,handler);
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送2号信号,进程退出"<<endl;
sleep(3);
kill(getpid(),SIGINT);		//给当前进程发送2号信号
return 0;
}

动画

int raise(int sig)		//给自己发送任意信号
void handler(int signum)
{
cout<<"捕捉到"<<signum<<"号信号"<<endl;
exit(1);
}
int main()
{
signal(SIGINT,handler);
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送2号信号,进程退出"<<endl;
sleep(3);
raise(SIGINT);		//给当前进程发送2号信号
return 0;
}
void abort(void)		//向自己发送SIGABRT信号(终止进程)
void handler(int signum)
{
cout<<"捕捉到"<<signum<<"号信号"<<endl;
exit(1);
}
int main()
{
signal(SIGABRT,handler);
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送6号信号,进程退出"<<endl;
sleep(3);
abort();
return 0;
}

动画

3、由软件条件产生信号

unsigned int alarm(unsigned int seconds);
//调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号 
//该信号的默认处理动作是终止当前进程
int main()
{
cout<<"进程运行中 pid:"<<getpid()<<endl;
cout<<"等待3秒后,发送14号信号,进程退出"<<endl;
alarm(3);
sleep(1000);
return 0;
}

动画

4、硬件异常发送信号

首先我们需要知道进程崩溃的本质就是该进程收到了异常信号

一般情况,导致进程崩溃主要是除零错误越界野指针问题

①除零错误

void handler(int signum)
{
    cout<<"捕获到"<<signum<<"号信号"<<endl;
    exit(1);
}

int main()
{
    for(int i=1;i<32;i++)
    {
        signal(i,handler);
    }
    int num=10/0;
    return 0;
}

image-20230111205724168

②越界野指针问题

野指针

void handler(int signum)
{
    cout<<"捕获到"<<signum<<"号信号"<<endl;
    exit(1);
}
int main()
{
    for(int i=1;i<32;i++)
    {
        signal(i,handler);
    }
    int* num=nullptr;
    *num=1000;
    return 0;
}

image-20230111214127014

越界

void handler(int signum)
{
    cout<<"捕获到"<<signum<<"号信号"<<endl;
    exit(1);
}
int main()
{
    for(int i=1;i<32;i++)
    {
        signal(i,handler);
    }
    int arr[10];
    for(int i=10;i<10000;i++)
    {
        arr[i]=100;
        cout<<i<<endl;
    }
    return 0;
}

image-20230111213843822

**注意:**我们的进程发生崩溃退出,是因为操作系统给进程发信号,进程合适的时候对于这个信号做出默认动作,终止进程,如果我们对信号设置(不终止进程的)自定义动作,这个进程就不会终止

那么进程发生崩溃时,是如何收到异常信号的?

①除零

在计算机中,运算都是在CPU中进行的,在CPU的内部有一个状态寄存器(硬件),这个状态寄存器的作用是检查计算是否出错

CPU进行计算时,发生除零错误,CPU内部的状态寄存器就会被设置为:有报错,浮点数错误

OS就会根据这个状态寄存器得知CPU内有报错,OS就会构建信号,并把这个信号发送给出错的这个进程,进程会在合适的时候处理这个信号,终止进程

②越界&&野指针

我们在语言层面使用的地址(指针)都是虚拟地址,我们使用的地址都是通过虚拟地址经过页表映射到物理地址,再通过物理地址找到物理内存,再读取对应的数据和代码的

虚拟地址转换到物理地址的工作是由(MMU(硬件)+页表(软件))来完成的,如果虚拟地址有问题,地址转化过程就会引起问题,表现在硬件MMU上,OS发现硬件出现问题,OS会构建信号,向出错的进程发送信号,目标进程会在合适的时候处理该信号,终止进程

补充:core_dump

某些信号的默认动作是Core,这些信号基本都是因为代码出现的问题导致的

image-20230112162957957

Core动作会将core_dump置为1,会产生一个大文件core.进程pid

那么这个core_dump 在在哪里,什么作用?

在父进程等待子进程时, waitpid(pid_t pid, int *status, int options),status是一个输出型参数,从子进程的pcb中获取,我们只取低16位,其中次低8位为子进程退出码,低七位为终止信号,剩下1位为core_dump

core_dump会把进程在运行中,对应的异常上下文数据,core_dump到磁盘上,方便调试

image-20230112164245870

在云服务器上,默认把core file size设置为0,无法生成core文件,我们需要ulimit命令修改core file size

image-20230112191036360

代码

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        cout<<"子进程 pid"<<getpid()<<endl;
        int* num=nullptr;
        *num=1000;
        cout<<"子进程 pid"<<getpid()<<endl;
        exit(1);
    }
    else
    {
        int status=0;
        waitpid(id,&status,0);
        cout<<"子进程退出码:"<<((status>>8)&0xFF)
        <<"终止信号:"<<(status&0X7F)
        <<"core_dump:"<<((status>>7)&0x1)<<endl;
    }
    return 0;
}

image-20230112192712660

使用core文件进行调试:

image-20230112193644337

上图中,红线中的调试信息,11号信号终止进程,段错误错误定位在第14行,*num=1000;

内核中的信号量

信号量在内核中的数据结构

信号存储在进程的task_struct中,task_struct中有三个表,block表(阻塞信号集),pending表(未决信号集),handler表,

其中pending表就是发送信号给进程,存储信号的位图

block表也是一个位图,这个位图上表示的是哪些信号被阻塞,信号被阻塞表示进程依旧可以收到这些信号,但是不会递达(处理)这些信号

hanlder表是一个函数指针数组,处理信号使用信号编号为数组下标(对应的处理方法默认动作或自定义方法或忽略)

image-20230112200322400

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号

    产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler

  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,也就是如果解除该信号的阻塞,只会处理一次该信号

sigset_t

专门为信号量设计的类型

信号集操作函数

虽然block表和pending表都是位图,但是不同系统的实现不同,位图的内部实现可能数组,所以不能直接使用位操作,需要使用特定的信号集操作函数

int sigemptyset(sigset_t *set);
//函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
int sigfillset(sigset_t *set);
//函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示 该信号集的有效信号包括系统支持的所有信号
int sigaddset(sigset_t *set,int signo);
//向set所指向的信号集添加signo对应的信号
int sigdelset(sigset_t *set,int signo);
//向set所指向的信号集删除signo对应的信号
int sigismember(const sigset_t *set,int signo);
//检查set所指向的信号集,是否包含signo对应的信号
  • 注意,在使用sigset_t类型的变量之前,一定要调用sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号
  • 前四个函数都是成功返回0,出错返回-1;sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

sigprocmask

int sigprocmask(int how, const sigset_t *restrict set,sigset_t *restrict oset);
//调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
//若成功返回0,失败返回1
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。

  • 如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。

  • 如果osetset都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据sethow参数更改信号屏蔽字。

  • how的取值

    SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字

    SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号

    STG_SETMASK:设置当前信号屏蔽字为set所指向的值

  • 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号抵达

sigpending

int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

代码

void printpending(sigset_t* p)
{
    for(int i=1;i<32;i++)
    {
        int ret = sigismember(p,i);
        if(ret==1)
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}
int main()
{
    sigset_t s,p;
    sigemptyset(&s);
    sigaddset(&s,SIGINT);
    sigprocmask(SIG_BLOCK,&s,NULL);
    while(1)
    {
        sigpending(&p);
        printpending(&p);
        sleep(1);
    }
    return 0;
}

动画

信号的检查和处理

在前面,都在说进程会在合适的时候处理信号,那么什么时候是合适的时候?

合适的时候,就是进程从内核态切换到用户态时会对信号做检测和处理

我们来讲一下用户态和内核态

在进程的虚拟地址空间0~3G为用户空间,3~4G为内核空间,所有进程的内核空间都相同,所有进程使用同一个内核空间,当我们的进程访问内核空间时,进程处于内核态,进程访问用户空间时,进程处于用户态。

那么进程如何访问内核空间

我们知道进程通过页表进行虚拟地址和物理地址的转换,访问用户代码和数据,那这个页表叫做用户级页表,用户级页表不能访问内核空间,除了用户级页表还有一个内核级页表,内核具有访问所有空间的权限(内核级空间和用户级空间),

用户态切换到内核态切换的时机

当进程的时间片到了,需要进行进程间切换时,进程会切换到内核态,执行进程调度算法,等执行完内核的代码,会切换回用户态

进行系统调用,

进行信号的检查和处理

在进程由于某些原因(进程切换,系统调用)切换到内核态后,再切换回用户态时,回进行信号的检测和处理

当进程在内核态执行完内核的代码(比如:系统调用等),准备返回,要从内核态切换回用户态时,会检查和处理信号,

下图中,会查看pending表中为1的信号量是否被阻塞,如果没被阻塞,执行handler表中的动作

  • 如果handler表中信号量编号下标对应的动作是SIG_DFL(默认动作),当前进程正处于内核态,执行默认动作
  • 如果handler表中信号量编号下标对应的动作是SIG_IGN(忽略动作),当前进程正处于内核态,直接把pending表中对应的信号量置为0
  • 如果handler表中信号量编号下标对应的动作是自定义动作(函数指针),进程就会从内核态切换到用户态执行用户代码,执行完用户的自定义动作代码后,再从用户态切换回内核态,在内核态中,再使用特定的系统调用返回,切换到用户态。再执行接下来的用户代码。
  • 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

image-20230112200322400

sigaction

int sigaction(int sig, const struct sigaction *restrict act,struct sigaction *restrict oact);
//sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1
  • signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。actoact指向sigaction结构体:

可重入函数

一个链表头插节点函数,同时设置了一个自定义动作函数,也是链表头插

node node1,node2;
node* head;
void insert(node* p)
{
 p->next = head;
 head = p;
}
int sighandler(int signo)
{
 insert(&node2);
}
int main()
{
 insert(&node1);
 …………
}

如果在主函数中进行链表头插时,当p->next = head;这句代码后,如果该进程的时间片到了,要进行进程间切换,由用户态切换到内核态,执行调度算法,执行完调度算法后,需要从内核态切换回用户态,这时会进行信号检测和处理,如果该进程收到信号,那么这时执行进程的自定义动作方法,进行链表头插节点,就会出现下图这种情况,导致node2丢失,内存泄漏

image-20230118105504914

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称
为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,
如果一个函数只访问自己的局部变量或参数,则称为可重入函数

如果一个函数符合以下条件之一则是不可重入的

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

volatile关键字

一个死循环

int flag=1;

void handler(int signo)
{
    flag=0;
}

int main()
{
    signal(SIGINT,handler);
    while(flag)
    {
        cout<<"hello world"<<endl;
        sleep(1);
    }
    return 0;
}

flag该为0,死循环停止

image-20230118194047707

使用GCC优化选项-O2cpu就不会从内存中取值,一直使用CPU中的flag值循环不会结束

使用volatile保持内存的可见性,死循环可以退出

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

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

相关文章

YOLOv7 Falsk Web 监测平台 | YOLOv7 Falsk Web 部署

YOLOv7 Falsk Web 监测平台图片效果展示 YOLOv7 Falsk Web 监测平台视频效果展示 YOLOv7 Flask Web 检测平台 什么是Flask? 简介 Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合MVC模式进行开发,开…

电子技术——MOS管的小信号模型

电子技术——MOS管的小信号模型 在上一节&#xff0c;我们已经学习过了MOS管的基本线性放大原理&#xff0c;本节我们继续深入MOS管的小信号放大&#xff0c;建立MOS管的小信号模型。 我们本节继续使用上节的电路&#xff0c;如下图所示&#xff1a; DC偏置点 根据上节的知识…

2.单例模式,工厂模式,建造者模式,原型模式

单例模式 单例模式的优点&#xff1a; 处理资源访问冲突表示全局唯一类 实现单例的关键&#xff1a; 构造函数需要是 private 访问权限的&#xff0c;这样才能避免外部通过 new 创建实例&#xff1b;考虑对象创建时的线程安全问题&#xff1b;考虑是否支持延迟加载&#xff1b…

Deformable DETR TBD范式的不二选择

TBD范式检测和跟踪是不分家的。当前,性能较优的目标检测方法大都基于Transformer来做,CNN在目标检测的表现逐渐走低。DETR是基于Transformer的目标检测开山作,其解决了霸榜的yolo系列一些令人讨厌的事情,不需要前处理和后处理,做到了真正意义上的end to end: 前处理:Anc…

测试做得好,犯错少不了【30个最容易犯的错误】谨记

最近跟一些刚刚进入软件测试行业的朋友去交流&#xff0c;发现了一个有趣的现象&#xff0c;就是对于这个行业的很多问题的认识都是一致的片面&#xff0c;当然也可以理解为误区。自己利用一点时间&#xff0c;把他们对于这个行业的认识误区都罗列出来&#xff0c;然后结合自己…

Centos7搭建Hadoop集群(V3.3.4)

Centos7搭建Hadoop集群V3.3.4一、准备工作1、配置hostname2、hosts映射3、关闭防火墙4、同步时间5、关闭selinux6、配置ssh免密登陆7、重启二、安装所需环境1、jdk安装2、hadoop安装三、修改配置hadoop-env.shcore-site.xmlhdfs-site.xmlmapred-site.xmlyarn-site.xmlworkers四…

web测试1:在ubuntu中运行boa

参考文档&#xff1a; https://blog.csdn.net/feit2417/article/details/84777523 1.下载boa源码 Boa Webserver主页&#xff1a;Boa Webserver 下载链接&#xff1a;http://www.boa.org/boa-0.94.13.tar.gz 下载后&#xff0c;解压 tar zxvf boa-0.94.13.tar.gz。 文件列表…

Mysql-query优化之explainProfiling

1、explain 要对执行计划有个比较好的理解&#xff0c;需要先对MySQL的基础结构及查询基本原理有简单的了解。 MySQL本身的功能架构分为三个部分&#xff0c;分别是 应用层、逻辑层、物理层&#xff0c;不只是MySQL &#xff0c;其他大多数数据库产品都是按这种架构来进行划分的…

Spring的快速入门代码实现

Spring的快速入门代码实现前言&#xff1a;需要创建Maven的项目&#xff08;这个一定要看下面的链接&#xff09;1.如何创建Maven项目直达链接2.将这个项目的名字 命名为spring_ioc一、导入坐标&#xff08;pom.xml文件中实现&#xff09;1.1 代码&#xff1a;1.2 version的版本…

【GD32F427开发板试用】从0开始到RTthread移植

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;军军 写在前面: 很高兴获得这次试用机会&#xff0c;一直很想了解国内ARM MCU的发展&#xff0c;尤其是GD32系列的MCU。我也成功获得了第一次使…

PyQt5开发环境搭建 1.2 简单的例子

目录 基本开发步骤 创建Eric6工程目录 使用Qt Creator创建Qt项目 创建项目 Kit Selection 打开编辑窗口 打开form 放一个Label 拖动Label 放一个Button 拖动Push Button 保存UI文件 编译ui文件 当前项目下文件列表 将ui文件编译成py文件 将ui文件和py文件拷贝到…

学习记录678@项目管理之合同管理案例

案例 系统集成商 Simple 公司与生产型企业 Perfect 集团签订了一份企业MIS(管理信息系统)开发合同&#xff0c;合同已执行到设计和开发阶段&#xff0c;由于 Perfect 集团内部组织结构调整可能会影响核心业务的流程。集成商 Simple 公司提出建议&#xff0c;合同暂停执行至新的…

使用 Jenkins + Gitee + maven 自动化部署 Spring Boot

目录 1、前言 1 .1、插件简介 1.2、插件安装 2、创建Maven项目 2.1、新建一个全新的项目 2.2、拷贝已有项目 3、项目配置 3.1 、General 3.2、源码管理 3.3、构建触发器 3.4、构建环境 3.5、Pre Steps 3.6、Build 1&#xff09;Root POM 2&#xff09;Goals and…

kubenertes集群部署kubeadm方式

一、安装要求 1、3台机器&#xff09;&#xff0c;操作系统Centos7,5.4内核,CentOS 7.x 系统自带的3.10.x内核存在一些Bugs.导致运行的Docker.Kubernetes不稳定。 2、硬件配置:4GB内存&#xff0c;至少2个CPU或更多CPU&#xff0c;至少硬盘30GB或更多 3、集群中所有机器之间网…

41 锚框【动手学深度学习v2】】

41 锚框【动手学深度学习v2】】 锚框&#xff1a;对边框位置的猜测。 先提出多个框在某个地方&#xff0c;比如5个&#xff0c;然后去看这5个框里面到底有没有物体。 两次预测&#xff1a;锚框位置和锚框内物体的预测。 如何处理锚框&#xff1f; IoU - 交并比 比较两个框之…

Java开发实现图书管理系统(超详细)

本文用Java代码实现图书代码管理系统&#xff0c;有些地方可能会有纰漏&#xff0c;希望各位大佬鉴赏&#xff01;&#xff01; 文章目录 文章目录 一、Java实现图书管理系统 1.1创建book包 二、创建图书管理系统的操作包 2.1创建Operation接口 三、创建User包 3.1创建User类 四…

深度盘点时间序列预测方法

本篇介绍时间序列的定义、任务、构成以及预测方法&#xff0c;主要是基本概念的介绍和理解。 时间序列定义 时间序列&#xff0c;通俗的字面含义为一系列历史时间的序列集合。比如2013年到2022年我国全国总人口数依次记录下来&#xff0c;就构成了一个序列长度为10的时间序列…

CAN FD的一致性测试 助力汽车电子智能化

后起之秀——CAN FD&#xff1a;随着各个行业的快速发展&#xff0c;消费者对汽车电子智能化的诉求越来越强烈&#xff0c;这也致使整车厂将越来越多的电子控制系统加入到汽车控制中。在传统汽车、新能源汽车、ADAS和自动驾驶等汽车领域中&#xff0c;无不催生着更高的需求&…

4.kafka--生产调优

文章目录1.硬件配置选择1.场景说明2.服务器台数选择3.磁盘选择4.内存选择1) 堆内存配置2&#xff09;页缓存配置5. cpu选择6.网络选择2.生产者3.kafka broker4. 服役新节点&#xff0c;退役旧节点1&#xff09;创建一个要均衡的主题。2) 生成一个负载均衡的计划leader分布不均匀…

多人配音怎么做的?这两个多人配音方法分享给你

大家在刷一些短视频的时候&#xff0c;肯定有看到过一些搞笑的视频&#xff0c;而这些视频总能让我们捧腹大笑&#xff0c;过后再多看几次&#xff0c;其实你可以明显的发现这是多人互动对话或者一人分饰多角所呈现的&#xff0c;我们想要做出这种类型的视频&#xff0c;一般需…