1. 进程状态
1.1什么是进程状态
Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度
睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
下面我们来一一总结一下:
进程状态 | 含义 |
---|---|
就绪态(Ready) | 指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程; |
运行态 | 指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态; |
僵尸态 | 僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸” |
可中断睡眠状态 | 可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒; |
不可中断睡眠状态 | 不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程系统调度的。 |
暂停态 | 暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号 |
一个新创建的进程会处于就绪态,只要得到 CPU 就能被执行。以下列出了进程各个状态之间的转换关系,如下所示:
1.2 进程关系
进程号(进程 ID、PID),也有自己的生命周期,进程都有自己的父进程、而父进程也有父进程,这就形成了一个以 init 进程为根的进程家族树;当子进程终止时,父进程会得到通知并能取得子进程的退出状态。
除此之外,进程间还存在着其它一些层次关系,譬如进程组和会话;所以,由此可知,进程间存在着多种不同的关系,主要包括:无关系(相互独立)、父子进程关系、进程组以及会话。
-
无关系
两个进程间没有任何关系,相互独立。 -
父子进程关系
两个进程间构成父子进程关系,譬如一个进程 fork()创建出了另一个进程,那么这两个进程间就构成了父子进程关系,调用 fork()的进程称为父进程、而被 fork()创建出来的进程称为子进程;当然,如果“生父”先与子进程结束,那么 init 进程(“养父”)就会成为子进程的父进程,它们之间同样也是父子进程关系。 -
进程组
每个进程除了有一个进程 ID、父进程 ID 之外,还有一个进程组 ID,用于标识该进程属于哪一个进程组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。
tips:Linux 系统设计进程组实质上是为了方便对进程进行管理。假设为了完成一个任务,需要并发运行 100个进程,但当处于某种场景时需要终止这 100 个进程,若没有进程组就需要一个一个去终止,这样非常麻烦且容易出现一些问题;有了进程组的概念之后,就可以将这 100 个进程设置为一个进程组,这些进程共享一个进程组 ID,这样一来,终止这 100 个进程只需要终止该进程组即可。
注意:关于进程组需要注意以下以下内容:
- 每个进程必定属于某一个进程组、且只能属于一个进程组;
- 每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID;
- 在组长进程的 ID 前面加上一个负号即是操作进程组;
- 组长进程不能再创建新的进程组;
- 只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关;
- 一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开该进程组;
- 默认情况下,新创建的进程会继承父进程的进程组 ID。
1.2.1 getpgid()函数与getpgrp()函数的使用
#include <unistd.h>
pid_t getpgid(pid_t pid);
pid_t getpgrp(void);
下面是进程代码编写:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid();
printf("进程组 ID<%d>---getpgrp()\n", getpgrp());
printf("进程组 ID<%d>---getpgid(0)\n", getpgid(0));
printf("进程组 ID<%d>---getpgid(%d)\n", getpgid(pid), pid);
exit(0);
}
从上面的结果可以发现,其新创建的进程对应的进程组 ID 等于该进程的 ID。
1.2.2 setpgid()函数 或 setpgrp()函数的使用
调用系统调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组,其函数原型如下所示:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);
setpgid()函数将参数 pid 指定的进程的进程组 ID 设置为参数 gpid。如果这两个参数相等(pid==gpid),则由 pid 指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数 pid 等于 0,则使用调用者的进程 ID;另外,如果参数 gpid 等于 0,则创建一个新的进程组,由参数 pid 指定的进程作为进程组组长进程。setpgrp()函数等价于 setpgid(0, 0)。
此外,一个进程只能为它自己或它的子进程设置进程组 ID,在它的子进程调用 exec 函数后,它就不能更改该
子进程的进程组 ID 了。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("更改前进程组 ID<%d>\n", getpgrp());
setpgrp();
printf("更改后进程组 ID<%d>\n", getpgrp());
exit(0);
}
这个结果就不运行了!
1.3 会话
介绍完进程组之后,再来看下会话,会话是一个或多个进程组的集合,其与进程组、进程之间的关系如下图所示:
一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一个会话首领(leader),即创建会话的进程。一个会话可以有控制终端、也可没有控制终端,在有控制终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(譬如通过 SSH 协议网络登录),一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程组。
会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进程被称为控制进程;产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程,譬如 Ctrl + C(产生 SIGINT 信号)、Ctrl + Z(产生 SIGTSTP 信号)、Ctrl + \(产生 SIGQUIT 信号)等等这些由控制终端产生的信号。
当用户在某个终端登录时,一个新的会话就开始了;当我们在 Linux 系统下打开了多个终端窗口时,实际上就是创建了多个终端会话。
一个进程组由组长进程的 ID 标识,而对于会话来说,会话的首领进程的进程组 ID 将作为该会话的标识,也就是会话 ID(sid),在默认情况下,新创建的进程会继承父进程的会话 ID。通过系统调用 getsid()可以获取进程的会话 ID,其函数原型如下所示:
#include <unistd.h>
pid_t getsid(pid_t pid);
写个最简单的查看会话代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("会话 ID<%d>\n", getsid(0));
exit(0);
}
同理可得:
使用setsid()可以创建一个会话,如下方所示:
#include <unistd.h>
pid_t setsid(void);
如果调用者进程不是进程组的组长进程,调用 setsid()将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用 setsid()创建的会话将没有控制终端。setsid()调用成功将返回新会话的会话 ID;失败将返回-1,并设置 errno。
本文参考正点原子的嵌入式LinuxC应用编程。