一、进程状态
CPU执行进程代码不是把进程代码执行完毕,才开始执行下一个,而是给每一个进程预分配一个时间片,基于时间片,进行调度轮转。
并行和并发
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
时间片
Linux/windows民用级别的操作系统是分时操作系统,调度任务追求公平。
运行(进程放在运行队列中)
只要进程在运行队列中,该进程就叫做运行状态,可以被CPU随时调度 。
阻塞(进程放在设备的等待队列中)
例如进程程序里面有scanf函数,其底层封装访问键盘的接口,对外部设备进行读取信息的操作,当键盘中没有信息输入的时候,进程被放置到外设结构体的等待队列中,这个过程便就是阻塞的过程。
挂起
挂起的背景:内存资源严重不足的时候。
例如在内存中,多个进程处于阻塞状态时,会被换出到磁盘中,当访问到外部设备信息的时候,再被换入到磁盘中(磁盘中有个swap分区专门进行换出换入操作)从而用时间换空间,因为换入换出的本质是IO。
二、Linux进程的状态
下面的状态在kernel源代码里定义:
/*
* 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 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
该进程可以被杀掉:
D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。(防止数据丢失)
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。(通俗点说就是,子进程退出了,子进程的代码和数据都被释放,但是父进程仍然管理着子进程的PCB,这使得内存资源被无效占用,从而引发了系统层的内存泄漏)
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
所以需要父进程读取子进程的信息后,子进程退出才行。
补充说明:
进程退出时,代码不会被执行了,首先可以立即释放的就是进程对应的程序信息数据。进程退出,要有退出信息(进程的退出码)保存在自己的task_struct内部,管理结构task_struct必须被操作系统维护起来,方便用户未来进行获取进程退出的信息。
进程创建时,先创建内核数据结构再添加代码和数据。进程退出时先释放代码和数据,然后就是内核数据结构。将代码数据释放,内核数据结构维护的状态就是僵尸状态。
创建维持30秒的僵死进程例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id > 0){ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}else{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护
一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
孤儿进程
父进程先退出,子进程就称之为“孤儿进程。
孤儿进程被系统接管,从而变成后台进程。之所以要被系统接管是因为要回收它的PCB。
三、进程优先级
基本概念
1、cpu资源分配的先后顺序,就是指进程的优先权(priority)。
2、优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
3、还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高
效完成任务,更合理竞争相关资源,便具有了优先级
查看系统进程
UID : 代表执行者的身份(表面进程是谁启动的,linux下一切皆文件,文件会记录下拥有者,所属组和对应的权限,所有操作都是进程的操作,进程会记录是谁启动的我)
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
优先级
PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值。
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice;这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值。nice其取值范围是-20至19,一共40个级别。
pri(最终)=pri(default80)+nice
nice范围:[-20,19]40个数字
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据
用top命令更改已存在进程的nice:进入top后按“r”–>输入进程PID–>输入nice值
四、进程切换
前提:
时间片到了,进程就要切换,Linux是基于时间片调度轮转的,一个进程在时间片到了的时候,不一定跑完了,可以在任何地方重新调度切换。
感性理解
切换就是保存上下文数据和恢复上下文数据的过程。
切换过程的理解
进程在运行时,会有很多的临时数据,都在cpu的寄存器中保存。其中eip(pc)中保存的是当前正在执行指令的下一条指令的地址。ir是指令寄存器,保存的就是正在执行的指令。CPU内部的寄存器的数据,是进程执行时的瞬时状态信息数据(上下文数据)。CPU里面有很多寄存器,但是寄存器不等于寄存器里面的数据。
进程切走时:将相关寄存器的内容保护起来;切回时:将历史保存的寄存器数据,恢复到寄存器中。中间所需的保护代码的容器并不在CPU内部(可以这么理解:进程的上下文寄存器数据,被保存到了当前进程的PCB中)。所以每次切换时,每次保存完上下文数据的时候CPU都是全新的。
Linux第一代内核:
五、Linux的调度算法
一开始,active指针指向array[0],expired指针指向array[1]。
优先级
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应)
实时优先级:0~99
活动队列
1、时间片还没有结束的所有进程都按照优先级放在该队列。
2、nr_active: 总共有多少个运行状态的进程
3、queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级
从该结构中,选择一个最合适的进程的过程:
1. 从0下标开始遍历queue[140]
2. 找到第一个非空队列,该队列必定为优先级最高的队列
3. 拿到选中队列的第一个进程,开始运行,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
5、 bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
过期队列
1、过期队列和活动队列结构一模一样
2、过期队列上放置的进程,都是时间片耗尽的进程
3、当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
1、active指针永远指向活动队列
2、expired指针永远指向过期队列
3、可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
4、没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
Linux链式结构
这样设计的好处是:一个进程,既可以在全局链表中,又可以在任何一个其他数据结构中,只要加结点字段即可。
总结:
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法