1.前置知识
1.1.并行与并发
并发:表示CPU在同⼀个时间内执⾏多个任务
并⾏:表示多个CPU在同⼀个时间内执⾏各⾃的任务
示意图如下:
1.2.时间片
时间⽚(timeslice),⼜称为“量⼦(quantum)”或“处理器⽚(processor slice)”,是分时操作系统分配给每个正在运⾏的进程微观上的⼀段CPU时间(在 抢占内核中是:从进程开始运⾏直到被抢占的时间)
现代操作系统(如:Windows、Linux、Mac OS X等)允许同时运⾏多个进程。例 如,在打开⾳乐播放器的同时⽤浏览器浏览⽹⻚并下载⽂件。由于⼀台计算机通常只 有⼀个CPU,所以不可能真正地同时运⾏多个任务。这些进程「看起来像」同时运 ⾏,实则是轮番运⾏,由于时间⽚通常很短(在Linux上为5ms-800ms),⽤户基 本不会感觉到。
时间⽚由操作系统内核的调度程序分配给每个进程。⾸先,内核会给每个进程分配相 等的初始时间⽚,然后每个进程轮番地执⾏相应的时间,当所有进程都处于时间⽚耗 尽的状态时,内核会重新为每个进程计算并分配时间⽚,如此往复。
在现代操作系统中,⼤部分的⺠⽤级操作系统均是分时操作系统,这类操作系统的最 ⼤特点就是可以通过多道程序和多任务处理的⽅式让⽤户感觉到「尽管只有⼀个 CPU,但是应⽤可以同时执⾏」
1.2.1多道程序:
多道程序:表示操作系统能够同时管理多个运⾏中的程序。在早期的计算机系统 中,⼀次只能运⾏⼀个程序。当这个程序结束或者因为某种原因暂停时,需要⼿ ⼯⼲预来加载下⼀个程序。⽽多道程序技术允许系统同时保持多个程序在内存 中,并且这些程序可以交替执⾏,这样就提⾼了系统的利⽤率和效率
1.2.2 多任务处理:
多任务处理:表示操作系统能够在同⼀时刻处理多个任务的能⼒。在多任务环境下,操作系统通过快速地切换上下⽂(即保存当前任务的状态并加载新任务的状 态),此处切换的时间依据就是时间⽚,使得多个任务看起来像是同时进⾏的⼀ 样
多道程序强调的是在⼀个系统中同时存在多个程序的能⼒,⽽多任务处理则进⼀步强 调了这些程序能够以⼀种看似同时的⽅式执⾏
与分时操作系统类似的,就是实时操作系统,该类操作系统最⼤的特点就是如果有⼀ 个任务需要执⾏,实时操作系统会⻢上(在较短时间内)执⾏该任务,不会有较⻓的 延时。
2.进程状态
2.1基本介绍
在操作系统中,⼀般会存在⼀个进程状态转换图,例如下图:
整个过程中涉及到五个基本进程状态:
1.创建(new):表示进程创建
2. 运⾏(running):表示进程正在被执⾏
3. 等待(waiting):表示进程正在等待具体事件发⽣,也被称为阻塞状态
4. 就绪(ready):等待被调度器调度执⾏
5. 终⽌(terminated):进程完成执⾏
执⾏过程如下:
当进程创建成功后(new),其状态转化为就绪(ready),等待调度器调度 (scheduler dispatch),调度到当前进程后开始运⾏(running),程序 正常结束退出(exit)向操作系统返回数据,最后终⽌(terminated)
整个过程中涉及到等待和中断,例如:当程序需要进⾏类似于IO或者其他事件 (I/O or event wait)时就会进⼊等待状态,等待IO结束或者其他事件结 束(I/O or event completion)再从等待转换为就绪状态(ready)等待 调度器调度重新进⼊运⾏状态(running)
2.2.等待状态的本质
下⾯重点考虑等待(waiting)状态
进程在被创建之后,此时根据操作系统「先描述,再组织」的管理⽅式,在创建进程 时,会形成对应进程的PCB(例如Linux下的 task_struct ),此时「描述」已经 完成
接着程序进⼊就绪状态,此时操作系统会将进程对应的PCB加载到就绪队列中,在 Linux下是⼀般是使⽤双向链表结构对每⼀个PCB进⾏连接,示意图如下,其中 current 指针表示当前正在被执⾏的进程:
每⼀个CPU需要执⾏进程,就需要⼀个与就绪队列有关的结构,这个结构在Linux下 被称为运⾏队列(具体⻅调度算法部分),⼀般该结构中会存在⼀个指针,该指针指 向正在运⾏的进程,例如下⾯的简化图中的 head 指针指向的就是当前进程,⽽ tail 表示当前闲置(idle)的进程
如果此时程序需要进⾏I/O操作,因为I/O操作速度远⼩于CPU的执⾏速度,在分时 操作系统中,会尽可能提⾼CPU的利⽤率,所以此时当前进程就会被操作系统切换到 指定设备的等待队列(例如键盘),⽽CPU继续执⾏其他存在于就绪队列中的进程。 等待队列与就绪队列结构基本⼀致,也是⼀个双向链表结构。进程进⼊等待队列中链 接后,对应进程状态更改为等待状态,等待I/O操作完成。
等待队列和就绪队列示意图如下:
当I/O操作完成,继续进⼊就绪队列等待被调度执⾏进⼊运⾏状态
综上所述:等待的本质就是进⼊对应设备的等待队列进⾏执⾏,只是不会执⾏对应的 代码,⽽等待和运⾏的切换就是进程PCB在不同的双向链表结构中连接
2.3.swap分区
swap 分区从字⾯意思上来看就是交换分区,该分区⼀般存在于硬盘中,主要⽤于内 存和硬盘之间的资源交换,但是这种交换并不是常规性的,⼀般出现于内存空间严重 不⾜的情况
当内存空间严重不⾜时,操作系统为了保证⾃身的运⾏正常,会将当前正在等待队列 的进程对应的代码和数据放到硬盘的 swap 分区,尽可能减少内存空间的占⽤,这个 过程也被称为「换出」,此时进程的状态也被称为阻塞挂起状态
整个过程中的「换⼊」和「换出」实际上就是利⽤「时间换空间」的思想,因为 swap 分区在硬盘上,所以避免不了交换速度慢,如果出现⼤量的交换,整机的效率 就被⼤⼤拉低
部分操作系统也会存在⼀个属于就绪队列的 swap 分区,同样内存空间严重不 ⾜时,会将处于就绪队列中的部分进程的代码和数据进⾏换⼊和换出
3.Linux进程状态
前⾯操作系统的进程状态只是⼀个⼴泛的状态,每⼀种操作系统的进程状态可能不尽 相同,下⾯主要谈Linux下的进程状态
3.1.Linux进程状态分类
在Linux下,进程状态被分为下⾯的7种:
在Linux下,就绪状态和运⾏状态⼀般不作区分,所以就绪队列也可以认为就 是调度队列,处于调度队列的进程对应的状态即为运⾏状态
3.2.运行状态(Running)与等待状态(Sleeping)
运⾏状态:表示程序正在就绪队列或者正在被CPU执⾏,包括前台运⾏和后台运⾏
在Linux下,通过 ps ajx查看到的状态代号后的+代表正在前台运⾏,可以 使⽤ ctrl+c 终⽌,没有+则表示后台运⾏,不可以使⽤ ctrl+c 终 ⽌,只能使⽤ kill 命令
例如下面一个C语言程序:
#include <stdio.h>
int main()
{
while(1) {
}
return 0;
}
对应的 Makefile 如下:
TARGET=status
SRC=status.c
$(TARGET):$(SRC)
gcc $^ -o $@
.PHONY:clean
clean:
rm -f $(TARGET)
查看进程效果:
但是,需要注意,如果上⾯程序写为:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1) {
printf("hello\n");
sleep(1);
}
return 0;
}
此时尽管程序在前台执⾏,查看进程时会显示S+,表示在前台等待:
之所以会出现这种情况,是因为 printf 函数本质是在做I/O,⽽因为I/O的速度远 ⼩于CPU的执⾏速度,所以为了保证CPU利⽤率,在做I/O的过程中,当前进程会被 操作系统列⼊到等待队列,⽽CPU继续执⾏其他处于就绪队列的进程
3.3.硬盘等待状态(Disk Sleeping)
硬盘等待状态是Linux系统特有的进程状态,前⾯提到当内存空间严重不⾜时,操作 系统为了保证⾃身在内存中的空间安全,会将部分处于等待队列的进程对应的代码和 数据换⼊ swap 分区
假设在「内存空间严重不⾜」的背景下,内存中的某⼀个进程需要向硬盘写⼊⾮常多 的数据,此时就会进⾏I/O操作,⽽正在做I/O的进程就处于等待队列中,⽽操作系 统此时因为要保证⾃身安全,就会换出⼀部分进程的代码和数据到 swap 分区
假设这个⾏为刚好将正在等待完成⼤量数据I/O的进程对应的代码和数据换⼊到了 swap 分区,当I/O设备向内存中指定的进程反馈相关信息(例如存储空间不⾜) 时,由于该进程的相关代码和数据被换⼊到了 swap 分区,也就没有办法接受I/O的 反馈信息,同时I/O设备也收不到后续的操作指令,这种情况下,就会出现因存储空 间不⾜的问题导致数据丢失。
上⾯的过程中,如果数据是⾮常重要的数据,就会导致严重的损失
Linux系统为了防⽌这个问题的出现,提出了Disk Sleeping,该状态可以保证当 内存空间严重不⾜时,该进程不会被操作系统换出
3.4.停止状态(Stopped)
依旧以上⾯C语⾔的代码为例:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1) {
printf("hello\n");
sleep(1);
}
return 0;
}
在kill指令中,存在两个选项:
代号为18的选项代表进程继续,代号为19的选项表示进程停⽌
在终端中输⼊:
// 停止进程
kill -19 进程PID
// 继续进程
kill -18 进程PID
需要注意,使⽤ kill -18 进程PID继续指定进程时,对应的进程状态代号后 ⾯不会带+
就可以停⽌进程,即将指定进程的状态更改为Stopped
例如上⾯的程序,运⾏后执⾏ kill -19 20667:
想要程序继续运⾏,可以使⽤ kill -18 20667:
此时想终⽌程序,就必须使⽤ kill -9 21827⽽不能使⽤ ctrl+c ,停⽌进 程后再按下 ctrl+c 即可
3.5.追踪停止状态(tracing stopped)
对于追踪停⽌状态,可以在 gdb 调试指定代码时程序在断点位置暂停看到,例如调试 前⾯的C语⾔代码,查看对应程序进程可以看到:
所以,调试代码之所以可以让程序停⽌运⾏,下⼀次还可以继续运⾏,本质就是通过 追踪停⽌状态(tracing stopped)控制
3.6.僵尸状态(Zombie)和终止状态(Dead)
每⼀个进程需要执⾏都需要管理者的调度,但是进程是否结束管理者也需要知道,这 ⾥管理者有操作系统和其⽗进程,⽽进程告诉操作系统或其⽗进程⾃⼰正常结束的⽅ 式就是通过进程的退出信息,⼀般退出信息存在进程退出码,0表示进程正常退出, ⽽这⼀过程发⽣时刻所处的状态就是僵⼫状态
当操作系统或⽗进程通过某种⽅式获取了对应的进程的退出信息(例如进程退出时的 退出码),进程状态就会变为终⽌状态,但是如果⼀直不查看进程退出信息,进程会 ⼀直处于僵⼫状态
可以使⽤ echo $?显示最近⼀次进程退出的信息,使⽤其查看 ls 命令在⽆法找到⽂ 件时的返回值以及找到⽂件时的返回值:
未找到文件时:
找到文件时:
这⾥使⽤echo $?查看进程退出码本质就是因为bash是ls命令进程的⽗进 程
这也就可以解释为什么之前在写C语⾔程序时,需要在主函数退出前写上return 0,这⾥的0就是告诉操作系统或其⽗进程当前进程正常退出
3.7.进程退出
进程退出:表示当前进程已经进⼊了僵⼫状态,但不⼀定进⼊了终⽌状态
在Linux中,进程退出的特点是:保留对应进程的PCB,但是会销毁对应进程的代码 和数据,⽽之所以要保留PCB就是因为进程的退出信息依旧存在于对应进程的PCB 中,⽽保留的PCB就会被操作系统管理,⽅便未来查看
在Linux最初的源码中,可以看到部分退出信息,例如退出码exit_code:
struct task_struct {
int exit_code;
};
4.僵尸进程
僵⼫进程就是处于僵⼫状态的进程,前⾯提到如果操作系统或者⽗进程没有获取对应 (⼦)进程的退出信息,该进程就会⼀直处于僵⼫状态
例如下⾯的代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("I am parent process, mypid: %d, myppid: %d\n", getpid(), getppid());
// 创建子进程
pid_t id = fork();
if (id == 0) {
while(1) {
printf("I am a child process, my pid = %d, my ppid = %d\n", getpid(), getppid());
sleep(2);
}
}
// 父进程不接收子进程的退出信息
while (1) {
}
return 0;
}
编译运⾏上⾯的代码,再结束掉对应的⼦进程可以看到下⾯的信息:
其中,PID为26544的为⽗进程,PID为26545的为⼦进程,在上⾯的代码中,结束⼦进程后,⽗进程并没有对⼦进程的退出信息进⾏接收,所以此时⼦进程就会持续 保持僵⼫状态,并且对应的进程会被修饰为,表示「失效的」,此时的 task_struct就会被操作系统保存,但是对应进程的代码和数据就会被操作系统移除
因为处于僵⼫状态时,进程已经退出,所以不可以再使⽤kill指令结束该僵⼫进 程:
进⼊僵⼫状态的进程,默认情况下是不会被任何进程托管,所以⼀旦出现了僵⼫进 程,就表示该进程退出信息没有任何进程接收,这种情况下就会出现内存泄漏问题
在前⾯C/C语⾔层⾯提到的内存泄漏表示开辟的空间在没有使⽤的情况下,程序运 ⾏时没有释放导致持续占⽤空间,但是这种内存泄漏最⼤的特点就是程序⼀旦结束, 该空间就会被释放。所以语⾔层⾯的内存泄漏在常驻内存的进程上影响最⼤,但是不 论如何,还是要处理这种内存泄漏问题
此处进程的内存泄漏表示处于僵⼫状态的进程,因为进程退出信息没有被接受,导致 其 task_struct ⼀直存在于内存中,但是这种内存泄漏是⽆法在程序结束后被操作 系统⾃动释放。所以为了避免出现这种内存泄漏问题,需要对每⼀个进程的退出信息 进⾏接收
5.孤儿进程
前⾯提到的是⼦进程先结束,⽗进程没有结束并且不接受⼦进程的退出信息,⼦进程 就处于僵⼫状态,如果反过来先结束⽗进程,再结束⼦进程就会出现⼦进程变为孤⼉ 进程,编译运⾏前⾯的代码,结束对应⽗进程结果如下:
孤⼉进程最⼤的特点就是其PPID 变为1,并且为后台运⾏,所以不可以使⽤Ctrl + c 退出
可以使⽤ top 指令查看 PID 为1对应的进程:
可以看到孤⼉进程会被系统托管
6.进程优先级
进程优先级,表示优先被CPU执⾏的进程的等级,在Linux下,进程优先级等级越 ⼩,优先级越⾼,被优先执⾏的概率越⼤
之所以需要进程优先级,是因为⼤部分的⺠⽤电脑都只有⼀个CPU,但是进程的个数 可以有很多,这种情况下就需要进程对CPU资源的抢夺,为了保证部分进程能以更⼤ 优势抢到CPU资源,就需要进程优先级
在Linux下,可以使⽤下⾯的指令查看到当前⽤户执⾏的进程对应的优先级:
ps -la
在Linux下,进程优先级由两个值进⾏控制,⼀个是 PRI (priority),另⼀个是 NI (nice), PRI 代表进程启动时系统⾃动分配的优先级,⽽NI代表优先级修正 值,这个值的范围是 [-20, 19]
在计算Linux进程的优先级时,使⽤公式:PRI = 初始 PRI + NI值
例如,启动下⾯的C语⾔程序:
#include <stdio.h>
int main()
{
while(1) {
}
return 0;
}
使⽤p s -la查看效果:
因为初始的PRI为80, NI 为0,所以最终的PRI = 80 + 0 = 80
在Linux下,不可以修改 PRI ,但是可以通过修改NI从⽽改变进程优先级。使⽤ top 指令,输⼊r,再输⼊已启动进程的 PID ,再输⼊对应的NI值即可修改
如果将NI修正为-6,则会出现下⾯的结果:
因为默认的PRI值为80,⽽此时 NI 值为-6,所以最终的PRI = 80 - 6 = 74
如果此时将NI修正为15,则会出现下⾯的结果:
默认的PRI值为80,⽽此时的 NI 值为15,所以最终的PRI = 80 + 15 = 95
可以看到,尽管开始修改了 PRI 为74,下⼀次再更改 NI 值时,计算PRI使⽤的还是 初始的PRI+NI
标签:状态,优先级,操作系统,队列,Linux,进程,CPU From: https://blog.csdn.net/2301_79582015/article/details/142946328进程优先级并不⽀持频繁修改,在Linux下,可能修改1次或者2次左右后再修 改NI就需要使⽤ root 权限
在实际中,进程优先级⼀般很少去修改,尽管可以在程序中使⽤函数更改进程优先级 或者使⽤命令修改进程优先级