1 环境变量的概念环境变量一般指在操作系统中用来指定操作系统运行环境的一些参数比如安装某些软件时还可能安装其他工具通过环境变量将所有工具都找到再比如我们编写C/C代码的时候在链接的时候从来不知道我们的所链接的动静态库在哪里但是还是可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途还有在系统当中通常具有全局特性。1.1 命令行参数main函数有参数。其中命令行参数版(两个参数)常用。形式写法用途无参数int main(void)最简单的程序不需要处理命令行参数两个参数int main(int argc, char *argv[])需要获取命令行参数时使用三个参数int main(int argc, char *argv[], char *envp[])需要获取环境变量时使用argv是char*类型的指针数组argc是指argv数组元素个数数组的最后一个元素是NULL方便遍历。envp是环境变量字符串的指针数组。参数类型含义argcintargument countargv数组的元素个数argvchar *[]argument vector字符串指针数组每个指针指向一个参数字符串envpchar *[]environment pointer环境变量字符串指针数组每个指针指向一个环境变量字符串命令行参数用途是为了让一个程序可以通过选项实现不同的子功能。main函数的命令行参数是实现程序不同子功能的方法这是指令的选项的实现原理。进程启动时进程拥有一张表argv表用来支持实现选项功能。程序根据这张表决定执行哪个子功能。// 当你运行 ./program hello world int main(int argc, char *argv[]) { // argv 就是一张表 argv[0] ./program // 程序名 argv[1] hello // 第一个参数 argv[2] world // 第二个参数 argv[3] NULL // 结束标记 }2 一个例子一个环境变量我们发现执行自己的命令要带./比如执行自己写的可执行程序而执行系统自带的命令就不带./这是为何原因是系统中存在环境变量来帮助系统找到目标的二进制文件。只要你将自己写的可执行程序拷贝到/usr/bin路径下比如自己的可执行程序是code使用sudo cp code /usr/bin/此时运行自己的程序就不用带./了但是特别不推荐自己写的二进制文件拷贝到系统中。2.1 环境变量PATHPATH里面表明的是告诉系统去哪些路径下找二进制文件(系统中搜索指令的默认搜索路径)。在Linux系统中如果想查看所有环境变量使用env回车。格式是环境变量名称内容显示。只看一个环境变量使用echo $环境变量名字来获取内容。例如echo $PATH如图正式因为/usr/bin在环境变量PATH里所以像ls这样的命令才能正常使用不用带路径可以使用PATH自己想添加的路径然后这个路径下的可执行程序执行就不用带./了。但是发现ls执行不了。echo $PATH发现如果直接这样赋值是把当前路径覆盖了PATH的原内容所以对PATH来讲PATH内部包含的用分割的一个个路径就是用来在系统中查找可执行程序所对应的路径。怎么恢复呢关掉重新登陆就行了。正常加使用PATH$PATH:自己的路径此时echo $PATH就会发现增加了自己的路径。2.2 如何理解环境变量从存储的角度环境变量是由名称内容这些被谁保存包括执行命令时是谁来找这个命令呢答案是bash。登录时操作系统创建bash进程bash读取环境变量信息在bash内形成一张环境变量表。当输入指令时首先这个命令字符串被bash先拿到bash将命令拆分比如命令是ls -a -l根据ls命令在环境变量表中找到PATH再根据PATH的一个个路径由bash在操作系统上找对应命令是否存在若存在就创建子进程执行它。最终结论bash内部有两张表一个是命令行参数表一个是环境变量表。环境变量是key value的长字符串bash启动时会在bash内new出一段空间给每个环境变量再new出一段空间形成二维数组然后把环境变量字符串依次放到这个表中bash内部就维护了这张环境变量表env查时就打印这张表的内容。环境变量最开始从哪里来的呢答从系统的相关配置文件来的。配置文件中包含所有环境变量的直接和间接内容所以bash启动时会从配置文件中读取所有的环境变量值并在自己内部创建这一张表。2.3 配置文件的位置ls -al看到.bashrc和.bash_profile这两个配置文件读取的是这两个文件的内容可以在.bash_profile中PATH后加自己路径在这个路径执行任何代码就不用./了。不管是命令行参数还是环境变量全部是bash内部两张表所对应的字符串。最后如果一个Linux系统中有10个用户登录那么就存在10个bash。执行一个程序必须先找到它是由bash通过PATH来找。3 认识更多的环境变量HOME/home/用户名。登录后创建bash将当前用户形成家目录放/home里。HOME环境变量当前用户家目录。SHELL代表用户登录是用哪一个版本shellUSER当前用户是谁LOGNANE当前登录是谁HOSTNAME主机名SSH_TTY哪一个终端设备PWD记录工作路径4 获取环境变量的方法4.1 操作env echo $XXX查看指定环境变量export MYENV...... 自己导入环境变量unset MYENV 取消环境变量4.2 代码方式方法一获取环境变量main函数参数char* env[]获取的是父进程的环境变量。环境变量可以被子进程继承子进程的子进程也是进程。所以说环境变量在系统中通常具有全局特性。方法二使用系统调用getenv(),头文件是stdlib.h。这种方法最推荐根据名字获取内容获取成功后返回环境变量的起始地址。#include stdio.h #include stdlib.h int main() { // 1. 获取常用的环境变量 char *home getenv(HOME); char *user getenv(USER); char *path getenv(PATH); char *shell getenv(SHELL); // 2. 打印结果需要检查是否为 NULL if (home ! NULL) printf(HOME %s\n, home); else printf(HOME 环境变量不存在\n); if (user ! NULL) printf(USER %s\n, user); else printf(USER 环境变量不存在\n); if (path ! NULL) printf(PATH %s\n, path); else printf(PATH 环境变量不存在\n); if (shell ! NULL) printf(SHELL %s\n, shell); else printf(SHELL 环境变量不存在\n); return 0; }为什么环境变量可以被子进程继承因为可以把环境变量的信息让子进程继承下去子进程可以给环境变量进行个性化操作(比如定制一个只能自己执行的程序)。方法三environenviron是 C 语言中一个全局变量它指向环境变量表让你可以访问当前进程的所有环境变量。#include stdio.h #include unistd.h // 声明外部全局变量 environ extern char **environ; int main() { // 遍历 environ 数组直到遇到 NULL for (int i 0; environ[i] ! NULL; i) { printf(%d: %s\n, i 1, environ[i]); } // 统计环境变量个数 int count 0; while (environ[count] ! NULL) { count; } printf(总共有 %d 个环境变量\n, count); return 0; }5 理解环境变量的特性环境变量具有全局特性。补充两个概念a.本地变量屏幕出打印出10i是本地变量。命令set会显示环境变量和本地变量。bash会记录两套变量环境变量和本地变量。本地变量不会被子进程继承只在bash内部被使用。b. export本地变量导入环境变量。环境变量在bash里面。内建命令不需要创建子进程而是让bash亲自执行一般是bash自己调用函数或者系统调用完成。类型特点例子内建命令Shell 自己实现的命令不创建子进程cd、export、alias、echo外部命令在/bin、/usr/bin等目录下的独立程序ls、gcc、cp6 初识进程地址空间6.1 虚拟地址#include stdio.h #include unistd.h #include stdlib.h int g_val0; int main() { pid_t idfork(); if(id0) { perror(fork); exit(1); } else if(id0)//子进程 { printf(child[%d]:%d:%p\n,getpid(),g_val,g_val); } else//父进程 { printf(parent[%d]:%d:%p\n,getpid(),g_val,g_val); } sleep(1); return 0; }运行结果输出的变量值和地址是一模一样的子进程按照父进程为模板父子并没有对变量进行过任何修改。然后对代码再改动一下1 #include stdio.h 2 #include unistd.h 3 #include stdlib.h 4 5 int g_val0; 6 7 int main() 8 { 9 pid_t idfork(); 10 if(id0) 11 { 12 perror(fork); 13 exit(1); 14 } 15 else if(id0)//子进程先修改然后父进程再读取 16 { 17 g_val100; 18 printf(child[%d]:%d:%p\n,getpid(),g_val,g_val); 19 } 20 else//父进程 21 { 22 sleep(3); 23 printf(parent[%d]:%d:%p\n,getpid(),g_val,g_val); 24 } 25 sleep(1); 26 return 0; 27 }运行结果发现父子进程输出地址是一样的但是变量内容不一样。这就说明这个地址不可能是物理地址如果是内存地址怎么可能不同的变量是相同的地址所以这个地址就是虚拟地址。我们使用C/C语言用到的地址全部都是虚拟地址。6.2 知识点虚拟地址空间不是物理内存。结论一个进程一个虚拟地址空间。创建进程时要有task_struct描述对应进程每个task_struct都要对应一个叫虚拟地址空间的东西。虚拟地址空间对应的单位是字节。32位机器下虚拟地址空间的范围是2^32个地址64位机器是2^64个地址在这个虚拟空间下32位机器2^32字节4GB一个地址是一个字节0-3G是用户空间3-4G是内核空间)。为什么叫用户空间因为在这里用户拿到地址就能直接访问。比如自己定义一个全局变量g_val的值为100与此同时在地址空间上有一个4字节全局变量这个g_val的起始地址称为虚拟地址。同时内存上这个g_val有一个物理地址与此同时操作系统为每个进程构建一个叫页表的东西。地址空间找到页表左侧填虚拟地址右侧填物理地址。结论一个进程一套页表。当进程想访问虚拟地址时操作系统会自动把虚拟地址查表转换成我们对应的物理内存的地址进而访问指定变量。结论页表是用来做虚拟地址和物理地址映射的。虚拟地址空间上所有数据代码初始化数据...栈堆都有地址虚拟地址加载到内存经过页表映射找到物理内存地址。上述所说的进程是父进程因为一个进程一个虚拟地址空间子进程也要有虚拟地址空间和页表。地址空间是拷贝自父进程的PCB页表也是从父进程拷贝形所以现在子进程g_val也有地址这就成了简单的浅拷贝。所以就能理解了父进程创建子进程时打印全局变量地址显示的地址是一样的。相当于父子进程页表指向同一物理内存父子进程默认情况下代码和数据都是共享的。后来子进程对变量要进行修改进行g_val因为进程要具有独立性操作系统介入重新开辟一段空间将老变量拷贝进来得到新的物理内存地址从现在开始操作系统让子进程更改了映射关系。子进程通过虚拟地址对应了新的物理内存地址(虚拟地址没变)然后可以修改g_val这种机制称之为写时拷贝所以就能理解为什么之前查看全局变量打印出来内容不一样但是地址一样。因为父子进程都是用的虚拟地址后来想修改操作系统做了写时拷贝(将目标变量重新拷贝一份申请新的物理内存有新的物理内存地址将页表的映射关系重新修改)。用户能不能看到物理地址呢答案是不能。7 什么是虚拟地址空间操作系统为了将虚拟地址空间管理起来要先描述再组织。所以虚拟地址空间本质是一个数据结构struct mm_struct { /*...*/ struct vm_area_struct *mmap; struct rb_root mm_rb; unsigned long task_size; /*...*/ // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。 unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; /*...*/ }结论虚拟地址空间是一个内核中给进程创建的结构体对象这个结构体中大部分属性都是各个区域的开始和结束方便区域划分区域划分只需要确认区域的开始和结束即可。我们要把一个程序加载到内存要做两件事。1在虚拟地址空间中申请指定大小的空间区域划分2加载程序申请物理空间页表进行映射。物理定址转化为虚拟地址(反向)以后虚拟地址提供给上层用户使用用户给了虚拟地址时再通过查表一定能找到物理地址。就能找到物理内存中指定数据。结论地址空间是一个对象但是对象要被初始化1开辟空间2初始化值相当一部分是从数据代码加载时候进行初始化。流程代码区域多大加载到内存里有多少字节就有多少地址在虚拟地址上开辟空间然后填页表物理地址也填进页表映射关系也就有了字段也就初始化了8 周边问题为什么要有地址空间意义就是将地址从“无序”变“有序”。为什么要有地址空间不直接访问物理内存页表中除了有虚拟地址和物理地址还有读、写、可执行权限如果要对一个代码区进行写入操作系统查表发现对代码区只有只读权限操作系统就不给转到物理内存就可以实现对物理内存的保护。所以一句话总结地址转换的过程中也可以对你的地址和操作进行合法性判定进而保护物理内存。举一个例子字符串在字符常量区字符常量区硬编码到正文代码区权限是只读的。所以char* str“helloworld”*str‘H’页表转换失败操作系统不让转程序运行崩溃。可是为什么在字符常量区写入就会崩溃答案是权限拦截了。创建进程时可不可以只创建PCB、虚拟地址空间和页表不加载代码和数据答案是可以执行时CPU拿到起始地址访问虚拟地址操作系统发现虚拟地址和物理地址映射转化不过来因为物理地址没在内存里所以页表还有个标记位来表示物理地址是否存在内存里如果不在操作系统自动做缺页中断自动完成加载任务并且填充页表此时进程暂停缺页中断结束后进程再执行。创建进程先有task_structmm_struct等还是先加载代码和数据先有内核数据结构。如何理解进程挂起当一个进程是阻塞状态同时内存资源严重不足操作系统就要把当前进程对应代码和文件唤出到对应磁盘上需要时再唤入。阻塞状态内存资源不足操作系统将页表清空指定代码和数据唤出到swap分区实际上mm_struct会维护一张叫vm_area_struct的链表这个链表会记录每个子区域的strat和end这样堆区不连续不影响将堆区管理起来。地址空间每一个区都要有vm_area_struct来真正标识每个区域开始和结束。mm_struct对应的是整个地址空间整体的描述。以上就是本文的全部内容了如果这篇文章对你有用可以点点赞哦你的支持就是我写下去的动力后续会不断地更新其他知识。