【Linux系统编程】:信号(4)——信号的处理

1.进程地址空间内核区

我们之前都是谈进程地址空间的用户区,接下来我们谈谈内核区。
进程地址空间中的内核区是操作系统内核在进程地址空间中所占据的特定区域。在这里插入图片描述

  • 一般情况下,一个操作系统只有一个内核区以及一个内核级页表。而进程可以有多个页表。
  • 不论进程怎么切换,进程中的内核区数据都是相同的(当然,可能由于权限问题看到的东西不同)
  • 进程调用系统接口,就是调用内核区的代码和数据,相当于在自己的进程地址空间调用。
  • 操作系统启动后,每时每刻都有进程在执行。所以用户可以调用系统接口并利用系统接口随时访问(前提有权限)操作系统的代码和数据。

内核态和用户态的切换过程:

  1. 保存现场:无论是通过系统调用、硬件中断还是异常触发切换,首先要做的就是保存当前用户态程序的执行上下文,包括程序计数器(PC)、寄存器状态、栈指针等,以便在切换回用户态时能够恢复程序的执行。
  2. 切换模式:CPU 通过修改其状态寄存器中的模式位,将当前执行模式从用户态切换到内核态。
  3. 跳转执行:根据不同的触发方式,CPU 会跳转到相应的内核代码入口点执行。对于系统调用,会跳转到系统调用处理程序入口;对于硬件中断,会根据中断向量表找到对应的中断处理程序入口;对于异常,会跳转到相应的异常处理程序入口。
  4. 执行内核代码:在内核态下,操作系统内核执行相应的代码来处理系统调用请求、中断事件或异常情况。在执行过程中,内核可以访问和操作系统的所有资源,包括硬件设备、内核数据结构等。
  5. 恢复现场:当内核代码执行完毕后,需要将之前保存的用户态程序的执行上下文恢复,包括将寄存器的值还原、栈指针复位、程序计数器指向原来的下一条指令等。然后,CPU 将执行模式从内核态切换回用户态,继续执行用户程序。
    来源:用户态和内核态是怎么切换的

2.简单谈谈操作系统的执行逻辑

操作系统启动,在内核中相当于执行一个死循环。为什么这么说呢?
因为OS是硬件和软件的管理者,这个管理的动作是持续的,就相当于是一个循环,不断的对各种资源和信号进行处理,处理完又进入这个循环。
我们拿进程调度举例,操作系统需要在多个进程之间进行调度,以实现 CPU 资源的合理分配。在启动后,操作系统会不断循环执行进程调度算法,根据进程的优先级、状态等因素,选择一个就绪进程并将 CPU 分配给它执行。当该进程的时间片用完或者因为等待资源等原因暂停时,操作系统会再次进行调度,选择下一个就绪进程执行,如此反复循环。

3.内核如何实现信号的捕捉

当我们对“内核”以及操作系统的运行状态、执行逻辑有了简单的了解后,就可以粗略的了解一下“内核如何实现信号的捕捉”。
在这里插入图片描述
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

  • 用户程序注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
  • 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
  • sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
  • 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

注意:信号自定义行为被处理的真正时间,是进程从内核态返回用户态。

4.sigaction()

在这里插入图片描述
https://man.cx/sigaction
sigaction函数,可以检查或修改一个信号的动作。

4.1 参数:

  • signum表示目标信号(SIGKILL和SIGSTOP除外)
  • act是一个输入型参数,old是一个输出型参数。
  • 若act指针非空,则根据act修改该信号的处理动作。
  • 若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
    在这里插入图片描述
    • sa_handler表示信号捕捉方法,
    • sa_sigaction是实时信号的处理函数,
    • sa_mask表示某个信号的处理方式正在被调用时,想屏蔽的信号集。
    • sa_flags字段包含一选项,本章的代码都把sa_flags设为0。
    • sa_restorer:一般不使用,已过时。

4.2 返回值

调用成功则返回0,出错则返回- 1。

4.3 样例

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signo)
{cout << "catch a signal,signal number:" << signo << endl;
}
int main()
{struct sigaction act,oact;memset(&act,0,sizeof(act));memset(&oact,0,sizeof(oact));act.sa_handler = handler;int ret = sigaction(2,&act,&oact);if(ret == -1) perror("sigaction");while(true){cout << "I am process" << endl;sleep(1);}return 0;
}

在这里插入图片描述
SIGINT就被捕获了。

5. 未决信号集中“1何时变为0”

现在我们有一个问题。当信号产生后,pending信号集对应的比特位由0置1,当信号处理后,该比特位又要从1置0,那么,是什么时候将该比特位从1置回0的呢?

我们可以在捕捉函数(handler)中打印未决信号集,如果打印结果显示“2号比特位”是0,则说明是在信号处理前就置回0了,反之是处理后。我们测试一下,

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>using namespace std;void Printpending()
{sigset_t pset;sigpending(&pset);for(int i = 31; i >= 1; --i){if(sigismember(&pset,i) ){cout << 1;}else{cout << 0;}}cout << "\n";
}
void handler(int signo)
{Printpending();cout << "catch a signal,signal number:" << signo << endl;
}
int main()
{struct sigaction act,oact;memset(&act,0,sizeof(act));memset(&oact,0,sizeof(oact));act.sa_handler = handler;int ret = sigaction(2,&act,&oact);if(ret == -1) perror("sigaction");while(true){cout << "I am process" << endl;sleep(1);}return 0;
}

编译运行
在这里插入图片描述
这说明,再执行捕捉方法前,未决信号集中的1就被置回0了。

6.OS不允许嵌套调用同个信号的捕捉函数

在一个进程内,当一个信号的处理/捕捉函数正在被调用时,OS会将该信号屏蔽,当调用结束后,会解除屏蔽。
那么如何证明这一点呢?
如果一个信号的捕捉函数正在被调用时,进程不会屏蔽该信号,按照“未决信号集中‘1变为0’是在递达前”,此时我们不断的向该进程发送同种信号,未决信号集对应的比特位应该一直是0,如果出现1了,说明该信号产生了但没有递达,也就是被阻塞了。
我们可以将handler函数写成死循环,在循环中不断打印pending信号集,并持续发送同种信号,观察未决信号集的变化,如果一直是0,则说明该信号没有被阻塞,如果打印的第一次是0并在我们发送第二次信号后变为1,就说明该信号被阻塞了。
我们修改handler函数

void handler(int signo)
{cout << "catch a signal,signal number:" << signo << endl;while(1){Printpending();sleep(1);}
}

重新编译运行,
在这里插入图片描述
打印结果说明,当进程的某个信号的处理方式正在被调用时,进程会屏蔽该信号。
如果我们想在2号信号被递达时,除了系统默认屏蔽的2号信号还想屏蔽1号、3号和4号信号,我们可以将这些信号加入sa_mask,

sigaddset(&act.sa_mask, 1);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);

注意:我们这里说“嵌套调用”,可能有些不恰当,因为信号的递达和main()函数的调用是两个不同的执行流,而嵌套一般是指在同一个执行流内。

7. 可重入函数与不可重入函数

定义

  • 可重入函数:是指一个可以被多个任务或线程同时调用,而不会产生数据错误或程序异常的函数。在函数执行过程中,无论被调用多少次,其执行结果都只与输入参数有关,而与调用的顺序和时机无关。
  • 不可重入函数:是指在多任务或多线程环境下,一个函数在被多个任务或线程同时调用时,可能会导致数据错误或程序异常的函数。

特点

  • 可重入函数
    • 无共享资源依赖:通常不依赖于全局变量或静态变量等共享资源,即使使用了全局变量,也会通过适当的方式进行保护,确保在多个调用之间不会产生冲突。
    • 函数状态独立:函数内部的状态是独立的,每次调用都不会影响其他调用的结果,也不会依赖于之前的调用状态。
    • 原子操作:其操作通常是原子的,即一个操作在执行过程中不会被中断,或者即使被中断,也能保证在重新执行时不会产生错误。
  • 不可重入函数
    • 共享资源访问冲突:通常会对全局变量、静态变量或其他共享资源进行读写操作,且在访问这些共享资源时没有采取适当的保护措施。
    • 函数内部状态依赖:其执行结果可能依赖于函数内部的静态状态或全局状态,而这些状态在多次调用之间可能会被修改。
    • 非原子操作问题:可能包含一些非原子操作,即一个操作在执行过程中可能会被中断,然后由其他任务或线程继续执行,从而导致数据不一致或程序逻辑错误。

使用场景

  • 可重入函数:适用于多任务或多线程并发执行的环境,如操作系统内核、服务器程序、多线程应用程序等。在这些环境中,多个任务或线程可能会同时调用同一个函数,使用可重入函数可以保证程序的正确性和稳定性。
  • 不可重入函数:通常适用于单任务环境,或者在多任务环境中对函数的调用进行了严格的同步控制,确保同一时刻只有一个任务或线程能够调用该函数。

示例

  • 可重入函数
int add(int a, int b) {return a + b;
}

这个函数只依赖于输入参数ab,不依赖于任何全局变量或静态变量,也没有对共享资源进行读写操作,因此是一个可重入函数。

  • 不可重入函数
int count = 0;
void increment_count() {count++;
}

这个函数对全局变量count进行了自增操作,在多任务或多线程环境下,如果多个任务或线程同时调用这个函数,就会导致count的值被错误地修改,因此是一个不可重入函数。

来源:https://www.doubao.com/chat/

总结

如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的:

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

8.关于G++编译器的优化

我们编写一段测试代码,

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>using namespace std;int flag = 0;
void handler(int signo)
{cout << "catch a signal,signal number:" << signo << endl;flag = 1;
}
int main()
{signal(2,handler);while(!flag);cout << "process exit normally" << endl;return 0;
}

编译运行,
在这里插入图片描述
当我们不发送2号信号时,“!flag”一直为真,该进程会一直运行;当我们发送2号信号后,flag会变为0,程序跳出循环,打印“print exit normally”。
但是对于一些编译器,可能会对flag进行优化,因为flag只进行了判断,没有进行修改,判断是逻辑计算在CPU中进行,判断的结果可能会被优化,直接保存在CPU的寄存器中,我们可以做一下测试,
下面是G++编译器编译时优化的选项,(参考:https://www.doubao.com/chat/)

-O(优化级别)系列选项

  • -O0(默认选项)
    • 这是不进行优化的级别。在这个级别下,编译器会以最快的速度生成目标代码,代码的编译速度相对较快。但生成的可执行程序可能会比较大,执行效率也相对较低。主要用于调试阶段,因为在调试时,我们通常希望代码的执行顺序和变量的存储方式等尽可能地接近源代码,这样更便于追踪错误。
  • -O1
    • 这是基本的优化级别。编译器会进行一些简单的优化,如删除未使用的代码、简化表达式、调整指令顺序等。这种优化可以在一定程度上提高程序的执行效率,同时对编译时间的影响相对较小。例如,它可能会将一些简单的常量表达式在编译时就计算出结果,而不是在运行时计算。
  • -O2
    • 这是比较常用的优化级别,它会进行更多的优化操作。除了包含 -O1的优化外,还会进行诸如函数内联(将一些小型函数的代码直接嵌入到调用它的地方,减少函数调用的开销)、循环优化(例如循环展开,将循环体中的代码复制多次,减少循环控制的开销,但可能会增加代码大小)等。这个级别可以显著提高程序的性能,但可能会使编译时间变长,并且在某些情况下可能会导致调试信息变得不太准确。
  • -O3
    • 这是最高级别的优化选项。它会在 -O2的基础上进行更多激进的优化,比如更广泛的内联、更多的循环优化以及对指令调度的进一步优化等。虽然可以使程序执行效率更高,但也可能会导致程序体积大幅增加,编译时间明显变长,并且在一些复杂的代码结构中可能会引入难以发现的错误。例如,过度的内联可能会导致代码缓存命中率下降,或者使程序的栈空间需求超出预期。

测试结果,
在这里插入图片描述
不同的编译器,不同的版本,优化结果都可能不一样。我们只讨论O1优化与默认优化中,为什么发送2号信号后运行结果不同。
因为编译器检测到(当前执行流)后续代码没有对flag进行修改,便将flag第一次逻辑计算的结果保存到寄存器中,当对flag进行逻辑计算时,直接从寄存器中获取结果(1),当我们向进程发送2号信号后,信号递达修改的是内存中flag的值,不影响寄存器中的结果,所以进程继续陷入死循环。
为了防止编译器的优化,我们可以用关键字volatile修饰flag。
在这里插入图片描述

9.volatile

参考:https://www.doubao.com/chat/

  1. 基本定义与作用

    • 在编程语言中,volatile是一个关键字,用于修饰变量。它告诉编译器该变量的值可能会在程序执行过程中被意外地改变,这种改变不是由程序本身的逻辑所控制的。例如,在多线程环境下,一个变量可能会被其他线程修改;或者在与硬件交互时,硬件设备可能随时更新变量的值。
    • 编译器在处理volatile修饰的变量时,不会对涉及该变量的操作进行某些常规的优化。通常情况下,编译器可能会将变量的值缓存在寄存器中,以提高访问速度。但对于volatile变量,编译器每次都要从内存中读取其实际值,以确保程序使用的是变量的最新状态。
  2. 适用场景

    • 多线程编程
      • 在多线程环境中,多个线程可能会共享和修改同一个变量。如果一个变量没有被声明为volatile,编译器可能会进行优化,导致一个线程无法察觉到另一个线程对变量的修改。例如,在一个线程修改了共享变量后,另一个线程可能由于编译器的优化而继续使用寄存器中缓存的旧值。使用volatile可以避免这种情况,保证每个线程都能获取到变量的最新值。
    • 硬件交互
      • 当程序与硬件设备(如寄存器、传感器等)进行交互时,硬件设备可能会随时更新内存中的变量值。例如,在嵌入式系统中,一个表示传感器数据的变量可能会被传感器硬件不断更新。将这个变量声明为volatile,可以确保程序每次访问该变量时都能获取到硬件更新后的最新值,而不是使用编译器缓存的旧值。
  3. 与其他关键字的对比

    • const关键字对比:const用于表示变量的值是常量,在程序执行过程中不能被修改。而volatile则强调变量的值是可变的,并且这种变化可能是不可预测的。一个变量可以同时被声明为const volatile,这表示该变量的值不能被程序本身修改(如通过赋值语句),但可能会被外部因素(如硬件设备)改变。
    • 与普通变量对比:普通变量在编译器优化过程中可能会被缓存,其访问效率可能会提高,但存在获取不到最新值的风险。而volatile变量通过牺牲一定的访问效率(每次从内存读取),保证了变量值的准确性和及时性。

10. SIGCHLD

10.1 基于信号的等待回收

子进程在退出时,会向父进程发送17号信号(SIGCHLD),如果证明这一点呢?
我们可以捕获17号信号,编写一段父进程创建子进程且子进程会退出的代码,看看是否会执行信号的捕获方法(自定义行为函数)。

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signo)
{cout << "I am process:" << getpid() << "catch a signal,signal number:" << signo << endl;
}int main()
{signal(17,handler);pid_t pid = fork();if(pid == 0){while(true){cout << "I am child process:" << getpid() << ",getppid:" << getppid() << endl;sleep(1);break;}exit(0);}//fatherwhile(true){cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}

编译运行,
在这里插入图片描述
所以父进程在“等待”时,我们可以采用“异步”的方式,也就是基于信号进行等待,把等待函数写入信号的捕捉函数中。

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;void handler(int signo)
{sleep(3);pid_t rid = waitpid(-1,nullptr,0);cout << "I am process:" << getpid() << "catch a signal,signal number:" << signo << "child process quit:" << rid << endl;
}int main()
{signal(17,handler);pid_t pid = fork();if(pid == 0){while(true){cout << "I am child process:" << getpid() << ",getppid:" << getppid() << endl;sleep(3);break;}exit(0);}//fatherwhile(true){cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}

编译运行,
在这里插入图片描述
注意:子进程创建成功后,子进程和父进程的调度是由调度器控制的,一般调度算法偏向父进程。

10.2 问题1——进程全部退出

上面只是一个子进程退出,如果是10个子进程呢?
我们知道当一个信号的捕获函数正在被调用时,进程会屏蔽该信号。也就是说,当有10个子进程要同时退出时,注意是同时退出,就会造成部分子进程发送的17号信号被屏蔽,怎么解决这个问题呢?我们可以在捕捉函数写一个循环,循环等待子进程。

void handler(int signo)
{sleep(3);pid_t rid;while ((rid = waitpid(-1, nullptr, 0)) > 0){cout << "I am process:" << getpid() << "catch a signal,signal number:"<< signo << "child process quit:" << rid << endl;}
}int main()
{signal(17, handler);for (int i = 0; i < 10; ++i){pid_t pid = fork();if (pid == 0){while (true){cout << "I am child process:" << getpid() << ",getppid:" << getppid() << endl;sleep(2);break;}exit(0);}}// fatherwhile (true){cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}

编译运行,
在这里插入图片描述

10.3 问题2——进程随机退出

但还有问题,如果一共有10个子进程,有5个子进程要同时退出,其余五个不退出,那么在捕捉函数中,第6次循环就会卡住,waitpid会阻塞等待第6个子进程退出。
所以父进程要以非阻塞的方式等待。
示例:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
using namespace std;void handler(int signo)
{sleep(3);pid_t rid;while ((rid = waitpid(-1, nullptr, 0)) > 0){cout << "I am process:" << getpid() << "catch a signal,signal number:"<< signo << "child process quit:" << rid << endl;}
}int main()
{srand(time(nullptr));signal(17, handler);for (int i = 0; i < 10; ++i){pid_t pid = fork();if (pid == 0){while (true){cout << "I am child process:" << getpid() << ",getppid:" << getppid() << endl;sleep(3);break;}// 每隔一段时间退出exit(0);}sleep(rand() % 5 + 3);}// fatherwhile (true){cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}

在这里插入图片描述

10.4 问题4:必须要用wait回收子进程吗?

父进程必须要通过wait或waitpid回收子进程吗?
上面代码的运行中,我们都是通过waitpid函数回收僵尸进程,有没有什么办法不产生僵尸进程,也就是子进程退出就“退出”了,父进程也不用“用wait/waitpid回收子进程”。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction或signal(任意一个信号捕捉的系统调用接口)将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
示例:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>
using namespace std;int main()
{signal(SIGCHLD, SIG_IGN);//把17号信号忽略//只要将17号信号忽略,那么子进程退出的时候,父进程什么都不用管//子进程会自动被系统释放资源for (int i = 0; i < 10; ++i){pid_t pid = fork();if (pid == 0){while (true){cout << "I am child process:" << getpid() << ",getppid:" << getppid() << endl;sleep(3);break;}cout << "child quit" << endl;exit(0);}sleep(1);// sleep(rand() % 5 + 3);}// fatherwhile (true){cout << "I am father process:" << getpid() << endl;sleep(1);}return 0;
}

编译运行,
在这里插入图片描述
进程退出后没有进入僵尸状态,直接被回收。
所以以后当父进程不需要知道子进程的退出信息时,可以直接将17号信号的处理方式设为SIG_IGN。
那么我们没讲信号之前,子进程被回收时处理的方式是什么呢?
输入“man 7 signal”,往下查找,
在这里插入图片描述
我们可以找到17号信号的默认动作就是“忽略”。奇怪了,我们没有自定义17号信号的动作前,其默认动作是忽略,但还是会出现僵尸进程,为什么我们显示捕捉17号信号并将其动作设为忽略,就不会出现僵尸呢?同样都是忽略,结果却不同,我们该怎么理解这种现象呢?
其实UNIX对于17号信号的默认处理方式是SIG_DFL(signal(17,SIG_DFL),而SIG_DFL对应的action是IGN,所以UNIX对17号信号的默认处理方式并不是SIG_IGN。

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

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

相关文章

用Python写炸金花游戏

文章目录 **代码分解与讲解**1. **扑克牌的生成与洗牌**2. **给玩家发牌**3. **打印玩家的手牌**4. **定义牌的优先级**5. **判断牌型**6. **确定牌型优先级**7. **比较两手牌的大小**8. **打印结果** 完整代码 以下游戏规则&#xff1a; 那么我们要实现的功能&#xff0c;就是…

掌握软件工程基础:知识点全面解析【chap03、chap05、chap06、chap08、chap09】

chap03 UML UML定义了哪些视图&#xff1f;分别具有什么特点&#xff1f; 1&#xff0e;用例图&#xff08;Use case diagram&#xff09; 用例图展示各类外部执行者与系统所提供的用例之间的连接。一个用例是系统所提供的一个功能的描述&#xff0c;执行者是指使用这些用例的…

Linux嵌入式编程中与线程有关的知识(线程的概念、线程的创建、互斥锁、线程挂起、主线程、如何看查线程的ID等知识点)

目录 01-线程的概念(通过“进程和线程”的区别来理解)简单的理解详细的理解1. **定义与本质**2. **资源占用**3. **切换开销**4. **通信方式**5. **独立性**6. **调度**7. **使用场景**8. **在Linux中实现**对比总结 02-线程创建函数pthread_create()详解**函数原型****参数说明…

数据结构(Java)——链表

1.概念及结构 链表是一种 物理存储结构上非连续 存储结构&#xff0c;数据元素的 逻辑顺序 是通过链表中的 引用链接 次序实现的 。 2.分类 链表的结构非常多样&#xff0c;以下情况组合起来就有 8 种链表结构&#xff1a; &#xff08;1&#xff09;单向或者双向 &#xff08;…

win版ffmpeg的安装和操作

一、ffmpeg软件安装&#xff1a; ffmpeg是一个通过命令行将视频转化为图片的软件。 在浏览器搜索ffmpeg在官网里找到软件并下载&#xff08;不过官网很慢&#xff09;&#xff0c;建议用这个下载。 下载的文件是一个zip压缩包&#xff0c;将压缩包解压&#xff0c;有如下文件…

SpringBoot学习

一、SpringBoot介绍 (一)SpringBoot简介 Spring Boot 是由 Pivotal 团队提供的一个用于简化 Spring 应用初始搭建以及开发过程的框架。它基于 Spring 框架&#xff0c;旨在通过减少配置和简化开发流程来加速应用的开发和部署。Spring Boot 提供了嵌入式的 Tomcat、Jetty 或 Un…

FIR数字滤波器设计——窗函数设计法——滤波器的时域截断

与IIR数字滤波器的设计类似&#xff0c;设计FIR数字滤波器也需要事先给出理想滤波器频率响应 H ideal ( e j ω ) H_{\text{ideal}}(e^{j\omega}) Hideal​(ejω)&#xff0c;用实际的频率响应 H ( e j ω ) H(e^{j\omega}) H(ejω)去逼近 H ideal ( e j ω ) H_{\text{ideal}}…

FreeType矢量字符库的介绍、交叉编译以及安装

FreeType矢量字符库的介绍 FreeType 是一个开源的跨平台字体引擎库&#xff0c;广泛用于 Linux 嵌入式系统中实现字符显示的功能。它提供了高效的 TrueType、OpenType 和其他字体格式的解析和渲染功能&#xff0c;在嵌入式开发中尤其适合用来绘制矢量字体和位图字体。 FreeTy…

vue css box-shadow transition实现类似游戏中的模糊圈游走的感觉

先看效果&#xff1a; 代码如下&#xff1a; <template><div style"height: 800px"></div><divclass"rainbow-position"ref"host"><divv-for"config in colorStyles"class"one-shadow":style&q…

欧拉计划启航篇(一)

目录 1.什么是欧拉计划 2.简单介绍 3.访问不上去怎么办 4.第一题的代码编写 5.代码的优化 1.什么是欧拉计划 欧拉计划是和我们的数学知识相关的一个网站&#xff0c;但是这个网站上面的相关的问题需要我们去使用编程的知识去进行解决&#xff0c;因此这个适合对于想要提升…

【Compose multiplatform教程12】【组件】Box组件

查看全部组件文章浏览阅读493次&#xff0c;点赞17次&#xff0c;收藏11次。alignment。https://blog.csdn.net/b275518834/article/details/144751353 Box 功能说明&#xff1a;简单的布局组件&#xff0c;可容纳其他组件&#xff0c;并依据alignment属性精确指定内部组件的对…

无人零售 4G 工业无线路由器赋能自助贩卖机高效运营

工业4G路由器为运营商赋予 “千里眼”&#xff0c;实现对贩卖机销售、库存、设备状态的远程精准监控&#xff0c;便于及时补货与维护&#xff1b;凭借强大的数据实时传输&#xff0c;助力深度洞察销售趋势、优化库存、挖掘商机&#xff1b;还能远程升级、保障交易安全、快速处理…

springboot 配置跨域访问

什么是 CORS&#xff1f; CORS&#xff0c;全称是“跨源资源共享”&#xff08;Cross-Origin Resource Sharing&#xff09;&#xff0c;是一种Web应用程序的安全机制&#xff0c;用于控制不同源的资源之间的交互。 在Web应用程序中&#xff0c;CORS定义了一种机制&#xff0…

Ubuntu离线安装Docker容器

前言 使用安装的工具snap安装在沙箱中&#xff0c;并且该沙箱之外的权限有限。docker无法从其隔离的沙箱环境访问外部文件系统。 目录 前言准备环境卸载已安装的Docker环境快照安装的Dockerapt删除Docker 安装docker-compose下载执行文件将文件移到 /usr/local/bin赋予执行权限…

【Unity3D】ECS入门学习(七)缓存区组件 IBufferElementData

组件继承于IBufferElementData&#xff0c;可以让一个实体拥有多个相同的组件。 using Unity.Entities;public struct MyBuffComponentData : IBufferElementData {public int num; }using System.Collections; using System.Collections.Generic; using UnityEngine; using U…

一种寻路的应用

应用背景 利用长途车进行货物转运的寻路计算。例如从深圳到大连。可以走有很多条长途车的路线。需要根据需求计算出最合适路线。不同的路线的总里程数、总价、需要的时间不一样。客户根据需求进行选择。主要有一些细节&#xff1a; 全国的长途车车站的数据的更新&#xff1a; …

15、【OS】【Nuttx】OS裁剪,运行指定程序,周期打印当前任务

背景 接之前wiki【Nsh中运行第一个程序】https://blog.csdn.net/nobigdeal00/article/details/144728771 OS还是比较庞大&#xff0c;且上面搭载了Nsh&#xff08;Nuttx Shell&#xff09;&#xff0c;需要接入串口才能正常工作&#xff0c;一般调试的时候用&#xff0c;非调试…

学习 Python 编程的规则与风格指南

文章目录 1. Python 编程规则1.1 Python 的哲学&#xff1a;The Zen of Python1.2 遵守 PEP 81.3 Python 是大小写敏感的1.4 使用 Pythonic 风格 2. Python 编程风格2.1 命名风格2.2 注释风格2.3 文档字符串&#xff08;Docstring&#xff09;2.4 空格使用2.5 文件和代码组织 3…

Seata AT 模式两阶段过程原理解析【seata AT模式如何做到对业务的无侵入】

在分布式事务中&#xff0c;Seata 的 AT 模式&#xff08;Automatic Transaction&#xff09;是一种基于两阶段提交协议的事务模式。它通过自动生成数据快照&#xff08;before image 和 after image&#xff09;&#xff0c;实现了对分布式事务的高效管理。本文将详细解析 Sea…

中关村科金外呼机器人智能沟通破解营销难题

当今&#xff0c;传统的营销方式在效率、成本控制、客户管理等方面逐渐显现出局限性&#xff0c;难以满足现代企业的需求。如何提升营销效率、降低运营成本、有效管理客户会员&#xff0c;成为企业的难题。中关村科金外呼机器人通过智能化沟通技术&#xff0c;为企业提供了一站…