理解Linux的进程状态

[本文由2012年的一篇文章翻译而来,详见原文]

进程是一个动态的概念,是应用程序当前正在运行的一个实例。和进程相关的属性很多,列举几个来说,如所有者,nice值,SELinux的上下文信息等,这些信息对其访问其他资源的能力进行扩展或者限制。在进程的整个生命周期中,它会处于不同的状态。

在开始介绍进程的各种状态之前,我们可以通过一个人的生命周期来和Linux进程进行类比。每个人的生命都有不同的阶段,一个人的生命以其父母将其生出来作为起始(就好比一个进程被其父进程forked出来一样)。

出生之后,人类开始在其所在的环境生活,并利用一些可用的资源来生存(就好比一个进程开始进行Running状态),在人生命周期的某些阶段,有时必须需要等待一些必要条件才能继续后面的生活(好比进程进入Sleep状态)。并且每个人的生活都会走到死亡的重点,同样的每个进程也都会在某个时点死亡。

以上可以帮助我们理解进程的不同的状态。

关于进程的类型

在Linux系统中存在不同类型的进程,包括用户进程、守护进程和内核进程。

用户进程

操作系统中的大多数进程都属于用户进程,一个用户进程由普通用户初始化并运行在用户进程空间中,除非通过一些方式给予该进程特殊的权限,否则该用户加载的普通用户进程并没有权限去访问不属于该用户的处理器或者文件系统。

守护进程

守护进程是一种被设计用来进行后台运行的程序,通常用来管理一些持续运行的服务。一个守护进程可以用来监听到达某一服务的请求。例如,httpd的守护进程就用来监听网页的请求。或者守护请求可以根据时间的推移来自己启动一些任务,如crond的守护进程就设计用来在某个时刻启动cron任务。

虽然守护进程通常被root用户以服务来运行,但是守护进程也常常被非root用户作为服务来运行。在不同的用户下运行守护进程,会给系统在某些攻击行为下以一定程度的保护。例如,如果一个攻击者攻击并接管了由Apache用户运行的httpd守护进程的权限,这个攻击者并不能额外的获得属于其他用户(包含root用户)拥有的文件及守护进程的访问权限。

操作系统经常在系统启动的时候加载守护进程,并且一直持续不断的运行直到系统关闭。守护进程可以在需要的时候启动、关闭或者被设置运行为特殊的系统运行级别,并且在一些场景下可以在运行时动态的加载配置信息(如crond就可以在运行时动态加载配置文件无须重启)。

内核进程

内核进程只会运行在内核空间。它们和守护进行比较类似,所不同的是内核进程对内核空间的数据具有无限的访问权限,因而使得它们的功能比运行的用户空间的守护进程更为强大。

内核进程通常不会像守护进程那么灵活。你可以通过改变守护进程的配置以及重新启动服务来改变其行为。然而如果要改变内核进程的功能就需要重新编译内核。

系统状态介绍

当一个进程被创建时,系统就会分配给它一个状态,这个状态字段就描述了进程当前的状态。

p->state=TASK_RUNNING

这里,P代表进程,state是一个标记,TASK_RUNNING意思为这个进程当前处于等待运行状态。

大多数进程都处于以下两种状态:

1、进程正在使用CPU(运行态)

2、进程未使用CPU(非运行态)

在单CPU下,同一时刻只能有一个进程在运行。所有其他的进程都要么处于等待状态要么其他非运行状态。也就是说,一个非运行态的进程可能处于不同的状态,状态包含以下几种:

1、Runnable(可运行,在等待CPU调度)态

2、Sleeping(睡眠,等待资源,资源到位后进入Runnable等待CPU调度)态

3、Uninterruptable(不可中断) 态

4、Defunct(结束) 或Zombie(僵尸)态

以下是常见的进程生命周期,进程如下:

1、新生或者forked

2、等待运行或者可运行的

3、运行在用户控件或者内核空间

4、Blocked、Waiting、Sleeping,在可中断睡眠或不可中断睡眠

5、进程在Sleeping,但是在主内存中Sleepoing

6、进程在Sleeping,但是在虚拟内存中Sleeping(被交换到磁盘)

7、被终结(如kill)或者停止(如shut down)

下图描述了进程的状态机:

启动一个进程(新生或forked)

按照fork manpage,fork通过复制调用的进程来创建一个新进程(相当于克隆一个进程),新建的进程被称为子进程,是调用进程(称为父进程)的副本,不过却有以下不同:

1、子进程有一个唯一的进程ID(PID,参见setpgid man page)

2、子进程的父进程ID(PPID)被设为父进程的进程ID

3、子进程并不继承父进程的内存锁(参见mlock和mlockall manpage)

4、子进程的资源使用量(参见getrusage man page)和CPU时间计数器(参见times manpage)被清0

5、子进程的等待信号被清空(参见sigpending man page)

6、子进程并不继承父进程的信号量调整(参见semop)、记录锁(fcntl)、计时器(settimer,alarm,timer_create)

7、等等还有一些不再叙述,有兴趣请参见

(https://access.redhat.com/sites/default/files/attachments/processstates_20120831.pdf)

Running状态

在操作系统中最宝贵的资源就是CPU了,一个进程在某个时刻正在运行并且占用CPU时就称为running进程。可以使用ps和top命令查看每个进程的状态,如果进程处于running状态,该状态在状态字段中表现为R。

译者补充进程状态码以及额外的状态符:

让我们来看看一个进程是如何到达running态的,当你触发了一个命令如grep,shell会在PATH环境变量指定的当前搜索路径下搜索grep命令是否存在,如果ls存在,shell就会使用之前提到的forking方法自己克隆自己生成子进程,然后新的子进程用grep命令的可执行二进制镜像替换它正在执行的二进制镜像(shell),如:

Grep命令就是由12781的进程也就是bash fork出来的子进程执行的。

这些running进程运行在以下空间

1、用户空间

2、系统空间

状态标记如下:

p->state=TASK_RUNNING

CPU即可以运行在内核态也可以在用户态。当一个用户初始化一个进程,这个进程期初运行在用户态。用户态进程每天访问内核的数据结构或者逻辑。每个CPU类型提供了特殊的指令用于在用户态和内核态之间切换。如果一个用户级别的进程想要访问内核数据结构或者逻辑,它就需要通过处理文件子系统或进程控制子系统的系统调用来获得这些信息,这些系统调用例如:

• File subsystemsystem calls: open(), close(), read(), write(), chmod(), and chown()

• Process controlsystem calls: fork(), exec(), exit(), wait(), brk(), and signal()

当内核开始响应用户级别的进程调用请求时,用户级别的进程就进入了内核空间,在/proc/

/stat文件中第14和15个字段,其记录了以PID为进程号的进程分布在用户态和内核态上耗费了多少时间,以下是proc man page描述的这两个字段的含义:

总之就是utime就是进程在用户态上耗费的CPU时钟周期(除以sysconf(_SC_CLK_TCK))

Stime就是在内核态上耗费的CPU时钟周期(同样除sysconf(_SC_CLK_TCK).)。

Top命令显示的是CPU工作在用户态(us)和系统态(sy)的百分比,是一个整体的值。

Runnable态

当一个进程处于可运行态,代表该进程已经具有了除了CPU之外的一切必要资源。可运行态的进程状态用R表示。

考虑一个例子,一个进程正在处理I/O,因此并不立即需要CPU,当进程处理完I/O任务,一个信号发给CPU,调度程序将该进程放入运行队列(由内核维护的等待运行的进程列表),当CPU可用时,这个进程将会进入running态。

这个标记如下:

p->state=TASK_RUNNING

Sleeping 状态

当进程锁需要的资源当前并未到位时,进程就会进入Sleeping状态。在这个时候,它可以自愿的进入sleep状态,或者内核将其置为sleep状态。进入sleep状态意味着进程会立刻放弃CPU的使用。

当进程等待的资源就绪后,就会向CPU发出信号,下次调度器就会有机会调度sleeping的进程,调度器会将该进程置为running或者runnable状态。

以下是一个示例,表示一个login的shell如何进入或者跳出sleep状态:

1、你输入了一个命令,shell进入sleep状态等待事件发生

2、Shell进程在WCHAN上sleep(意为在某个系统函数上sleep)

3、当事件发生后,如来自键盘的中断发生了,每个在其WCHAN上等待的进程被唤醒。

要找出哪些wait channels进程在系统上等待,通过ps -l或者ps -el命令,通过WCHAN字段的值可以显示进程在等待哪一个系统调用。

某些时候进程会进入sleep状态很长时间。Linux内核使用sleep函数,该函数有一个参数可以设置进程在运行之前可以sleep的最小时间。这可以让CPU挂起当前的进程并在sleep周期结束前继续执行其他的进程,调度器会将该进程置为Read-to-run状态,当CPU活动了空闲时间,该进程会重新进入running状态。

一些进程从不会被终结:它们一直处于sleep状态去等待一些事件的发生。一旦事件发生,这些进程会被切换为running或runnable状态,然后再返回Sleep状态。

考虑一个进程已经被forked,如果当前有充足的主内存可以满足该进程的需要,进程就会进入read-to run状态。在这种状态下进程会最终进入read-to-run状态,当CPU可用时就会变成running状态。

如果进程需要的资源没有就绪,进程可能会sleeping,如果没有充足的内存,进程可能会被交换到swap,要么在内存中sleep,要么在磁盘上sleep。这里有两种sleep状态:可中断的和不可中断的。

可中断的sleep状态

一个可中断的sleep状态意为进程在等待特殊的时间片或者特定的事件发生。当任一条件发生后,进程都会脱离可中断的sleep,ps命令中显示为S,状态标记为:

p->state =TASK_INTERRUPTABLE

不可中断的sleep状态

所谓不可中断sleep是指不能立刻响应处理一些信号的sleep进程,唯一可以唤醒进程的只能是一些等待的资源变得可用(如I/O)或者是等待的时间超时(如果超时时间在进程被置为sleep时已经指定)。

不可中断的sleep通常都由设备驱动程序用来等待磁盘或者网络I/O。该进程从系统调用或陷阱中返回后会收到在其sleep期间累积的信号。在Linux系统中ps -l指令使用字母D来表示不可中断sleep的状态。

终结/停止状态

当进程执行了退出的系统调用时,其释放他的数据结构,但是它不会释放它在进程表中的槽。取而代之的是,它发送一个SIGCHLD信号给父进程。会上升到父进程哪里去释放子进程的槽,因此父进程决定了进程能否成功的退出。

在进程何时终结和父进程何时释放子进程这两个时间之间,子进程会进入被称之为僵尸的状态。如果父进程在有机会释放子进程的进程槽之前就已经消亡,那么进程就会进入僵尸态。你不能kill掉一个僵尸进程的原因就是你不能向该进程发送kill的信号,因为进程已经不存在了。

僵尸进程用字母Z来表示状态,状态标记为:

P->state =TASK_ZOMBIE

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181225G0ED3X00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券