Linux 修炼:进程概念(下)
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)进程管理和内存管理进行解耦合。
有了虚拟地址空间,系统可以运行更多或者更大的程序,突破的物理内存的限制。当创建进程的时候,先有内核数据结构,再加载进程的代码和数据。但是这个代码和数据不是一次性从磁盘加载到内存中的,而是边执行,边加载,也叫惯性加载。基于这一技术,我们实际运行的程序可以大于物理内存的大小。
好了,今天的内容就分享到这,我们下期再见!