🪐🪐🪐欢迎来到程序员餐厅💫💫💫
主厨:邪王真眼
主厨的主页:Chef's blog
所属专栏:青果大战linux
总有光环在陨落,总有新星在闪烁
++每日小感慨++
想看番,打游戏,睡懒觉,让我快点攒够下半辈子积蓄吧,然后就可以摆烂了。
注意:总结的思维导图在最后面,并发并行、前台进程、后台进程的介绍也在后面
进程状态概念
当一个程序被加载入内存,有了自己的PCB,他就成为了进程,为了更好的管理进程,就在PCB结构体中定义了一个变量status来描述进程的状态,从而对他们进行不同的管理。
该进程的状态标识本质就是一个整型。
看看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 */
};
这里出现了'R','S','D','T','t','X','Z'这些标志以及其所对应的数字,表示不同的进程状态,如"R"代表运行状态,用数字0表示。 不难发现他们都是二进制,表示进程没有运行的原因,而运行状态则是0,
在PCB中是以位图的方法存储这些数字的,所以我们也可以通过简单的位运算来对他们进行组合。
R运行状态
我们打开我们的任务管理器会发现其实电脑运行了几十上百个进程。
但是我们的电脑只有很少甚至是一个CPU,而进程的运行又需要CPU,所以哪个进程在CPU上跑,什么时候跑,要跑多久,这是个重要的管理问题。
这时我们就可以用"先描述,再组织"的思想进行分析,得出结论CPU可以把进程的PCB以队列的形式维护起来,遵循FIFO的原理,这个队列就叫运行队列,第一个来的就让他先跑,他跑完了就把它弹出队列,跑下一个进程(具体调度方法并不是队列这么简单,下面再说)。
我们现在也习惯把处于运行队列的进程都看做是运行状态。
++代码演示:++
这是个死循环,我们可以预测,当CPU执行这段代码时,他会一直处于运行状态
#include<iostream>
using namespace std;
int main(){
while(1)
;
}
事实上,我们的ps指令告诉我们,它就是运行状态 ,"+"表示该进程处于前台,
但我们也要想到一个问题,既然该进程是死循环,且已经开始运行,那CPU按理来说就无法再去执
行别的进程,所以我开的别的软件应该都直接闪退了,但其实并没有,这说明,这个死循环没有
一直执行,这里就不卖关子,我们民用的CPU基本都是分时CPU,他的特点是要保证各个进程
被尽量平均的运行,于是它有个时间片的概念,每个进程在单次CPU上运行的时间不能超过这个
时间片,所以当我们死循环运行时间一长,他就自动被CPU移下去了,但是CPU也知道他还需要继续运行,所以就会把它移到运行队列的末尾,让它继续排队。
有人或许会问"这个进程被停止了一段时间啊吗,我怎么没发现。
朋友,你还能反应CPU吗,等CPU能被人类反应到它的行为,那他就老的行将就木了
阻塞状态
我们来看下面的代码,这是个死循环,但是我们在里面加入了输入函数,显然运行到该语句时进程就会停止,等待用户输入,运行状态也会改变,
#include<iostream>
using namespace std;
int main(){
int a=0;
while(1)
cin>>a;
}
实际上他的状态确实改变了,改变成了"S+ ",这表示该进程处于阻塞状态,正在等待某种资源就绪。
问题是CPU为什么会怎么做呢?
CPU当然不会让进程就在他上面卡着,因为后面的进程还都等着呢!!所以他会被弹出,但是弹出之后呢?像上面一样放到运行队列的末尾?不不不,他现在还没准备好资源继续运行!所以我们需要在建立一个队列叫做等待队列,用于存放那些资源还没准备好的进程的PCB。
进一步思考:假设很多进程都在等待资源,比如有十个在等显示器资源,有十个在等键盘资源,有十个在等磁盘资源,那该怎么管理呢?很简单,把等待显示器的PCB连接成队列,然后接到显示器的PCB内部的某个指针,表示这是显示器对应的等待队列,其他依次类推。
这里我们也是再举个例子
#include<iostream>
using namespace std;
int main(){
while(1)
cout<<"666"<<endl;
}
运行结果是显示器以极快的速度打印"666",那么该进程状态是什么呢,相信认真听的老铁都没被骗到,其实是S+状态,我们视奸一下
原因很简单,要打印字符串就需要显示器资源,需要资源你就得等着显示器把资源给你,要是在该进程之前就有进程也申请了显示器资源你就得排队等着,所以等啊等啊等啊,你的进程状态大部分时间就是等待状态了,当然如果你运气好的话,多试个几十次,还是有机会测到该进程处于运行状态,就像你多试几次,说不定就从备胎上位了(bushi) 。
这个S状态是可中断睡眠状态,我们直接在前台ctrl+c或者kill -9就可以结束它
D深度睡眠状态
disk sleep状态,又称深度睡眠状态或者不可中断睡眠状态
我们知道OS是管理进程的,加入有一天你的内存告急了,那他就会开始杀掉那些看着貌似游手好闲进程,比如在你向磁盘写入大量数据时,因为要不停的和磁盘做IO,所以就要不断地等待磁盘资源准备好,所以你的进程就会一直处于等待状态,OS看到这个进程在内存危机时占用了大量的空间(因为要写大量数据),但是又游手好闲啥也不干(因为他在等待),所以一气之下就杀了他。
这下不要紧,要是磁盘把数据写了一半发现写入失败了,磁盘想向进程反馈这个事,但该进程已经结束了!!,它无法告诉任何人写入失败了,假如这是很重要的数据,那所有人都不知道它写入失败了,那这份信息就丢失了,后果不堪设想。于是OS就设置了D状态,对于某些进程OS会给他设置D状态(如网络IO,磁盘IO),这样就相当于给了一张免死金牌,保证他正常完成工作。
T暂停状态
表示该进程被暂停。
当OS发现某些进程在进行下去会对计算机有威胁,就可能直接暂停他,接下来如何处置交给用户发落,除此之外用户也可以直接暂停进程,通过kill -19 PID即可
该进程被暂停的同时,也会进入后台,我们可以通过kill -18 PID来让他再次运行,但他会依旧留在后台
t被追踪的暂停状态
我们之前学了gdb的调试,其中就有断点设置,当程序运行到断点时就会停止,但是这个停止是为了方便用于对程序进行调查的,所以被称为trace stop
++代码展示++
挂起状态
OS是用来管理进程的,我们直到再加载进程时要把代码和数据都加载进来,创建PCB结构体也需要空间,但是内存空间是有限的,当内存吃紧时OS就要想办法了,他会把处于阻塞状态的进程的代码和数据先放回磁盘,这样就可以省出一部分空间缓解内存压力。因为处于阻塞状态的进程暂时是不会运行的,所以我们就先让他的PCB在阻塞队列里排队就好,这个过程就叫换出。当该进程资源准备好了,就要被放到运行队列了,于是OS就会把它的代码和数据加载回来,这个过程就叫换入。
这个操作的好处是节省了内存,缺点是换入换出带来了时间上的开销,即用时间换空间,但这也没办法,内存吃紧了,再不想办法OS就直接挂了,那后果就严重多了。
Z僵尸状态
当一个进程结束时,他的代码和数据就从内存释放了,因为他们已经没用了,但是这个进程的PCB还不可以结束。我们思考一下,父进程创建了子进程一定是让它完成某项任务的,那子进程现在结束了,这个任务到底是完成了吗?你得把结果告诉父进程才可以。我们现在接触的程序大多是算法题、数据结构啥的,代码对不对不可以看最后显示器上的输出来分析,但是这些东西OS看不懂,附进程也看不懂,所以我们设置了一个叫退出码的东西,当子进程结束时就会把结束码存在PCB里,等着父进程查看。我们可以通过$?的指令查看上一个结束的进程的退出码
#include<stdio.h>
int main(){
printf("我死了\n");
return 11;
}
于是父进程就可以通过退出码直到子进程是否完成了任务,如果没有那失败原因是什么。
在子进程结束时,除了退出码,还有别的信息会被保存在子进程的PCB结构体里,等待着被父进程回收,于是这个子进程结束到子进程结束信息被回收期间的子进程的状态就是"僵尸状态"。
我们来验证一下
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
pid_t p= fork();
if(p==0)
{
int cnt=8;
while(cnt--)
{
printf("我是子进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());
printf("我还有%d秒\n",cnt);
sleep(1);
}
printf("我死了\n");
}
else
while(1)
sleep(1);
return 11;
}
while :; do ps -ajx | head -1 && ps -ajx | grep test2.exe | grep -v grep; sleep 1 ;done
可以看到,进行一段时间后,子进程结束,变为Z状态,后面的defunct表示死的,失灵的,不再使用的。
孤儿状态
父进程先结束,就剩可怜的子进程在那里运行着,但是子进程也有结束的一天,等他结束了就不会有父进程回收他的PCB,那他就会一直保持僵尸状态,那他的PCB所占用的空间就永远回不来了,为了防止这种情况发生,OS决定,对于孤儿进程,全部交给OS管理,即孤儿进程的父进程会变为OS,当孤儿进程结束,OS会回收其PCB相关信息
可以看到当父进程消失时,子进程 的PPID变成了1即OS,同时他的状态也变成了后台状态
X状态
死亡状态,进程开始僵尸进程, 接着父进程对其信息进行回收,然后进程就会进入死亡状态,这时的它就彻底结束了声明,但X状态存在时间极短,我们就不演示了,大家知道这回事就行
总结
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
番外:
关于前后台
一个终端只有一个前台进程,他可以和用户直接交互,用户在终端输入命令,进程接受到信息并将运行结果返回到终端。当前台进程在运行时会阻塞终端,用户无法在终端运行别的指令。
我们先打开了一个可执行文件,接着当我们在终端输入指令 ls时,ls并没有被运行,说明终端被阻塞
一个终端可以有多个后台命令,后台命令不与用户直接交互,一般不接受来终端的信号,他在运行时也不会阻塞终端。
想要让程序在后台运行,只需要./程序名 +&即可
对前台进程输入ctrl+z,会暂停前台进程并将其移入后台
并发并行
并发:单个CPU不断切换不同的进程,是他们看起来像一起在运行
并行:多个CPU,每个CPU执行的进程相互之前确实是一起运行,成为并行,要注意,多CPU下既有并发也有并行。
---------------------------------------------觉得有帮助的话就点赞支持一下吧 --------------------------------------