目录
- 进程优先级
-
- 基本概念
- 查看系统进程
- [PRI and NI](#PRI and NI)
- [PRI vs NI](#PRI vs NI)
- 修改进程优先级的命令
- renice修改优先级进程
- 其他概念
- 环境变量
- 环境变量特征
-
- 命令行参数
- [main函数中的俩个参数 argc argv](#main函数中的俩个参数 argc argv)
- main函数的第三个参数env------全局性验证
- 环境变量的组织方式
- 内建命令
- 程序地址空间回顾
- fork变量问题
- 进程地址空间
进程优先级
系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
基本概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程
在linux或者unix系统中,使用ps al
查看命令则会类似输出以下几个内容。
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
UID
其中,在Linux中,系统是通过编号来识别文件,用户等信息的,如上图,1001即是用户的编号。
PRI and NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别
PRI vs NI
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
修改进程优先级的命令
用top命令更改已存在进程的nice来更改进程优先级。
- top
- 进入top后按"r"-->输入进程PID-->输入nice值。
当前myproc进程的优先级
查看myproc进程指令
cpp
ps al|head -1&&ps al|grep myproc|grep -v grep
使用top命令进入查看进程,再输入r,然后输入要修改进程的PID
最后输入要调整的nice值
注意 :我们这里输入的值是100。
最后查看修改后的进程优先级。
nice值确实修改了,对应的PRI值也对应变化了,但nice值却不是我们输入的100,所以可以看出,nice值的修改确实是有范围要求的。其范围为:[20, -19]。
- 而且每次调整NI值,都会从原本的PRI值开始加减NI值
- 如:当前的PRI为30,现在将NI调整为19,那么新的PRI=(旧PRI值)+NI值=20+19=39;而不是30+19=49。
- 旧PRI为最初的PRI值,当前版本下为20。
- 由于版本不同,大家的PRI值有可能不一样,但是NI的调整范围是一样的。
- NI有调整范围是因为Linux还是奉行不相信用户的原则,不能让用户过渡调整优先级,因为当操作系统运行起来后,还有许多系统内部的进程,如果我们可以随意将我们自己创建出来的进程的优先级设置先于系统的进程,我们的进程可能就会霸占原来属于系统进程的资源,这时候就容易会造成进程饥饿问题
renice修改优先级进程
我们也可以使用renice对进程优先级调整。
用法:renice nice值 进程PID
注意 :调整进程优先级时,将NI值调为负值需要使用sudo命令提升权限
其他概念
-
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
-
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
-
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
-
并发: 多个进程在一个CPU下采用进程切换的方式 ,在一段时间之内,让多个进程都得以推进,称之为并发 。
- 这里对基于时间片轮转的进程切换简要描述。
当需要运行多个进程时,如一边打游戏一边听歌;CPU在一个时间点只能运行一个进程,所以基于时间片轮换切换进程的方法,当要将一个进程切换为另一个进程时,需要先将当前进程在CPU的上下文信息(也就是需要知道当前进程运行到哪了,以便下次再切换到该进程时得以继续从上次运行的位置开始运行;这也是为什么觉得是多个进程一起运行的假象)保存到PCB对象中,以便下次切换到该进程时将上下文恢复到CPU中。
环境变量
基本概念
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
查看环境变量方法
cpp
echo $NAME //NAME:你的环境变量名称
加上$才能输出变量的内容,否则就是它的名字。
常见环境变量
PATH
PATH : 指定命令的搜索路径
我们曾在指令介绍一文中讲过指令的本质就是一个个的可执行程序,只不过其在对应的系统路径中,所以不需要.来执行。
- 查看PATH环境变量:发现路径间用:分隔,以上就是我们输入指令后,PATH环境变量会帮助我们找到其可执行程序所在路径。
所以我们如果想让我们自己的可执行程序按照指令的方法执行(不用.来执行)有以下两种方法:
- 将我们的可执行程序加到PATH其中一个路径中;
如":/usr/bin
- 将我们的可执行程序的所处路径加到PATH中。
修改PATH的方法:PATH=$PATH:添加的路径;该修改方法是覆盖式的,所以把原本的路径也加上,不然本来的系统指令就用不了了。
如果不小心改错了也不用担心,重新登陆即可。
HOME
HOME: 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
不同用户的家目录是不同的,该信息保存在HOME这个环境变量中。
普通用户:
超级用户:
SHELL
SHELL : 当前Shell,它的值通常是/bin/bash。
我们平时所敲的指令都是通过命令行解释器SHELL(可理解为媒婆)向操作系统发送指令的,但在Linux当中有许多种命令行解释器(例如bash、sh(王姓媒婆,陈姓媒婆)),我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器的种类。
查看环境变量
使用env
查看全部环境变量
可以看到有众多的环境变量,包括刚才介绍的PATH,HOME,SHELL。
除此之外还有
环境变量名 | 内容 |
---|---|
HOSTNAME | 主机名 |
HISTSIZE | 记录历史命令的条数 |
SSH_TTY | 当前终端文件 |
USER | 当前用户 |
PWD | 当前所处路径 |
SSH_TTY :向指定终端文件输出
Linux下一切皆文件,SSH_TTY就是对应终端文件
PWD与OLDPWD
这两个环境变量记录当前路径和上一路径;有了PWD,我们的指令pwd就是依赖它实现的,而cd -
这一返回上一路径指令就是依靠OLDPWD完成的。
系统调用接口------getenv
获取指定的环境变量,并将该环境变量返回。
USER
USER该环境变量记录用户是谁。
以下测试:让不同用户跑同一份代码,看看获取的USER是谁。
cpp
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("who am i:%s\n",getenv("USER"));
return 0;
}
可以看到不同用户跑一份代码其结果不一样。这就是环境变量USER起的作用。
环境变量相关的命令
- echo: 显示某个环境变量值
前面已经进行过介绍了
-
export: 设置一个新的环境变量
name=value 这种形式只能定义本地变量,如若要新增一个环境变量需要在前面加export ;myproc中使用getenv查找环境变量my_value。
-
env: 显示所有环境变量
-
unset: 清除环境变量
-
set: 显示本地定义的shell变量和环境变量
一般搭配管道|和grep使用
环境变量特征
有了以上例子,我们再回顾,什么是环境变量这一问题:
环境变量是系统提供的一组name=value形式的变量,不同的用户有对应的环境变量,通常具有全局性。
- 全局性可以理解为可以被子进程继承
除环境变量外还可以定义本地变量。
与本次登录有关的变量,只在本次登录内有效。
可以看到本地变量确实不能被getenv函数找到,仅在当前命令行有效,不能被子进程继承,没有全局属性。
那么怎么证明全局变量具有全局性------即可以被子进程继承呢?
命令行参数
在验证环境变量具有全局性前,我们再了解一个知识点------命令行参数。
main函数中的俩个参数 argc argv
我们平时写的代码都是从main函数开始对吧
cpp
int mian()
{
//code
return 0;
}
但你知道main函数是可以带参的吗?如以下代码:
cpp
int main(int argc, char* argv[])
{
int i=0;
for(;i<argc;i++)
{
printf("argv[i]->%s\n",argv[i]);
}
return 0;
}
- argc: 记录了argv中的数据个数
- argv: 保存了命令行参数
我们都知道,是函数就可以传参,main函数也不例外,而我们在命令行输入的指令本质上就是在向main函数传参,而char*argv[]本质是一个字符指针数组,叫做命令行参数表;该表就是将在命令行输入的指令当作字符一样存放起来;argc:用来记录了argv中的数据个数。
- 命令行参数表最后一位为NULL
但这就完了吗?这也和环境变量扯不上关系啊!
main函数的第三个参数env------全局性验证
实际上,main函数除了argc和argv两个参数,还有第三个参数,char*env[],这就是环境变量表,它和命令行参数表是一样的。
代码如下:
cpp
int main(int argc, char* argv[],char* env[])
{
int i=0;
for(;env[i];i++)
{
printf("env[%d]->%s\n",i,env[i]);
}
return 0;
}
打印环境变量表发现:这不就是我们使用env指令查看的环境变量嘛!!!
于是我们再次阐述几个概念:
- 一个程序加载到内存后,最终变为一个进程;
- 我们所运行的每一个进程都是bash的子进程;
所以,我们运行的进程都是bash的子进程,bash本身启动的时候,会从操作系统的配置文件中读取环境变量,当执行一个指令时,该指令就会继承从父进程中交给我的环境变量。 也就是说环境变量具有全局性!!!
实际上每一份代码运行时都需要将上述的命令行参数表,环境变量表传给main函数。
在windows中同样也存在在这种情况。
除了以命令行参数的方式获取环境变量,还可以通过第三方变量environ获取。
cpp
#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;
}
- libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以'\0'结尾的环境字符串。
内建命令
上面已经证明了环境变量具有全局性,可以被子进程继承;我们输入的指令都是bash的子进程,所以可以继承父进程传过来的环境变量。
但是本地变量是不具有全局性的,但是我们观察一下下面的现象。
不是说本地变量不具有全局性,指令又是bash的子进程吗?为什么echo还能打印本地变量my_value的值呢?
实际上,指令都是bash的子进程这句话并不是完全正确的,指令实际上可以分为两种。
- 常规指令
通过创建子进程完成
- 内建命令
bash不创建子进程,而是由自己亲自执行,类似bash调用自己写的或者系统提供的函数实现。类似echo,cd这样的指令就越是内建命令,不通过创建子进程完成。
利用系统调用接口------chdir自己写一个类似cd的内建命令
chdir:将调用进程的当前工作目录更改为path中指定的目录
模拟cd指令的实现:
利用命令行参数和chdir这一系统调用完成切换路径的工作。
cpp
int main(int argc, char* argv[], char* env[])
{
if (argc == 2)
{
printf("change begin\n");
sleep(20);
chdir(argv[1]);
printf("change done\n");
sleep(10);
printf("exit\n");
}
else
{
printf("err\n");
}
return 0;
}
我们用myproc这个进程当作bash,为更好观察更换当前路径这一现象,我们在更换路径前后都休眠一段时间,此时在另一个窗口监视myproc这个进程,在根目录下的系统文件/proc中查看该进程相关信息,cwd为该进程的工作路径,也就是我们的监视目标。当myproc显示切换完成时,可以看到myproc这个进程的cwd确实切换到我们输入的要切换到的路径了。
所以cd指令完全可以由bash自己调用系统函数或者自己写的函数实现。不需要创建子进程。
程序地址空间回顾
以C/C++视角的内存区域划分。
再细致划分一下就是这样的
通过以下代码可以验证以一下
cpp
int g_val_1;
int g_val_2 = 100;
int main()
{
printf("code addr: %p\n", main);
const char *str = "hello CSDN";
printf("read only string addr: %p\n", str);
printf("init global value addr: %p\n", &g_val_2);
printf("uninit global value addr: %p\n", &g_val_1);
char *mem = (char*)malloc(100);
char *mem1 = (char*)malloc(100);
char *mem2 = (char*)malloc(100);
printf("heap addr: %p\n", mem);
printf("heap addr: %p\n", mem1);
printf("heap addr: %p\n", mem2);
printf("stack addr: %p\n", &str);
printf("stack addr: %p\n", &mem);
static int a = 0;
int b;
int c;
printf("a = stack addr: %p\n", &a);
printf("stack addr: %p\n", &b);
printf("stack addr: %p\n", &c);
//char *str = "hello bit";
//*str = 'H';
return 0;
}
运行发现确实如此
- 全局数据中,未初始化的数据区域在初始化数据之上。
fork变量问题
在上一篇介绍fork时我们还遗留了一个问题:
现在我们就可以对这个问题进行解释了,用以下代码更直观看出这个问题
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int g_val = 100;
if (fork() > 0)
{
//parent
while (1)
{
printf("I am father;g_val:%d g_val_addr:%p\n", g_val, &g_val);
sleep(1);
}
}
else
{
//child
int num = 3;
while (num--)
{
if (num == 1)
{
printf("#############################\n");
g_val = 200;
printf("child changed g_val to %d\n", g_val);
printf("#############################\n");
}
printf("I am child:g_val:%d ,g_val_addr:%p\n", g_val, &g_val);
}
}
}
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量。
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
OS必须负责将 虚拟地址 转化成 物理地址!!!
进程地址空间
从上文得出可知,之前说'程序的地址空间'是不准确的,准确的应该说成进程地址空间,那该如何理解呢?看图
在学习C/C++时我们都以为那张内存分布图是内存,实际并不是,它实际上也是一个内核数据结构对象,叫mm_struct,也称进程地址空间 。
进程地址空间:
将各类数据按照其种类有序存放,存放的是虚拟地址,并不是存放数据代码的真实地址。
页表:
是操作系统中用于存储虚拟内存到物理内存映射的数据结构;负责将虚拟地址转化为真实的物理地址,进而找到数据代码。
之前介绍进程时,我们将进程定义为:内核数据结构对象(task_struct)+数据代码,这时提到的内核数据结构对象是不完整的 ,现在来看,这里的内核数据对象就不止task_struct一个内核数据结构对象了 。而是task_struct + mm_struct (进程地址空间)+ 页表。
fork变量问题原因
所以现在以操作系统的视角看待进程是这样的:
由task_struct,mm_struct,页表组成的内核数据结构 + 数据代码;task_struct有指向mm_struct的指针,mm_struct包含了指向进程页表的指针,该指针为虚拟地址,并不是真实的地址,而页表则是将这个虚拟地址转化为真实的存放数据代码的物理地址,以便CPU可以访问物理内存。
这种设计使得Linux内核可以独立地管理进程的状态和内存资源,从而支持多任务和多用户操作系统的核心功能
注意:每一个进程都会有自己独立的内核数据结构对象(即每个进程都会有一套task_struct,mm_struct,页表)。
所以这时我们就能解释清楚为什么同一个地址会有两个值的问题了:
该地址是虚拟地址,并不是真实存放数据的物理内存的地址;父子进程都会有自己一套独立的内核数据结构对象,但是子进程的内核数据结构对象是拷贝父进程的,所以他们的内容是一样的,所以父子进程会有相同进程地址空间(mm_struct),所以地址是一样的,此时父子进程所指向的数据代码也还是一样的;但是当子进程对父进程的某一个数据进行修改时,就会引发写时拷贝,开辟一块新的空间,这块空间属于子进程;再通过页表将虚拟地址转化,于是就看到了同一个地址,不同值的情况。
进程地址空间本质
所谓进程地址空间,本质是一个描述进程可视范围的大小,这个可视范围指的地址总线的大小,即在C/C++内存分布中所显示的4GB大小,也就是可映射到的内存大小。
这个4GB是怎么来的呢?
读写数据时,CPU和内存之间有⼤量的数据交互的,所以,两者必须也⽤线连起来,这些线一共有三组,我们现在只关注地址总线 。
地址总线:
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含义,依次类推。32根地址线,就能表⽰2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。
而2^32*byte=4GB。
进程地址空间本质
在C/C++层面我们将程序地址空间当作存放数据的内存实际上是不对的,准确的应该说成进程地址空间。
在操作系统的角度来看:进程地址空间本质上是内存中的一种内核数据结构,在Linux当中进程地址空间具体由结构体mm_struct实现。面对大量且不同种类的数据,mm_struct如何进行管理呢?这就得想起管理的六字真言了------先描述,再管理。
进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xffffffff,尺子按照刻度被划分为各个区域,例如代码区、堆区、栈区等。而在结构体mm_struct当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,如下图所示:
为什么要有进程地址空间
为什么要有进程地址空间和页表这样的结构呢?让数据直接映射到真实的物理地址上不行吗?
一:让所有进程以统一的视角看待内存
计算机硬件资源是有限的,内存更是如此;但进程却可以有很多,当不同进程向内存申请空间时,进程地址空间可以将这些数据按他们的类型在对应内存类型区域(栈,堆等)生成一个虚拟内存地址,再将该数据随意保存到真实的物理内存中,在页表中将虚拟地址与真实地址建立映射关系,日后通过页表就能进行数据的查找读写。
这样一来,方便了对数据的管理,而且也不需要知道内存中哪里还有空间,做到让所有进程以统一的视角看待内存。
二:增加转化过程,可以对寻址请求做审查,可以保护数据
我们一直说在常量区的数据只能读不能写,这都是从语言的角度上理解的。那么请问:该数据是如何写入的?
cpp
char* str = "hello CSDN";//不合法的,有的编译器会直接报错
*str = "HELLO csdn";
从系统角度理解:
实际上,物理内存是不会进行请求审查的,物理内存上的的数据是任意读写的;但页表上还有一个标识位来标识这个数据是可读写还是可写,可读的;当想要访问数据时,页表的标识位会先对你的读写请求进行审查,如果没有对应权限,会直接拒绝该请求,达到保护数据的请求。
三:将进程管理,内存管理进行解耦合,确保内存管理模块不会影响进程管理模块
操作系统不仅仅只有进程管理模块,还有内存管理模块;一款好的软件要求做到低耦合的效果,不能因为一个模块出了毛病导致整个软件崩溃。进程地址空间和页表能够将进程管理模块和内存管理模块很好的分离,做到进程管理模块不需要知道一个程序是怎么加载到内存的,它只需要做好自己对进程的管理即可,内存管理模块也是如此。
再补充一点:
当进程在CPU运行时,CPU的cr3寄存器保存的是当前进程对应页表的地址;并且页表的地址属于task_struct的上下文信息。进行进程切换时仍是切换task_struct就可以了,因为进程地址空间也是由task_struct指向的,页表地址也会保存在task_struct中。