news 2026/8/18 10:55:57

【Linux】线程概念与控制:从地址空间、LWP 到 pthread_create、join、detach 与 NPTL

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux】线程概念与控制:从地址空间、LWP 到 pthread_create、join、detach 与 NPTL

【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,同时通过共享内存描述、文件描述符表、信号处理动作等方式组成线程组。

从不同观察角度,会得到三个层级:

应用/Pthreads:pthread_t Linux 线程组:TGID + 每线程 TID 内核调度器:每个线程都是可独立调度的 task

教材常把 Linux 线程称为 LWP(Lightweight Process,轻量级进程),强调它与传统进程使用相近的调度基础设施,但共享更多资源。

2.2 为什么“轻量”

创建新进程通常要建立新的进程地址空间视图、页表关系和独立资源语义;创建同进程线程时,新执行流复用已有地址空间和大量进程级资源。

线程切换通常不需要切换到另一个进程地址空间,因此避免了一部分内存管理上下文变化。但“线程切换一定非常便宜”也不是绝对真理:寄存器保存、调度器开销、CPU cache/TLB 行为、NUMA 位置和锁竞争仍可能很昂贵。

2.3 线程组中的 leader

Linux 线程组有一个组长:

  • 线程组 ID(TGID)通常就是用户看到的进程 PID;
  • 线程组 leader 的 TID 等于 TGID;
  • 其他线程拥有各自系统范围内唯一的 TID;
  • 同组线程调用getpid()得到相同 TGID;
  • Linux 上调用gettid()才得到当前线程的内核 TID。

这解释了ps -L中同一 PID 下出现多个不同 LWP/TID 的现象。


三、为什么理解线程必须先理解地址空间

3.1 CPU 使用虚拟地址,不直接把程序指针当物理地址

用户程序看到的是连续虚拟地址空间。MMU 根据页表把虚拟页映射到物理页框,TLB 缓存近期地址转换结果。虚拟地址连续,不要求物理内存连续。

简化流程:

CPU 产生虚拟地址 ↓ MMU 查询 TLB ├─ 命中 → 得到物理页框 └─ 未命中 → 查询多级页表 → 回填 TLB ↓ 物理页框 + 页内偏移 → 实际物理地址

页大小不是“Linux 永远 4 KiB”。4 KiB 在许多系统上很常见,但具体值取决于体系结构和配置,可以查询:

getconf PAGESIZE

3.2 同一进程的线程为什么天然能看到同一个指针

因为它们共享同一进程地址空间。一个全局变量、堆对象或内存映射在这个地址空间中只有一套虚拟地址语义,同组线程通过同一映射关系访问它。

这与两个独立进程不同:两个进程里数值相同的虚拟地址,通常会经各自页表映射到不同物理页。

同进程线程 A:0x7f...1000 ─┐ ├─ 同一地址空间映射 → 同一对象 同进程线程 B:0x7f...1000 ─┘ 进程 P:0x400000 → 物理页 X 进程 Q:0x400000 → 物理页 Y

3.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 线程的主要优势

  1. 通信直接:共享内存中的对象可以直接访问,不需要序列化和 IPC 搬运;
  2. 创建与切换通常较轻:复用地址空间和进程资源;
  3. 可利用多核:独立计算任务可以在多个 CPU 上并行;
  4. 覆盖 I/O 等待:一个线程阻塞时,其他线程继续处理任务;
  5. 职责拆分:UI、网络、磁盘、后台计算可由不同执行流负责。

5.2 线程的真实成本

  • 每个线程需要栈、线程控制信息和内核调度资源;
  • 线程数过多导致上下文切换和调度开销;
  • 共享数据需要锁、原子操作或无锁协议;
  • 锁竞争会把并行代码重新串行化;
  • false sharing 会让多个核心反复争用 cache line;
  • 调试时执行顺序不稳定,竞态可能难以复现;
  • 任一线程的内存破坏可能影响整个进程。

5.3 什么时候不应“一个任务一个线程”

当连接数或任务数很大时,无限制创建线程通常不是好设计。更常见的方案是:

  • 固定大小或可伸缩线程池;
  • 工作队列 + 少量 worker;
  • 非阻塞 I/O + 事件循环;
  • 分阶段流水线;
  • 对故障隔离要求高时使用多进程或服务边界。

线程只是并发执行工具,不是自动变快的开关。


六、Pthreads 编译、返回值与创建接口

6.1 编译请使用 -pthread

gcc 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")

正确模式:

interr=pthread_create(&tid,NULL,worker,arg);if(err!=0){fprintf(stderr,"pthread_create: %s\n",strerror(err));return1;}

6.3 pthread_create() 参数

#include<pthread.h>intpthread_create(pthread_t*restrict thread,constpthread_attr_t*restrict attr,void*(*start_routine)(void*),void*restrict arg);
参数含义
thread成功后写入新线程的pthread_t
attr创建属性;NULL使用默认属性
start_routine新线程的入口函数
arg原样传给入口函数的单个void *参数

6.4 一个生命周期正确的最小示例

#include<pthread.h>#include<stdio.h>#include<stdlib.h>#include<string.h>structtask_arg{intvalue;constchar*message;};staticvoid*worker(void*opaque){conststructtask_arg*arg=opaque;int*result=malloc(sizeof(*result));if(result==NULL)returnNULL;printf("worker: %s, value=%d\n",arg->message,arg->value);*result=arg->value*2;returnresult;}intmain(void){pthread_ttid;structtask_argarg={21,"hello"};interr=pthread_create(&tid,NULL,worker,&arg);if(err!=0){fprintf(stderr,"pthread_create: %s\n",strerror(err));return1;}void*raw=NULL;err=pthread_join(tid,&raw);if(err!=0){fprintf(stderr,"pthread_join: %s\n",strerror(err));return1;}if(raw!=NULL){int*result=raw;printf("result=%d\n",*result);free(result);}return0;}

为什么把arg放在main栈上仍然安全?因为mainpthread_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 内核线程 ID

Linux 上:

#define_GNU_SOURCE#include<unistd.h>pid_ttid=gettid();pid_ttgid=getpid();
  • getpid()返回线程组 ID(TGID);
  • gettid()返回当前内核线程 ID;
  • 主线程通常TID == TGID
  • 其他线程TID != TGID,但getpid()仍相同。

7.3 pthread_self() 不是 gettid()

二者面向不同接口层:

标识作用域主要用途
pthread_t进程内、有效线程生命周期内pthread_joinpthread_cancelpthread_equal
Linux TID当前 PID namespace 中的内核任务标识日志、/proc、跟踪、系统级诊断
TGID/PID线程组/进程进程管理、普通kill()目标语义

不要把当前 glibc 某个版本里pthread_t的具体表示写进协议或持久化文件。实现可以变化,POSIX 也没有要求它等于 TID。



上篇总结

本文建立了 Linux 线程最重要的认知主线:

进程提供资源容器 ↓ 线程是可独立调度的执行流 ↓ 同组线程共享地址空间和进程级资源 ↓ 每个线程保留独立寄存器、栈、mask、errno 与调度状态 ↓ pthread_create 创建新执行流 ↓ pthread_t、TGID、TID 分别服务于不同观察层

请牢牢记住下面六点:

  1. 线程共享地址空间,但不共享寄存器上下文和栈的使用权
  2. “线程私有栈”不代表其他线程在硬件上无法访问它
  3. 编译线程程序应使用-pthread
  4. 多数 Pthreads 函数直接返回错误号,不通过errno报错
  5. pthread_t是不透明库标识,不能当成 Linux TID 使用
  6. 创建成功不代表父线程或子线程一定先运行,执行顺序必须靠同步建立

到这里,我们解决了“线程是什么”和“线程如何创建”。但线程创建只是生命周期的起点:它怎样返回结果?退出后谁回收资源?detached 为什么仍有生命周期风险?pthread_cancel()为什么不是立即杀死线程?这些问题将在下篇继续展开。

📖下篇预告:《Linux 线程控制全解析:终止、join/detach、cancel、线程栈与 NPTL(下)》

发布上下篇后,可以在这里补充下篇链接,形成完整系列导航。

权威参考

  • pthreads(7):POSIX 线程共享属性、独立属性与 NPTL
  • pthread_create(3):线程创建、继承属性与默认栈说明
  • pthread_self(3):pthread_t 的作用域与比较方式
  • gettid(2):Linux 内核线程 ID

如果本文对你有帮助,欢迎点赞、收藏。下篇将继续拆解线程终止、join/detach、取消、线程栈与 NPTL 实现。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/18 10:55:35

SWE-TRACE:过程奖励与动态扩展如何优化AI编程智能体

1. 从“一次性生成”到“过程优化”&#xff1a;长周期软件工程智能体的新范式最近在AI辅助编程这个圈子里&#xff0c;讨论的热点已经从“大模型能不能写代码”转向了“大模型写的代码到底靠不靠谱&#xff0c;以及如何让它更靠谱”。我们经常遇到这样的场景&#xff1a;丢给大…

作者头像 李华
网站建设 2026/8/18 10:55:35

AI Agent开发——设计本身就是交付

【摘要】AI Agent 正从信息辅助工具走向真实业务执行端&#xff0c;错误影响从对话内扩散至业务流程与外部关系。围绕风险分层、动态权限体系与上下文交接机制&#xff0c;拆解人工接管的设计逻辑与工程落地路径&#xff0c;为生产级 Agent 系统提供可复用的管控框架与评测方法…

作者头像 李华
网站建设 2026/8/18 10:55:11

Java全渠道同城回收系统技术实现与优化

1. 项目概述&#xff1a;全渠道同城回收系统的技术实现方案 这套基于Java技术栈的同城上门回收系统&#xff0c;本质上是一个打通线上线下服务闭环的O2O解决方案。我在实际开发这类系统时发现&#xff0c;它需要同时解决三个核心问题&#xff1a;如何高效匹配供需双方&#xff…

作者头像 李华
网站建设 2026/8/18 10:53:37

EtherCAT主站SOEM -- 52 -- SOEM2.0之ec_config.h/c从站扫描与PDO映射解析

EtherCAT主站SOEM -- 52 -- SOEM2.0之ec_config.h/c从站扫描与PDO映射解析 0 Win QT & VS和Ubuntu QT & STM32F767 移植SOEM 0.0 移植环境预览: 0.1 Ubuntu18.04系统QT-SOEM博客、视频欣赏及源代码链接 0.2 STM32F767-SOEM 博客、视频欣赏及源代码链接 0.3 Win11/10系…

作者头像 李华