文章目录
目录
前言
一、环境变量
1. PATH
2. HOME
3. 其他环境变量
系统调用接口--getenv
4. 命令行参数
4.1 双参数main
4.2 三参数main
5. 设置环境变量
5.1 本地环境变量
5.1.1 内建命令
5.2 固定环境变量
6. 取消环境变量
7. 小总结
二、程序地址空间
1. 空间划分
2. 进程地址空间
3. 缺页中断
4. 进程地址空间的意义
前言
进程切换
为什么函数返回值会被外部拿到?
return a --> mov eax 10,是通过CPU寄存器实现的
系统如何得知我们的进程当前执行到哪行代码了?
程序计数器pc,eip:记录当前进程正在执行指令的下一行指令的地址
寄存器大致分类:
通用寄存器:eax,ebx,ecx,edx
栈帧:ebp,esp,eip
状态寄存器:status
寄存器有很多,那么寄存器扮演着什么角色?
1. 提高效率,有关进程的高频访问数据放入寄存器中
2. CPU中寄存器保存的是进程相关的数据,即进程的临时数据——进程的上下文数据,CPU中有大量的进程上下文数据
由于进程切换与时间片概念,是进程切换基于时间片轮转的调度算法,可以在同一时间,让多个进程得以推进,称之为并发。
所以进程在从CPU上离开的时候,要将自己进程的上下文数据保存好带走(一些临时数据,ebp、esp、eip等),如果不保存这些临时数据,进程轮转之后新的进程数据会覆盖原先的进程上下文数据,所以保存的目的是为了下次轮转到自己时快速恢复进程上下文数据,这些数据保存到PCB中(真正的是软硬件合作保存)
进程切换时:
1. 保存上下文
2. 回复上下文
一、环境变量
基本概念:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
例如,我们编写的C/C++代码,在各个目标文件进行链接的时候,从来不知道我们所链接的动静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,并且在系统当中通常具有全局特性。
常见环境变量:
PATH : 指定命令的搜索路径HOME : 指定用户的主工作目录 ( 即用户登陆到 Linux 系统中时 , 默认的目录 )SHELL : 当前 Shell, 它的值通常是 /bin/bash 。查看环境变量方法:echo $name //name:环境变量名称env //显示全部的环境变量
1. PATH
echo $PATH
必须要$,不然会被echo识别为字符串
这些路径用冒号进行连接,在调用指令时,系统默认从左到右,按指定路径进行搜索。如果不在指定的路径中,例如我们写的./mycmd,如果不写 ./ 只是简单的输入mycmd,会报错,指令找不到(找的操作是shell进行的)
如果将mycmd所在路径添加到PATH中,那么我们直接输入mycmd不加./,也可以直接执行,所以输入 ./ 是为了确定路径
PATH=$PATH:/home/ljs/XXXXX
#等号左右两侧不能有空格
//如果不写$PATH:那么PATH会被覆写,如果被覆写后,可以重启xshell
//因为环境变量是被存储在Linux配置文件中,每次启动后会加载到内存中
指令也是可执行程序,执行前shell要先找到路径,所以shell会维护PATH环境变量,来保存指令搜索路径。
可以知道,which指令就是从PATH环境变量中搜索路径。
2. HOME
不同的用户,$HOME不同。
在xshell中,不同用户登陆后,由xshell分配bash,命令行解释器进程,此时会默认的cd到自己的家目录路径下,而HOME环境变量就是 shell 用来保存各个用户的家目录路径。
3. 其他环境变量
和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
env
- HOSTNAME:主机名
- HISTSIZE:保存到是历史命令被保存下来的条数(history指令可以查历史指令,由histsize变量控制条数)
- SSH_TTY:终端名称,我们在xshell中可以打开多个终端,每个终端互不干扰,只在指定的终端输出就是依靠终端名分别终端
- USER:用户名
- LS_COLORS:配色方案
- PATH:可执行程序的路径
- PWD:当前进程所在路径
- LOGNAME:当前登录用户
- OLD_PWD:当前路径的上一个路径(cd - 等同于 cd $OLDPWD)
系统调用接口--getenv
系统调用接口,getenv 可以根据传入的实参字符串,返回对应的环境变量
printf("PATH: %s\n", getenv("PATH");
环境变量:是系统提供的一组形如 name = value形式的变量,不同的环境变量由不同的用户,通常具有全局属性
4. 命令行参数
我们初学c语言时,可能看到过main函数有参数
int main(int argc, char* argv[])
{
return 0;
}
其中,argv是指针数组,argc是数组内元素个数。main函数并不是第一个函数,它是被CRTStartup()调用的,那么这两个参数是干什么用的呢?
它是为指令、工具、软件等提供命令行选项的支持!
4.1 双参数main
我们在输入 ./mycmd 时,如果后面追加 -a -b -c -d这一串字符串,那么
由于argv指针数组的最后一个数据的下一个位置默认设置为nullptr,所以我们也可以改变for循环内的条件。
我们输入的 ./mycmd 在bash看来就是一个字符串,bash将空格作为分隔符,第一个字符串为指令,剩下的字符串是指令选项,bash可以得到小的字符串,根据小字符串的数量赋予argc变量值,argv数组中每个数据存放的是这些字符串首字符的地址。
那么我们可以根据这个原理,来设计带选项的指令,根据argv[1]中指向的字符串来判断携带的是什么选项
这就是指令为什么可以有选项的原因,例如 ls -l 、ls -a等等。所以命令行参数它是为指令、工具、软件等提供命令行选项的支持!根据不同的选项,表现不同的功能
所以在学c语言时,没有使用该知识点是因为当时我们不使用命令行,所以不需要。
4.2 三参数main
main函数除了这两个参数类型外,还重载的有三参数类型
int main(int argc, char* argv[], char* env[])
{
return 0;
}
我们使用env指针数组,也实现了输出全部环境变量的效果
综上,main函数有两张核心的向量表
1. 命令行参数表
2. 环境变量表
即一个进程的创建不仅仅只是将我们的程序加载到内存就结束了,而是需要被调用main函数并完成两张核心表的建立
我们所运行的进程,都是子进程,bash本身在启动时,会从操作系统的配置文件中读取环境变量信息,而子进程会继承父进程的环境变量,所以我们所有的进程环境变量几乎相同,这就体现了环境变量的全局性
进程之间具有独立性,如果子进程对环境变量有修改,那么就会发生写时拷贝
除了env、和三参数的main函数可以获取环境变量,我们还可以使用第三方变量environ来获取环境变量,可以使用man命令查看用法
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明#include <stdio.h> int main(int argc, char *argv[]) { extern char **environ; int i = 0; for(; environ[i]; i++) { printf("%s\n", environ[i]); } return 0; }
5. 设置环境变量
5.1 本地环境变量
MY_VALUE=12345678
如果再命令行处直接定义,它会成为本地环境变量,在env中查找不到
本地环境变量不会被子进程继承,它只在本bash处有效
set
查看本地环境变量
其中的ps1环境变量表示对是命令行提示符的格式,\u为用户名,\h为主机名、\w为当前工作目录,$为提示符。如果是root用户,那么提示符为#,这就表明了本地环境变量只在本bash内有效
ps2是命令行次提示符,当一行指令还没输完,可以用 \ 续行
ls \ > -a
5.1.1 内建命令
问题:既然本地变量只在本bash内部有效,那么为什么echo能够输出本地变量的值呢?(echo是指令,我们之前认为任何指令都会创建进程,该进程是bash的子进程),也就是说bash的子进程为什么能输出bash的本地变量?
答:指令分为两类
- 常规命令 -- 通过创建子进程完成
- 内建命令 -- bash不创建子进程,而是由自己完成,类似于bash调用了自己写的或是系统提供的函数,即echo在bash内部有代码实现,所以不创建子进程
那么类似的内建命令还有export、cd等命令,cd并不是一个新的bash的子进程,而是直接由bash操作,因为如果是bash子进程,那么它修改的就是子进程的当前工作目录,修改不了父进程工作目录,我们可以简单的实现以下cd功能,cd命令依靠的是chdir系统调用接口。
终端1: ./mycmd / 终端2: ps axj | head -1 && ps axj | grep mycmd 从而找到pid ls /proc/pid/cwd -l 显示当前工作目录
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main(int argc, char* argv[], char* env[]) { sleep(20); printf("change beign\n"); //判断argv的值,就是命令行参数是否为两个字符串 if (argc == 2) { chdir(argv[1]); } printf("change end\n"); sleep(20); return 0; }
这样就可以观察到mycmd进程确实更改了路径,将mycmd名称更改为bash,那么这就是cd的简答代码
5.2 固定环境变量
export MY_VALUE=12345678
再进行grep查找
此时我们再次执行我们的程序,即可证明环境变量是会被子进程继承的,因为有我们新设置的MY_VALUE
6. 取消环境变量
unset MY_VALUE
再查找就查找不到了,即一旦bash的环境变量改变,子进程继承的环境变量也会改变
7. 小总结
- 程序运行需要命令行参数表、环境变量表,这两张表是系统维护的,可以简单理解为bash(因为bash广义上也是操作系统的一部分)
- main函数不要简单的被语言方面局限了,main也是函数,它也需要被传参调用(被系统调用),传入两张表
二、程序地址空间
1. 空间划分
1. 首先,我们来验证一下各区地址的大小情况
可以看到代码区、字符常量区、已初始化全局变量、为初始化全局变量、堆区、栈区地址依次从低到高
2. 我们再来验证堆、栈各自地址的生长情况
可以看到,堆区向上生长,地址数为七位,栈区向下生长,地址数位12位,两者相对,中间空缺的部分为动静态库
如果将变量a前加上static修饰,那么a的地址数将由12位更改为7位,故static修饰的局部变量编译的时候已经被编译到全局数据区,只是作用域只在该函数内部,而生命周期是全局的
2. 进程地址空间
我们再来看进程方面
问:为什么同一个变量,同一个地址,同时读取,却读到了不同的内容?
结论:该地址一定不是物理地址,如果是物理地址,那么一定不会出现上面的现象,这种地址被称为线性地址或虚拟地址
任何一个进程都需要PCB内核数据结构和进程虚拟地址表,进程虚拟地址表的地址在PCB结构体中保存,并且有一张页表来保存映射虚拟地址到物理地址
父进程创建子进程后,子进程继承父进程的大部分PCB数据并进行修改,同时也完全拷贝父进程的虚拟地址表和页表,此时因为页表相同,所以父子进程同时指向同一块的物理地址(数据和代码都相同),当子进程检测到g_val被修改时,操作系统会先创建一个新的g_val空间,并拷贝父进程的数据,在根据子进程修改的数据进行修改,再修改页表的映射关系,原先的虚拟地址不变,修改映射的物理地址
前提是该进程正在被CPU执行
地址空间:我们的计算机地址总线排列组合形成地址范围【0,2^32】(在32位的计算机中,有32位的地址和数据总线)
所谓进程地址空间,本质是一个描述进程可视范围的大小。地址空间内一定要存在各种区域划分,对线性地址进行start,end描述。
任何一个进程都要有PCB和进程地址空间
地址空间本质是内核的一个数据结构对象,和PCB一样,也是要被操作系统管理,进行先描述,再组织
struct mm_struct
{
long code_start, code_end;
long readonly_start, readonly_end;
long init_start, init_end;
long uninit_start, uninit_end;
long heap_start, heap_end;
long stack_start, stack_end;
}
每一个task_struct都要指向自己的 mm_struct结构体,32位系统默认内存为4GB,那么每个进程都在该物理内存中规划自己的进程地址
进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,
在结构体mm_struct当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffffff线性增长的,所以虚拟地址又叫做线性地址。
32位操作系统有4GB内存,每个进程的进程地址空间可分得一部分内存大小,但是不能全部获取4GB大小内存
页表:每一个进程都要在CPU上被调度,在CPU上有一个cr3寄存器,存放的是页表的起始地址(物理地址),本质上是进程的硬件上下文,所以在进程轮转时,该数据会被保存,下一次被调度时再次加载 。当CPU需要访问物理地址的数据时,会根据cr3寄存器找到页表,从而找到物理地址
此时我们页表有三个字段,分别为进程虚拟地址、物理地址、标志位,标志位的意义在于我们查找到数据所在的物理地址后,我们应该区分该地址的数据是可读写还是只可读,这就是标志位字段的意义,标记该地址处的数据可读写情况
所以为什么代码、字符常量是只读的?因为代码加载到磁盘就是写入,而你说了磁盘的代码处是只读的,这里的关键就是页表的第三个字段——标志位,因为标志位是r,所以是只读!!
3. 缺页中断
我们也玩过大几十G的游戏,我们知道进程是要将自己的代码和数据加载到内存中的,但是内存大多是都是16或32GB,怎么能容纳几百G的游戏呢?
此时操作系统的解决方案就是对大文件实现分批加载,先加载并运行一小部分,之后再加载剩余的代码和数据,这个行为就是惰性加载。此时页表的映射关系中,虚拟地址是全填的,但是会缺少一部分的物理地址,因为此时后面的代码和数据还没有加载到内存,当CPU根据虚拟地址找到内存的物理地址,发现该物理地址处还没有加载,那么就会申请空间,将对应剩余的代码和数据加载到内存中,该机制被称为缺页中断
页表的第四个字段也是标志位,表示物理地址此处对应的代码和数据是否已经加载到内存,从而使CPU根据这一标志来判断是否触发缺页中断机制,将在磁盘中剩余的代码和数据自动加载到内存中,并构建号页表内剩余的映射关系。
所以,进程在被创建的时候是先构建内核数据结构(PCB、虚拟地址空间、页表),再慢慢加载对应的可执行程序,从而填充页表。
写时拷贝就是根据缺页中断实现的
那么进程之间具有独立性更具体的理解:
首先,每一个进程都已各自的PCB、进程地址空间、页表,这些都是互相独立的
其次是程序的代码和数据层面,因为页表的存在,即使是父子进程,代码指向相同,即使虚拟地址相同,但是它们所映射的物理地址不同,这就使得它们的数据层面解耦合,互相独立,这就是进程之间的独立性根源
4. 进程地址空间的意义
综上所述,为什么要设计进程地址空间?
- 让进程以统一的视角看待内存。如果没有进程地址空间,那么进程PCB需要记录代码和数据的物理地址,一旦进程换出再换入时,PCB还要修改内容,而且还有越界访问的风险,所以我们需要中转站,即进程地址空间这一结构,此时进程只需要根据相应的映射即可找到代码和数据
- 增加转换过程(页表),在转换过程中可以对我们的寻址请求进行审查。一旦异常访问,直接拦截,该请求不会到达物理内存,从而保护了物理内存
- 因为有地址空间和页表的存在,将进程管理模块(PCB、进程地址空间、页表)和内存管理模块(页表和物理内存)进行解耦合,只负责各自的工作
进程 = 内核数据结构(task_struct && mm_struct && 页表)+ 程序的代码和数据
所以,进程切换的时候,只用切换进程上下文数据即可,因为task_struct中有指针指向进程地址空间,而cr3寄存器属于进程上下文数据,cr3寄存器存放的是页表的起始地址。