内核模块里获取当前进程和父进程的cmdline的方法及注意事项,涉及父子进程管理,和rcu的初步介绍

一、背景

在编写内核态系统监控代码时,有时候为了调试的便捷性,不仅要拿到异常事件有关的线程id,进程id和父进程id,还需要拿到当前进程和父进程的comm和cmdline。主要有几下几个原因:

1)单纯的pid或者tgid其信息本身并不能给我们多少线程有关的有效信息,除了一些系统的内核线程的pid是固定的以外,其他的线程的pid都是每次运行会发生变化的,这些信息如果不配合其他的监控信息就不能独立作为详细的调试依据

2)如果仅仅是获取comm也就是不带上args参数(cmdline当然是带上args参数的完整的命令)的话,那么其信息一般也是非常有限的,因为启动程序如果不是fork运行的话,都是要通过shell来解释运行,这时候该进程一开始运行的时候comm就继承了父进程的comm也就是bash,什么具体信息也提供不了,当然后面通过泛exec的系统调用变成了实际运行的程序以后,光一个程序名字有时候信息还是不够的,比如如果是运行一个脚本,如执行sudo python3 xxx,其实它是父子两个进程,父进程的comm是sudo,子进程的comm是python3,那么这时候如果只是抓comm信息的话,就算也同时抓父进程的comm,那也只能拿到sudo和comm这两个名字,也不知道到底运行的什么python3脚本。如下截图:

但如果去捞cmdline信息的话,你就能拿到详细的信息,能大致知道是跑的什么程序:

接下来,我们将在第二章里给出内核模块里获取到当前进程和父进程的cmdline方法,以及非常重要的注意事项,另外还会涉及父子进程管理的一些细节,并对用这些细节做了不少验证的实验,包括孤儿进程的归属问题,如child_subreaper的细节。在第三章里会贴一张第二章里讲到的获取进程和父进程的cmdline方法时用到的rcu这个内核重要的高性能神器。第三章里只会简单介绍一下rcu,关于rcu的复杂和繁琐的细节,后面的博客会逐一展开,我会在第三章里贴出总结的rcu的一些概念的思维导图截图,先对rcu相关的内容起一个头。注意,本文还并不涉及rt-linux下的rcu,rt-linux下的rcu比普通linux下的rcu在管理上更加复杂,暂且先不涉及。

本文有不少篇幅在进程线程管理细节上,尤其父子进程管理,相关的进程/线程管理的之前博文,可以参考 进程/线程创建和退出事件的捕获_for (inti=0;i<2;++i){ threads.emplace back(worker,-CSDN博客。

二、内核模块里获取当前进程和父进程的cmdline的方法

我们先在2.1一节里看一下核心代码,对代码进行一些说明,然后在2.2一节里讲一下注释事项重点时什么上下文下才能调用这段核心代码。

2.1 内核模块里获取当前进程和父进程的cmdline的核心代码

这里面其实分为两个子任务,一个是获取当前进程的父进程的pid,另一个是根据pid获取pid对应的cmdline。

为什么要这么分?因为获取当前进程的pid和其父进程的pid是没有上下文环境限制的,当前进程的pid非常简单,只需要current->pid即可,当前进程的父进程的pid的获取相对复杂一些,只需要加rcu的锁保护,确保current的real_parent变量在获取real_parent变量(real_parent变量是一个task_struct指针,指向task_struct表示的线程的父进程,一个线程通过getppid得到的是父进程的pid,而不是父进程里创建该子进程的线程id,这个会在2.3一节里做实验验证,但是如果通过real_parent->pid得到的是父进程里创建该子进程的线程id,这个会在2.4一节里做实验验证)在访问其pid信息时是有效的(在访问期间没被释放)。

而获取线程/进程的cmdline则是有上下文环境限制的,因为获取一个进程的cmdline并没有获取一个进程的comm那么简单(获取一个进程的comm直接从task_struct里的comm[16]数组里拷贝走就行了),获取一个进程的cmdline的获取期间需要加锁,需要放在可睡眠的上下文环境里,如kworker里。

2.1.1 获取当前进程的父进程的pid和comm的逻辑

先看代码:

void get_parent_pid_and_comm(struct output_items* io_pitems, struct task_struct* i_ptask)rcu_read_lock();parent = rcu_dereference(i_ptask->real_parent);io_pitems->currppid = parent->pid;strlcpy(io_pitems->currppidcomm, parent->comm, TASK_COMM_LEN);rcu_read_unlock();
}

从上面代码可以看到,需要用rcu锁来保护,在引用rcu保护的变量时,要用rcu_dereference去引用指针,然后再去获取指针指向的内容。关于rcu的细节见第三章及后面的博文。

关于real_parent的细节见下面的 2.1.1.1 一节。

2.1.1.1 关于task_struct的real_parent指针

首先如下图,task_struct里的real_parent是一个task_struct的指针,且受到rcu的保护:

关于ppid等父进程的信息的获取,需要使用real_parent变量,而不是其他变量,可以参考getppid系统调用的实现,如下:

我们实现的方式和系统调用getppid实现方式还是有一点区别,getppid系统调用用的是父进程id,我们直接用real_parent->pid是父进程里创建子进程的那个线程的线程id。getppid的例子验证在2.3一节,我们的real_parent->pid的例子验证在2.4一节。

不管是哪种,rcu锁还是要保护,使用real_parent这个指针也是一样。

关于real_parent这个受rcu锁保护的变量,在父进程退出以后,会进行刷新动作,如果不做prctl(PR_SET_CHILD_SUBREAPER)的动作的话,刷新后的real_parent指向到了systemd(pid是1),这个会在2.5一节里做实验,而做prctl(PR_SET_CHILD_SUBREAPER)的动作的话,刷新后的real_parent指向到了做prctl(PR_SET_CHILD_SUBREAPER)的进程,这个会在2.6一节里做实验。

下面,我把上面描述的相关的内核代码的部分贴出来:

上图中通过find_new_reaper函数找到了子收尸者,然后一一线程进行real_parent指针的重新assign,这里用的是RCU_INIT_POINTER这个宏,这个宏相对于常用的rcu_assign_pointer来赋值,它有性能优势,但是要使用起来非常小心,细节可以看到内核里的RCU_INIT_POINTER的宏的注释,这里就不展开了。

关于find_new_reaper函数是如何找子收尸者的,见下图,可以从注释里看到一些逻辑上的思路细节:

2.1.2 获取线程/进程的cmdline的逻辑

先看一下核心逻辑代码:

分为几个部分,先是根据pid找到对用的struct pid,通过struct pid获取task_struct的指针,这里面find_get_pid会增加pid结构体的引用计数,所以需要调用put_pid来释放。另外get_pid_task会增加task_struct的引用计数,所以在用完后,要通过put_task_struct来释放。下面代码里有一个my_set_cmdline函数,这个函数马上会介绍。

int pid_to_get_cmdline;
struct task_struct* ptask;
struct pid* pid_struct;
char temp_commandline[128];pid_struct = find_get_pid(pid_to_get_cmdline);
if (pid_struct) {ptask = get_pid_task(pid_struct, PIDTYPE_PID);if (ptask) {my_set_cmdline(temp_commandline, 128, ptask);put_task_struct(ptask);}else {temp_commandline[0] = '\0';}put_pid(pid_struct);
}
else {temp_commandline[0] = '\0';
}

my_set_cmdline函数的实现:

这个my_set_cmdline函数是调用了my_get_cmdline获取到cmdline的原始数据,但是cmdline原始数据内容每个arg参数的最后一个字节是\0,我们要把cmdline作为完整的字符串记到别的地方方便显示的话,要把这些参数最后的\0替换成空格,当然最后一个\0不能替换成空格。

void my_replace_null_with_space(char *str, int n) {for (int i = 0; i < n - 1; i++) {if (str[i] == '\0') {str[i] = ' ';}}
}void my_set_cmdline(char* i_pbuff, int i_buffsize, struct task_struct* i_ptask)
{int ret = my_get_cmdline(i_ptask, i_pbuff, i_buffsize);if (ret <= 0) {i_pbuff[0] = '\0';return;}my_replace_null_with_space(i_pbuff, ret);i_pbuff[ret - 1] = '\0';
}

下面我们来看一下核心逻辑里的核心my_get_cmdline函数:

int my_get_cmdline(struct task_struct *task, char *buffer, int buflen)
{int res = 0;unsigned int len;struct mm_struct *mm = get_task_mm(task);unsigned long arg_start, arg_end, env_start, env_end;if (!mm)goto out;if (!mm->arg_end)goto out_mm;	/* Shh! No looking before we're done */spin_lock(&mm->arg_lock);arg_start = mm->arg_start;arg_end = mm->arg_end;env_start = mm->env_start;env_end = mm->env_end;spin_unlock(&mm->arg_lock);len = arg_end - arg_start;if (len > buflen)len = buflen;res = access_process_vm(task, arg_start, buffer, len, FOLL_FORCE);/** If the nul at the end of args has been overwritten, then* assume application is using setproctitle(3).*/if (res > 0 && buffer[res-1] != '\0' && len < buflen) {len = strnlen(buffer, res);if (len < res) {res = len;} else {len = env_end - env_start;if (len > buflen - res)len = buflen - res;res += access_process_vm(task, env_start,buffer+res, len,FOLL_FORCE);res = strnlen(buffer, res);}}
out_mm:mmput(mm);
out:return res;
}

上面这段代码,其实内核里有这个函数的实现,只是这个函数并不作为export symbol给外部模块使用,我们拷贝了一份到我们模块里,这个函数里的主要函数access_process_vm是export symbol的,所以拿过来用没有什么问题。

参考的内核源码里的函数是(mm/util.c里get_cmdline函数):

需要注意,相对早一些的内容,需要多包含linux/sched/mm.h这个头文件,下面的是我当前这个用到nmi和sched的tracepoint以及interrupt和workqueue和hrtimer的这些的头文件集合,供参考:

#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/osmonitor.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <trace/events/kmem.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <asm/processor.h>
#include <linux/sched/task_stack.h>
#include <linux/nmi.h>
#include <asm/apic.h>
#include <linux/version.h>
#include <linux/sched/mm.h>

我们回到内核里的get_cmdline这个函数,看看这个函数是怎么拿到任意一个task_struct的cmdline的。

由于cmdline内容是属于具体进程上下文内容部分的,需要借助具体进程上下文的vm_area_struct信息:

get_cmdline用到了access_process_vm,access_process_vm里用到了__access_remote_vm,__access_remote_vm用到了vm_area_struct和get_user_page_vma_remote函数:

而get_user_page_vma_remote其实就是调用的之前的博客 里讲到get_user_pages_remote函数,关于get_user_pages_remote和get_user_pages/pin_user_pages 参考之前的博客 非gdb方式观察应用程序的运行时的变量状态_程序运行变量监控-CSDN博客 和 内存管理之——get_user_pages和pin_user_pages及缺页异常-CSDN博客。

要注意,从get_cmdline里设的是FOLL_FORCE作为gup_flags传下来,在__access_remote_vm里带上了FOLL_WRITE参数,一块传给get_user_page_vma_remote里,虽然和get_user_pages和pin_user_pages一样最终调用了__get_user_pages,但是和get_user_pages和pin_user_pages不一样的是,get_user_pages和pin_user_pages会带上能锁住内存的FOLL_GET或FOLL_PIN标志位,而这里的__access_remote_vm里和非gdb方式观察应用程序的运行时的变量状态_程序运行变量监控-CSDN博客 博客里讲到的get_user_pages_remote函数都不会带上这两个锁内存的标志位来传给最终调用的__get_user_pages核心函数。具体关于锁内存相关内容,见 内存管理相关——malloc,mmap,mlock与unevictable列表_mlock内存可以迁移吗-CSDN博客 和 内存管理之——get_user_pages和pin_user_pages及缺页异常-CSDN博客。

2.2 什么上下文下才能调用这段核心代码

其实在2.1里也提及了部分原因,这里再强调一下,分了两个部分,获取当前进程的pid和父进程的pid是可以在任何上下文,包括中断里甚至nmi中断里。而获取一个进程的cmdline则不能在硬中断里执行,因为获取逻辑里可能会涉及缺页异常,导致中断里套中断;另外,获取逻辑会使用spin_lock,非spin_lock_irqsave,其运行期间会被硬中断打断,就算用了spin_lock_irqsave也还是会被nmi打断,所以放到硬中断里容易发生spinlock死锁,稍有不慎就panic或卡死。所以,获取进程的cmdline这段逻辑,必须运行在可睡眠的上下文里,推荐的就是运行在kworker里。

2.3 一个子进程通过getppid得到的是其父进程的pid而不是父进程里创建该子进程的线程id

关于这个,做一下实验验证:

实验代码:

include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>// 获取线程 ID 的宏
#define gettid() syscall(SYS_gettid)// 线程函数
void *thread_function(void *arg) {printf("Process ID: %ld\n", getpid());printf("Thread ID: %ld\n", gettid());// 创建子进程pid_t pid = fork();if (pid < 0) {perror("fork failed");return NULL;}if (pid == 0) {// 子进程printf("In child process:\n");printf("Child PID: %d\n", getpid());printf("Child TID: %ld\n", gettid());printf("Parent PID: %d\n", getppid());exit(0); // 子进程结束} else {// 父进程wait(NULL); // 等待子进程结束}return NULL;
}

请用gcc来编译,用g++编译会找不到wait函数

运行后的结果:

可以看到进程105597里创建了一个线程105598,在这个105598线程里,创建了一个子进程105599,子进程里读取自己的parent pid,用的是getppid()这个函数,得到的是父进程的pid,而不是父进程里创建该子进程的线程id

这里,普及一个基础:

用户态的pid是表示进程id,用户态的tid是表示线程id

内核态的pid是表示线程id,内核态的tgid是表示当前线程所在的进程的线程id,也就是thread group id,即tgid

2.4 通过real_parent->pid得到的是父进程里创建该子进程的线程id

借助2.3的程序,在退出前增加一个while(1)死循环,然后,在insmod的内核模块的sched_stat_runtime里判断是子进程的pid就打印其real_parent的pid和tgid,来做确认。

先加上while(1)死循环逻辑,然后编译运行:

在sched_stat_runtime的注册的tracepoint回调里编写判断和打印逻辑(关于tracepoint如何添加自定义注册和内核模块里添加自定义注册的方法参考之前的博客 内核模块注册调度的tracepoint的回调,逻辑里判断当前线程处于内核态还是用户态的方法-CSDN博客 内核tracepoint的注册回调及添加的方法_tracepoint 自定义回调-CSDN博客):

dmesg里的输出如下:

这个结果其实也印证了在2.1.1.1一节里提到的getppid系统调用里的实现:

上图中用的是task_tgid_vnr,注意里面包含了tgid的字样

2.5 默认情况下,在父进程退出以后,子进程的real_parent会被指向到pid是1的systemd进程

所谓默认情况,就是不做prctl(PR_SET_CHILD_SUBREAPER)的动作。关于做prctl(PR_SET_CHILD_SUBREAPER)的实验,我们在2.6一节里做实验

下面的实验是在2.4的程序的基础上再做一下改动来实验:

把testppid.c的父进程的wait动作去掉:

cb_sched_stat_runtime里的改动:

dmesg的输出信息:

对于用户态里进行getppid获取也一样,sleep(10)以后获取的结果如下:

如果通过pstree -p | grep <pid>去查看进程父子链关系,可以看到其实如果父进程不退出的话,它的父子链是很长的

2.6 prctl(PR_SET_CHILD_SUBREAPER)设置当前进程为子“收尸者”,即child_subreaper

相关的代码逻辑在2.1.1.1里已涉及。

这里直接做实验,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/prctl.h>int main() {// 设置当前进程为子收尸者if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {perror("prctl(PR_SET_CHILD_SUBREAPER) failed");return 1;}// 创建父进程pid_t parent_pid = fork();if (parent_pid < 0) {perror("fork failed");return 1;}if (parent_pid == 0) {// 这是父进程// 创建子进程pid_t child_pid = fork();if (child_pid < 0) {perror("fork failed");return 1;}if (child_pid == 0) {// 这是子进程sleep(2);printf("Child process (PID: %d)(ppid: %d) is exiting...\n", getpid(), getppid());exit(0); // 子进程正常退出} else {// 父进程printf("Parent process (PID: %d) is exiting...\n", getpid());exit(0); // 父进程正常退出}} else {// 这是祖父进程printf("Grandparent process (PID: %d) is waiting for child...\n", getpid());// 等待子进程结束{int status;while(wait(&status) > 0); // 祖父进程等待子进程结束}printf("Grandparent process reaped the child process!\n");}return 0;
}

注意:上面的代码里在子进程退出前,我sleep了2秒,然后打印了getppid的值,即父进程的pid

实验的运行结果:

可以看到在父进程退出后,子进程child process的ppid并没有和2.5一节里的实验一样变成systemd(pid是1),而是变成了调用过prctl(PR_SET_CHILD_SUBREAPER)的祖父进程。

三、给内核的高性能神器rcu的介绍起一个头

内核的rcu锁来自于rwlock的进一步演变,rcu锁和rwlock锁都是针对读多写少的情形,但是rwlock在一些极端情况下,比如多个读者同时进行读操作,因为涉及到cas的add操作,会导致性能因为缓存一致性导致降低得很厉害,而rcu锁如果只有读者的情况下并不需要额外的针对数据和数据相关的管理的开销,并不会标记一些脏数据而导致缓存一致性mesi协议导致的性能开销。

下图是一张浓缩了一系列相关概念精华的思维导图截图,在后面的博文中会详细展开介绍:

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

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

相关文章

电脑启动需要经历哪些过程?

传统BIOS启动流程 1. BIOS BIOS 启动&#xff0c;BIOS程序是烧进主板自带的ROM里的&#xff0c;所以无硬盘也可以启动。BIOS先进行自检&#xff0c;检查内存、显卡、磁盘等关键设备是否存在功能异常&#xff0c;会有蜂鸣器汇报错误&#xff0c;无错误自检飞快结束。 硬件自检…

网络原理(一)—— http

什么是 http http 是一个应用层协议&#xff0c;全称为“超文本传输协议”。 http 自 1991 年诞生&#xff0c;目前已经发展为最主流使用的一种应用层协议。 HTTP 往往基于传输层的 TCP 协议实现的&#xff0c;例如 http1.0&#xff0c;http1.0&#xff0c;http2.0 http3 是…

【亚马逊云科技】使用Amazon Lightsail搭建nginx服务

文章目录 前言一、为什么选择Amazon Lightsail二、创建账号与登录注册亚马逊账号登录控制台 三、创建Amazon Lightsail进入控制台创建实例登录服务器部署nginx服务关闭防火墙 总结 前言 不论是个人名片还是官方网站都离不开网站建设工作。计算机技术经历漫长的发展&#xff0c…

南京大学苏州校区学生代表团到访合合信息,开启“沉浸式”人工智能企业行

为进一步深化校企合作&#xff0c;探索产业科技拔尖创新人才培养新模式&#xff0c;近期&#xff0c;南京大学苏州校区师生代表到访上海合合信息科技股份有限公司&#xff08;以下简称“合合信息”&#xff0c;股票代码&#xff1a;SH688615&#xff09;。此次活动设置了展厅讲…

DM-VIO(ROS)+t265配置运行记录(ubuntu18.04+ros melodic)

在工作中需要对DM-VIO算法进行测试&#xff0c;于是配置并记录了一下&#xff1a; 首先运行ros接口的dm-vio&#xff0c;一定要先配置源码 https://github.com/lukasvst/dm-vio在这个网址把源码下载下来并解压&#xff0c;并安装一下依赖&#xff1a; sudo apt-get install …

pageoffice最新版本浏览器点击没反应解决办法

一、问题现象 最新版本的谷歌、火狐浏览器&#xff0c;调用pageoffice时&#xff0c;点击后没反应&#xff08;旧的谷歌浏览器不受影响&#xff09;。 二、产生原因 服务器返回pageOffice的客户端唤起链接格式为&#xff1a; PageOffice://|http://192.168.1.120:8080/xxx …

【Linux相关】服务器无网情况配置conda

【Linux相关】 服务器无网情况配置conda 文章目录 环境配置1. 本地下载miniconda&#xff0c;传到服务器2. 确认安装包是否传送成功3. 确保有安装权限4. 安装5. 写路径6. 看一下是否成功 环境配置 ssh的话&#xff0c;服务器连不上网&#xff0c;无法在线下载&#xff0c;需要本…

鸿蒙学习使用模拟器运行应用(开发篇)

文章目录 1、系统类型和运行环境要求2、创建模拟器3、启动和关闭模拟器4、安装应用程序包和上传文件QA:在Windows电脑上启动模拟器&#xff0c;提示未开启Hyper-V 1、系统类型和运行环境要求 Windows 10 企业版、专业版或教育版及以上&#xff0c;且操作系统版本不低于10.0.18…

Android studio 签名加固后的apk文件

Android studio打包时&#xff0c;可以选择签名类型v1和v2&#xff0c;但是在经过加固后&#xff0c;签名就不在了&#xff0c;或者只有v1签名&#xff0c;这样是不安全的。 操作流程&#xff1a; 1、Android studio 对项目进行打包&#xff0c;生成有签名的apk文件&#xff…

【科研】9如何高效阅读和理解学术论文

【科研】9如何高效阅读和理解学术论文 写在最前面一、为什么需要系统的阅读方法&#xff1f;二、阅读论文的11步方法三、实践示例四、常见问题解答五、结语 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ …

3.22【计组】 流水线加法器

实验一 timescale 1ns / 1ps/* ALU模块实现两个32bit数的add、sub、and、or、not、slt功能&#xff0c; 但由于Nexy7输入口限制&#xff0c;将num1简化为8位&#xff0c;在过程中再extend成32位&#xff0c;num2作为内部wire自行赋值&#xff0c;此处赋为5 由于最后的结果在to…

算法与数据结构练习——异或

知识点讲解&#xff1a; 一、异或操作定义&#xff1a; 异或是指相同为0&#xff0c;不同为1&#xff0c;也可理解为无进位相加&#xff01;&#xff01; 很重要&#xff01;&#xff01; 二、关于异或运算的几个性质&#xff1a; 1.0^NN &#xff08;0和任何数异或都…

计算机的错误计算(一百六十九)

摘要 探讨 MATLAB 中一个不动点的计算精度问题。 不动点是一类特殊的循环迭代。它有形式 例1. 已知迭代[1] 计算 显然&#xff0c;每个 均为 0.5 . 下面看看 MATLAB 的计算结果。不妨不用循环语句&#xff0c;直接用算术表达式表示 这时计算结果在如下图片&#xff1a; …

11.25.2024刷华为OD

文章目录 HJ76 尼科彻斯定理&#xff08;观察题&#xff0c;不难&#xff09;HJ77 火车进站&#xff08;DFS&#xff09;HJ91 走格子方法&#xff0c;&#xff08;动态规划&#xff0c;递归&#xff0c;有代表性&#xff09;HJ93 数组分组&#xff08;递归&#xff09;语法知识…

思科实现网络地址转换(NAT)和访问控制列表(ACL)和动态路由配置并且区分静态路由和动态路由配置。

实验拓扑(分为静态路由和动态路由两种) 静态路由互通 动态路由互通 实验背景 这个是想实现外网与内网的连接跟网络的探讨&#xff0c;最终实现互通以及使用并且在网络地址转换后能使用网络然后再这个基础上再配置访问控制列表和网络地址转换的的学习过程。 实验需了解的知识…

Idea 2024.3 突然出现点击run 运行没有反应,且没有任何提示。

写这篇文章的目的是为了提供一个新的解决思路&#xff0c;因为存在同病不同原因。 如果你进行了1. 检查运行配置 (Run Configuration) 2. 清理和重建项目 3. 清除缓存并重启 IDEA 4.排除kotlin 5.重装idea等等操作之后仍然没有解决&#xff0c;可以试着按一下步骤进行解决。 检…

数据结构--树二叉树顺序结构存储的二叉树(堆)

前言 前面我们学习了顺序表、链表、栈和队列&#xff0c;这些都是线性的数据结构。今天我们要来学习一种非线性的数据结构——树。 树的概念及结构 树的概念 树是一种非线性的数据结构&#xff0c;是由n&#xff08;n≥0&#xff09;个有效结点组成的一个具有层次关系的集合…

qt QProxyStyle详解

1、概述 QProxyStyle是Qt框架中QStyle类的一个子类&#xff0c;它提供了一种代理机制&#xff0c;允许开发者在不直接修改现有样式&#xff08;QStyle&#xff09;实现的情况下&#xff0c;对样式行为进行定制或扩展。通过继承QProxyStyle&#xff0c;开发者可以重写其虚方法&…

STL基本算法之copy与copy_backward

copy 不论是对客端程序或对STL内部而言&#xff0c;copy()都是一个常常被调用的函数。由于copy进行的是复制操作&#xff0c;而复制操作不外乎应用assignment operator或者copy construct(copy 算法用的是前者)&#xff0c;但是某些元素型别拥有的是trivial assignment operato…

不可分割的整体—系统思考的微妙法则

不可分割的整体——系统思考的微妙法则 作为企业领导者&#xff0c;我们经常需要做出决策&#xff0c;但有时候&#xff0c;我们会忽略一个事实&#xff1a;每个决策都不是孤立的&#xff0c;它背后都是一个复杂系统的一部分。 无论是市场动态、团队协作&#xff0c;还是产品…