Linux知识点 – 进程概念(二)
文章目录
- Linux知识点 -- 进程概念(二)
- 一、进程优先级
- 1.概念
- 2.进程中的优先级信息
- 3.更改进程优先级
- 4.进程切换
- 二、环境变量
- 1.概念
- 2.常见的环境变量
- 3.环境变量相关命令
- 4.通过代码获取环境变量
- 5.环境变量的全局属性
- 6.定义普通变量
- 7.命令行参数
- 三、进程地址空间
- 1.验证进程地址空间
- 2.计算机访问地址的历史
- 3.对进程地址空间的理解
- 4.进程地址空间的扩展内容
一、进程优先级
1.概念
为什么要有优先级:就是因为CPU资源有限,进程太多,需要通过某种方式竞争资源;
- Linux具体优先级做法
优先级 = 老的优先级 + nice值
2.进程中的优先级信息
使用ps -al指令可查看进程的优先级信息;
- PRI是进程的原优先级,此值越小,进程的优先级别越高;
- NI是nice值,表示进程可被执行的优先级修正指数,取值范围是-20至19,一共40个级别;
- PRI(NEW) = PRI(OLD) + NI
3.更改进程优先级
更改进程的优先级就是更改nice值,在top命令中可以更改进程的优先级:
-
进入top后按"r" -> 输入进程PID -> 输入nice值
如果系统不允许将进程优先级调高,可以使用sudo top命令;
-
注:每次设置优先级都要从进程最开始的优先级开始设置,老的优先级基本都是80,也就是PRI(old)都是固定的;
-
每个进程在运行的时候都有时间片的概念,时间片一到,就让出cpu资源;
操作系统还支持抢占,更高优先级的进程会抢占低优先级的进程的cpu资源,还可以出让cpu资源
4.进程切换
- 如果进程A正在被运行,CPU内的寄存器里面,一定保存的是A的临时数据,这些临时数据叫做进程A的上下文;
- CPU的寄存器只有一份,但是上下文可以有多份,分别对应不同的进程;
- 当进程A暂时被切换下来的时候,需要进程A顺便带走自己的上下文,带走暂时保存是为了下次回来的时候能够恢复上次的运行状态,继续按照之前的逻辑向前运行,就如同没有中断过一样;
二、环境变量
1.概念
-
环境变量:一般是指在操作系统中用来指定操作系统运行环境的一些参数,通常在系统中具有全局特性;
-
系统中的bash命令也是存在文件中的,为什么执行bash命令时不用带路径,而执行我们自己的可执行程序时需要带路径:
这是因为bash命令所在的路径在环境变量路径的搜索当中,而自己的程序不在; -
执行自己的程序不带路径的方法:
(1)将自己的程序拷贝到系统命令文件中(不推荐);
(2)将程序的路径复制到环境变量维护的路径中;
加入环境变量后,自己的程序就不用带路径了;
2.常见的环境变量
-
PATH:指定命令的搜索路径;
-
HOME:指定用户的祝工作目录(即用户登陆到Linux系统中时,默认的目录);
-
SEHLL:当前shell,它的值通常是/bin/bash;
-
查看环境变量的方法:
echo $NAME //NAME:你的环境变量名称
3.环境变量相关命令
- echo:显示某个环境变量值;
echo $NAME //NAME:你的环境变量名称
- export:设置一个新的环境变量;
set PATH=$PATH:/home/lmx/linux/lesson0729_process/myproc //冒号后跟想要设置的路径
-
env:显示所有环境变量;
-
unset:清除环境变量;
-
set:显示本地定义的shell变量和环境变量;
4.通过代码获取环境变量
- 方法一:通过命令行第三个参数获取
main函数可以有三个参数:
其中env是环境变量参数,是char*类型的指针数组,是每个进程在启动时,启动该进程的进程传递给main函数的环境变量信息;
- 方法二:通过第三方变量environ获取
- 方法三:调用getenv函数(常用)
5.环境变量的全局属性
- 子进程的环境变量是继承父进程的
export:向当前bash中导出环境变量
这个环境变量属于bash的上下文;
通过程序拿到"class_105"的环境变量:
由于该进程的父进程是bash,环境变量也是继承于bash,因此也可以拿到class_105的环境变量;
因此子进程的环境变量都是从父进程继承来的,环境变量具有全局属性;
6.定义普通变量
- 定义bash中的普通局部变量
我们在命令行将xo定义为12358,可以通过echo打印出来,但是无法通过env查到,因为xo并没有定义为环境变量,而只是bash中的普通局部变量;
也可以通过set打印出来;
这里的cnt也是局部变量;
7.命令行参数
main函数的前两个参数argc和argv是系统给程序传的命令行参数,argc是参数个数,argv是参数列表;
可以通过这两个参数得到命令后面的参数个数和类型;
- 命令行参数的应用
命令行参数最大的意义在于可以让同一个程序通过选项的方式选择使用同一个程序的不同子功能,-a -l这些命令都是通过命令行参数实现的;
当我们在命令行做命令调用时,命令行参数是由父进程bash先拿到,再给子进程;
三、进程地址空间
1.验证进程地址空间
#include<stdio.h>
#include <unistd.h>
int g_val = 10;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 0;
while(1)
{
printf("I am child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
cnt++;
if(cnt == 5)
{
g_val = 200;
printf("child change g_val 100 -> 200 success\n");
}
}
}
else
{
//father
while(1)
{
printf("I am father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
上面这段代码在Linux系统下运行结果为:
- 从结果中我们可以看出:在父子进程中,g_val的值和地址一开始都是相等的,在子进程改变了 g_val的值之后,父子进程中g_val的值不相等了,但是他们的地址还是一样的;
- 父子进程即便定义全局变量,依然不会是同一个变量,且父子进程的变量地址一样,但是值不一样;
- 这里的地址不是物理内存的地址,几乎所有的语言,如果有地址的概念,几乎都不是物理地址,而是虚拟地址;
上图是之前学过的程序的空间分布图,下面用代码来验证一下:
#include <stdio.h>
#include <stdlib.h>
int g_unval;//未初始化全局变量
int g_val = 100;//已初始化全局变量
int main(int argc, char* argv[], char* env[])
{
printf("code addr: %p\n", main);//打印main函数的地址 - 代码区
printf("init global addr: %p\n", &g_val);//已初始化数据区
printf("uninit global addr: %p\n", &g_unval);//未初始化数据区
char* heap_mem = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem);//堆区地址
printf("stack addr: %p\n", &heap_mem);//栈区地址
int i = 0;
for(i = 0; i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
该代码在Linux系统下的运行结果为:
可以看出,验证结果满足进程地址空间的分布;
-
注:
堆区向高地址增长,栈区向低地址增长,堆栈相对而生; -
static修饰变量,变量被开辟到了全局数据区,本质是将局部变量转化为全局变量;
-
字符常量
字符常量和代码是在同一个区的,因为正文代码区上面有一个小分区是字符常量区,属于正文代码区,都是只读的; -
在32位系统下,一个进程的地址空间,取值范围是0x00000000 ~ 0xFFFFFFFF,共4GB内存,其中[0, 3GB]是用户空间,[3GB, 4GB]是内核空间;
-
以上结论,默认只在Linux系统下有效;
2.计算机访问地址的历史
-
计算机刚开始是直接使用物理地址来进行内存管理的,因为内存本身是能够随时被读写的,但是直接对物理内存进行操作,如果遇到了野指针,可能会导致非法访问,进而导致内存中的代码或数据被改写,因此直接访问物理内存是不安全的;
-
现代计算机的内存管理是使用虚拟地址的方式,具体形式为:进程直接访问的的地址空间是一段虚拟地址空间,所有的进程的地址都是0x00000000 ~ 0xFFFFFFFF,通过虚拟地址来访问物理地址,中间需要一个映射关系,通过这个映射关系就能将虚拟地址转换为相应的物理地址,进而能够访问物理内存;对于野指针,映射关系对于非法地址是禁止映射的,很好的解决了野指针非法访问物理内存的问题;
3.对进程地址空间的理解
(1)进程地址空间实际上是一种内核数据结构
- 在这种数据结构中完成对进程内各个区域的划分,所谓的区域划分,本质是在一个范围里定义出start和end;
- 进程地址空间在Linux系统中的结构名为mm_struct,每个进程的PCB里面都有一个指针指向自己的mm_struct;
- mm_struct里面包含了对进程的各个区域的划分,而这些区域的起始地址都是虚拟地址,存储虚拟地址和物理地址之间的映射关系的结构叫做页表;
- 地址空间和页表是每个进程都私有一份的,只要保证每一个进程的页表,映射的是物理内存的不同区域,就能做到进程之间不会互相干扰,保证进程的独立性;
(2)父子进程的创建
- 子进程的创建以父进程为模板,各种变量的创建以及地址的划分都是按照模板来,所以两者的虚拟地址是一样的,当父进程或子进程做修改时,就会发生写时拷贝;
- 当我们修改子进程数据时,操作系统会将需要修改的、被父进程也共享的区域为子进程在内存中拷贝一份,然后再修改子进程虚拟地址到物理地址之间的映射关系,因此子进程修改只是修改自己的地址空间对应的内存;
(3)fork函数执行后为什么会有两个返回值
- 因为fork函数内部实现在return之前就已经创建好了子进程,在return是就是父子进程一块运行了,return的本质就是对id进行写入,return语句被父子进程都执行了,发生了写时拷贝,所以父子进程各自其实在物理内存中已经有了属于自己的变量空间,也就是各自在内存中都管理着一个id变量,只不过在用户层用同一个变量(虚拟地址)来标识了;
4.进程地址空间的扩展内容
(1)当我们的程序编译完、形成可执行程序,但是还没有被加载到内存中时,我们的程序内部有地址吗?
- 是有地址的,可执行程序在编译的时候,内部已经有地址了;
- 地址空间不仅是操作系统内部要遵守的,其实编译器也是要遵守的;即编译器在编译代码的时候,就已经给我们形成了进程的各个区域,代码区、数据区…,并且采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,所以程序在编译的时候,每一个字段在已经具有了一个地址;
- 程序内部的地址,依旧用的是编译器编译好的虚拟地址;当程序加载到内存的时候,每行代码、每个变量便有了一个物理地址,进程地址空间就可以知道每个数据段的大小了;
- mm_struct存储的是每个数据段的起始地址和终止地址,存的是虚拟地址,放在页表左侧,页表右侧是其对应的物理地址,CPU通过页表可以读到指令;
- CPU读到的指令内部也有地址,这个地址是虚拟地址,CPU拿到虚拟地址后,再通过页表找到物理地址进行操作;
例如CPU读到了func函数的跳转地址,这个地址是虚拟地址,CPU通过页表找到该虚拟地址对应的物理地址,然后读取func函数的代码;
(2)创建进程时,一定是操作系统先为该进程创建PCB,然后为该进程创建对应的地址空间mm_struct,并在PCB内用指针指向对应的地址空间对象,地址空间内部可以帮我们找到页表和操作系统对应的逻辑,帮我们自动进行虚拟地址到物理地址之间的转化;
(3)为什么要有进程地址空间?
-
凡是非法的访问或者映射,OS都会识别到,并且终止该进程,所有的进程崩溃,就是进程退出;虚拟地址有效的防止了用户对内存的非法访问,有效的保护了物理内存;
因为地址空间和页表是OS创建并维护的,意味着想要使用地址空间和页表进行映射,也一定在OS的监管之下来进行访问,这样便保护了物理内存中所有的合法数据,包括各个进程,以及内核的相关有效数据;
如果一个进程去修改常量字符串,那就是非法访问代码区,OS会杀掉该进程;
-
因为有地址空间的存在,有页表的映射的存在,在物理内存中,我们可以对未来的数据进行任意位置的加载,物理内存的分配和进程的管理可以做到没有关系;内存管理模块和进程管理模块就完成了解耦合;
我们在语言层面上申请空间(malloc、new),本质是在虚拟地址空间上申请,计算机会使用延迟分配的策略来提高整机的效率:上层申请空间是在地址空间上申请的,物理内存不会分配空间,在用户真正对物理地址空间进行访问的时候,才执行相关的管理算法,进行申请内存、构建页表映射关系等,然后再让用户进行内存的访问;这一步是由操作系统自动完成的,用户,包括进程都是完全没有感知的; -
在物理内存中理论上可以在任意位置加载,但是并不意味着物理内存中的数据和代码就是乱序的,因为有页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,在进程的视角,所有的内存分布都是有序的;
地址空间 + 页表可以将内存分布有序化 ,同样也可以实现进程的独立性:因为有地址空间的存在,每一个进程都认为自己拥有4GB空间(32位),并且各个区域都是有序的,进而可以通过页表映射到不同区域,来实现进程的独立性;
每一个进程不知道,也不需要知道其他进程的存在; -
进程 = task_struct + mm_struct + 页表 + 分配的内存
(4)进程的挂起
- 加载的本质就是创建进程,那么是不是非得把进程所需的所有代码和数据立马加载到内存中,并创建内核数据结构,建立映射关系?
不是的,在最极端的情况下,甚至只有内核数据结构被创建出来了,这就叫做进程的新建状态;理论上,OS可以实现对程序的分批加载和换出,甚至,某个进程短时间内不会再被执行了,比如进入了阻塞状态,那么OS就会将该进程的数据和代码换出,清理内存空间,等到该进程需要执行的时候,再将数据和代码换入,这个状态就叫做挂起;