四张图搞懂 Linux 进程是什么?

四张图搞懂 Linux 进程是什么?

目录

大家好,这里是物联网心球。

本期文章,我们通过四张图来了解Linux进程是什么?

1.什么是进程?

Linux 进程是正在运行的程序的实例,它是操作系统进行资源分配(如CPU、内存)和独立调度的基本单位。每个进程都拥有自己独立的虚拟地址空间和运行环境,彼此相互隔离。

2.从内核角度看进程

从内核的角度看进程,进程是一个 task_struct 结构,task_struct 完整地描述了一个进程或线程的所有属性和状态,通常被称为进程描述符(Process Descriptor) 或进程控制块(PCB) 。

task_struct 的定义如下:

struct task_struct {
    unsigned int            __state; /* 进程状态 */
    /* 进程调度相关 */
    int             prio; /* 动态优先级(实际优先级) */
    int             static_prio; /* 静态优先级,由 nice 值决定 */
    int             normal_prio; /* 普通优先级 */
    unsigned int    rt_priority; /* 实时优先级 */
struct sched_entity     se;
/* 普通进程调度实体,CFS 完全公平调度器专用 */
struct sched_rt_entity  rt;
/* 实时进程调度实体,RT 实时调度器专用 */
struct sched_dl_entity  dl;
/* 限期进程调度实体,DL 限期调度器专用 */
    const struct sched_class    *sched_class;/* 进程所属调度类,决定由哪个调度器管理 */
    unsigned int        policy; /* 调度策略 */
    /* 进程 CPU 亲和性相关 */
    int             nr_cpus_allowed; /* 进程可以绑定的CPU核心总数 */
    const cpumask_t     *cpus_ptr; /* 指向当前生效的CPU亲和性掩码(内核调度使用) */
    cpumask_t       *user_cpus_ptr; /* 用户态通过 sched_setaffinity() 设置的原始CPU掩码 */
    cpumask_t       cpus_mask; /* 进程实际允许运行的 CPU 位图掩码 */
    /* 进程地址空间 */
struct mm_struct        *mm;
/* 进程的用户态虚拟内存地址空间 */
    struct mm_struct        *active_mm;/* 进程运行时正在使用的活跃地址空间(用于内核线程借用)*/
    /* 进程ID相关 */
    pid_t               pid; /* 线程ID */
    pid_t               tgid; /* 线程组ID */
    /* 进程关系相关 */
    struct task_struct __rcu    *real_parent;/* 真正的父进程 */
    struct task_struct __rcu    *parent;/* 接收 SIGCHLD 信号和 wait4() 报告的父进程 */
struct list_head        children;
/* 子进程链表 */
struct list_head        sibling;
/* 兄弟进程链表节点 */
    struct task_struct      *group_leader;/* 线程组组长 */
    /* 进程文件相关 */
struct fs_struct    *fs;
/* 文件系统信息:进程当前工作目录、根目录、umask */
struct files_struct *files;
/* 打开文件信息:进程所有打开的文件描述符表(fd 0,1,2...)*/
    /* 进程信号处理相关 */
struct signal_struct        *signal;
/* 线程组共享的信号管理结构 */
    struct sighand_struct __rcu     *sighand;/* 信号处理函数表 */
    sigset_t            blocked; /* 当前线程阻塞的信号掩码 */
    sigset_t            real_blocked; /* 真实阻塞掩码 */
struct sigpending       pending;
/* 进程挂起信号队列 */
    ......
};

task_struct 结构体很复杂(几百个字段),逻辑上可分为以下几大模块:

  • 进程标识与身份
  • 进程状态
  • 调度相关
  • 进程家族关系(父子、兄弟)
  • 内存管理信息
  • 文件与文件系统信息
  • 信号处理
  • 时间统计
  • 线程 / 进程组 / 会话
  • 权限与安全(UID/GID、LSM)
  • IO 与调度统计
  • 调试、性能、特殊标记

task_struct 是内核中的对象,如果用户程序想要查看其中的信息,需要通过特定的方式。/proc/[pid] 目录是 Linux proc 文件系统为每个进程自动创建的目录,内核通过该目录直接暴露进程运行时信息,用户通过该目录查看、调试进程状态。

/proc/[pid] 目录结构如下:

四张图搞懂 Linux 进程是什么? 图1

该目录下的信息来源来自于进程 task_struct 结构。

3.进程状态

task_struct 结构中的 __state 成员表示进程状态,Linux 总共定义了以下几种进程状态:

// 基础运行状态
#define TASK_RUNNING 0x00000000 /*(R)运行/就绪态 */
#define TASK_INTERRUPTIBLE 0x00000001 /*(S)可中断睡眠:可被信号唤醒 */
#define TASK_UNINTERRUPTIBLE 0x00000002 /*(D)不可中断睡眠:仅等待事件,不响应信号 */
#define __TASK_STOPPED 0x00000004 /*(T)进程停止:收到 SIGSTOP 暂停 */
#define __TASK_TRACED 0x00000008 /*(t)被调试跟踪:gdb/strace 暂停 */
// 退出状态
#define EXIT_DEAD 0x00000010 /*(X)进程完全死亡:资源已回收,最终态 */
#define EXIT_ZOMBIE 0x00000020 /*(Z)僵尸态:进程退出,父进程未回收 */
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD) /* 调试跟踪用退出态掩码 */
--------------------------------------------------------------------------------------------------------------------
// 内核内部特殊状态
#define TASK_PARKED 0x00000040 /* 线程停泊:CPU 热插拔/暂停工作 */
#define TASK_DEAD 0x00000080 /* 死亡过渡态 */
#define TASK_WAKEKILL 0x00000100 /* 可被致命信号唤醒的睡眠态 */
#define TASK_WAKING 0x00000200 /* 正在唤醒:已触发唤醒,未完成调度 */
#define TASK_NOLOAD 0x00000400 /* 不参与系统负载统计:idle 类进程 */
#define TASK_NEW 0x00000800 /* 进程创建中:未初始化完成 */
#define TASK_RTLOCK_WAIT 0x00001000 /* 实时锁等待:持有 RT 锁时等待 */
#define TASK_FREEZABLE 0x00002000 /* 可冻结标志:支持挂起/休眠(电源管理)*/
#define __TASK_FREEZABLE_UNSAFE (0x00004000 * IS_ENABLED(CONFIG_LOCKDEP)) /* 锁依赖检查用 */
#define TASK_FROZEN 0x00008000 /* 已冻结:系统挂起时进程被冻结 */
#define TASK_STATE_MAX 0x00010000 /* 状态最大值:状态掩码边界 */

我们把这些状态分为:基础运行状态、退出状态、内核内部特殊状态。其中内核内部特殊状态我们不需要太关注,我们将重点放在基础运行状态和退出状态。

Linux 进程的生命周期通过一个进程状态机来完成,进程状态机如图1所示。

四张图搞懂 Linux 进程是什么? 图2

图1 进程状态机

当用户程序调用 fork 函数创建进程时,内核首先会创建一个 task_struct 对象(进程),此时进程处于 TASK_NEW 状态(进程刚创建未初始化完成的状态),当进程完成初始化后,进程会加入 CPU 就绪队列,此时进程处于 TASK_RUNNING(就绪态),如果进程被 CPU 选中,CPU 会执行进程中的代码,进程切换至 TASK_RUNNING(运行态)。当进程的时间片用完,进程会重新加入 CPU 就绪队列,进程状态又变为 TASK_RUNNING(就绪态)。

当处于 TASK_RUNNING(运行态)的进程需要等待某个事件或者资源时,进程会切换成睡眠状态,睡眠状态分为两种:

  • TASK_INTERRUPTIBLE(可中断睡眠):等资源,但可以被信号叫醒
  • TASK_UNINTERRUPTIBLE(不可中断睡眠):等资源,不能被信号打断,只能等事件完成自己醒。

当事件发生或者资源准备好后,进程从睡眠状态中被唤醒,切换至TASK_RUNNING(就绪态)。

当处于 TASK_RUNNING(运行态)的进程调用 exit 函数或者异常退出时,进程切换至EXIT_ZOMBIE(僵尸态),处于僵尸态的进程的生命周期并没有结束,该进程需要等待父进程回收资源,父进程调用 wait 函数回收进程资源,进程从 EXIT_ZOMBIE 状态切换至 EXIT_DEAD(死亡)状态,进程生命周期结束。

4.进程 ID

进程 ID 的定义如下:

task_struct {
    pid_t               pid; /* 进程或线程 ID */
    pid_t               tgid; /* 线程组 ID */
    ......
};

PID 是进程标识符,每个线程(轻量级进程 LWP)都有独立 PID。

在 Linux 2.4 及更早版本中,PID 就是传统意义上的进程 ID,线程没有独立的 PID。从 Linux 2.6 开始引入 NPTL (Native POSIX Thread Library),每个线程都有独立的 PID。

TGID 是线程组标识符,TGID 等于线程组 leader(主线程)的 PID。

要正确理解 pid和 tgid,我们需要借助线程组这个概念,如图2所示。

四张图搞懂 Linux 进程是什么? 图3

图2 线程组

引入线程组概念后,我们把进程称为主线程,主线程创建的线程是子线程。

如果线程组只有一个进程,我们把它称为单线程进程,单线程进程的只有一个主线程,主线程的 pid 和 tgid 相等,并且线程组组长(task_struct 的 group_leader 成员)指向自身。

当主线程创建子线程后,线程组将包含一个主线程和多个子线程,此时进程被称为多线程进程,线程组所有的线程的 tgid 都等于主线程的 tgid,并且 group_leader 都指向主线程,当然主线程的 pid 和 tgid 依然相等。每个线程的 pid 都不一样,因为 pid 是独立和唯一的。

我们可以通过判断 pid 和 tgid 是否相等,来判断线程是否为主线程。另外每个线程的 tgid 都相等,表示所有的线程都处于同一个线程组。

我们在做软件开发时,通常需要通过以下两个函数来获取进程 ID。

getpid 函数用于获取当前进程的 PID(对应 task_struct 的 tgid 字段)。

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);

gettid 函数用于获取当前线程的真实线程 ID(Thread ID)(对应 task_struct 的 pid 字段)。

#include <unistd.h>
#include <sys/types.h>

pid_t gettid(void);

这两个函数的内核实现原理如图3和图4所示。

四张图搞懂 Linux 进程是什么? 图4

图3 获取主线程pid和tgid

如图3所示,当进程(主线程)调用 getpid 和 gettid 函数时,内核不会直接获取主线程 task_struct 的 pid 和 tgid 字段,因为引入容器技术后,从主机和容器内看到的进程 ID 信息是不一样的。

task_struct 维护了一个 pid 表(signal 字段),pid 表中每个元素为:不同类型的 ID 对应的 struct pid 结构。PIDTYPE_TGID 表示 tgid 对应的 struct pid 结构,struct pid 结构中有一个 upid 表,表中的每个元素为:命名空间级别对应的 struct upid 结构,struct upid结构中的 nr 就是 ID 信息,命名空间 level 0对应的nr 最终会赋值给主线程pid 和 tgid。

upid 表中的元素是按照命名空间等级来划分,所以主机和容器中获取到的进程 struct upid 结构是不一样的,所以 ID 信息也不一样。

需要注意的是,gettid 函数是从 task_struct 结构的 thread_id 成员获取 struct pid 结构,而不是从 pid 表中获取,这个是内核的一个特殊设计。

最后,我们再来看一下如何获取子线程的 pid 和 tgid,如图4所示。

四张图搞懂 Linux 进程是什么? 图5

图4 获取子线程pid和tgid

子线程调用 getpid 函数获取到的 tgid 信息和主线程是一样的,因为主线程创建子线程时,主线程和子线程会指向相同的 pid 表,所以二者查询到的是同一个 struct pid 结构,获取的 tgid 信息也是一样。

子线程和主线程通过 gettid 获取的是进程 pid 是不一样的,因为内核创建子线程时,会新创建一个 struct pid 结构并初始化,子线程 task_struct 的 thread_id 指向新的 struct pid 结构,所以子线程获取到的是一个独立的 pid。

← 返回文章列表