目录
一、基本概念
书本上的概念:程序的一个执行实例,正在执行的程序等
基于内核的观点:担当分配系统资源(CPU时间,内存)的实体。
我们知道,我们在写代码的时候,你的代码进行编译链接后生成可执行文件,这个文件就在磁盘当中,当我们双击这个文件之后,该文件就被加载到了内存之中,因为只有加载到了内存之中才能被cpu逐语句执行。而加载了内存中的程序,不再是程序而应该是进程。
二、描述进程——PCB
实际上我们在系统当中有很多的进程,我们可以通过ps aux命令来查看:
我们知道操作系统是第一个被加载到内存的,而操作系统就是做的管理工作的,那么操作系统是怎么做到管理的呢?
这里就用到了之前在谈操作系统时候提到的六个字:先描述,再组织。操作系统作为管理者,是不需要与进程直接交互的,当进程到来时,操作系统需要对进程进行描述,那么对进程的管理就变成了对描述的管理。进程的描述信息会被放到一个叫进程描述块之中,官方称之为PCB(process control block)。
操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来:
这样一来,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作。
例如创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。
总的来说,操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作。
1.task_struct——PCB的一种
因为Linux是拿C语言写的,那么这里的task_struct其实是一个结构体。
1)在Linux中描述进程的结构体叫做task_struct。
2)task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
2.task_struct的内容分类
主要内容:
- 标示符:描述本进程的唯一标示符,用来区别其他进程
- 状态: 任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- 1/0状态信息:包括显示的I/0请求,分配给进程的!/0设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
三、查看进程
1.通过系统目录查看
我们除了上面的,通过ps aux命令查看以外,还可以在根目录下找系统文件夹proc。
我们打开文件夹,可以看到一些以数字命名的目录
这些数字就是我们之前所说的PID,在对应的目录中记录了进程的相关信息,比如我们查看1的文件内容:
2.通过ps命令查看
单独使用ps:
ps aux
ps结合grep可以看到更加标准的进程信息:
四、通过系统调用获取进程的PID和PPID
这里我们要用到两个调用函数,分别是getpid()和getppid()来分别获取PID和PPID。
我们可以写个代码来测试一下:
输出:
我们也可以看一看进程的信息是不是和我们调用函数获取的一致
五、通过系统调用创建进程
1.fork函数创建子进程
fork函数是一个系统调用函数,他可以创建一个子进程:
例如:
运行结果如下:
运行结果中的第一行数据是该进程的PID和PPID,第二行是、fork函数创建的PID和PPID,不难发现fork函数创建的进程的PPID就是proc的PID,也就是说proc进程和fork创建的进程是父子关系。
没出现一个进程,操作系统都会为其创建PCB,fork也不例外。
我们知道加载到内存的代码和数据是父进程的,那么fork创建的子进程的代码和数据是哪里来的呢?
我们来写个代码来看看:
运行结果:
实际上,fork函数创建子进程,在fork函数之前的代码要被父进程执行,而fork()之后的代码默认是父子进程都可以执行的。
敲黑板:
1)这里虽然是父子进程代码共享,但是父子进程各自开辟空间(写时拷贝)。
2)这里面可能大家都会有一个疑问,那就是这里父子进程的执行顺序是什么样的,其实这里执行的顺序完全是不确定的,取决于操作系统的调度。
2.使用if来引出问题
我们在之前说了,在fork()函数之后的父子进程共享代码,那么这样一来,我们创建子进程就没有了意义,实际上使用的时候是要使用if来分流的,也就是父子进程去做不同的事情。
fork的返回值:
1.如果子进程创建成功了,在父进程中返回子进程的PID,而在子进程中返回0。
2.如果子进程创建失败,则父进程中返回。
既然子进程创建的返回值不一样,那么我们就可以通过这个性质来分流。
代码如下:
运行结果:
六、Linux的进程状态
进程从创建开始到被系统清理消亡的这个时间里,有时会占用CPU,有时在等待CPU分配资源,从这里看,进程是不同于程序的,进程有着自己动态变化的状态。下面的这个图就是我们待会要说明的。
我们可以看看在Linux中的进程状态的内容,下面是Linux内核的部分源代码:
/*
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*/
static const char *const task_state_array[] = {
"R (running)", /*0 */
"S (sleeping)", /*1 */
"D (disk sleep)", /*2 */
"T (stopped)", /*4 */
"t (tracing stop)", /*8 */
"X (dead)", /*16 */
"Z (zombie)", /*32 */
};
敲黑板:
这里的进程状态实际上是保存在进程控制块(PCB)中的,而在Linux中就是保存在了task_struct中的。
在Linux中我们可以使用ps aux和ps axj来查看进程的状态。
ps aux
ps axj
1.运行状态——R
这个状态(Runing)不一定就是进程处在运行当中,也有可能是在运行队列当中,也就是说系统中可以同时有多个处于R状态的进程。
敲黑板:
所有处于这个状态的进程,都是可以被调度的,他们在运行队列当中,在操作系统需要切换进程时,可以在这个里面直接选取。
2.浅度睡眠状态——S
一个进程处于浅度睡眠状态(sleeping),意味着该进程在等待事件的完成,这时的进程是可以被随时唤醒的(和人一样),也可以被杀亖(由于这个原因,这个状态也被叫做可中断睡眠(interruptible sleep)。
我们可以写个代码来演示一下:
这里我们让程序休眠100秒来模拟进程处于浅度睡眠状态。
ps aux | head -1 && ps aux | grep test | grep -v grep
之前说过处于这个状态的进程是可以被杀亖的,我们来看看:
3.深度睡眠状态——D
一个进程处于深度睡眠状态(disk sleep),表示这个进程是不可以被杀亖的,即使是操作系统也不行,只能该进程自动唤醒才可以恢复。所以这个状态也可以被叫做是不可中断睡眠(uninterruptible sleep),处在这个状态的进程通常需要等待IO的结束。
其实我们通过他的英文也可以知道,他肯定适合磁盘(disk)相关的,具体的场景就是要向硬盘中写数据,这个过程是不能被杀亖的,因为我们等待磁盘的回复(是否写入完毕)来做出反应(磁盘处于休眠状态)。
4.暂停状态——T
在Linux中我们可以发送SIGSTOP信号让进程处于暂停状态(stopped),发送SIGCONT信号让处于暂停状态的进程重新运行起来。
例如:
我们发送一个SIGSTOP信号给test让进程处于暂停状态。
然后我们再发送一个SIGCONT信号让进程重新运行,这里运行的时候尽量快一点,因为时间一代程序就结束了:
补充说明:
我们可以使用kill -l命令来列出命令集
kill -l
5.僵尸状态——Z
一个进程在要退出时,在系统层面,并不是我们想的直接就释放资源,而是会保存一段时间,来供操作系统或父进程读取退出信息,如果没有读取到相关的退出信息,那么数据也不会被释放,一个进程在等待数据被释放的过程就是处于僵尸状态(zombie)。
我们通过上面的描述也能知道僵尸状态其实是很必要的,因为进程就是去被指示去做事情的,那么指示方就应该要知道被指示方的完成情况,而僵尸状态就是指示方来获取完成情况的。
例如:我们之前一直在写的return 0;实际上这个0就是返回给给操作系统的,让操作系统知道我们完成的情况。在Linux中我们可以通过打印$?来获取最近一个进程的退出码。
echo $?
敲黑板:
和运行状态一样,这个退出码也是被保存在PCB中的,在Linux中就是在task_struct中。
6.死亡状态
死亡状态就是一个理论上的返回状态,当一个进程的退出信息被读取后,该进程申请的资源就被释放掉了,那么该进程也就不存在了,所以我们不可能在我们列出来的信息中看到死亡状态。
七、僵尸进程
顾名思义处在僵尸状态的进程就是僵尸进程。
我们也可以来见一见僵尸进程,例如下面的代码:
这个代码的意思是,fork创建的子进程打印五次信息后退出,而父进程一直在打印信息,也就是说子进程退出了,父进程并没有获取子进程退出的信息,那么子进程就是僵尸进程了。
我们可以执行下面的shell程序来进行监控:
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;echo "######################";sleep 1;done
八、僵尸进程的危害
1)僵尸进程的状态会一直维护,因为他要告诉父进程他的退出信息,也就是父进程不读取,子进程的僵尸状态会一直在。
2)僵尸进程的退出信息一直保存在task_struct中,僵尸状态不退出,那么PCB就要一直维护。
3)如果一个父进程创建了许多子进程,但是都没有回收,那么就会造成资源浪费。
4)如果僵尸进程申请的资源不被回收,僵尸进程越多,浪费的越多,可用的空间越少,就有可能会造成内存泄漏。
九、孤儿进程
这个进程就是同僵尸进程一样的父子进程中的另一种情况,那就是如果是父进程先退出,子进程还在运行,那么子进程进入僵尸状态后就没有了父进程来来处理他了,那么此时的子进程就成了孤儿进程。
如果一直不处理孤儿进程,那么他会一直占用资源,这时就会造成内存泄露,因此孤儿进程将被1号init进程领养,之后进入僵尸状态就由init进程回收。
我们也可以写个代码来看看:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("我在运行!\n");
pid_t id = fork();
if(id == 0){//子进程
//int count = 5;
while(1){
printf("我是一个子进程,我的PID是:%d,我的PPID是:%d\n", getpid(), getppid());
sleep(1);
}
//printf("子进程退出!\n");
//exit(1);
}else if(id > 0) //父进程
{
int count = 5;
while(count--)
{
printf("我是一个父进程,我的PID是:%d,我的PPID是:%d\n", getpid(), getppid());
sleep(1);
}
printf("父进程退出!\n");
exit(0);//正常退出
}else{ //创建失败
}
//sleep(100);
return 0;
}
这个代码和上面的相反,就是父进程打印5次然后正常退出,子进程还在打印。
运行结果:
这里面我们可以看到,父进程退出后,子进程的PPID变成了1,也就是被1号进程领养了。
十、进程优先级
1.基本概念
什么是?
1)cpu资源分配的先后顺序,就是指进程的优先权(priority)。
2)优先权高的进程有优先执行权利。
为什么要有?
配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
2.查看系统进程
在Linux和unix系统中使用ps -l命令就可以输出下面的内容:
我们注意到这里面有几个重要的信息:
- UID:代表执行者的身份
- PID:代表这个进程的代号
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行
- NI:代表这个进程的nice值
我们重点谈谈PRI和NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别
说明一下:
在Linux中PRI(old)默认就是80,即 PRI(new)=80+nice
3.查看进程优先级
我们可以用ps -al命令来查看进程的优先级:
说明:初始的进程一般优先级PRI是80,NI是0。
4.使用top来该nice值
top指令就像是Windows系统的任务管理器,可以显示系统中进程资源的占用情况。
使用完top后我们按r键会要求我们输入要调整的进程的PID:
输入完PID后,就会要求我们输入nice值,我们这里选择了5476进程,输入nice值19,然后回车,最后q退出:
这里显示的结果就是80+19之后的值。
敲黑板:这里如果是想将nice值设置为负数,就要使用sudo来提权执行top。
5.通过renice来修改nice值
使用renice指令,就是在后面加上nice值和进程PID。
我们注意到基本上优先级的提升都要有sudo提权,设置nice值为负数更是如此。
6.补充四个概念
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为
了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
十一、环境变量
1.基本概念
环境变量是操作系统中一个具有特定名字的对象,用来指定一些操作系统运行环境的参数。
比如:我们在些C/C++程序时,我们要链接一些动静态库,而我们也不知道动静态库在哪,然而我们还是可以生成可执行程序,这就是环境变量在帮我们查找。
环境变量通常具有特殊的用途,并且在系统中通常具有全局特性(比如我们在配置java的jdk和Python时都会用到)。
2.常见的环境变量
- PATH:指定命令的搜索路径
- HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录
- SHELL:当前Shell,它的值通常是/bin/bash。
3.查看环境变量的方法
我们可以使用echo命令来打印出环境变量
echo $name // name是环境变量的名称
echo $__
举例:看一看PATH
echo $PATH
4.测试PATH
其实我们曾经会有一个这样的疑问,那就是ls命令是C语言写的,为什么它运行的时候不需要./就可以执行?
首先,我们知道系统要执行一个可执行程序一定是要知道在哪的,既然、ls命令可以不靠./来找到位置,那么就是系统能够通过ls来找到它的位置。
也就是说,系统是通过环境变量来找到这个命令的位置的。
其实系统就是通过环境变量PATH来找到命令的,我们可以打印出来看看:
我们可以看到,PATH会有用:分割的路径,当我们在使用ls命令时,系统就会从左到右各个路径下来查找命令的位置。
而我们发现其实ls命令就是在这些路径之中的:
那我们可不可以也不带路径来执行自己写程序呢?
其实是可以的,这里介绍两种方式:
第一种方式:直接拷贝可执行程序到PATH的某一个路径下
我们这里就拿/usr/bin举例,这里相当于借这个路径来办自己的事。
sudo cp test /usr/bin
第二种方式:将所在的目录导入PATH中
直接讲可执行程序导入到PATH就不需要借助其他路径了
export PATH=$PATH:/home/xywl/Blog
直接执行test,我们发现可以执行。
5.测试HOME
任何一个用户在系统登录时,都会有自己的主工作目录(家目录),那么环境变量HOME就保存了这一个主工作目录。
普通用户:
超级用户:
6.测试SHELL
实际上我们在linux操作系统上面敲的每一条指令都是需要命令行解释器进行解释的,而事实上Linux中有许多种命令行解释器(例如bash,sh),我们可以查看环境变量SHELL来看看:
而这个解释器其实是系统中的一个可执行的指令,当他执行起来后,就可以对我们的命令进行解释了。
7.和环境变量相关的指令
1、echo:显示环境变量的值
2、export:设置新的环境变量
3、env:显示所有的环境变量
图中部分变量的说明:
变量名 | 表示内容 |
---|---|
XDG_SESSION_ID | 当前登录会话的唯一标识符(此处为 60),用于桌面环境和系统服务管理会话。 |
HOSTNAME | 主机名(此处为localhost.localdomain),用于网络标识和内部通信。 |
SHELL | 当前用户使用的默认 shell 程序(此处为 /bin/bash )。 |
TERM | 终端类型(此处为 xterm ),影响命令行显示和交互方式。 |
USER | 当前登录的用户名(此处为 root ,即超级管理员)。 |
LS_COLORS | ls 命令的颜色配置规则,定义不同文件类型的显示颜色(如目录为蓝色、可执行文件为绿色)。 |
当前用户的邮件存储路径(此处为 /var/spool/mail/root )。 |
|
PATH | 系统命令搜索路径,按顺序查找可执行文件(此处包含 /bin 、/usr/bin 等核心目录)。 |
PWD | 当前工作目录(此处为 /root ,即 root 用户的主目录)。 |
LANG | 系统语言和字符集(此处为 zh_CN.UTF-8 ,表示中文简体 + UTF-8 编码)。 |
HISTCONTROL | shell 历史记录控制选项(此处 ignoredups 表示忽略重复命令)。 |
SHLVL | shell 嵌套层级(此处为 1,表示当前是第一层 shell)。 |
HOME | 当前用户的主目录(此处为 /root )。 |
LOGNAME | 用户登录名(此处为 root ),通常与 USER 相同。 |
XDG_DATA_DIRS | 桌面环境的数据文件搜索路径(如应用程序图标、主题等)。 |
LESSOPEN | less 命令的预处理程序(此处使用 lesspipe.sh 处理压缩文件等特殊格式)。 |
XDG_RUNTIME_DIR | 当前用户的临时文件存储目录(此处为 /run/user/0 ,0 通常代表 root 用户)。 |
4、set:显示本地定义的shell和环境变量
5、unset:清除环境变量
8.环境变量的组织形式
组织形式如下:
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以0'结尾的环境字符串,最后一个字符指针为空。
9.通过代码获取环境变量
我们之前写代码的时候,经常要写main函数,其实main函数是有参数的,只是我们用的少罢了,
main函数是有三个参数的:
int main(int argc, char *argv[], char *env[])
我们先来看看前面的两个参数:
在Linux系统中运行下面的代码来观察:
运行结果:
我们来分别说说这两个参数,我们先说第二个参数,第二个参数实际上就是一个字符指针数组,第一个字符指针存的是可执行程序的位置,其余的字符指针存的就是其他的选项了,最后一个字符指针为空,第二个参数实际上就是字符数组中有效元素的个数。
我们可以基于此来编写一个代码来测试:
运行结果:
我们最后再来谈谈第三个变量:
第三个参数从他的(Environment Pointer)英文缩写也知道他和我们之前写的环境变量有关,实际上他就是一个环境变量表,我们可以编写程序来运行一下:
运行的结果就是环境变量的值:
其实获取环境变量还可以使用第三方变量environ来获取:
结果就是环境变量的值:
10.通过系统调用来获取环境变量
除了上面的两种方法,我们还可以使用系统函数调用来获取环境变量。
getenv()函数可以根据所给的环境变量名在环境变量表中查找,并返回一个指向对应值的指针。
例如,获取PATH的值:
结果如下:
十二、程序地址空间
想必大家对这个图也是比较了解的:
在Linux中我们可以通过以下代码来验证这个图:
结果是相吻合的:
先引入问题,看一段代码:
代码当中用fork函数创建了一个子进程,其中让子进程相将全局变量g_val该从100改为200后打印,而父进程先休眠3秒钟,然后再打印全局变量的值。
按道理来说子讲程打印的全局变量的值为200,而父进程是在子进程将全局变量改后再打印的全局变量,那么也应该是200,但是代码运行结果如下:
更奇怪的是,值不一样,但是地址却是一样的,也是说他们在同一个地址出取出了不同的值。
如果是物理地址,那么这里的值一定是相同的,也就是说这里的地址一定不是物理地址。事实上在语言层面打印出来的地址都不是物理地址,物理地址统一由操作系统管理,用户是不可见的,打印出来的是虚拟地址。所以就算是虚拟地址相同,物理地址也有可能不相同。
敲黑板:虚拟地址与物理地址之间的转化由操作系统完成。
十三、进程地址空间
我们之前的那张图,我们称之为程序地址空间其实是不准确的,那个图应该叫进程地址空间,进程地址空间实际上是一种内核的数据结构,在Linux中以mm_struct表示。
进程地址空间就类似于一把尺子,尺子的刻度由0x00000000到0xf,尺子按照刻度被划分为各个区域,例如代码区、堆区、栈区等而在结构体mm struct当中,便记录了各个边界刻度。
在结构体mm struct当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是由0x00000000到0xffffff线性增长的,所以虚拟地址又叫做线性地址。
补充说明:
1、堆向上增长以及栈向下增长实际就是改变mm struct当中堆和栈的边界刻度。
2、我们生成的可执行程序实际上也被分为了各个区域,例如初始化区、未初始化区等。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,大大提高了操作系统的工作效率。而进行可执行程序的“分区“操作的实际上就算编译器,所以说代码的优化级别实际上是编译器说了算。
每个进程被创建时,其对应的进程控制块(task struct)和进程地址空间(mm struct)也会随之被创建。而操作系统可以通过进程的task struct找到其mm struct,因为task struct当中有一个结构体指针存储的是mm struct的地址。
例如,父进程有自己的task_struct和mm_struct,该父进程创建的子进程也有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到物理内存的某个位置,如下图:
而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中数据拷贝一份出来,然后再进行修改。
例如,子讲程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并目改变子进程当中g_val的虎拟地址通过页表映射后得到的物理地址即可。
这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术。
那么我们来解答以下问题:
问题一:为什么数据要进行写时拷贝?
进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程
问题二:为什么不在创建子进程的时候就进行数据的拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。
问题三:代码会不会进行写时拷贝?
90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。
问题四:为什么要有进程地址空间?
1、有了进程地址空间后,就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属干你的物理内存,总的来说,虚拟地址和页表的配合使用,本质功能就是包含内存。
2、有了进程地址空间后,每个进程都认为看得到都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。
3、有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦或分离。
十四、Linux2.6内核进程调度队列
1.一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的父子均衡问题。
2.优先级
- 普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!
- 实时优先级:0~99(不关心)
我们进程的都是普通的优先级,前面说到nice值的取值范围是-20~19,共40个级别,依次对应queue当中普通优先级的下标100~139。
说明一下:
实时优先级一般用于需要实时控制的场景中,比如刹车系统,制导等这些对实时性要求高的系统中,我们目前不需要。
3.活动队列
时间片还没有结束的所有进程都按照优先级放在该队列
nr_active:总共有多少个运行状态的进程
queue[140]:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,
数组下标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 1.从0下表开始遍历queue[140]
- 2.找到第一个非空队列,该队列必定为优先级最高的队列
- 3.拿到选中队列的第一个进程,开始运行,调度完成!
- 4.遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用
5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
敲黑板:在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不会随着进程增多而导致时间成本增加,我们称之为进程调度的O(1)算法。
4.过期队列
- 过期队列和活动队列的结构相同。
- 过期队列上放置的进程都是时间片耗尽的进程。
。 - 当活动队列上的进程被处理完毕之后,对过期队列的进程进行时间片重新计算
5. active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!