️作者:@malloc不出对象
⛺专栏:Linux的学习之路
个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐
进程状态是指操作系统对进程的运行状态进行的描述。操作系统通过跟踪进程的状态来控制和管理它们的执行。这些状态的改变是由操作系统内核调度器决定的,根据进程需要,进程可以在这些状态之间进行切换。进程的状态取决于它需要等待的事件和资源,以及操作系统内核的调度决策。因此,不同的进程可能会处于不同的状态。
在大多数操作系统中,进程可以处于以下三种状态之一:
- 就绪状态(Ready):进程已经准备好执行,只等待CPU资源。
- 运行状态(Running):进程正在CPU上执行指令。
- 阻塞状态(Blocked):进程由于等待某种资源(如I/O操作)而暂时无法执行,处于等待状态。
什么是阻塞?
进程因为等待某种条件就绪,而导致的一种不推进的状态。这就类似我们平时在下载一些东西时,网络有时候很差就导致进度条一直卡在那里,所以阻塞一定是在等待某种资源。
为什么阻塞?进程要通过等待的方式,等具体资源被别人使用完成之后,再被自己使用。
阻塞:进程等待某种资源就绪的过程。
如何理解这个概念呢?
挂起状态:挂起(等待,阻塞)进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作。它是一种特殊的阻塞状态。
在了解阻塞状态之后挂起状态就很好理解了。
不同的操作系统有不一样的进程状态,但是大体上是差不多的,进程状态之间可以相互转换。接下来我们将具体来学习Linux下的进程状态!!
/*
* 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 */
};
S休眠状态(可中断休眠),它本质上也是一种阻塞状态,等待某种资源就绪。
下面我们来看一个例子:
我们发现此时为什么它是一个S休眠状态?它不是一直往显示器上打印字符吗,难道不是处于R状态??
这是由于显示器是外设,我们频繁向外设打印字符,当外设未就绪时,进程处于阻塞状态,CPU是非常快的,不会等外设就绪之后调度进程。那为什么我们也看到它一直往显示器上打印字符啊,就算有时外设未就绪但是我们频繁的向外设打印字符,难道不能看到R状态吗?这还是因为进程就绪等待的时间远远超过了CPU的执行时间,CPU的执行时间极短,也许在打印很多次之后才可能会出现一次R状态,大部分时间主要还是在等待资源,所以我们认为此时处于S休眠状态。
我们将这段代码修改一下注释掉printf函数向外设打印字符,看看此时进程处于什么状态:
我在之前讲到S休眠状态它是一个可中断休眠的状态,下面我们就来演示一下:
kill -9 pid # 杀掉进程
D状态是深度休眠状态(不可中断休眠状态),平常我们几乎看不到这种休眠状态,它常出现在高 IO、高并发的场景。
下面我们来了解一下这种状态:
那么问题来了,如果这是一个银行系统,这1GB数据中是保存着用户的转账信息等,此时大量用户的重要信息全部被删除了,这就导致了非常严重的问题。所以为了解决这个问题,我们的设计者最后将该进程设置为深度休眠状态,谁也无法杀掉它,它只能通过断点或者进程自己醒来来解决!!
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
kill -19 pid # 停止pid进程
kill -18 pid # 恢复停止进程的信息,使其继续执行
S和S+是两种不同的进程状态,它们之间的区别如下:
S状态:表示进程处于睡眠状态,等待某些条件的发生,例如等待I/O操作完成或等待某个信号量。
S+状态:也表示进程处于睡眠状态,但是它在等待资源时处于优先级高于S状态的状态。通常情况下,S+状态的进程正在等待CPU资源,例如正在等待某个进程释放CPU。
可以将S+状态看作是S状态的一个子集,即所有处于S+状态的进程都是处于S状态的进程,但并非所有处于S状态的进程都是处于S+状态的进程。换句话说,S+状态只是对S状态的一个进一步细分。
需要注意的是,进程状态不是固定的,它们可以随时改变,取决于进程当前的状态和操作系统调度器的调度策略。例如,当进程等待的条件得到满足时,S状态的进程可以被唤醒并切换到就绪状态,而S+状态的进程可以降级为S状态。
这里关于S+与S状态,我们就简单来理解:S+是在前台运行,S是在后台运行。
Ctrl+C可以用于终止前台进程,在Linux终端中,前台进程指的是当前正在运行的进程,它会接收到终端输入和信号。当您在终端中按下Ctrl+C时,它会发送一个终止信号(SIGINT)给当前的前台进程,让它终止执行。Ctrl+C只能终止前台进程,如果您希望终止后台进程,您需要使用其他命令或工具,例如kill命令或者使用工具如htop或top来查找进程ID并发送信号。此外,一些进程可能会忽略SIGINT信号或者捕获它们,并执行自己的信号处理程序。在这种情况下,您可能需要使用其他的信号或命令来终止进程。
下面我们来使用kill命令杀掉S状态的进程:
T状态与S状态的特点:
1.T状态的进程不能自己唤醒,只能通过外部事件来唤醒。这也就意味着,如果一个进程一直处于T状态而没有任何外部事件来唤醒它,那么它就会一直处于T状态,无法继续执行。
2.T状态的进程通常是由于调试或者程序bug导致的,比如在gdb中断点停止执行,或者程序代码中有死循环等。而S状态的进程则是由于等待某些事件而暂停执行,是一种正常的进程状态。
3.T状态的进程在被唤醒后,会继续执行中断前的指令。而S状态的进程则会从等待的事件处继续执行。
4.T状态的进程不能被kill -9强制终止,只能通过kill或者其他信号来终止。这是因为T状态的进程在等待事件时,会被系统记录在一个等待队列中,kill -9命令无法直接中断等待队列中的进程。
总之,T状态和S状态都是进程在不同状态下的表现,它们的区别主要在于进程处于何种状态、如何被唤醒以及终止进程的方式。
追踪暂停,断点处停下来本质上就是让进程暂停。它也是一种特殊的暂停状态。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
- 僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程,僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
- 僵尸进程:一个子进程在其父进程还没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
在讲僵尸状态之前我们先来引入一个话题:为什么我们要创建进程?
因为我们想要进程帮我们办事,那对于操作系统和用户来说,肯定会关心该进程把任务完成的如何,而退出码就可以作为我们检验结果的一个标准,我们通常在main函数最后中return 0,0其实就是退出码,假设你设计了一个算法为了完成某项任务,成功返回0,失败返回-1,此时我们通过查看退出码就能判断是否成功的完成了任务!!在Linux中我们可以通过
echo &?
来查看该进程的退出码。
如果一个进程退出了,立马进入X状态,立马退出,你作为父进程有没有机会拿到退出结果呢??
答案是没有,所以在Linux中当进程退出时,一般进程不会立马彻底退出,而是要维持一个状态Z(僵尸状态),它是为了方便后续父进程读取子进程退出的退出结果!!
下面我们就来演示一下出现僵尸进程的场景:
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 僵尸状态会造成内存泄露,我们要采取其他方法进行回收。我们将在进程控制那块进行讲解。
孤儿进程:孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。这些孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
接下来我们来看一个例子:
在我们不采用任何回收机制的情况下,任何进程退出都要成为Z(僵尸)进程,那么请问这里我们杀掉父进程之后,为什么父进程没变成僵尸进程??
这是因为它对应的父进程为bash,bash会自动回收它的子进程!!而且细心的读者也发现了我们在之前只有一个进程退出的时候,是直接退出了不会出现僵尸进程,这都是因为bash帮我们自动回收了。
Q:为什么我们的孤儿进程要找一个新的父进程??
当我们的孤儿进程退出之后它是不是也会变成一个僵尸进程?如果没有父进程在它退出后回收它,此时它就占用了资源 -->内存泄露,所以它一定要找一个新的父进程在它退出时对它进行回收!!
我们将这个过程简单的类比一下:孤儿进程是没有父进程的进程,所以在孤儿进程退出时这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害 。
关于孤儿进程的用处,后续我们在多线程那块再来提及。
基本概念:cpu资源分配的先后顺序,就是指进程的优先权(priority)。优先权高的进程有优先执行权利,配置进程优先权对多任务环境的linux很有用,可以改善系统性能,还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
优先级和权限的理解:权限是能不能的问题,而优先级是谁先谁后的问题。
为什么要存在优先级?
因为CPU的资源有限,但是我们的进程可以有多个,所以必定会存在多进程之间竞争资源的情况,只要出现竞争那么就一定要确认谁先谁后执行,所以就有了进程优先级。
ps -al # 查看系统进程
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
- PRI即进程的优先级或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
- 那NI呢? 就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值。
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
。在Linxu中PRI(old)
默认为80
!!这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值!!- 当然了,实际上我们很少去调整进程的优先级,因为我们的调度器的任务是尽量让每个进程的调度保持持平的状态,它不能太偏向某个进程,因为这样可能会导致其他进程无法进行调度;另外一个原因是调整进程优先级的代价比较大,所以我们实际上很少去调整进程的优先级!!那么既然不能将某个进程的PRI修正的太小或太大,所以我们的nice值是一定有限制的!!
- 在Linux中nice其取值范围是
-20至19
,一共40个级别。
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进
程的优先级变化。可以理解nice值是进程优先级的修正修正数据。
sudo top --> r --> pid --> nice值 q退出
下面我将nice值设为-20,则PRI经过修正后就变为了60,PRI(new)=PRI(old)+nice;如果你设定的nice超过了范围[-20, 19]则会取它的边界值,如:nice = -30,实际上取的是20
下面我们继续来设定一下nice值调整进程优先级:
我们之前的PIR为60,我们设定nice为100,但是最多只能修正到最大范围19,那么修正之后的PRI为79?
从图中我们可以看到PRI为99?这是为何?根据公式:PRI(new)=PRI(old)+nice,这里我们的PRI(old)在Linux中默认为80,所以每次计算都是从80开始计算的!!
- 竞争性: 系统进程数目众多,而 CPU 资源只有少量,甚至 1 个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
- 并行: 多个进程在多个 CPU 下分别,同时进行运行,这称之为并行。
- 并发: 多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
注:CPU 调度进程的方式,并不是将该进程执行完再去执行另一个进程,而是采取时间片轮转的方式来调度进程的。也就是将时间段切分为很多的小时间片,这个时间片来调度这个进程,这个时间片过后,该进程执行完毕或者重新到运行队列中排队。到了下一个时间片,就会去调度另外一个进程。这样,在一段时间内,多个进程都能够得以推进,这也就是并发。
本篇文章的内容就到这里了,如果有任何疑问或者错处欢迎大家评论区相互交流orz~