Linux进程概念及进程状态
目录
引入
在计算机科学中,进程是一个正在执行中的程序的实例。它是操作系统进行资源分配和调度的基本单位。Linux作为一种流行的操作系统,也采用了进程的概念来管理系统中的任务和应用程序。
在本篇博客中,我们将深入探讨Linux进程的概念以及进程状态。我们将解释进程的定义、进程的生命周期、以及进程状态的转换过程。此外,我们还将介绍一些常用的Linux命令,以便您可以更好地了解和管理Linux系统中的进程。
1、什么是进程
👉 进程 vs 程序
程序的本质是存放在硬盘上的文件,而进程是一个运行起来(加载到内存中)的程序,因此进程具有动态属性
1.1 描述进程
管理的本质是先描述,再组织;在操作系统的进程管理上同样如此,我们将进程的各个属性先描述,再利用某种数据结构讲它们组织起来,这样就可以很好的将进程管理起来
在Linux系统中,使用struct
来进行对进程的描述,该结构体称为PCB(进程控制块)
此时,所谓的对进程进行管理,变成了对进程对应的PCB进行相关的管理!
对进程管理:转化成了对链表(某种数据结构)的增删查!
1.2 task_struct组织进程
struct task_struct 内核结构体 -> 内核对象task_struct —> 将该结构与对应代码和数据关联起来
前文介绍过,进程的是硬盘中的程序加载到内存中,每个加载入内存的可执行程序,即对应一个描述它们属性的struct task_struct
,如图:
1.3 proc目录
proc是Linux系统上的内存文件系统,在proc当中存储着当前系统实时的进程信息:
ls proc
2、进程标识符
进程id:PID 父进程id:PPID
我们可以调用以下接口来查看进程id:
getpid(); //得到进程id
getppid(); //得到父进程id
我们可以通过man
指令查看linux手册关于此接口的介绍:
man getpid
pid_t
代表无符号整型
下面我们执行以下程序:
vim Test.c
//Test.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("子进程PID:%d,父进程PPID:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
gcc Test.c -o Test.exe
./Test.exe
运行结果如下:
我们由以上信息可以得到该进程的pid,我们执行:
ls /proc/551/ #其中551是子进程pid
我们发现,当我们运行Test.exe
程序时,获取进程的pid,再查看proc目录下的pid文件夹,发现一定会存在一个以该程序pid命名的文件夹;
当我们使用Ctrl C
结束该程序时:
此时,该进程文件夹消失了!
刚刚我们采用Ctrl C
关闭正在执行的程序,我们也可以使用kill
指令结束进程,命令如下:
kill -9 [进程的pid]
3、查看进程
例如我们要查看Test.exe
程序的进程信息:
ps ajx | grep Test.exe
使用上述grep命令后我们发现屏幕上会显示有两个进程信息,这是因为grep指令也是一个进程,可以通过如下指令去掉grep进程信息:
//-v表示匹配上的不显示
ps ajx | grep Test.exe | grep -v grep
而我们发现进程信息为我们显示了该进程的各个属性,但它们名没有名称,我们可以加上:
//显示各项属性名称,且不显示grep的进程
ps ajx | head -1 && ps ajx | grep Test.exe | grep -v grep
通过该指令可以得到该进程的详细信息,我们最常用的即是它的PID
和PPID
4、bash进程
我们试着多次运行Test.c
程序:
我们发现:子进程pid一直在变化,而父进程pid却一直没有变化!
那么,这个pid为3743的父进程,是什么呢?我们查看它的进程信息:
ps ajx | head -1 && ps ajx | grep 3743 | grep -v grep
结论:几乎所有我们在命令行上所执行的指令,都是bash进程的子进程!
5、初始fork
man fork
其返回值如下:
fork有两个返回值,子进程中fork返回0,父进程中fork返回子进程的pid。
示例:我们通过vim创建如下文件:
//fork_test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else if(id > 0)
{
//父进程
while(1)
{
printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(3);
}
}
return 0;
}
gcc fork_test.c -o fork.exe
./fork.exe
运行结果如下:
程序中两个死循环同时运行,查看此时的fork进程信息:
ps ajx | head -1 && ps ajx | grep fork.exe | grep -v grep
我们发现有两个fork进程,并且这两个进程是父子进程的关系(第一个进程是另一个进程的父进程)
这真的是太奇怪了!函数返回值只有其中一个!分支语句也只能选择一个!
创建子进程–fork是一个函数–函数执行前:只有一个父进程–函数执行后:父进程+子进程
6、进程状态
6.1 操作系统层面
进程状态有:运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡等等进程状态,进程具有如此多的状态,本质是满足未来不同的运行场景的;要理解进程状态,我们首先需要搭建一个os系统的宏观概念:
结论:
1、一个CPU一个运行队列(runqueue
)
2、让进程进入队列,本质是将该进程task_struct
结构体对象放在运行队列中!
3、进程PCB在runqueue
中,就是运行状态(R),不是说这个进程正在运行,才是运行状态!
4、进程不仅仅只等待(占用)CPU资源,也随时随地需要外设资源
5、所谓的进程状态不同,本质是进程在不同的队列中,等待某种资源!
因此我们可以总结出,在操作系统层面上,三种重要的进程状态:
运行状态:进程只要在(CPU)运行队列中,就是运行状态
阻塞状态:当进制等待某种非CPU类资源时,该资源还未就绪,进程PCB在该资源等待队列中,即为阻塞状态
挂起状态:当内存不足时,操作系统会将短期内不会调度执行的进程的代码和数据从内存中替换出去
6.2 Linux内核源代码
有了上述在操作系统层面上,对于进程状态的认识,我们来认识Linux操作系统具体的状态,状态在LInux内核源代码中定义如下:
/*
* 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 */ //僵尸状态
};
在Linux内核当中,进程状态可以理解为就是一个整数,例如:
//Linux中的进程状态在task_struct
//以下仅是示例
#define RUN 1 //用1表示运行
#define STOP 2 //用2表示停止
#define SLEEP 3 //用3表示睡眠
具体状态介绍如下:
R
S
例如,当我们涉及printf
输出时即需要访问外设资源,此时我们查看该进程状态如下:
ps ajx | head -1 && ps ajx | grep Test.exe | grep -v grep
D
X
6.3 僵尸进程
例如我们模拟实现僵尸进程:创建子进程,父进程不退出,子进程正常退出,让父进程什么都不做
//zombie_Test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
int count = 3;
while(count--)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
printf("子进程结束,进入僵尸状态\n");
}
else if(id > 0)
{
//父进程
while(1)
{
printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(3);
}
}
return 0;
}
gcc zombie_Test.c -o zom.exe
./zom.exe
我们创建一个循环打印进程状态的指令:
while :; do ps ajx | head -1 && ps ajx | grep zom.exe | grep -v grep | grep -v sys; sleep 1; echo "-----------------------------"; done
此时我们左边窗口执行该程序,而右边窗口监视进程状态:
通过观察进程状态的变化,我们知道当我们该程序子进程结束后,由于父进程为为它进行回收操作,这时候该子进程即成为了僵尸进程
僵尸进程危害:
因此,及时清除僵尸进程对于系统的稳定性、可用性和安全性都非常重要。一般情况下,父进程应该在子进程结束后调用 wait()
或 waitpid()
系统调用来回收僵尸进程,以保持系统的稳定性和可用性。
6.4 孤儿进程
例如我们模拟实现孤儿进程:让父进程结束,子进程继续运行
//nofather_Test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
printf("这是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
}
else if(id > 0)
{
//父进程
int cnt = 3;
while(cnt--)
{
printf("这是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
sleep(1);
}
printf("父进程结束,子进程变成孤儿进程!\n");
}
return 0;
}
gcc zombie_Test.c -o nof.exe
./nof.exe
同样的,我们还是使用循环打印进程状态的指令:
while :; do ps ajx | head -1 && ps ajx | grep nof.exe | grep -v grep | grep -v sys; sleep 1; echo "-----------------------------"; done
此时我们左边窗口执行该程序,而右边窗口监视进程状态:
当我们想通过Ctrl C
的方式终止子进程时,发现没有用:
带+
代表该进程在前台,而父进程执行后没有+
,说明该进程为后台进程,无法被Ctrl C
终止
我们只能使用kill
指令终止该进程:
kill -9 [PID] #输入该子进程pid结束该进程
关于kill指令
格式:kill -[选项/信号编号] 进程PID
可以通过kill -l
来查看kill
指令的信号编号(选项)
而我们最常使用的,是9
号选项,即终止进程