进程是程序的一次动态执行实例,是操作系统分配 CPU 时间、内存等资源的基本实体。
内核使用进程控制块管理进程。在 Linux 中,进程控制块就是 task_struct,里面记录了这个进程的一切:它是谁(PID)、处于什么状态、优先级多少、占用了哪些内存、打开了哪些文件、父进程和子进程是谁。
当用使用fork() 创建一个新进程时,内核做的核心工作之一,就是为这个新进程复制或分配一个 task_struct,然后把它挂到进程树中合适的位置。没有 task_struct,进程在内核眼里就不存在。所以进程是linux分配资源的最小单位,而task_struct记录了分配的资源
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 等)
};
一个进程从诞生到退出,会经历一系列状态变迁。在经典的操作系统模型中,进程有五个状态:
task_struct 和相关资源。
在 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 并不“让单个连接的网络操作变快”,它解决的是大规模并发连接的管理效率问题。
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 删除。