深入理解linux进程

在 Linux 系统中,进程是操作系统资源分配和调度的基本单位,也是理解操作系统工作原理的核心。从程序的执行实例到内核中的数据结构,从进程的状态切换到僵尸、孤儿进程的处理,进程的相关知识贯穿了 Linux 开发和运维的整个过程。本文将基于 Linux 内核的实现,从进程的基本概念、描述方式、状态分类、特殊进程以及进程优先级等方面,全面拆解进程的核心特性,帮助大家建立对 Linux 进程的完整认知。

一、进程的基本概念

从不同视角来看,进程的定义有着不同的表述:

  • 课本视角 :进程是程序的一个执行实例,是正在运行的程序,一个程序可以对应多个进程(比如多次执行ls命令会创建多个独立的ls进程)。
  • 内核视角:进程是操作系统分配 CPU、内存等系统资源的基本实体,内核为每个进程维护独立的资源集合,保证进程的正常运行。
  • 本质定义 :在 Linux 系统中,进程 = 内核数据结构(task_struct) + 程序的代码和数据。简单来说,进程不仅包含了程序的可执行代码和数据,还包含了内核对该程序执行状态的所有描述信息。

与程序相比,程序是保存在磁盘上的静态指令集合,而进程是程序加载到内存中运行后的动态实体,拥有生命周期,会经历创建、运行、暂停、退出等一系列状态变化。

二、进程的描述与组织 ------PCB(task_struct

操作系统要管理大量进程,首先要做的就是描述进程组织进程,这是操作系统管理所有资源的通用思路。在 Linux 中,描述进程的核心数据结构是进程控制块(PCB) ,其具体实现为task_struct结构体,该结构体被装载到内存中,包含了进程的所有属性信息。

2.1 task_struct的核心内容

task_struct是一个庞大的结构体,涵盖了进程的各类关键信息,核心内容可分为以下几类:

  1. 标示符:进程的唯一标识(PID),用于区分系统中的不同进程;还有父进程标识(PPID),描述进程间的父子关系。
  2. 状态:记录进程当前的运行状态,如运行、睡眠、停止、僵尸等,同时包含进程的退出代码和退出信号。
  3. 优先级:定义进程获取 CPU 资源的先后顺序,优先级越高的进程,越容易被调度执行。
  4. 程序计数器:保存进程即将执行的下一条指令的内存地址,保证进程被切换后能恢复继续执行。
  5. 内存指针:指向进程的代码、数据在内存中的存储位置,也包含与其他进程共享内存块的指针。
  6. 上下文数据:进程执行时 CPU 寄存器中的数据,是进程切换的核心数据,保证进程恢复执行时的状态一致性。
  7. I/O 状态信息:记录进程的 I/O 请求、分配的 I/O 设备以及打开的文件列表等。
  8. 记账信息:统计进程占用的 CPU 时间、内存使用量等资源信息,用于系统资源调度和统计。

2.2 进程的组织方式

系统中同时运行的进程数量众多,内核需要将这些进程的task_struct高效组织起来。Linux 内核采用双向链表的方式将所有进程的task_struct连接起来,这样内核可以快速完成进程的遍历、新增、删除等操作,实现对进程的统一管理。

三、进程的查看与标识获取

3.1 查看系统进程

在 Linux 中,有多种方式可以查看进程的信息,常用的有文件系统和命令行工具两种:

  1. /proc 文件系统 :Linux 提供了伪文件系统/proc,以文件和目录的形式映射系统的内核状态和进程信息。每个进程对应/proc/[PID]目录,其中包含了该进程的内存、CPU、文件描述符等详细信息,例如查看 PID 为 1 的进程信息可访问/proc/1
  2. 命令行工具pstop是最常用的进程查看工具。ps aux可以列出系统中所有进程的详细信息(包括 PID、PPID、状态、资源占用等);top则可以实时监控进程的运行状态,动态展示 CPU、内存的占用变化。

3.2 通过系统调用获取进程标识

在 C/C++ 程序中,可以通过 Linux 系统调用获取当前进程和父进程的标识,核心函数为getpid()getppid(),使用前需要包含<sys/types.h><unistd.h>头文件。

编译运行后,会输出当前程序对应的进程 PID 和其父进程 PID(通常是执行该程序的 Shell 进程)。

四、进程的创建 ------fork 系统调用

Linux 中通过fork()系统调用创建新进程,这是进程创建的核心方式,fork()的特性也充分体现了 Linux 进程的设计思想。

4.1 fork 的基本特性

  1. 一次调用,两个返回值fork()调用后,会创建一个子进程,父进程和子进程会分别返回。父进程返回子进程的 PID,子进程返回 0;若创建失败,返回 - 1。
  2. 代码共享,数据写时拷贝:子进程会复制父进程的代码段和数据段,但为了提高效率,内核采用写时拷贝(Copy On Write) 机制 ------ 父子进程初始共享同一份数据,只有当其中一个进程修改数据时,内核才会为修改的进程分配独立的内存空间,保证数据的独立性。

4.2 fork 的基本使用

通过fork()的返回值可以实现父子进程的分流执行,这是多进程编程的基础:

运行上述代码,会同时输出父进程和子进程的信息,实现了一个简单的多进程程序。输出的结果也符合fork()的设计原理:

  • 子进程只需要知道自己是子进程 :子进程不需要知道父进程的 PID(可以通过 getppid() 获取),用 0 就能清晰标识 "我是新创建的子进程"。
  • 父进程需要知道子进程的 PID :父进程需要用子进程 PID 来做后续操作(比如 waitpid() 等待子进程结束、发送信号等),所以返回子进程的 PID。

五、Linux 进程的核心状态

进程在生命周期中会处于不同的运行状态,Linux 内核通过task_struct中的状态字段标记进程状态,内核源码中定义了 7 种核心进程状态,各状态对应不同的运行场景,核心状态如下(按内核定义顺序):

  1. R(运行状态) :并非进程一定在 CPU 上运行,而是表示进程要么正在 CPU 执行,要么处于运行队列中等待被调度。这是进程最活跃的状态,只要获取 CPU 时间片就能执行。
  2. S(睡眠状态 / 可中断睡眠):进程正在等待某个事件完成(如等待 I/O、等待信号),此时进程会让出 CPU 资源。该状态的进程可以被信号中断,唤醒后进入 R 状态。
  3. D(磁盘睡眠 / 不可中断睡眠) :进程通常在等待磁盘 I/O 完成,此时进程不会响应任何信号,即使是kill -9也无法杀死,只能等待 I/O 完成后自行唤醒。这是为了保证磁盘 I/O 的原子性,避免数据丢失。
  4. T(停止状态) :进程被暂停执行,可通过发送SIGSTOP信号让进程进入 T 状态,发送SIGCONT信号可唤醒进程恢复为 R 状态。
  5. Z(僵尸状态):进程已经退出,但父进程尚未读取其退出状态,此时进程的task_struct仍保存在内存中,成为僵尸进程。
  6. X(死亡状态) :进程的最终状态,进程退出并被父进程回收后,task_struct被释放,该状态仅为内核内部的返回状态,无法通过ps等工具查看到。

可以通过ps aux或者ps axj命令查看进程的状态,例如Z+表示前台运行的僵尸进程,S+表示前台运行的可中断睡眠进程。

a:显示一个终端所有的进程,包括其他用户的进程。
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等。

六、特殊进程 ------ 僵尸进程与孤儿进程

在父子进程的生命周期中,由于退出顺序的不同,会产生两种特殊的进程:僵尸进程和孤儿进程,其中僵尸进程是开发中需要重点避免的问题。

6.1 僵尸进程

形成原因

子进程先于父进程退出,父进程未通过wait()/waitpid()等系统调用读取子进程的退出状态,子进程的task_struct无法被内核释放,进程进入 Z 状态,成为僵尸进程。

核心危害

僵尸进程的task_struct会一直占用内存资源,若父进程长期不回收子进程,会创建大量僵尸进程,导致内存泄漏;当僵尸进程数量过多时,会耗尽系统的 PID 资源,导致无法创建新进程。

简单示例
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if (id < 0) 
    {
        perror("fork");
        return 1;
    } 
    else if (id > 0) 
    { 
        // 父进程睡眠30秒,不回收子进程
        printf("父进程PID:%d正在睡眠\n", getpid());
        sleep(30);
    } 
    else 
    { 
        // 子进程运行5秒后退出
        printf("子进程PID:%d即将退出\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);
    }
    return 0;
}

运行后通过ps aux | grep 程序名可看到子进程处于 Z 状态,成为僵尸进程。

6.2 孤儿进程

形成原因

父进程先于子进程退出,子进程失去父进程,成为孤儿进程。

内核处理方式

Linux 内核会让1 号进程(init/systemd) 领养孤儿进程,成为孤儿进程的新父进程,由 1 号进程负责回收孤儿进程的退出状态,因此孤儿进程不会造成资源泄漏,是系统允许的正常现象。

简单示例
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if (id < 0) 
    {
        perror("fork");
        return 1;
    } 
    else if (id == 0) 
    { 
        // 子进程睡眠10秒
        printf("子进程PID:%d,父进程PID:%d\n", getpid(), getppid());
        sleep(10);
        printf("子进程新父进程PID:%d\n", getppid()); // 输出1,被init领养
    } 
    else 
    { 
        // 父进程睡眠3秒后退出
        printf("父进程PID:%d,即将退出\n", getpid());
        sleep(3);
        exit(0);
    }
    return 0;
}

运行后可看到子进程的父进程最终变为 1 号进程,实现了孤儿进程的领养。

七、进程优先级

在多进程系统中,CPU 资源是稀缺资源,进程之间存在对 CPU 的竞争,进程优先级决定了进程获取 CPU 资源的先后顺序,优先级越高的进程,越容易被内核调度执行。

7.1 核心优先级指标

Linux 中描述进程优先级的核心指标是PRINI,可通过ps -l命令查看:

  1. PRI(进程实际优先级):值越小,进程优先级越高,默认值为 80。PRI 决定了进程的实际调度顺序,由内核动态维护。
  2. NI(nice 值) :进程优先级的修正值,取值范围为-20 ~ 19,共 40 个级别。NI 不会直接改变 PRI,而是通过公式**PRI(new) = PRI(old) + NI** 调整进程的实际优先级。

关键特性:NI 为负值时,PRI 会减小,进程优先级升高;NI 为正值时,PRI 会增大,进程优先级降低;NI 为 0 时,PRI 保持默认值,进程为默认优先级。

7.2 调整进程优先级的方式

  1. top 命令动态调整 :进入top界面后,按r键,输入要调整的进程 PID,再输入新的 nice 值,即可完成优先级调整。
  2. nice 命令 :启动进程时指定 nice 值,如nice -n 5 ./test,表示以 NI=5 的优先级启动 test 程序。
  3. renice 命令 :修改已运行进程的 nice 值,如renice -n -3 1234,表示将 PID 为 1234 的进程 NI 值改为 - 3。
  4. 系统调用 :通过getpriority()setpriority()函数在程序中获取和设置进程优先级,需包含<sys/resource.h>头文件。

7.3 进程的核心特性

进程的优先级是其竞争性的直接体现,而除了竞争性,进程还具有两个核心特性:

  1. 独立性:多进程运行时,各自拥有独立的资源空间(内存、文件描述符等),进程之间互不干扰,一个进程的崩溃不会影响其他进程。
  2. 并行与并发并行 是指多个进程在多个 CPU 上同时执行;并发 是指多个进程在一个 CPU 上通过进程切换,在一段时间内轮流执行,看似同时运行。进程切换是实现并发的核心,而优先级则决定了进程切换的调度策略。

八、总结

进程是 Linux 操作系统的核心概念,从本质上来说,Linux 系统的运行就是多个进程的调度和资源分配过程。本文围绕 Linux 进程展开,核心要点可总结为:

  1. 进程是动态的执行实体,由task_struct(PCB)+ 程序代码和数据组成,task_struct是内核管理进程的核心数据结构。
  2. Linux 进程有 7 种核心状态,其中 R、S、D、T、Z 是开发中需要重点关注的状态,各状态对应不同的运行场景。
  3. 僵尸进程是父进程未回收子进程退出状态导致的,会造成内存和 PID 资源泄漏,是开发中需要避免的问题;孤儿进程会被 1 号进程领养,无资源泄漏风险。
  4. 进程优先级由 PRI 和 NI 共同决定,NI 是优先级修正值,通过调整 NI 可以改变进程的实际调度顺序,实现对 CPU 资源的合理分配。

理解进程的相关知识,是掌握 Linux 多进程编程、进程调度以及系统优化的基础,后续无论是学习进程间通信、线程开发,还是 Linux 内核调度,都需要以进程的基本概念为前提。掌握进程的核心特性,才能更好地理解 Linux 操作系统的工作原理,写出高效、稳定的 Linux 程序。

相关推荐
HABuo2 小时前
【linux线程(一)】线程概念、线程控制详细剖析
linux·运维·服务器·c语言·c++·ubuntu·centos
王老师青少年编程2 小时前
2026年3月GESP真题及题解(C++七级):物流网络
c++·题解·真题·gesp·csp·七级·物流网络
xushichao19892 小时前
C++中的职责链模式实战
开发语言·c++·算法
fqbqrr2 小时前
2603C++,C++强项
c++
2301_818419012 小时前
C++中的协程编程
开发语言·c++·算法
add45a2 小时前
C++中的工厂方法模式
开发语言·c++·算法
java1234_小锋2 小时前
Java高频面试题:Spring-AOP通知和执行顺序?
java·开发语言·spring
路溪非溪2 小时前
BLE的广播、扫描和连接等工作机制总结
linux·arm开发·驱动开发
番茄去哪了2 小时前
Java基础面试题day02
java·开发语言·面向对象编程