目录
二十七步天注定
📖1、进程的状态
进程状态是指在操作系统中,一个进程所处的不同运行状态,进程状态就决定了该进程接下来要执行什么任务。常见的操作系统中的进程状态有以下几种:
📖2、运行状态
我们生活中的绝大部分电脑都是只有一个CPU的,而进程运行起来需要把进程放到CPU上,但是我们的进程是有很多个的,这就导致很多个进程抢一个CPU,为了维护进程的秩序,CPU会搭建一个队列,让所有的进程想要在CPU上运行必须先进行排队
(参与排队的是我们进程的PCB对象,因为PCB是可以指向代码和数据的)
如下图->:
所有在CPU维护的队列中排队的进程,它们所处的状态叫做 " 运行状态(R状态) "
📖3、时间片的概念
我们刚才提到了运行状态,但是CPU是等一个进程运行完毕才运行下一个进程的吗?
当然不是,如果一个进程是死循环,那后面的进程岂不是都不用运行了
因此,提出了 " 时间片 " 这种东西,时间片是操作系统进行任务调度算法的一种思想,即CPU给每一个进程分配一定量的时间去执行,当这个时间过去了,操作系统就会中断当前的进程,把过了时间片的进程放到就绪状态(下面讲),接着将CPU分配给下一个进程。
( 即每个进程都有各自的时间片,时间片结束后,就被操作系统放到就绪状态中 )
时间片一般为10ms,所以一定的时间内所有的进程都会被执行,我们将这种情况叫做并发执行
在时间片结束后会有大量的进程从CPU拿下或者拿上CPU,这种方式叫做进程切换
📖4、就绪状态
当我们进程在时间片内结束后,会被操作系统从CPU的运行状态队列拽到就绪状态队列中,在就绪队列,其代码和数据都已准备好,只要获得 CPU 资源,就能够立即开始执行
所以只要操作系统重新将就绪状态中的进程传给CPU,就可以马上重新调度进程,所以并不是给进程杀掉了,最直观的感觉就是我们的进程并不会打开一下马上就被关掉了
📖5、阻塞状态
最常见的阻塞状态就是一个进程需要通过键盘读取数据,当一个进程等待从键盘输入的过程,此时该进程就处在阻塞状态
回顾一下,操作系统对于软硬件的管理是" 先描述,再组织 ",因此操作系统会有一个管理硬件的结构体,并针对不同的硬件创建出不同的结构体
阻塞状态队列其实就是操作系统对硬件的结构体中创建的一个等待队列
那么我们的进程在等待从键盘输入时,操作系统就会把进程的PCB对象从运行状态队列拿到阻塞状态队列中,如下图->:
注意: 结构体对象是操作系统的,软硬件无法对结构体创建出结构体对象
小Tips:操作系统中的等待队列可能有成百上千个,不仅每一种硬件有等待队列,进程中也有等待队列,可能会出现一个进程等待另一个进程结束后才能继续运行。不同的操作系统,调度算法也会不同。
📖6、挂起状态
当一个进程处于空闲状态时,也就是不需要去运行或者说该进程从来没有被使用过,那么当内存空间所剩无几时,操作系统就会把这些没有被 CPU 调度的进程的代码和数据先放到磁盘中存储,只留进程的 PCB 对象在运行队列中排队,这种进程就处于挂起状态。
当这个进程需要运行时并且PCB排到它时,它的代码和数据就会从磁盘中重新拿到内存中
小Tips:挂起状态对用户是不可见的,这是操作系统的一种行为。
📖7、Linux中的进程状态
上述所讲的进程状态都是操作系统的进程状态,我们同样需要掌握
我们来看看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 */
};
我们接下来讲解主要的几个->:
📖8、查看进程状态(S状态)
新知识:
- 可中断睡眠(S 状态):进程等待事件发生,期间可被信号唤醒。比如调用
sleep
函数的进程进入此状态,若收到信号,会被唤醒处理信号,然后决定是否继续执行或做其他操作。
想要查看进程状态,我们可以使用这样的命令->:
ps axj |head -1 ; ps axj | grep 可执行程序名称
先来看看下面这段代码执行起来后的进程状态
int main()
{
while(1)
{
printf("你好\n!");
}
return 0;
}
我们会发现,明明运行了啊,怎么显示的是S睡眠状态呢?
我们把输出去掉再看一看
int main()
{
while(1);
return 0;
}
可以发现,这次变成了运行状态,为什么?我明明两次都是运行啊
原因是,我们的CPU执行的速度是很快的,但是printf是要去调用显示器设备的,而显示器设备需要决定哪个进程先显示或者后显示,这就导致显示器设备像键盘一样也有一个等待队列,只不过具体功能不一样,所以相对于CPU来说,该进程的大部时间都在显示器的等待队列里等待显示设备就绪(因为CPU执行进程的速度远大于硬件),因此最终查出来的进程状态是 S睡眠状态
当我们去掉 printf 之后,该进程就不会去访问显示器设备,始终都在运行队列里,所以最终查出来的进程状态是 R运行状态。
小Tips:查询结果中显示的+表示该进程在前台运行,这意味我们此时在 bash 命令行输指令是不会有任何反应的,可以在输入指令的后面加上&,此时表示让该进程在后台运行,要终止掉该进程只能通过指令kill -9 进程PID
。
📖9、D磁盘休眠状态(Disk sleep)
D状态也是一种阻塞状态,在 Linux 系统层面我们称作深度睡眠,S状态称作浅度睡眠。浅度睡眠是可以被唤醒的,即可以响应外部的变化,我们可以通过 kill 指令(其他进程)将浅度睡眠的进程终止掉。下面通过一个情景剧来给大家介绍为什么要有 D 状态,以及 D 状态的作用。
当一个进程A要向磁盘中写入数据时,进程A向磁盘写入数据时,会发起系统调用,将数据从用户空间复制到内核空间的缓冲区,然后由内核负责将数据写入磁盘。在这个过程中,进程需要等待磁盘完成写入操作,因为它需要知道写入是否成功,以便进行后续的处理。而磁盘的写入速度相对较慢,与内存和 CPU 的速度存在较大差距,这就导致进程大部分时间都在休息。那么这时操作系统发现内存已经快满了!必须作出处理,恰好发现进程A正在空闲,于是操作系统为了保护内存防止内存中的进程全部崩溃,果断的给进程A杀掉了(关掉了),这就会导致写入磁盘的数据没有全部写完,因为进程A都没了,往磁盘中传入的数据只传了部分,就导致了数据丢失
那么,这次事故的问题出现在谁?
操作系统吗?不,操作系统为了保护内存并没有错
进程A吗?不,进程A是在为了后续更稳定的数据处理
磁盘?磁盘就是个接收数据的它能干什么错
那么,人类搞了一个D状态,也就是不能被操作系统杀死。
当一个进程处于 D 状态的时候,它不会响应任何请求,任何人和操作系统都不能将该进程 kill 掉。
小Tips:结束掉 D 状态的方法有两种,一是等待某个条件满足,如等待数据写完,二是直接断电。如果被用户查到 D 状态的进程,那就预示着这个操作系统离崩溃不远啦(因为内存快满了)。所以 D 状态会有,但是一般出现的时间都非常短。
📖10、T/t 停止状态
在 Linux 内核源代码中我们可以看到连个 T 状态,一个是 T ,一个是 t,我们可以认为这两个 T 状态是一样的,对于一个进程,我们可以通过下面这条指令将它设置成停止状态。
kill -19 进程PID
停止状态顾名思义,就是将一个进程停止
可以通过下面这条指令来结束停止状态。
kill -18 进程PID//
小Tips:结束停止状态的进程会到后台运行,要终止掉这个进程只能通过 kill -9
指令。T状态和S状态很像,其中S状态的进程一定是在等待某种特定条件,而T状态的进程可能是在等待某种特定条件,也可能是在被其他进程控制。我们在打断点调试一段代码的时候,该进程就会处于T状态。
📖11、僵尸进程
一个进程在退出时并不是立即将自己所有资源全部释放,当一个进程退出时,操作系统会把当前进程的各种信息维持一段时间,这个状态就叫做 Z 僵尸状态。
举个例子:维持信息是给关心它的“人”,也就是父进程来查看的。如果父进程一直没有来关心退出的子进程,那么这个子进程将长时间处于 Z 状态。
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("我是子进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
_exit(0);
}
else
{
while(1)
{
printf("我是父进程,PID是:%d,PPID:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
上面这段代码在 process 进程中通过调用 fork 接口创建了一个子进程,子进程在执行完五次打印后就会被终止掉,其中的 exit 函数就是用来终止一个进程,父进程将一直运行。
可以看见,子进程结束后,父进程一直运行,父进程不结束就不会去看子进程的信息,所子进程就会一直处于僵尸状态
僵尸进程的危害
1. 进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就将一直处于 Z 状态。
2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在 PCB 对象中,换句话说,Z状态一直不退出,PCB一直都要维护。
3. 一个父进程如果创建了很多的子进程,就是不回收,会造成内存资源的浪费,因为 PCB 对象本身就要占用内存。
4. 造成内存泄漏,因为进程的信息一直被维持着
📖12、孤儿进程
孤儿,顾名思义,就是没有父进程的子进程
上面我们是让子进程先退出,父进程一直运行,接下来我们让父进程先退出,子进程一直运行,看看会有什么结果。
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int cnt = 500;
while(cnt)
{
printf("我是子进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
_exit(0);
}
else
{
//父进程
int cnt = 5;
//这里的cnt是5,意味着父进程会先执行结束
while(cnt--)
{
printf("我是父进程,PID是:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
}
}
return 0;
}
可以看到父进程在执行结束后就只剩下子进程,为什么父进程不会处在 Z僵尸状态呢?答案是父进程也是 bash 的子进程,父进程在执行结束后,它的父进程 bash 会将其回收掉,并且过程非常快,所以我们我们没有看到父进程处在 Z僵尸状态,父进程也是存在僵尸状态的。
其次我们发现,当父进程结束后,它的子进程的父进程会变成1号进程,这个1号进程就是操作系统。我们将父进程是1号进程的进程叫做孤儿进程,该进程被操作系统领养。因为孤儿进程未来也会退出,也要被释放,所以它需要被领养。
小Tips:所有的进程只对它的“儿子”,即子进程负责,不会对它的孙子进程负责。
所以当一个进程的父进程结束后,会把该进程交给操作系统,让操作系统来充当它的父进程。