目录
1、环境变量
我们都清楚自己写的一串代码,经过编译后生成可执行程序,我们用./即可运行,但是系统里有些命令也是64位的可执行程序:
- 既然都是程序,那就可以把你自己的写的程序叫做指令,把系统的指令叫做命令程序or二进制文件。所以自己写的程序和系统中的指令没区别,均可以称为指令、工具、可执行程序。
但是系统里的命令(ls、pwd……)可以直接用,既然你自己写的可执行程序myproc也是命令,那为什么不能像系统中的那样直接使用呢?反而要加上./才能运行。
- 注意看这里的报错:command not found,就是说执行一个可执行程序,前提是要先找到它,这也就说明了系统的命令能找到它,自己写的程序却找不到它。
原因:linux系统中存在相关的环境变量,保留了程序的搜索路径的!所以出现上面的情况。下面就来具体讲解环境变量。
基本概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量
输入指令env能够查看所有环境变量:
不过常见的如下:
- PATH:系统中搜索可执行程序(命令)的环境变量
- HOME:指定用户的家目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL:当前Shell,它的值通常是/bin/bash。
如下查看系统中的PATH命令:
查看环境变量方法
我们通过echo命令来查看环境变量,输入如下指令:
echo $NAME //NAME:你的环境变量名称
我们以查看具体的PATH环境变量示例:
注意这里的路径分隔符是用:间隔的,当我们输入ls指令时,系统会在这些路径里面一个一个寻找,找到了就执行特定路径下的ls。 这也就证实了一开始我说的,因为你自己写的myproc程序不在此路径里头,自然不能直接用。
测试PATH
还是以我们先前创建的myproc文件示例:
根据我们前面的分析得知,我们不能让自己写的可执行程序myproc像系统的命令一样直接使用:
如果我非要让自己写的myproc像系统中的命令样使用,有如下两种方法:
- 1、手动添加到系统路径/usr/bin/里头
- 2、使用export命令把myproc当前所处的路径也添加到PATH环境变量里
1、手动添加到系统路径/usr/bin/里头:
但是并不建议把你自己写的可执行程序随意添加到系统里头(会污染),所以执行下面的命令删除即可:
sudo rm /usr/bin/myproc
2、使用export命令把myproc当前所处的路径也添加到PATH环境变量里:
Linux命令行也是可以定义变量的,分为两种:
- 本地变量
- 环境变量(全局属性)
如果我们在变量前面加上export,这就是导出环境变量:
下面演示把myproc的路径导入PATH里头,输入下面的命令:
export PATH=$PATH:/home/xzy/dir/date12
这条命令的意思是把所有的PATH环境变量的内容提取出来放到PATH里头,并且在后面追加myproc的当前路径。往后就可以像命令一样直接使用myproc了:
如果想删除这个环境变量的话,执行unset命令。
测试HOME
任何一个用户在运行系统登录时都有自己的主工作目录(家目录),环境变量HOME当中即保存的该用户的主工作目录。
普通用户示例:
root超级用户示例:
和环境变量相关的命令
- 1、echo: 显示某个环境变量值
- 2、export: 设置一个新的环境变量
- 3、env: 显示所有环境变量
- 4、unset: 清除环境变量
- 5、set: 显示本地定义的shell变量和环境变量
前面几个命令已经说过,下面来看下set命令:
我们使用export可以导出环境变量,使用env显示环境变量:
不加export定义的就是本地变量,可以通过set命令查看本地变量,也可以查看环境变量:
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
main函数的三个参数
前面获取环境变量是通过命令行的方式,下面我将通过代码程序来演示如何获取环境变量。
问:main函数可以带参数吗?最多可以带多少?
其实main函数的参数有3个,只是平时不经常用到,所以也没咋见过它们:
int main(int argc, char* argv[], char* envp[]) { return 0; }
- char* argv[ ]:指针数组
- int argc:数组里的元素个数
我们先谈它的前两个参数,先来一段代码作为测试用例:
运行结果如下:
main函数的第二个参数是一个字符指针数组,此数组的下标0的位置存储的是你命令行的第一个位置(可执行程序),所以这里是第一行为:argv[0]:./myproc。其余的字符指针存储的是命令行对应的选项,所以会出现argv[1]:-a……,而main函数里的第一个参数存储的是数组里的元素个数。
总结:我们给main函数传递的argc,char* argv[ ]是命令行参数,传递的内容是命令行中输入的程序名和选项,并且结尾以NULL结束!!!
问:main函数传这些参数的意义是什么?
假设我们现在要实现一个命令行计算器,如果输出./myproc -a 10 20,那么就是加法10+20=30,如果输出./myproc -s 10 20,那么就是减法10-20=-10……。代码如下:
#include<stdio.h> #include<unistd.h> #include<string.h> #include<stdlib.h> int main(int argc, char* argv[]) { if (argc != 4) { printf("Usage: %s [-a|-s|-m|-d] one_data two_data\n", argv[0]); return 0; } int x = atoi(argv[2]); int y = atoi(argv[3]); if (strcmp("-a", argv[1]) == 0) { printf("%d+%d=%d\n", x, y, x + y); } else if (strcmp("-s", argv[1]) == 0) { printf("%d-%d=%d\n", x, y, x - y); } else if (strcmp("-m", argv[1]) == 0) { printf("%d*%d=%d\n", x, y, x * y); } else if (strcmp("-d", argv[1]) == 0 && y != 0) { printf("%d/%d=%d\n", x, y, x / y); } else { printf("Usage: %s [-a|-s|-m|-d] one_data two_data\n", argv[0]); } return 0; }
此时我们就可以运行此程序并通过命令行参数来实现我们想要的计算方式:
- 综上:同一个程序,通过传递不同的参数,让同一个程序有不同的执行逻辑,执行结果。Linux系统中,会根据不同的选项,让不同的命令,可以有不同的表现,这就是指令中各个选项的由来和起作用的方式!!这也就是命令行参数的意义,同样也就是main函数参数的意义。
下面来谈下main函数的第三个参数:
int main(int argc, char* argv[], char* envp[]) { return 0; }
char*envp就是环境变量。也是一个字符指针数组,前面的argv是指向的命令行参数字符串,而这里envp指向的是一个一个环境变量字符串,最后也是以NULL结尾。结构图如下:
我们以如下代码来测试第三个参数:
int main(int argc, char* argv[], char* env[]) { for (int i = 0; env[i]; i++) { printf("env[%d]: %s\n", i, env[i]); } return 0; }
- 总结:一个进程是会被传入环境变量参数的。
补充:一个函数在声明和定义的时候都没有参数,那么我实际传参的时候可以传参。
通过代码获得环境变量
上面在学校main函数的三个参数的时候,我们得知通过main函数的第三个参数可以获得环境变量:
我们也可以通过第三方变量environ获取:
通过系统调用获取环境变量
- 除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。
getenv可以通过目标环境变量名进行查找,返回一个对应的字符指针,从而直接获得环境变量的内容:
问:我为什么要获得环境变量?
- 假设我们当前的用户USER为xzy,但是我只允许自己使用,不允许其它人(zxy)访问,就可以用获得环境变量来解决:
此时用xzy用户是可以正常访问的,但是zxy用户就不能了:
综上,环境变量一定在某些地方有特殊用途,上面粗略的展示了其中一个方面。
环境变量通常是具有全局属性的
先来回顾下bash进程:
- 我们先前说过,子进程pid每次运行的结果是在不断变化的,因为其每次运行,进程都在重启,但是父进程是不变的,因为父进程就是bash,是系统创建的命令行解释器,
- 如果我现在把bash杀掉,出现的结果是我输入任何命令都没有反应了,命令行直接挂掉。所以我们能够正常使用命令行,是因为这些命令本身就是被bash进程获得的。而这个bash进程是在我们登录系统时,系统自动帮你创建的。命令行中启动的进程,父进程全都是bash。
下面来理解环境变量具有全局属性:
- 看如下代码:(在原有的pid和ppid基础上添加了获取环境变量)
- 通过运行结果得知:我们的进程刚开始并不存在环境变量,但是若我们自己导出一个环境变量,再运行看看:(此时就获取到了环境变量)
总结:
- 环境变量是会被子进程继承下去的!!类比于从bash进程开始,往后创建一大批的子进程,若在bash进程的位置就创建了export环境变量,那么此环境变量就会从定义处bash位置开始被所有的子进程拿到。所以环境变量具有全局属性!!!
- 而本地变量之所以在本地有效,是因为本地变量是在bash内部定义的变量,不会被子进程继承下去!!!
补充:
- 这里local_val是一个本地变量,命令行中启动的所有程序都要创建子进程,echo也是一个命令、一个子进程, 你父bash内定义的local_val变量怎么能被子进程读到呢?不是说本地变量不会被子进程读到吗?后面为何能用echo打出hello呢?
Linux下大部分命令是通过子进程的方式执行的!但是,还有一部分命令,不通过子进程的方式执行,而是由bash自己执行(调用自己的对应的函数来完成特定的功能),我们把这种命令叫做内建命令。
2、程序地址空间
相信我们在学习C的过程中,下面这幅图都见过:
1、验证程序地址空间的排布:
- 程序地址空间不是内存。我们在linux操作系统中通过代码对该布局进行如下的验证:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int un_g_val; int g_val = 100; int main() { 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);//栈区 return 0; }
- 通过我们这段代码的运行结果看,从上到下地址是在逐渐增大的,且栈区和堆区之间有一块非常大的镂空,也证实了我们的程序地址空间的布局是按照上图所示的地址空间的布局。
2、验证堆和栈增长方向的问题:
- 堆:
我们用如下代码进行测试:
运行结果如下:
从运行结果看,堆区的确是向上增长。
- 栈:
我们用如下代码进行测试:
从图中可以看出,栈区向上减少。
总结:
- 堆区向地址增大方向增长(箭头向上)
- 栈区向地址减少方向增长(箭头向下)
- 堆,栈相对而生
- 我们一般在C函数中定义的变量,通常在栈上保存,那么先定义的一定是地址比较高(先定义先入栈,后定义后入栈)的。
3、如何理解static变量:
- 先前我们知道如果一个变量被static修饰,它的作用域不变,依旧在该函数内有效,但是其声明周期会随着程序一直存在,可为什么呢?
先来看下正常定义的变量:
此时正常定义的变量s就符合先前栈的地址分布规则:后定义的变量在地址较低处,下面来看下static定义的变量:
- 根据图示:变量s一旦被static修饰,尽管s是在代码函数里面被定义,可是此变量已经不在栈上面了,此时变成了全局变量,这也就是为什么声明周期会一直存在。
总结:函数内定义的变量static修饰,本质是编译器会把该变量编译进全局数据区内。
感知虚拟地址空间的存在
用下面代码为示例:
当父子进程没有人修改全局数据的时候,父子是共享该数据的!可以通过下面的运行结果看出:
如果尝试写入呢?
- 这里父子进程读取同一个变量(因为地址一样),但是后续在没有人修改的情况下,父子进程读取到的内容却不一样!!!怎么会出现子进程和父进程对全局变量的地址是一样的,但是输出的内容确是不一样的呢?
结论:我们在C、C++中使用的地址,绝对不是物理地址。因为如果是物理地址,上述现象是不可能产生的!!!这种地址我们称之为虚拟地址、线性地址、逻辑地址!!!
补充:为什么我的操作系统不让我直接看到物理内存呢?
- 因为不安全,内存就是一个硬件,不能阻拦你访问!只能被动的进行读取和写入。不能直接访问。
3、进程地址空间
之前说‘程序的地址空间’其实是不准确的,准确的应该说成进程地址空间,概念如下:
- 每一个进程在启动的时候,都会让操作系统给它创建一个地址空间,该地址空间就是进程地址空间。每一个进程都会由一个自己的进程地址空间。操作系统需要管理这些进程地址空间,依旧是先描述,再组织。所谓的进程地址空间,其实是内核的一个数据结构(struct mm_struct )
- 前面我们知道进程是具有独立性的,体现在相关的数据结构是独立的,进程的代码和数据是独立的……。类比于一位海王同时撩三个女的(广撒网,钓大鱼)。并对每一个女的说我只中意你,且画大饼说以后对你怎么怎么好……,从而使得每个女的都天真的认为我是不可替代的那个人。这个例子中,海王充当的就是OS,三个女的就是进程,海王画的大饼就是进程地址空间。海王画大饼 的原因在于为了维护这三个女的独立性,互补干扰,若有交集则必然乱套。
综上:所谓的进程地址空间,其实就是OS通过软件的方式,给进程提供一个软件视角,让其认为自己会独占系统的所有资源(内存)。
分页 & 虚拟地址空间
在Linux内核中,每个进程都有task_struct结构体,该结构体有个指针指向一个结构mm_struct(程序地址空间),我们假设磁盘的一个程序被加载到物理内存,我们需要将虚拟地址空间和物理内存之间建立映射关系,这种映射关系是通过页表(映射表)的结构完成的(操作系统会给每一个进程构建一个页表结构)。如下图:
问1:什么叫做区域(代码区……)?
- 区域就类似于小时候桌子上的三八线,一半是你的,一半是我的,而每一半又可以进行划分,这块放书,这块放笔盒,这块放水杯……,就和这里的mm_struct划分的机制差不多,而此结构体就是按照类似如下的代码方式进行限制的:
struct mm_struct { long code_start; long code_end; long init_start; long init_end; long uninit_start; long uninit_end; //…… }
问2:程序是如何变成进程的?
- 程序被编译出来,没有被加载的时候,程序内部是有地址和区域的。不过这里的地址采用的是相对地址的方式,而区域实际是在磁盘上已经划分好了。加载无非就是按照区域加载到内存。
问3:为什么先前修改一个进程时,地址是一样的,但是父子进程访问的内容却是不一样的?
- 当父进程被创建时,有自己的task_struct和地址空间mm_struct,地址空间会通过页表映射到物理内存,当fork创建子进程的时候,也会有自己的task_struct和地址空间mm_struct,以及子进程对应的页表,如下:
- 而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。所以先前打印的g_val的值和内容均是一样的。当子进程需要修改数据g_val时,结果就变了,先看图:
- 无论父进程还是子进程,因为进程具有独立性,如果子进程把变量g_val修改了,那么就会导致父进程识别此变量的时候出现问题,但是独立性的要求是互不影响,所以此时操作系统会给你子进程重新开辟一块空间,把先前g_val的值100拷贝下来,重新给此进程建立映射关系,所以子进程的页表就不再指向父进程的数据100了,而是指向新的100,此时把100修改为200,无论怎么修改,变动的永远都是右侧,左侧页表间的关系不变,所以最终读到的结果为子进程是200,父进程是100.
- 总结:当父子对数据修改的时候,操作系统会给修改的一方,重新开辟一段空间,并且把原始数据拷贝到新空间中,这种行为我们称之为写时拷贝。通过页表,将父子进程的数据就可以通过写时拷贝的方式,进行了分离。从而做到父子进程具有独立性的特点。
问4:fork有两个返回值,pid_t id,同一个变量,怎么会有不同的值??
- 一般情况下,pid_t id是属于父进程的栈空间中定义的变量,fork内部,return会被执行两次,return的本质就是通过寄存器将返回值写入到接收返回值的变量中!当id = fork()的时候,谁先返回,谁就要发生写时拷贝,所以,同一个变量,会有不同的内容值,本质是因为大家的虚拟地址是一样的,但是大家对应的物理地址是不一样的。
问5:为什么要有虚拟地址空间?
- 1、保护内存
假设我们写了个非法访问野指针(*p = 110),假设此野指针指向了进程2,甚至直接指向了操作系统,那么当你进行访问的时候,你就会直接修改其它进程的数据,所以直接让进程访问物理内存的方式是不安全的。可是假设虚拟地址空间就不会出现这种现象了,因为遇到野指针,页表就不会给你建立映射关系,那么就不会给你访问到物理内存的机会。综上,有了虚拟地址空间,相当于在访问内存的时候添加了一层软硬件层,可以对转化过程进行审核,非法的请求就可以直接拦截了。
- 2、可以把Linux内存管理,进程管理通过地址空间进行功能模块的解耦。
- 3、让进程或者程序可以以一种统一的视角看待内存,方便以统一的方式来编译和加载所有的可执行程序,简化进程本身的设计与实现!
4、Linux2.6内核进程调度队列-选学
一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的负载均衡问题
优先级
- 普通优先级: 100~ 139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
- 实时优先级: 0~ 99(不关心)
活动队列
时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
- 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
总结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法!