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




在这里插入图片描述



再谈操作系统与内核区


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访问该变量就还需要从内存中取出,这叫做保存内存的可见性


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

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

相关文章

LabVIEW双光子成像系统:自主创新,精准成像,赋能科研

双光子成像系统&#xff1a;自主创新&#xff0c;精准成像&#xff0c;赋能科研 第一部分&#xff1a;概述 双光子成像利用两个低能量光子同时激发荧光分子&#xff0c;具有深层穿透、高分辨率、低光损伤等优势。它能实现活体深层组织的成像&#xff0c;支持实时动态观察&…

「全网最细 + 实战源码案例」设计模式——策略模式

核心思想 享元模式&#xff08;Flyweight Pattern&#xff09;是一种行为型设计模式&#xff0c;用于定义一系列算法或策略&#xff0c;将它们封装成独立的类&#xff0c;并使它们可以相互替换&#xff0c;而不影响客户端的代码&#xff0c;提高代码的可维护性和扩展性。 结构…

安全策略实验

安全策略实验 1.拓扑图 2.需求分析 需求&#xff1a; 1.VLAN 2属于办公区&#xff0c;VLAN 3属于生产区 2.办公区PC在工作日时间&#xff08;周一至周五&#xff0c;早8到晚6&#xff09;可以正常访问OA server其他时间不允许 3.办公区PC可以在任意时刻访问Web Server 4.生产…

一文了解边缘计算

什么是边缘计算&#xff1f; 我们可以通过一个最简单的例子来理解它&#xff0c;它就像一个司令员&#xff0c;身在离炮火最近的前线&#xff0c;汇集现场所有的实时信息&#xff0c;经过分析并做出决策&#xff0c;及时果断而不拖延。 1.什么是边缘计算&#xff1f; 边缘计算…

对象的实例化、内存布局与访问定位

一、创建对象的方式 二、创建对象的步骤: 一、判断对象对应的类是否加载、链接、初始化: 虚拟机遇到一条new指令&#xff0c;首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用&#xff0c;并且检查这个符号引用代表的类是否已经被加载、解析和初始化…

Altium Designer绘制原理图时画斜线的方法

第一步&#xff1a;检查设置是否正确 打开preferences->PCB Editor ->Interactive Routing->Interactive Routing Options->Restrict TO 90/45去掉勾选项&#xff0c;点击OK即可。如下图所示&#xff1a; 然后在划线时&#xff0c;按下shift空格就能够切换划线…

【R语言】环境空间

一、环境空间的特点 环境空间是一种特殊类型的变量&#xff0c;它可以像其它变量一样被分配和操作&#xff0c;还可以以参数的形式传递给函数。 R语言中环境空间具有如下3个特点&#xff1a; 1、对象名称唯一性 此特点指的是在不同的环境空间中可以有同名的变量出现&#x…

NeuralCF 模型:神经网络协同过滤模型

实验和完整代码 完整代码实现和jupyter运行&#xff1a;https://github.com/Myolive-Lin/RecSys--deep-learning-recommendation-system/tree/main 引言 NeuralCF 模型由新加坡国立大学研究人员于 2017 年提出&#xff0c;其核心思想在于将传统协同过滤方法与深度学习技术相结…

【ChatGPT:开启人工智能新纪元】

一、ChatGPT 是什么 最近,ChatGPT 可是火得一塌糊涂,不管是在科技圈、媒体界,还是咱们普通人的日常聊天里,都能听到它的大名。好多人都在讨论,这 ChatGPT 到底是个啥 “神器”,能让大家这么着迷?今天咱就好好唠唠。 ChatGPT,全称是 Chat Generative Pre-trained Trans…

c++ 定点 new 及其汇编解释

&#xff08;1&#xff09; 代码距离&#xff1a; #include <new> // 需要包含这个头文件 #include <iostream>int main() {char buffer[sizeof(int)]; // 分配一个足够大的字符数组作为内存池int* p new(&buffer) int(42); // 使用 placement new…

Linux系统之whereis命令的基本使用

Linux系统之whereis命令的基本使用 一、whereis命令介绍二、whereis命令的使用帮助2.1 whereis命令的帮助信息2.2 whereis命令帮助解释 三、whereis命令的基本使用3.1 查找命令的位置3.2 仅查找二进制文件3.3 仅查找手册页3.4 输出实际使用的查找路径3.5 指定自定义搜索路径 四…

CH340G上传程序到ESP8266-01(S)模块

文章目录 概要ESP8266模块外形尺寸模块原理图模块引脚功能 CH340G模块外形及其引脚模块引脚功能USB TO TTL引脚 程序上传接线Arduino IDE 安装ESP8266开发板Arduino IDE 开发板上传失败上传成功 正常工作 概要 使用USB TO TTL&#xff08;CH340G&#xff09;将Arduino将程序上传…

整形的存储形式和浮点型在计算机中的存储形式

在计算机科学的底层世界里&#xff0c;数据存储是基石般的存在。不同数据类型&#xff0c;如整形与浮点型&#xff0c;其存储方式犹如独特的密码&#xff0c;隐藏着计算机高效运行的秘密。理解它们&#xff0c;是深入掌握编程与计算机原理的关键。 一、整形的存储形式 原码、反…

无人机PX4飞控 | PX4源码添加自定义uORB消息并保存到日志

PX4源码添加自定义uORB消息并保存到日志 0 前言 PX4的内部通信机制主要依赖于uORB&#xff08;Micro Object Request Broker&#xff09;&#xff0c;这是一种跨进程的通信机制&#xff0c;一种轻量级的中间件&#xff0c;用于在PX4飞控系统的各个模块之间进行高效的数据交换…

QMK启用摇杆和鼠标按键功能

虽然选择了触摸屏&#xff0c;我仍选择为机械键盘嵌入摇杆模块&#xff0c;这本质上是对"操作连续性"的执着。   值得深思的是&#xff0c;本次开发过程中借助DeepSeek的代码生成与逻辑推理&#xff0c;其展现的能力已然颠覆传统编程范式&#xff0c;需求描述可自动…

Rust 所有权特性详解

Rust 所有权特性详解 Rust 的所有权系统是其内存安全的核心机制之一。通过所有权规则&#xff0c;Rust 在编译时避免了常见的内存错误&#xff08;如空指针、数据竞争等&#xff09;。本文将从堆内存与栈内存、所有权规则、变量作用域、String 类型、内存分配、所有权移动、Cl…

基于深度学习的视觉检测小项目(十七) 用户管理后台的编程

完成了用户管理功能的阶段。下一阶段进入AI功能相关。所有的资源见文章链接。 补充完后台代码的用户管理界面代码&#xff1a; import sqlite3from PySide6.QtCore import Slot from PySide6.QtWidgets import QDialog, QMessageBoxfrom . import user_manage # 导入使用ui…

ubuntuCUDA安装

系列文章目录 移动硬盘制作Ubuntu系统盘 前言 根据前篇“移动硬盘制作Ubuntu系统盘”安装系统后&#xff0c;还不能够使用显卡。 如果需要使用显卡&#xff0c;还需要进行相关驱动的安装&#xff08;如使用的为Nvidia显卡&#xff0c;就需要安装相关的Nvidia显卡驱动&#xff…

Docker Compose的使用

文章首发于我的博客&#xff1a;https://blog.liuzijian.com/post/docker-compose.html 目录 Docker Compose是什么Docker Compose安装Docker Compose文件Docker Compose常用命令案例&#xff1a;部署WordPress博客系统 Docker Compose是什么 Docker Compose是Docker官方的开源…

【深度分析】DeepSeek 遭暴力破解,攻击 IP 均来自美国,造成影响有多大?有哪些好的防御措施?

技术铁幕下的暗战&#xff1a;当算力博弈演变为代码战争 一场针对中国AI独角兽的全球首例国家级密码爆破&#xff0c;揭开了数字时代技术博弈的残酷真相。DeepSeek服务器日志中持续跳动的美国IP地址&#xff0c;不仅是网络攻击的地理坐标&#xff0c;更是技术霸权对新兴挑战者的…