问:为什么我们执行自己编译的可执行程序的时候必须带路径,但是执行系统命令的时候比如
ls
、pwd
等命令的时候不需要带路径?(注:ls、pwd等命令本质也是可执行程序,存储在路径/usr/bin/目录下)答:系统中是存在相关的环境变量的,保存了程序的搜索路径的。
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
注意:在Linux中有环境变量和普通变量两种变量,环境变量就是上面所描述的,普通变量就像下面这一种:
上面的aaaa就是普通变量,无法通过env环境变量命令查看到。
问:我们自己如何定义环境变量?
答:可以通过
export NAME=xxx
来进行定义(NAME是环境变量名,xxx是我们想给环境变量定义的值)问:如何取消定义的环境变量?
答:通过
unset NAME
命令可以进行取消我们自己定义的环境变量:
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
使用env命令查看环境变量:
echo $NAME
//NAME:环境变量的名称
使用举例:
此时就可以回答了,使用ls
、pwd
等命令时,能够正常使用的原因是这些命令对应的可执行程序在PATH环境变量所标明的路径下,我们自己的可执行程序不能像这样运行的原因是因为我们自己的可执行程序并没有在PATH路径下。
问:如果我们想直接使用我们编译形成的可执行文件该如何去做?
答:
方法一:将编译形成可执行文件拷贝入到PATH路径下。(使用
sudo cp NAME PATH
)//NAME是可执行程序文件名,PATH是方法二:将当前文件的路径添加到PATH路径下。
注意:在修改环境变量时,上面的方式是属于新增,而不是覆盖,最好不要进行覆盖,但是一旦覆盖了的话可以通过重启终端来解决,因为我们进行的修改只是在内存上进行的修改,所以并不会有任何的影响。
注意:
which
命令之所以能够找到我们的命令所在路径就是通过环境变量查找可执行程序的。
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
先了解C/C++main()函数的前两个参数:
运行结果:
分析argv结构:
从上面可以知道,给main()函数传递的argc、argv[],命令行参数传递的是,命令行中输入的程序名和选项。
问:main()函数传递命令行选项的意义何在?
答:通过传入不同的参数,让同一个程序有不同的执行逻辑和执行结果,这也是为什么我们能够通过对指令进行不同的选项能够有不同表现的原因。
使用举例:简易计算器:
代码:
#include
#include //./process -a 10 20 //10 + 20 = 30 //./process -s 10 20 //10 - 20 = -10 //./process -m 10 20 //10 * 20 = 200 //./process -d 10 20 //10 / 20 = 0 int main(int argc, char* argv[]) { if(argc != 4) { printf("Usage: %s [-a|-s|-m|-d] firstData secondData\n", argv[0]); return 0; } int x = atoi(argv[2]);//atoi()函数的作用是将char*类型的字符串转换为整数 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] firstData secondData\n", argv[0]); return 0; } return 0; } 使用详情:
- 命令行第三个参数
#include
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++)
{
printf("%s\n", env[i]);
}
return 0;
}
- 通过第三方变量environ获取
#include
int main()
{
extern char** environ;
int i = 0;
for(; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
- getenv()函数,获取环境变量
使用举例:
#include
int main()
{
char* path = getenv("PATH");
printf("PATH:%s\n", path);
return 0;
}
- 环境变量通常具有全局属性,可以被子进程继承下去
代码:
执行结果:
注意:创建的myproc
进程中的环境变量也是从bash进程继承到的。
注意:所谓的本地变量,本质就是在bash内部定义的变量,不会被子进程继承下去。
问:既然本地变量不会被子进程继承,那么我们
echo $NAME
是如何将本地变量进行打印的?答:Linux下的大部分命令都是通过子进程的方式进行执行的,但是还有一部分命令不通过子进程的方式进行执行,而是由bash自己执行(直接调用自己的对应的函数来完成特定的功能),我们把这类命令叫作内建命令。
测试代码:
运行结果:
注意:静态局部变量和全局变量存储的位置是一样的,即全局数据区。
代码感受:
执行结果:
子进程和父进程的g_val的值和地址是一样的。
将代码进行如下修改:
通过上述代码发现:子进程和父进程读取的同一个地址的同一个变量,但是g_val在子进程中修改后,g_val的值只在子进程中发生了改变,即变成了20,但是父进程中g_val的值依旧是10。
结论:我们在C/C++中使用的地址不是物理地址,因为如果是物理地址,就不会出现上面的情况,同一个物理地址中存储的必然是同一个值。
在上面中的地址是虚拟地址!操作系统负责将
虚拟地址
转化为物理地址
。
每一个进程在启动的时候,都会让操作系统给它创建一个地址空间,该地址空间就是进程地址空间。
注意:所谓的进程地址空间,其实是内核的一个数据结构,和task_struct类似,类型叫struct mm_struct。
struct mm_struct
{
//代码区
long code_start;
long code_end;
//初始化全局数据区
long init_start;
long init _end;
······
}
问:程序被编译出来,没有被加载到内存的时候,程序内部有地址吗?程序内部有区域吗?
答:可执行程序在编译后,本身就是有自己的一套地址,并且本身也是有区域的,这些都在磁盘上已经划分好了。
编译程序的时候,就认为程序是按照0000~FFFF进行编址的。
对上面g_val现象进行解释:
修改前:
修改后(写时拷贝):
因为打印的始终都是虚拟地址,所以即使修改后,&g_val的值仍然没有发生改变。
问:fork有两个返回值,pid_t id,同一个变量,为什么会有不同的值?
答:pid_t id是父进程栈空间中定义的变量,fork内部,return会被执行两次,return的本质,就是通过寄存器将返回值写入到接收返回值的变量中!当id = fork()的时候,谁后返回,谁就要先发生写时拷贝,所以同一个变量,会有不同的值,本质是因为大家的虚拟地址是一样的,但是大家对应的物理地址是不一样的。
问:页表是一开始就形成的,还是在运行的时候动态生成的?
答:页表是一开始就形成的,但是有一些是动态生成的,比如堆空间。
问:为什么要有虚拟地址空间?
答:1. 虚拟地址空间为访问内存添加了一层软硬件层,可以堆转化过程进程审核,非法的访问可以直接进行拦截,即保护内存的作用。2. 当我们扩大申请的内存空间时,扩大的只是地址空间,起到了节约内存的作用,并且通过地址空间在进程管理和Linux内存管理两个功能模块之间进行结耦。3. 让进程或者程序可以以一种统一的视角看待内存,即每个进程都有一个自己的进程地址空间,方便以统一的方式来编译和加载所有的可执行程序,进而简化进程本身的设计与实现。
上图是Linux2.6内核中进程队列的数据结构,
- 如果有多个CPU就要考虑进程个数的负载均衡问题
- 普通优先级:60~99
时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下 标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个 比特位表示队列是否为空,这样,便可以大大提高查找效率!
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
- active指针永远指向活动队列
- expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在 的。
- 在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
- 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增 加,我们称之为进程调度O(1)算法!