目录
- 进程状态
- Linux内核源代码怎么说
- 运行&&阻塞&&挂起
- 内核链表
- 进程状态查看
- Z(zombie)-僵尸进程
- 僵尸进程危害
- 孤儿进程
- 进程优先级
- 进程切换
- Linux2.6内核进程O(1)调度队列
进程状态
Linux内核源代码怎么说
为了弄明白正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
进程状态就是task_struct内的一个整形变量,也是一个标记位(宏定义的)。
状态决定了当前系统如何处理这个进程。
运行&&阻塞&&挂起
运行状态
该进程在调度队列中或者正在被CPU执行。
阻塞状态
等待某种设备或资源就绪(如果不就绪,我的进程就不会被调度)。
运行状态转化为阻塞状态,本质上就是把PCB链入到不同的数据结构当中!只有在运行队列中,进程才会被调度,但是现在进程在设备队列中,那么如何再被CPU调度呢?
总结:进程状态变化的表现之一,就是CPB要在不同的数据结构之间进行流动,本质都是数据结构的增删查改。
挂起状态
当内存资源不足时,将wait_queue里的进程的代码和数据唤出到磁盘当中swap交换分区。如果次数键盘就绪了,OS会把磁盘当中该PCB的代码和数据再重新加载到内存,构建指针映射,这个也叫做唤入。我们把这个称为阻塞挂起,运行挂起也是同样的道理。
内核链表
下⾯的状态在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 */ //暂停(追踪状态),例如debug的调试状态下,设置断点,进程被站暂停了
"X (dead)", /*16 */ //死亡
"Z (zombie)", /*32 */ //僵尸 ->为了获取退出信息
};
• R运⾏状态(running): 并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏队列⾥。
• S睡眠状态(sleeping): 意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠,浅睡眠。ctrl+c是可以杀掉这个进程的。
(interruptible sleep)(浅睡眠))。
• D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),深度睡眠,在这个状态的进程通常会等待IO的结束,不可中断睡眠状态的进程不能被杀掉,只能等待它自己被唤醒,这样可以防止数据的丢失。例如:当进程往磁盘里写入内容时,此时如果内存资源严重不足时,需要杀掉进程时,该进程处于S状态,可以被杀掉,等待磁盘写入完数据,如果杀掉该进程,那么这个内容到底是写完了?还是没写完?我都不知道,磁盘也不知道该如何处理这一段内容,所以可能会存在数据丢失,此时,把该进程的状态修改为D,此时该进程不对任何杀掉的动作进行响应,OS就不会杀掉它。• T停⽌状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏,如果进行非法操作,系统可以暂停进程,止损。
t (tracing stop): 暂停(追踪状态),例如debug的调试状态下,设置断点,进程被暂停了。
• X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
插播一个关于内核结构申请的小的知识点:要把释放的PCB回收时,可以用unuse链表把要回收的PCB节点链接起来,等创建新进程时,需要创建PCB节点时,把unuse链表里的PCB拿来用,这种技术叫做slab
。数据结构对象的缓存,可以加速创建和释放进程的速度。
进程状态查看
ps aux / ps axj
- a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
- x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
- j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
- u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等
Z(zombie)-僵尸进程
- 僵死状态(Zombies)是⼀个⽐较特殊的状态。当进程退出,进程的代码和数据被释放,但是PCB不会被释放,此时父进程还没有从子进程的PCB里获取退出信息。
- 僵死进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码。
- 所以,只要⼦进程退出,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程进⼊Z状态。
- 如果父进程一直不管,不回收,不获取子进程的退出信息,那么Z(僵尸状态)会一直存在,此时,会造成内存泄漏!!!
僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的!
- 维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
- 那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置进⾏开辟空间!
- 内存泄漏?是的!
孤儿进程
- ⽗进程如果提前退出,那么⼦进程后退出,进⼊Z之后,那该如何处理呢?
- ⽗进程先退出,⼦进程就称之为“孤⼉进程”
- 孤⼉进程被1号init进程领养,当然要有init进程回收喽。1号init进程我们可以理解为操作系统。
有1号进程,那么有0号进程吗?
有的,只不过刚开机时,被1号进程给替换掉了。
为什么要领养?
为了回答这个问题,我们就要想一想,如果不领养会怎么样?子进程退出后,进入僵尸状态,如果没有父进程回收子继承,那么会造成内存泄漏。hou
注意:变成孤儿进程之后,被1号进程领养,子进程会自动变成后台进程,使用ctrl+c杀不掉它的,但是后台进程可以向前台进程打印信息。
进程优先级
基本概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性能。
- 还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
总结:进程优先级就是进程得到资源的先后顺序。
为什么会有优先级这个概念?
CPU太少了,内存太少了,进程又多,目标资源稀缺,导致需要通过优先级来确认谁先谁后的问题。
优先级VS权限
优先级:能得到这个资源,只不过是先后的问题。
权限:能不能得到这个资源。
查看系统进程
在linux或者unix系统中,⽤ps ‒l命令则会类似输出以下⼏个内容:
我们很容易注意到其中的⼏个重要信息,有下:
• UID : 代表执⾏者的⾝份,user id。
Linux系统中,访问任何资源,都是进程代表用户去访问的。进程当中的UID代表的是该进程是被哪个用户启动的。OS中用户名字是给我们看的,系统里面区分用户是用的UID,当进程去访问文件的时候,OS会把进程的UID和文件的用户的权限进行对比,查看该进程是否有权限。
• PID : 代表这个进程的代号
• PPID :代表这个进程是由哪个进程发展衍⽣⽽来的,亦即⽗进程的代号
• PRI :代表这个进程可被执⾏的优先级,其值越⼩越早被执⾏,默认是80
• NI :代表这个进程的nice值,进进程优先级的修正数据,默认值为0
进程真实的优先级 = PRI(默认) + NI
修改进程优先级
1.
top->r->pid
2.
使用 nice 命令
nice 命令用于在启动新进程时设置其优先级。其语法为:
nice -n <nice_value> <command>
3.
使用 renice 命令
renice 命令用于调整已经运行中的进程的优先级。其语法为:
renice <nice_value> -p <pid>
4
使用 setpriority() 系统调用
#include <sys/time.h> #include <sys/resource.h> int setpriority(int which, id_t who, int value); 参数: which:指定要调整优先级的对象类型,可以是 PRIO_PROCESS(进程)、PRIO_PGRP(进程组)或 PRIO_USER(用户)。 who:指定要调整优先级的对象 ID。如果 which 是 PRIO_PROCESS,则 who 是进程 ID;如果是 PRIO_PGRP,则 who 是进程组 ID;如果是 PRIO_USER,则 who 是用户 ID。 value:新的 nice 值,范围是 -20 到 19。
PRI and NI
• PRI也还是⽐较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执⾏的先后顺序,此值越⼩进程的优先级别越⾼。
• 那NI呢?就是我们所要说的nice值了,其表⽰进程可被执⾏的优先级的修正数值。
• PRI值越⼩越快被执⾏,那么加⼊nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
• 这样,当nice值为负值的时候,那么该程序将会优先级值将变⼩,即其优先级会变⾼,则其越快被执⾏。
• 所以,调整进程优先级,在Linux下,就是调整进程nice值。
PRI vs NI
需要强调⼀点的是,进程的nice值不是进程的优先级,他们不是⼀个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据。
优先级的极值问题
nice的范围是[-20,19],也就是说,PRI的范围是[60,99],幅度为40。考虑到公平性,幅度不能太大,优先级设立的不合理,会导致优先级低的进程,长时间得不到CPU的资源,进而导致进程饥饿。
查看进程优先级的命令
⽤top命令更改已存在进程的nice值:
• top
• 进⼊top后按“r”‒>输⼊进程PID‒>输⼊nice值
注意:
其他调整优先级的命令:nice
,renice
进程切换
CPU上下⽂切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运⾏另外的任务时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务⾃⼰的堆栈中, ⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器,并开始下⼀个任务的运⾏, 这⼀过程就是context switch
。
结论:
- 寄存器:保存正在运行的进程的临时数据,是CPU内部保存数据的临时空间。
- 寄存器 != 寄存器里的数据。寄存器是一段空间,只有1份,寄存器里的数据,是内容,变化的,可以有多份。
- 进程切换最核心的,就是保存和恢复当前进程的硬件上下文数据,即CPU内寄存器的内容。
- 当前进程把自己的硬件上下文数据保存到了
task_struct
里,在结构体里可以命名一些寄存器的变量,来存放相关数据,当进程被切换时,找到task_struct
里的数据,把数据放到CPU里的寄存器里。由于task_struct
的内容很多,所以当代OS把硬件上下文数据独立保存了TSS
里,没有放在task_struct
里了,但是task_struct存放了TSS
的地址。
- 如何区分全新的进程和已经被调度过的进程?可以使用标记位(int isrunning),如果是新进程,isrunning的值为0,如果被调度过,isrunning为1.
- 时间⽚:当代计算机都是分时操作系统,每一个进程都有它合适的时间⽚(其实就是⼀个计数器)。时间⽚到达,进程就被操作系统从CPU中剥离下来。
Linux2.6内核进程O(1)调度队列
背景知识:
调度器:是由调度和切换共同构成的,调度和切换也是两个不同的动作。
分时操作系统VS实时操作系统分时操作系统是基于时间片的操作系统,时间片一结束,进程就会从CPU上剥离下来。
实时操作系统是一个进程运行完,才运行下一个进程,工业领域运行较多。
上图是Linux2.6内核中进程队列的数据结构。
⼀个CPU拥有⼀个runqueue
如果有多个CPU就要考虑进程个数的负载均衡问题
优先级
普通优先级:100〜139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级:0〜99(不关⼼)
活动队列
• 时间⽚还没有结束的所有进程都按照优先级放在该队列
• nr_active: 总共有多少个运⾏状态的进程
• queue[140]: ⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以,数组下标就是优先级!
• 从该结构中,选择⼀个最合适的进程,过程是怎么的呢?
1. 从0下表开始遍历queue[140]
2. 找到第⼀个⾮空队列,该队列必定为优先级最⾼的队列
3. 拿到选中队列的第⼀个进程,开始运⾏,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
• bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤5*32个⽐特位表⽰队列是否为空,这样,便可以⼤ 提⾼查找效率!
过期队列
- 过期队列和活动队列结构⼀模⼀样
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间⽚耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进⾏时间⽚重新计算
active指针和expired指针
• active指针永远指向活动队列
• expired指针永远指向过期队列
• 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间⽚到期时⼀直都存在的。
• 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了⼀批新的活动进程
为什么优先级既有PRI,又有NI?
如果在活跃队列里,把一个进程的优先级由80到60,我们是把该进程的位置调整吗?还是调整过期队列里的位置呢?这样做都不是很好,利用NI,活跃进程里的进程调度完之后,在过期队列里,根据NI,再重新调整该进程的位置。
总结
• 在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成本增加,我们称之为进程调度O(1)算法!