程序明明只有一个 PID,top -H 却显示十几个可调度对象;给进程发信号和给线程发信号,结果也可能不同。后面要学绑核、优先级和实时调度,必须先把进程、线程和 Linux 内核里的 task 分清。
进程更像资源容器,里面有地址空间、文件描述符、信号处理配置和一个或多个执行流。线程是这些执行流在用户态的抽象。Linux 内核最终调度的是 task_struct 对应的任务,进程与线程的差别主要来自创建时共享了哪些资源。
用命令先看到线程
1 | ps -eLf |
ps -T 会列出同一进程里的线程,/proc/<PID>/task 下每个目录名都是一个 TID。主线程的 TID 通常等于 PID,其他线程有各自的 TID。调试线程绑核时,应检查具体 TID,而不是只看进程号。
1 | grep -E '^(Name|Pid|Tgid|Threads|Cpus_allowed_list):' /proc/<PID>/status |
Tgid 是线程组 ID,Pid 在任务状态文件里表示该 task 的 ID。工具的列名不完全一致,排查时最好回到 /proc 核对。
写一个程序打印 PID 和 TID
1 |
|
1 | gcc -O2 -Wall -Wextra -pthread pid_tid.c -o pid_tid |
两行输出的 PID 相同,TID 不同。这个实验是理解线程绑核、线程定向信号和 /proc/<PID>/task 的起点。
哪些资源共享,哪些独立
同一进程的线程通常共享虚拟地址空间、打开文件、当前工作目录和信号处理函数。它们有独立的寄存器、用户栈、线程局部存储、调度策略、CPU affinity 和信号屏蔽字。
共享地址空间让线程通信很快,也让错误互相影响。一个线程写坏堆内存,另一个线程可能稍后才崩;一个线程关闭共享 fd,其他线程继续使用时会失败。线程安全不是“用了 mutex 就结束”,还要说清对象生命周期、退出顺序和取消路径。
fork 和 pthread_create 不做同一件事
fork() 创建新的进程视图,子进程获得独立的虚拟地址空间语义,物理页通常先通过写时复制共享。pthread_create() 创建同一进程中的新线程,直接共享地址空间和文件表。
在 Linux 内核里,两者最终都依赖类似 clone 的资源共享选择。用户程序应使用 fork() 和 pthread 接口,不要因为内核实现相近就混用语义。
调度器看到的是什么
调度器选择的是可运行 task。两个线程属于同一进程,也会分别进入运行队列,拥有各自的调度策略、优先级和 affinity。一个进程有八个 CPU 密集线程,就可能同时占用八颗 CPU;“只有一个 PID”不代表只使用一个核心。
这也是后续绑核文章要按线程检查的原因:
1 | 进程资源范围:地址空间、文件、凭据 |
退出时容易漏掉什么
主线程从 main() 返回会终止整个进程;某个工作线程返回只结束自己。Joinable 线程退出后仍保留少量状态,直到其他线程 pthread_join();detached 线程则由线程库自动回收。没有设计 join、detach 和停止协议,长期运行服务会积累资源或在退出时访问已销毁对象。
理解这一层后,可以继续看线程绑核和周期控制循环。前者操作具体 TID 的 CPU 集合,后者把 affinity、调度和 deadline 放进同一个循环。
参考资料
证据边界:本文解释 Linux 用户态线程与内核 task 的基本关系,没有覆盖所有 namespace、凭据和信号细节。命令输出会受发行版、工具版本和容器环境影响。