目录
前言
1. 进程的状态
1.1 进程排队
1.2 运行,阻塞,挂起
2.Linux下具体的进程状态
2.1僵尸和孤儿
3.进程的优先级
4.Linux的调度与切换
前言
这篇继续来学习进程的其它知识
上篇文章:Linux--进程(1)-CSDN博客
1. 进程的状态
关于进程的状态:
首先我们来轻量聊一下进程排队这件事情--队列
然后谈一谈教材上关于进程状态的表述---运行,阻塞,挂起
最后看看Linux下具体的进程状态(具体的看看什么是运行,什么是阻塞,什么是挂起)
1.1 进程排队
进程不是一直在运行的;进程放在了cpu上,也不是一直在前全运行的
进程不是在一直运行的,也可能在等待某中软硬件资源,比如我们在写c语言代码的时候,运行到scanf那一行,进程就在等待输入。
进程放在了cpu上,也不是一直在前全运行的:每个进程都会被分配一个时间段,我们称之为时间片,如果时间结束该进程还在运行,CPU将进行剥夺资源并分配给另一个进程。我们写代码的时候有时候会遇到死循环,当代码运行生成了进程,进程时间结束,我们会发现循环仍未终止,但实际上这个进程资源已经被剥夺了。在Linux系统上,他可能在5毫秒到800毫秒之间。由于时间片极短,用户不会感觉到程序的切换,从而形成了多个进程同时运行的错觉。
我们接着来谈谈排队的问题
首先进程排队一定是在等待某种资源
1.进程=task_struct+可执行程序,进程排队不是可执行程序在排队,而是task_struct对象(PCB)在排队,比如你实习投简历的时候,是简历在排队不是你人在排队。
2.PCB的问题
一个进程的PCB可以被链入多种数据结构中,在不同的场景下,操作系统可能会采用不同的数据结构来组织和管理PCB。一个PCB通常不会同时被链接到多种数据结构中。它会被放置在最适合当前管理需求的数据结构中。例如,当进程处于 就绪状态时,它的PCB会被放在就绪队列中;当进程因等待某个事件而被阻塞时,它的PCB会被移到阻塞队列中。操作系统会根据需要动态地更新和移动PCB,以确保进程能够按照正确的顺序和方式得到调度和执行。
1.2 运行,阻塞,挂起
1.所谓的状态本质就是在task_struct中的一个整型变量
eg:所谓的状态不过是status所被给予的数字
2.状态决定了什么?
状态决定了进程的后续动作,Linux中可能存在多个进程都要根据他的状态执行后续的动作。在这种情况下就需要让进程去排队了,一个CPU一个运行队列
运行状态:运行状态是进程实际占用CPU执行其程序代码的状态。我们在主流的操作系统中,只要进程处在运行队列中排队,我们都可以称这个进程在运行状态。
在前面我们说了,进程不是在一直运行的,也可能在等待某中软硬件资源,进程放在了cpu上,也不是一直在前全运行的。
在这里我以硬件为例,来了解阻塞状态。
操作系统,管理硬件,要先把硬件表述起来,再去管理,每个硬件都有属于自己的对象,cup也是硬件,他有他的运行队列,其它硬件也有其它硬件的队列,称之为等待队列。
我引用上面的例子,当代码运行到scanf的时候,进程就在等待输入,这时进程的状态就会从运行状态变为阻塞状态,进程会被操作系统(OS)搬迁到键盘的等待队列中去,当输入完成之后,OS就会知道进程已经就绪了,这时进程又会重新被搬迁到运行队列中去,此时进程变为运行状态。(状态的变迁,实际上就是OS将PCB搬迁到不同的队列中)
总结阻塞状态:当我们的进程正在等待软硬件资源的时候,资源如果没有就绪,我们的进程task_struct只能:1.将自己设置为阻塞状态 2.将自己的PCB链入等待的资源提供的等待队列中去。
挂起状态:这个状态并不常见,这个状态的前提是计算机资源已经比较吃紧了。因为这些进程暂时没有动作,此时OS就会将阻塞状态的进程(代码和数据,不包括PCB)转到外设中存(磁盘的swap分区)储起来(唤出),需要时再唤入。
2.Linux下具体的进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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))。
我们举一个样例,来看看在Linux中状态具体的样子。
我们运行这个代码,查看了这个进程的状态发现它处于S(阻塞)状态,这时我们就有疑问了,这段代码进入了死循环应该是一直在运行的啊,不应该是R(运行)状态吗?
这是因为,printf的缘故,这个程序的大多数时间都在执行printf函数,这时就会发生系统调用访问外设,在这些系统调用期间,进程大多数时间都是处于等待的状态。所以就会处于S状态。
此时我们把printf删去,这个程序就只做死循环了,不会去访问任何外设,这时候进程就一直处于运行状态了。
进程后面+的含义:表示这个进程是前台进程。如果我们在运行可执行程序的时候后面加上&符合,这时就会变成后台进程,没有+进行标识了。此时进程进行CTRL+C是无法终止的,只能使用kill指令杀掉该进程。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。(在资源极度紧张的情况,操作系统主动杀掉了进程,为了避免这种情况,就引入了这个状态,等磁盘工作完返回给进程,进程任务完成了才结束。),这个状态也属于阻塞状态,因为它在等待资源就绪。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- t停止状态,这个状态一般在调试代码,对代码进行打断点的操作时,等待你的的下一步操作,此时进程就会处于t状态。(T/t也可以理解成一种阻塞状态,因为在这个状态进程也是在等待某种资源的就绪)
kill命令的第19号选项,就可以让进程暂停,在输入18号选项就可以就绪了。但被继续的进程会自动转为后台进程。下面是示例:
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
2.1僵尸和孤儿
- Z僵尸状态(zombie)
1.僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
2.僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
3.所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
来一个创建僵死进程例子:
#include <stdio.h> #include<stdlib.h> #include <unistd.h> int main() { pid_t id = fork(); if (id == 0) { //child int cnt = 5; while (cnt) { printf("I am child,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(1); cnt--; } exit(0);//让子进程退出 } //father while (1) { printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid()); sleep(1); } return 0; }
编译并在另一个终端下启动监控
开始测试
看到结果
我们看到子进程已经进入僵尸状态了,此时子进程已经死了,只是还要维持到被父进程读取
为什么要有Z状态?
创建进程是希望这个进程给用户完成工作的,子进程必须有结果数据,PCB中的。
僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
- 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏
- 如何解决呢?在这里我们见一个函数wait,后面我们在详细的介绍
开始5s子进程和父进程一起进行,5s后子进程进入僵尸状态父进程,10s父进程结束执行wait函数(回收子进程资源,获取子进程状态)(bash创建的子进程bash会自动回收)
孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
我们重新设置一下代码,让父进程先结束:
测试结果:
我们发现父进程结束后,子进程转为了后台进程,且父进程为”1“,1号进程其实就是操作系统,也就是说父进程结束后,它的子进程将会被操作系统领养。
3.进程的优先级
是什么?
前提:进程需要访问某种资源,进程通过一定的方式(排队),确认享受资源的先后顺序
为什么?
因为资源有限。
怎么办?
在Linux中,你可以使用多种方法来查看和修改进程的优先级。
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
PRI and NI
- PRI也还是比较好理解的,即进程的优先级(Linux的默认优先级是80),或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高(Linux优先级的范围是:[60,99]--->40。)
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
- 所以,调整进程优先级,在Linux下,就是调整进程nice值。
- nice其取值范围是-20至19,一共40个级别。
PRI vs NI
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
用top命令更改已存在进程的nice:进入top后按“r”–>输入进程PID–>输入nice值
eg:初始状态
我给nice的值为10
此时PRI的值就为90了
Linux调整优先级为什么是要受限制的
如果不加限制,将自己进程的优先级调整的非常高,别人的优先级调整的非常低。优先级高的进程,优先得到资源--后续还有源源不断的进程产生,导致常规进程很难享受到CPU的资源!这就产生了进程饥饿问题。
4.Linux的调度与切换
概念准备:进程在运行的时候,放在CPU上,直接必须把进程代码跑完,才行吗?
不对,现代操作系统,都是基于时间片进行轮转执行的。
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
对于进程切换的理解:
进程切换,就像我们在日常生活中更换任务或活动一样,是操作系统中非常关键的一个环节。当我们从一个进程切换到另一个进程时,操作系统需要确保前一个进程的状态被保存下来,同时加载并恢复新进程的状态。
具体来说,进程切换涉及几个关键步骤。首先,系统会暂停当前正在运行的进程,保存其上下文信息,这包括CPU寄存器的内容、内存管理信息以及程序计数器等。这些信息对于进程的恢复和继续执行至关重要。(进程在执行的时候,它的数据是保存在CPU的寄存器中的。)
接下来,系统会选择一个新的进程来运行。这个选择过程可能基于多种因素,如进程的优先级、系统的调度策略等。一旦选择了新的进程,系统就会恢复其上下文信息(进程在运行的过程中,要产生大量的临时数据,放在cpu的寄存器中!cpu内部所有的临时数据,我们叫做进程的硬件上下文),将其状态设置为就绪状态,并将其加载到CPU中。
完成这些步骤后,新的进程就可以开始执行了。而原来的进程则处于暂停状态,等待下一次被调度执行。
进程切换的开销是相对较大的,因为涉及保存和恢复上下文信息、更新系统数据结构等操作。因此,操作系统会尽量优化进程切换的过程,减少不必要的开销,以提高系统的整体性能。(执行 保存 恢复 再执行)注意:cpu内的寄存器只有一套,寄存器内部保存的数据,可以有多套!虽然寄存器数据放在了一个共享的cpu设备里面,但所有的数据,其实都是被进程私有的!!!
cup内所有的数据在任意一个时刻只属于一个进程,怎么理解?
在任意一个时刻,CPU内的数据都属于当前正在执行的进程。这是操作系统通过进程调度和CPU管理实现的,确保了每个进程都能独占地使用CPU资源,并且其数据不会与其他进程的数据混淆或冲突。
需要注意的是,虽然CPU内的数据在任意时刻只属于一个进程,但在多核CPU系统中,每个核心可以同时执行不同的进程。这种情况下,每个核心的数据仍然只属于它当前执行的进程,但不同的核心可以同时处理不同进程的数据。(寄存器!=寄存器的内容)
对于调度的理解:
这是一个cpu的运行队列(Linux实现进程调度的算法,考虑优先级,考虑饥饿,考虑效率)
蓝色区域(表示活跃队列):时间片还没有结束的所有进程都按照优先级放在该队列
queue[140]:它是task_struc*类型,task_struct* queue[140] 是一个包含140个元素的数组,每个元素都是一个指向 task_struct 的指针,每个指针指向一个进程。它分为0-99,和100-139连个部分。
如果系统使用优先级调度,那么0-99这部分的队列可能用于存储优先级较高的进程,为了保证用户的公平性,这个区段的进程优先级都差不多。
100-139这部分包含数组的剩余40个元素。这些元素可能用于存储另一种类型的进程或任务(优先级较低的进程,处于不同状态的进程:这些进程可能处于阻塞状态,等待某些资源或事件。后台任务:这些可能是系统后台运行的任务,不需要立即执行)
为什么这里的范围刚好是40?因为进程的优先级刚好有40个等级,cup调用优先级只需要根据优先级依次去调用就好了。
bitmap[5]:(bitmap)常用于快速检查一个元素是否存在于某个集合中,或者用于高效地表示和管理大量的二进制数据。具体到bitmap[5],他是int类型的,那么他就有160个bit位,可以管理160个位置的信息。
如果bitmap[5]的某个位被设置为1,这可意味着该位置的资源已经被占用;如果设置为0,则表示该资源是空闲的。队列调度算法在决定下一个要执行的任务时,可能会查看这个bitmap来确定哪些资源是可用的,从而选择能够利用这些资源的任务。
bitmap[5]本身并不执行调度算法,它只是调度算法在决策过程中可能参考的一个数据点。实际的调度逻辑会更为复杂,可能涉及多个因素,如任务的优先级、资源的需求和可用性、系统的当前状态等。
红色区域(表示过期队列):这个结构和蓝色区域的一样,他也是一个优先级数组
过期队列和活跃列结构一模一样。
过期队列上放置的进程,都是时间片耗尽的进程。
当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
下面是对上面结构的理解,有一个q的结构体,我们封装成结构体数组,array[0]表示蓝色区域,array[1]表示红色区域。当活跃队列的进程只需完毕后,交换两个结构体指针的指向就好了,让过期队列的进程得以执行,这样就以O(1)的时间复杂度,实现了对进程的调度