1. 进程状态
先看看一般教材和课本对于进程状态的描述
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
1.1 运行状态
先用代码抽象的描述一下该状态
struct runqueue
{
int type;
int status;
// ...更多其他属性
};
什么叫做运行状态:
只要在运行队列中的进程,它们的状态都是运行状态。处在运行状态的下的进程表示,我已经准备好了,可以随时被调度。(也就是说上图中的创建状态、就绪状态、执行状态也就不再加以区分了)
每一个CPU都会维护一个运行队列(多个CPU就会有多个运行队列~)
1.2 阻塞状态
同样用代码抽象一下该状态
#define KEYBOARD 1 // 键盘类型为1
struct dev
{
int type; // type为1
int status; // 状态为ok
struct dev* next;
// 如果访问的对应资源没有就绪
int datastatus;// 读取数据的状态,没有准备好的
// 阻塞等待
PCB* wait_queue;
};
我们日常所写的代码中,或多或少都会访问系统中的某些资源,比如:磁盘、键盘、网卡等硬件设备。
拿个简单栗子来描述一下阻塞状态:
平时我们所用的
scanf
、cin
本质就是从键盘读取数据,但如果我们使用这些函数就是在屏幕上不输入呢?那么键盘上的数据就是没有就绪,对应的我们进程索要访问的资源没有就绪,此时就不具备访问条件,代码也就无法继续向后执行
操作系统中会存在非常多的队列,如运行队列、等待硬件设备的等待队列等。
进程状态变化的本质:
- 更改PCB status的整数变量;
- 将PCB连入不同的队列中。
我们所说的所有过程,都只和进程的PCB有关,和进程的代码数据没有关系!
结合上述我们可以知道操作系统一定是最先知道它所管理的设备的状态变化的。
当一个进程阻塞了,我们(用户)应该看到什么现象,为什么呢?
用户会发现进程卡住了。因为PCB没有在运行队列中(状态不是运行状态),CPU不调度你的进程了。
1.3 挂起状态
阻塞挂起:
如果一个进程当前被阻塞了,那么注定这个进程在它所等待的资源没有就绪的时候,该进程是无法
被调度的。
假设此时恰好OS内的内存资源已经严重不足了,该怎么办?
将内存数据进行置换到外设,针对所有阻塞进程。
此时操作系统注定会变慢,但这也是无可厚非的,毕竟现在的主要矛盾已经升级为了内存资源严重不足与OS会不会挂掉的矛盾,如果一味放任下去,带来的后果会比OS变慢严重的多。所以现在关系的是让OS继续执行下去。
OS内的数据会被交换到swap分区中去。
当进程被OS调度时,曾经被置换出去的进程代码和数据,又会被重新加载进来。
注:以上操作,全部由操作系统自动执行!
1.4 在Linux内核源码中的运行状态
先看看2.6.32版的Linux内核代码,该版本属于中等偏老的代码了。
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"T (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)" /* 32 */
};
R(running)运行状态:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列
里。S(sleeping)休眠状态:前度睡眠,可以被终止,浅度睡眠会对外部信号做出响应。意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠)。
D(Disk sleep)磁盘休眠状态:深度睡眠,有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
我们在日常生活中很难遇到D状态,用户看到了D,几乎就是计算机快要歇菜(挂掉)了。
T(stopped)停止状态: 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
t(tracing stop)追踪状态:debug程序的时候追踪程序,遇到断点就成就会暂停。
X(dead)死亡状态:这个状态只是一个返回状态,瞬间就没了,你不会在任务列表里看到这个状态。
Z(zombie)僵尸状态:在Linux中特有的一个状态。当进程退出并且父进程(使用wait()系统调用) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
1.4.1 运行状态
运行一个检测进程状态的脚本
while :; do ps ajx | head -1 && ps ajx | grep mytest | grep -v "grep"; sleep 1; echo "---------------------------------"; done
效果图:
写一段死循环代码打印在屏幕上
#include <stdio.h>
int main()
{
while (1)
{
printf("hello world");
}
return 0;
}
利用检测脚本我们发现
该进程的大部分状态是S
(休眠)状态,而仅有极少数的状态是R
(运行)状态,这是为什么呢?
因为计算机的I/O速度相对于CPU的速度来说是非常慢的,可以说仅有1%的情况的进程在运行状态,其他都处于进行I/O的阻塞状态。
如果不想I/O影响进程的运行状态该怎么办呢?
改一下上段代码:
#include <stdio.h>
int main()
{
while (1)
{
; // 把打印变成空语句
}
return 0;
}
此时我们检测到进程的状态一直都处在R
(运行)状态了。
我们发现上述进程的状态后都带一个+号,这是什么意思捏?
这些进程在运行起来都不能在命令行执行各种命令了,并且可以用
Ctrl+c
杀死该进程,这种进程我们称之为前台进程。
当我们使用在命令后带个&
,该进程的运行就会变成后台运行了,例如:
./mytest &
此时,我们观察到,进程的状态后不带+
了,大部分状态也都为S
状态。并且各种命令也能继续在bash
中使用,但是Ctrl+c
却不管用了。
莫慌,用kill
命令即可!
1.4.2 休眠状态
进程在什么情况下才会处于休眠状态呢?
其实在上面运行状态演示的时候我们不难发现,在进程处于I/O的状态下,一般处于休眠状态,所以休眠状态也就可以和阻塞状态所对应起来了。
举个栗子,当我们使用scanf但是不在屏幕上输入任何东西时,进程就会处于休眠状态也就是阻塞状态。
1.4.3 暂停状态
系统中存在许多信号,使用如下命令可以查看这些信号:
kill -l
使用kill
命令来达到进程暂停的效果
kill -OPTION pid
可以清楚的看到STAT状态由S+
->T
了。
进程为什么要暂停呢?
在进程访问软件资源的时候,可能暂时不让进程进行访问,就将进程设置为STOP。
2. 僵尸进程
进程 = 内核PCB + 进程的代码和数据,它们都是要占据内存空间的。进程退出的核心就是:将PCB && 自己的代码的数据释放掉!
为什么我们要创建一个进程捏?
一定是因为要完成某种任务!
但是你(一般指的是退出进程的父进程~)怎么知道此任务完成的怎么样呢?
进程在退出的时候,要有一些退出信息,然后表明自己把任务完成的怎么样!
当一个进程在退出的时候,退出信息会由OS写入到当前退出进程的PCB中,可以允许进程的代码和数据空间被释放,但是不能允许进程的PCB被立即释放!为什么捏(又来了,十万个为什么)?
因为要让OS或者父进程读取退出进程PCB中的退出信息,得知子进程退出的原因!
进程退出了,但是还没有被父进程或者OS读取,OS必须维护这个退出进程的PCB结构!此时,问题来了,这个进程算退出了吗?
从概念上来说此进程已经不能被调度了,算终止了,但不算彻底退出,此时的状态就叫做
Z
状态,也就叫僵尸状态。
父进程或者OS读取之后,PCB状态先由Z
状态改成X
状态,才会被释放。
如果一个进程变成Z
状态了,但是父进程就是不回收它,PCB就要一直存在吗?
答案是肯定的,PCB是会一直存在的,所以我们不及时回收,就会有内存泄漏!
用一段代码来演示一下僵尸进程的栗子~
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)
{
// child
int count = 5;
while (count)
{
printf("I am a child process, and the running times are: %d\n", count--);
sleep(1);
}
printf("I am a child process, I am dead: %d\n", count); exit(2);
}
else
{
// Parent
while (1)
{
printf("I am a parent process, running any time!\n");
sleep(1);
}
}
return 0;
}
可以看到子进程的状态已经变为Z
状态了,并且defunct的意思就是已故的消失的,证明该进程已经终止了。
3. 孤儿进程
上面我们先退出了子进程,父进程一直没有回收子进程的PCB导致子进程变成了僵尸进程。那如果交换一下顺序,先退出父进程会变成什么样呢?
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t ret = fork();
if (ret < 0)
{
perror("fork");
return 1;
}
else if (ret == 0)
{
// child
while (1)
{
printf("I am a child process");
sleep(1);
}
}
else
{
// Parent
int count = 5;
while (count)
{
printf("I am a parent process, and the running times are: %d\n", count--);
sleep(1);
}
printf("I am a parent process, I am dead: %d\n", count);
exit(2);
}
return 0;
}
看出一点问题,父进程并没有变成僵尸进程,而是直接bash回收走了,并且子进程的父进程变成了1。
得到的结论是:
子进程的父进程直接退出了,子进程要被领养,于是变成了孤儿进程。被1号进程所领养,这里的一号进程就是操作系统,名字一般叫systemd或initd。
为什么孤儿进程要被领养捏?
如果不被领养,孤儿进程在退出之后,就没有进程为他回收PCB了,也就造成孤儿进程变成
Z
状态,从而导致大面积的内存泄漏。
4. 进程优先级
4.1 进程优先级
什么是进程优先级?
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
为什么会存在进程优先级这个概念呢?
本质就是因为资源的不足,不能同时满足多个进程的需要,如果资源足够充分也就没有进程优先级的概念了。就好比在食堂打饭一样,有100个同样的食堂窗口,那么这100位同学就没有理由排队在一个窗口打饭了,反之如果有1000位同学同时打饭,那么就需要排队了。
在Linux系统如何做到进程优先级排队的呢?
- 其实就是PCB中的一个int字段,数值越小,优先级越高(可以类比考试排名,名次那个数字越小越牛逼)。
- Linux进程的优先级数值范围:60~99 。
- Linux中默认进程的优先级是:80。
存在进程优先级对操作系统所带来的影响:
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善统整体性能。
使用命令ps -la
查看一下Linux中的进程优先级
我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值-
可以发现PRI
就是priority表示进程的优先级,在Linux中是支持动态优先级调整的,但是不支持直接修改PRI
的值。那么如何修改呢?
从上图可以看到,在
PRI
旁边还有个NI
值,就是nice值,是进程优先级的修正数据。
那么如何修改进程的优先级呢?
首先遵循该公式:PRI(new) = PRI(old) + NI(nice)
- 使用
top
命令打开任务管理器- 按下
R
- 输入你要修改目标进程的pid
- 修改目标进程的优先级,也就是修改nice值(这里我们修改为10)
这时我们发现,当前可执行文件mytest
的PRI
变为90了
上面我们降低了进程的优先级,我们再来提高试试看,提高10,也就是nice-10。
被狠狠拒绝了捏!那该怎么办呢?联想一下在Windows的操作,这种权限不通过的情况下,一般用管理器运行一下就行,在Linux上亦是如此,用命令sudo top
。
修改成功!但是我们是接上面降低优先级的情况下修改的,此处的PRI
没有变回80而是变成了70,于是这里给出一个结论:
PRI(new) = PRI(old) + NI(nice)该公式中的pri(old)每次都会从基准值80开始,而不是从上一次的PRI(old)开始的!
如果提高优先级提高100,也就nice-100会发生什么情况呢?
此时优先级也仅仅只来到了60,而并不是正常的减法来到-20(如果这样的话优先级还欠我20显然不合适)。降低优先级同样如此。
与上面所提到的结论相呼应:
nice调整最小值是-20,超过-20统一当成-20;
nice调整最大值是19,超过19统一当成19。
为什么要把优先级限定到一定的范围呢??
OS调度的时候,要较为均衡的让每一个进程都要得到调度!现如今的操作系统基本都会采用分时调度,分时调度顾名思义就是让每个进程都要均衡的在每个时间段内或多或少的都要享受操作系统的调度。如果优先级没有限定范围,用户可以随意更改,那么就有可能会导致优先级的差距过大,有些优先级比较低的进程长时间得不到CPU调度,从而产生饥饿现象。
4.2 一些其他概念
- 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
- 并发:多个进程在一个CPU下采用进程切换的方式,在同一时间内只有一个进程在运行,在一段时间之内,让多个进程都得以推进,称之为并发。
一个进程不是占有CPU就一直运行,每隔一段时间(时间片)自动被从CPU上剥离下来。Linux内核支持进程之间进行CPU资源抢占(基于时间片的轮转式抢占内核),所以并发必定要考虑进程间的切换,使得进程在宏观上看起来是同时运行的。
1. 活动队列
- 时间片还没有结束的所有进程都按照优先级放在该队列
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么样的呢?
- 从0下表开始遍历queue[140];
- 找到第一个非空队列,该队列必定为优先级最高的队列;
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数,但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率。
2. 过期队列
- 过期队列和活动队列结构一模一样。
- 过期队列上放置的进程,都是时间片耗尽的进程。
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
3. active指针和expired指针
- active指针永远指向活动队列
- expired指针永远指向过期队列
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
没关系!在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!