【Linux系统】信号:再谈OS与内核区、信号捕捉、重入函数与 volatile

news2025/2/5 19:26:23




在这里插入图片描述



再谈操作系统与内核区


1、浅谈虚拟机和操作系统映射于地址空间的作用


我们调用任何函数(无论是库函数还是系统调用),都是在各自进程的地址空间中执行的。无论操作系统如何切换进程,它都能确保访问同一个操作系统实例。换句话说,系统调用方法的执行实际上是在进程的地址空间内完成的。

基于这种机制,如果让不同进程同时映射到不同的操作系统实例,是否就可以实现在多个“系统”环境中运行?这与虚拟机的实现密切相关。

虚拟机主要分为两种——内核级虚拟机和用户级虚拟机。

  • 内核级虚拟机提供了硬件级别的资源隔离和环境模拟,允许在同一物理机器上运行多个操作系统实例。
  • 用户级虚拟机通常指的是那些不需要操作系统层面支持的应用层隔离方案,如Java虚拟机。

Linux中的Docker就是一个例子,它利用了Linux的命名空间和资源隔离技术来实现类似虚拟机的功能。通过这种映射,我们可以创建多个相互隔离的应用环境,从而更好地理解为什么需要进行这样的映射以及它可以带来哪些有趣的可能性。



2、访问内核区需要软件和硬件层面的支持


在这里插入图片描述


此外,不论是通过哪个进程的地址空间,最终访问的都是同一个内核操作系统,并且是通过软中断进入内核区操作的。在进入内核区之前,进程会从用户态转变为内核态。这个转变不仅需要软件层面的许可,还需要硬件的支持,比如CPU的当前特权级别(CPL,Current Privilege Level)。CPL 为 0 表示内核态,为 3 表示用户态,存储在CS段寄存器中,占用 2 比特位。只有当CPL从 3 变为 0 时,进程才能访问内核 [3,4]GB 的空间位置。

CPU内部的内存管理单元(MMU)负责将虚拟地址转换为物理地址。在用户态下,只能访问[0,3]GB的空间,无法直接访问内核区的[3,4]GB地址空间。因此,用户程序不能直接访问内核数据;而是通过触发软中断(例如使用int 0x80syscall指令)间接访问。这些指令会触发CPU的固定例程,执行完后恢复用户代码的执行上下文。

如果用户自己非法访问内核区代码,会触发访问内核内存的保护

  • 内存保护:当用户程序试图访问内核空间的内存(例如 [3, 4] GB 区域)时,MMU 会检测到这是一个无效的内存访问,并触发一个页面错误。
  • 异常处理:内核会捕获这个页面错误,并根据情况进行处理,通常会终止违规的用户进程并生成一个错误报告。

假设用户程序试图直接访问内核内存:

void *kernel_ptr = (void *)0xC0000000; // 假设这是内核空间的一个地址
*(int *)kernel_ptr = 42; // 尝试写入内核内存

在这个例子中,当程序执行到 *(int *)kernel_ptr = 42; 时,MMU 会检测到这是一个无效的内存访问,并触发一个页面错误。内核会捕获这个错误,终止该进程,并生成一个段错误(Segmentation Fault)。



3、Linux 的两种权限等级


具体可以看这篇文章:【Linux系统】CPU指令集 和 Linux系统权限 ring 0 / ring 3


Linux 中只有 0 和 3 两种权限等级,没有 1 和 2,那为什么CPU设计不用 1 个比特位表示就好?

因为 Linux 系统如此,不代表其他系统,其它系统需要使用 1 和 2,就要留着,空间设计成 2 比特位大小

很多时候,这些“奇怪不统一”的设计,一般都是为了兼容不同系统等其他需求

拓展:

现代 CPU 通常有多个特权级别(也称为环或模式)。常见的特权级别有:

  • Ring 0:最高特权级别,内核模式。操作系统内核代码在这里运行,可以访问所有硬件资源和内存。
  • Ring 3:最低特权级别,用户模式。用户程序在这里运行,只能访问分配给它的内存和有限的硬件资源。


4、操作系统如何做到定期检查并刷新缓冲区?

操作系统通过创建特定的进程或线程来执行诸如定期检查和刷新缓冲区这样的固定任务。这些进程或线程专门用于处理一些系统级的维护工作,确保系统的高效运行。

  • 内核固定例程:这类例程包括了用于执行刷新缓冲区等操作的进程或线程。它们是操作系统为了完成某些周期性或持续性的任务而设置的,比如刷新文件系统的缓冲区以确保数据的一致性和最新状态。

此外,操作系统还会安排特定的进程或线程来定期检查定时器是否超时。这种机制对于实现延迟执行、轮询或其他基于时间的操作至关重要。

  • 定时器检查例程:这是另一类内核固定例程,专注于检测定时器是否已经到达预设的时间点。这有助于触发事件、执行预定的任务或者进行其他需要定时执行的操作。

在这些场景中,操作系统扮演的角色主要是调度这些固定例程的进程或线程,确保它们能够按时执行所需的任务而不干扰到其他用户进程的正常运行。通过这种方式,操作系统不仅保证了内部管理任务的有效执行,还维护了整个系统的稳定性和效率。



再谈细节:操作系统处理自定义信号


1、信号捕捉方法执行的时候,为什么还要做权限切换?直接内核权限执行不可以吗??


在这里插入图片描述



信号捕捉的方法是用户自定义的,如果允许以内核的权限执行这个方法,

这个方法里万一有:删除用户、删除 root的配置文件、非法赋权等非法操作指令怎么办

我们对应的操作系统不就助纣为虐了吗,岂不是会让某些用户钻漏洞,基于信号捕捉来利用操作性的内核的这种权限优先级更高的这种特性

因此如果不转回用户态执行用户自定义信号处理函数,则会有严重的安全风险



这些删除用户、删除 root的配置文件、非法赋权等操作指令,用户自己可以通过一些开放的允许的操作接口达到目的,这样只会影响到操作的用户本身,而不会影响整个系统的其他用户

这样达到了保护其他用户的目的



2、信号处理完,只能从内核返回:信号自定义处理函数执行完了,直接切换到同为用户态的 main 执行的主程序不行吗,为什么还要再切换回内核

答:若想从一个函数执行完毕返回到另一个函数,这两个函数必须曾经要有调用关系

因为只有我调你时,形成函数栈帧,主调用函数会把被调用函数所对应的返回值地址代码的地址入栈,将来调完再弹栈,就能回到原函数,而当前的 main函数和 信号自定义处理函数这 2 个函数现在有调用关系吗?答案是根本就没有

信号自定义处理函数是被内核中信号处理的相关程序所调用的,因此在信号自定义处理函数运行完,就需要回到内核的程序调用处,再从内核态回到用户态



3、回到 main 主程序,如何知道从哪条程序语句继续执行

PC 指针保存着下条指令地址,当中断陷入内核前就已经将PC指针的值作为上下文保护起来了




信号捕捉的补充


1、系统调用 sigaction


在这里插入图片描述



这个系统调用和 signal 差不多,只是这个功能更加丰富一点(wait/waitpid 的关系一样)


在这里插入图片描述




该结构体内

  • 第一个属性 void(*sa handler)(int) :指向我们信号的自定义函数 Handler
  • 第二个属性:用于处理实时信号,不用管,
  • 第三个属性 sa_mask :后面会讲解
  • 第四个属性 sa_flags : 一般置为 0
  • 最后一个属性也不要管


使用该代码:就直接当作 signal 函数使用即可,只是稍稍使用形式上不同

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


void Handler(int signum)
{
    std::cout << "get a signal : " << signum << '\n';
    exit(1);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = Handler;
    ::sigaction(2, &act, &oact);

    while(true)
    {
        pause();
    }
    return 0;
}


运行结果如下:


在这里插入图片描述




2、问题:信号是否会被嵌套处理?


问题:处理信号期间,有没有可能进行陷入内核呢?

答:有可能!因为信号自定义函数里面也可以调用系统调用


问题:同一信号会被嵌套处理吗?

当在自定义信号处理函数中处理信号期间,若有相同的信号出现,是否会触发中断,重新开始执行一轮该信号的自定义信号处理函数,导致形成一种嵌套递归呢?

如果此时有其他不同的信号产生,是否会嵌套进行多信号的自定义处理呢??

都不会,在一个信号处理期间,OS会自动的把所有后续产生的信号的 block 位设置为 1,以致于保证一个信号的完整处理完成

信号处理完成,会自动解除对其他信号的block位限制,然后就按顺序串行处理这些信号



证明:信号不会嵌套处理

代码如下:

自定义处理函数循环不停,该函数运行期间,我们不断键盘输入:ctrl+c ,发送 2 号信号


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


void Handler(int signum)
{
    static int cnt = 0;
    cnt ++;  // 每次处理信号,cnt 自增一次,用此证明是否会信号嵌套处理
    while(true)
    {
        std::cout << "get a signal : " << signum << ", cnt: " << cnt << '\n';
        sleep(1);
    }
    exit(1);
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = Handler;

    ::sigaction(2, &act, &oact);

    while(true)
    {
        pause();
    }
    return 0;
}


运行结果如下:


在这里插入图片描述




可以发现,计数器 cnt 一直为 1 不变,证明了连续发送同一信号不会造成嵌套

同理,发送其他不同信号,也不会立刻被处理的(可以自己试试)


这一切的底层原理:操作系统会在一个信号处理期间,将后续的信号全都在 Block 中屏蔽掉

使得后续信号不会立即被处理



证明:其原理

1、当 2 号信号在处理时,循环打印当前进程的 Block 表

#include<iostream>
#include <unistd.h> // 提供sleep函数和pause函数
#include <signal.h> // 提供信号处理相关函数和结构体

// 打印当前进程屏蔽信号集中的阻塞信号
void PrintBlock()
{
    sigset_t set, oldset; // 定义两个信号集,一个用于设置,另一个用于保存旧的状态
    sigemptyset(&set); // 清空信号集set
    sigemptyset(&oldset); // 清空信号集oldset

    std::cout << "Block: "; // 打印提示信息
    for(int signum = 31; signum >= 1; --signum) // 遍历信号编号从31到1
    {
        sigprocmask(SIG_BLOCK, &set, &oldset); // 获取当前进程的信号屏蔽字,并将其存储在oldset中
        int ret = sigismember(&oldset, signum); // 检查信号signum是否在oldset中
        if(ret != -1) // 如果检查成功(即ret不是错误码)
            std::cout << ret; // 打印结果,1表示该信号被阻塞,0表示未被阻塞
    }
    std::cout << '\n'; // 打印换行符
}

// 信号处理函数
void Handler(int signum)
{
    std::cout << "get a signal : " << signum << '\n'; // 打印接收到的信号编号

    while(true)
    {
        PrintBlock(); // 调用PrintBlock函数打印当前进程的信号屏蔽状态
        sleep(1); // 线程睡眠1秒
    }

    //exit(1); // 注释掉的退出语句
}

int main()
{
    struct sigaction act, oact; // 定义信号行为结构体变量
    act.sa_handler = Handler; // 设置信号处理函数为Handler

    ::sigaction(2, &act, &oact); // 修改信号2(SIGINT,通常是Ctrl+C产生的中断信号)的行为

    while(true)
    {
        pause(); // 暂停执行,等待信号的到来
    }
    return 0;
}


运行结果如下:


在这里插入图片描述


如图,OS 将 2 号 3 号信号都给屏蔽了(至于为什么 3 号也被屏蔽了,后面解释)



2、当 2 号信号处理完后,即信号处理结束后,打印当前进程的 Block 表

我将该结束后打印语句 PrintBlock() 放到 main 函数内部的循环中

while(true)
{
    PrintBlock();
    pause();
}


完整代码: 我去掉了 2 号信号自定义处理函数中的循环,为了让处理函数能退出

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


void PrintBlock()
{
    // 循环打印本进程的 Block 表
    //int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

    sigset_t set, oldset;
    sigemptyset(&set);
    sigemptyset(&oldset);


    std::cout << "Block: ";
    for(int signum = 31; signum >= 1; --signum) 
    {
        sigprocmask(SIG_BLOCK, &set, &oldset);
        int ret = sigismember(&oldset, signum);
        if(ret != -1)
            std::cout << ret;
    }
    std::cout << '\n';
}


void Handler(int signum)
{
    std::cout << "get a signal : " << signum << '\n';

    PrintBlock();
    sleep(2);

    //exit(1);
}




int main()
{
    struct sigaction act, oact;
    act.sa_handler = Handler;

    ::sigaction(2, &act, &oact);

    while(true)
    {
        PrintBlock();
        pause();
    }
    return 0;
}


运行结果如下:


在这里插入图片描述



效果很明显了吧!




3、struct sigactionsa_mask


在这里插入图片描述



我们使用代码打印出来看看,看一下默认创建的 struct sigaction ,其中的 sa_mask 会是什么样子的:


代码如下:

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



void Handler(int signum)
{
    std::cout << "get a signal : " << signum << '\n';

    //PrintBlock();
    sleep(2);

    //exit(1);
}




int main()
{
    struct sigaction act, oact;
    act.sa_handler = Handler;

    
    // 查看一下默认设置的屏蔽信号:发现确实默认在一个信号处理阶段,不能再收到 2 和 3 号信号
    std::cout << "sa_mask: ";
    for(int signum = 31; signum >= 1; --signum) 
    {
        int ret = sigismember(&act.sa_mask, signum);
        if(ret != -1)
            std::cout << ret;
    }
    std::cout << '\n';
    

    ::sigaction(2, &act, &oact);

    while(true)
    {
        pause();
    }
    return 0;
}


运行结果如下:


在这里插入图片描述



这串信号的 01 你是否觉得似曾相识,这个不就和前面测试:信号处理期间,系统默认在 Block 中屏蔽某些信号,而我们前面打印出来的 Block 表,刚好屏蔽了 2 号和 3 号!!

直说:这个属性就是使用 struct sigaction 来自定义处理某个信号时,设置在该信号处理期间,默认需要屏蔽的信号



如果想要屏蔽其他信号,也可以自己手动设置:

代码如下:打印默认的和设置后的 sa_mask 值,ctrl+c 发送 2 号信号,打印 block

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


void PrintBlock()
{
    // 循环打印本进程的 Block 表
    //int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

    sigset_t set, oldset;
    sigemptyset(&set);
    sigemptyset(&oldset);


    std::cout << "Block: ";
    for(int signum = 31; signum >= 1; --signum) 
    {
        sigprocmask(SIG_BLOCK, &set, &oldset);
        int ret = sigismember(&oldset, signum);
        if(ret != -1)
            std::cout << ret;
    }
    std::cout << '\n';
}


void Handler(int signum)
{
    std::cout << "get a signal : " << signum << '\n';

    PrintBlock();
    sleep(2);

    //exit(1);
}




int main()
{
    struct sigaction act, oact;
    act.sa_handler = Handler;

    
    // 查看一下默认设置的屏蔽信号:发现确实默认在一个信号处理阶段,不能再收到 2 和 3 号信号
    std::cout << "设置前默认的 sa_mask: ";
    for(int signum = 31; signum >= 1; --signum) 
    {
        int ret = sigismember(&act.sa_mask, signum);
        if(ret != -1)
            std::cout << ret;
    }
    std::cout << '\n';


    // 手动设置 sa_mask
    sigset_t myset;
    // int sigaddset(sigset_t *set, int signum);
    sigaddset(&myset, 3);
    sigaddset(&myset, 4);
    sigaddset(&myset, 5);
    sigaddset(&myset, 6);
    sigaddset(&myset, 7);

    act.sa_mask = myset;  //设置到 2 号信号的 sa_mask 中


    std::cout << "设置后的 sa_mask: ";
    for(int signum = 31; signum >= 1; --signum) 
    {
        int ret = sigismember(&act.sa_mask, signum);
        if(ret != -1)
            std::cout << ret;
    }
    std::cout << '\n';
    

    ::sigaction(2, &act, &oact);

    while(true)
    {
        //PrintBlock();
        pause();
    }
    return 0;
}


运行结果如下:


在这里插入图片描述




4、问题:处理信号时,Pending是在处理信号之前就置为0,还是处理信号完成后才置为0


答:Pending是在处理信号之前就置为0,

1、从定义来看,Pending的意思为信号未决,即未被处理的信号,如果信号处理完成,岂不是处于pending表的这个信号定义不确定了:处理完了,还算做未被处理的pending吗???

2、从作用来看,这个也是根本原因,当你处理pending表的某个信号,在该信号处理期间,用户可能再向该进程发送相同编号的信号,此时如果 pending表的信号没有置为 0,那用户如何清楚这个信号是旧信号还是新信号?



证明阶段:打印pending表

代码如下:在 2 号信号的自定义处理函数中打印当前进程的信号 Pending表

void PrintPending()
{
    // 循环打印本进程的 Pending 表
    //int sigpending(sigset_t *set);

    sigset_t set;
    sigemptyset(&set);


    std::cout << "Block: ";
    for(int signum = 31; signum >= 1; --signum) 
    {
        sigpending(&set);
        int ret = sigismember(&set, signum);
        if(ret != -1)
            std::cout << ret;
    }
    std::cout << '\n';
}


void Handler(int signum)
{
    std::cout << "get a signal : " << signum << '\n';

    std::cout<<"开始处理2号信号的 pending: ";
    PrintPending();

    sleep(2);

    //exit(1);
}




int main()
{

    struct sigaction act, oact;
    act.sa_handler = Handler;


    ::sigaction(2, &act, &oact);


    while(true)
    {
        pause();
    }
    return 0;
}


运行结果如下:

可以发现,2 号信号的 pending位置已经被置为 0 了,说明根本不是在处理信号后才做处理


在这里插入图片描述





重入函数

这个情况我们不做演示,这种情况概率太低,暂时是做不出来的,


什么样的函数算作 :不可重入函数 和 可重入函数


不可重入函数

当该函数中使用一些全局的资源,如某些全局数据结构(全局链表、全局数组、全局红黑树…)

就是调一次这个函数,数据变化会随着你的调用而变化。

可重入函数

当该函数定义和使用的都是函数内的局部变量和数据,每次调用该函数都会为该函数创建一份单独的函数栈帧空间,则不同执行流重复调用该函数,互不干扰

但是要保证不能传同一个参数

可重入函数可以被中断并在相同的线程或者其他线程中重新进入(包括递归调用),而不会导致任何数据不一致或其他问题。这种特性对于编写并发程序非常重要

为了确保函数的可重入性,通常需要注意以下几点:

  1. 使用局部变量:函数内部使用的变量应该是局部变量,这样每次调用函数时都会为这些变量分配新的栈空间,不会影响其他调用。
  2. 避免全局变量和静态变量:全局变量和静态变量在多次调用之间会保持其值,这可能导致线程安全问题。
  3. 避免使用非可重入函数:如果函数内部调用了其他非可重入函数,那么整个函数也会变得不可重入。
  4. 参数传递:传入的参数应该是独立的,不能是共享的数据结构,除非这些数据结构本身是线程安全的。


不可重入函数 和 可重入函数一定是优点或缺点吗?

这两者仅仅是一种特性,没有好坏之分

内向不是缺点,这是人的一种特点,内向的人也有适合做的事情,没有优缺之分


可重入函数的例子

像是这种函数名带 _r 的基本都是可重入函数(系统设计好的)


在这里插入图片描述




volatile


这是C语言的关键字,平时比较少用,但是需要了解一下

1、演示不可重入函数的现象

代码:这段代码中存在两个执行流(一个是 main函数的循环,一个是 信号处理函数),当接收到信号2 时,执行自定义信号处理函数,在自定义信号处理这个执行流中,将全局变量变成 1,使得 main 主执行流中的 while(!flag) {}; 退出

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

int flag = 0;

void handler(int signum)
{
    printf("get a signal, signum: %d\n", signum);
    flag = 1;
}

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


    while(!flag) {};
    printf("我正常退出了\n");
}


在这里插入图片描述




2、编译器优化:O1、O2

CPU运行这段代码,CPU内部存在两种运算:算术运算和逻辑运算

逻辑运算就是判断真与假相关逻辑

执行这句代码 while(!flag) {}; ,CPU需要不断循环的先从内存中取到 flag 的值,再在CPU中做逻辑运算这两步


我们可以为代码添加优化:如 O1优化、 O2优化、

现代编译器如 GCC 和 Clang 提供了多种优化级别,这些优化级别可以帮助编译器生成更高效的机器码。下面是这些优化级别的简要介绍和一些常见的使用场景:

优化级别

  1. -O0(默认)
    • 不进行任何优化,编译速度快,调试信息完整。
    • 适用于开发和调试阶段。
  2. -O1
    • 启用基本的优化,包括常量折叠、死代码消除、简单的指令调度等。
    • 平衡了编译时间和代码性能,适合快速构建和测试。
  3. -O2
    • 启用更多的优化,包括函数内联、循环优化、更复杂的指令调度等。
    • 在大多数情况下,这是推荐的优化级别,因为它提供了较好的性能提升而不会显著增加编译时间。
  4. -O3
    • 启用所有可用的优化,包括激进的函数内联、循环展开、向量化等。
    • 可能会增加编译时间和二进制文件的大小,但通常能提供最佳的性能。
    • 适用于性能要求极高的应用。


编译器在启用优化(如 -O1 及更高级别)时,会尝试将常量或很少变化的变量优化为寄存器变量,以减少内存访问的开销

O1优化开始,编译器会为代码添加各种优化,其中会将一些常量或整个程序中不变的量直接设置成寄存器数据,相当于使用 register 修饰该数据,表示既然你这个量不会变,干脆将其直接设置到寄存器中,这样在访问某个变量时,就不用频繁的访问内存获取该数据,如 while(!flag) {}; ,不用频繁的访问内存获取 flag,相当于直接和编译器说这个量我不常改动,你固定使用第一次定义的值即可,就不会去内存中获取了

这说明如果开启这个优化,你在程序中修改某个变量,编译器可能不会使用更新后的量



3、volatile 的作用


volatile 作用: 意思是可变的,保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

说白了: 前面的编译器优化会将某些变量优化至寄存器中,让程序无需多次访问内存取数据,而这个 volatile 的作用就是不允许让编译器对该变量的优化



如果没有使用 volatile 关键字,编译器在启用优化(如 -O1 及更高级别)时,可能会将 flag 的值优化为寄存器中的值,从而导致 while 循环变成死循环。这是因为编译器假设 flag 的值在 while 循环中不会发生变化,因此会将 flag 的值加载到寄存器中,并在每次循环迭代中使用寄存器中的值,而不是重新从内存中读取。


当变量使用 关键字 volatile 修饰时,表示该变量我可能会修改他,编译器不能将其优化成寄存器变量,就不会出现开启编译器优化时导致的该变量被优化进入寄存器的情况

volatile int flag = 0;

CPU访问该变量就还需要从内存中取出,这叫做保存内存的可见性


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

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

相关文章

自定义数据集 使用paddlepaddle框架实现逻辑回归

导入必要的库 import numpy as np import paddle import paddle.nn as nn 数据准备&#xff1a; seed1 paddle.seed(seed)# 1.散点输入 定义输入数据 data [[-0.5, 7.7], [1.8, 98.5], [0.9, 57.8], [0.4, 39.2], [-1.4, -15.7], [-1.4, -37.3], [-1.8, -49.1], [1.5, 75.6…

LabVIEW图片识别逆向建模系统

本文介绍了一个基于LabVIEW的图片识别逆向建模系统的开发过程。系统利用LabVIEW的强大视觉处理功能&#xff0c;通过二维图片快速生成对应的三维模型&#xff0c;不仅降低了逆向建模的技术门槛&#xff0c;还大幅提升了建模效率。 ​ 项目背景 在传统的逆向建模过程中&#xf…

MySQL(高级特性篇) 13 章——事务基础知识

一、数据库事务概述 事务是数据库区别于文件系统的重要特性之一 &#xff08;1&#xff09;存储引擎支持情况 SHOW ENGINES命令来查看当前MySQL支持的存储引擎都有哪些&#xff0c;以及这些存储引擎是否支持事务能看出在MySQL中&#xff0c;只有InnoDB是支持事务的 &#x…

前端进阶:深度剖析预解析机制

一、预解析是什么&#xff1f; 在前端开发中&#xff0c;我们常常会遇到一些看似不符合常规逻辑的代码执行现象&#xff0c;比如为什么在变量声明之前访问它&#xff0c;得到的结果是undefined&#xff0c;而不是报错&#xff1f;为什么函数在声明之前就可以被调用&#xff1f…

【力扣】53.最大子数组和

AC截图 题目 思路 这道题主要考虑的就是要排除负数带来的负面影响。如果遍历数组&#xff0c;那么应该有如下关系式&#xff1a; currentAns max(prenums[i],nums[i]) pre是之前记录的最大和&#xff0c;如果prenums[i]小于nums[i]&#xff0c;就要考虑舍弃pre&#xff0c;从…

基于Spring Security 6的OAuth2 系列之七 - 授权服务器--自定义数据库客户端信息

之所以想写这一系列&#xff0c;是因为之前工作过程中使用Spring Security OAuth2搭建了网关和授权服务器&#xff0c;但当时基于spring-boot 2.3.x&#xff0c;其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0&#xff0c;结果一看Spring Security也升级…

vim-plug的自动安装与基本使用介绍

vim-plug介绍 Vim-plug 是一个轻量级的 Vim 插件管理器&#xff0c;它允许你轻松地管理 Vim 插件的安装、更新和卸载。相较于其他插件管理器&#xff0c;vim-plug 的优点是简单易用&#xff0c;速度较快&#xff0c;而且支持懒加载插件&#xff08;即按需加载&#xff09; 自动…

Deep Crossing:深度交叉网络在推荐系统中的应用

实验和完整代码 完整代码实现和jupyter运行&#xff1a;https://github.com/Myolive-Lin/RecSys--deep-learning-recommendation-system/tree/main 引言 在机器学习和深度学习领域&#xff0c;特征工程一直是一个关键步骤&#xff0c;尤其是对于大规模的推荐系统和广告点击率预…

想品客老师的第十天:类

类是一个优化js面向对象的工具 类的声明 //1、class User{}console.log(typeof User)//function//2、let Hdclass{}//其实跟1差不多class Stu{show(){}//注意这里不用加逗号&#xff0c;对象才加逗号get(){console.log(后盾人)}}let hdnew Stu()hd.get()//后盾人 类的原理 类…

MyBatis-Plus速成指南:条件构造器和常用接口

Wrapper 介绍 Wrapper&#xff1a;条件构造抽象类&#xff0c;最顶端父类 AbstractWrapper&#xff1a;用于查询条件封装&#xff0c;生成 SQL 的 where 条件QueryWrapper&#xff1a;查询条件封装UpdateWrapper&#xff1a;Update 条件封装AbstractLambdaWrapper&#xff1a;使…

(脚本学习)BUU18 [CISCN2019 华北赛区 Day2 Web1]Hack World1

自用 题目 考虑是不是布尔盲注&#xff0c;如何测试&#xff1a;用"1^1^11 1^0^10&#xff0c;就像是真真真等于真&#xff0c;真假真等于假"这个测试 SQL布尔盲注脚本1 import requestsurl "http://8e4a9bf2-c055-4680-91fd-5b969ebc209e.node5.buuoj.cn…

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】2.25 多线程并行:GIL绕过与真正并发

2.25 多线程并行&#xff1a;GIL绕过与真正并发 目录 #mermaid-svg-JO4lsTIyjOweVkos {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JO4lsTIyjOweVkos .error-icon{fill:#552222;}#mermaid-svg-JO4lsTIyjOweVkos …

Java 大视界 -- Java 大数据在智能医疗影像诊断中的应用(72)

💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也期待你毫无保留地分享独特见解,愿我们于此携手成长,共赴新程!💖 一、…

【Leetcode刷题记录】1456. 定长子串中元音的最大数目---定长滑动窗口即解题思路总结

1456. 定长子串中元音的最大数目 给你字符串 s 和整数 k 。请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。 英文中的 元音字母 为&#xff08;a, e, i, o, u&#xff09;。 这道题的暴力求解的思路是通过遍历字符串 s 的每一个长度为 k 的子串&#xf…

upload-labs安装与配置

前言 作者进行upload-labs靶场练习时&#xff0c;在环境上出了很多问题&#xff0c;吃了很多苦头&#xff0c;甚至改了很多配置也没有成功。 upload-labs很多操作都是旧时代的产物了&#xff0c;配置普遍都比较老&#xff0c;比如PHP版本用5.2.17&#xff08;还有中间件等&am…

从Transformer到世界模型:AGI核心架构演进

文章目录 引言:架构革命推动AGI进化一、Transformer:重新定义序列建模1.1 注意力机制的革命性突破1.2 从NLP到跨模态演进1.3 规模扩展的黄金定律二、通向世界模型的关键跃迁2.1 从语言模型到认知架构2.2 世界模型的核心特征2.3 混合架构的突破三、构建世界模型的技术路径3.1 …

每日一博 - 三高系统架构设计:高性能、高并发、高可用性解析

文章目录 引言一、高性能篇1.1 高性能的核心意义1.2 影响系统性能的因素1.3 高性能优化方法论1.3.1 读优化&#xff1a;缓存与数据库的结合1.3.2 写优化&#xff1a;异步化处理 1.4 高性能优化实践1.4.1 本地缓存 vs 分布式缓存1.4.2 数据库优化 二、高并发篇2.1 高并发的核心意…

【工欲善其事】利用 DeepSeek 实现复杂 Git 操作:从原项目剥离出子版本树并同步到新的代码库中

文章目录 利用 DeepSeek 实现复杂 Git 操作1 背景介绍2 需求描述3 思路分析4 实现过程4.1 第一次需求确认4.2 第二次需求确认4.3 第三次需求确认4.4 V3 模型&#xff1a;中间结果的处理4.5 方案验证&#xff0c;首战告捷 5 总结复盘 利用 DeepSeek 实现复杂 Git 操作 1 背景介绍…

【C++】线程池实现

目录 一、线程池简介线程池的核心组件实现步骤 二、C11实现线程池源码 三、线程池源码解析1. 成员变量2. 构造函数2.1 线程初始化2.2 工作线程逻辑 3. 任务提交(enqueue方法)3.1 方法签名3.2 任务封装3.3 任务入队 4. 析构函数4.1 停机控制 5. 关键技术点解析5.1 完美转发实现5…

数据结构实战之线性表(三)

目录 1.顺序表释放 2.顺序表增加空间 3.合并顺序表 4.线性表之链表实现 1.项目结构以及初始代码 2.初始化链表(不带头结点) 3.链表尾部插入数据并显示 4.链表头部插入数据 5.初始化链表&#xff08;带头结点&#xff09; 6.带头结点的链表头部插入数据并显示 7.带头结…