首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >linux:进程详解

linux:进程详解

原创
作者头像
武状元
发布于 2026-10-25 10:43:40
发布于 2026-10-25 10:43:40
90
举报

​

一、进程是什么

进程是程序的一次动态执行实例,是操作系统分配 CPU 时间、内存等资源的基本实体。

内核使用进程控制块管理进程。在 Linux 中,进程控制块就是 task_struct,里面记录了这个进程的一切:它是谁(PID)、处于什么状态、优先级多少、占用了哪些内存、打开了哪些文件、父进程和子进程是谁。

当用使用fork() 创建一个新进程时,内核做的核心工作之一,就是为这个新进程复制或分配一个 task_struct,然后把它挂到进程树中合适的位置。没有 task_struct,进程在内核眼里就不存在。所以进程是linux分配资源的最小单位,而task_struct记录了分配的资源

代码语言:javascript
复制
struct task_struct
{
    // ========== 标识符:描述本进程的唯一标识符,用来区分其他进程 ==========
    pid_t pid;                        // 内核视角:每个任务(线程)的唯一 ID
    pid_t tgid;                       // 用户视角:线程组 ID,等于主线程的 pid
    struct task_struct *group_leader; // 指向线程组的主线程
    char comm[TASK_COMM_LEN];         // 可执行文件名(不含路径),ps 显示的名称

    // ========== 状态:任务状态,退出代码,退出信号 ==========
    volatile long state;              // 任务主状态(TASK_RUNNING / INTERRUPTIBLE / UNINTERRUPTIBLE / STOPPED 等)
    int exit_state;                   // 退出专用状态(EXIT_ZOMBIE / EXIT_DEAD)
    int exit_code;                    // 退出码
    int exit_signal;                  // 导致退出的信号
    unsigned int flags;               // 进程标志位(PF_EXITING / PF_VCPU / PF_FORKNOEXEC 等)
    unsigned int ptrace;              // ptrace 调试状态
    int pdeath_signal;                // 父进程死亡时发送给子进程的信号

    // ========== 优先级:相对于其他进程的优先级 ==========
    int prio;                         // 调度器实际使用的有效优先级(可能因优先级继承临时变化)
    int static_prio;                  // 静态优先级,由 nice 值换算
    int normal_prio;                  // 正常优先级,由调度策略推导
    unsigned int rt_priority;         // 实时任务优先级(0–99)
    unsigned int policy;              // 调度策略(SCHED_NORMAL / SCHED_FIFO / SCHED_RR / SCHED_DEADLINE 等)
    const struct sched_class *sched_class; // 所属调度类(fair_sched_class / rt_sched_class / dl_sched_class)
    struct sched_entity se;           // CFS 调度实体,vruntime 封装在此
    struct sched_rt_entity rt;        // 实时调度实体
    struct sched_dl_entity dl;        // Deadline 调度实体
    int on_rq;                        // 是否在运行队列上
    int nr_cpus_allowed;              // 允许运行的 CPU 数量
    const cpumask_t *cpus_ptr;        // CPU 亲和性掩码指针
    cpumask_t cpus_mask;              // CPU 亲和性掩码

    // ========== 程序计数器与上下文数据 ==========
    // 说明:程序计数器(PC)与寄存器上下文并不以“pc”或“registers”字段直接出现在 task_struct 主体中。
    // 用户态的完整陷阱帧 pt_regs 保存在该任务的内核栈上;
    // 上下文切换需要恢复的 CPU 状态保存在架构相关的 thread_struct 中。
    struct thread_struct thread;      // 架构相关的 CPU 状态(栈指针、被调用者保存的寄存器等)

    // ========== 内存指针:包括程序代码的进程相关数据的指针,还有其他进程共享的内存块的指针 ==========
    struct mm_struct *mm;             // 用户地址空间描述符(页表、VMA 等);内核线程为 NULL
    struct mm_struct *active_mm;      // 当前生效的地址空间;内核线程借用前一个用户进程的 active_mm
    void *stack;                      // 内核栈指针

    // ========== I/O 状态信息:显示 I/O 请求,分配的 I/O 设备和使用的文件列表 ==========
    struct files_struct *files;       // 文件描述符表:fd → struct file 的映射
    struct fs_struct *fs;             // 文件系统上下文:pwd 与 root
    struct nsproxy *nsproxy;          // 命名空间代理(mount、pid、net 等)

    // ========== 记账信息:处理器时间总和,时钟数总和,时间限制等 ==========
    u64 utime;                        // 用户态累计 CPU 时间
    u64 stime;                        // 内核态累计 CPU 时间
    u64 gtime;                        //  Guest 时间
    unsigned long nvcsw;              // 自愿上下文切换次数
    unsigned long nivcsw;             // 非自愿上下文切换次数
    u64 start_time;                   // 进程启动时间(单调时间)
    struct timespec real_start_time;  // 基于 boot 时间的启动时间
    unsigned long min_flt;            // 次要缺页次数
    unsigned long maj_flt;            // 主要缺页次数

    // ========== 信号处理(进程间通信与事件通知的核心) ==========
    struct signal_struct *signal;     // 线程组共享的信号状态(挂起信号、统计等)
    struct sighand_struct *sighand;   // 共享的信号处理函数表
    sigset_t blocked;                 // 本任务私有的阻塞信号掩码
    sigset_t real_blocked;            // 临时阻塞掩码
    struct sigpending pending;        // 本任务私有的待处理信号队列

    // ========== 进程关系(进程树、线程组、全局链表) ==========
    struct task_struct *real_parent;  // 真实父进程(调试时可能被替换)
    struct task_struct *parent;       // 接收 SIGCHLD 的父进程(可能被调试器替换)
    struct list_head children;        // 子进程链表头
    struct list_head sibling;         // 兄弟进程链表节点
    struct list_head tasks;           // 系统中所有任务的全局链表
    struct list_head thread_group;    // 线程组内线程链表
    struct list_head thread_node;     // 线程组链表节点
    struct pid_link pids[PIDTYPE_MAX]; // PID 哈希表链接(PIDTYPE_PID / TGID / PGID / SID)
    struct completion *vfork_done;    // vfork() 完成同步

    // ========== 凭证与安全(进程权限) ==========
    const struct cred *cred;          // 进程凭证(UID/GID、capabilities)的 RCU 指针
    // 旧版本中直接内嵌在 task_struct 中:
    // uid_t uid, euid, suid, fsuid;
    // gid_t gid, egid, sgid, fsgid;
    // struct group_info *group_info;
    // kernel_cap_t cap_effective, cap_inheritable, cap_permitted;

    // ========== 其他关键子系统指针与杂项 ==========
    struct thread_info thread_info;   // 架构相关的线程信息(若 CONFIG_THREAD_INFO_IN_TASK)
    refcount_t usage;                 // 引用计数
    spinlock_t alloc_lock;            // 保护 mm、files、fs 等指针的分配锁
    spinlock_t pi_lock;               // 保护优先级继承数据
    struct io_context *io_context;    // I/O 调度上下文
    struct audit_context *audit_context; // 审计上下文
    seccomp_t seccomp;                // seccomp 过滤器状态
    struct rcu_node *rcu_blocked_node; // RCU 阻塞节点(CONFIG_PREEMPT_RCU)

    // ...(仍有大量条件编译字段,如 cgroup、NUMA balancing、perf_event、bpf 等)
};

二、进程的生命周期

一个进程从诞生到退出,会经历一系列状态变迁。在经典的操作系统模型中,进程有五个状态:

  1. 创建态:进程刚被创建,正在初始化 task_struct 和相关资源。
  2. 就绪态:进程已经获得了除 CPU 之外的所有必要资源,在运行队列中等待调度。
  3. 执行态:进程正在 CPU 上运行。在单核系统中,同一时刻最多只有一个进程处于此状态。
  4. 阻塞态:进程因为等待某个事件(如 I/O 完成、信号量)而主动让出 CPU,暂时无法运行。
  5. 终止态:进程执行完毕或因故被杀死,等待父进程回收其残留信息。

在 Linux 内核的具体实现中,task_struct 的 state 成员记录了更细粒度的状态,例如 TASK_RUNNING(可运行/正在运行)、TASK_INTERRUPTIBLE(可中断睡眠)、TASK_UNINTERRUPTIBLE(不可中断睡眠,通常对应阻塞态)、TASK_STOPPED(暂停)等。

三、调度方式

进程就绪后,谁来决定哪个进程上 CPU、运行多久?这就是调度器的职责。Linux 现代调度器的核心是 CFS(完全公平调度器)。

早期调度器依赖复杂的优先级计算和交互式启发式规则。CFS 的设计哲学非常简洁:在真实硬件上“模拟”一个理想的多任务 CPU。在这个理想 CPU 上,如果有 *n* 个任务在运行,每个任务都应该精确地获得 1/*n* 的 CPU 时间,并行不悖。

真实硬件无法做到真正的并行,CFS 引入了一个关键概念:虚拟运行时间(vruntime)。一个任务的 vruntime 记录了它“在理想 CPU 上本应获得的执行时间”。调度器每次总是选择 vruntime 最小的任务来运行,也就是“至今获得 CPU 时间最少的那个”。。所有可运行任务按照 vruntime 为键值挂在红黑树上,CFS 永远取最左侧的节点(vruntime 最小者)投入运行。任务运行一段时间后,它的 vruntime 增大,在树中向右移动;原本“吃亏”的任务逐渐来到左侧,获得下一次运行机会。

CFS 并没有抛弃优先级,而是把它转化为了权重。nice 值越低的进程权重越高,其 vruntime 的增长速度相对更慢,因此它能更频繁地回到红黑树的左侧,获得更多的 CPU 份额。这就是“完全公平”与“优先级”的折中:公平是基调,但高优先级任务在同样的真实时间内,积累的虚拟时间更少,从而“合法地”多占一些 CPU。

需要注意,进程切换所需要的时间大概在3-5微秒左右,因此如果想要实现多任务同时处理的话一般会使用多线程来实现,线程切换的优势主要在于共享地址空间,省去了页表切换和TLB刷新的开销,因此在需要并发处理任务时,多线程往往比多进程更轻量。

而在linux网络中,早期的 PPC(Process Per Connection)模型会为每个TCP连接 fork 一个子进程来处理。当连接数上升时,系统中可运行的进程数量急剧增加,调度器需要在大量进程之间频繁切换,累积的上下文切换开销会显著挤占有效计算时间。epoll 通过I/O多路复用机制,让一个或少量线程就可以同时监视大量文件描述符上的就绪事件,其核心优势是只处理“活跃”的连接,效率与就绪连接数相关,而与总连接数基本无关。因此,在高并发、长连接的场景下,epoll 避免了为每个连接创建独立执行流所带来的调度与切换开销,成为比“每连接一进程/一线程”更具扩展性的选择。但需要明确:epoll 并不“让单个连接的网络操作变快”,它解决的是大规模并发连接的管理效率问题。

五、进程间通信(IPC):隔离世界中的协作

Linux 的进程拥有独立的地址空间,一个进程不能直接访问另一个进程的变量或内存。这种隔离是安全与稳定的基石,但也带来了协作的难题:浏览器渲染进程需要和网络进程交换数据,ps aux | grep 中的 ps 和 grep 需要传递文本流。IPC(进程间通信) 就是内核提供的一套“跨隔离墙”的机制。-14

机制

核心原理

适用场景

关键局限

匿名管道

内核环形缓冲区,通过 fork 继承文件描述符

父子进程、简单单向数据流

半双工、仅限亲缘进程、容量有限

命名管道(FIFO)

有文件系统路径的管道

无亲缘关系进程间的简单通信

仍是半双工、字节流无结构

消息队列

内核维护的消息链表,消息带类型

多对多通信,需要选择性接收

性能开销高于共享内存

共享内存

多个进程映射同一块物理内存

大数据量、高速传输

必须配合信号量等同步机制,否则产生竞态

信号

内核向进程发送的异步通知

事件通知(如终止、中断)、错误处理

不能携带大量数据

套接字

网络协议栈的统一接口,支持 Unix Domain Socket

跨主机通信,或本机需要全双工/灵活协议时

实现复杂度高于管道

信号量

内核维护的计数器

同步原语,保护共享资源

本身不传递数据,管理不当易死锁

选型的一条实用原则:如果只是父子进程传一点简单数据,管道足够;如果追求极致速度且数据量大,选共享内存,但务必配好信号量做保护;如果需要跨机器或者要全双工、灵活的通信模式,套接字是通用答案。盲目用 Socket 去传本机两个进程的几 KB 数据,往往会引入不必要的序列化和协议栈开销。

总结

Linux 进程的概念可以沿着一条清晰的因果链来理解:程序需要运行,所以产生了进程;内核为了管理进程,所以有了 task_struct;进程需要在就绪、执行、阻塞之间流转,所以有了状态模型和调度器;多个进程需要协作,所以有了 IPC。fork 创建的是新的 task_struct 和执行上下文;CFS 用红黑树和 vruntime 把“公平”变成了可计算的调度决策;而 IPC 则是在隔离的地址空间之间,由内核架起的一座座桥梁。把这些环节打通,你对 Linux 进程的理解就不再是孤立的命令或概念,而是一套从内核数据结构到用户空间行为的完整机制。

​

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、进程是什么
  • 二、进程的生命周期
  • 三、调度方式
  • 五、进程间通信(IPC):隔离世界中的协作
  • 总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档