)
♂️个人主页进击的荆棘作者其它专栏《数据结构与算法》《算法》《C起始之路》《Linux》目录1.冯诺依曼体系结构2.操作系统Operator System3.进程4.命令行参数和环境变量5.程序地址空间4.命令行参数和环境变量4.1基本概念●环境变量enironment variables一般指在操作系统中用来指定操作系统运行环境的一些参数●如在编写C/C代码时在链接的时候从来不知道我们所链接的动态静态库在哪里但是照样可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找●环境变量通常具有某些特殊用途还有在系统当中通常具有全局特性4.2常见环境变量●PATH指定命令的搜索路径●HOME指定用户的主工作目录即用户登陆到Linux系统中时默认的目录●SHELL当前shell它的值通常是/bin/bash4.3查看环境变量方法echo $HOSTNAME //HOSTNAME本机主机名测试PATH1.创建code.c文件#include stdio.h int main() { printf(hello world!\n); return 0; }2.对比./hello执行和执行hello执行3.为什么有些指令可以直接执行不需要带路径而自己的二进制程序需要带路径才能执行4.将自己的程序所在路径加入环境变量PATH中export PATH$PATH:hello所在路径5.对比测试4.4和环境变量相关的命令1.echo显示某个环境变量值2.export设置一个新的环境变量3.env显示所有环境变量4.unset清除环境变量4.set显示本地定义的shell变量和环境变量4-5 环境变量的组织方式每个程序都会收到一张环境表环境表是一个字符指针数组每个指针指向一个以\0结尾的环境字符串4.6通过代码如何获取环境变量不推荐●命令行第三个参数#include stdio.h int main(int argc, char *argv[], char *env[]) { for(int i0; env[i]; i){ printf(%s\n, env[i]); } return 0; }●通过第三方变量environ获取#include stdio.h int main(int argc, char *argv[]) { extern char **environ; for(int i0; environ[i]; i){ printf(%s\n, environ[i]); } return 0; }libc中定义的全局变量environ指向环境变量表environ没有包含任何头文件中所以在使用时要用extern声明4.7通过系统调用获取或设置环境变量推荐●putenv●getenv#include stdio.h #include stdlib.h int main() { printf(%s\n, getenv(PATH)); return 0; }常用getenv和putenv函数来获取特定的环境变量4.8环境变量通常是具有全局属性的●环境变量通常具有全局属性可以被子进程继承下去#include stdio.h #include stdlib.h int main() { char *env getenv(MYENV); if(env){ printf(%s\n, env); } return 0; }直接查看发现没有结果说明该环境变量根本不存在●到处环境变量export MYENVhello world●再次运行程序发现结果有了说明环境变量是可以被子进程继承下去的5.程序地址空间5.1研究平台●kernel 2.6.32●32位平台5.2程序地址空间回顾在学习语言时像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); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1) printf(heap addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1) printf(test static addr: %p\n, test); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1) printf(stack addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1) 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; }结果$ ./a.out code addr: 0x40055d init global addr: 0x601034 uninit global addr: 0x601040 heap addr: 0x1791010 heap addr: 0x1791030 heap addr: 0x1791050 heap addr: 0x1791070 test static addr: 0x601038 stack addr: 0x7ffd0f9a4368 stack addr: 0x7ffd0f9a4360 stack addr: 0x7ffd0f9a4358 stack addr: 0x7ffd0f9a4350 read only string addr: 0x400800 argv[0]: 0x7ffd0f9a4811 env[0]: 0x7ffd0f9a4819 env[1]: 0x7ffd0f9a482e env[2]: 0x7ffd0f9a4845 env[3]: 0x7ffd0f9a4850 env[4]: 0x7ffd0f9a4860 env[5]: 0x7ffd0f9a486e env[6]: 0x7ffd0f9a4892 env[7]: 0x7ffd0f9a48a5 env[8]: 0x7ffd0f9a48ae env[9]: 0x7ffd0f9a48f1 env[10]: 0x7ffd0f9a4e8d env[11]: 0x7ffd0f9a4ea6 env[12]: 0x7ffd0f9a4f00 env[13]: 0x7ffd0f9a4f13 env[14]: 0x7ffd0f9a4f24 env[15]: 0x7ffd0f9a4f3b env[16]: 0x7ffd0f9a4f43 env[17]: 0x7ffd0f9a4f52 env[18]: 0x7ffd0f9a4f5e env[19]: 0x7ffd0f9a4f93 env[20]: 0x7ffd0f9a4fb6 env[21]: 0x7ffd0f9a4fd5 env[22]: 0x7ffd0f9a4fdf5.3虚拟地址代码验证#include stdio.h #include unistd.h #include stdlib.h int g_val 0; int main() { pid_t id fork(); if(id 0){ perror(fork); return 0; } else if(id 0){ //child printf(child[%d]: %d : %p\n, getpid(), g_val, g_val); }else{ //parent printf(parent[%d]: %d : %p\n, getpid(), g_val, g_val); } sleep(1); return 0; }输出//与环境相关观察现象即可parent[2995]: 0 : 0x80497d8child[2996]: 0 : 0x80497d8可以发现输出的变量值和地址是一模一样的因为子进程按照父进程为模板父子并没有对变量进行任何修改。改动代码后#include stdio.h #include unistd.h #include stdlib.h int g_val 0; int main() { pid_t id fork(); if(id 0){ perror(fork); return 0; } else if(id 0){ //child,⼦进程肯定先跑完也就是⼦进程先修改完成之后⽗进程 再读取 g_val100; printf(child[%d]: %d : %p\n, getpid(), g_val, g_val); }else{ //parent sleep(3); printf(parent[%d]: %d : %p\n, getpid(), g_val, g_val); } sleep(1); return 0; }输出结果//与环境相关观察现象即可child[3046]: 100 : 0x80497e8parent[3045]:0 : 0x80497e8可以发现父子进程输出地址一致但是变量内容不同。得出如下结论●变量内容不同所以父子进程输出的变量绝对不是同一个变量●但地址值是一样说明该地址绝对不是物理地址●在Linux地址下这种地址叫做虚拟地址●在用C/C语言所看到的地址全部都是寻地址物理地址用户一概看不到由OS统一管理OS必须负责将虚拟地址转化成物理地址5-4 进程地址空间分页虚拟地址空间说明●从上图可以看出同一个变量地址相同其实是虚拟地址相同内容不同其实是被映射到了不同的物理地址5.5虚拟地址内存管理描述linux下进程的地址空间的所有的信息的结构体是mm_struct内存描述符。每个进程只有一个mm_struct结构在每个进程的task_struct结构中有一个指向该进程的mm_struct结构体指针。struct task_struct { /*...*/ struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他 的虚拟地址空间的⽤⼾空间部分对于内核线程来说这部分为NULL。 struct mm_struct *active_mm; // 该字段是内核线程使⽤的。当 该进程是内核线程时它的mm字段为NULL表⽰没有内存地址空间可也并不是真正的没有这是因 为所有进程关于内核的映射都是⼀样的内核线程可以使⽤任意进程的地址空间。 /*...*/ }可以说mm_struct结构是对整个用户空间的描述。每一个进程都会有自己独立的mm_struct这样每一个进程都会有自己独立的地址空间才能互不干扰。下图是由task_struct到mm_struct进程的地址空间的分布情况定位mm_struct文件所在位置和task_struct所在路径是一样的不过它们所在文件不一样mm_struct所在文件是mm_types.hstruct mm_struct { /*...*/ struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */ struct rb_root mm_rb; /* red_black树 */ 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; /*...*/ }既然每个进程都由自己独立的mm_struct操作系统肯定是要将这么多进程的mm_struct组织起来的。虚拟空间的组织方式有两种1.当虚拟区较少时采取单链表由mmap指针指向这个链表2.当虚拟区较多时采取红黑树进行管理由mm_rb指向这棵树。linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域VMA由于每个不同质的虚拟内存区域功能和内部机制都不同因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。上面的两种组织方式使用的就是vm_area_struct结构来连接各个VMA方便进行快速访问。struct vm_area_struct { unsigned long vm_start; //虚存区起始 unsigned long vm_end; //虚存区结束 struct vm_area_struct *vm_next, *vm_prev; //前后指针 struct rb_node vm_rb; //红⿊树中的位置 unsigned long rb_subtree_gap; struct mm_struct *vm_mm; //所属的 mm_struct pgprot_t vm_page_prot; unsigned long vm_flags; //标志位 struct { struct rb_node rb; unsigned long rb_subtree_last; } shared; struct list_head anon_vma_chain; struct anon_vma *anon_vma; const struct vm_operations_struct *vm_ops; //vma对应的实际操作 unsigned long vm_pgoff; //⽂件映射偏移量 struct file * vm_file; //映射的⽂件 void * vm_private_data; //私有数据 atomic_long_t swap_readahead_info; #ifndef CONFIG_MMU struct vm_region *vm_region; /* NOMMU mapping region */ #endif #ifdef CONFIG_NUMA struct mempolicy *vm_policy; /* NUMA policy for the VMA */ #endif struct vm_userfaultfd_ctx vm_userfaultfd_ctx; } __randomize_layout;更细致的描述5.6为什么要有虚拟地址空间此问题转化为若程序直接可以操作物理内存会造成什么问题早期计算中要运行一个程序会将这些程序全部装入内存程序都是直接运行在内存上的即程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。那当程序同时运行多个程序时操作系统是如何为这些程序分配内存的如某台计算机总的内存大小是128M同时运行两个程序A和BA需占用内存10MB需占用内存110M。计算机在给程序分配内存时会采取这样的方法先将内存中的前10M分配给程序A接着再从剩余的118中划分处110M分配给程序B。这种分配方法可以保证程序A和程序B都能运行但是这种简单的内存分配策略问题很多。●安全风险▨每个进程都可以访问任意的内存空间意味着任意一个进程都能去读写系统相关内存区域若是一个木马病毒那它就能随意的修改内存空间让设备直接瘫痪。●地址不确定▨编译完成后的程序是存放再硬盘上的当运行的时候需将程序般到内存当中去运行若直接使用物理地址的话我们无法确定内存现在使用到哪里了即拷贝的实际内存地址每一个运行都是不确定的如第一次执行a.out的时候内存当中一个进程都没有运行所以搬迁到内存地址是0x00000000但第二次的时候内存已经有10个进程再运行那执行a.out的时候内存地址就不一定了。●效率低下▨若直接使用物理内存的话一个进程就是作为一个整体内存块操作的若出现物理内存不够用的时候一般的办法是将不常用的进程拷贝到磁盘的交换分区中好腾出内存但若是物理地址的话就需要将整个进程一起拷走这样在内存和磁盘之间拷贝时间太长效率低下。当有了虚拟地址空间和分页机制就能解决。●地址空间和页表是OS创建并维护的。即意味着凡是像使用地址空间和页表进行映射也一定要在OS的监管之下来进行访问也顺便保护了物理内存中的所有合法数据包括各个进程以及内核的相关有效数据●因为有地址空间的存在和页表的映射的存在我们的物理内存中可以对未来的数据进行任意位置的加载物理内存的分配和进程的管理就可以做到没有关系进程管理模块和内存管理模块就完成了解耦合。▨因为有地址空间的存在所以我们在C、C语言上newmalloc空间的时候其实是在地址空间上申请的物理内存可以甚至一个字节都不给你。而当真正进行对物理地址空间访问的时候才执行内存的相关管理算法帮你申请内存构建页表映射关系延迟分配这是由操作系统自动完成的用户包括进程完全0感知。●因为页表的映射的存在出现在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟和物理地址进行映射在进程视角所有的内存分布都可以是有序的。