[Linux]环境变量 进程地址空间(虚拟内存与物理内存的关系)

news2024/11/24 0:26:50

hello,大家好,这里是bang_bang,今天我们来讲一下语言层级上的程序地址空间和系统层级上的进程地址空间的区别,在下面中我举的例子会设计到环境变量,所以开篇我先讲讲环境变量。

目录

1️⃣环境变量

🍙 基本概念

🍙环境变量相关命令

🍥查看环境变量echo

🍥添加全局环境变量export

🍥显示环境变量env/set

🍥清除环境变量unset

🍥全局与局部环境变量对比

🍥系统调用getenv获取特定环境变量

🍥系统定义全局变量environ

🍥浅谈main函数参数及environ的使用

2️⃣进程地址空间

🍙程序地址空间(语言层级)

🍥验证地址空间排布

🍥探究物理内存or虚拟内存

🍙进程地址空间(系统层级)

🍥地址空间发展由来

🍥地址空间结构

🍥地址空间与物理内存的联系

🍥写时拷贝

🍥编译器同样遵守虚拟地址

🍙 为什么需要进程地址空间

🍥安全性

🍥独立性

🍥分批加载and分批换出


1️⃣环境变量

初识:

        大家在Linux下执行自己写的程序的时候不知道有没有注意到一个细节。命令行是这样的:

[roothost]$ ./可执行程序文件名

 ./ 是当前路径,也就是说我们要执行我们的程序需要带上路径!!!

        那么有没有方法可以让我们的程序执行不需要带路径,而是像系统命令一样,直接输入命令执行呢?

——这就需要环境变量了!!!

🍙 基本概念

⭐环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
⭐环境变量通常具有某些特殊用途,还有在系统当中通常具有 全局特性
常见环境变量:
        ★ PATH : 指定命令的搜索路径
        ★ HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
        ★ SHELL : 当前Shell,它的值通常是/bin/bash。

🍙环境变量相关命令

🍥查看环境变量echo

echo $NAME    //NAME是你的环境变量名称

🌰查看环境变量PATH 

如何直接使我们写的程序不用带上路径,像系统命令一样执行?

我们可以在PATH路径下加入我们这个可执行程序文件的当前路径。

🍥添加全局环境变量export

命令行改环境变量,只在本次登陆中生效。环境变量具有全局性!

export将环境变量设置为全局环境变量

export NAME=$NAME:添加的路径   //添加环境变量  export全局

$NAME:表示在原路径后增加路径

目录之间用冒号(:)隔开

🍥显示环境变量env/set

env        //显示全局环境变量
set        //显示本地shell变量和环境变量

这2条命令显示的结果太长,这里就不贴了,小伙伴们可以自己在Linux中使用查看结果。

查看env和set的详细信息可以使用 man命令进行查看

man set
man env

🍥清除环境变量unset

unset NAME        //NAME为要清除的变量 

 🌰清除环境变量XDG_SESSION_ID

🍥全局与局部环境变量对比

局部环境变量只能在当前shell中使用,无法在子shell中使用。(局部性)

创建局部shell变量

NAME=内容            //创建shell变量,局部性

🌰创建一个AAA的局部环境变量

🌰创建一个HOST的全局环境变量

🍥系统调用getenv获取特定环境变量

#include <stdlib.h>
char *getenv(const char *name);

🌰获取PATH环境变量

#include<stdio.h>
#include<stdlib.h>
int main()
{
    printf("PATH:%s\n",getenv("PATH"));
    return 0;
}

🍥系统定义全局变量environ

#include<unistd.h>
extern char **environ;

environ是C语言提供的一个全局变量,其指向环境表,环境表是一个字符指针数组,每个指针指向以'\0‘结尾的环境字符串。注意:环境表最后是NULL

🍥浅谈main函数参数及environ的使用

前两个参数是命令行参数,最后一个参数是环境变量参数。 

 int main(int argc, char *argv[], char *env[])

 🌰测试命令行参数:argc是命令参数个数(命令本身算1个参数),字符指针数组argv存放命令

//测试argv[]
int main(int argc,char* argv[])
{
   if(argc != 2)
   {
       printf("Usage: %s 至少要有一个选项\n", argv[0]);
       return 1;
   }

   if(strcmp("-a", argv[1]) == 0)
   {
       printf("这个是功能一\n");
   }
   else if(strcmp("-b", argv[1]) == 0)
   {
       printf("这个是功能二\n");
   }
   return 0;
}


🌰测试环境变量参数:

#include<stdio.h>

int main(int argc,char* argv[],char* env[])
{
    for(int i=0;env[i];i++)
    {
        printf("env[%d]:%s\n",i,env[i]);
    }
    return 0;
}


 🌰使用系统定义变量environ测试:

#include<stdio.h>
#include<unistd.h>
int main(int argc,char* argv[],char* env[])
{
    extern char** environ;
    
    for(int i=0;environ[i];i++)    //环境表最后是NULL,可以直接做for的循环退出条件
    {
        printf("environ[%d]:%s\n",i,environ[i]);
    }
    return 0;
}

2️⃣进程地址空间

🍙程序地址空间(语言层级)

相信大家无论在学习何种语言的时候都听说过“地址”这个概念,但不知道大家有没有仔细想过这个“地址”有没有可能不是物理内存上的呢? 

在这里我明确的告诉大家,语言上说的“地址“不是物理内存地址,而是虚拟内存地址!!!

🍥验证地址空间排布

32位下地址空间

🌰测试各区域地址空间排布:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_unval;
int g_val = 100;


int main(int argc, char *argv[], char *env[])
{
    //字面常量
    const char *str = "helloworld";
    //代码区
    printf("code addr: %p\n", main);
    //位于代码区和全局初始化数据之间(有的教材叫常量区)
    printf("constant addr:%p\n",str);
    //全局初始化数据
    printf("init global addr: %p\n", &g_val);
    static int test = 10;

    printf("test static addr: %p\n", &test);
    //全局未初始化数据
    printf("uninit global addr: %p\n", &g_unval);
    
    char *heap_mem = (char*)malloc(10);
    char *heap_mem1 = (char*)malloc(10);
    char *heap_mem2 = (char*)malloc(10);
    char *heap_mem3 = (char*)malloc(10);
    //堆
    printf("heap addr: %p\n", heap_mem); //heap_mem(0)
    printf("heap addr: %p\n", heap_mem1); //heap_mem(1)
    printf("heap addr: %p\n", heap_mem2); //heap_mem(2)
    printf("heap addr: %p\n", heap_mem3); //heap_mem(3)
    //栈
    printf("stack addr: %p\n", &heap_mem); //&heap_mem(0)
    printf("stack addr: %p\n", &heap_mem1); //&heap_mem(1)
    printf("stack addr: %p\n", &heap_mem2); //&heap_mem(2)
    printf("stack addr: %p\n", &heap_mem3); //&heap_mem(3)
    //命令行参数环境变量
    for(int i = 0 ;i < argc; i++)
    {
        printf("argv[%d]: %p\n", i, argv[i]);
    }
    for(int i = 0; env[i]; i++)
    {
        printf("env[%d]: %p\n", i, env[i]);
    }

    return 0;
}
验证结果讲解图

进程地址空间分为:正文代码、初始化数据、未初始化数据、堆、栈、命令行参数环境变量,其中堆、栈相对而生。

static修饰局部变量(本质:将该变量开辟在全局区域)

🍥探究物理内存or虚拟内存

我上面一开始就说语言上的地址是虚拟地址,为什么?

🌰下面代码中,我们仔细观察全局变量g_val,在进程中,我们讲过子进程和父进程共享代码

     那g_val理论上应该是一模一样的,实际中是这样吗?

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

int g_val=10;

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        //child
        g_val=100;
        printf("child:pid:%d,val:%d,addr:%p\n",getpid(),g_val,&g_val);
    }
    else if(id>0)
    {
        //parent
        printf("parent:pid:%d,val:%d,addr:%p\n",getpid(),g_val,&g_val);
    }
    return 0;
}

这怎么可能!这说明语言层级的地址一定不是物理内存地址,而是虚拟内存地址!

🍙进程地址空间(系统层级)

所以之前说的程序地址空间并不准确,准确的应该称作进程地址空间。

虚拟地址肯定不止一份,每一个进程都会有一个虚拟地址,所以它会被管理,那么管理本质又回到了先描述再管理。那么虚拟地址我们猜一定也是一种数据结构!

🍥地址空间发展由来

历史计算机,程序直接放到物理内存,进程中如果出现野指针或者指针指向另一个进程,可以直接读取另一个进程的内容,十分不安全。并且当物理内存满的时候,再插入进程,需要重新排序,十分繁琐。

现代计算机,提出了下面的方式!

——给每个进程一个虚拟地址空间(本质:一个数据结构)(0x0000 0000~0xFFFF FFFF)

🍥地址空间结构

地址空间是一种内核数据结构mm_struct,进程控制块(PCB)包含着mm_struct数据结构的指针

根据上面验证的地址空间排布,我们得知mm_strcut内至少要有各区域的划分,那么是如何实现划分呢?我来讲个小故事引导大家理解。

上小学的时候,我们应该都见过三八线(或者正是你的亲身经历),当你和同桌小美闹矛盾的时候,五五开的桌子变会被重新划分,中间划分的线便是三八线。

不难看出,划分的本质就是使用2个数作为两边界+或者-去一定的范围作划分区域。

mm_struct同样如此,通过2个数确定上下边界来划分区域。

我们可以猜测mm_struct是这样定义的:

struct addr_room
{
    int code_start;
    int code_end;
    
    int init_start;
    int init_end;

    int uninit_strat;
    int uninit_end;

    int heap_start;
    int heap_end;
    
    int stack_start;
    int stack_end;

    其他属性;
}

Linux源码中mm_struct结构体部分定义:

所谓的区域划分(也就是范围变化),本质是对start或者end标记值+-特定的范围即可!!!

🍥地址空间与物理内存的联系

地址空间和物理内存是通过页表映射联系起来的!

我们的程序是在磁盘中的,当要运行这个程序的时候,程序就会被加载到物理内存中,CPU寻虚拟地址,通过页表映射找到对应的物理地址,读取数据。

上篇文章进程详解中,我们提到进程具有独立性!如何实现呢? 

其实只需要我们每个进程都有一份自己的地址空间和页表就可以做到进程彼此分开独立运行!

OS是通过什么机制给每个进程都分配独属于自己的地址空间呢?

        通俗的讲是'画饼',32位的物理内存有4G,OS欺骗每个进程,我物理内存的4G是独属于你的,每个进程往往需要的空间都是很少的,想想你平时写的程序才多大(可能不会超过几Kb),OS自然很乐意的分配给这个进程,但当这个进程需要的空间过大(比如:正好4个G),OS肯定会拒绝,因为它还要管理其他的进程,但是这个进程平时申请的空间OS又都允许了,进程仍旧认为他拥有全部内存的使用权。

如果上面讲的你很难理解,那就看我接下来描述的生活例子,有助于你理解刚刚所说。

        我们在电影中曾看到过富豪在即将去世的时候,会把家产继承给自己的孩子。

        假如现在就有一个富豪,他的家产有4个亿,并且他有3个孩子,3个孩子彼此都认为自己是富豪唯一的孩子,这3个孩子为了子承父业,各自在不同的行业上努力拼搏,平时的一些必要小花销投资向富豪申请,富豪也会同意,但是当有一天,老大向富豪要3个亿去投资大项目,富豪肯定会拒绝他(已经没这么多钱了),老大或许只会抱怨几句,但他还是深信不疑自己就是富豪唯一的孩子,这就是吃了富豪画的饼。

为什么地址空间要被管理?

        你可以想象,一个公司企业的老板,给A员工说你好好干活,我给你加薪;给B员工说你好好干活,我给你升职......画的饼多了,他很有可能会记错,等再给A员工画饼的时候,给他说我给你升职(A员工一定会满脸???之前不是说给我加薪吗)所以为了防止出现错误,老板一定会通过某种手段管理给员工画的饼。

        OS同样如此,他给每个进程画饼,也需要管理,这就回到了上面说的地址空间需要被管理,管理的本质又是先描述再管理。

🍥写时拷贝

会到上面我们探究的例子,地址相同却有2个不同的值!这里面其实发生了一个机制:写时拷贝

我们在上篇文章中说父子进程代码和数据是共享的,也就是说父子进程的地址空间通过页表映射到物理内存上应该是一个位置(这是对的!)但是当我们子进程要进行写入的时候,这时为了不影响父进程的数据,OS就会在内存中拷贝出一个新的位置,同时断开页表的映射关系,让页表映射到这个新的位置,供子进程使用。

子进程执行读权限的时候,父子进程页表映射到同一物理内存,当执行写权限时,OS重新拷贝一份数据到物理内存上,同时子进程的页表断开原来的映射关系,映射到拷贝数据的物理地址。

🍥编译器同样遵守虚拟地址

地址空间不要仅仅理解成为是OS内部要遵守的,其实编译器也要遵守!!

编译器编译代码的时候,就已经给我们形成了 各个区域 代码区,数据区........并且,采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,故,程序在编译的时候,每一个字段早已经具有了一个虚拟地址!!!

CPU读取指令的时候寻址如下:

CPU读取到指令的时候指令内部也有虚拟地址,CPU读取到指令内部的虚拟地址再用该虚拟地址通过页表映射找到数据的物理地址。

🍙 为什么需要进程地址空间

🍥安全性

凡是非法的访问或者映射,OS都会识别到,并终止你这个进程!!(富翁拒绝4个亿的请求)

有效保护了物理内存。也有效的保护了物理内存中的所有有效数据以及内核的相关有效数据!

        比如:修改字符常量区,页表管理的代码区没有写权限。

所有的进程崩溃,本质是进程退出!(OS杀掉这个进程)

🍥独立性

因为有地址空间的存在,页表的映射的存在。我们的物理内存中可以对未来的数据进行任意位置的加载。物理内存的分配就可以和进程的管理,可以做到没有关系!(不关心数据所在物理内存位置,只关心能否映射到物理内存对应位置)

 内存管理模块 vs 进程管理模块就完成了解耦合!(减少模块和模块的关联性

因为在物理内存中理论上可以任意位置加载,那么物理内存中的所有的数据和代码在内存中是乱序的!但是,因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,那么在进程视角所有的内存分布,都可以是有序的!

地址空间+页表的存在 可以 将内存分布 有序化!

地址空间是OS给进程画的大饼+任意加载:进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,便很容易做到,进程独立性的实现!!

    进程的独立性,可以通过地址空间+页表的方式实现。

    因为有地址空间的存在,每一个进程都认为自己独占内存4G(32)空间,并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性!!

🍥分批加载and分批换出

我们在C、C++new,malloc空间的时候,本质是在虚拟地址空间申请的。

        本质上,(因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给。而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系)延迟分配策略(页表中断),然后,再让你进行内存的访问。

        通过延迟分配策略我们可以实现分批加载, 我们下载的大型程序(几个G)实际上就运用了分批加载的方式,要不然超过了我们的物理内存大小,我们是如何下载下来的?

加载本质就是 创建进程,但不是必须飞的立马把所有的的程序的代码和数据加载到内存中并创建内核数据结构建立映射关系。在最极端的情况下:只有内核结构被创建出来!进程这种状态叫做新建状态!

既然可以分批加载,可以分批换出吗?

        当然可以,甚至这个进程短时间不会再被执行了(比如网络太卡了,要等很久,OS不想让该进程的数据和代码占着位置,那么就换出),进程的数据和代码被换出了,就叫做挂起


文末结语,本篇文章详细讲解了探究地址空间排布的前言知识环境变量,通过程序验证了地址空间排布,并探究程序所说的地址究竟是物理地址还是虚拟地址,铺垫完后进入重点进程地址空间,详细介绍了进程地址空间是什么?结构,与物理内存的联系,并讲解写时拷贝的现象,拓展补充编译器同样遵守虚拟地址;最后讲解为什么需要地址空间,分为3个部分:安全性,独立性和OS的延迟分配策略实现内存的分批换出and分批换入。

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

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

相关文章

android studio JNI开发

一、JNI的作用&#xff1a; 1.使Java与本地其他类型语言&#xff08;C、C&#xff09;交互&#xff1b; 2.在Java代码调用C、C等语言的代码 或者 C、C调用Java代码。 由于JAVA具有跨平台的特点&#xff0c;所以JAVA与本地代码的交互能力弱&#xff0c;采用JNI特性可以增强JA…

手写Nacos基本原理——服务注册 配置管理

手写Nacos基本原理 一、背景介绍二、 思路方案三、过程nacosService代码pom文件配置文件具体类 nacosSDK代码pom文件配置类具体类 serviceA代码pom文件配置文件具体类 serviceB代码pom文件配置文件具体类 实现效果四、总结五、升华 一、背景介绍 之前在项目开发的过程中&#…

Java 贪心算法经典问题解决

文章目录 分金条题目思路代码实现测试用例以及结果输出 花费资金做项目最大收益题目思路代码实现测试用例以及结果输出 预定会议室题目思路代码实现测试用例以及结果输出 取中位数题目思路代码实现测试用例以及结果输出 最低字典序题目思路代码实现测试用例以及结果输出 结语 分…

如何在工作中利用Prompt高效使用ChatGPT

导读 AI 不是来替代你的&#xff0c;是来帮助你更好工作。用better prompt使用chatgpt&#xff0c;替换搜索引擎&#xff0c;让你了解如何在工作中利用Prompt高效使用ChatGPT。 01背景 现在 GPT 已经开启了人工智能狂潮&#xff0c;不过是IT圈&#xff0c;还是金融圈。 一开…

【线性规划、非线性规划、多目标规划】

有限的条件下&#xff0c;最大的收益 线性规划就是在一组线性约束条件下&#xff0c;求线性目标函数的最大或者最小值 线性就是指所有的变量都是一次方 整数规划、0-1规划都是默认为线性规划的特例 MATLAB自带的函数求解线性规划问题&#xff1a; Linprog函数 模型化为MATL…

docker+Jenkins

拉取镜像 docker pull jenkins/jenkins启动容器 8080端口映射58080 jenkins_home 映射本地/data/下方便查看 docker run -d -p 58080:8080 -p 5000:50000 -v /data/jenkins_home:/var/jenkins_home -v /etc/localtime:/etc/localtime --name jenkins jenkins/jenkins访问ip:5…

线性神经网路——线性回归随笔【深度学习】【PyTorch】【d2l】

文章目录 3.1、线性回归3.1.1、PyTorch 从零实现线性回归3.1.2、简单实现线性回归 3.1、线性回归 线性回归是显式解&#xff0c;深度学习中绝大多数遇到的都是隐式解。 3.1.1、PyTorch 从零实现线性回归 %matplotlib inline import random import torch #d2l库中的torch模块&a…

前端密码加密 —— bcrypt、MD5、SHA-256、盐

&#x1f414; 前期回顾悄悄告诉你&#xff1a;前端如何获取本机IP&#xff0c;轻松一步开启网络探秘之旅_彩色之外的博客-CSDN博客前端获取 本机 IP 教程https://blog.csdn.net/m0_57904695/article/details/131855907?spm1001.2014.3001.5501 在前端密码加密方案中&#xff…

开发一个RISC-V上的操作系统(三)—— 串口驱动程序(UART)

目录 文章传送门 一、什么是串口 二、本项目串口的FPGA实现 三、串口驱动程序的编写 四、上板测试 文章传送门 开发一个RISC-V上的操作系统&#xff08;一&#xff09;—— 环境搭建_riscv开发环境_Patarw_Li的博客-CSDN博客 开发一个RISC-V上的操作系统&#xff08;二&…

Linux-定时清除日志No space left on device

由于开发环境上一般机器资源较少&#xff0c;很容易导致因日志文件过大而导致系统宕机&#xff0c;报错No space left on device等问题&#xff0c;我们可以通过添加定时任务&#xff0c;自动删除日志从而达到节省空间的目的 操作步骤&#xff1a; 云服务器进入救援模式(若服…

目前主流的几个Web前端框架

启动项目时&#xff0c;请查看 2023 年最好的 Web 前端框架。为什么选择合适的工具很重要? 前端开发人员使用前端框架来简化工作。这些软件包通常提供可重用的代码模块、系统化的前端技术和预构建的接口块。这使团队可以更快、更轻松地创建可持续的 Web 应用程序和用户界面&am…

[linux]VI编辑器常用命令

VI编辑器常用命令 命令用法含义dd删除游标所在一整行d1G删除光标所在到第一行的所有数据dG删除光标所在到最后一行的所有数据d$删除光标所在处&#xff0c;到该行的最后一个字符d0那个是数字0,删除光标所在到该行的最前面的一个字符x,Xx向后删除一个字符(相当于[del]按键),X向…

深入浅出多种开发语言对接淘宝京东1688阿里巴巴等电商平台,获取实时商品详情数据API接口介绍

api接口详解大全?优秀的设计是产品变得卓越的原因设计API意味着提供有效的接口&#xff0c;可以帮助API使用者更好地了解、使用和集成&#xff0c;同时帮助人们有效地维护它每个产品都需要使用手册&#xff0c;API也不例外在API领域&#xff0c;可以将设计视为服务器和客户端之…

Oracle中varchar2、clob字段类型中特殊字符会显示为问号解决方法

项目中遇到varchar2、clob字段存储数据&#xff0c;内容中存在特殊字符导致显示问号&#xff0c;以下说明解决此问题的办法 首先我们查询下数据库编码、客户端编码、查询用户操作系统字符集 --查看oracle数据库编码 select * from nls_database_parameters where parameter NL…

MySQL数据库第十一课---------SQl语句的拔高-------水平提升

作者前言 个人主页::小小页面 gitee页面:秦大大 一个爱分享的小博主 欢迎小可爱们前来借鉴 ______________________________________________________ 目录 SQL提高 日期函数 length round reverse substring ifnull case when cast grouping sets 排序函数 开窗函…

从零到一,激活GPU的力量:使用TensorRT量化和CUDA并行编程

TensorRT学习笔记 前情提要&#xff1a;TensorRT 模型优化与推理&#xff1a;从零到一&#xff0c;激活GPU的力量&#xff1a;使用TensorRT优化和执行深度学习模型&#xff0c;你的TensorRT入门指南 本篇将会介绍TensorRT下的模型量化与CUDA并行计算编程的介绍。 TensorRT模型…

【雕爷学编程】Arduino动手做(170)---LGT8F328P 开发板

37款传感器与模块的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&#x…

如何在3ds max中创建可用于真人场景的巨型机器人:第 2 部分

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 1. 创建主体 步骤 1 打开 3ds Max。选择机器人头部后&#xff0c;二次单击鼠标并选择隐藏未选中。机器人的其他部分 除了头部之外&#xff0c;将被隐藏。 打开 3ds Max 步骤 2 在人脸选择模式下&#x…

自动化测试项目实战

目录 1.熟悉项目 2.针对核心流程设计手工测试用例 3.手工测试用例转换为自动化测试用例 前置工作 测试工作 登陆界面 博客列表页数量 博客详情页检验 写博客并发布 校验标题&#xff0c;时间 删除博客 注销博客 针对博客系统进行自动化测试 1.熟悉项目 2.针对核…

2023年9月北京/广州/深圳CDGA/CDGP认证考试报名开启

据DAMA中国官方网站消息&#xff0c;2023年度第三期DAMA中国CDGA和CDGP认证考试定于2023年9月23日举行。 报名通道现已开启&#xff0c;相关事宜通知如下&#xff1a; 考试科目: 数据治理工程师(CertifiedDataGovernanceAssociate,CDGA) 数据治理专家(CertifiedDataGovernanc…