【Linux】信号的产生、保存、捕捉处理 (四种信号产生、核心存储、用户态与内核态、信号集及其操作函数)

news2024/11/26 17:50:37

文章目录

    • 1、什么是信号?
    • 2、信号的产生
      • 2.1 通过键盘产生信号
      • 2.2 通过系统调用产生信号
      • 2.3 硬件异常产生的信号
      • 2.4 由软件条件产生的信号
      • 2.5 进程的核心转储
    • 3、信号的保存
    • 4、信号的捕捉
      • 4.1 用户态和内核态
      • 4.2 用户态到内核态的切换
      • 4.3 信号捕捉过程
    • 5、信号集操作函数以及测试

1、什么是信号?

在生活上

比方说到了的外卖的提醒,这是一种信号。 我们对于这种信号有着自己的处理方式和意识,这就是处理信号。

值得注意的是,信号到来的时候,可能会因为有更加紧急的事,促使我们会先忽略这个信号,并且先保留这个信号。

在处理信号的时候,可能不同的个体会有着不同的处理动作。(比如红绿灯信号,一般人都是红停绿走,但是在一些可能存在的特殊群体下他们会有不一样的动作。)

在计算机上

首先,信号是给进程发的。(比如kill -9 pid)
进程如果需要识别信号,就一定要对信号有认识,并且有处理动作。

当进程收到信号可能有更重要的代码要执行,所以信号不一定会被处理。
那么进程本身就必须有对信号进行保存的能力

进程捕捉信号再对信号进行处理,一般有三种动作(默认动作,自定义动作,忽略动作)

通过 kill -l 可以看到所有信号,一个信号的数字对应其宏名。
其中 1–31号称为普通信号,34–64号称为实时信号。这里不关注实时信号。

在这里插入图片描述
通过man 7 signal 查看signal手册
再通过/Standard signals 可以查看所有信号对应的内容

在这里插入图片描述

如果一个信号发给进程,进程应该保存在哪呢?

可以推测:
信号将会保存在进程的task_struct{… unsigned int signal; …}
通过位图的结构,每一个bit位置代表一种信号,0和1代表是否受到信号,0无,1收到。
在这里插入图片描述

发送信号的本质就是修改PCB中signal的位图,
而对应PCB是由操作系统通过数据结构进行管理的,所以如果需要修改信号,就需要通过OS修改。
也就是说信号发送的方式,本质就是OS向目标进程发生信号。

修改OS数据只能通过操作系统接口来修改。
那么操作系统就应该提供发送信号的接口,而kill命令一定是调用了某种系统接口实现的。

下面就来证明这些推测。

2、信号的产生

进程可以通过以下四种方式收到信号。

2.1 通过键盘产生信号

系统提供的发送信号的接口 signal
在这里插入图片描述

这个接口用来捕捉信号,也就是当进程收到操作系统发送的sig信号后,将会由原来的默认动作转换成执行func函数中的自定义动作。
如果没有收到信号sig,将不会执行。

参数 sig : 对应信号名
参数 func 对应信号的三个处理动作:
SIG_DFL 默认的信号处理程序。
SIG_IGN 忽视信号。
或者传用户写的对应自定义动作的函数指针。

下面是一个捕捉2号信号和3号信号的例子:
2号信号的默认动作是在用户键盘输入ctrl+c时会退出前台进程。
3号信号的默认动作是在用户键盘输入ctrl+\时会退出前台进程。

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

using std::cout;
using std::endl;

void handler(int signal)
{
    cout << signal << ":自定义处理\n" << endl;
    exit(0);
}

int main()
{
    signal(2, handler); //2号信号SIGINT ctrl+c 触发 
    //signal(3, handler); //3号信号SIGQUIT ctrl+\ 触发 
    while(true)
    {
        cout << "hello world" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述

2.2 通过系统调用产生信号

操作系统是有能力向目标进程发送信号,但是得由用户来操作。

在这里插入图片描述

向一个pid进程发送sig信号。成功返回0,失败返回-1。
kill()可以向任意进程发送任意信号。

可以通过kill调用来模拟kill命令,只要向sig中发送9号信号就完成了kill命令。

//mysignal.cc
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>

using std::cout;
using std::endl;

static void Usage(const std::string& proc)
{
    cout << "\nUsage: " << proc << " signo pid\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int signo = atoi(argv[1]);
    pid_t pid = atoi(argv[2]);
    int n = kill(pid, signo);
    assert(n == 0);
    return 0;
}
/
//mytest.cc
#include <iostream>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while(true)
    {
        printf("我是一个正在运行的进程, pid: %d\n", getpid());
        sleep(2);
    }
    return 0;
}

在这里插入图片描述


raise调用
在这里插入图片描述

raise()函数发送一个任意信号给调用者
sig指明信号。

举例

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

int main()
{
    int cnt = 0;
    while(cnt++ < 10)
    {
        printf("cnt : %d\n", cnt);
        if(cnt == 5) raise(9); //kill(getpid(), sig);
    }
    return 0;
}

在这里插入图片描述


abort调用
在这里插入图片描述

abort向当前进程发送 6) SIGABRT 信号,中止当前进程。

#include <iostream>
#include <stdlib.h>

int main()
{
    int cnt = 0;
    while(cnt++ < 10)
    {
        printf("cnt : %d\n", cnt);
        if(cnt == 5) abort(); //kill(getpid(), SIGABRT)
    }
    return 0;
}

在这里插入图片描述

关于信号的处理行为的理解:
很多情况,进程收到大部分的信号,都是为了中止进程。
因为信号的不同,代表不同的事件,但结果是可以相同的。

2.3 硬件异常产生的信号

CPU异常产生的信号

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

void catchSig(int signo)
{
    printf("捕获信号:%d\n", signo);
    sleep(2);
}

int main()
{
	//捕获 8)SIGFPE 信号
	//Floating point exception
    signal(8, catchSig);
   	int n = 10/0; //
    return 0;
}

在这里插入图片描述

当代码执行后,由于除0的错误被操作系统捕获,向该进程发送了8号信号,通过signal捕获后,确认是8号信号,并且发现一个怪事。

为什么捕获信号后,一直打印?
这是因为处理器在处理10/0时,由于除0造成数据溢出,将一个状态寄存器的溢出标记位置为1。

CPU向OS发送运算异常的信息,根据寄存器上下文确定进程,OS就对进程发送了信号。这个信号默认是中止进程,被捕获后改成了打印。

由于进程收到信号后没有退出,因为进程切换,无数次寄存器当再次加载这个进程上下文,就让OS识别到了CPU内部状态寄存器溢出位是1。


MMU异常产生的信号

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

void catchSig(int signo)
{
    printf("捕获信号:%d\n", signo);
    exit(0);
}

int main()
{
    signal(11, catchSig);
    int* p = nullptr;
    *p = 100; //野指针访问 OS发送 11) SIGSEGV信号
    return 0;
}

在这里插入图片描述

代码中p访问的是虚拟地址空间的地址,虚拟地址映射到页表,再通过MMU(内存管理单元,集成在CPU当中)读取页表地址放入到对应物理地址。

p解引用访问0号地址,MMU因为非法访问的原因发生异常,操作系统识别到异常将11号信号发生给进程。

所以以上就是由硬件自发的让OS给进程发送信号。
OS给进程发送信号虽然结果一样,但是由于种类不同,也能让进程知道发送了何种错误。

2.4 由软件条件产生的信号

在之前管道通信中,如果一个管道只写不读,就会浪费空间。
操作系统面对这种情况就会给进程发送一个SIGPIPE信号。
在这里插入图片描述

这种当fds[0]关闭,fds[1]存在的条件判断所造成的信号,就是软件产生的信号。


下面的alarm 函数调用也会产生一个14号信号。
在这里插入图片描述

闹钟功能,seconds秒后,向进程发送一次SIGALRM信号。
这个信号默认也是中止进程。

下面看两段代码

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

static int cnt = 0;

void catchSig(int signo)
{
    printf("捕获信号:%d, 最终cnt:%d\n", signo, cnt);
    exit(0);
}

int main()
{
    signal(14, catchSig);
    alarm(1); // 14) SIGALRM
    while(true)
    {
        cnt++;
        printf("cnt:%d\n", cnt);
    }
    return 0;
}

在这里插入图片描述

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

static int cnt = 0;

void catchSig(int signo)
{
    printf("捕获信号:%d, 最终cnt:%d\n", signo, cnt);
    alarm(1);
}

int main()
{
    signal(14, catchSig);
    alarm(1); // 14) SIGALRM
    while(true)
    {
        cnt++;
    }
    return 0;
}

在这里插入图片描述

首先第一段代码,展示了数据通过外设和网络打印是多快。
第二段代码,没有打印数据,单纯反应了cpu的计算力,并且通过两个alarm函数简单的实现了sleep()的功能。

那么为什么alarm函数能看作一个软件条件产生的信号呢?
因为alarm本质就是通过用户层上数据结构实现的,是一种软件。
任何进程都可以使用alarm系统调用在内核中设置闹钟,这么多的闹钟就一定需要被OS进行结构体描述后,通过相应数据结构管理。
(比如通过堆结构来管理闹钟,通过闹钟的里到点最近时间建立最小堆)

2.5 进程的核心转储

下面运行这个代码

很明显,它会因为非法内存引用报错。

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

int main()
{
    while(true)
    {
        int arr[10];
        arr[10000] = 10;
    }
    return 0;
}

在这里插入图片描述

在之前看信号的标准内容的时候,发现每个信号有着自己的Action,而这其中有Term和Core两种类型。

这里是引用

这两个主要的区别是,core类型的信号在信号中止异常进程后,会在本地文件生成一个核心文件,这个文件在对应时刻存储着进程的有效数据。
Term类型的信号中止进程后就不会有这个步骤。


core file文件在云服务器默认关闭,通过ulimit -a可以看默认限制的资源,看到core file默认是关闭的。
通过ulimit -c 相应size 可以设置core文件。
在这里插入图片描述
在这里插入图片描述

再次运行程序:
与之前不同的是运行结果多了个(core dumped),意为核心转储
核心转储:当进程出现异常时,进程在对应时刻,将内存中的有效数据转储在磁盘中,也就是下面的core.14626(对应pid)文件中。

在这里插入图片描述

通过gdb 调试,输入core-file 对应核心文件名,就可以生成进程具体的异常原因。
看到由于11号信号,发送了段错误。
在这里插入图片描述

3、信号的保存

前面介绍了信号产生的四种情况,那么进程在收到信号后具体是怎么保存的呢?

首先了解一些概念

  1. 进程实际执行信号处理的动作称为信号递达(Delivery)
  2. 信号从产生到处理的中间的状态,称为信号未决(Pending)
  3. 进程可以选择阻塞某个信号。(可以在信号传递之前,选择阻塞这个信号,当信号到来时,阻塞信号)
  4. 当进程选择阻塞的信号到来时,信号处于未决状态。直到接触对这个信号的阻塞,才执行递达动作。
  5. 忽略和阻塞不同,阻塞是在执行递达动作前可以选择的,而忽略是在递达动作后可以选择的一种动作。


在进程的PCB中,其实有三种数据结构:
在这里插入图片描述

其中:
pending表(未决表): 其作为一个位图结构用来保存收到了哪些信号,通过bit位置表示具体信号,通过1/0来表示是否收到信号。(比如从右到左第一个bit位为1,代表收到了1号信号)
block表(阻塞表): 其作为一个位图结构用来保存阻塞了哪些信号,通过bit位置表示具体信号,通过1/0来表示是否阻塞信号。
handler表: 其作为一个函数指针数组,每个数组下标对应一种信号,通过下标可以调用对应信号的处理函数。(比如通过下标1,调用1号信号的处理函数)

如果一个信号对应pending表位置为1,block表位置为1,说明这个信号被阻塞,处于未决状态。
如果一个信号对应pending表位置为1,block表位置为0,说明这个信号抵达。
如果在信号抵达前,收到了多次信号,Linux下对普通信号只抵达一次,而如果是实时信号,将会放入一个队列中,这里不讨论实时信号。

综上,信号是由PCB保存的,发送信号修改PCB,因为PCB也是操作系统数据,而操作系统只能通过操作系统自身修改,也就是说信号是由操作系统发送的

操作系统拥有发送信号的能力,但是它无法自己使用这个能力,这就需要用户来运用它的能力,也就是说需要用户进程通过相应系统调用发送信号。

4、信号的捕捉

在之前,我们知道了信号是由进程通过系统调用修改PCB对应数据结构所产生,是由OS发送的,知道了信号如何产生,如何保存,那么信号是如何被进程接收处理的呢?

首先进程收到信号也在没有阻塞信号下,不会立马处理信号,而是在合适的情况下,由内核态到用户态再处理。
那么什么是内核态和用户态呢?

4.1 用户态和内核态

用户态和内核态是两种运行级别。
用户态是最低的级别,处于用户态的进程,会有一些命令的限制,不能访问内核资源和硬件。
内核态是最高的级别,处于内核态的进程,可以访问内核资源以及硬件。

值得注意的是,内核态是可以访问用户态程序的数据,也是在理论上可以访问用户态程序代码的,但是操作系统不允许这种情况发送,因为会出现安全性问题!

如果进程需要通过系统调用访问内核资源或者硬件,就一定需要从用户态切换到内核态。
(比如用户态的进程访问系统内核:waitpid、getpid,访问硬件:printf、fopen、write、read。再比如C++STL中vector的扩容需要访问内存,但是其扩容机制也一定避免了多次调用系统调用。)
系统调用也因此相比普通调用,耗时更多,因此尽量少使用系统调用。

用户态和内核态怎么表示的?

其实进程运行的时候,进程对应上下文被加载到CPU的寄存器中,CPU内有些寄存器指向进程有关的结构,比如PCB,页表等。

其中有一个CR3寄存器,表示当前CPU的运行级别,0代表内核态,3代表用户态。

这里是引用

4.2 用户态到内核态的切换

前面说了,进程如果需要系统调用访问内核资源或硬件,就需要从用户态切换到内核态。
这之中有个问题:
进程又是如何找到对应系统调用的呢?

进程通过用户级页表将虚拟地址空间数据映射到物理内存,每个进程都有自己的虚拟地址空间和自己的用户级页表,这样就能确保进程间的独立性,使得物理内存出现不一样的数据。

当操作系统加载到物理内存的时候,操作系统也为进程准备了一个内核级页表,内核级页表是为了维护在虚拟和物理之间的操作系统的代码而构成的内核级映射表。开机时会将操作系统加载到内存,因此操作系统在物理空间只存在一份,这就决定了内核级页表在内核中只有一份就够了。同时在CPU内也有一个寄存器一直指向这个内核级页表。内核级页表将物理内存中系统代码,在进程地址空间对应的3-4G空间进行映射,所以进程就可以通过内核级页表访问系统调用。

也就是说,在进程运行的时候,对应上下文会加载到寄存器中,这就使得一些寄存器能找到进程的PCB与页表,并且也能找到内核级页表,那么当需要调用系统接口的时候,与动态链接类似就可以直接从虚拟地址空间进行跳转,再通过内核级页表从物理内存找到对应代码,再返回到用户空间。

并且,每一个进程的虚拟地址3-4G空间都共享同一个内核级页表,所以无论进程怎么切换,进程对应3-4G空间是不会变的。因此,进程是可以随意访问系统调用的。
这里是引用

那么用户凭什么能访问内核呢?
其实进程以用户态调用系统接口一开始也是用户态的,Linux在系统调用接口初始位置有一个从用户态到内核态的转换(通过汇编指令int 80进行陷入内核),因此进程能以内核态的身份访问内核资源或硬件资源。

4.3 信号捕捉过程

接着前面 首先进程收到信号也在没有阻塞信号下,不会立马处理信号,而是在合适的情况下,由内核态到用户态再处理。 这也说明,此前是处于内核态的。那么什么时候会进入内核态呢?系统调用和进程切换。

从整个信号捕捉过程来看:
在这里插入图片描述

5、信号集操作函数以及测试

前面说的信号保存的数据结构都是内核中的,而在用户层可以通过一些手段访问它们。

比如:
sigset_t是一个信号集,它是一个位图结构,可以表示每个信号的有效和无效状态。在阻塞信号集中有效和无效对应阻塞和未阻塞,在未决信号集中有效和无效对应是否处于未决状态。阻塞信号集也称为当前进程的信号屏蔽字,这里的屏蔽是阻塞的意思而不是忽略。

在用户层有以下函数调用可以用来设置这个信号集:
详细可以通过man 3 手册查看

#include <signal.h>
int sigemptyset(sigset_t *set); //初始化信号集,全部清0
int sigfillset(sigset_t *set); // 填充信号集 全部置1
int sigaddset (sigset_t *set, int signo); // 往信号集里添加指定信号
int sigdelset(sigset_t *set, int signo); // 往信号集里删除指定信号
int sigismember(const sigset_t *set, int signo);// 判断信号集里是否有指定信号


除此以外还有以下函数调用可以修改内核相应信号结构:

sigprocmask
sigprocmask针对的是对PCB中block结构(阻塞位图)进行修改。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

参数部分:
how有三种选择:
SIG_BLOCK 代表在原来的基础上添加set信号集中的信号。
SIG_UNBLOCK 代表在原来的基础上删除set信号集中的信号。
SIG_SETMASK 代表直接将block位图修改成set。

set代表需要传的信号集
oset 代表block位图修改前的信号集

sigpending
sigpending就是将PCB当前的pending位图传给set信号集。
set参数作为一个输出型参数。

#include <signal.h>
int sigpending(sigset_t *set);


知道了上面,就可以做一个小小实验。
大概内容如下:

创建一个阻塞的信号集,可以将2、3号信号添加进去,然后再添加到block位图中,再给进程发送对应信号,打印pending位图,看是否阻塞了该信号。
(并且添加解除阻塞功能,再捕获信号将对应默认处理改成自定义处理)

代码如下:

#include <iostream>
#include <vector>
#include <signal.h>
#include <unistd.h>
using std::cout;
using std::endl;

#define MAX_SIGNO 31

static std::vector<int> blocks = {2, 3};

static void show_pending(const sigset_t& pending)
{
    for(int signo = MAX_SIGNO; signo > 0; --signo)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

static bool pendingIsEmpty(const sigset_t& pending)
{
    for(int signo = MAX_SIGNO; signo > 0; --signo)
    {
        if(sigismember(&pending, signo))
        {
            return false;
        }
    }

    return true;
}

static void myhandler(const int signo)
{
    cout << signo << " 号信号已经被捕捉" << endl;
}

int main()
{
    //对信号做捕捉 自定义操作
    for(const int& signo : blocks) signal(signo, myhandler);

    //1 添加阻塞信号
    sigset_t block, oblock, pending;
    //1.1 初始化信号集
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    //1.2 添加阻塞信号到阻塞信号字
    for(const int& signo : blocks) sigaddset(&block, signo);

    //1.3 设置阻塞信号 (进入内核 直接将阻塞修改成block信号集)
    sigprocmask(SIG_SETMASK, &block, &oblock);

    //2 打印pending信号 显示收到信号但是没有做出处理动作,意味着被阻塞
    int cnt = 5;
    while(true)
    {
        //2.1 接收pending信号集
        sigpending(&pending);

        //2.2 打印pending表
        show_pending(pending);
        sleep(1); //缓慢打印
        if(cnt-- == 0)
        {
            if(pendingIsEmpty(pending))
            {
                cout << "未收到任何信号" << endl;
                break;
            }
            else
            {
                //2.3 解除信号的阻塞,完成自定义动作后,再次阻塞信号
                cout << "解除信号的阻塞" << endl;
                sigprocmask(SIG_SETMASK, &oblock, &block);//解除后信号由内核到用户态执行自定义处理方法
                sigprocmask(SIG_SETMASK, &block, &oblock);
                cnt = 5;
            }
        }
    }
    return 0;
}

实验结果:
在这里插入图片描述
本章完~

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

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

相关文章

Spring——Spring整合Mybatis(XML和注解两种方式)

框架整合spring的目的:把该框架常用的工具对象交给spring管理&#xff0c;要用时从IOC容器中取mybatis对象。 在spring中应该管理的对象是sqlsessionfactory对象&#xff0c;工厂只允许被创建一次&#xff0c;所以需要创建一个工具类&#xff0c;把创建工厂的代码放在里面&…

Qt不会操作?Qt原理不知道? | Qt详细讲解

文章目录Qt界面开发必备知识UI界面与控件类型介绍Qt设计器原理控件类型的介绍信号与槽机制处理常用控件创建与设置常见展示型控件创建与设置常见动作型控件创建与设置常见输入型控件创建与设置常见列表控件创建于设置Qt中对象树的介绍项目源码结构刨析.pro.hmain.cpp.cppQt界面…

JVM的几种GC

GC JVM在进行GC时&#xff0c;并不是对这三个区域统一回收。大部分时候&#xff0c;回收都是新生代~ 新生代GC&#xff08;minor GC&#xff09;&#xff1a; 指发生在新生代的垃圾回收动作&#xff0c;因为Java对象大多都具备朝生夕灭的特点&#xff0c;所以minor GC发生得非…

【问题排查】Linux虚拟机无法识别串口与ttyUSB

虚拟机串口连接失败问题 小哥的Linux系统是用虚拟机来装的&#xff0c;最近恰好需要用到串口和Linux进行通信&#xff0c;连接好硬件之后&#xff0c;发现虚拟机上找不到串口。 经查询才发现通过虚拟机启动的系统&#xff0c;正常情况下是无法使用串口进行通信的&#xff0c;需…

Ast2500增加用户自定义功能

备注&#xff1a;这里使用的AMI的开发环境MegaRAC进行AST2500软件开发&#xff0c;并非openlinux版本。1、添加上电后自动执行的任务在PDKAccess.c中列出了系统启动过程中的所有任务&#xff0c;若需要添加功能&#xff0c;在相应的任务中添加自定义线程。一般在两个任务里面添…

隐私计算将改变金融行业的游戏规则?

开放隐私计算 01背景2月底&#xff0c;相关部门印发《数字中国建设整体布局规划》提出&#xff0c;到2025年&#xff0c;基本形成横向打通、纵向贯通、协调有力的一体化推进格局&#xff0c;数字中国建设取得重要进展&#xff1b;到2035年&#xff0c;数字化发展水平进入世界前…

前端安全(自留)

目录XSS——跨站脚本常见解决CSRF ——跨站请求伪造常见解决XSS——跨站脚本 当目标站点在渲染html的过程中&#xff0c;遇到陌生的脚本指令执行。 攻击者通过在网站注入恶意脚本&#xff0c;使之在用户的浏览器上运行&#xff0c;从而盗取用户的信息如 cookie 等。 常见 解…

机械学习 - scikit-learn - 数据预处理 - 2

目录关于 scikit-learn 实现规范化的方法详解一、fit_transform 方法1. 最大最小归一化手动化与自动化代码对比演示 1&#xff1a;2. 均值归一化手动化代码演示&#xff1a;3. 小数定标归一化手动化代码演示&#xff1a;4. 零-均值标准化(均值移除)手动与自动化代码演示&#x…

优秀开源软件的类,都是怎么命名的?

日常编码中&#xff0c;代码的命名是个大的学问。能快速的看懂开源软件的代码结构和意图&#xff0c;也是一项必备的能力。 Java项目的代码结构&#xff0c;能够体现它的设计理念。Java采用长命名的方式来规范类的命名&#xff0c;能够自己表达它的主要意图。配合高级的 IDEA&…

什么是谐波

什么是谐波 目录 1. 问题的提出 2. “谐”字在中英文中的原意 2.1 “谐”字在汉语中的原义 2.2 “谐”字对应的英语词的原义 3.“harmonics(谐波)”概念是谁引入物理学中的&#xff1f; 4.“harmonics(谐波)”的数学解释 1. 问题的提出 “谐波”这个术语用于各种学科&am…

国外SEO升级攻略!一看就懂!

SEO是搜索引擎优化的缩写&#xff0c;它是指通过优化网站内容和结构&#xff0c;提升网站在搜索引擎中的排名&#xff0c;从而获得更多的有价值的流量。 而关键词研究和选择是SEO优化中最基础也是最关键的环节&#xff0c;它决定了网站将面向哪些用户、哪些关键词和词组将被优…

SWF (Simple Workflow Service)简介

Amazon Simple Workflow Service (Amazon SWF) 提供了给应用程序异步、分布式处理的流程工具。 SWF可以用在媒体处理、网站应用程序后端、商业流程、数据分析和一系列定义好的任务上。 举个例子&#xff0c;下图表明了一个电商网站的工作流程&#xff0c;其中涉及了程序执行的…

C#【汇总篇】语法糖汇总

文章目录0、语法糖简介1、自动属性2、参数默认值和命名参数3、类型实例化4、集合4.1 初始化List集合的值4.2 取List中的值5、隐式类型&#xff08;var&#xff09;6、扩展方法【更换测试实例】7、匿名类型&#xff08;Anonymous type&#xff09;【待补充】8、匿名方法&#xf…

使用 Microsoft Dataverse 简化的连接快速入门

重复昨天本地部署dynamics实例将其所有的包删除之后&#xff0c;再次重新下载回来。运行填写跟之前登陆插件一样的信息点击login 然后查看控制台&#xff0c;出现这样就说明第一个小示例就完成了。查看你的dy365平台下的 “我的活动”就可以看到刚刚通过后台代码创建的东西了。…

MyBatis学习笔记(十三) —— 分页插件

13、分页插件 SQL语句中添加 limit index,pageSize pageSize: 每页显示的条数 pageNum: 当前页的页码 index: 当前页的起始索引, index (pageNum 1) * pageSize count: 总记录数 totalPage: 总页数 totalPagecount/pageSize; if(count % pageSize !0 ){ totalPage 1; } page…

java多线程(二六)ReentrantReadWriteLock读写锁详解(2)

3、读写状态的设计 同步状态在重入锁的实现中是表示被同一个线程重复获取的次数&#xff0c;即一个整形变量来维护&#xff0c;但是之前的那个表示仅仅表示是否锁定&#xff0c;而不用区分是读锁还是写锁。而读写锁需要在同步状态&#xff08;一个整形变量&#xff09;上维护多…

树与二叉树(概念篇)

树与二叉树1 树的概念1.1 树的简单概念1.2 树的概念名词1.3 树的相关表示2 二叉树的概念2.1 二叉树的简单概念2.1.1 特殊二叉树2.2 二叉树的性质2.3 二叉树的存储结构1 树的概念 1.1 树的简单概念 树是一种非线性的数据结构&#xff0c;它是由n(n>0)个有限节点组成的一个具…

OpenCV入门(三)快速学会OpenCV2图像处理基础(一)

OpenCV入门&#xff08;三&#xff09;快速学会OpenCV2图像处理基础&#xff08;一&#xff09; 1.颜色变换cvtColor imgproc的模块名称是由image&#xff08;图像&#xff09;和process&#xff08;处理&#xff09;两个单词的缩写组合而成的&#xff0c;是重要的图像处理模…

【Error: ImagePullBackOff】Kubernetes中Nginx服务启动失败排查流程

❌pod节点启动失败&#xff0c;nginx服务无法正常访问&#xff0c;服务状态显示为ImagePullBackOff。 [rootm1 ~]# kubectl get pods NAME READY STATUS RESTARTS AGE nginx-f89759699-cgjgp 0/1 ImagePullBackOff 0 103…

Python(青铜时代)——容器类的公共方法

内置函数 内置函数&#xff1a;不需要使用 import 导入库&#xff0c;就可以直接使用的函数 函数描述备注len(&#xff09;计算容器中元素个数del( )删除变量max( )返回容器中元素最大值如果是字典&#xff0c;只针对key比较min( )返回容器中元素最小值如果是字典&#xff0c…