欢迎来到博主的专栏:从0开始linux
博主ID:代码小豪
文章目录
- 进程与操作系统
- 并发与并行
- 进程的状态
- linux的进程状态
进程与操作系统
进程的状态可分为3种,分别是新建态,运行态,终止态,就绪态与阻塞态。状态之间的关系如下图所示:
并发与并行
一个cpu在一段时间内只能有一个程序运行,但是有人就会说了,我在使用电脑时,一边运行qq,一边听音乐,还能一边打游戏,这些程序都是同时运行的啊。一方面,这是由于现在的电脑大多数都是多核的,而另一方面,即使是单核的电脑也能同时运行多个程序,这是与并发和并行有关。
并行:多个进程可以由cpu同时调度
并发:cpu在单个时间内只能运行一个程序。
这两个概念看起来有点互相矛盾了,既然多个进程能同时调度,但是在单一时间内有只能运行一个进程,这不是互相矛盾吗?
我们现在的cpu每秒能运算惊人的上亿次,那么也就是说,在一个人眼无法察觉的时间内就能运算非常多次,比如,我们规定cpu每运行一个程序100ms,就切换另外一个程序开始运行,那么就会出现这样的情况
由于程序暂停的时间太短,以至于我们察觉不到,所以就导致在宏观角度上,这些程序是同时运行的,而在微观角度上,cpu在单个时间内(100ms)内,只能运行一个程序。
我们将这种单个时间称为时间片,在linux系统中,时间片为5ms到800ms,用户是察觉不到程序暂停的。
进程的状态
在linux系统中,进程被描述成一个task_struct的结构体,当一个进程开始时,linux就会创建一个task_struct对象,将该进程的各个属性就记录在task_struct当中,通过task_struct来管理,以一个例子为例:
我们假设现在有三个进程,分别为进程1,进程2,进程3,而linux分别用task_struct类型的对象:task1,task2,task3来描述它,在内存当中它们的关系是这样的。
现在,我们让进程1,进程2,进程3进入运行态,当进程处于运行态时,cpu会轮着处理这些进程(并发)。而进程是由操作系统来管理的,此时操作系统会创建一个运行队列,以控制cpu处理的进程。
控制的方法如下:将队列中的第一个task_struct对应的进程(比如task1对应进程1)调度给cpu处理,每隔一个时间片,就将运行队列中的task_struct放到队列的最后一位。
只要处于运行态中的进程,都会在这个运行队列当中。
有时候,我们需要让进程去进行IO操作,比如在代码中写了printf,或者scanf之类的系统调用函数,我们来思考一个问题,IO操作与cpu有没有关系?
答案是没有,IO操作是与硬件相关的操作,那么也就是说,当程序执行IO操作时,并不需要cpu处理,而是需要与硬件进行交互,而我们在前面的章节中降到了,管理硬件是谁的工作?没错还是操作系统。
当我们的进程需要与硬件发生交互时,此时进程不再需要让cpu进行处理了,这意味着,cpu不应该在花时间在该进程中。对应的,该进程不应该放在运行队列中,而是要让这个进程与硬件进行交互,等待这个交互结束时,再回到运行队列当中,这个等待进程与硬件交互的过程,就是进程的阻塞态。
由于我们的计算机通常会与多个硬件进行交互,因此操作系统会为每个硬件都提供该硬件对应的等待队列。
我们以一个例子为例:
假如进程1中有scanf,当程序运行到scanf时,该进程就会等我们的键盘输入,此时进程1就会被操作系统拖入到我们的键盘的等待队列中,当我们在键盘完成输入时,进程1才会从键盘灯等待队列回到运行队列,即从阻塞态回到运行态中。
这里我们得出一个结论:进程运行态与阻塞态的区别,是处于不同的队列当中!运行态的进程在运行队列,阻塞态的进程在等待队列。运行队列的进程等待cpu调度,等待队列的进程等待与外设的交互结束!
阻塞状态的进程有时还会被挂起,挂起进程的原因是由于,虽然阻塞状态的进程没有运行,但是它依然处于内存当中,会占用内存的资源,而此时随着进程的不断的增多,内存资源严重不足,处于阻塞状态的进程依然在等待外设的交互,此时操作系统就会将进程挂起。
挂起的操作就是将处于阻塞状态的进程从内存移动到磁盘swap分区当中,这个操作叫换出,这个磁盘的swap分区是专门用来存放挂起的进程。此时由于被挂起的进程不再占用内存空间,因此操作系统可以继续让进程运行(内存不足就不行了)。等设备的交互完成后,再将被挂起的进程移动回内存当中,这个操作称之为换入。
由于换入换出的本质是内存与磁盘之间做IO操作(将内存中的进程输出到磁盘中)。因此,换入换出的过程会比较慢,在用户的严重,就是程序运行变慢了!因此,服务器通常都不会让进程挂起,因为对于线上服务器来说,用户的使用体验是第一位的,用时间换取空间的做法并不明智,因此如果服务器的内存不够了,采取的做法是扩大服务器,或者优化算法。
linux的进程状态
linux的进程状态不止上面提到的五种,为了弄清楚linux的状态具体有多少中,博主在linux的内核代码中找到有关状态的定义:
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状态即运行态,指的是该进程正在处于等待cpu调度的运行队列。比如下面的代码:
#include<stdio.h>
#include<unistd.h>
int main()
{
int i=0;
while(1)
i++;
return 0;
}
我们将其编译生成二进制文件code,并运行,接着打开另外一个窗口,输入命令:
ps ajx | head -1; ps ajx | grep code
这说明,如果一个进程的状态为R,说明该进程处于运行状态。
S状态
接着我们将代码改一下:
重新编译并运行该程序。
可以发现,code程序正在不停的运行,而且向显示器输出i的数据。然后我们来查看一下code程序的进程状态
诶,code程序明明在运行,但是为什么它的状态显示的是S,而非R?我们先来搞清楚这个S状态又是什么状态呢?
S状态在linux中被称为睡眠状态(sleeping),但是实际上这个状态对应的是阻塞态,我们在前面提到了,当进程处于阻塞态时,其实是该进程需要与硬件进行交互时,操作系统为了不让该进程浪费cpu的资源(因为进程和硬件交互不需要用到cpu),将进程从运行队列,拖动到了硬件对应的等待队列。
比如在code当中,当程序执行printf时,此时是code程序与显示器进行做交互,所以每当cpu执行printf函数时,操作系统就会将code程序从R态,转换成S态。但是按照理论,这说明如果我们运气好,在cpu执行i++的时候,去查看进程的状态,那么是不是显示code的状态就是R态呢?是的,但是概率远低于出现S态。因为cpu的计算速度远高于内存与硬件设备做交互的速度,因此绝大多数的情况,code的状态都是S态。当然,你可以多试几次,会有可能出现R态的。
现在,我们已经弄清楚linux进程的两种状态了,一种是R态,一种是S态,R态代表该进程正在被cpu调度,S态则说明该进程处于与应交交互的过程中(比如向显示器输出数据)。
D状态
那么D态是什么呢?D态的全称为(disk sleep),sleep是睡眠的意思,说明D态也是睡眠状态。而disk则是磁盘的意思,那么disk sleep也就很好理解了,那就是进程与磁盘交互时,进程的状态。
那么为什么进程和磁盘交互就要搞特殊,与其他外设进行交互,状态用的是S,而磁盘则是D呢?
还记得我们前面说的挂起吗?当一个程序处于阻塞状态时,如果此时内存严重不足,则会将处于阻塞状态的进程挂起,以保持其他进程的正常使用,又或者出现更严重的情况,即操作系统选择将进程杀死,而非挂起。
但是如果进程正在与磁盘进行交互,特别是进程向磁盘写入的状况。此时如果杀死进程,那么向磁盘中写入的操作就会失败,而进程原本准备写入到磁盘中的数据就会消失,如果这个数据非常重要(比如用户数据),那么这将是一个灾难级别的事故。
因此与磁盘交互的进程状态被设为D,这个D像是一个免死金牌一样,可以让操作系统在需要杀死进程才能继续工作的情况下,去杀死其他的进程,而非D状态的进程。
由于D状态不好展示(因为一般情况下与磁盘交互的速度很快,很难查看到),如果一个进程长期处于D状态,那么此时就要考虑一下磁盘或者内存是否出现问题了。
T状态
T状态也称stoped状态,和sleep状态都属于是阻塞状态,但是和sleep状态有所不同,T状态是由用户决定的,比如一些程序正在执行我们不想要的情况,此时用户就能主动让程序进入T状态,处于T状态的程序不能执行任何代码,除非用户取消T状态。
我们接着运行刚才的code程序。然后打开另外一个窗口,输入:kill -19 [pid]
,就能让程序处于T状态。
这个kill -19的意思的就是向进程发送暂停信号,使进程进入T状态。
可以发现,输入kill -19命令后,code程序从S+变为了T状态,此时在前台也能看到code程序显示stoped,已表明程序已经被暂停了。
如果我们想要让code恢复运行,此时输入kill -18 [pid]
即可
SIGCONT是让被暂停的程序恢复到运行状态,但是要注意,恢复的进程默认进入后台运行,此时我们无法使用[ctrl +c]的快捷键关闭该进程,因为该快捷键只能关闭处于前台的当前运行进程。如果想要关闭处于后台的进程,我们只能使用kill -9命令。
这就好比windows当中我们打开音乐播放器时,此时该播放器是属于前台运行的,我们点击右上角的‘×’,就能将该进程关闭,而如果我们将该播放器放在了后台运行,此时想关闭就要用任务管理器了,或者将其调到前台再关闭。
** t状态**
t状态则是调试暂停状态,比如我们用gdb调试一个进程时,此时进程若是来到断点处,该进程则的状态则是‘t’状态。这里不多展示
X状态
X状态又称“dead”状态,即死亡状态,当一个进程结束时,操作系统需要对其回收以及其他操作,此时的进程状态就会变成X,由于X状态持续时间非常短,因此博主无法向大家展示这个内容
Z状态
Z状态又称“zombie”状态,即僵尸状态,其实和X状态类似,处于Z状态的进程已经结束了,但是由于某个操作未完成,因此还不能消失。那么这个操作是什么呢?
我们先来了解一下一个正常“死亡”的进程需要干什么事才能正常死亡,首先该进程已经执行完成,或者被我们kill了,该进程就会结束,此时它要完成一下事情:
(1)将进程占用的内存回收
(2)向父进程发送结束信息
由于(1)是由系统完成的,所以基本上不会发生不回收的事(如果发生了我们也束手难测,不是吗?)。
而向父进程发送结束信息则是写在main()函数的return的返回值。比如输入echo $?
,我们可以查看最近一个进程的结束的信息。比如:
结束信息可以让父进程根据子进程的结束信息来判断子进程的执行结果,这也是为什么我们在写C语言代码时,经常在程序正常退出时,要写一个return 0,通常来说,正常退出时,结束信息都是0,而错误退出时,结束信息为非。
那么如何查看到进程处于“Z”状态呢?前面说了,当结束信息没有发送给父进程时,进程就会处于僵尸状态。所以我们可以写一个程序,在程序运行的过程中,fork出一个子进程,然后让子进程先结束,且父进程不接收子进程的结束信息,此时由于子进程的结束信息没有发送出去,一直处于“Z”状态。
代码如下:
运行该代码
查看testzombie_proc的进程信息
此时子进程处于Z状态,而且<defunct>的意思是该进程已经不起作用了,已经结束了。
我们将这种处于Z状态的进程称为僵尸进程,这种僵尸进程其实并不好,这是因为,一个进程结束,系统会对其做两种事,(1)将进程的内存空间回收(2)将该进程对应的task_struct回收。而僵尸进程虽然被回收了内存空间,但是由于其结束信息一致没有发送给父进程,此时系统会保留下该进程的task_struct,而这个task_struct是会占用内存空间的,因此僵尸进程会导致内存泄漏的事故!!!
当然,解决僵尸进程可以用wait()函数解决,但是博主打算后面再说。
** 孤儿进程**
我们都知道,一个子进程在结束时,需要向父进程发送结束标志,否则就会导致僵尸进程的出现。那么如果父进程先结束了呢?
那么我们将代码修改一下,让子进程进入循环,而父进程直接结束。
运行改代码,然后查看该进程信息
可以发现,如果父进程比子进程结束的还快,为了让子进程的结束信息能够成功返回,避免让子进程变成僵尸进程,系统会将子进程的父进程给改了。这种进程则是孤儿进程。