Linux - 信号阻塞 信号捕捉

Linux - 信号阻塞 & 信号捕捉

    • 信号阻塞
      • 信号集
      • 操作信号集
      • sigporcmask
      • sigpending
      • sigaction
    • 信号捕捉
      • 用户态与内核态
      • 信号捕捉的时机


在博客[Linux - 信号概念 & 信号产生]中,我讲解了信号的基本概念,以及信号是如何产生的,本博客将继续讲解信号阻塞信号捕捉问题。

信号阻塞

信号集

我们主要讨论的是非实时信号当一个进程收到非实时信号后,不是立马处理的,而是会挑选合适的时候进行处理,那么既然信号会被延时处理,就要有一个机制来保存进程之前收到的信号。

在了解这个保存信号的机制前,我们先了解一些信号的相关概念:

  1. 信号递达(Delivery):进程处理信号的过程称为递达,递达可以是执行默认处理函数,或者执行自定义的信号处理函数,忽略信号Ign也是一种处理信号的方式,也算递达
  2. 信号未决(Pending):当进程收到一个信号,但是还没有处理这个信号,称为未决
  3. 信号阻塞(Block):当一个信号被阻塞,就会一直保留在未决状态,不会执行任何处理函数

注意:忽略信号Ign与信号阻塞不同,忽略信号是一种递达方式,即进程处理这个信号的方式就是忽略它;而阻塞则是相当于进程根本收不到这个信号,信号被阻挡了

对应未决阻塞递达三个信号的状态,Linux内核中,进程的PCB维护了三张表pendingblockhandler

在这里插入图片描述

  • pending:该表的本质是一个位图,也称为未决信号集。当进程接收到一个信号,会把对应的比特位修改为1,表示进程已经接收到该信号
  • block:该表的本质是一个位图,也称为阻塞信号集。当进程收到信号,在pending中把对应的位修改1,此时就要经过block如果block中对应的位为1,表示该信号被阻塞,不会被递达penidng上的该位一直保持为1;如果block中对应的位为0,表示该信号未被阻塞,进程挑选合适的时候递达该信号
  • handler:该表本质是一个函数指针数组,指向信号的处理函数。如果时机合适,进程会检测pending表和block表,然后检测出已经接收到的信号,若该信号未被阻塞,执行对应信号的处理函数,并把pending中的该位变回0,表示该信号已经处理完了。

以上表还有以下特性:

  1. 当用户通过signal修改信号的默认处理方式,其实就是在修改这个handler内部的函数指针。
  2. 如果连续收到多个相同的非实时信号,此时pending位图只会记录一次,如果是实时信号,则会把收到的所有信号放进队列中,每个信号都会被处理。

操作信号集

简单了解了这三张表后,我们又要如何操纵这三种表呢?

对于handler表来说,其实就是通过signal函数来修改内部的处理函数,而对于blockpending表,有另外一套系统调用来处理。

blockpending表它们都叫做信号集,本质都是一张位图,要做的无非就是修改某一个位是0还是1,因此这两个表的操作是一样的。

操作这两个信号集,都依赖一个类型sigset_t,其包含在<signal.h>中,Linux中该类型的源码如下:

typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;//注意这前面有两个下划线


typedef __sigset_t sigset_t;//这次typedef后,前面没有下划线了

也就是说,sigset_t本质是一个结构体,结构体内部只有一个成员,且该成员是一个数组。这个数组就是用于存储位图的工具。从宏观上看,你可以理解为sigset_t就是一个位图,不过这不太严谨。

想要操作这张信号集,需要通过以下五个函数,这些函数也需要头文件<signal.h>,函数原型如下:

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);
  • sigemptyset:使信号集set中的所有比特位变为0
  • sigfillset:使信号集set中的所有比特位变为1
  • sigaddset:使信号集set的第signum位变为1
  • sigdelset:使信号集set的第signum位变为0
  • sigismember:检测信号集set的第signum位是0还是1

前四个函数的返回值都是:如果成功返回0,失败返回-1

也就是说,我们可以通过以上函数,来操作信号集这个位图,但要注意,我们通过这个函数操作的信号集,既不是block也不是pending,它目前只是一个进程中的变量而已。

那么我们接下来要做的,就是把我们自己创建并设置的信号集,与blockpending交互。

sigporcmask

sigprocmask函数用于读取或者更改进程的block,需要头文件<signal.h>,函数原型如下:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

  • how:以何种方式调用该函数,有以下选项
    • SIG_BLOCK:将set中为1的比特位添加到block中,相当于block = block | set
    • SIG_UNBLOCK:将set中为1的比特位,从block中删除,相当于block = block & ~set
    • SIG_SETMASK:直接将block设置成当前set的样子,相当于block = set
  • set:指向自己维护的信号集sigse_t的指针
  • oldset:输出型参数,用于接收修改前的block

sigpending

sigpending函数用于读取进程的pending,需要头文件<signal.h>,函数原型如下:

int sigpending(sigset_t *set);

参数:

  • set:输出型参数,将pending传入到set

接下来我们综合以上的所有接口,进行几个实验:

  1. 证明block确实可以阻塞信号,信号确实保存在pending
int main()
{
    sigset_t set;

    sigemptyset(&set);
    sigaddset(&set, 2);
    sigprocmask(SIG_BLOCK, &set, nullptr);

    while (true)
    {
        sigset_t pending_set;
        sigpending(&pending_set);

        for (int i = 31; i > 0; i--)
        {
            if (sigismember(&pending_set, i))
                cout << "1";
            else
                cout << "0";
        }
        cout << endl;

        sleep(1);
    }

    return 0;
}

首先定义了一个sigset_t类型的变量set,由于不清楚这个变量会被初始化为什么样子所以要用sigemptyset将其全部初始化为0

此处我们用(2) SIGINT做检测,先通过sigaddset(&set, 2)set中的第二位变为1,随后通过sigprocmask(SIG_BLOCK, &set, nullptr)set添加到block中,由于我们并不想知道旧的block是什么样,所以第三个参数设为nullptr

接着进程陷入一个死循环,循环体内通过sigpending获取当前进程的pending,随后通过一个for循环将这个pending输出,此处要注意:不能直接通过循环输出结构体内部的数组,必须通过sigismember检测一个位是0还是1

这个pending是用于保存进程中的未决信号的,我们已经把(2) SIGINT阻塞了,如果预测没有错误的话,那么输入ctrl + C时,pending的第二位会变成1,这就说明我们已经接收到该信号了。但是block(2) SIGNAL给阻塞了,导致其一直处于pending中,无法被递达,所以pending的第二位会一直是1

输出结果:

在这里插入图片描述

可以看到,输入ctrl + C之后,第二位从0变成了1,并且持续为1,即信号被阻塞了。

  1. 检测是否所有信号可以被阻塞
void showSet(sigset_t *pset)
{
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(pset, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

int main()
{
    sigset_t set, old_set;

    sigemptyset(&old_set);
    sigfillset(&set);
    sigprocmask(SIG_BLOCK, &set, &old_set);

    cout << "old_set: ";
    showSet(&old_set);

    sigprocmask(SIG_BLOCK, &set, &old_set);

    cout << "new_set: ";
    showSet(&old_set);
    
    return 0;
}

以上代码中,我将输出信号集的各个比特位的功能,封装为了一个函数showSet,然后定义了两个信号集setold_set。我们的目的是:将set的所有位变成1,然后添加到block中,在将block提取到old_block中。最后观察old_block,就可以知道是否block被成功设置为了全1

首先通过sigemptysetold_set设为全0,通过sigfillsetset设为全1。接着通过sigprocmask(SIG_BLOCK, &set, &old_set);set添加到block中,把初始的block添加到old_block中,随后通过showSet输出old_set

此处要注意,第一次old_set提取到的,不是设置后的block而是设置前的block,也就是说现在old_set拿到的是block的默认值。

现在我们要拿到block被添加了全1后的值,所以要再进行一次sigprocmask(SIG_BLOCK, &set, &old_set);,这次拿到的是上一次的block的值,也就是添加了全1后的值,再通过showSet输出。

以上代码我们输出了两次old_set,此处再强调一遍:第一次输出的是block的默认值,第二次输出的是全1设置后的block

输出结果:

在这里插入图片描述

  • 第一次输出为全0说明block的默认值是全0,不阻塞任何信号

为什么第二次输出时,我们明明把全1set添加到block中,却不是全1呢?

不妨设想:如果一个进程将所有信号都阻塞了,那么我们就无法通过任何信号杀死它,出现一个不死的进程,如果这个进程是一个病毒,那么就会带来大麻烦。因此操作系统设计的时候,就应该避免这个情况,所以有一些信号不允许被阻塞!

通过实验现象可以看出来:信号(9) SIGKIILL(19) SIGSTOP不允许被阻塞

  1. 信号被递达时,block表和pending表是什么状态
void showSet(sigset_t *pset)
{
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(pset, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

void handler(int sig)
{
    cout << "get sig:" << sig << endl;

	sigset_t pending_set;
	sigemptyset(&pending_set);
    sigpending(&pending_set);
    cout << "pending: ";
    showSet(&pending_set);
    
    sigset_t tmp;
	sigemptyset(&tmp);
	
    sigset_t block_set;
	sigemptyset(&block_set);
    sigprocmask(SIG_BLOCK, &tmp, &block_set);
    cout << "block:   ";
    showSet(&block_set);
    
    exit(0);
}

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

    while (true);

    return 0;
}

以上代码,先通过signal(2, handler);设置信号(2) SIGINT的处理函数,随后进程陷入while的死循环。

handler中,会先通过sigpending(&pending_set);获取当前的pending,随后输出这个pending。再通过sigprocmask(SIG_BLOCK, &tmp, &block_set);,获取当前的block,存到block_set中。

输出结果:

在这里插入图片描述

我们通过ctrl + C给进程发送(2) SIGINT,毫无疑问pending的第二位会被设置为1

进入handler后,发现pending的第二位为0,我们明明发送信号把第二位设置为1,为什么输出的时候发现是0呢?

这说明在处理handler前就已经把pending变回0了!也就是先清除信号,后递达

另外的,我们发现block的第二位变成了1,我们明明没有阻塞(2) SIGINT,为什么显示block的第二位是1呢?

这是为了防止信号在handler中自己触发自己,导致信号的嵌套调用,比如这样:

void handler(int sig)
{
	kill(getpid(), 2);
}

上面这个handler中,再次触发了信号,这样会造成无限递归,因此操作系统在处理信号的时候,把对应的位阻塞,防止无限递归


通过以上三个实验,我们熟悉了信号相关的接口,并证明了一些知识点:

  1. 信号确实是被保存在pending中的,block确实可以阻塞信号递达
  2. block的默认值为全0,不阻塞任何信号,信号(9) SIGKIILL(19) SIGSTOP不允许被阻塞
  3. 信号是先清除,后递达的
  4. 操作系统在处理信号的时候,把对应的位阻塞,防止无限递归

sigaction

了解前面的接口后,我们来讲解本博客的最后一个接口sigaction

刚刚我们说:操作系统在处理信号的时候,把对应的位阻塞,防止无限递归。

操作系统提供了sigaction接口,可以让我们在处理信号的时候,自定义要阻塞哪些信号=!

sigaction用于设置信号处理的自定义函数和block,需要头文件<signal.h>,函数原型如下:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

第一个参数signum用于指定该处理函数作用于哪一个信号。第二个参数和第三个参数的类型都是struct sigaction,接下来我简单讲解以下这个结构体:

该结构体用于描述信号的处理方式,定义如下:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  • sa_handler:一个函数指针,指向自定义信号处理函数handler
  • sa_sigaction:不管,使用的时设为空指针即可
  • sa_mask:被设置的block信号集
  • sa_flags:不管,使用时设为0即可
  • sa_restorer:不管,使用的时设为空指针即可

第二个参数act用于传入信号处理方式,oldact用于接收老的信号处理方式。

示例:

void showSet(sigset_t *pset)
{
    for (int i = 31; i > 0; i--)
    {
        if (sigismember(pset, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

void handler(int sig)
{
    cout << "get sig:" << sig << endl;

    sigset_t block_set;
    sigset_t tmp;
    sigemptyset(&block_set);
    sigprocmask(SIG_BLOCK, &tmp, &block_set);
    cout << "block:   ";
    showSet(&block_set);

    exit(0);
}

int main()
{
    struct sigaction act;
    act.sa_handler = handler;
    act.sa_flags = 0;

    sigemptyset(&act.sa_mask);
    
    for (int i = 1; i <= 5; i++)
        sigaddset(&act.sa_mask, i);

    sigaction(2, &act, nullptr);

    while (true);

    return 0;
}

以上示例中,先定义了struct sigaction act,用于传入信号处理方式。其sa_handler 用于传入处理函数handlersa_flags 设为0即可。然后通过sigemptysetsa_mask全部位变成0,再通过一个for循环把前五位变成1

最后通过sigaction(2, &act, nullptr),设置2号信号的处理方式。在handler内部,获取并输出当前的block信号集。

按照预期,在处理handler的时候,block的前5位会被设置成1

输出结果:

在这里插入图片描述

可以看到,此时前五位信号就被block了。

sigaction的功能与signal的功能有重合的部分,那就是自定义信号处理函数,但是sigaction可以额外设置处理信号时的block信号集。


信号捕捉

之前讲解信号的递达时,我一直说:在合适的时候,操作系统会处理信号,那么问题来了,到底什么时候才是合适的时候?也就是说,到底什么时候操作系统会去处理pending中的信号呢?

为了解决这个问题,我们要先了解操作系统的用户态内核态

用户态与内核态

Linux 操作系统是一个多用户、多任务的操作系统,为了安全性和资源管理,它将系统划分为 用户态内核态 两种运行模式。

每个进程都有自己独立的进程地址空间:

在这里插入图片描述

进程地址空间被分为两部分:用户空间内核空间,每个空间都有自己的页表去映射内存,内核空间使用的页表叫做内核级页表,用户空间使用的页表叫做用户级页表内核级页表在整个内存中指保留一份

内核空间中的虚拟地址,指向了内存中的操作系统的代码和数据,操作系统本身也是一个软件,也要有自己的代码和数据,任何用户访问操作系统的本质,其实都是去执行操作系统的代码。用户访问操作系统,其实就是通过地址空间的内核空间区域来访问的!当一个进程想要访问操作系统,就可以通过自己的地址空间的内核部分来访问。

每个进程都有自己的独立的地址空间,那么每个进程都有自己独立的内核空间,因此每个进程都可以访问到操作系统!也就是说,内核空间存在的意义在于,不论当前哪一个进程在调度,都可以随时通过该进程的内核空间来找到操作系统!

可以简单理解为:当进程执行用户空间的代码,此时就处于用户态,当进程执行内核空间的代码,此时就处于内核态。当然,其实此处并不是进程自己去执行内核空间的代码,而是唤醒操作系统去执行

最简单的例子就是系统调用,当进程调用系统调用的时候,此时需要更高级别的权限来访问内核的底层数据。毫无疑问普通的进程是没有这个权限的,当进程进行系统调用,此时就会唤醒操作系统去执行内核空间的代码,此时就完成了用户态内核态的切换

进程从用户态切换到内核态主要有以下几种情况:

1. 系统调用(System Call)

  • 用户态程序需要执行一些需要内核权限的操作,比如读写文件、创建进程、访问网络等,就需要通过系统调用进入内核态。

2. 硬件中断(Hardware Interrupt)

  • 当硬件设备(例如磁盘、网络接口、键盘等)发生中断时,会触发硬件中断,将控制权从用户态转移到内核态。
  • 内核会根据中断类型进行处理,并可能需要调用相应的驱动程序来处理硬件事件。

3. 时钟中断(Clock Interrupt)

  • 内核会设置定时器,定期触发时钟中断,用于执行一些周期性任务,例如进程调度、内存管理等。

简单来说,进程不仅仅是系统调用的时候会进入内核态,只要是进程需要操作系统提供的服务时,都会进入内核态,比如收到硬件中断,发送时钟中断,需要进行进程调度。

从用户态切换到内核态的过程叫做陷入内核态,而从内核态切换到用户态的过程叫做返回用户态

陷入内核是一个非常频繁的操作,操作系统会不断地进行用户态和内核态之间的切换,以保证系统的正常运行

用户态与内核态的区别主要如下:

特性用户态内核态
权限受限全权
资源访问有限所有
代码执行用户程序操作系统内核
硬件访问受限可直接访问
安全性较高较低

更详细的解释:

  • 权限: 用户态程序只能访问有限的系统资源,例如用户空间内存、文件系统等,而内核态程序可以访问所有系统资源,包括硬件设备和内核数据结构。
  • 资源访问: 用户态程序只能访问用户空间的内存,而内核态程序可以访问所有内存空间,包括用户空间和内核空间。
  • 代码执行: 用户态程序是指用户编写的程序,例如浏览器、文本编辑器等,而内核态程序是指操作系统内核代码,负责管理系统资源和处理系统核心功能。
  • 硬件访问: 用户态程序无法直接访问硬件设备,需要通过系统调用向内核请求访问,而内核态程序可以直接访问硬件设备。
  • 安全性: 用户态程序的权限有限,因此安全性相对较高,而内核态程序拥有完全的系统权限,安全性相对较低。

简而言之,用户态其实就是给出更低的权限,执行用户自己写的代码,保证操作系统的安全;而内核态则持有极高的权限,几乎可以做到任何事情,因此这部分不能直接给用户使用,而是在内核态中执行操作系统自己的代码,保证安全。


信号捕捉的时机

讲了这么多用户态与内核态,那么这和信号有什么关系?

在从内核态返回用户态之前,操作系统会处理信号

执行过程大致如下图:

在这里插入图片描述

当操作系统因为某些原因陷入内核后,会先处理用户的需求,当处理完需求后,就会检测当前是否有需要处理的信号。也就是上图的C部分,该过程就是检测是否有信号要处理。

检测的结果有三种:

  1. 没有要处理的信号,直接返回用户态
  2. 有要处理的信号,且该信号的处理方式是默认处理函数,那么直接在内核态处理该信号,处理完毕后返回(C->D->A)
  3. 有要处理的信号,且该信号的处理方式是用户自定义函数,那么要先切换回用户态执行自定义函数(E),执行完函数后再到内核态(F),最后再切换回用户态(A)

如果信号的处理方式是默认处理方式,此时直接在内核态执行代码,主要有两个原因:

  1. 信号的默认处理方式,是操作系统自己提供的,因此不会有安全性问题,可以直接以内核态的高级权限执行
  2. 大部分信号的默认处理方式,是直接杀掉当前进程,杀掉进程的行为,需要内核态的权限,因此直接在内核态就可以杀掉这个进程

当信号的处理方式是用户自定义函数,那么要先切换回用户态执行,这是因为用户自定义的handler函数,其安全性是不确定的,如果贸然给这个函数一个内核态的权限,用户有可能会拿高级权限去做不安全的事情,所以不能给用户自定义的函数内核态权限,而是回到用户态执行这个函数

那么下一个问题就是,执行完handler函数后,E已经在用户态了,为什么还要回到内核态,在回到用户态

比如说某个时刻,进行了A -> B的陷入内核过程,那么当B执行完毕后,就要回到A。所以在内核态B中一定会存储一条信息(准确来说叫做上下文),指明之前A执行到那一行代码,从而在B -> A的时候,可以知道跳转回哪里。

  • 当用户态进程陷入内核态时,内核会保存用户态进程的上下文信息,包括:

    • 寄存器值:例如程序计数器(PC)、堆栈指针(SP)、通用寄存器等。
    • 内存状态:例如内存页表、虚拟地址空间等。
    • 其他状态:例如进程状态、信号掩码等。
  • 内核通过保存这些上下文信息,可以记录用户态进程执行到哪个位置,以及该进程的运行状态。

  • 当内核处理完用户态进程的请求后,会恢复用户态进程的上下文信息,并将控制权返回给用户态进程。

  • 用户态进程恢复执行后,会从之前中断的位置继续执行,而不会意识到自己曾经陷入内核态。

也就是说,陷入内核态之后,只有内核态知道之前的用户态执行到哪里,所以E状态下不能直接跳转回原来执行的地方,必须先回到内核态,去找到原先执行的位置,在返回用户态。

每一次在从内核态返回用户态之前,操作系统都会处理信号

这句话是什么意思呢?我们先前说在从内核态返回用户态之前,操作系统会处理信号,这句话完全没有问题的。但我在此要额外强调一个每一次

再次看到下图:

在这里插入图片描述

请问上图中,发生了几次内核态返回用户态

一共发生了两次,也就是我标红的这两个箭头C->EF->A,这两个时候都会检测并处理信号。

比如说某一次在E状态下处理完毕一个信号后,回到F,再准备回到A的时候,操作系统就会再做一次检测,检测还有没有要处理的信号,如果有,继续处理。


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

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

相关文章

IPTV,OTT,DVB有线数字电视

当我们买了一台电视回家&#xff0c;满心欢喜的打开&#xff0c;准备收看最新节目的时候&#xff0c;却发现没办法看直播&#xff0c;这个时候去广电办理业务&#xff0c;IPTV&#xff0c;OTT&#xff0c;DTV有线数字电视等这种词语整的眼花缭乱&#xff0c;那么今天我们来解释…

计算机网络 期末复习(谢希仁版本)第4章

路由器&#xff1a;查找转发表&#xff0c;转发分组。 IP网的意义&#xff1a;当互联网上的主机进行通信时&#xff0c;就好像在一个网络上通信一样&#xff0c;看不见互连的各具体的网络异构细节。如果在这种覆盖全球的 IP 网的上层使用 TCP 协议&#xff0c;那么就…

【FreeRTOS】源码概述

FreeRTOS源码概述 参考《FreeRTOS入门与工程实践(基于DshanMCU-103)》里《第7章 FreeRTOS源码概述》 相关文章&#xff1a;http://t.csdnimg.cn/QK0aO 1 FreeRTOS目录结构 使用 STM32CubeMX 创建的 FreeRTOS 工程中&#xff0c; FreeRTOS 相关的源码如下: 主要设计两个目录 C…

【教程】从0开始搭建大语言模型:实现Attention机制

从0开始搭建大语言模型 从0开始搭建大语言模型&#xff1a;实现Attention机制建模长序列存在的问题使用attention机制获得数据间的依赖Self-attention介绍带有可训练权重的self-attention1.生成Q&#xff0c;K&#xff0c;V变量2.计算attention score3.attention weight的获得4…

PS2045L-ASEMI低Low VF肖特基PS2045L

编辑&#xff1a;ll PS2045L-ASEMI低Low VF肖特基PS2045L 型号&#xff1a;PS2045L 品牌&#xff1a;ASEMI 封装&#xff1a;TO-277 最大平均正向电流&#xff08;IF&#xff09;&#xff1a;20A 最大循环峰值反向电压&#xff08;VRRM&#xff09;&#xff1a;45V 最大…

Armbian OS(基于ubuntu24) 源码编译mysql 5.7

最近弄了个S905X3的盒子刷完Armbian OS &#xff08;基于ubuntu24&#xff09;&#xff0c;开始折腾Arm64之旅。第一站就遇到了MySQL的问题&#xff0c;由于MySQL没有提供Arm64版本&#xff0c;又不想塞Docker镜像&#xff0c;因此选择源码来编译MySQL5.7。下面记录详细过程和遇…

马斯克的战略选择:特斯拉的H100显卡转移风波及其影响

引言 最近&#xff0c;一则关于马斯克将特斯拉的H100显卡转给他的新公司xAI的消息引发了广泛关注。这一决定不仅导致特斯拉股价下跌&#xff0c;还引发了关于马斯克战略决策的激烈讨论。本文将深入探讨这一事件的背景、过程及其对特斯拉和整个科技行业的影响。 背景与事件回顾…

8.transformers量化

Transformers 核心设计Auto Classes Transformers Auto Classes 设计:统一接口、自动检索 AutoClasses 旨在通过全局统一的接口 from_pretrained() ,实现基于名称(路径)自动检索预训练权重(模 型)、配置文件、词汇表等所有与模型相关的抽象。 灵活扩展的配置AutoConfig…

本地GPT-window平台 搭建ChatGLM3-6B

一 ChatGLM-6B 介绍 ChatGLM-6B 是一个开源的、支持中英双语的对话语言模型&#xff0c;新一代开源模型 ChatGLM3-6B 已发布&#xff0c;拥有10B以下最强的基础模型&#xff0c;支持工具调用&#xff08;Function Call&#xff09;、代码执行&#xff08;Code Interpreter&…

【Python】成功解决ModuleNotFoundError: No module named ‘PyQt5‘

【Python】成功解决ModuleNotFoundError: No module named ‘PyQt5’ 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博主简介&#xff1a;985…

c语言回顾-函数递归

1.递归的介绍 1.1什么是递归 递归是指在一个函数的定义中调用自身的过程。简单来说&#xff0c;递归是一种通过重复调用自身来解决问题的方法。 递归包括两个关键要素&#xff1a;基本情况和递归情况。基本情况是指当问题达到某个特定条件时&#xff0c;不再需要递归调用&am…

SpringBoot整合SpringDataRedis

目录 1.导入Maven坐标 2.配置相关的数据源 3.编写配置类 4.通过RedisTemplate对象操作Redis SpringBoot整合Redis有很多种&#xff0c;这里使用的是Spring Data Redis。接下来就springboot整合springDataRedis步骤做一个详细介绍。 1.导入Maven坐标 首先&#xff0c;需要导…

LLM应用实战:当图谱问答(KBQA)集成大模型(三)

1. 背景 最近比较忙(也有点茫)&#xff0c;本qiang~想切入多模态大模型领域&#xff0c;所以一直在潜心研读中... 本次的更新内容主要是响应图谱问答集成LLM项目中反馈问题的优化总结&#xff0c;对KBQA集成LLM不熟悉的客官可以翻翻之前的文章《LLM应用实战&#xff1a;当KBQ…

弘君资本:苹果股价暴涨,创历史新高!

当地时间6月11日&#xff0c;美股三大指数涨跌纷歧&#xff0c;标普500指数与纳指再创新高。 到收盘&#xff0c;道指跌0.31%&#xff0c;纳指涨0.88%&#xff0c;标普500指数涨0.27%。 苹果大涨逾7%创前史新高。美联储开端召开6月货币方针会议&#xff0c;周三发布利率决定。…

传神论文中心|第11期人工智能领域论文推荐

在人工智能领域的快速发展中&#xff0c;我们不断看到令人振奋的技术进步和创新。近期&#xff0c;开放传神&#xff08;OpenCSG&#xff09;社区发现了一些值得关注的成就。传神社区本周也为对AI和大模型感兴趣的读者们提供了一些值得一读的研究工作的简要概述以及它们各自的论…

如何进行电子故障失效分析FA?

在电子主板生产的过程中&#xff0c;一般都会出现失效不良的主板&#xff0c;因为是因为各种各样的原因所导致的&#xff0c;比如短路&#xff0c;开路&#xff0c;本身元件的问题或者是认为操作不当等等所引起的。 所以在电子故障的分析中&#xff0c;需要考虑这些因素&#x…

5.5 业务流程和业务逻辑设计

一、引言 1.1 项目背景 经过上述的论述&#xff0c;我们讨论一下业务流程和业务逻辑设计&#xff0c;通过合理的业务流程设计和业务逻辑设计&#xff0c;可以提高用户的购物体验&#xff0c;降低用户的操作成本&#xff0c;并确保用户的购物行为符合平台的规则和要求。同时&a…

旅游网页(HTML+CSS+JS)

前言 本篇博客就不给大家讲解了&#xff0c;直接上代码 &#x1f493; 个人主页&#xff1a;普通young man-CSDN博客 ⏩ 文章专栏&#xff1a;https://blog.csdn.net/2302_78381559/category_12644031.html?spm1001.2014.3001.5482https://blog.csdn.net/2302_78381559/catego…

Linux防火墙管理

计算机防火墙用于保护内部网络&#xff0c;主机和网络安全&#xff0c;有硬件防火墙和软件防火墙两种&#xff0c;软件主要是用对数据包进行分析过滤来保证软件层面安全。 此外还有根据对数据封包形式确定的分类方法&#xff0c; 如代理服务器&#xff0c;类似网关的形式监控整…

Mcgs 屏幕Modbus RTU通讯调试

目录 1. 设备窗口1.1 添加设备构件1.2 设备配置1.2.1 通用串口父设备配置1.2.2 设备0--ModbusRTU配置2. 设计用户窗口2.1 关联设备通道与实时数据库2.3 用户窗口3. 通信测试本文想要实现通过Modbus协议与Mcgs屏幕进行通信收发数据。在使用Mcgs屏幕进行Modbus通信时,一般Mcgs屏…