进程概念完了吗?不,还没有!今天博主就来和大家接着学习后半部分的概念~
今天博主主要讲优先级、环境变量、进程地址空间。尤其是进程地址空间的知识,这是我们学习操作系统由量变到质变的一个重要的小转折!
目录
进程优先级
基本概念
1、cpu资源分配的先后顺序,就是指进程的优先权(priority)。
2、优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。3、还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
为什么要有优先级?
因为CPU资源太少,而需要消耗资源的进程是有很多个的。优先级本质上就是分配资源的一种方式。
查看系统进程
在linux或者unix系统中,用ps –l或ps -al命令来查看进程优先级:
[cyq@VM-0-7-centos test]$ ps -al
我们来观察一下:
我们发现会出现几个重要的名词信息:
UID: 代表执行者的身份
这个就好比现实生活中一个人的姓名和他的身份证号的关系,通常我们并不会叫一个人去念他的身份证号,而是念他的名字。比如张三,这个就好比一个文件名;该文件名的UID,就相当于张三的"身份证号",是唯一的。
PID: 代表这个进程的代号
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI:代表这个进程可被执行的优先级,其值越小越早被执行默认情况下这个值就是80。
NI:代表这个进程的nice值我们修改PRI实际上是通过修改nice值来实现的。
PRI and NI
1、PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是进程被CPU执行的先后顺序,此值越小进程的优先级别越高。
2、那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。这个PRI(old)实际上就是80
3、当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
4、调整进程优先级,在Linux下,就是调整进程nice值。nice其取值范围是-20至19,一共40个级别。
PRI vs NI
1、需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
2、可以理解nice值是进程优先级的修正数据。
有了上面概念理解,我们有几个问题:
为什么要有NI值,我们,要想修改PRI直接去修改PRI不就好了吗?为啥还要通过修改NI值来间接修改优先级呢?
实际上,我们要想修改优先级,用直接修改PRI的方式也是可以的,但是有了nice值之后,我们可以更加直观的看出进程优先级PRI修改前后值变化幅度(nice的大小正负就可以反应出来)。
nice值为什么是一个相对比较小的范围呢?
我们知道,nice的设定范围在-20~19。假设我们设置范围是比较大的话,会出现什么后果?
举一个现实的栗子:假设张三去食堂打饭,我们首先要排队,排队就是在确认优先级。如果张三后面的人都块头大,很强壮,后面的人不断插到张三前面,即使队尾的人也插队到张三前面,那么张三长时间就会打不到饭,那么最后会导致一个严重的问题---"饥饿问题"。
站在OS角度:假设每一个进程都可以跨度很大,如果nice设置不当,那么很可能就会有很多进程抢占资源,导致后面的进程长时间不能享受到CPU资源,那么进程就会出现---"饥饿问题"。
总结:
优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则就会出现严重的进程"饥饿问题"。
如何保证?
调度器:通过调度器,限制了随意修改nice值大小的行为,较为均衡的让每个进程享受到CPU资源。
用top命令更改已存在进程的nice
1、top(任务管理器)
2、进入top后按“r”–>输入进程PID–>输入nice值
我们先写一个死循环的C程序(这样可以得到它的pid来修改nice值):
int main()
{
while(1);
return 0;
}
我们运行程序后,先获取对应进程的pid:
接着我们输入指令top,打开任务管理器:
然后我们再按r(不要按回车):
这时候出现了一个让我们输入进程pid的命令行,我们把对应pid输入进去:
输入对应进程pid按下回车键之后,会出现让我们输入nice值的命令行:
比如我们输入10按回车,然后ctrl+c终止退出top,我们再来查看一下修改后进程的优先级。
[cyq@VM-0-7-centos test]$ ps -al
我们这时候发现,该22210进程PRI就被修改成了90。PRI(new) = 80 + 10 = 90。
注意
1、我们如果频繁修改nice值的话,OS会阻止的,不过我们可以通过sudo top提升权限的方式来修改。
2、如果我们对一个进程在原来PRI为90,NI为10的进程进行修改。比如我们对nice设置成5,这时候该进程PRI为80,NI为5,。意思就是,每次设置nice值时,并不会以当前数为标准,而是以PRI默认是80,NI默认是0为标准进行变化。
3、 假设我们对nice设置值超过了规定范围,那么nice就会取对应的临界值。比如nice输入为50,那么实际被设置成19;nice输入-30,nice实际被设置成-20。
其他补充概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
环境变量
基本概念
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
常见的环境变量
我们先思考一个问题:为什么我们在运行自己的可执行程序时要加上./来指定路径,而执行系统的命令不需要加./呢?
实际上这和环境变量(PATH)有关。
PATH
指定命令的搜索路径。
我们来查看一下,系统中默认的搜索路径:
[cyq@VM-0-7-centos test]$ echo $PATH //注意,这里一定要加$
/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/cyq/.local/bin:/home/cyq/bin
这里的搜索路径,是以冒号 : 为每个路径的分隔符,我们输入指令时,系统默认会到PATH环境变量中搜索,从第一个路径开始,直到在某个路径找到位置。
因此,我们就知道了为什么我们运行自己的可执行程序要加./。
因为环境变量(PATH)中没有默认对应的路径可以找到这个可执行程序!
那我们怎么去把我们自己的可执行程序加到环境变量中?
方法一:把自己的可执行程序路径拷贝到系统文件中
这种方法不太推荐,因为这么做会污染人家的命令池,在这里博主也就不做了~
方法二:使用export设置一个新的环境变量,仅在本次登录有效。
步骤一:我们先使用pwd指令把当前工作目录的路径显示出来
步骤二:我们使用export命令把当前工作目录路径拷贝进去
export PATH=$PATH:hello程序所在路径
[cyq@VM-0-7-centos test]$ export PATH=$PATH:/home/cyq/practice/test
这时候我们可以查看我们是否导入成功了:
这时候我们就可以不加./来运行一下该目录下的可执行文件:
我们发现这次就设置成功了。
注意:
注意步骤二:export PATH=$PATH:hello程序所在路径,其中$一定要加,否则这时候环境变量就乱了,我们举个栗子:
当我们 不加$进行设置时,我们再查看路径就发现,原来的搜索路径不存在了,同时我们输入的很多命令行也不能用了!
我们来看一下:
这时候不要慌,关掉这个终端,重新打开就可以恢复了。
HOME
指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录。不同的用户身份登录是不同的。
我们来举个栗子:
[cyq@VM-0-7-centos test]$ echo $HOME
比如我们换几个用户身份来观察一下:
SHELL
当前Shell,它的值通常是/bin/bash 。
我们来查看一下:
[cyq@VM-0-7-centos test]$ echo $SHELL
和环境变量相关的命令
echo: 显示某个环境变量值
export: 设置一个新的环境变量
env: 显示所有环境变量
unset: 清除环境变量
set: 显示本地定义的shell变量和环境变量
上面的命令博主就不演示了,但是老铁们要下去试试来感受一下哦~
环境变量的组织方式
程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以NULL结尾的环境字符串。
通过代码获取环境变量
有了上面的结构,我们就可以使用代码来获取环境变量了~
命令行第三个参数
我们介绍第三个参数前,也顺便先介绍前两个参数。
int main(int argc, char *argv[], char *env[]){}
argc:输入的命令行参数个数。
argv:指针数组,通过下标找到对应的参数。
![]()
我们用代码来演示一下:
int main(int argc, int* argv[])
{
for(int i = 0; i < argc; i++)
{
printf("argv[%d]-> %s\n", i ,argv[i]);
}
return 0;
}
运行一下:
好了,介绍完前两个参数后,我们继续。
第三个参数:char* env[] ,env是一个指针数组,里面存着环境变量,我们利用它,就可以使用代码打印环境变量。
代码:
int main(int argc, int* argv[], int* env[])
{
for(int i = 0; env[i]; i++) //env为NULL就结束条件
{
printf("%s\n",env[i]);
}
return 0;
}
运行一下(部分截图):
通过第三方变量environ获取
我们在上面的环境变量的组织方式中可以发现environ指向指针数组的第一个指针。我们通过man手册来了解一下:
[cyq@VM-0-7-centos test]$ man environ
我们发现它是一个二级指针,那么二级指针是可以指向一级指针的,那么通过environ就可以找到env[0]了。
代码演示:
int main()
{
extern char** environ;
for(int i = 0; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
运行结果(部分截图):
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
通过系统调用获取或设置环境变量
putenv , 我们后面讲解
getenv
我们通过man手册来查看一下getenv:
const char* name:就是环境变量名
举个栗子:
int main()
{
printf("%s\n",getenv("PATH"));
printf("%s\n",getenv("HOME"));
return 0;
}
运行结果:
我们发现和我们使用echo $环境变量名 的效果是一样的。
环境变量通常是具有全局属性的
环境变量具有全局属性本质:环境变量可以被子进程继承下去。
我们由之前的知识知道,命令行的父进程是bash,同时bash可以说是最上层的父进程了,进程创建是由bash进程开始的,往下不断fork创建子进程,并且我们也学习过,子进程会继承父进程的大部分数据,则父进程的环境变量数据就被子进程继承下去了。间接可以反映出环境变量具有全局属性。
补充知识,先做个小实验:
[cyq@VM-0-7-centos test]$ env_string=wmm
[cyq@VM-0-7-centos test]$ echo $env_string
wmm
我们在本地变量导入一个字符串env_string="wmm"。
我们在环境变量中看能不能直接找到它?
我们发现并没有在环境变量中找到,说明它还在本地变量中,当我们export env_string,把它导入环境变量中再来试试?
[cyq@VM-0-7-centos test]$ export env_string
这时候我们发现就导入环境变量成功了~
我们先写一个不fork的程序,看看它的父进程是谁:
代码测试:
int main()
{
printf("i am process pid: %d ppid: %d\n", getpid(),getppid());
return 0;
}
测试结果:
我们发现是bash,说明一个进程的最终父进程就是bash!这个和多叉树的第一个结点有点像。
假设一个场景,证明环境变量具有全局属性:我们写一个代码,打印环境变量(我们手动导入的)中的一个字符串。对比一下把这个字符串导入到env前后,程序的运行结果。
测试代码:
int main()
{
printf("my_env_string: %s\n", getenv("my_env_string"));
return 0;
}
我们先在本地变量设置一个字符串:
[cyq@VM-0-7-centos test]$ my_env_string="wmmandcyq"
这时候我们发现my_env_string字符串在本地变量中,环境变量中还没有,这时候,我们运行程序,看一下打印my_env_string的情况:
我们发现什么也没有打印出来。
当我们把my_env_string导入环境变量后再来打印:
我们发现我们写的程序就可以打印环境量中的my_env_string了。
总结
1、bash进程在创建时,环境变量是首先被加载进去的,然后它不断fork创建子进程,同时把自己的环境变量传给子进程,子进程依次类推,这样,环境变量就具有了"全局属性"。
2、注意本地变量和环境变量区别。
3、环境变量也是变量,它是OS自己给自己申请的变量。
补充
了解一下即可:
1、系统的环境变量在~/.bash_profile 系统默认执行的环境变量。
[cyq@VM-0-7-centos test]$ vim ~/.bash_profile
2、我们发现上面的环境变量会调用另一个文件,我们来看一下~/.bashrc下的内容
[cyq@VM-0-7-centos test]$ vim ~/.bashrc
3、/etc/bashrc配置文件,获取各种环境变量信息
[cyq@VM-0-7-centos test]$ vim /etc/bashrc
部分截图:
进程地址空间
一个重要现象
在这里博主讲的内容是32位平台的~
我们先来回顾一下曾经学习的空间分布图:
我们先抛出一个问题,我们曾经写的代码定义的变量打印的地址是物理地址吗??
先不多说,来段代码演示一个现象:
int g_val = 100;
int main()
{
pid_t fd = fork();
if(fd == 0)
{
//child
int count = 5;
while(count)
{
printf("i am child, &g_val = %p, g_val = %d\n", &g_val, g_val);
sleep(1);
count--;
if(count == 2)
{
printf("##########child 修改ing##############\n");
g_val = 200;
printf("##########child 修改done#############\n");
}
}
}
else if(fd > 0)
{
//parent
while(1)
{
printf("i am parent, &g_val = %p, g_val = %d\n", &g_val, g_val);
sleep(1);
}
}
return 0;
}
我们看一下运行结果:
我们让父子进程同时打印一个全局变量的值和它的地址,3s后,子进程修改全局变量g_val(我们知道这时候要发生写实拷贝),修改之后,我们惊奇的发现父子进程打印的g_val值是不一样的,但是他们的地址竟然是一样的!
这就说明如果我们打印的地址,还认为是我们曾经以为的物理地址,那是肯定错的!同样的物理地址,是不可能存储两个值的!
那么,我们打印的地址是什么地址的??答案是虚拟地址!
1、变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。但地址值是一样的,说明,该地址绝对不是物理地址!
2、在Linux地址下,这种地址叫做虚拟地址3、我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
什么是虚拟地址?
虚拟地址实际就是进程地址空间,地址空间本质是内核中的一种数据类型struct mm_struct{}
虚拟地址和PCB的关系
struct mm_struct里面描述什么?
注意:
1、每个进程都认为地址空间划分是按照4GB空间划分的!
2、每个进程都认为自己拥有4GB空间!
3、虚拟地址空间是以1字节为单位的,地址从0x00000000~0xffffffff。
进一步理解虚拟地址
虚拟地址(线性地址)是与物理地址区分开来的。
我们知道当我们创建进程时,一定会有PCB被创建,同时还有虚拟地址也会被创建!每一个进程都有自己独立的PCB和虚拟地址!每个进程都认为自己有4GB的资源!可以说虚拟地址也是进程的一部分。
我们运行代码,实际上还是要找到物理内存上的实际代码,可是PCB看向的是虚拟地址,那么CPU调度PCB时,怎么能找到对应的物理地址上的代码和数据呢?答案是:页表映射
代码和数据加载到内存时,OS建立task_truct、mm_struct、页表等。
OS通过页表映射将虚拟地址转换为物理地址,进而可以运行代码和数据。页表实际上是一张哈希表,页表其实是非常复杂的,它还涉及到权限等。
进程地址空间有什么用?
作用一
老铁们会不会这么想,让PCB直接指向物理内存不就行了吗?为什么还要在中间添加虚拟地址、页表,这样不是好麻烦?
我们假设没有中间层场景:如果task_struct可以直接访问物理内存的代码和数据的话,这样中间是没有限制的,task_struct是能随意访问物理内存的。如果进程A的task_struct恶意访问其他进程的代码和数据,是不是就存在安全隐患?并且进程和进程之间就会被相互影响,就不能保证独立性了。
所以有了虚拟地址之后,由有了页表,我们每个段地址通过页表映射时,OS会检查一定的权限,检查该进程做法是否合理。
比如我们在语言层面上学习的常量字符串不能被修改,那到底是为什么呢?在语言层面上,是不可能解释清楚的,在这里我们就知道,常量字符串在常量区,当它通过页表映射时会有权限检查,它很显然只有r(只读)权限,当我们修改时,OS会立马检查出来并阻止修改!
为什么这么设计呢?因为所有只读的数据一般只有一份,操作系统维护一份的成本是最低的!
作用一:通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存以及各个进程的数据安全。
作用二
引入一个场景:我们申请1000字节,我们立马就去使用1000字节吗?
不一定,可能存在暂时不会全部使用甚至暂时不会使用的情况。
站在OS角度:如果空间立马给你,是不是意味着,整个系统会有一部分空间,本来可以立马给别人使用的,现在却被你闲置着?!
所以说,我们申请空间时,OS并不是立马给我们的,而是先在虚拟地址空间中对应的线性空间的参数进行修改(比如,heap_end += 20;相当于已经承诺给你准备空间了),而当我们真正使用空间的时候,OS才给我们申请。注意,给我们申请的空间分两种情况:一是空间本来就充裕,直接开辟给我们的;二是空间不够了,OS使用内存管理算法腾出来的空间给我们使用的。站在用户的角度我们实际上并不关心内存从哪里来~
作用二:将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件上的分离!
作用三
我们知道程序的代码和数据加载到物理内存中的位置是不确定的,那么,如果让CPU直接去找对应进程物理内存上的代码main函数入口,实现起来是很困难的,所以有了虚拟地址后,我们可以规定main函数在代码段一个相对特定的位置,这样CPU就能快速锁定虚拟地址main函数的位置,再通过页表映射立马就能找到物理地址了。
好处:这样程序的代码和数据就可以加载到物理内存的任意位置!!大大的减少了内存管理的负担!
作用三:站在CPU的和应用层的角度,进程统一可以看做统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的。
总结
OS最终这样设计的目的,达到一个目标:每个进程都认为自己是独占系统资源的!!进程是具有独立性的!!
回顾一下
最后我们就能解释最开始的演示现象了,子进程继承了父进程的大部分PCB数据,所以g_val的地址也是一样的,当子进程的g_val值被修改后,在物理内存中发生了写时拷贝!虚拟地址中没有变化,所以最后出现了打印变量的地址一样,但是打印出来变量的值不一样的结果。这个打印的地址就是虚拟地址!
写时拷贝前:
写时拷贝后:
我们在这里只分析用户层的3G空间(这里页表只映射用户层的3G空间),至于内核的那1G在后面多线程的博客会讲~
看到这里,支持博主一下吧~