目录
前言
一、进程概念
二、如何创建一个进程
三、进程状态
3.1运行状态&休眠状态:
3.2前台与后台状态
3.3磁盘休眠状态
3.4暂停状态
3.5调试状态
3.6僵尸状态
3.7一种特殊的进程状态——孤儿进程
总结
前言
在博主的上一篇文章中(点我查看),解释了有关冯诺依曼体系和操作系统的管理问题,当时在文末博主只解释了操作系统是以何种姿态来管理进程,但并没有解释什么是进程,本文博主就来解释一下有关进程的几个问题。
一、进程概念
在介绍进程之间,我们需要了解这样一个事实:对于现代PC机来说,某一时刻往往可能有多个进程在运行,即使你开机之后什么也不干,操作系统和一些驱动程序也会强制运行。所以,此时操作系统作为第一个开机后“苏醒”的程序他如何来管理之后醒来的程序呢?在博主的上一篇文章中已经提到——“先描述,再组织”,在对进程进行描述时,我们知道单一的数据类型是难以将管理对象描述清楚的,所以操作系统使用了一个名为task_struct类型的结构体来对管理对象进行描述,这个结构体在操作系统上被叫做PCB(进程控制块)。它通常需要包含以下信息:
1.唯一标识符(pid)
2.状态:任务状态、退出码等
3.程序计数器:用来指认下一条需要执行的语句
4.内存指针:用来指向进程需要的数据、代码等
5.上下文数据:用来记录此时进程的执行状态
6.记账信息:用来记录进程消耗的时间信息,以保证时间片能正确轮转
7.I\O信息:包括显示的I\O请求、使用的文件列表信息、分配给进程的I\O设备等信息
8.其他信息
这里先对来说比较重要的就是操作系统是如何处理上下文数据的,我们要搞懂进程就要先解释以下这一点,我们首先要了解一下什么是上下文数据,比如说一个程序包括但不限与以下几种情况:程序正常运行结束、程序运行时间片到期程序被迫中断等,对于第二种情况,程序等待下一次被分配时间片时需要从上一次执行结束的地方继续执行。这时候就是上下文数据发挥作用的时候——当程序时间片到期时,CPU会将程序的执行状况从CPU的寄存器中拷贝回进程的PCB中,这些数据数据就是这个进程的上下文数据,等到下一次该进程再一次被分配时间片,上下文数据从PCB中拷入到CPU寄存器中,这样就可以接着从上一起程序离开的地方在接着执行。
PCB中的其他信息将在后面一一说清,但是仅仅有PCB对于一个进程来说是远远不够的,因为PCB还需要进程的代码和数据。所以一个真正意义上的进程应该是这样的:
二、如何创建一个进程
那么在Linux中是如何创建一个进程的呢?
我们可以使用系统调用接口——fork(),来创建一个子进程:
我们可以使用系统调用接口来观察进程的pid信息,也可以使用shell命令观察,我们先来看一下getpid()接口的文档:
我们从文档中不难看出,getpid会返回该进程的ID信息,getppid则会返回父进程的ID信息,我们使用一下这两个接口,需要注意的是fork之后会有出现一个子进程,子进程和父进程共享代码,此时,也许你会有这样的疑惑为什么id这个变量既可以大于0又可以等于0,关于这一点,现在你只需要知道:fork之后子父进程共享代码,fork会返回两次数值,如果是子进程则返回0,如果是父进程则返回其ID值即可,关于为什么,不是本文讨论的话题,之后博主会再写一篇文章来解释这个现象。
三、进程状态
在一些书本上,我们可以看到一些有关进程状态的图文,比如:
但是这样的描述过于泛泛,我们来详细的看一下Linux中的进程状态:
1.R运行状态 (running) :程序正在运行或处在运行队列中的状态
2.S睡眠状态 (sleeping) :程序等待某些资源而陷入的等待状态
3.D磁盘休眠 (disk sleep) :又叫不可中断睡眠,这一状态的结果往往是等待I\O结束
4.T暂停状态 (stopped) :可以通过发送SIGSTOP来实现,可用SIGCONT恢复
5.t调试状态 (tracing stop) :可以被追溯的暂停状态6.X执行结束 (dead) :程序执行返回
7.Z僵尸状态 (zombie) :因为父进程没有接受子进程返回码的子进程状态
我们接下来一一介绍这几个状态:
3.1运行状态&休眠状态:
我们运行该程序:
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4 {
5 pid_t id=fork();
6 while(1)
7 {
8 if(id>0)
9 {
10 printf("id:%d\n",id);
11 printf("i am a father process! my pid=%d myppid=%d\n",getpid(),getppid());
12 }
13 else if(id==0)
14 {
15 printf("i am a child process! myp id=%d myppid=%d\n",getpid(),getppid());
16 }
17 sleep(1);
18 }
19
20 return 0;
21 }
并在命令窗口中使用一个监控脚本来观察这个程序的状态:
while :;do ps ajx |head -1 && ps axj | grep <可执行文件名> | grep -v grep ;sleep 1;done
那么,你猜猜此时代码的运行状态应该是哪一种呢??
我们运行程序并打开监控查看:
你也许会很惊讶,你可能觉得疑惑——为什么不是运行状态而是休眠状态呢?要解释这个问题我们要先知道,运行状态是对CPU而言的,CPU的速度极快,实际上程序的时间大多消耗在了等待输出设备相应这一过程上了,所以此时是S状态,那我们更改一下代码,再看看结果。
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4 {
5 while(1);
6
7 return 0;
8 }
则依次,主函数中只有一个循环,没有输入没有输出,不需要进行资源等待。此时就是一直处于运行态 。
3.2前台与后台状态
你也许会好奇,为什么状态后面有一个+,这个其实就是表示此进程在前端运行,在前端运行的状态是会被默认的Ctrl+c操作阻止的,而没有+表示进程在后端运行,此时无法通过默认的Ctrl+c操作阻止。在Linux中我们可以在./执行程序名后面加上&符号就可让程序在后台运行:
3.3磁盘休眠状态
由于磁盘休眠状态的发生前提过于刁钻,因此很难进行复现,这里简单解释一下:当计算机资源严重匮乏时,进程发出读写操作时,进程处于等待状态,然而此时由于资源紧张,操作系统会换出或终止一些正在等待的进程,则就可能导致了,进程控制读写操作时被强制终止,此时假设读写数据失败,返回进程时,磁盘缺发现进程终止,没有办法返回相应的信息。
为了避免上述情况的发生,当数据十分重要时,我们可以将D状态给进程,这样就操作系统再清理一些等待资源的进程时就会越过这些进程。所以此时D状态的资源一般是在等待某些I\O的结束。因为磁盘休眠状态一般是发生在资源不足时,所以想要捕获这个状态是比较困难的。因为资过分紧张时带来的卡顿和死机都会给捕捉这一状态带来困难。
3.4暂停状态
暂停状态我们可以使用SIGSTOP来实现一下,我们通过kill -l来查表发现SIGSTOP信号值为19
运行该程序:
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4 {
5 while(1)
6 {
7 printf("i am running!!\n");
8 sleep(1);
9 }
10
11 return 0;
12 }
~
3.5调试状态
调试状态就是程序再进行调试的状态,这一状态比较好理解就不在做演示了,读者可以自行使用gbd等工具进行实验。
3.6僵尸状态
僵尸状态就是由于主进程没有接收子进程返回的信息而导致的状态。
运行该程序:
#include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5 {
6 pid_t id=fork();
7
8 if(id==0)
9 {
10 sleep(5);
11 }
12 else
13 {
14 sleep(10);
15 exit(0);
16 }
17
18 return 0;
19 }
~
我们要考虑一下僵尸进程的危害:
我们这个程序显然不是实际场景中多进程的应用,一个进程被创建出来一定是要求完成一定的工作的,这个工作做的怎么样,好与不好,成功与失败,都要将结果返回给父进程来对特殊情况进行处理,Z状态存在的意义就是提醒进程需要被接收处理。倘若父进程不对子进程进行处理,那么子进程一直不退出,维护该进程的内核数据结构(PCB)也不会进行释放,进而导致了内存空间的泄漏问题。
3.7一种特殊的进程状态——孤儿进程
当子进程再运行而父进程先于子进程退出,那么该子进程会由1号进程进行领养。
注意:进程于进程之间是有独立性的。这一点将在进程间的关系的时候详解,也就是说父进程的退出并不会影响子进程。
由1号进程进行接管孤儿进程也是出于对进程资源回收进行考虑。
总结
本文介绍了进程这一特殊的数据,在之后还会对这些数据之间的关系进行解释。