【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); // 清空信号集setsigemptyset(&oldset); // 清空信号集oldsetstd::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_masksigset_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.rhkb.cn/news/12424.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

冰蝎v4.0.5 来啦

webshell始终是渗透测试的热门&#xff0c;上次护网写冰蝎检测规则&#xff0c;加密流量&#xff0c;有点压力&#xff0c;今天终于有空来复现一下&#xff0c;我知道玩知乎的大佬很多&#xff0c;轻一点喷&#xff0c;学习新知识不丢人&#xff5e; ailx10 1949 次咨询 4.9 …

WPS怎么使用latex公式?

1、下载并安装mathtype https://blog.csdn.net/weixin_43135178/article/details/125143654?sharetypeblogdetail&sharerId125143654&sharereferPC&sharesourceweixin_43135178&spm1011.2480.3001.8118 2、将mathtype嵌入在WPS MathType面板嵌入器,免费工具…

基于微信小程序的私家车位共享系统设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

安全策略配置

需求: 1、VLAN 2属于办公区;VLAN 3属于生产区 2、办公区PC在工作日时间(周一至周五&#xff0c;早8到晚6)可以正常访问0A Server&#xff0c;其他时间不允许 3、办公区PC可以在任意时刻访问web server 4、生产区PC可以在任意时刻访问0A Server&#xff0c;但是不能访问Web serv…

【大数据技术】教程05:本机DataGrip远程连接虚拟机MySQL/Hive

本机DataGrip远程连接虚拟机MySQL/Hive datagrip-2024.3.4VMware Workstation Pro 16CentOS-Stream-10-latest-x86_64-dvd1.iso写在前面 本文主要介绍如何使用本机的DataGrip连接虚拟机的MySQL数据库和Hive数据库,提高编程效率。 安装DataGrip 请按照以下步骤安装DataGrip软…

响应式编程_01基本概念:前世今生

文章目录 引言响应式编程的技术优势全栈式响应式编程从传统开发模式到异步执行技术Web 请求与 I/O 模型异步调用的实现技术回调Future机制 响应式编程实现方法观察者模式发布-订阅模式数据流与响应式 响应式宣言和响应式系统 引言 大流量、高并发的访问请求的项目&#xff0c;…

龙芯+FreeRTOS+LVGL实战笔记(新)——16数码管驱动

本专栏是笔者另一个专栏《龙芯+RT-Thread+LVGL实战笔记》的姊妹篇,主要的区别在于实时操作系统的不同,章节的安排和任务的推进保持一致,并对源码做了完善与优化,各位可以前往本人在B站的视频合集(图1所示)观看所有演示视频,合集首个视频链接为: https://www.bilibili.…

正态分布和标准正态分布区别与联系(复习)

1)区别&#xff1a;正态分布的平均数为μ&#xff0c;标准差为σ&#xff1b;不同的正态分布可能有不同的μ值和σ值&#xff0c;正态分布曲线形态因此不同。 标准正态分布平均数μ0&#xff0c;标准差σ1&#xff0c;μ和σ都是固定值&#xff1b;标准正态分布曲线形态固定。…

Airflow:深入理解Apache Airflow Task

Apache Airflow是一个开源工作流管理平台&#xff0c;支持以编程方式编写、调度和监控工作流。由于其灵活性、可扩展性和强大的社区支持&#xff0c;它已迅速成为编排复杂数据管道的首选工具。在这篇博文中&#xff0c;我们将深入研究Apache Airflow 中的任务概念&#xff0c;探…

Golang 并发机制-5:详解syn包同步原语

并发性是现代软件开发的一个基本方面&#xff0c;Go&#xff08;也称为Golang&#xff09;为并发编程提供了一组健壮的工具。Go语言中用于管理并发性的重要包之一是“sync”包。在本文中&#xff0c;我们将概述“sync”包&#xff0c;并深入研究其最重要的同步原语之一&#xf…

走向基于大语言模型的新一代推荐系统:综述与展望

HightLight 论文题目&#xff1a;Towards Next-Generation LLM-based Recommender Systems: A Survey and Beyond作者机构&#xff1a;吉林大学、香港理工大学、悉尼科技大学、Meta AI论文地址&#xff1a; https://arxiv.org/abs/2410.1974 基于大语言模型的下一代推荐系统&…

LabVIEW微位移平台位移控制系统

本文介绍了基于LabVIEW的微位移平台位移控制系统的研究。通过设计一个闭环控制系统&#xff0c;针对微位移平台的通信驱动问题进行了解决&#xff0c;并提出了一种LabVIEW的应用方案&#xff0c;用于监控和控制微位移平台的位移&#xff0c;从而提高系统的精度和稳定性。 项目背…

list容器(详解)

list的介绍及使用&#xff08;了解&#xff0c;后边细讲&#xff09; 1.1 list的介绍&#xff08;双向循环链表&#xff09; https://cplusplus.com/reference/list/list/?kwlist&#xff08;list文档介绍&#xff09; 1. list是可以在常数范围内在任意位置进行插入和删除的序…

昆仑万维Java开发面试题及参考答案

进程和线程的区别是什么? 进程和线程都是操作系统中非常重要的概念,它们在多个方面存在显著的区别。 从定义上看,进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间,包括代码段、数据段、堆栈段等。例如,当你在电脑上同时打开浏览器和音乐播放…

系统学习算法:专题九 穷举vs暴搜vs深搜vs回溯vs剪枝

其中标题的深搜&#xff0c;回溯&#xff0c;剪枝我们之前专题都已经有过学习和了解&#xff0c;这里多了两个穷举和暴搜&#xff0c;其实意思都差不多&#xff0c;穷举就是穷尽力气将所有情况都列举出来&#xff0c;暴搜就是暴力地去一个一个情况搜索&#xff0c;所以就是全部…

人类心智逆向工程:AGI的认知科学基础

文章目录 引言:为何需要逆向工程人类心智?一、逆向工程的定义与目标1.1 什么是逆向工程?1.2 AGI逆向工程的核心目标二、认知科学的四大支柱与AGI2.1 神经科学:大脑的硬件解剖2.2 心理学:心智的行为建模2.3 语言学:符号与意义的桥梁2.4 哲学:意识与自我模型的争议三、逆向…

VLAN 基础 | 不同 VLAN 间通信实验

注&#xff1a;本文为 “ Vlan 间通信” 相关文章合辑。 英文引文&#xff0c;机翻未校。 图片清晰度限于原文图源状态。 未整理去重。 How to Establish Communications between VLANs? 如何在 VLAN 之间建立通信&#xff1f; Posted on November 20, 2015 by RouterSwi…

渗透测试之文件包含漏洞 超详细的文件包含漏洞文章

目录 说明 通常分为两种类型&#xff1a; 本地文件包含 典型的攻击方式1&#xff1a; 影响&#xff1a; 典型的攻击方式2&#xff1a; 包含路径解释&#xff1a; 日志包含漏洞&#xff1a; 操作原理 包含漏洞读取文件 文件包含漏洞远程代码执行漏洞: 远程文件包含…

蓝桥杯更小的数(区间DP)

题目描述 小蓝有一个长度均为 n 且仅由数字字符 0 ∼ 9 组成的字符串&#xff0c;下标从 0 到 n − 1&#xff0c;你可以将其视作是一个具有 n 位的十进制数字 num&#xff0c;小蓝可以从 num 中选出一段连续的子串并将子串进行反转&#xff0c;最多反转一次。小蓝想要将选出的…

洛谷 P1387 最大正方形 C语言

题目描述 在一个 n m 的只包含 0 和 1 的矩阵里找出一个不包含 0 的最大正方形&#xff0c;输出边长。 输入格式 输入文件第一行为两个整数 n, m (1 ≤ n, m ≤ 100)&#xff0c;接下来 n 行&#xff0c;每行 m 个数字&#xff0c;用空格隔开&#xff0c;0 或 1。 输出格式 …