Linux进程概念及进程状态
目录
- Linux进程概念及进程状态
- 引入
- 1、什么是进程
- 1.1 描述进程
- 1.2 task_struct组织进程
- 1.3 proc目录
- 2、进程标识符
- 3、查看进程
- 4、bash进程
- 5、初始fork
- 6、进程状态
- 6.1 操作系统层面
- 6.2 Linux内核源代码
- R
- S
- D
- X
- 6.3 僵尸进程
- 6.4 孤儿进程
- 关于kill指令
引入
在计算机科学中,进程是一个正在执行中的程序的实例。它是操作系统进行资源分配和调度的基本单位。Linux作为一种流行的操作系统,也采用了进程的概念来管理系统中的任务和应用程序。
在本篇博客中,我们将深入探讨Linux进程的概念以及进程状态。我们将解释进程的定义、进程的生命周期、以及进程状态的转换过程。此外,我们还将介绍一些常用的Linux命令,以便您可以更好地了解和管理Linux系统中的进程。
1、什么是进程
进程是计算机中正在运行的程序实例
👉 进程 vs 程序
程序的本质是存放在硬盘上的文件,而进程是一个运行起来(加载到内存中)的程序,因此进程具有动态属性
1.1 描述进程
管理的本质是先描述,再组织;在操作系统的进程管理上同样如此,我们将进程的各个属性先描述,再利用某种数据结构讲它们组织起来,这样就可以很好的将进程管理起来
在Linux系统中,使用struct
来进行对进程的描述,该结构体称为PCB(进程控制块)
PCB通常包含以下信息:
- 进程标识符:每个进程都有一个唯一的标识符,用于区分不同的进程。
- 进程状态:进程可以处于就绪、运行或者阻塞状态。PCB中记录了当前进程的状态,以及进程在不同状态间的转换条件。
- 程序计数器(PC):PCB中记录了程序计数器的值,即下一条要执行的指令的地址。
- 内存指针:记录了进程占用的内存空间的起始地址和结束地址。
- CPU寄存器:记录了进程在执行过程中需要保存的CPU寄存器的值,例如程序计数器、堆栈指针等。
- 进程优先级:记录了进程的优先级,用于操作系统调度程序决定哪个进程先执行。
- 执行时间:记录了进程已经执行的时间和剩余的执行时间。
- I/O状态:记录了进程在进行I/O操作时的状态,以及需要进行I/O操作的设备和资源。
此时,所谓的对进程进行管理,变成了对进程对应的PCB进行相关的管理!
对进程管理:转化成了对链表(某种数据结构)的增删查!
1.2 task_struct组织进程
Linux操作系统下的PCB是: task_struct
struct task_struct 内核结构体 -> 内核对象task_struct —> 将该结构与对应代码和数据关联起来
前文介绍过,进程的是硬盘中的程序加载到内存中,每个加载入内存的可执行程序,即对应一个描述它们属性的struct task_struct
,如图:
1.3 proc目录
进程信息保存在根目录下的
proc
目录中
proc是Linux系统上的内存文件系统,在proc当中存储着当前系统实时的进程信息:
ls proc
2、进程标识符
每一个进程在系统中,都会存在一个唯一的标识符,用来标识唯一的一个进程,也叫做pid(
process id
)。
进程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、查看进程
大多数进程信息可以使用
ps
用户级工具来获取
例如我们要查看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() creates a new process by duplicating the calling process. The new process, referred to as the child, is an exact duplicate of the calling process, referred to as the parent
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进程,并且这两个进程是父子进程的关系(第一个进程是另一个进程的父进程)
- 父进程可以有多个进程,但子进程只能有一个父进程;
- 而父进程可能有多个子进程,因此需要pid来标识每一个子进程;
- 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本很低。
这真的是太奇怪了!函数返回值只有其中一个!分支语句也只能选择一个!
所以我们调用完
fork()
后,操作系统究竟做了什么?fork之后系统多了一个进程,实质就是内存中多了一个task_struct
结构体以及子进程对应的代码和数据;子进程的task_struct
对象内部的数据基本是从父进程继承下来的,代码和数据则是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
R
代表运行状态,进程在运行队列中
S
S
是阻塞状态,代表进程在等待某种资源,该进程的PCB在其等待资源的等待队列中
由于CPU的运行速度很快,而资源(硬件外设)的速度很慢,因此一个程序从加载到内存中开始运行,大部分时间都是在等待外设资源,因此大部分时间处于阻塞状态
例如,当我们涉及printf
输出时即需要访问外设资源,此时我们查看该进程状态如下:
ps ajx | head -1 && ps ajx | grep Test.exe | grep -v grep
D
S
是一种阻塞状态,是浅度睡眠,可中断睡眠,操作系统和用户都可以中断其睡眠。
D
是磁盘休眠状态(Disk sleep),也一种阻塞状态,有时候也叫不可中断睡眠状态(uninterruptible sleep),在该状态的进程,无法被OS杀掉!只能通过断电或者进程自己醒来解决
该状态出现的原因?
当计算机处理大量工作时(高IO的情况下),由于内存大小时固定的,当我们正在对某些重要信息处理时,为了避免因为内存不足,操作系统终止一些用户进程导致信息丢失的情况,Linux中提高了深度睡眠状态,使得该状态的进程是无法被操作系统和用户终止,通过这种方式保护我们正在处理的重要信息
X
进程进入死亡状态,资源可以立马被回收。这个状态只是一个返回状态,你不会在任务列表里看到这个状态
6.3 僵尸进程
当进程被创建出来,其目的是为了完成某个任务,那我们怎么知道它是否完成了呢?因此,当进程退出的时候,它不能立即释放该进程对应的资源;它会保存一段时间,让父进程或者OS来进行读取!
那么,当进程退出了,而该进程的父进程或操作系统并没有对该进程进行回收,此状态叫做僵尸状态
例如我们模拟实现僵尸进程:创建子进程,父进程不退出,子进程正常退出,让父进程什么都不做
//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
此时我们左边窗口执行该程序,而右边窗口监视进程状态:
通过观察进程状态的变化,我们知道当我们该程序子进程结束后,由于父进程为为它进行回收操作,这时候该子进程即成为了僵尸进程
僵尸进程危害:
僵尸进程不占用 CPU 或内存资源,但它们占用了系统中有限的进程号资源,而且如果大量的僵尸进程积累,会导致系统进程表满,无法再创建新的进程,从而影响系统的稳定性和可用性。
另外,僵尸进程还会占用一定的系统内存,因为它们的 PCB(进程控制块)并没有被完全清除,只是在系统中被保留,等待其父进程来回收它们。如果父进程不及时回收僵尸进程,这些 PCB 就会一直存在于系统中,占用内存资源。(即:僵尸进程一直存在就会造成内存泄漏)
此外,僵尸进程还会对系统的安全性产生潜在的威胁,因为它们的存在可能会被恶意程序利用,从而导致系统遭受攻击。
因此,及时清除僵尸进程对于系统的稳定性、可用性和安全性都非常重要。一般情况下,父进程应该在子进程结束后调用 wait()
或 waitpid()
系统调用来回收僵尸进程,以保持系统的稳定性和可用性。
6.4 孤儿进程
在 Linux 系统中,当一个进程的父进程在该进程结束之前先结束了,那么这个进程就成为孤儿进程(Orphan Process)
1、这种现象是一定存在的
2、子进程此时会被操作系统接管——进程号为1
3、为什么要接管该进程:如果该子进程退出了,成为僵尸进程,就没有对应父进程回收它了!
4、因此,这个被接管的进程,叫做孤儿进程
5、如果前台进程创建的子进程,成为了孤儿进程,那么它会自动变成后台进程
例如我们模拟实现孤儿进程:让父进程结束,子进程继续运行
//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
只能终止前台进程,由于该前台进程创建的子进程成为了孤儿进程,此时子进程变成后台进程,此时无法通过Ctrl C
的方式终止该进程
带+
代表该进程在前台,而父进程执行后没有+
,说明该进程为后台进程,无法被Ctrl C
终止
我们只能使用kill
指令终止该进程:
kill -9 [PID] #输入该子进程pid结束该进程
关于kill指令
kill指令是给目标进程发送信号
格式:kill -[选项/信号编号] 进程PID
可以通过kill -l
来查看kill
指令的信号编号(选项)
而我们最常使用的,是9
号选项,即终止进程