【Linux系统编程】第三十九弹---探索信号处理的奥秘:阻塞信号与sigset_t的深入剖析及实战

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、信号处理

2、阻塞信号

2.1、信号其他相关常见概念 

2.2、在内核中的表示

2.3、sigset_t 

2.4、信号集操作函数

3、完整代码

3.1、Makefile

3.2、testsig.cc


1、信号处理

补充:云服务器之间文件传输

scp 文件名 另外一个云服务器ip:文件路径 # 会需要用到另外服务器的密码

 信号的默认处理动作中有两个终止程序,区别是什么呢?

core默认操作是终止进程并转储核心(帮助我们形成debug文件 -- 进程退出的时候的镜像数据)。

term默认操作是终止进程

使用对空指针进行解引用或者除零错误做测试!!! 

#include <iostream>
#include <unistd.h>int main()
{// int* p = nullptr;// *p = 10;std::cout << getpid() << std::endl;int a = 10;a /= 0;return 0;
}

1、直接执行可执行程序不会进行核心转储,因为云服务器默认是关闭的。

ulimit -a : 显示当前用户的所有资源限制情况。

2、ulimit -c 文件大小,单位为字节 : 设置核心文件大小

3、再执行可执行程序就会进行核心转储,生成core文件(ubuntu(20.04) 和 centos系统方式不一样,ubuntu只能core dump一个文件centos以 core + pid 的方式生成文件,可以core dump多个文件)。

注意:ubuntu22.04版本可能默认不会生成core文件

# 将 core 文件的命名模式设置为 core.[PID],普通用户需要使用sudo提权
sudo bash -c "echo core.%p > /proc/sys/kernel/core_pattern"  

在 Bash shell 环境中执行一个命令,该命令将 /proc/sys/kernel/core_pattern 文件的内容设置
core.%p。这里,/proc/sys/kernel/core_pattern 是一个系统文件,用于控制当程序崩溃时生成的 core 文件的命名和行为。 

centos 7 

ubuntu 22.04

 云服务器为什么要关闭核心转储?

1、节省存储空间,生成核心转储文件(core dump)可能会占用大量磁盘空间,特别是在服务频繁崩溃或异常终止的情况下。

2、保护用户隐私和数据安全,核心转储文件包含了程序崩溃时的内存快照和寄存器状态,可能包含敏感信息,如用户数据、密钥等。 

3、core是协助我们进行debug的文件 --- 事后调试。

通过核心转储文件查看代码在哪里崩溃的? 

apt install gdb # ubuntu下下载gdbcore-file core文件名  # 查看代码在哪里崩溃

遗留问题:

获取子进程status中,前面我们只讲解了退出码和退出信号,还有一个标记位就是核心转储标志

代码测试

int Sum(int start, int end)
{int sum = 0;for (int i = start; i <= end; i++){// sum /= 0; // 除零错误sum += i;}return sum;
}int main()
{// int total = Sum(0, 100);// std::cout << "total = " << total << std::endl;pid_t id = fork();if (id == 0){// childsleep(1);int total = Sum(0, 100);std::cout << "total = " << total << std::endl;exit(1);}// fatherint status = 0;int rid = waitpid(id, &status, 0);if (rid > 0){printf("exit code: %d, exit sig: %d, core dump: %d\n",(status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);}return 0;
}

2、阻塞信号

2.1、信号其他相关常见概念 

  • 实际执行信号的处理动作称为信号递达(Delivery)  --- 默认,忽略,自定义捕捉
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

一个信号 是否递达 和 他 有没有未决 有关系?   无关

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2.2、在内核中的表示

信号在内核中的表示示意图

  1. block表(信号屏蔽字表)
    • 表示是否对信号进行阻塞
    • 使用位图(BitSet)来存储,若信号被阻塞,则相应位置1,否则置0。
    • 此表用于控制哪些信号当前被阻塞,不会被处理。
  2. pending表(未决位图表)
    • 记录当前未决信号,即信号已产生但尚未递达的状态
    • 也使用位图来存储,若信号存在,则相应位置1,否则置0。
    • pending表中的数据是判断信号是否存在的唯一依据。当信号产生但未被处理(可能因阻塞或其他原因)时,它会被记录在pending表中。
  3. handler表(处理函数表)
    • 存储信号的处理方法的指针,通常是一个函数指针数组。
    • 每个信号的处理函数指针都存储在相应的位置,当信号递达时,会根据handler表找到相应的处理函数并执行。
    • 此表不直接用于判断信号是否产生,而是定义了信号被递达时应执行的动作。

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

2.3、sigset_t 

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。 

  • sigset_t -> Linux给用户提供的一个用户级的数据类型, 禁止用户直接修改位图 

2.4、信号集操作函数

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

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  1. int sigemptyset(sigset_t *set);
    • 功能:初始化信号集,将信号集中的所有信号都设置为未设置状态(即清空信号集,表示该信号集不包含 任何有效信号)
    • 参数:set 指向要初始化的信号集的指针。
    • 返回值:成功时返回0;失败时返回-1,并设置errno以指示错误。
  2. int sigfillset(sigset_t *set);
    • 功能:将信号集中的所有信号都设置为已设置状态(即填充信号集,表示使其包含所有信号)。
    • 参数:set 指向要填充的信号集的指针。
    • 返回值:成功时返回0;失败时返回-1,并设置errno以指示错误。
  3. int sigaddset(sigset_t *set, int signo);
    • 功能:将指定的信号添加到信号集中
    • 参数:set 指向信号集的指针;signo 是要添加的信号编号。
    • 返回值:成功时返回0;如果signo无效(即不是一个有效的信号编号),则返回-1,并设置errno以指示错误。
  4. int sigdelset(sigset_t *set, int signo);
    • 功能:从信号集中删除指定的信号
    • 参数:set 指向信号集的指针;signo 是要删除的信号编号。
    • 返回值:成功时返回0;如果signo无效,则返回-1,并设置errno以指示错误。
  5. int sigismember(const sigset_t *set, int signo);
    • 功能:检查指定的信号是否存在于信号集中
    • 参数:set 指向信号集的指针(注意这里是const,表示该信号集不会被修改);signo 是要检查的信号编号。
    • 返回值:如果signo在信号集中,则返回1;如果不在,则返回0;如果signo无效,则返回-1,并设置errno以指示错误。

注意, 在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

sigprocmask()

sigprocmask - 更改(阻塞、解除阻塞或查询)进程信号屏蔽字#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how:指定了如何更改进程的信号屏蔽字。它可以是以下三个常量之一:SIG_BLOCK:将 set 指向的信号集中的信号添加到当前进程的信号屏蔽字中,即阻塞这些信号。SIG_UNBLOCK:从当前进程的信号屏蔽字中移除 set 指向的信号集中的信号,即解除对这些信号的阻塞。SIG_SETMASK:将当前进程的信号屏蔽字设置为 set 指向的信号集,忽略 oldset 参数(如果它不为 NULL,
则仍然会保存旧的屏蔽字)。set:是一个指向 sigset_t 类型的指针,它包含了要添加或移除的信号集。
如果 how 参数是 SIG_SETMASK,则它指定了新的信号屏蔽字。oldset:是一个指向 sigset_t 类型的指针,用于保存调用 sigprocmask 之前的信号屏蔽字。
如果 oldset 是 NULL,则不保存旧的屏蔽字。返回值:成功时,sigprocmask 返回 0;失败时,返回 -1,并设置 errno 以指示错误。
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
  • 如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
  • 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

sigpending()

sigpending - 获取当前进程pending位图#include <signal.h>int sigpending(sigset_t *set); // 输出型参数set:参数是一个指向 sigset_t 类型的指针,该函数会将当前进程的挂起信号集复制到 set 指向的位图中。返回值:成功时,sigpending 返回 0;失败时,返回 -1,并设置 errno 以指示错误。

用到的头文件 

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

测试对2号信号屏蔽的现象

打印未决表

void PrintPending(sigset_t& pending)
{std::cout << "curr process[" << getpid() << "]pending: ";for(int signo = 31;signo >= 1;signo--){// 判断signo信号是否在pending中if(sigismember(&pending,signo)) {std::cout << "1";}else{std::cout << "0";}}std::cout << "\n";
}

主函数

int main()
{// 1.屏蔽2号信号sigset_t block_set,old_set;sigemptyset(&block_set); // 清空信号集sigemptyset(&old_set);sigaddset(&block_set,SIGINT); // 屏蔽2号信号,没有修改内核结构// 1.1.进入进程的block表// 阻塞block_set信号集中的信号,old_set为输出型参数,保存原始信号集sigprocmask(SIG_BLOCK,&block_set,&old_set); // 修改当前进行的内核表,完成对2号信号的屏蔽int cnt = 10;while(true){// 2.获取当前信号集sigset_t pending;sigpending(&pending); // 输出型参数// 3.打印pending信号集PrintPending(pending);cnt--;// 4.解除对2号信号的屏蔽if(cnt == 0){std::cout << "解除对2号信号的屏蔽" << std::endl;// 解除对old_set信号集的屏蔽,原信号集保存到block_set中sigprocmask(SIG_SETMASK,&old_set,&block_set);}sleep(1);}return 0;
}

信号解除屏蔽之后,pending位图也要置零,是在递达前还是递达后呢?

通过对2号信号自定义捕捉来证明,捕捉之后在自定义捕捉函数内部打印pending表,如果打印的表中,2号信号的比特位为1表示在递达前置零,如果为0则在递达后置零。

自定义捕捉方法

// 测试在递达之前清零还是递达之后
void handler(int signo)
{std::cout << signo << "号信号被递达!!!" << std::endl;std::cout << "--------------------------" << std::endl;sigset_t pending;sigpending(&pending);PrintPending(pending);std::cout << "--------------------------" << std::endl;
}

主函数

int main()
{// 0.对2号信号自定义捕捉signal(2,handler);// 1.屏蔽2号信号sigset_t block_set,old_set;sigemptyset(&block_set); // 清空信号集sigemptyset(&old_set);sigaddset(&block_set,SIGINT); // 屏蔽2号信号,没有修改内核结构// 1.1.进入进程的block表// 阻塞block_set信号集中的信号,old_set为输出型参数,保存原始信号集sigprocmask(SIG_BLOCK,&block_set,&old_set); // 修改当前进行的内核表,完成对2号信号的屏蔽int cnt = 10;while(true){// 2.获取当前信号集sigset_t pending;sigpending(&pending); // 输出型参数// 3.打印pending信号集PrintPending(pending);cnt--;// 4.解除对2号信号的屏蔽if(cnt == 0){std::cout << "解除对2号信号的屏蔽" << std::endl;// 解除对old_set信号集的屏蔽,原信号集保存到block_set中sigprocmask(SIG_SETMASK,&old_set,&block_set);}sleep(1);}return 0;
}

3、完整代码

3.1、Makefile

testsig:testsig.ccg++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:rm -rf testsig

3.2、testsig.cc

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>// int main()
// {
//     int* p = nullptr;
//     *p = 10;
//     std::cout << getpid() << std::endl;
//     // int a = 10;
//     // a /= 0;
//     return 0;
// }// int Sum(int start, int end)
// {
//     int sum = 0;
//     for (int i = start; i <= end; i++)
//     {
//         sum /= 0; // 除零错误
//         sum += i;
//     }
//     return sum;
// }// int main()
// {
//     // int total = Sum(0, 100);
//     // std::cout << "total = " << total << std::endl;//     pid_t id = fork();
//     if (id == 0)
//     {
//         // child
//         sleep(1);
//         int total = Sum(0, 100);
//         std::cout << "total = " << total << std::endl;
//         exit(1);
//     }
//     // father
//     int status = 0;
//     int rid = waitpid(id, &status, 0);
//     if (rid > 0)
//     {
//         printf("exit code: %d, exit sig: %d, core dump: %d\n",
//                (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
//     }
//     return 0;
// }// int main()
// {
//     // sigset_t Linux给用户提供的一个用户级的数据类型, 禁止用户直接修改位图
//     sigset_t bits; //     return 0;
// }void PrintPending(sigset_t& pending)
{std::cout << "curr process[" << getpid() << "]pending: ";for(int signo = 31;signo >= 1;signo--){// 判断signo信号是否在pending中if(sigismember(&pending,signo)) {std::cout << "1";}else{std::cout << "0";}}std::cout << "\n";
}// 测试在递达之前清零还是递达之后
void handler(int signo)
{std::cout << signo << "号信号被递达!!!" << std::endl;std::cout << "--------------------------" << std::endl;sigset_t pending;sigpending(&pending);PrintPending(pending);std::cout << "--------------------------" << std::endl;
}int main()
{// 0.对2号信号自定义捕捉signal(2,handler); // 自定义捕捉signal(2,SIG_IGN); // 忽略一个信号signal(2,SIG_DFL); // 信号的默认处理动作// 1.屏蔽2号信号sigset_t block_set,old_set;sigemptyset(&block_set); // 清空信号集sigemptyset(&old_set);sigaddset(&block_set,SIGINT); // 屏蔽2号信号,没有修改内核结构// 1.1.进入进程的block表// 阻塞block_set信号集中的信号,old_set为输出型参数,保存原始信号集sigprocmask(SIG_BLOCK,&block_set,&old_set); // 修改当前进行的内核表,完成对2号信号的屏蔽int cnt = 10;while(true){// 2.获取当前信号集sigset_t pending;sigpending(&pending); // 输出型参数// 3.打印pending信号集PrintPending(pending);cnt--;// 4.解除对2号信号的屏蔽if(cnt == 0){std::cout << "解除对2号信号的屏蔽" << std::endl;// 解除对old_set信号集的屏蔽,原信号集保存到block_set中sigprocmask(SIG_SETMASK,&old_set,&block_set);}sleep(1);}return 0;
}

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

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

相关文章

零基础Apifox测试FastAPI接口入门

文章目录 一、FastAPI部分二、Apifox部分1、安装Apifox2、创建接口3、更改测试环境4、发送请求 一、FastAPI部分 python使用fastapi编写接口内容&#xff08;文件名&#xff1a;text.py&#xff09;&#xff1a; from fastapi import FastAPI import uvicornapp FastAPI()ap…

Linux终端退出程序后,TCP地址仍被占用

报错如下&#xff1a; Error on binding: Address already in use 这是一个正在运行的服务器&#xff0c;运行在linux的终端。上一次我使用CtrlZ退出这个程序&#xff0c;再次./my_server想运行这个程序时&#xff0c;出现这个报错。这是由两点原因&#xff1a; 1、守护进程或…

嵌入式通信协议:IIC简明学习笔记

IIC学习笔记 IIC特点 1.适合 小数据场合使用&#xff0c;传输距离短。 2.只能有一个主机。 3.标准IIC速度为100kHZ&#xff0c;高速IIC一般可达400kHZ以上。 4.SCL和SDA都需要接上拉电阻&#xff08;大小由速度和容性负载决定&#xff0c;一般在3.3k-10k之间&#xff09;。 5…

基于anaconda的python3.6安装opencv4.1.15

opencv-python一些新版本由于部分函数涉及专利问题&#xff0c;如sift和surf&#xff0c;有些功能不能很好地被使用&#xff0c;所以最好使用opencv-python 3.4.1.15版本的。 下载地址分别为&#xff1a; 1、https://pypi.tuna.tsinghua.edu.cn/simple/opencv-python/ 查找…

【自制操作系统】0x01MBR

环境 ubuntu 20.04 gcc 9.4.0&#xff08;加载硬盘程序之前都是&#xff0c;最后可能会切换到 gcc 4.4&#xff09; bochs 2.7 bochs 配置 bochs 安装之前文章记录过&#xff0c;现在记录一下本次使用的bochs配置 bochsrc #第一步&#xff0c;首先设置 Bochs 在运行过程中…

SpringBoot接入星火认知大模型

文章目录 准备工作整体思路接入大模型服务端和大模型连接客户端和服务端的连接测试 准备工作 到讯飞星火大模型上根据官方的提示申请tokens 申请成功后可以获得对应的secret&#xff0c;key还有之前创建的应用的appId&#xff0c;这些就是我们要用到的信息 搭建项目 整体思…

ssm056基于Java语言校园快递代取系统的设计与实现+jsp(论文+源码)_kaic

毕 业 设 计&#xff08;论 文&#xff09; 题目&#xff1a;校园快递代取系统设计与实现 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本校园快递代取系统…

简单语音信号识别的MATLAB仿真

简单语音信号识别的MATLAB仿真 摘要&#xff1a; 隐马尔可夫模型&#xff08;HMM&#xff09;作为描述语音信号的一种统计模型&#xff0c;在现代语音处理中获得了广泛应用。本文概述了基于HMM的语音识别技术&#xff0c;阐述了预处理&#xff0c;特征提取以及训练&#xff0c…

童年玩具:两款线绳陀螺

1,2图是过去用来安装明线电线的瓷夹。现在应该找不到了。过去安装电线后&#xff0c;家里留下了一些&#xff0c;拿来做线陀螺非常好。 因为它非常重&#xff0c;旋转起来很有力&#xff0c;那声音呼呼响。 3,4图是现在都能看到的一个圆木片&#xff0c;两个孔&#xff0c;穿绳…

AntFlow一款开源免费且自主可控的仿钉钉工作流引擎

在现代企业管理中&#xff0c;流程审批的高效性直接影响到工作的流畅度与生产力。最近&#xff0c;我发现了一个非常有趣的项目——AntFlow。这个项目不仅提供了一个灵活且可定制的工作流平台&#xff0c;还能让用户以可视化的方式创建和管理审批流程。 如果你寻找一个快速集成…

光纤资源APP开发及二次开发说明

光纤资源APP主要由以下几部分组成&#xff1a; 登录界面选择项目界面地图创建节点界面填写详细信息界面成端及端口表界面接续及接续表界面 其中1、2、4界面不需要涉及到ht&#xff0c;故用原生界面即可实现&#xff0c;但是3、5、6涉及到ht&#xff0c;而ht在app中是不兼容的…

鉴源实验室·如何通过雷达攻击自动驾驶汽车-针对点云识别模型的对抗性攻击的科普

01 引 言 随着自动驾驶技术的迅速发展&#xff0c;雷达和激光雷达等传感器在自动驾驶汽车中的作用愈发重要。它们能够生成3D点云数据&#xff0c;帮助车辆实时感知周围环境并做出安全决策。然而&#xff0c;尽管这些传感器对驾驶环境的检测非常精确&#xff0c;它们也面临一种…

Stable Diffusion(2024)Ai绘画AIGC最新安装包资源下载+自学教程

以下内容为整理的Stable Diffusion保姆级教学内容&#xff0c;请购买的资料的同学务必认真学习&#xff01;按以下步骤操作快速掌握Stable Diffusion这个工具&#xff01; Stable Diffusion&#xff08;简称SD&#xff09;是一款地表最强AI绘图工具(AIGC)之一&#xff0c;Stab…

基于单片机的宠物自动喂食系统的设计

本设计以STM32单片机为核心控制器&#xff0c;搭载了OLED显示屏作为显示交互模块&#xff0c;HX711称重模块获取食物重量&#xff0c;ESP8266与手机APP通信从而远程控制&#xff0c;PWM输出控制舵机模拟投喂食物开关打开&#xff0c;驱动继电器控制水泵打开加水&#xff0c;HC-…

vue+websocket实现即时聊天平台

目录 1 什么是websocket 2 实现步骤 2.1 导入依赖 2.2 编写代码 1 什么是websocket WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它主要用于在客户端和服务器之间建立持久的连接&#xff0c;允许实时数据交换。WebSocket 的设计目的是为了提高 Web 应用程序的…

Spring Boot框架:大学城水电管理自动化

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

关于三色标记算法的理解

三色标记算法是一种垃圾标记的算法&#xff0c;用于cms和g1。 它将对象分为3种颜色&#xff1a; 1.白色对象&#xff1a;未被标记的对象 2.灰色对象&#xff1a;自身被标记&#xff0c;引用的其它对象还没被标记 3.黑色对象&#xff1a;自身以及所引用的对象都被标记完 标记过…

Python Matplotlib:基本图表绘制指南

Python Matplotlib&#xff1a;基本图表绘制指南 Matplotlib 是 Python 中一个非常流行的绘图库&#xff0c;它以简单易用和功能丰富而闻名&#xff0c;适合各种场景的数据可视化需求。在数据分析和数据科学领域&#xff0c;Matplotlib 是我们展示数据的有力工具。本文将详细讲…

深入探讨SEO分析技巧助力网站流量提升

内容概要 在当前的数字化时代&#xff0c;SEO分析的重要性不言而喻。它是提升网站流量的关键工具&#xff0c;帮助站长有效地优化网站内容和结构。通过系统的SEO分析&#xff0c;站长可以掌握用户搜索行为和需求&#xff0c;从而制定出更具针对性的内容策略。例如&#xff0c;…

配置QINQ

1. 配置公司A和公司B的私有网络&#xff0c;创建对应的VLAN&#xff0c;并且接口的链路类型 S3的配置: 系统视图进入&#xff1a; <Huawei>system-view 设置设备名称为s3&#xff1a; [huawei]sysname s3 创建VLAN 10和20&#xff1a; [s3]vlan batch 10 20 配置Gigabit…