Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客:<但凡.

我的专栏:《编程之路》《数据结构与算法之美》《C++修炼之路》《Linux修炼:终端之内 洞悉真理》《Git 完全手册:从入门到团队协作实战》

感谢你打开这篇博客!希望这篇博客能为你带来帮助,也欢迎一起交流探讨,共同成长。

目录

1、命令行参数

2、环境变量

        2.1、环境变量引入

        2.2、获取环境变量 

3、进程地址空间

        3.1、验证C/C++内存空间分布特征

        3.2、虚拟地址

        3.2.1、虚拟地址空间介绍

        3.2.2、mm_struct和vm_area_struct

        3.2.3、虚拟地址空间的意义


1、命令行参数

        命令行参数是在终端或脚本中执行命令时,跟随命令名称后输入的附加信息。这些参数用于向程序传递特定指令、选项或数据,从而控制程序的行为或指定操作目标。

        那么命令行参数是怎么实现不同的命令行参数实现不同功能的呢?

        我们运行以下代码:

#include<stdio.h>
#include<string.h>

int main(int argc,char *argv[])
{
  //打印命令行参数
  //argv[0]是可执行程序的名字
  //for(int i=1;i<=argc;i++)
  //{
  // printf("%s",argv[i]);
  //}
  //printf("\n");                                                                                                                                                             
    if(strcmp(argv[1],"-v1")==0)
    {
        printf("这是功能一\n");
    }
    else if(strcmp(argv[1],"-v2")==0)
    {
        printf("这是功能二\n");
    }
    else if(strcmp(argv[1],"-v3")==0)
    {
        printf("这是功能三\n");
    }
    else
    {
        printf("这是其他功能\n");
    }
       return 0;
}

​         可以发现,打印结果随着我们给的命令行参数不同而不同。其实linux系统中的各种指令也是这么实现的,大部分指令都是C语言写的,这些指令内部的main函数也有参数,根据传参,也就是命令行参数的不同,实现不同的功能。这些参数本质上其实是字符串。

        argv中第一个元素是我们可执行程序的名字,最后一个元素是NULL,其他元素是命令行参数对应的字符串。

2、环境变量

        2.1、环境变量引入

        环境变量是操作系统或应用程序运行时使用的动态值,用于存储系统路径、配置参数或用户偏好等数据。它们在全局或特定进程范围内生效,允许程序在不同环境中灵活调整行为。

        我们拿一个常见的环境变量PATH来举例:

        比如我们想执行一个命令,bash会去PATH这个环境变量里记录的几个路径中找这个命令,如果找到了就执行,如果没找到就打印comment not found。

        我们可以通过以下命令查看PATH中包含了哪几个路径:

echo $PATH

​         当然我们也可以手动修改环境变量,PATH支持直接赋值:

export PATH=$PATH:新增路径

        这样我们自己写的可执行程序,放在某个被新增到PATH的路径下,就可以直接执行了。 

        我们可以通过env查看当前系统中所有的环境变量

         其中包括记录用户名的,记录历史命令条数的,记录上一次命令的,记录字体颜色的,还有比如记录家目录的,等等。

        2.2、获取环境变量 

        接下来我们介绍几个方法获取环境变量:

        (1)main函数获取

        其实main函数的参数还可以是env:

         (2)通过函数获取某个环境变量

        我们可以通过调用getenv来实现访问特定的环境变量的内容。这个函数本质上是遍历环境变量表,而环境变量表其实就是一个指针数组。

        (3)environ

        这是c语言提供的存储环境变量的全局变量:

         进程是如何获得环境变量的呢?

         其实不是我们的进程获得了环境变量,而是父进程获得了环境变量,形成了环境变量表。这个环境变量表是父进程的数据。在上面几个例子中,或者我们直接执行的可执行程序,他们的父进程是bash。bash从系统的配置文件中拿到环境变量,然后根据环境变量形成的环境变量表。

        需要注意的是,我们上面提到的两张表,环境变量表和命令行参数表,都是内存级的。即使清空了,我们重启Xshell他就又回来了。

        我们可以通过环境变量的限制,写一个只有自己能执行的程序:

         这个程序就只能被除mrWang以外的用户执行。

        每个进程都会记录当前的工作路径,我们可以通过cwd查看,这个之前说过,那么父进程bash同样也有cwd,他的cwd在他自己的task_struct内部保存。子进程以父进程的task_struct为模板创建自己的task_struct,所以说每个进程的当前工作路径都是从bash来的。那么bash的cwd从哪来呢?bash会自动调用一个getcwd函数,当然了我们也可以手动调用getcwd。

        环境变量是具有全局属性的。我们可以自己设置环境变量

        我们执行以上命令,接着就可以执行env,在里面找到设置好的环境变量。

        其中,我们通过以下命令查看环境变量

ehco $环境变量

        如果想取消这个环境变量,我们可以执行以下命令

unset 环境变量

3、进程地址空间

        3.1、验证C/C++内存空间分布特征

        我们运行以下代码,看看他们的内存空间分布特征是否和我们之前学的空间分布一致。

#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("init global addr: %p\n", &g_val);
   printf("uninit global addr: %p\n", &g_unval);


   static int test = 10;
   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);
   printf("heap addr: %p\n", heap_mem1);
   printf("heap addr: %p\n", heap_mem2);
   printf("heap addr: %p\n", heap_mem3);
   printf("test static addr: %p\n", &test);
   printf("stack addr: %p\n", &heap_mem);
   printf("stack addr: %p\n", &heap_mem1);
   printf("stack addr: %p\n", &heap_mem2);
   printf("stack addr: %p\n", &heap_mem3);
   printf("read only string addr: %p\n", str);


   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;   
}

         经过验证,内存空间分布特征是和我们之前学的一样的。

        需要注意的是在Windows环境下验证可能结果会略有不同,因为存在编译器优化。

        堆空间是向上增长的,栈空间是向下增长的。static修饰的变量存储在静态存储区,根据是否显式初始化绝对位于未初始化数据区还是初始化数据区。

        以上我们讨论的内存空间分布并不是物理内存,而是进程地址空间,是虚拟的。

        我们再次简单总结一下C/C++中的内存空间分布规律:

        内存分为以下几个区域:代码区,全局/静态数据区(其中又分为初始化数据区和未初始化数据区),堆区,栈区,再栈区之上,紧邻内核空间,又有命令行参数和环境变量两个区域。

        代码段存储着可执行的代码,常量数据(如"abcd")。

        数据段中存储着全局变量,static修饰的变量,其中又根据是否初始化分为初始化数据和为初始化数据。

        栈区用于存储函数调用相关的信息,存放着普通局部变量(如int a,char* b,int c[]),函数调用信息(返回地址,函数参数,栈帧指针),临时数据,由编译器自动管理。

        堆区存放着动态分配的对象(如malloc,new分配的对象)。

        命令行参数与环境变量区位于栈区之上、内核空间之下,存储程序启动时传入的命令行参数和环境变量。

        3.2、虚拟地址

        3.2.1、虚拟地址空间介绍

        两个进程之间时具有独立性的,父子进程之间也具有独立性。哪怕父子进程同时修改同一个全局变量,他们两个对于这个全局变量的修改也是独立的。也就是说,父子进程查看同一个地址的同一个变量,值居然是不一样的!

        那么这是如何做到的呢?

        首先,这一点足够说明,我们刚才讨论的内存空间分布不是物理内存,不然一个地址的变量的值不可能会有不同的访问结果。我们历史上所学的地址都是虚拟地址。   

        其实,我们所看到的地址都是通过页表映射出来的虚拟地址,当子进程修改变量时,系统会自动的开辟一块内存,修改我们页表和物理内存的映射关系,是子进程对应的实际物理内存发生变化。我们把上述开辟空间,拷贝内容,更改映射关系的技术叫做写时拷贝。

        父子进程使用的不是同一块物理内存,所以说父子进程对于同一个变量的修改是互不影响的。正因此,我们使用fork创建子进程,接收fork返回值的变量对于父子进程来说虽然是同一个变量但是是不同的值。

       系统给每个进程都划分了一个虚拟的进程地址空间,这个虚拟地址空间,就是上面我们划分出来的那个表,本质上是一个结构体。

        进程地址空间内划分出了很多空间,这个空间的划分,其实就是标记这段线性空间的开始地址和结束地址。而空间大小的调整,也是调整这个结束地址的数字。当然这个划分的地址也是虚拟的地址,这个虚拟地址根据页表,再映射到物理内存上。

         每个进程地址空间的大小是4GB,注意是虚拟大小4GB而不是真的有4GB的物理内存。其中,这4GB空间由低地址到高地址又分为用户空间和内核空间。用户空间3GB,内核空间1GB。用户空间可以直接用地址进行访问,而访问内核空间必须要用系统调用。

        进程等于内核数据结构(task_struct,mm_struct,页表)加上进程的代码和数据。我们创建子进程的过程就是把父进程的虚拟地址空间,页表都复制一份,由于页表所映射的物理内存是相同的,所以说父进程和子进程的代码和数据是共享的。内核数据结构是新拷贝出来的,代码和数据一旦修改系统会新开辟空间,所以说进程之间是相互独立的。

        复制的时候,子进程会把父进程的环境变量也复制过来,这也就对应了我们上面说的子进程都是通过父进程获得的环境变量。

        地址空间只要存在,那么全局数据区就要存在,所以全局变量会一直存在,包括static静态变量。

        我们知道,数据区是可读可写的,但是代码区是只读的。字符串常量其实和代码是编译在一起的,所以字符串常量也是只读的。那么这个只读是怎么实现的呢?其实这个是通过页表实现的。页表在虚拟地址到物理地址映射的过程中,会进行权限的检查。代码区对于物理地址映射的权限是r,那么我们想执行写操作就会运行报错。而const 实现的不能修改是从语言角度实现的。被const修饰的变量,如果修改会在编译阶段报错,而不是运行阶段。

        3.2.2、mm_struct和vm_area_struct

        mm_struct 存储进程的虚拟内存布局信息,包括代码段、数据段、堆、栈等区域的起始和结束地址。它还包含指向页全局目录(Page Global Directory, PGD)的指针,PGD 是页表的顶层结构。


       除了上面提到的mmstruct,还有一个数据结构是vm_area_struct。mm_struct汇总了虚拟地址空间的总体情况,而vm_area_struct标记了每一个分区的情况,也就是上面提到的start和end地址,除此之外还有权限的标记,以及其他的一些东西。vm_area_struct之间可以通过双链表链接,也可以通过红黑树链接。 

        3.2.3、虚拟地址空间的意义

        那么虚拟地址空间存在的意义是什么呢?

        (1)保护物理内存的安全,维护进程独立性。

        (2)可以使内存从“无序”到“有序”。

        (3)进程管理和内存管理进行解耦合。

        有了虚拟地址空间,系统可以运行更多或者更大的程序,突破的物理内存的限制。当创建进程的时候,先有内核数据结构,再加载进程的代码和数据。但是这个代码和数据不是一次性从磁盘加载到内存中的,而是边执行,边加载,也叫惯性加载。基于这一技术,我们实际运行的程序可以大于物理内存的大小。

         好了,今天的内容就分享到这,我们下期再见!

 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐