Linux操作系统之进程信号

news2024/11/16 7:32:45

代码存放在:https://github.com/sjmshsh/System-Call-Learn/tree/master/signal

我们先来看一张图,了解一下通过阅读本博客,你可以收获什么。

在这里插入图片描述

背景知识

首先我说明一点

信号 != 信号量

我们这篇文章讲解的是信号,不是信号量

信号在生活中处处有在,例如红绿灯,上课铃声等等。信号可以让我们知道我们要做什么事情。

其实Linux操作系统就像是一个社会,处处充满着生活中的哲学。Linux操作系统也是有信号的。

信号的产生就代表场景的触发,在Linux中,信号是给进程发的,进程要在合适的时候执行对应的动作。光有信号是没有意义的,重要的是一种类似协议的东西。也就是我制定一个规则,你看到信号的时候就固定触发某些场景,例如我看到红灯就停止走路,看到绿灯就继续走路。

Linux操作系统给进程发送信号,并且它具有识别和处理信号的能力。

那么我们看到信号就一定要处理吗?不一定,生活中有很多信号,例如上课铃声响了,但是我生病了没有去上课。我生病了,体温39°这个信号的优先级明显要高于上课铃声。

因此信号随时都有可能产生,但是并不是立即会处理,而是等到合适的时候再处理。

既然信号不能被立即处理,那么已经到来的信号是不是应该暂时存储起来呢?答案肯定是的,所以在进程在收到信号后,要先把信号保存起来,等到合适的时候再处理。

那么应该保存在哪里呢?task_struct,这毫无疑问。

信号的本质也是数据,信号发送的本质就是往task_struct结构体中写入对应的数据。

task_struct是一个内核数据结构,用来定义进程的内核对象,而内核不相信任何人,用户不可以对内核数据结构进行写入,所以是谁向task_struct中写入信号数据的呢?是OS!

所以无论我们的信号如何发送,本质都是通过OS发送的

在这里插入图片描述

信号产生的各种方式

signal函数修改信号处理动作

首先我们可以用kill -l指令查看我们有哪些信号。

在这里插入图片描述

前31个是普通信号(1 - 31)

后31个是实时信号(34 - 64)

例如我们CTRL + C 其实就是在给操作系统发送2号(SIGINT)信号。

那么怎么证明呢?

我们先来介绍一个系统调用接口:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

sighandler_t是一个参数为int,返回类型为void的函数指针。

第一个参数是一个整数,可以用信号名,也可以用信号的编号。

第二个参数是一个函数。

这个函数的作用是修改进程对信号的默认处理动作。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signal)
{
    printf("I got a signal, signal id : %d, pid : %d \n", signal, getpid());
}

int main()
{
    // 通过signal函数把2号动作处理为我们特定动作
    signal(2, handler);
    while (1)
    {
        printf("hello world! My pid : %d\n", getpid());
        sleep(1);
    }
    
    return 0;
}

在这里插入图片描述

可以看到我CTRL + C就变成执行handler函数,而不是退出了。

所有信号,除了9号信号之外,都可以像这样进行操作。9号信号是用来杀死进程的,它是特殊的,不能被自定义,也不能被阻塞。

总结:进程收到信号后的处理方式有3种:

  • 默认动作,一部分是终止自己,暂停等
  • 忽略动作,也是信号处理的一种方式,就是什么都不干
  • 自定义动作,例如上面展示的

这个函数介绍完了,那么现在我们来了解一下,有哪些方式可以发出信号。

键盘命令产生信号与运行时软硬件错误收到os发的信号

野指针或者数据越界的时候,有的时候会发生段错误(Segmentation fault

现在我们来证明一下:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signal)
{
    printf("I got a signal, signal id : %d, pid : %d \n", signal, getpid());
}

int main()
{
    // 我们不管三七二十一,把除了2之外所有的信号都处理为我们的特定动作,留个2是退出CTRL+C就行了,比较方便
    for (int i = 0; i <= 31; i++)
    {
        if (i != 2)
            signal(i, handler);
    }
    while (1)
    {
        int* p = NULL;
        p = (int*)100;
        *p = 100;
        printf("hello world! My pid : %d\n", getpid());
        sleep(1);
    }
    return 0;
}

看一下结果:

I got a signal, signal id : 11, pid : 14520 

11号信号是SIGSEGV.是段错误的意思。

再来看看经典的除零错误

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signal)
{
    printf("I got a signal, signal id : %d, pid : %d \n", signal, getpid());
}

int main()
{
    // 我们不管三七二十一,把除了2之外所有的信号都处理为我们的特定动作,留个2是退出CTRL+C就行了,比较方便
    for (int i = 0; i <= 31; i++)
    {
        if (i != 2)
            signal(i, handler);
    }
    while (1)
    {
        int a = 1;
        a /= 0;
        printf("hello world! My pid : %d\n", getpid());
        sleep(1);
    }
    return 0;
}

结果是:

I got a signal, signal id : 8, pid : 15487 

8号,也就是SIGFPE,浮点数错误。

我们的进程本来会因为错误程序直接崩溃,但是却没有,原因是进程崩溃的本质就是进程收到了对应的信号,执行信号的默认行为。

那么为什么会被发送对应信号呢?

操作系统是硬件的管理者,硬件的各种状态操作系统都要管理,这些错误会对硬件造成影响,那么操作系统必然不会视而不见,肯定要发送信号处理相关的错误。

软件上面的错误,通常会体现在对应的硬件上或者其他软件上。

a /= 0在CPU计算的时候,如果有浮点数错误,会有一个标志位标记出错。*p = 100野指针访问的时候,管理虚拟内存映射的mmu硬件会标记你越界了。

那么一个进程崩溃的时候,我们希望获得崩溃的原因,即获得对应收到的信号,而前面学习过,waitpid时拿到的是status,它的低7位(status & 0x7f)就是对应的信号。

这里做一个回顾。

在这里插入图片描述

但是光获得报错信息是没有意义是,我们需要解决它,因此这里就需要用到core dump标志了,这也算是把前面一个没有解决的坑给填上了。

在Linux种中,档一个进程正常退出的时候,退出码和退出状态都会被设置,只不过退出码是0而已。当一个进程异常退出的时候,进程的退出信号会被设置,表明进程退出的原因。如果你设置了,那么会把core dump标志位设置成1,如果这个位是1的话,那么进程在内存中的数据会转出到磁盘中,方便后期调试。

ulimit -a:查看系统资源,可以查看core dump是否开启。

在这里插入图片描述

0,说明没有开启。

ulimit -c 10240:允许core dump操作。

当我们开启之后,如果进程崩溃了,就会生成一个文件。

在这里插入图片描述

这个文件是一个二进制文件。

在这里插入图片描述

然后在编译的时候带上-g,代表程序可以被调试,然后core-file core.pid进行调试,就可以知道错误原因,然后解决了。

系统产生信号

这里介绍几个可以产生信号的系统调用接口:

#include <signal.h>
// 向某个进程发送指定信号
int kill(pid_t pid, int signo);
// 对自己发送某个信号
int raise(int signo);
这两个函数都是成功返回0,错误返回-1

abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

也就是发送6号信号。

软件条件产生信号

例如:进程间通信,读端不读且把fd关闭了,写端一直还在写,最终写进程会收到SIGPIPE(13号)信号。

还有系统调用接口alarm

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。

如何理解操作系统向task_struct写入信号数据

普通信号的取值范围是[1, 31],进程的task_struct内部一定要有对应的数据变量来保存记录,表明是否收到了对应的信号。

很明显是一个位图uint32_t sigs

打个比方:

0000 0000 0000 0010 0101表示进程收到了1号,3号,6号信号。

因此操作系统向task_struct写入信号的本质就是OS向进程PCB的位图对应的比特位置1,完成信号的发送就是完成对信号的写入。

信号的保存状态

背景知识

实际执行信号的处理动作成为信号抵达,分为三种:

  • 自定义捕捉
  • 默认
  • 忽略

信号从产生到抵达之间的状态叫做信号未决,本质是这个信号被存在task_struct里面还没有被处理。

进程可以选择阻塞(Block)某个信号,本质就是操作系统允许进程暂时屏蔽指定的信号,它表明:该信号依然是未决的;该信号不会被抵达直到解决阻塞。

忽略,阻塞的区别:

  • 忽略是一种信号处理方式,阻塞是没有抵达,是一种独立的状态

信号处理在内核中有三张表:pendingblockinghandler

上图:

在这里插入图片描述

pending就是写入的那个位图,表示已经收到但是还没有抵达的信号。

handler是一个函数指针数组void(*handler[31])(int),存放信号的处理方法。

block是阻塞数据,如果标记是1的话,代表信号被阻塞,不会被执行。

这个图是横着看的,如果信号对应的位置的比特位是1,代表信号被阻塞了,后续就不用进行操作了,如果不是1,才有后面两个位图的事情。

os检测处理信号的伪代码如下:

int isHandler(int signo)
{
    if (block & signo)
    {
        // 阻塞了 根本不管有没有信号
    }
    else
    {
        // 没有被block
        if (signo & pending)
        {
            // 该信号被收到了
            hadnler_array[signo](signo);
            return 0;
        }
    }
    return 1;
}

因此block表又被称为信号屏蔽字

相关系统调用接口

不是只有接口才是系统调用,OS也会给用户提供数据类型,配合系统调用来完成,比如shmget中的key_t、struct ipc_perm等,这些是配合接口使用的数据类型。

sigset_t:从上面的信号保存状态图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,

这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当 前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信息集操作函数:

#include <signal.h>
int sigemptyset(sigset_t *set);// 把位图集合清空 全部置0
int sigfillset(sigset_t *set);// 全部置1
int sigaddset (sigset_t *set, int signo);// 把一个信号添加到这个位图里 也就是把这个信号对应的位图的位置1
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);// 判定一个信号是否在集合中 

除sigismember外,这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask:修改进程的block位图。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
//返回值:若成功则为0,若出错则为-1 

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask = mask
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set
SIG_SETMASK设置当前信号屏蔽字为set所指的值,相当于mask = set

这个有相关代码去我GitHub上面看就可以了。

信号处理方式

信号发送后为什么是合适的时候才选择处理信号呢?这是因为信号的产生是异步的,当前进程可能会有更重要的工作要去做。

那么这些信号什么时候去处理呢?

当进程从内核态返回到用户态的时候,进行上面的检测与处理

那么什么是内核态和用户态呢?

内核态和用户态

内核态:执行OS的代码和数据时,计算机所处的状态。就叫做内核态。OS的代码的执行,全部都是在内核态。

用户态:就是用户代码和数据被访问或者执行的时候,所处的状态。我们自己写的代码,全部都是在用户态执行的。

主要区别:在于权限。

这里用一张大图解决所有的问题:

在这里插入图片描述

进程之间不管如何切换,我们一定可以找到同一个OS,因为每个进程都有3 - 4G的内核空间,使用同一张内核级别页表就可以找到内核相关的数据和代码。

所谓系统调用,本质就是进程身份转换成内核,然后根据内核页表转换成为内核态,然后根据内核页表找到对应的系统函数。

那么我们现在回到刚才的问题,信号执行的时机:

在这里插入图片描述

抽象一下就是:

在这里插入图片描述

那么为什么一定要回到用户态执行自定义函数呢?

其实在内核态也可以执行,但是这样很不安全,如果我这个handler函数里面有一个破坏操作系统内核的脚本的话,那么一套下来OS就废了。

sigaction - 注册信号捕捉函数

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 

它修改的是handler表,它也可以处理实时信号,第二个参数是输入型参数,动作方法填入这个结构体中,oact是一个输出型参数,返回老的信号处理方法。

img

sa_mask的含义:处理信号时希望暂时屏蔽其他信号,不让其他信号影响当前信号的处理。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

对应的代码,也可以在GitHub里面看,这里就不写了。

可重入函数

可重入函数实际上就是所谓的非线程安全函数,我有多个执行流可以进入一个函数执行逻辑就叫可重入函数,在STL中,大部分函数都是不可重入的,也就是线程安全的。

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

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

volatile关键字

我们先来看一个代码:

#include <stdio.h>
#include <signal.h>

int flag = 0;

void handler(int signo)
{
    flag = 1;
    printf("change flag 0 to 1.\n");
}

int main()
{
    signal(2, handler);

    while (!flag);

    printf("这个进程是正常退出的.\n");
    
    return 0;
}

这个代码就是当我CTRL+C的时候发送2号信号,然后被捕获,在handler里面改变flag的值,从而while循环结束,程序退出。

测试一下发现完全没有任何问题。

原因是我们的Makefile是这么写的:

signal: test.cc
	g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
	rm -f signal

没有写编译器优化的选项,我们加上-O3

然后允许,发现程序无法停止,然而在flag变量前面加上关键字volatile就可以了,这是为什么呢?

这里解释一下编译器优化:

编译器编译,构建语法树的时候可以发现main主程序里面没有对flag变量做更改,所以会进行优化,为了提高速度,会把flag放入寄存器里面,而我们改flag的值是在内存里面的改的,对CPU不可见了。

在这里插入图片描述

那么volatile的作用就很简单了,就是告诉编译器不要对我的这个变量做任何的优化

  1. 保持内存可见性
  2. 防止指令重排序

SIGCHILD信号

我们之前了解过用waitpidwait清理回收僵尸进程,但是这样父进程会阻塞等待,或者每过一段时间就回去看一下,这样效率很低。而当子进程退出之后会向父进程发送SIGCHILD信号。因此,如果父进程不关心子进程的退出信息的话,我们可以直接把SIGCHILD忽略了,这样就不存在僵尸进程没有被回收的问题了。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}
int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    { // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}

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

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

相关文章

POJ3263. Tallest Cow题解(c++ 前缀和)

POJ3263. Tallest Cow 传送门&#xff1a;Tallest Cow 题目&#xff1a; 有N头牛站成一行。两头作能够相支看见&#xff0c;当且仅当它们中间的牛身高都比它们矮。现在&#xff0c;我们只知道其中最高的牛是第P头&#xff0c;它的身高是H&#xff0c;不知道剩余N-1头牛的身高。…

大数据之Kafka高级知识点

文章目录前言一、分片和副本机制&#xff08;一&#xff09;分片机制&#xff08;二&#xff09;副本二、Kafka如何保证数据不丢失&#xff08;一&#xff09;Producer生产者&#xff08;二&#xff09;Broker&#xff08;三&#xff09;Consumer消费者三、消息存储和查询机制总…

重新设计 TCP 协议

看一段关于 TCP 协议的历史讨论&#xff0c;源自&#xff1a;The design philosophy of the DARPA internet protocols 读这段文字时&#xff0c;你可能觉得这不是在谈 TCP&#xff0c;而是在创造一个新协议&#xff0c;但事实上这就是 TCP 在被创造过程中真实的纠结。 现在来…

Java知识点细节简易汇总——(6)面向对象编程(中级部分)

一、IDE快捷键 删除当前行, 默认是 ctrl Y 自己配置 ctrl d复制当前行, 自己配置 ctrl alt 向下光标补全代码 alt /添加注释和取消注释 ctrl / 【第一次是添加注释&#xff0c;第二次是取消注释】导入该行需要的类 先配置 auto import , 然后使用 altenter 即可快速格式化…

Day867.事务隔离 -MySQL实战

事务隔离 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于事务隔离的内容。 提到事务&#xff0c;肯定不陌生&#xff0c;和数据库打交道的时候&#xff0c;总是会用到事务。 最经典的例子就是转账&#xff0c;你要给朋友小王转 100 块钱&#xff0c;而此时你的银行…

[前端笔记——CSS] 10.层叠与继承、选择器

[前端笔记——CSS] 10.层叠与继承、选择器1.层叠与继承1.1 冲突规则1.2 继承1.3 层叠1.4 CSS位置的影响2.选择器2.1 选择器是什么&#xff1f;2.2 选择器列表2.3 选择器的种类类型、类和 ID 选择器标签属性选择器伪类与伪元素运算符选择器参考表1.层叠与继承 1.1 冲突规则 CS…

一起自学SLAM算法:8.1 Gmapping算法

连载文章&#xff0c;长期更新&#xff0c;欢迎关注&#xff1a; 下面将从原理分析、源码解读和安装与运行这3个方面展开讲解Gmapping 算法。 8.1.1 Gmapping原理分析 首先要知道&#xff0c;Gmapping是一种基于粒子滤波的算法。在7.7.2节中已经提到过用RBPF&#xff08;Rao-…

linux系统中使用QT实现APP开发的基本方法

大家好&#xff0c;今天主要和大家分享一下&#xff0c;如何使用QT进行APP的主界面开发的方法。 目录 第一&#xff1a;APP界面开发基本简介 第二&#xff1a;滑动界面实现 第三&#xff1a;APP界面开发实现 第四&#xff1a;APP主界面测试 第一&#xff1a;APP界面开发基本…

ARP渗透与攻防(一)之ARP原理

ARP原理 前言 ARP攻击就是通过伪造IP地址和MAC地址实现ARP欺骗&#xff0c;能够在网络中产生大量的ARP通信量使网络阻塞&#xff0c;攻击者只要持续不断的发出伪造的ARP响应包就能更改目标主机ARP缓存中的IP-MAC条目&#xff0c;造成网络中断或中间人攻击。ARP攻击主要是存在…

CSS3基础内容

目录 CSS基本样式 选择器分类 标签选择器 类选择器 利用类选择器画三个盒子 多类名 id选择器 id选择器和类选择器的区别 通配符选择器 CSS字体属性 字体粗细font-weight 字体样式 CSS文本属性 CSS的引入方式 行内样式表&#xff08;行内式&#xff09; 内部样式表…

2023年集卡活动简记

文章目录支付宝总评&#xff1a;【强烈推荐】年味浓&#xff0c;必中奖&#xff0c;单倍金额不算少。只关注开奖可以除夕当天玩儿。集卡分1.88元难度&#xff1a;【非常低】必中奖时间投入&#xff1a;【较少】无需打开其他App&#xff0c;比较轻松。操作体验&#xff1a;【好】…

深度理解卷积神经网络

神经网络包括卷积层&#xff0c;池化层&#xff0c;全连接层。一个最简单的神经元结构&#xff0c;假如有三个输入&#xff0c;都对应一个权重参数&#xff0c;然后通过权重加起来&#xff0c;经过一个激活函数&#xff0c;最后输出y。CNN中独特的结构就是卷积层&#xff0c;就…

拓展:阿里巴巴中文站架构演进分析【部分】

文章目录前言阿里巴巴中文站架构发展历程阿里巴巴第五代架构数据架构的复杂前言 由学习整理而来&#xff0c;并非有意抄袭。如果有冒犯行为&#xff0c;请及时联系作者进行处理&#xff01; 阿里巴巴中文站架构发展历程 时间关键技术1999第一代网站架构Perl&#xff0c;CGl&…

【实操案例八】元组、集合操作 实例代码及运行效果图!

任务一&#xff1a;我的咖啡馆你做主 方法一&#xff1a;使用列表 # 任务一&#xff1a;我的咖啡馆你做主 # 方法一&#xff1a;使用列表lst[蓝山,卡布奇诺,拿铁,皇家咖啡,女王咖啡,美丽与哀愁]for i in lst:print(lst.index(i)1,.,i,end\t) print()while True:chice int(in…

SpringCloud+Ribbon 报错:java.net.unknownhostexception:XXX

SpringCloudRibbon 报错&#xff1a;java.net.unknownhostexception:XXX 问题分析&#xff1a; 网上很多的说法是依赖冲突导致&#xff0c;原因是什么呢&#xff1a;如果你的org.springframework.cloud:spring-cloud-starter-netflix-eureka-client 依赖中包含了ribbon依赖&…

常用JVM配置参数简介

既然学习JVM&#xff0c;阅读GC日志是处理Java虚拟机内存问题的基础技能&#xff0c;它只是一些人为确定的规则&#xff0c;没有太多技术含量。 既然如此&#xff0c;那么在IDE的控制台打印GC日志是必不可少的了。现在就告诉你怎么打印。 &#xff08;1&#xff09;如果你用的是…

Elasticsearch7.8.0版本高级查询—— 分页查询文档

目录一、初始化文档数据二、分页查询文档2.1、概述2.2、示例一、初始化文档数据 在 Postman 中&#xff0c;向 ES 服务器发 POST 请求 &#xff1a;http://localhost:9200/user/_doc/1&#xff0c;请求体内容为&#xff1a; { "name":"zhangsan", "ag…

数据分析-深度学习 Pytorch Day8

一。什么是循环神经网络&#xff1a;循环神经网络&#xff08;Rerrent Neural Network, RNN&#xff09;&#xff0c;历史啊&#xff0c;谁发明的都不重要&#xff0c;说了你也记不住&#xff0c;你只要记住RNN是神经网络的一种&#xff0c;类似的还有深度神经网络DNN&#xff…

广州周立功CanTest卡使用教程一

网上有不少Can采集平台,包括Ardunio,Can卡,也有不少人用Freescale自己DIY一个平台,这些都是相当不错,并且都有成熟的代码,这里介绍在汽车诊断软件领域普遍都会选择的Can卡使用。 大家是不是对这个节面非常熟悉,CAN-bus 通用测试软件是一个专门用来对所有的 ZLGCAN 系列板…

【Python】基于经典网络架构训练图像分类模型——图像识别模型与训练策略(2023年1月22日,大年初一,春节快乐,兔年大吉)

声明:仅学习使用~ 今天是大年初一,祝大家新年快乐!!! 这个练习使用的图片稍多,因此初次在PyCharm里面可能会需要一些时间。 (注释中包含遇到的一些错误以及修正,同时也含有一些输出,部分较长的输出以省略号的形式在注释里面展示了) 2023.1.22,大年初一,新年快乐…