文章目录
目录
前言
进程切换
为什么函数返回值会被外部拿到?
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修饰的局部变量编译的时候已经被编译到全局数据区,只是作用域只在该函数内部,而生命周期是全局的
3. 再来看命令行参数环境变量所在区域
可以看到地址是递增的,且大于堆的地址,所以命令行参数和环境变量在堆的上层空间
在学完进程地址空间后,我们再来看这里的环境变量,可以理解为什么环境变量具有全局属性,因为命令行参数和环境变量也在虚拟地址空间上,在页表中也有对应的映射关系,所以在创建子进程时,子进程以父进程为模板,拷贝了父进程的虚拟地址空间和页表,此时即使我们不向子进程的main函数传入环境变量表,子进程也能获取环境变量
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寄存器存放的是页表的起始地址。