最近,我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念,而且内容风趣幽默。我觉得它对大家可能会有所帮助,所以我在此分享。点击这里跳转到网站。
目录
- 一、进程状态
- 1.1运行状态
- 1.2阻塞状态
- 1.3挂起状态
- 二、具体Linux中的进程状态
- 2.1看看Linux内核源代码怎么说
- 2.2进程状态查看
- D磁盘休眠状态(Disk sleep)
- T停止状态(stopped)
- 三、僵尸进程
- 3.1僵尸进程危害
- 四、孤儿进程
- 🍀小结🍀
🎉博客主页:小智_x0___0x_
🎉欢迎关注:👍点赞🙌收藏✍️留言
🎉系列专栏:Linux入门到精通
🎉代码仓库:小智的代码仓库
一、进程状态
进程状态在操作系统中可以分为五大状态,分别是:
- 新建状态:一个进程刚被创建,把PCB和对应的代码和数据刚申请出来,尚未进入就绪队列
- 就绪状态:进程已经分配到除CPU以外的所有必要资源,只要再获得CPU,便可立即执行,进程这时的状态称为就绪状态
- 阻塞状态:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态1。处于阻塞状态的进程也可能有多个,通常将它们排成一个队列,称为阻塞队列
- 运行状态:进程占有处理器正在运行的状态。进程已获得CPU,其程序正在执行1
- 终止状态:指进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态
1.1运行状态
当多个进程需要运行时,它们会竞争CPU资源。在极端情况下,如果只有一个CPU,那么众多进程就必须通过调度器来合理使用CPU资源,确保资源的均衡使用。为了管理这些进程,每个CPU都会维护一个自己的运行队列struct runqueue。这个运行队列是一个重要的数据结构,用于存储当前CPU上待运行的进程。
每个进程都有一个对应的task_struct结构体,这个结构体包含了进程的各种属性。而运行队列中最重要的属性是head和tail指针,它们分别指向队列的头部和尾部。这意味着排队的不是进程的代码,而是进程的PCB在排队。
当CPU需要运行某个进程时,它会从运行队列中挑选一个进程来执行。挑选的过程是由调度器完成的,它会根据一定的算法从队列中选择一个合适的进程。一旦选定了进程,调度器就会将该进程从队列中移除,并将其放到CPU上运行。
当多个进程处于运行队列中时,它们所处的状态被称为运行状态(r状态)。这意味着这些进程已经准备好,可以随时被调度执行。当一个进程开始运行时,它将自己置于CPU上并执行。然而,一个进程并不需要一直执行到完成才让出CPU。例如,如果有一个进程陷入了一个无限循环中,它可能会一直占用CPU,导致其他进程无法得到调度和执行。
为了防止这种情况的发生,操作系统引入了时间片的概念。每个进程都被分配了一个时间片,它定义了该进程在CPU上的最大执行时间。一旦一个进程的执行时间超过了它的时间片,操作系统会强制将其从CPU上移除,并将其放回运行队列的尾部等待再次调度。
时间片的引入确保了每个进程都有机会在一段时间内得到执行,从而实现了多个进程的并发执行。并发执行是指在同一时间段内,多个进程的代码都会被执行。通过合理分配时间片,操作系统可以确保所有进程都得到公平调度和执行,从而提高了系统的整体效率和响应速度。
1.2阻塞状态
我们每个外设都有一个等待队列,每个使用设备的进程,如果想要使用某个设备,就需要等待该设备变为就绪状态。如果设备当前不可用,即处于不可读状态,那么进程会自动将其PCB(进程控制块)加入到该设备的等待队列中。一旦设备变为可用状态,即处于可读状态,进程就可以从等待队列中取出PCB,进入就绪状态,等待CPU调度。如果设备仍然没有准备好,进程就会在等待队列中等待,这时我们称进程处于阻塞状态。因此,当我们说让进程去某个资源中等待时,实际上是将进程加入到该资源的等待队列中,也就是让进程进入阻塞状态。
系统中可能有很多阻塞的进程,每个都在等待资源或设备。进程间也可能互相等待。如果只有一个CPU,那同一时间只能运行一个进程,其他的都得等着。
1.3挂起状态
如果有很多进程都在等一个设备,比如磁盘,但磁盘一直没准备好,这些进程就只能在内存里等着,也不占用CPU。但有时候,操作系统的内存可能会不够用,因为每个进程都需要占用一些内存。如果一个进程只是等着,没有被CPU调度,那它的代码和数据其实在内存里是空闲的。为了解决这个问题,操作系统可以把等待进程的代码和数据移到磁盘里去,只留下一个PCB在内存里排队。这样,当设备准备好了,再把代码和数据从磁盘移回内存。这个过程叫做换出和换入。当代码和数据不在内存里时,我们称这个进程为挂起状态。这样操作系统就可以空出一些内存给其他进程用。这个挂起状态适用于所有等待的进程。
挂起状态对用户是不可见的,这是操作系统的一种行为。
上面介绍的这些属于操作系统学科的理论知识,不同的操作系统可能会有不同的实现方案,下面我们来深入研究研究具体的 Linux 操作系统中有哪些进程状态。
二、具体Linux中的进程状态
2.1看看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))。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
2.2进程状态查看
我们先来写一段代码观察效果:
int main()
{
while(1)
{
printf("hello linux\n");
}
return 0;
}
可以看到这里并不是R状态,而是S状态,这是因为当程序疯狂地进行打印操作时,你的设备可能并不处于可以直接写入的状态。因此,你的进程有很大可能性一直处于等待状态。你可能认为你的进程正在直接向设备写入数据,但事实上,操作系统内部,你的进程可能大部分时间都在等待I/O设备(如显示器)就绪。因此,你的进程状态一直显示为S状态,这个S状态实际上对应着我们操作系统原理中的阻塞状态。
我们再来把代码修改一下去掉printf
:
int main()
{
while(1);
return 0;
}
当去掉printf语
句后,程序就不再涉及I/O
操作,而是纯粹地运行计算任务。在这种情况下,进程的状态会变成R状态,也就是运行状态。这是因为没有I/O
等待,进程可以一直占用CPU进行计算,直到任务完成。所以,当没有I/O
操作时,进程就处于运行状态,也就是R状态。
查询结果中显示的+
表示该进程在前台运行,这意味我们此时在 bash 命令行输指令是不会有任何反应的,可以在输入指令的后面加上&
,此时表示让该进程在后台运行,要终止掉该进程只能通过指令kill -9 PID
给进程发送终止信号。
D磁盘休眠状态(Disk sleep)
如果一个进程处于D状态,我们也称之为disk sleep,在Linux系统中它也是一种阻塞状态,只不过这种状态在操作系统层面被称为深度睡眠。相对应的,我们之前提到的S状态可以被称为浅度睡眠。就像人们睡觉时有深浅之分,浅度睡眠可以被直接唤醒,比如被程序员或其他进程唤醒。
我们再来通过一个有趣的故事来了解D状态:
有一个进程要完成I/O操作,此时进程呢他那朝着远端的磁盘大喊了一声磁盘啊,我这有1GB的数据我把数据存到你存到磁盘的某个位置上,你帮我做一下吧然后呢,当磁盘听到这句话,磁盘慢慢悠悠的探出来个脑袋,就给进程说好的,那你把数据给我吧,我去帮你写,于是磁盘就抱着进程的数据去做写入了,磁盘进行写入时,这个进程它翘着二郎腿,悠哉悠哉的在这里等待,此时操作系统它从旁边路过,操作系统现在压力很大,不知道什么原因,反正整个计算机里,进程变得非常多了,而且每一个地方都特别吃资源,内存资源严重不足,当前操作系统火急火燎的在想办法,当他从这个进程旁边路过的时候,他瞥了一眼这个进程,觉得他很不爽,他把他能做的全做了,包括进程所对应的代码和数据能置换全置换了,所以操作系统一气之下啊,就对着这个进程说,你这个进程这么没眼色,你没看到当前内存已经被撑得快爆了吗?我已经马上快挂掉了,你还在这里翘着二郎腿,你还在等着呢,不要等了,所以操作系统直接把这个进程干掉了,(如果系统压力已经在内存辗转腾挪已经解决不了时操作系统就要开始动手杀进程了,所以操作系统是会杀掉处于某一些他自己判定不太重要的进程) 删掉了之后,此时这个刚刚在写入数据的磁盘也遇到了问题,当前他在写入时发现写到一半时突然发现磁盘空间不够了,这个磁盘就转过头,探出脑袋就对这个进程说进程啊,不好意思,我写入失败了,唉,进程呢,进程怎么不见了?那我数据没有写成功该怎么办呢我到底是再尝试一次呢还是我再等一等,我该怎么做决策呢,(有的硬件直接丢掉的有的给你再试着写一下,大部分都是掉直接丢掉),此时数据就被丢失了,丢失的数据非常重要。于是呢,法官就把操作系统,进程和磁盘一并带上了法庭,法官先审问操作系统说你怎么能杀掉进程呢?操作系统却说:“请问用户有没有赋予我管理他软件资源的权利?请问我杀掉进程是不是在特别极端的情况下杀掉了?请问我有没有履行我操作系统的职责?我的职责是保证系统不挂数据丢失和我有什么关系,我就是在做我操作系统该做的事情,如果你判我有罪了,那么请问下次如果再碰到这样的极端情况,那到底我还做不做,我如果不杀最后导致操作系统挂掉,第一,它的数据该丢还是会丢,第二,可能还会影响其他进程,那么这个责任谁来承担。”法官说:“唉,这话说的还挺有道理啊,是的,他只是履行了他的义务,而且他确实是在极端情况下做的这个事情”于是法官又把目光转移到了磁盘身上,对磁盘说:“你怎么能把人家数据丢掉呢?”此时磁盘就说:“法官大人不要怪我,在这件事情上我就是个跑腿的人家让我干啥就干啥我在写入的时候就已经告诉了对方,我可能会失败,我让他去等的我要给他结果的,我的工作模式从来向来都是这样,从我出生的时候就是这样,其他磁盘也是这样,如果你认为我有罪的话,那是不是我的兄弟磁盘,我的朋友磁盘也有问题,那是不是我们把储存的逻辑全部都改下,我就是个跑腿的,我怎么能决定这个数据写入失败该怎么办,数据被丢失是因为我还有其他事情要做,因为有其他进程也要让我写入”,法官听了之后觉得也有道理,那么就把视角转向了受害人进程的身上,反正操作系统没错,磁盘也没错,那就是你的问题了,在法官刚看向进程的时候,进程扑通一下就跪下来了对法官说:“法官我可是受害人啊,我是被杀掉的,我就是静静的坐在那里,人在家中坐,锅从天上来,我是被杀掉的,我怎么能有错呢啊”。法官此时一想,唉,他们三个好像说的都挺有道理,难道是我错了吗? (凡是有争议的地方,那么一定是因为制度设计不合理) 法官最后说算了算了,你们三个都回去吧,我完了去改改操作系统,法官就把进程的状态加了一个状态,那么这个状态就是只要这个进程当前正在有写入任务交给了磁盘,如果磁盘没有办法立马响应的话,需要进程等待这个进程绝对不能以浅度睡眠的方式运行即S状态,必须把自己设为D状态,从我们的源代码方式规定,D状态进程任何人都不能杀掉,包括操作系统 所以故事又来了,这个进程呢,又喊出来磁盘啊,磁盘说怎么又是你,进程说没关系,这次写吧,这次我不怪你了,所以呢,磁盘抱着数据就去进行写入了,那么当前这个进程翘着二郎腿在这里啊,嗑着瓜子,在这里等,一个操作系统路过了操作系统说你这个进程话还没说完,进程就亮出了一个免死金牌,一边看去,不要影响我,我还忙着呢,你现在是没资格杀我啊,所以操作系统一看行啊,有这回事就行反正我尽力了,人家不让杀他就不让杀我去那么去杀别的进程,反正对我来我做了我的工作,你没有被杀死那后果自己承担,所以当前进程的此时就处了一个状态叫D状态,当它进行等待时进程不可被杀死,这样的话就不会存在刚刚我们的这种问题了所以当磁盘把数据写完之后,返回来时告诉进程进程啊,你的数据写完了这个时候得到了结果之后,进程再把自己的状态由D状态恢复成R状态。
我们最后再来总结一下这个小故事:
在这个故事中,一个进程请求磁盘保存1GB数据。磁盘接受了任务,进程则等待。由于系统内存紧张,操作系统决定杀掉该进程以释放资源。磁盘在写入过程中发现空间不足,但进程已被杀死,数据丢失。法官审问后,决定给进程增加D状态,保证正在进行I/O操作的进程不会被杀死。这样,即使操作系统再次路过,也无法杀掉处于D状态的进程,避免了数据丢失。
D状态也是阻塞状态的一种
T停止状态(stopped)
T状态称之为暂停状态也叫做stop状态,我们来举一个例子:
int main()
{
while(1)
{
printf("hello linux\n");
sleep(1);
}
return 0;
}
此时我们的进程还是S状态.
接下来我们给进程发送信号来暂停进程kill -19 759
:
可以看到我们的进程被stop了,我们再来查一下进程状态
此时进程就变成了T状态,我们想让进程再次跑起来,可以给进程发送18号信号kill -18 759
:
可以看到程序又正常跑了起来,再来查看进程状态:
这里又变成了S状态,但是却少了+
这是因为我们将进程暂停启动之后进程就会变成后台进程。此时我们想要杀掉这个进程就只能使用kill -9
来杀死进程。
当一个进程处于T状态一般是不会接受信号,除了特殊信号,我们使用gdb调试工具调试的时候,设置断点,程序遇到断点停止时候就会处于t状态。
T
和t
的区别:
在进程状态中,“T”代表停止状态或常规暂停,而“t”代表追踪停止。二者的主要区别在于,“T”状态通常可以通过发送SIGSTOP信号给进程来使其停止,而这个被暂停的进程可以通过发送SIGCONT信号来继续运行。“t”状态主要发生在进程被调试过程中遇到断点时,此时进程会进入追踪停止状态。
三、僵尸进程
如果今天我们有父和子两个进程,父进程一直在运行,并且父进程不关心对应的子进程,而子进程直接退出之时,他并不是退出之后立马要将自己的所有资源全部释放,因为父进程没有还没有来关心他,那么此时操作系统就必须把子进程的状态一直给我维持着,直到我的父进程开始关心他,那么其中我们把这种已经死掉的,但是当前需要由父进程来关心,此时这个进程所维持的这种状态,我们称之为Z状态。
下面我们再来写一段代码验证一下:
子进程执行完5次打印后就处于 Z 状态,等待父进程来回收它的资源。处于 Z 状态的进程的相关资源尤其是 task_struct 结构体不能被释放。只有当父进程把子进程的相关资源回收后,子进程才能变成 X死亡状态。我们将这种处于 Z 状态的进程就叫做僵尸进程,如果父进程一直不来回收,那这种进程会长时间占用内存资源,造成内存泄漏。后面我们会介绍解决这种问题的方法(waitpid())感兴趣的老铁可以期待后续更新!
3.1僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
- 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏
四、孤儿进程
上面的例子中我们是让子进程先退出,父进程一直运行,接下来我们让父进程先退出,子进程一直运行,我们再来看看会有什么结果。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
using namespace std;
int main()
{
pid_t id=fork();
if(id==0)
{
int cnt =500;
while(cnt)
{
printf("i an child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
printf("child quit!\n");
exit(0);
}
else
{
int cnt=5;
while(cnt)
{
printf("i am parent,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
printf("parent quit!\n");
}
return 0;
}
我们可以看到父进程在执行结束后就只剩下子进程,为什么父进程不会处在 Z僵尸状态呢?答案是父进程也是 bash 的子进程,父进程在执行结束后,它的父进程 bash 会将其回收掉,并且过程非常快,所以我们我们没有看到父进程处在 Z僵尸状态。其次我们发现,当父进程结束后,它的子进程的父进程会变成1号进程,即操作系统。我们将父进程是1号进程的进程叫做孤儿进程,该进程被系统领养。因为孤儿进程未来也会退出,也要被释放。
🍀小结🍀
今天我们学习了"【Linux】探索Linux进程状态 | 僵尸进程 | 孤儿进程"
相信大家看完有一定的收获。种一棵树的最好时间是十年前,其次是现在!
把握好当下,合理利用时间努力奋斗,相信大家一定会实现自己的目标!加油!创作不易,辛苦各位小伙伴们动动小手,三连一波💕💕~~~
,本文中也有不足之处,欢迎各位随时私信点评指正!