Linux进程概念及状态

Linux进程概念及进程状态

引入

在计算机科学中,进程是一个正在执行中的程序的实例。它是操作系统进行资源分配和调度的基本单位。Linux作为一种流行的操作系统,也采用了进程的概念来管理系统中的任务和应用程序。

在本篇博客中,我们将深入探讨Linux进程的概念以及进程状态。我们将解释进程的定义、进程的生命周期、以及进程状态的转换过程。此外,我们还将介绍一些常用的Linux命令,以便您可以更好地了解和管理Linux系统中的进程。

1、什么是进程

进程是计算机中正在运行的程序实例

👉 进程 vs 程序

程序的本质是存放在硬盘上的文件,而进程是一个运行起来(加载到内存中)的程序,因此进程具有动态属性

1.1 描述进程

管理的本质是先描述,再组织;在操作系统的进程管理上同样如此,我们将进程的各个属性先描述,再利用某种数据结构讲它们组织起来,这样就可以很好的将进程管理起来

在Linux系统中,使用struct来进行对进程的描述,该结构体称为PCB(进程控制块)

PCB通常包含以下信息:

  1. 进程标识符:每个进程都有一个唯一的标识符,用于区分不同的进程。
  2. 进程状态:进程可以处于就绪、运行或者阻塞状态。PCB中记录了当前进程的状态,以及进程在不同状态间的转换条件。
  3. 程序计数器(PC):PCB中记录了程序计数器的值,即下一条要执行的指令的地址。
  4. 内存指针:记录了进程占用的内存空间的起始地址和结束地址。
  5. CPU寄存器:记录了进程在执行过程中需要保存的CPU寄存器的值,例如程序计数器、堆栈指针等。
  6. 进程优先级:记录了进程的优先级,用于操作系统调度程序决定哪个进程先执行。
  7. 执行时间:记录了进程已经执行的时间和剩余的执行时间。
  8. I/O状态:记录了进程在进行I/O操作时的状态,以及需要进行I/O操作的设备和资源。

此时,所谓的对进程进行管理,变成了对进程对应的PCB进行相关的管理!

对进程管理:转化成了对链表(某种数据结构)的增删查!

1.2 task_struct组织进程

Linux操作系统下的PCB是: task_struct

image-20230411154621011

struct task_struct 内核结构体 -> 内核对象task_struct —> 将该结构与对应代码和数据关联起来 

前文介绍过,进程的是硬盘中的程序加载到内存中,每个加载入内存的可执行程序,即对应一个描述它们属性的struct task_struct,如图:

image-20230411160714917

1.3 proc目录

进程信息保存在根目录下的proc目录中

image-20230411161023288

proc是Linux系统上的内存文件系统,在proc当中存储着当前系统实时的进程信息:

ls proc

image-20230411161357508

2、进程标识符

每一个进程在系统中,都会存在一个唯一的标识符,用来标识唯一的一个进程,也叫做pidprocess id)。

进程id:PID 父进程id:PPID

我们可以调用以下接口来查看进程id:

getpid();    //得到进程id
getppid();   //得到父进程id

我们可以通过man指令查看linux手册关于此接口的介绍:

man getpid

image-20230411162851502pid_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

运行结果如下:

image-20230411163217659

我们由以上信息可以得到该进程的pid,我们执行:

ls /proc/551/       #其中551是子进程pid

image-20230411163519223

我们发现,当我们运行Test.exe程序时,获取进程的pid,再查看proc目录下的pid文件夹,发现一定会存在一个以该程序pid命名的文件夹;

当我们使用Ctrl C结束该程序时:

image-20230411163738813

此时,该进程文件夹消失了!

刚刚我们采用Ctrl C关闭正在执行的程序,我们也可以使用kill指令结束进程,命令如下:

kill -9 [进程的pid]

image-20230411164128536

3、查看进程

大多数进程信息可以使用ps用户级工具来获取

例如我们要查看Test.exe程序的进程信息:

ps ajx | grep Test.exe

image-20230411165410416

使用上述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 

image-20230411165633103

通过该指令可以得到该进程的详细信息,我们最常用的即是它的PIDPPID

4、bash进程

我们试着多次运行Test.c程序:

我们发现:子进程pid一直在变化,而父进程pid却一直没有变化!

那么,这个pid为3743的父进程,是什么呢?我们查看它的进程信息:

ps ajx | head -1 && ps ajx | grep 3743 | grep -v grep

image-20230411170339223

结论:几乎所有我们在命令行上所执行的指令,都是bash进程的子进程!

5、初始fork

man fork

image-20230411170601996

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()系统调用通过复制调用进程来创建一个新进程。新进程被称为子进程,是调用进程的一个完全副本,也被称为父进程。

其返回值如下:

image-20230411171112150

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

运行结果如下:

image-20230411193400631

程序中两个死循环同时运行,查看此时的fork进程信息:

ps ajx | head -1 && ps ajx | grep fork.exe | grep -v grep

image-20230411193721075

我们发现有两个fork进程,并且这两个进程是父子进程的关系(第一个进程是另一个进程的父进程)

  • 父进程可以有多个进程,但子进程只能有一个父进程;
  • 而父进程可能有多个子进程,因此需要pid来标识每一个子进程;
  • 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本很低。

这真的是太奇怪了!函数返回值只有其中一个!分支语句也只能选择一个!

所以我们调用完fork()后,操作系统究竟做了什么?fork之后系统多了一个进程,实质就是内存中多了一个task_struct结构体以及子进程对应的代码和数据;子进程的task_struct对象内部的数据基本是从父进程继承下来的,代码和数据则是fork之后父子进程代码共享,数据各自独立

创建子进程–fork是一个函数–函数执行前:只有一个父进程–函数执行后:父进程+子进程

6、进程状态

6.1 操作系统层面

进程状态有:运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡等等进程状态,进程具有如此多的状态,本质是满足未来不同的运行场景的;要理解进程状态,我们首先需要搭建一个os系统的宏观概念:

image-20230414131409064

结论:

1、一个CPU一个运行队列(runqueue)

2、让进程进入队列,本质是将该进程task_struct结构体对象放在运行队列中!

3、进程PCB在runqueue中,就是运行状态(R),不是说这个进程正在运行,才是运行状态!

4、进程不仅仅只等待(占用)CPU资源,也随时随地需要外设资源

5、所谓的进程状态不同,本质是进程在不同的队列中,等待某种资源!


image-20230414135038559


因此我们可以总结出,在操作系统层面上,三种重要的进程状态:

运行状态:进程只要在(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

image-20230414143531204

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

此时我们左边窗口执行该程序,而右边窗口监视进程状态:

image-20230414152408591

通过观察进程状态的变化,我们知道当我们该程序子进程结束后,由于父进程为为它进行回收操作,这时候该子进程即成为了僵尸进程


僵尸进程危害:

僵尸进程不占用 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

此时我们左边窗口执行该程序,而右边窗口监视进程状态:

image-20230414154522970


当我们想通过Ctrl C的方式终止子进程时,发现没有用:

Ctrl C只能终止前台进程,由于该前台进程创建的子进程成为了孤儿进程,此时子进程变成后台进程,此时无法通过Ctrl C的方式终止该进程

+代表该进程在前台,而父进程执行后没有+,说明该进程为后台进程,无法被Ctrl C终止

image-20230414155138397

我们只能使用kill指令终止该进程:

kill -9 [PID]     #输入该子进程pid结束该进程

image-20230414155000681

关于kill指令

kill指令是给目标进程发送信号

格式:kill -[选项/信号编号] 进程PID

可以通过kill -l来查看kill指令的信号编号(选项)

image-20230420144132277

而我们最常使用的,是9号选项,即终止进程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值