购物网站的英文,长沙网站优化厂家,移动端网站一般宽做多大,大连市公众平台网站目录
1. 环境变量
1.1 环境变量基本概念
1.2 环境变量PATH
1.3 环境变量HOME和SHELL
1.4 获取环境变量#xff08;main函数参数#xff09;
1.4.1 main函数第三个参数
1.4.2 设置普通变量和环境变量
1.4.3 main函数前两个参数
2. 进程地址空间
2.1 验证进程地址空…目录
1. 环境变量
1.1 环境变量基本概念
1.2 环境变量PATH
1.3 环境变量HOME和SHELL
1.4 获取环境变量main函数参数
1.4.1 main函数第三个参数
1.4.2 设置普通变量和环境变量
1.4.3 main函数前两个参数
2. 进程地址空间
2.1 验证进程地址空间的分步
2.2 进程地址空间的引入
2.3 进程地址空间是什么
2.4 为什么要有进程地址空间
3. 进程创建fork
3.1 fork函数概念和用法
3.2 写时拷贝
本篇完。 1. 环境变量
1.1 环境变量基本概念 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如我们在编写C/C代码的时候在链接的时候从来不知道我们的所链接的动态静态库在哪里但是照样可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途还有在系统当中通常具有全局特性。 继续用上一篇Linux_写的代码 当我们 ls 显示当前路径文件时和运行自己写的代码时比如 ./process有没有想过
为什么我们的代码运行要带路径而系统的指令不用带路径
如果我们直接输入我们的可执行程序会显示 bash: process: command not found
我们说过执行系统的指令实际上也是程序系统的指令你也是可以带上路径的 系统中是存在相关的 环境变量保存了程序的搜索路径的
为什么我们的代码运行要带路径而系统的指令不用带其本质是由环境变量 PATH引起的。 1.2 环境变量PATH 和环境变量相关的命令 1. echo: 显示某个环境变量值 2. export: 设置一个新的环境变量 3. env: 显示所有环境变量 4. unset: 清除环境变量 5. set: 显示本地定义的shell变量和环境变量 输入 env 显示所有环境变量 这些变量每一个都有它特殊的用途系统中搜索可执行程序的环境变量叫做 PATH。
我们可以通过 grep 去抓一下输入 env | grep PATH 如何查看环境变量的内容我们可以使用 echo 去显示输入 echo $PATH
$用来区别直接打出PATH这个字符串 环境变量 PATH中会承载多种路径中间用冒号 ( : ) 作为分隔符。
我们在执行某一个程序时比如执行 ls 时我们的系统识别到 ls 的输入时会在上面路径中逐个搜索只要在特定的路径下找到了 ls就会执行特定路径下的 ls 并停止搜索。
换言之PATH就提供了环境变量可执行程序搜索的路径。
我们的 ls 在 usr/bin 路径下这说明当前的 ls 在 PATH中是可以被找到的
所以执行 ls 的时侯自然可以不带路径所以我们自己的 process 不带路径自然就不能执行。
因为当前的 process 所在的路径并没有这里的环境变量程序在搜索的时侯找了路径也没有找到你这个可执行程序搜索完找不到自然就报 command not found 了。
那我现在就想让我的可执行程序 process 不带路径直接执行起来可以吗
可以我们先讲述一种简单粗暴的方式直接把我们的可执行程序 cp 拷贝到系统的路径中
转到root用户然后输入cp process /usr/bin/ 既然系统的所有命令都在 usr/bin 路径下那我们把我们的 process 拷进去就行了。 实际上刚才那个操作我们可以称之为 软件被安装到系统上但是我们不建议你去自己安装。也更不建议你将你的指令拷贝到 Linux 系统路径下因为这会污染 Linux 下的命令池。 更好的方式是将 process 所处的路径也添加到环境变量中。
pwd查看当前路径把当前路径添加到环境变量中export PATH$PATH:路径
添加完成后我们的程序就能不用输入路径直接运行了 这里只要重新登录环境变量又变成原本的了。 1.3 环境变量HOME和SHELL 常见环境变量 PATH : 指定命令的搜索路径 HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录) SHELL : 当前Shell,它的值通常是/bin/bash。 其他环境变量
USER当前用户名 PWD当前所处路径 HOSTNAME主机名
环境变量实在多全部讲完不太现实所以就先讲到这 1.4 获取环境变量main函数参数
环境变量是以数组的形式存储的它的组织方式如下图 其实main函数有三个参数 前两个参数叫作命令行参数第三个叫作环境变量参数。 第一个参数int argc是个整型变量表示命令行参数的个数含第一个参数。 第二个参数char *argv[ ]是个存放字符指针的数组每一个元素一个字符指针指向一个字符串。这些字符串就是命令行中的每一个参数字符串。 第三个参数char *envp[ ]也是存放字符指针的数组数组的每一个原元素是一个指向一个环境变量字符串的字符指针。 1.4.1 main函数第三个参数
下面我们按照上面先讲讲第三个参数环境变量参数。 据图看一段在Linux环境下运行的代码 根据图env最后放的是空指针退出for循环编译运行 这是在代码里获取环境变量的第一种方式。 根据这个图我们还可以用environ获取环境变量问一下man怎么用 这是一个全局的第三方变量 编译运行 这是代码获取环境变量的第二种方式但是一般不用前两种方式用第三种 这是一个函数接口用来获取一种环境变量这里获取上面讲的PATH 编译运行 现在我们学了这么多获取环境变量的方式那么这个环境变量是谁设置的main函数的第三个参数是谁传入的 环境变量具有全局属性环境变量是会被子进程继承下去的。 所以环境变量都是有这个进程的父进程设置的。 我们这里用gerenv顺便获取一个不存在的环境变量 编译运行
发生了段错误因为根本没有这个环境变量。 1.4.2 设置普通变量和环境变量 在 Linux 命令行中我们也是可以定义变量的命令行变量分为两种 普通变量环境变量具备全局属性 命令行上直接写变量名等于值 你所定义的这个变量 abcdef就是 普通变量。
用系统查看环境变量的命令 env 去查看一下这个本地变量会发现根本找不到
试试编译运行上面这个代码 还是段错误因为它不以环境变量的形式存在不具有全局性但是它是存在的
如果你想让一个变量变成环境变量你可以通过 export 导出一个在系统中可以查看的环境变量 env可以看到代码也不用编译再运行也能打印。 所以环境变量具有全局属性环境变量是会被子进程继承下去的。 所以环境变量都是由这个进程的父进程设置的。 父进程只要有用父进程那也是继承的直到头一般是bash。 题外Linux 下大部分命令都是通过子进程的方式执行的但是还有一部分命令不通过子进程的方式执行而是由 bash 自己执行调用自己的对应的函数来完成特定的功能比如 cd 命令我们把这种命令叫做 内建命令。 1.4.3 main函数前两个参数 main函数前两个参数叫作命令行参数第三个叫作环境变量参数。 第一个参数int argc是个整型变量表示命令行参数的个数含第一个参数。 第二个参数char *argv[ ]是个存放字符指针的数组每一个元素一个字符指针指向一个字符串。这些字符串就是命令行中的每一个参数字符串。 第三个参数cahr *envp[ ]也是存放字符指针的数组数组的每一个原元素是一个指向一个环境变量字符串的字符指针。 前面讲了第三个参数现在讲前两个参数第二个参数也是指针数组第一个应该是第二个参数指向的个数先打印一下 编译运行 通过一顿乱敲机智的童鞋已经发现什么了 这就是Linux下命令设置的方式 第一个参数int argc是个整型变量表示命令行参数的个数含第一个参数。 第二个参数char *argv[ ]是个存放字符指针的数组每一个元素一个字符指针指向一个字符串。这些字符串就是命令行中的每一个参数字符串。 所以第二个参数的数组存放的指针指向的就是命令行参数。argc是含第一个参数的参数个数。 Linux下命令设置的方式再规范一丢丢就是这样的 编译运行 再加上前面把process路径设置成不用绝对路径的形式和 ls 命令差不多了。
至此main函数的三个参数全部讲完。先跑步去脑子累死......洗完澡复活复活 2. 进程地址空间
进程地址空间可以叫作程序地址空间不准确也叫作内存地址空间也叫作虚拟地址空间。
我们先基于是什么为什么怎么办问三个问题
① 什么是进程地址空间是什么2.2
② 为什么要有进程地址空间为什么2.3
③ 进程地址空间是如何设计的怎么办2.1
2.1 验证进程地址空间的分步
进程地址空间是如何设计的怎么办
之前在学习C和C的时候经常画过类似的空间布局图其实就是进程地址空间的分步
我们先验证下 但是真的理解它吗物理内存中就是这样的吗其实并不是这样的。来看一段代码这里返回rtx2目录创建linux_9目录
写一个新的Makefile以前写的是这样的 如果有很多文件要这样写 一直有就一直要写两遍所以就有一个符号可以这样写 以后输入只有一个文件我们也这样写了这里应该没配置过所以加上-stdc99 验证最开始的那张内存图写test.c
#include stdio.h
#include unistd.h
#include stdlib.hint un_g_val;
int g_val 100;int main(int argc, char* argv[], char* env[])
{printf(code addr : %p\n, main);printf(init global addr : %p\n, g_val);printf(uninit global addr : %p\n, un_g_val);char* m1 (char*)malloc(100);printf(heap addr : %p\n, m1);printf(stack addr : %p\n, m1);for (int i 0; i argc; i) {printf(argv addr : %p\n, argv[i]); }for (int i 0; env[i]; i) {printf(env addr : %p\n, env[i]);}return 0;
} 编译运行 验证成功整体向高地址增长的再验证一下堆像高地址增长栈向低地址增长 编译运行 所以堆像高地址增长栈向低地址增长堆栈相对而生 我们再来理解一下 static 变量如何理解 static 变量 一个变量在函数内被定义如果声明其为 static如果不声明应该是在栈上的 那么它的作用域不变但它的生命周期会随着程序存在一直存在。 我们可以加入一个 static 变量进刚才的代码中我们来观察观察
int a 77;
static int s_a 777;printf(%p\n,a);
printf(%p\n,s_a); 编译运行 至此成功验证进程地址空间的分步。 2.2 进程地址空间的引入
还是看一段代码
#include stdio.h
#include unistd.h
#include sys/types.hint g_val 100;
int main()
{pid_t id fork();if (id 0){while (1){printf(子进程: %d, ppid: %d, g_val: %d, g_val: %p\n, getpid(), getppid(), g_val, g_val);sleep(1);}}else if (id 0){while (1){printf(父进程: %d, ppid: %d, g_val: %d, g_val: %p\n, getpid(), getppid(), g_val, g_val);sleep(1);}}else{printf(创建子进程失败\n);}return 0;
} 编译运行 当父子进程没有人修改全局数据的时候父子是共享该数据的。
如果此时尝试写入比如我们让子进程有一个修改的操作。
我们让子进程执行五次之后给它改值 编译运行 父子进程打出来的地址是一样的值却不一样
所以我们在 C/C 中使用的地址绝对不是真实物理内存的地址。
如果是物理地址上面出现的那种现象是不可能产生的
不是物理地址那是什么呢就是下面进程地址空间的虚拟地址。 虚拟地址 在 Linux 下也称为 线性地址有些教材中也称之为 逻辑地址。这三个概念实际上是不一样的但是在 Linux 下它是一样的这和其本身的空间布局有关系。 2.3 进程地址空间是什么 ① 什么是进程地址空间是什么 进程地址空间是指每个进程在计算机内存中所占用的地址空间。地址空间是指能被访问的内存地址范围它由若干个连续的内存块组成。每个进程都有自己的地址空间这意味着每个进程都有自己的内存地址范围不会与其他进程冲突。 进程地址空间通常被划分为几个部分 代码段存储程序代码的内存区域。 数据段存储程序运行时所使用的数据的内存区域。 堆动态分配内存的区域。 栈存储函数调用时所需的数据(如参数、返回地址和临时变量)的区域。 每个进程在执行时都会使用自己的地址空间。进程间通信时必须通过操作系统提供的机制来实现因为不同进程之间的地址空间是独立的。 一个列子 有一个大富翁他有十亿美金他有三个私生子这些私生子都不知道彼此的存在大富翁为了鼓励这些私生子努力工作分别与三个孩子单独会面并且许诺他们在自己死后把全部的资产继承给这个孩子所以所有的私生子都非常努力的工作而且所有的私生子都认为十亿美金迟早是自己的。这十个亿的美金只是大富翁给所有私生子画的一个大饼。 但是私生子也需要花钱于是每一个私生子有需要就问大富翁要钱每次只要几十或者几百万等等的美金。 我们以系统的方式进行理解 大富翁就是操作系统十亿美金真实的物理内存画出的饼就是虚拟的进程地址空间这三个私生子就是进程所有的进程都认为自己会独占系统资源实际上这只是操作系统给进程画的饼。所有私生子要的几十或者几百万等等美金只是进程申请的资源和空间。 操作系统怎么画饼呢 先描述再组织 内核中的地址空间本质也是一种数据结构要和一个个进程关联起来。 内核中的地址空间本质也是一种数据结构所以就要有区域的划分 这样一来进程地址空间就被描述了出来也就是被划分了出来而且可以通过修改结构体变量中的值来调整各个区域的大小。 每一个进程在启动的时侯都会让操作系统给它创建一个地址空间该地址空间就是 进程地址空间
操作系统为了管理一个进程给该进程维护一个 task_struct 叫做进程控制块。
虚拟地址空间是怎么和内存联系起来的现代计算机提出了下面的机制 物理内存本身是可以被顺便读写的非常的不安全所以就有了上面虚拟地址空间的机制 所以进程地址空间是内存吗不是进程地址空间不是内存。 页表在讲线程的时候才会更好讲解这里弱化一下虚拟地址空间和页表每个进程都有一份所以保证了进程之间的独立性所以就有了这样的图 2.4 为什么要有进程地址空间
为什么要有进程地址空间三大原因进程地址空间的意义 1. 上面不还是访问了物理内存吗怎么保证安全 进程地址空间会识别不安全的命令并拒绝这样就能保证物理内存的安全了 凡是非法的访问或者映射OS都会识别到并终止你这个进程。 因为地址空间和页表是OS创建并维护的也就意味着凡是想使用地址空间和页表进行映射也一定要在OS的监管之下来进行访问。 也便保护了物理内存中的所有的合法数据包括各个进程以及内核的相关有效数据。 2. 进程地址空间的存在可以更方便的进行进程和进程的数据代码之间的解耦合减少模块与模块直接的关联性以前说过开发要尽量低耦合高内聚保证了进程的独立性。 不同的进程即使操作同一块空间因为虚拟地址空间的存在操作系统在将不同进程的相同地址和物理空间做映射的时候就可以在物理内存中分配不同的物理空间供不同的进程使用。如此一来即使进程操作的是相同的地址但是映射到物理内存中后操作的物理空间就不同了并不会互相影响保证了进程的独立性。 3.因为在物理内存中理论上可以任意位置加载那么物理内存中的几乎所有的数据和代码在内存中是乱序的。 但是因为页表的存在它可以将地址空间上的虚拟地址和物理地址进行映射那么在进程视角所有的内存分布都可以是有序的。地址空间页表的存在可以将内存分布有序化。 结合第2个原因。让进程以统一的视角来看待进程对应的代码和数据等不同的区域也方便编译器以统一的视角规则来进行编译。 进程空间地址的存在让进程以为自己拥有所有的空间从而可以随意进行操作而不用考虑是否会影响到其他的进程。 同样的编译器在编译不同进程的时候只需要按照进程地址空间一套规则编译即可每个进程都一视同仁不用考虑不同进制之间的影响。 总的来说进程地址空间的存在就是让各个进程只做自己的事而不用考虑其他人编译器在编译的时候也只需要考虑一个进程。 不同进程的虚拟地址最后会由操作系统通过页表与物理内存映射起来。 因为有地址空间的存在每一个进程都认为自己拥有4GB空间(32)并且各个区域是有序的进而可以通过页表映射到不同的区域来实现进程的独立性。 每一个进程不知道也不需要知道其他进程的存在。大富翁的故事 所以为什么要有进程地址空间三个关键1保护物理内存2解耦合3有序地保证独立性。 本质上(因为有地址空间的存在所以上层申请空间mallocnew其实是在地址空间上申请的物理内存可以甚至一个字节都不给你。而当你真正进行对物理地址空间访问的时候才执行内存的相关管理算法帮你申请内存构建页表映射关系)然后在让你进行内存的访问。上面都是由操作系统自动完成的用户和进程完全零感知。 这样的机制叫作缺页中断。这个名词先知道就行 3. 进程创建fork
3.1 fork函数概念和用法
在linux中fork函数是非常重要的函数它从已存在进程中创建一个新进程。新进程为子进程而原进程为父进程。前面《零基础Linux_7(进程)冯诺依曼结构操作系统原理进程的概念和基本操作》中已经讲了一部分fork的内容。 函数pid_t fork(void);头文件unistd.h返回值子进程返回0 父进程返回的是子进程的pid创建失败返回-1作用创建一个子进程如果创建成功返回两个值。 进程调用fork当控制转移到内核中的fork代码后 内核 分配新的内存块和内核数据结构给子进程 将父进程部分数据结构内容拷贝至子进程 添加子进程到系统进程列表当中 fork返回开始调度器调度 fork之前父进程独立执行fork之后父子两个执行流分别执行。
注意fork之后谁先执行完全由调度器决定。 进程调用 fork当控制转移到内核中的fork代码后操作系统会做什么 ① 将给子进程分配新的内存块和内核数据结构 创建 task_struct 和进程地址空间。 ② 将父进程部分数据结构内容拷贝至子进程 以父进程为模板设置子进程的相关数据结构和父进程相关字段保持一致。 task_struct、地址空间、区域划分很多东西都是一样的。 但不是无脑拷贝比如累计调度的时间片是不一样的。 ③ 添加子进程到系统进程列表当中 取决于你进程是要做什么创建后如果状态没问题就会直接链入运行队列中。 ④ fork 返回开始调度器调度 当准备返回时上面三个工作都有了父进程继续执行开始 return子进程也可能执行 fork 的返回值然后就会得到两次返回。 那么fork之后是否只有fork之后的代码是被父子进程共享的 实际上fork之后代码共享这样的说法并不准确。一般情况fork之后父子共享所有的代码 子进程执行的后续代码不等于共享的所有代码只不过子进程只能从这里开始执行。 子进程怎么知道从这里开始执行靠的是eip eip 叫做 程序计数器用来保存当前正在执行的指令的下一条指令。 eip 程序计数器会拷贝给子进程子进程便从该 eip 所指向的代码处开始执行。 我们再来重新思考一下fork之后操作系统会做什么 进程 进程的数据结构 进程的代码和数据 创建子进程的内核数据结构 struct task_struct struct mm_struct 页表 代码继承父进程数据以写时拷贝的方式来进行共享或者独立。 所以fork之后创建一批结构代码以共享的方式数据以写时拷贝的方式两个进程保证 独立性做到互不影响。在这种共享机制下子进程或父进程任何一方挂掉不会影响另一个进程。 3.2 写时拷贝
程序被编译出来没有被加载的时候程序内部有地址吗有有没有区域也有 我们程序内部的地址和内存的地址是没有关系的。 编译程序的时候我们就认为程序是按照 0000 0000到FFFF FFFF进行编址的。 虚拟地址空间不仅仅是操作系统会考虑编译器也会考虑。 每个进程都会创建一个 task_struct每一个进程都会维护一个 mm_struct自己有对应的区域当我们的程序加载到内存时程序有自己的加载到物理内存的物理地址虚拟地址和物理地址建立映射关系进程访问某个区域当中的地址时经过页表找到对应的代码和数据。当找到代码和数据后代码加入到对应的 CPU 中代码中的地址在加载中就已经转化成了线性地址/虚拟地址所以 CPU 可以继续照着这个逻辑向后运行。 所以刚才我们代码测试打印看到的虚拟地址值是一样的并且内容也是一样的。在没有人写入的时候虚拟地址到物理地址之间映射的页表是一样的所以指向的代码和数据都是一样的。 因为进程具有独立性比如如果此时子进程把变量改了写入就会导致父进程识别的问题就出现了父进程和子进程不一的情况因为进程是具有独立性的所以我们就要做到互不影响。我们的子进程要进行修改了影响到父进程怎么办当我们识别到子进程要修改时操作系统会重新给子进程开辟一段空间并且把 100 拷贝下来重新给进程建立映射关系所以子进程的页表就不再指向父进程所对应的 100 了而直接指向新的 100。你在做修改时又把它的值从 100 改成 777时我们就出现了 改的时候永远改的是页表的右侧左侧不变 的情况所以最后你看到了父子进程的虚拟地址一样但是经过页表映射到了不同的物理内存所以了你看到了一个是 100 一个是 777父子进程的数据不同的结果。 我们的操作系统当我们的父子对数据进行修改时操作系统会给修改的一方重新开辟一块空间并且把原始数据拷贝到新空间当中这种行为就是 写时拷贝。 当父子有任何一个进程尝试修改对应变量时有一个人想修改就会触发写时拷贝让他去拷贝新的物理内存这只需要重新构建也表的映射关系虚拟地址是不发生任何变化的所以最终你看的结果是虚拟地址不变而内容不同。 操作系统为什么要写时拷贝创建子进程的时候就把数据分开不行吗 有浪费空间之嫌父进程的数据子进程不一定全用即便使用也不一定全部写入。最理想的情况只有会被父子修改的数据进行分离拷贝。不需要修改的数据共享即可。但是从技术角度实现复杂。如果 fork 的时候就无脑拷贝数据给子进程会增加 fork 的成本内存和时间 最终采用写时拷贝只会拷贝父子修改的、变相的就是拷贝数据的最小成本。拷贝的成本依旧存在。 写时拷贝实际上以一种 延迟拷贝策略延迟拷贝最大的价值只有真正使用的时候才给你拷贝。其最大的意义在于你想要但是不立马使用的空间先不给你那么也就意味着可以先给别人。反正拷贝的成本总是要有早给你晚给你都是一样。万一我现在给你你又不用那其实不很浪费所以我选择暂时先不给你等你什么时候要用什么时候再给。这就变相的提高了内存的使用情况。 我们在前面零基础Linux_7(进程)冯诺依曼结构操作系统原理进程的概念和基本操作
就提出过一个问题关于 fork 为什么有两个返回值的问题。
当时我们还提出了两个问题局限于当时还没有讲到进程地址空间所以没有办法深入讲解
我直接穿越回去截个图 fork 有两个返回值pid_t id同一个变量为什么会有两个返回值
现在我们就可以理解了因为当它 return 的时候pid_t id 是属于父进程的栈空间中定义的。
fork 内部 return 会被执行两次return 的本质就是通过寄存器将返回值写入到接收返回值的变量中。当我们的 id fork() 时谁先返回谁就要发生 写时拷贝。所以同一个变量会有不同的返回值本质是因为大家的虚拟地址是一样的但大家的物理地址是不一样的。 还有如何理解父子进程让if和else if同时执行 fork之后的代码父子进程是共享的。
也就是说在fork之后的代码父子进程是共同执行的并且父子进程使用的是同一块物理空间中的代码。但是各自的id值是不同的所以会父子进程会进入不同的条件判断中并且执行不同的代码。 本篇完。
下一篇零基础Linux_10进程进程终止main函数的返回值进程等待。
再下一篇进程程序替换实现简单的shell。