
【Linux线程】线程到底是什么从地址空间、LWP 到 pthread_create 与 TID上篇本文定位这是 Linux 线程系列上篇。我们先不急着背 API而是从虚拟地址空间和内核调度实体出发回答“一个进程为什么能拥有多条执行流”再完整走通pthread_create()与线程标识体系。学习目标读完本文你应该能准确区分进程与线程、并发与并行、共享资源与线程上下文、pthread_t与 Linux TID并能写出生命周期正确的线程创建代码。系列导航上篇讲“线程是什么、如何创建”下篇继续讲“线程如何退出、回收、取消以及 NPTL 如何实现”。文章目录一、线程到底是什么二、Linux 如何表示线程三、为什么理解线程必须先理解地址空间四、同一进程的线程共享什么、独享什么五、多线程的收益、代价与适用场景六、Pthreads 编译、返回值与创建接口七、pthread_t、PID、TGID 与 TID上篇总结一、线程到底是什么1.1 线程是进程内部的一条执行流一个正在运行的程序至少有一条执行路线。只有main执行流时它是单线程进程通过线程库创建更多执行流后同一个进程中就可以同时推进多个任务。更准确地说进程承载地址空间、文件描述符、权限、信号处理动作等资源线程承载程序计数器、寄存器、栈和调度状态是 CPU 调度的执行实体多个线程在同一个进程资源容器内执行同一份程序代码每条线程可以从不同函数入口、不同指令位置继续运行。进程 资源容器 至少一条线程 单线程进程资源容器 main 执行流 多线程进程资源容器 main worker 1 worker 2 ...“进程是资源分配单位、线程是调度单位”是一个有用的入门概括但不要理解成内核里存在一道绝对分割线。Linux 内核以可调度任务描述执行实体并通过共享地址空间、文件表等资源关系组成线程组。1.2 并发与并行不是一回事并发多个任务在一段时间内交替推进单核也能并发并行多个任务在同一时刻运行在不同 CPU 核上需要多核或多处理器线程数量超过可用 CPU 数量并不会凭空增加算力反而可能增加调度、缓存失效和同步成本。对于 I/O 密集任务即使线程没有同时占用多个核心也能通过“一个线程等待 I/O另一个线程继续工作”改善吞吐与响应性。对于 CPU 密集任务则要结合可用核心数、任务粒度和共享状态量决定线程数。1.3 一个线程崩溃为什么常常拖垮整个进程线程共享同一地址空间。某个线程越界写内存可能破坏其他线程使用的堆对象、函数指针或同步结构。某个线程触发SIGSEGV默认处理动作通常终止整个进程进程中的其他线程也随之结束。所以线程之间的隔离远弱于进程。多线程换来了低成本共享也把故障影响面扩大到了整个进程。二、Linux 如何表示线程2.1 Linux 线程不是一套完全独立于进程的内核对象在现代 Linux NPTL 中每个用户线程映射到一个内核可调度实体。它们拥有各自的内核任务描述、调度状态和 TID同时通过共享内存描述、文件描述符表、信号处理动作等方式组成线程组。从不同观察角度会得到三个层级应用/Pthreadspthread_t Linux 线程组TGID 每线程 TID 内核调度器每个线程都是可独立调度的 task教材常把 Linux 线程称为 LWPLightweight Process轻量级进程强调它与传统进程使用相近的调度基础设施但共享更多资源。2.2 为什么“轻量”创建新进程通常要建立新的进程地址空间视图、页表关系和独立资源语义创建同进程线程时新执行流复用已有地址空间和大量进程级资源。线程切换通常不需要切换到另一个进程地址空间因此避免了一部分内存管理上下文变化。但“线程切换一定非常便宜”也不是绝对真理寄存器保存、调度器开销、CPU cache/TLB 行为、NUMA 位置和锁竞争仍可能很昂贵。2.3 线程组中的 leaderLinux 线程组有一个组长线程组 IDTGID通常就是用户看到的进程 PID线程组 leader 的 TID 等于 TGID其他线程拥有各自系统范围内唯一的 TID同组线程调用getpid()得到相同 TGIDLinux 上调用gettid()才得到当前线程的内核 TID。这解释了ps -L中同一 PID 下出现多个不同 LWP/TID 的现象。三、为什么理解线程必须先理解地址空间3.1 CPU 使用虚拟地址不直接把程序指针当物理地址用户程序看到的是连续虚拟地址空间。MMU 根据页表把虚拟页映射到物理页框TLB 缓存近期地址转换结果。虚拟地址连续不要求物理内存连续。简化流程CPU 产生虚拟地址 ↓ MMU 查询 TLB ├─ 命中 → 得到物理页框 └─ 未命中 → 查询多级页表 → 回填 TLB ↓ 物理页框 页内偏移 → 实际物理地址页大小不是“Linux 永远 4 KiB”。4 KiB 在许多系统上很常见但具体值取决于体系结构和配置可以查询getconf PAGESIZE3.2 同一进程的线程为什么天然能看到同一个指针因为它们共享同一进程地址空间。一个全局变量、堆对象或内存映射在这个地址空间中只有一套虚拟地址语义同组线程通过同一映射关系访问它。这与两个独立进程不同两个进程里数值相同的虚拟地址通常会经各自页表映射到不同物理页。同进程线程 A0x7f...1000 ─┐ ├─ 同一地址空间映射 → 同一对象 同进程线程 B0x7f...1000 ─┘ 进程 P0x400000 → 物理页 X 进程 Q0x400000 → 物理页 Y3.3 缺页异常不等于非法访问MMU 找不到有效映射时触发 page fault内核检查虚拟内存区域和权限合法但尚未驻留分配匿名页、从文件调页或完成写时复制页已在内存但当前页表未映射建立映射地址不属于合法区域或权限不允许且无法修复可能向当前线程产生SIGSEGV/SIGBUS。因此缺页是虚拟内存正常工作的组成部分只有无法修复的故障才会演变成进程可见的崩溃。3.4 线程共享地址空间但拥有不同栈典型布局中代码段、全局数据、BSS、堆和大多数映射区由线程共享初始线程使用进程启动时建立的主栈pthread_create()创建的线程通常由线程库准备独立栈映射和保护页“线程栈私有”描述的是使用约定和执行上下文而不是硬件访问隔离。只要知道另一个线程栈上对象的地址同进程线程原则上也能访问它但把栈地址跨线程传递极易造成生命周期错误和数据竞争。四、同一进程的线程共享什么、独享什么4.1 共享的进程资源多个线程通常共享虚拟地址空间中的代码、全局数据、BSS、堆和映射打开的文件描述符当前工作目录和根目录用户 ID、组 ID、进程组、会话和控制终端信号处理动作默认、忽略、捕获资源限制、umask 和多种进程级计时属性进程 ID也就是线程组 ID。一个线程调用close(fd)其他线程对同一描述符的后续使用也会受到影响一个线程修改当前工作目录整个进程随后使用相对路径的行为都会变化。4.2 每个线程独立的执行上下文每个线程拥有或维护自己的pthread_t线程标识Linux TID程序计数器与寄存器上下文栈和栈保护区域信号掩码errno备用信号栈调度策略、实时优先级Linux 特有的 CPU affinity、能力集等线程属性线程局部存储TLS。4.3 “线程私有”不等于“其他线程访问不到”寄存器上下文和内核调度状态确实属于单个线程但用户空间里的栈、TLS 实现区域仍处于共享地址空间。其他线程如果拿到地址硬件页表不会因为“这是线程 B 的栈”就自动拒绝线程 A。这也是多线程安全的本质地址可达性由共享地址空间提供访问秩序必须由程序自己建立。如果两个线程在没有同步关系时并发访问同一普通对象且至少一个是写操作在 C/C 内存模型中可能构成数据竞争并导致未定义行为。后续学习互斥锁、条件变量和原子操作就是在解决这个问题。五、多线程的收益、代价与适用场景5.1 线程的主要优势通信直接共享内存中的对象可以直接访问不需要序列化和 IPC 搬运创建与切换通常较轻复用地址空间和进程资源可利用多核独立计算任务可以在多个 CPU 上并行覆盖 I/O 等待一个线程阻塞时其他线程继续处理任务职责拆分UI、网络、磁盘、后台计算可由不同执行流负责。5.2 线程的真实成本每个线程需要栈、线程控制信息和内核调度资源线程数过多导致上下文切换和调度开销共享数据需要锁、原子操作或无锁协议锁竞争会把并行代码重新串行化false sharing 会让多个核心反复争用 cache line调试时执行顺序不稳定竞态可能难以复现任一线程的内存破坏可能影响整个进程。5.3 什么时候不应“一个任务一个线程”当连接数或任务数很大时无限制创建线程通常不是好设计。更常见的方案是固定大小或可伸缩线程池工作队列 少量 worker非阻塞 I/O 事件循环分阶段流水线对故障隔离要求高时使用多进程或服务边界。线程只是并发执行工具不是自动变快的开关。六、Pthreads 编译、返回值与创建接口6.1 编译请使用 -pthreadgcc main.c-O2-Wall-Wextra-pthread-oapp g main.cc-O2-Wall-Wextra-pthread-oapp相比只写-lpthread-pthread既处理链接也允许编译器/头文件启用线程相关选项是 Linux 上更推荐的用法。6.2 Pthreads 错误码不通过 errno 返回绝大多数 Pthreads 函数成功返回0失败直接返回错误号不要期待它把错误写入当前线程的errno不应直接perror(pthread_create)。正确模式interrpthread_create(tid,NULL,worker,arg);if(err!0){fprintf(stderr,pthread_create: %s\n,strerror(err));return1;}6.3 pthread_create() 参数#includepthread.hintpthread_create(pthread_t*restrict thread,constpthread_attr_t*restrict attr,void*(*start_routine)(void*),void*restrict arg);参数含义thread成功后写入新线程的pthread_tattr创建属性NULL使用默认属性start_routine新线程的入口函数arg原样传给入口函数的单个void *参数6.4 一个生命周期正确的最小示例#includepthread.h#includestdio.h#includestdlib.h#includestring.hstructtask_arg{intvalue;constchar*message;};staticvoid*worker(void*opaque){conststructtask_arg*argopaque;int*resultmalloc(sizeof(*result));if(resultNULL)returnNULL;printf(worker: %s, value%d\n,arg-message,arg-value);*resultarg-value*2;returnresult;}intmain(void){pthread_ttid;structtask_argarg{21,hello};interrpthread_create(tid,NULL,worker,arg);if(err!0){fprintf(stderr,pthread_create: %s\n,strerror(err));return1;}void*rawNULL;errpthread_join(tid,raw);if(err!0){fprintf(stderr,pthread_join: %s\n,strerror(err));return1;}if(raw!NULL){int*resultraw;printf(result%d\n,*result);free(result);}return0;}为什么把arg放在main栈上仍然安全因为main在pthread_join()返回前不会离开其作用域且示例没有并发修改arg。如果创建后立刻返回、复用同一参数对象或让多个线程修改它就需要重新设计所有权和同步。6.5 创建成功后谁先运行没有保证pthread_create()返回前后新线程和创建者谁先获得 CPU 通常是不确定的。不要通过sleep(1)猜测时序也不要假定创建者一定先完成下一条语句。如果新线程必须等待初始化完成应使用互斥锁、条件变量、屏障等明确同步机制而不是延时碰运气。七、pthread_t、PID、TGID 与 TID这是本章最容易被旧资料带偏的地方。7.1 pthread_t 是 POSIX 线程库标识pthread_t是不透明类型只保证在当前进程和有效生命周期内标识线程不保证一定是整数或指针不应用%x、%ld等格式猜测其内部类型两个pthread_t是否相等应使用pthread_equal()线程被 join 或 detached 线程退出后标识可能被复用继续使用已经失效的 ID 属于未定义行为。if(pthread_equal(pthread_self(),tid)){/* 当前线程就是 tid 表示的线程 */}7.2 TID 是 Linux 内核线程 IDLinux 上#define_GNU_SOURCE#includeunistd.hpid_ttidgettid();pid_ttgidgetpid();getpid()返回线程组 IDTGIDgettid()返回当前内核线程 ID主线程通常TID TGID其他线程TID ! TGID但getpid()仍相同。7.3 pthread_self() 不是 gettid()二者面向不同接口层标识作用域主要用途pthread_t进程内、有效线程生命周期内pthread_join、pthread_cancel、pthread_equalLinux TID当前 PID namespace 中的内核任务标识日志、/proc、跟踪、系统级诊断TGID/PID线程组/进程进程管理、普通kill()目标语义不要把当前 glibc 某个版本里pthread_t的具体表示写进协议或持久化文件。实现可以变化POSIX 也没有要求它等于 TID。上篇总结本文建立了 Linux 线程最重要的认知主线进程提供资源容器 ↓ 线程是可独立调度的执行流 ↓ 同组线程共享地址空间和进程级资源 ↓ 每个线程保留独立寄存器、栈、mask、errno 与调度状态 ↓ pthread_create 创建新执行流 ↓ pthread_t、TGID、TID 分别服务于不同观察层请牢牢记住下面六点线程共享地址空间但不共享寄存器上下文和栈的使用权“线程私有栈”不代表其他线程在硬件上无法访问它编译线程程序应使用-pthread多数 Pthreads 函数直接返回错误号不通过errno报错pthread_t是不透明库标识不能当成 Linux TID 使用创建成功不代表父线程或子线程一定先运行执行顺序必须靠同步建立。到这里我们解决了“线程是什么”和“线程如何创建”。但线程创建只是生命周期的起点它怎样返回结果退出后谁回收资源detached 为什么仍有生命周期风险pthread_cancel()为什么不是立即杀死线程这些问题将在下篇继续展开。下篇预告《Linux 线程控制全解析终止、join/detach、cancel、线程栈与 NPTL下》发布上下篇后可以在这里补充下篇链接形成完整系列导航。权威参考pthreads(7)POSIX 线程共享属性、独立属性与 NPTLpthread_create(3)线程创建、继承属性与默认栈说明pthread_self(3)pthread_t 的作用域与比较方式gettid(2)Linux 内核线程 ID如果本文对你有帮助欢迎点赞、收藏。下篇将继续拆解线程终止、join/detach、取消、线程栈与 NPTL 实现。