【Linux】信号保存与信号捕捉处理

news2024/11/25 2:35:45

信号保存与信号捕捉

  • 一、信号保存
    • 1. 信号的发送
    • 2. 理解信号保存
      • (1)信号保存原因
      • (2)信号保存概念
    • 3. 信号保存系统接口
      • (1)sigset_t
      • (2)sigprocmask()
      • (3)sigpending()
      • (4)signal()
      • (5)测试系统接口
  • 二、信号捕捉处理
    • 1. 信号的处理
    • 2. 理解用户态和内核态
    • 3. 信号的捕捉
    • 4. 系统调用
      • (1)sigaction()
      • (2)pending 表的置0顺序
      • (3)struct sigaction 中的 sa_mask 字段
  • 三、信号扩展
    • 1. 可重入函数
    • 2. volatile
    • 3. SIGCHLD 信号

一、信号保存

1. 信号的发送

那么在学习信号保存之前,我们先了解一下信号的发送,我们知道普通信号一共有31个,如下:

在这里插入图片描述

但是这个31就非常特殊,对于普通信号而言,对于进程而言,自己有还是没有收到哪一个信号。实际上,我们发送信号是给进程发,具体点就是给进程的 PCB 发,所以 task_struct 中必定有维护信号的字段,那么在 task_struct 中其实只需要维护一个整数即可,因为一个整数有 32 个比特位!而我们忽略第一位,从第2位开始到第32位一共31个比特位,就分别表示31种信号!也就是说,用0、1来描述信号,用位图管理普通信号!

所以,

  1. 比特位的内容是0还是1,表明是否收到;
  2. 比特位的位置(第几个),表示信号的编号;
  3. 所谓的发送信号,本质就是操作系统去修改 task_struct 的信号位图对应的比特位;

那么为什么必须是操作系统向进程PCB中写入呢?因为操作系统是进程的管理者,只有它有资格才能修改 task_struct 内部的属性!所以这就是为什么只有操作系统才能有资格给进程发信号!

2. 理解信号保存

(1)信号保存原因

信号为什么要保存呢?因为进程收到信号之后,可能不会立即处理这个信号,可能正在处理更重要的事情,所以信号不会被处理,就要有一个时间窗口,所以信号就要被保存。

(2)信号保存概念

  • 实际执行信号的处理动作称为信号递达(Delivery);
  • 信号从产生到递达之间的状态,称为信号未决(Pending);
  • 进程可以选择阻塞 (Block )某个信号;
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作;
  • 注意,阻塞忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

所以进程的 task_struct 中不仅要保存信号的状态,还要保存信号的阻塞状态;而且信号的范围是 1~31,每一种信号都要有自己的一种处理方法,所以在 task_struct 中还要为每一个信号维护一张 handler 表,这张表是函数指针数组,比如数组中的内容是 typedef void (*handler_t)(int); 类型,数组名为 handler_t handler[31];,所以它就是一个函数指针数组,里面放的就是指向方法的地址,而下标就是信号的编号!那么当我们捕捉对应信号后自定义的方法,就将我们的方法的地址填入对应的位置即可!如下图:

在这里插入图片描述

而上面的 pending 表就是一个位图,表示信号未决的状态;

那么 block 表也是一个位图,1表示被阻塞,0表示未阻塞。一旦阻塞了某个信号,在该信号没有被解除阻塞之前,即便收到了该信号,对应的信号也不会被操作系统进行递达。

所以 pending 表记录当前进程是否收到了信号以及收到了哪些信号;block 表记录特定信号是否被屏蔽;handler 表记录每种信号的处理方法。

所以对于某个信号,对应上面的三张表中,我们应该横向看,是否被屏蔽、是否收到、对应方法。所以我们对于信号的学习,无论给我们提供多少系统接口,都是围绕这三张表获取或者修改。

3. 信号保存系统接口

上面的两张表中,blockpending 是两张位图,也就是两个整数,我们当然可以用位操作去修改,但是整数都是32个比特位,而如果当操作系统想要扩展这两张位图的时候,一个整型就放不下了,所以操作系统给我们提供了一种用户层的类型,这个类型可以直接设置进操作系统的位图里。

而且上面的三张表都属于操作系统的内核数据结构,它不允许用户直接修改这三张表,所以操作系统必须给我们提供系统调用修改这三张表。我们要获取的 pending 表和 block 表都是位图,这就注定了要在用户空间到内核空间,内核空间到用户空间进行来回拷贝,所以数据拷贝时就要在系统调用接口的参数上设置输入输出型参数。

所以操作系统给我们提供了一种经过封装的数据类型,来获取内核中的位图,就是 sigset_t.

(1)sigset_t

sigset_t 其实就是一个位图结构,我们称为信号集。因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

sigset_t 类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作 sigset_t 变量,而不应该对它的内部数据做任何解释,比如用 printf 直接打印 sigset_t 变量是没有意义的。

				#include <signal.h>
				int sigemptyset(sigset_t *set);		// 清空信号集
				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);	// 判断指定信号是否在信号集中

(2)sigprocmask()

调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集),接口如下:

				int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

在这里插入图片描述

其中第一个参数代表我们需要设置还是获取 block 表,有三个选项,如下,它们之间是并列关系,只能三选一:

  • SIG_BLOCK:set 包含了我们希望添加到当前信号屏蔽字(block表)的信号,相当于 mask = mask | set;
  • SIG_UNBLOCK:set 包含了我们希望从当前信号屏蔽字(block表)中解除阻塞的信号,相当于 mask = mask & ~set;
  • SIG_SETMASK:设置当前信号屏蔽字(block表)为 set 所指向的值,相当于 mask = set;

第二个参数就是我们当前设置的信号集,它是一个输入型参数;第三个参数是一个输出型参数,当我们对进程的 block 表做修改的时候,在改之前,系统会将改之前的表通过 oldset 保存起来。那么修改之后,然后我们就能通过 oldset 拿到上一次的 block 表。 如果不需要的话可以设为 nullptr.

返回值则是成功返回0;失败返回-1.

在这里插入图片描述

(3)sigpending()

读取当前进程的未决信号集,通过 set 参数传出。调用成功则返回0,出错则返回-1;接口如下:

				int sigpending(sigset_t *set);

在这里插入图片描述

对于 sigpending 函数的唯一参数,它是一个输出型参数,就是调用进程所对应的 pending 表带出来。

(4)signal()

signal() 接口我们早就接触过了,它就是用来修改 handler 表的,接口如下:

				typedef void (*sighandler_t)(int);
				sighandler_t signal(int signum, sighandler_t handler);

在这里插入图片描述

(5)测试系统接口

下面我们写一个这样的代码:先将2号信号阻塞,然后打印出 pending 表,我们初始化的时候应该是全0的,然后我们给进程发送2号信号,因为2号信号被阻塞了,所以 pending 表中2号信号所对应的比特位在没有被解除阻塞前一直都是1的,然后我们打印 pending 表出来观察,是否如此。代码如下:

				void Print(sigset_t sigset)
				{
				    for(int i = 31; i >= 1; i--)
				    {
				        if(sigismember(&sigset, i))
				        {
				            cout << "1";
				        }
				        else
				        {
				            cout << "0";
				        }
				    }
				    cout << "     pid: " << getpid() << endl;
				}
				
				int main()
				{
				    sigset_t sigset;
				    sigemptyset(&sigset);   // 信号集清0
				    sigaddset(&sigset, 2);  // 将2号信号的比特位设置为1
				    sigset_t oldsigset;
				    
				    // 1. 屏蔽2号信号
				    sigprocmask(SIG_SETMASK, &sigset, &oldsigset);
				
				    while(1)
				    {
				        // 重复打印当前进程的 pending 表
				        int n = sigpending(&sigset);
				        if(n < 0) break;
				        Print(sigset);
				        sleep(1);
				
				        // 发送2号信号... kill -2 pid
				    }
				
				
				    return 0;
				}

结果如下:

在这里插入图片描述

注意,9号和19号信号也是不可以被阻塞的!

二、信号捕捉处理

1. 信号的处理

我们在上面说过,信号保存是为了让进程在合适的时候处理,那么信号是什么时候被处理的呢?首先进程要处理一个信号,前提是要知道自己收到信号了,就必须得合适的时候去查 pending表、block表和 handler表,而它们都属于内核数据结构,而这说明进程必须处于内核状态才能对信号做处理,所以结论就是,当进程从内核态返回到用户态的时候,进行信号的检测和处理

那么用户态什么时候才会陷入内核态呢?一般最常见的就是调用系统调用的时候,这时候操作系统是自动会做“身份切换”的,用户身份变成内核身份,或者反过来。

2. 理解用户态和内核态

下面我们开始理解用户态和内核态,这时候我们又要回到我们学习过的地址空间了,我们知道,每个进程PCB都有自己的地址空间,而我们以前也讲过,0~3GB 的空间为用户空间,3~4GB 为内核空间,如下图:

在这里插入图片描述

下面我们正式介绍一下内核空间,其实内核空间中映射的就是操作系统的代码和数据。由于操作系统是被计算机最先加载的软件,所以一般操作系统被加载的时候,它的代码和数据是被加载到靠内存的底侧的位置,那么内核空间怎么和操作系统的代码和数据建立映射呢?没错,它们之间还有一个内核级的页表!但是其实内核空间可以直接和物理内存建立映射,它就是固定的偏移量,用其中的地址减去3GB就可以得到,但是我们先不谈这种方式。

也就是说用户有用户级的页表映射到物理内存中,内核有自己的内核级页表映射到操作系统的代码和数据。那么当系统中有许多进程的话,有几个进程就有几份页表,因为进程之间具有独立性!但是内核级页表只有一份!所以所有进程的 3~4GB 的内核空间,和内核级页表,还有映射的操作系统的代码和数据,都是一样的!也就是说,在整个系统中,进程再怎么切换,3~4GB 的空间内容是不变的!

在这里插入图片描述

所以站在进程角度,所有的系统调用都在内核空间中,被进程所看到,所以每一个进程在调用系统调用时,在代码区调用,就可以相当于在自己的地址空间里面调用该方法,调用完成之后再返回自己的代码区中,就如同在自己的地址空间里直接调用!

那么站在操作系统角度,任何一个时刻,都有进程在执行。因为操作系统在我们开机的时候已经启动了,说明操作系统本身也是一个进程,那么它也要自己的地址空间,它甚至可以不要用户的空间,只要自己的内核空间。那么只要有进程在执行,我们想执行操作系统的代码,就可以随时执行!

其实操作系统的本质就是基于时钟中断的一个死循环。在计算机硬件中,有一个时钟芯片,在每一个非常短的时间内,会向CPU发送时钟中断;而CPU接收到了中断,就要执行该中断所对应的方法,这个中断所对应的方法就是操作系统的代码,相当于这个时钟中断在推动操作系统在运行!

那么我们再回到地址空间中,我们以前在进程中调用自己的方法或者代码,都是在用户区调的,但是当我们需要调用操作系统的代码,并不是我们想调就调的,因为用户无法直接访问操作系统!那么我们就要再介绍CPU了,在CPU中有一个叫做CR3的寄存器,这个寄存器直接指向的就是当前进程所对应的用户级页表,当进程被调度的时候,该进程的用户级页表的地址就会被放在这个寄存器中,这里保存的是物理地址。

我们怎么知道当前访问的是用户态还是内核态呢?那么,在CPU中还有一个寄存器叫做ecs寄存器,当我们执行用户态的代码的时候,ecs寄存器一定指向的是用户态的代码;如果我们切换到了内核,它就会指向内核中的代码;而在ecs寄存器里它的最低两个比特位,记录的是CPU的工作模式,其中CPU常见的工作模式有用户态和内核态,我们知道两个比特位一共就四种状态,那么它内部就是用 00(0)11(3) 分别代表内核态用户态。也就是说,如果我们想访问内核中的代码,我们必须要将ecs寄存器中的低两位由 11 设为 00,就变成内核态了,我们就可以访问操作系统的数据了!那么谁能修改ecs寄存器呢?所以CPU必须给我们提供一个方法,能够改变CPU的工作级别,于是就有了 int 80,这其实是一个汇编语句,意思就是陷入内核

在这里插入图片描述

所以我们就能理解什么是用户态,什么是内核态了。其中,

  • 内核态:允许访问操作系统的代码和数据
  • 用户态:只能访问用户自己的代码和数据

3. 信号的捕捉

我们理解了内核态和用户态之后,我们下面结合下图来理解信号的捕捉:

在这里插入图片描述

所以信号保存是为了让进程在合适的时候处理,那么信号是在内核态返回用户态时进行处理的!

4. 系统调用

(1)sigaction()

我们前面已经知道信号捕捉可以使用 signal(),那么除了 signal() 之外,还有一个系统调用接口:

			int sigaction(int signum, const struct sigaction *act,
                 	struct sigaction *oldact);

在这里插入图片描述

在这里插入图片描述

sigaction() 的功能也是捕捉特定信号,和 signal() 功能一模一样。它的第一个参数是信号的编号;第二个参数和第三个参数的类型是一样的,都是 struct sigaction*,而第二个参数是输入型参数,它是把我们用户设置的自定义捕捉方法以及其它信息,通过 act 传递给操作系统;第三个参数 oldact 就是输出型参数,就是将旧的方法保存给我们传递出来。

下面我们看一下 struct sigaction 的结构体:

				struct sigaction {
					void     (*sa_handler)(int);
					void     (*sa_sigaction)(int, siginfo_t *, void *);
					sigset_t   sa_mask;
					int        sa_flags;
					void     (*sa_restorer)(void);
				};

以上的五个字段中,我们只需要知道第一个字段 sa_handler 和第三个字段 sa_mask 即可,其它都是与实时信号有关的字段,我们不用关心。

例如我们使用如下代码进行测试:

				void myhandler(int signo)
				{
				    cout << "...get a signal: " << signo << endl;
				}
				
				int main()
				{
				    struct sigaction act, oact;
				    memset(&act, 0, sizeof(act));
				    memset(&oact, 0, sizeof(oact));
				    act.sa_handler = myhandler;
				
				    sigaction(2, &act, &oact);
				
				    while(1)
				    {
				        cout << "i am a process, pid: " << getpid() << endl;
				        sleep(1);
				    }
				
				    return 0;
				}

在这里插入图片描述

(2)pending 表的置0顺序

下面我们说一下,当进程收到一个信号,pending 位图对应的位置变成1,那么它是在执行对应方法前由1置0还是在执行对应方法后由1置0呢?

我们可以在执行捕捉方法时,打印 pending 表,观察 pending 表在执行捕捉方法时对应的位置是否已经置0,如果已经置0,说明是在执行捕捉方法前由1置0,否则相反,下面我们验证一下:

				void PrintPending()
				{
				    sigset_t sigset;
				    sigpending(&sigset);
				
				    for(int signo = 31; signo >= 1; signo--)
				    {
				        if(sigismember(&sigset, signo))
				            cout << "1";
				        else
				            cout << "0";
				    }
				    cout << ",   ";
				}
				
				void myhandler(int signo)
				{
				    PrintPending();
				    cout << "...get a signal: " << signo << endl;
				}
				
				int main()
				{
				    struct sigaction act, oact;
				    memset(&act, 0, sizeof(act));
				    memset(&oact, 0, sizeof(oact));
				    act.sa_handler = myhandler;
				
				    sigaction(2, &act, &oact);
				
				    while(1)
				    {
				        cout << "i am a process, pid: " << getpid() << endl;
				        sleep(1);
				    }
				
				    return 0;
				}

结果如下:

在这里插入图片描述

如上,说明一旦收到信号时 pending 表的对应位置置,一旦进行信号递达时,先将 pending 表的对应位置由1置0,再执行方法。

(3)struct sigaction 中的 sa_mask 字段

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前信号处理结束为止。也就是说,不允许同一个信号不断向一个进程发送,使进程不断执行该信号的处理函数。

下面我们也可以验证一下,我们只需要将上面代码的自定义处理方法修改一下即可,我们在 myhandler 中写个死循环打印 pending表,这样就能让2号信号一直在处理了,这时候我们再给进程发送2号信号,这时候2号信号位置的 pending 表应该变成1,如下代码:

				void myhandler(int signo)
				{
				    cout << "...get a signal: " << signo << endl;
				    while(1)
				    {
				        PrintPending();
				        sleep(1);
				    }
				}

结果如下:

在这里插入图片描述

那么 struct sigaction 中的 sa_mask 字段是用来干什么的呢?正如我们上面所说,如果正在处理2号信号,2号信号会被屏蔽,那么如果还希望自动屏蔽另外一些信号,则用 sa_mask 字段说明这些需要额外屏蔽的信号,当信号处理函数返回时会自动恢复原来的信号屏蔽字。

我们在给 sa_mask 字段说明需要屏蔽哪些信号时,需要使用 sigaddset 设置信号集,然后往 sa_mask 中设置即可,例如,添加屏蔽3号信号:

				sigaddset(&act.sa_mask, 3);

三、信号扩展

1. 可重入函数

当我们进行链表插入时,假设插入节点 node1insert 分为两个步骤,先连接 next 指针,再更新 head 指针,那么如果我们在刚刚完成第一步的时候,因为硬件中断等原因使进程切换到内核,再次回用户态之前检查到有信号待处理,于是就去处理该信号,而该信号的处理方法又是自定处理方法,该方法就是再插入一个节点 node2,那么该方法执行完毕后返回用户态,此时的 head 指向 node2。然后继续回到插入 node1 的代码中完成剩下的代码,最后 head 指向了 node1,此时 node2 发生节点丢失,内存泄漏!如下图:

在这里插入图片描述

更形象的图如下:

在这里插入图片描述

像上例这样,insert 函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入insert 函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数;反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

2. volatile

该关键字在 C++ 当中的类型转换我们已经有所涉猎,今天我们站在信号的角度重新理解一下。

我们先看以下代码,定义一个全局的 flag,然后在 main 函数中以 flag 为恒真条件执行死循环,最后打印一语句,我们知道,这种情况下该语句是不会被打印的:

				int flag = 1;
				int main()
				{
				    while(flag);
				    
				    cout << "process quit!" << endl;
				    return 0;
				}

我们只能通过 ctrl + c 发送2号信号终止该进程:

在这里插入图片描述

但是今天我们可以使用信号捕捉,对2号信号自定义方法中将 flag 的值修改为1,这样就可以让主程序跳出死循环,从而打印该语句了,如下:

				int flag = 1;
				
				void myhandler(int signo)
				{
				    flag = 0;
				}
				
				int main()
				{
				    signal(2, myhandler);
				    while(flag);
				    
				    cout << "process quit!" << endl;
				    return 0;
				}

结果如下:

在这里插入图片描述

但是如果在优化条件下,当编译器检测到我们的 flag 在主程序中并没有被修改的时候,flag 变量可能被直接优化到 CPU 内的寄存器中,即每次读取 flag 的数据的时候,只在 CPU 中读取,但是 flag 在内存中也有对应的空间,当我们使用信号捕捉修改 flag 的值时,只会修改内存中的 flag 的值,不会影响 CPU 中的 flag 的值!

那么这个优化条件怎么设置呢?在 g++ 下,这种优化条件一般是被关闭的,需要在编译时加上选项设置,那么在 g++ 中设置这种优化条件的选项为 g++ -O1,其中 O1、O2、O3 都可以,我们可以验证一下:

在这里插入图片描述

如上,我们捕捉2号信号将 flag 修改为 0 也无法终止死循环,因为此时被优化后是直接从 CPU 中读取 flag 的值了。

那么如果此时我们想关闭这种优化,我们就可以在 flag 前加上 volatile 关键字,如下:

				volatile int flag = 1;

在这里插入图片描述

所以加上 volatile 关键字就是为了防止编译器过度优化,保持内存的可见性!

3. SIGCHLD 信号

我们在进程控制的时候讲过用 waitwaitpid 函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发 SIGCHLD 信号,也就是 17 号信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD 信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用 wait 清理子进程即可。

但是由于 UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用 sigaction 或者 signalSIGCHLD 的处理动作置为 SIG_IGN,也就是忽略,这样 fork 出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

但是上面不是说该信号的默认处理动作是忽略的吗?为什么还要我们自己使用系统接口处理呢?其实系统对于17号信号的默认处理动作是 SIG_DFL,也就是使用默认处理动作,只不过 SIG_DFL 默认执行的动作是忽略!而我们自己使用接口设置的 SIG_IGN 就是直接将默认处理动作设置为忽略!还记得我们上一节讲的,信号的处理方式有三种:默认动作、忽略、自定义动作 吗?其中 SIG_DFL 就是默认动作,SIG_IGN 就是忽略!

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

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

相关文章

【机器学习】支持向量机(SVM)

支持向量机&#xff08;SVM&#xff09; 1 背景信息 分类算法回顾 决策树 样本的属性非数值 目标函数是离散的 贝叶斯学习 样本的属性可以是数值或非数值目标函数是连续的&#xff08;概率&#xff09; K-近邻 样本是空间&#xff08;例如欧氏空间&#xff09;中的点目标函…

《统计学简易速速上手小册》第9章:统计学在现代科技中的应用(2024 最新版)

文章目录 9.1 统计学与大数据9.1.1 基础知识9.1.2 主要案例&#xff1a;社交媒体情感分析9.1.3 拓展案例 1&#xff1a;电商销售预测9.1.4 拓展案例 2&#xff1a;实时交通流量分析 9.2 统计学在机器学习和人工智能中的应用9.2.1 基础知识9.2.2 主要案例&#xff1a;预测客户流…

CSS基础---新手入门级详解

CSS:层叠样式表 CSS&#xff08;Cascading Style Sheets,层叠样式表&#xff09;&#xff0c;是一种用来为结构化文档添加样式&#xff08;字体、间距和颜色&#xff09;的计算机语言&#xff0c;css扩展名为.css。 实例: <!DOCTYPE html><html> <head><…

自然语言处理(NLP)—— 基本概念

自然语言处理&#xff08;Natural Language Processing&#xff0c;简称NLP&#xff09;是人工智能和语言学领域的一个分支&#xff0c;它涉及到计算机和人类&#xff08;自然&#xff09;语言之间的相互作用。它的主要目标是让计算机能够理解、解释和生成人类语言的数据。NLP结…

二十、K8S-1-权限管理RBAC详解

目录 k8s RBAC 权限管理详解 一、简介 二、用户分类 1、普通用户 2、ServiceAccount 三、k8s角色&角色绑定 1、授权介绍&#xff1a; 1.1 定义角色&#xff1a; 1.2 绑定角色&#xff1a; 1.3主体&#xff08;subject&#xff09; 2、角色&#xff08;Role和Cluster…

树莓派编程基础与硬件控制

1.编程语言 Python 是一种泛用型的编程语言&#xff0c;可以用于大量场景的程序开发中。根据基于谷歌搜 索指数的 PYPL&#xff08;程序语言流行指数&#xff09;统计&#xff0c;Python 是 2019 年 2 月全球范围内最为流行 的编程语言 相比传统的 C、Java 等编程语言&#x…

文章页的上下篇功能是否有必要?boke112百科取消上下篇功能

也不知道是从什么时候开始&#xff0c;我们很多站长的博客网站文章页都会在文末添加上“上一篇”和“下一篇”功能&#xff0c;目的是进行站内SEO优化和方便用户阅读上下篇文章。 boke112百科不管是以前使用的Three主题还是现在使用的YIA主题&#xff0c;刚开始的文章页都是有…

微信,支付宝在线换钱平台系统源码

探索全新、全开源的在线换钱系统源码&#xff0c;它将以前所未有的方式改变您的支付体验。我们为您精心打造了一个集简单易用与安全高效于一身的优质产品&#xff0c;它采用最新的技术开发&#xff0c;为您带来前所未有的便捷与安心。 这款在线换钱系统源码设计直观&#xff0…

【hcie-cloud】【27】华为云Stack网络安全防护

文章目录 前言网络安全概述常见网络攻击类型流量型攻击DDoS单包攻击网络攻击防范 网络安全服务华为云Stack网络防护HCS租户网络纵深防护HCS常用网络安全防护服务对比 云防火墙详述云防火墙&#xff08;CFW&#xff09;- 定义云防火墙&#xff08;CFW&#xff09;- 实现原理云防…

Days28 ElfBoard 板]修改开机动画

1.可能需要安装的库 elfubuntu:~/work/psplash$ sudo apt-get install build-essential libncurses5-dev elfubuntu:~/work/psplash$ sudo apt-get install libtool elfubuntu:~/work/psplash$ sudo apt-get install gettext elfubuntu:~/work/psplash$ sudo apt-get install l…

Linux--基础开发工具篇(2)(vim)(配置白名单sudo)

目录 前言 1. vim 1.1vim的基本概念 1.2vim的基本操作 1.3vim命令模式命令集 1.4vim底行命令 1.5 异常问题 1.6 批量注释和批量去注释 1.7解决普通用户无法sudo的问题 1.8简单vim配置 前言 在前面我们学习了yum&#xff0c;也就是Linux系统的应用商店 Linux--基础开…

Redis核心技术与实战【学习笔记】 - 30.番外篇:Redis学习资料、运维说明及使用规范建议

1.Redis学习资料 虽然前面已经学习了 Redis 理论和技术点&#xff0c;但是如果想要持续提升自己的技术能力&#xff0c;还是需要不断丰富自己的知识体系。本章&#xff0c;给你推荐几本优秀的书籍&#xff0c;以及拓展知识面的其他资料。 1.1 经典书籍 在学习 Redis 时&…

python coding with ChatGPT 打卡第20天| 二叉搜索树:搜索、验证、最小绝对差、众数

相关推荐 python coding with ChatGPT 打卡第12天| 二叉树&#xff1a;理论基础 python coding with ChatGPT 打卡第13天| 二叉树的深度优先遍历 python coding with ChatGPT 打卡第14天| 二叉树的广度优先遍历 python coding with ChatGPT 打卡第15天| 二叉树&#xff1a;翻转…

Python程序一直在window后台进程运行

CMD命令执行方法 windows 后台运行并输出日志文件 命令&#xff1a; python qipa250.py >> qipa250_logs.log 2>&1 & 窗口关闭后程序也就关闭了 windows 前台运行并输出日志文件 命令&#xff1a; pythonw qipa250.py >> qipa250_logs.log 2>&a…

第74讲Breadcrumb 面包屑实现

Breadcrumb 面包屑实现 为了实现二级路由&#xff0c;我们搞成搞个子路由&#xff0c;对于二级菜单 const routes [{path: /,name: 首页,component: () > import(../views/layout),redirect:/home,children:[{path: /home,name: 首页,component: () > import(../views…

飞天使-k8s知识点15-kubernetes散装知识点4-CNI网络插件与kubectl

文章目录 CNI 网络插件安装任意节点运行kubectlAPI的版本区别与废弃API查询 CNI 网络插件安装 这里将以 Calico 为例&#xff0c;提供在 Kubernetes 1.20.6 版本上安装 CNI 插件的步骤。请注意&#xff0c;具体的步骤可能会因 CNI 插件的类型和你的特定环境而略有不同。设置 Ku…

linux系统下vscode portable版本的c++/Cmake环境搭建001

linux系统下vscode portable版本的Cmake环境搭建 vscode portable 安装安装基本工具安装 build-essential安装 CMake final script code安装插件CMake Tools & cmakeC/C Extension Pack Testsettings,jsonCMakeLists.txt调试和运行工具 CG 目的&#xff1a;希望在获得一个新…

PKI - 借助Nginx实现_客户端使用CA根证书签发客户端证书

文章目录 Pre概述步骤1. 创建根证书2. 生成客户端证书3. 准备客户端证书扩展文件4. 签发客户端证书5. 配置Nginx5. 重启 Nginx6. 测试 SAN 证书扩展案例&#xff1a;使用IP访问 Pre PKI - 借助Nginx 实现Https 服务端单向认证、服务端客户端双向认证 PKI - 数字签名与数字证书…

24个已知403绕过方法的利用脚本

介绍 一个简单的脚本&#xff0c;仅供自用&#xff0c;用于绕过 403 在curl的帮助下使用24个已知的403绕过方法 它还可用于比较各种条件下的响应&#xff0c;如下图所示 用法 ./bypass-403.sh https://example.com admin ./bypass-403.sh website-here path-here 安装 git …

C#,泰波拿契数(Tribonacci Number)的算法与源代码

1 泰波拿契数&#xff08;Tribonacci Number&#xff09; 泰波拿契数&#xff08;Tribonacci Number&#xff09;是斐波那契的拓展。 泰波拿契数 (Tribonacci Number) 即把费波拿契数 (Fibonacci Number) 的概念推广至三个数。 2 计算结果 3 源程序 using System; namespace…