1. Linux中的进程状态
上图是操作系统学科中,对进程状态的分类。但是这样细致的划分是在操作系统的设计层面上做的,其中的很多细节,用户其实不必关心。
在Linux操作系统中,面向用户层面,对进程状态做了如下的划分:
cpp
/*
*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 (running)" - 运行状态(0):表示进程正在CPU上执行指令或者在就绪队列中等待CPU资源以便执行。处于这个状态的进程要么正在占用CPU进行计算,要么即将获得CPU时间片来运行。
- "S (sleeping)" - 睡眠状态(1):进程正在等待某个事件的发生,例如等待I/O操作完成(如读取磁盘文件、等待网络数据到达等)。在这种状态下,进程不会占用CPU资源,而是将CPU让给其他可运行的进程,直到被等待的事件发生(如数据准备好)才会被唤醒重新进入就绪状态或者直接运行。
- "D (disk sleep)" - 磁盘睡眠状态(2):这是一种特殊的睡眠状态,进程正在等待磁盘I/O操作完成。与普通睡眠状态不同的是,处于"D"状态的进程不能被轻易杀死(即使使用kill命令发送信号),因为这样可能会导致磁盘数据不一致。例如,当进程正在向磁盘写入重要数据时,如果被强制终止,可能会破坏文件系统的完整性。
- "T (stopped)" - 停止状态(4):进程接收到特定的信号(如SIGSTOP信号)而被暂停执行。这个状态下的进程不会继续运行,直到收到SIGCONT信号来恢复执行。例如,调试器可能会发送SIGSTOP信号使进程停止,以便进行调试操作,调试完成后再发送SIGCONT信号使进程继续运行。
- "t (tracing stop)" - 跟踪停止状态(8):当进程被调试器或跟踪工具跟踪时,可能会处于这种状态。在这种状态下,进程的执行被暂停以便进行跟踪和调试相关的操作,例如查看进程的变量值、执行路径等。与"T (stopped)"状态类似,它也需要特定的信号来恢复执行。
- "X (dead)" - 死亡状态(16):进程已经结束执行,并且已经释放了大部分资源,但是可能还有一些内核资源(如进程描述符的部分内容)正在被清理。这个状态是进程生命周期的最后阶段,一旦清理完成,进程就完全从系统中消失。
- "Z (zombie)" - 僵尸状态(32):进程已经执行完毕,但是父进程还没有调用wait或waitpid等函数来回收子进程的资源(如进程的退出状态码等)。在这种状态下,子进程的进程描述符仍然存在于内核中,占用少量的系统资源。僵尸进程是一种需要注意的情况,因为如果大量的僵尸进程存在,可能会消耗过多的系统资源。
进程处于某个状态,实际上就是其task_struct结点被加入对应的队列。
例如,准备就绪等待运行或正在运行的程序就被添加到运行队列;等待外部设备进行I/O操作的进程就被添加到对应设备的等待队列 依次进行I/O操作。
2. 查看进程状态
bash
ps aux / ps axj
- a:显示一个终端所有的进程,包括其他用户的进程。
- x:显示没有控制终端的进程,例如后台运行的守护进程。
- j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
- u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等。
结合head、grep等指令,我们可以在命令行输入如下指令,以方便地查看指定进程:
bash
ps ajx | head -1; ps ajx | grep [指定进程的关键字] | grep -v grep
其中,STAT栏描述的就是进程的状态,除了上一模块介绍到的各个状态的标识,这些标识后面通常还会带上一些其他符号以提供更加详细的信息:
- <:高优先级进程。
- N:低优先级进程。
- L:锁定内存页。
- s:会话进程组的领头进程。
- l:多线程,进程拥有多个线程。
- +:前台进程,该进程正在和用户交互。
查看进程状态还可通过top指令完成,在Linux笔记---进程:初识进程-CSDN博客中有详细介绍。
3. 向进程发送信号
在Linux系统中,向进程发送信号可以通过以下几种方式:
1. 使用 kill
命令
kill
命令是最常用的向进程发送信号的方式。它可以向指定的进程发送指定的信号。例如,要向进程ID为 1234
的进程发送 SIGTERM
信号,可以使用以下命令:
kill -s SIGTERM 1234
或者使用信号编号:
kill -15 1234
使用kill -l指令可以查看有哪些信号:
bash
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
这些信号,我们会在之后的文章中再做细致讲解。
2. 使用 pkill
命令
pkill
命令允许通过进程名来杀死一组进程。例如,要杀死所有名为 "firefox" 的进程,可以使用以下命令:
pkill firefox
3. 使用 killall
命令
killall
命令与 pkill
类似,但如果给出的进程名不完整,killall
会报错。例如:
killall -9 firefox
4. 使用系统调用
在C语言中,可以使用 kill
系统调用来向进程发送信号。例如:
cpp
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
int main() {
pid_t pid = 1234; // 目标进程的PID
int sig = SIGTERM; // 要发送的信号
int ret = kill(pid, sig);
if (ret == 0) {
printf("信号发送成功\n");
} else {
perror("kill");
}
return 0;
}
5. 使用键盘快捷键
在终端中,可以使用键盘快捷键向当前前台进程发送信号。例如:
Ctrl+C
:发送SIGINT
信号,通常用于终止正在运行的程序。Ctrl+\
:发送SIGQUIT
信号,终止进程并生成核心转储文件。Ctrl+Z
:发送SIGTSTP
信号,暂停进程的执行。
6. 使用函数产生信号
在C语言中,可以使用 raise
函数向当前进程发送信号,或者使用 abort
函数向当前进程发送 SIGABRT
信号,强制使程序异常终止。例如:
cpp
#include <signal.h>
#include <stdio.h>
int main() {
raise(SIGTERM); // 向当前进程发送SIGTERM信号
return 0;
}
7. 由软件条件产生信号
例如,当进程执行某些系统调用(如 pause()
或 wait()
)时,它们可能会因为信号而提前返回。这种机制是基于内核的同步原语和进程的状态管理。
8. 异常产生信号
当进程执行时发生硬件错误或异常,如除以零、访问非法内存地址等,CPU的异常处理机制会触发,内核会为当前进程生成相应的信号。例如,除以零错误会产生 SIGFPE
信号,非法内存访问会产生 SIGSEGV
信号。
请注意,有些信号(如 SIGKILL
和 SIGSTOP
)不能被捕获、阻塞或忽略,因为它们是用于确保系统管理员能够控制所有进程的。
4. 僵尸进程
进程执行完毕之后并不会直接消失,而是会进入僵尸状态,并等待其父进程来回收其资源(也就是所谓的"收尸"),因为它要向父进程反馈自身执行任务的信息,例如完成情况、因什么原因退出等。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入Z状态。
由于进程的状态等信息是由PCB维护的,进程处于僵尸状态时其代码数据可能会得到释放,但是其PCB不会被释放。在长期运行的项目中,僵尸进程忘记被释放就会导致内存泄露的问题。
在进程运行过程中,父进程创建子进程,子进程结束时,它的部分资源(如进程描述符等)需要被父进程回收。
如果父进程没有及时回收子进程的资源,子进程就会一直处于僵尸状态,并且会一直在等待父进程读取退出状态代码,这可能会导致系统资源的浪费,特别是在有大量子进程产生且父进程没有正确处理子进程结束情况的程序中。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0) {
perror("fork");
return 1;
}
else if (id > 0) { //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}
else {
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
5. 孤儿进程
与僵尸进程相对的就是孤儿进程,即父进程在子进程之前退出了。
这会导致什么问题呢?很明显,子进程在变成僵尸进程之后没有人为他收尸,我们称其为孤儿进程。这时候就需要有一个进程来领养这个孤儿。
谁来领养呢?1号进程会对孤儿进程进行领养,并负责为其收尸。
在Linux系统中,1号进程通常是指systemd
,它是一个系统和服务管理器,负责在系统启动时初始化系统,并启动所有其他系统服务。systemd
是现代Linux发行版中最常用的初始化系统,它取代了传统的init
进程。
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id == 0) {//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}
else {//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}