1.操作系统学科中的进程状态
运行
时间片 - 维基百科,自由的百科全书 (wikipedia.org)
阻塞
一般被阻塞的进程是指等待某个事件的进程,例如该进程正在等待计算机释放释放或等待I/O操作的完成。
当我们执行一个含有scanf的可执行C程序时,会发现终端会停顿一下,等待我们用键盘输入字符,这个可执行程序等待键盘输入的状态就叫做进程的阻塞状态。
操作系统内存在管理各种硬件相关的数据结构对象,数据结构对象中包含等待队列,等待队列执行的一系列进程都处在阻塞状态。
挂起
进程的挂机状态很少见。当阻塞进程过多,造成内存不足时,操作系统会先将阻塞进程的代码和资源存储在磁盘,只保留PCB对象在队列中排队,这个过程叫做"内存换出",同理,当从磁盘中取回进程的代码和数据,这叫做"内存的换入"。当进程只有PCB对象在排队时,此时进程的状态被称为"挂起"。
2.1.2 操作系统之进程的状态(运行、就绪、阻塞、创建、终止)及转换(就绪->运行、运行->就绪、运行->阻塞、阻塞->就绪)_计算机的就绪状态-CSDN博客
2.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:运行状态
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
我们编写程序来看一下, 我们去掉循环中的打印。
S:等待状态(阻塞状态)
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
我们编写一个需要键盘输入的C程序。
大多数情况下,bash进程都在阻塞状态,等待用户输入指令。
S是浅度睡眠,可以被唤醒(杀死)。比如当程序执行到scanf函数时,可以直接Ctrl+C,中断进程。
D:阻塞状态(深度睡眠)
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
为什么进程要有Disk sleep状态呢?
因为当进程向磁盘写入数据时,需要写入一些非常重要的信息,比如涉及100亿元的1个G银行转账信息,需要等待磁盘写入完成并返回写入情况(磁盘是否写入成功等情况),如果在等待期间,操作系统因内存不足需要杀掉一些进程释放空间,一不小心将这条正在等待磁盘写入返回的进程杀掉,后果不堪设想。为了防止中断控制写入磁盘数据的进程,Linux操作系统会把正在写入的进程设置为D状态,处在D状态的进程,不会响应任何请求,包括操作系统要强制杀死该进程都不允许。
总结:处在D状态的进程不响应任何请求。
T:停止状态
T停止状态(stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
S状态和T状态有什么区别?
T状态可能也跟S状态一样,在等待某个事件的响应,也可能单纯地被另一个进程控制为暂停。
比如用gdb调试一个程序,打断点然后运行到断点处,该进程就处于T状态。(我们暂时认为T和t是一个状态)
X:死亡状态
X死亡状态(dead):这个状态只是一个返回状态,我们不会在任务列表里看到这个状态。表示进程已经终止。
Z(zombie):僵尸进程
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
cpp
#include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 //child
10 int cnt = 5;
11 while(cnt)
12 {
13 printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid (),getppid(),cnt);
14 --cnt;
15 sleep(1);
16 }
17 exit(0);
18 }
19 else
20 {
21 //father
22 while(1)
23 {
24 printf("I am father,pid:%d,ppid:%d,\n",getpid(),ge tppid());
25 sleep(1);
26 }
27 }
28 return 0;
29 }
我们编写上面的.c文件,执行,
fork创建的子进程在打印五次"I am child"后,会进入僵尸状态,等待父进程读取。
僵尸进程的危害
- 任何一个进程都要返回自身的执行状态,当该进程任务完成后,在进程销毁前要将完成信息返回给父进程,在此前见,该进程进入僵尸状态,直到交接完成信息给父进程才可被销毁。
- 维护退出状态本身就是维护数据,也就是维护进程的基本信息,所以task_struct(PCB)会继续占用内存,换句话说,父进程不回收子进程,子进程的PCB实例化对象一直都要占用内存。
- 一个父进程创建了很多子进程,就是不回收,就会造成内存泄漏。因为数据结构对象会一直占用内存。
孤儿进程
在操作系统领域中,孤儿进程 (Orphan Process)指的是在其父进程执行完成或被终止后仍继续运行的一类进程。孤儿进程 - 维基百科,自由的百科全书 (wikipedia.org)
我们编写一个父进程比子进程先销毁的程序。
cpp
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5 {
6 pid_t id = fork();
7 if(id == 0)
8 {
9 //child
10 int cnt = 500;
11 while(cnt)
12 {
13 printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppi d(),cnt);
14 --cnt;
15 sleep(1);
16 }
17 exit(0);
18 }
19 else
20 {
21 //father
22 int cnt = 5;
23 while(cnt--)
24 {
25 printf("I am father,pid:%d,ppid:%d,\n",getpid(),getppid());
26 sleep(1);
27 }
28 }
29 return 0;
30 }
生成对应的可执行程序并执行,如下,
我们发现当8682进程的原父进程结束后,父进程变为"1"。我们输入top指令回车,可以看到1号进程。1号进程其实就是系统进程,
所以当父进程比其子进程先被回收时,子进程一般会被系统的1号进程"领养"(不同的操作系统回收的进程可能不同),1号进程就变成了子进程的父进程。
注意:Ctrl + C不能终断孤儿进程,要用kill + 9 + 孤儿进程标识符。