Linux:线程 ID 与地址空间布局:深入理解线程内存分布(线程七)

一、核心结论

Linux 中有两种线程 ID:用户态线程 ID(pthread_t)和内核态线程 ID(tid),前者是进程内唯一的虚拟地址,后者是系统全局唯一标识。线程的地址空间布局中,主线程栈在栈区,子线程栈在共享区(mmap 映射),所有线程共享代码段、数据段、堆等资源。

二、两种线程 ID:用户态 vs 内核态

1. 用户态线程 ID(pthread_t

  • 来源:由 pthread 库分配,存储在pthread_create的第一个参数中;
  • 本质:Linux 下是虚拟地址空间中的地址(指向线程控制块struct pthread);
  • 作用域:进程内唯一,仅 pthread 库识别,内核不感知;
  • 获取方式:pthread_self()函数。

2. 内核态线程 ID(tid/LWP)

  • 来源:内核创建线程时分配的全局唯一标识;
  • 本质:task_struct中的tid字段,是内核调度的唯一依据;
  • 作用域:系统全局唯一,所有内核操作(如pthread_cancel)最终依赖此 ID;
  • 获取方式:syscall(SYS_gettid)(需包含<sys/syscall.h>);
  • 查看方式:ps -aL命令,LWP列显示的就是内核态线程 ID。

3. 代码示例:获取两种线程 ID

复制代码
#include <stdio.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>

void* thread_func(void* arg) {
    // 用户态线程ID
    printf("用户态线程ID:%lu\n", (unsigned long)pthread_self());
    // 内核态线程ID(LWP)
    printf("内核态线程ID(LWP):%d\n", syscall(SYS_gettid));
    sleep(2);
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);

    printf("主线程用户态ID:%lu\n", (unsigned long)pthread_self());
    printf("主线程内核态ID(LWP):%d\n", syscall(SYS_gettid));

    pthread_join(tid, NULL);
    return 0;
}

4. 运行结果与ps -aL验证

复制代码
主线程用户态ID:140709324857152
主线程内核态ID(LWP):12345
用户态线程ID:140709316464384
内核态线程ID(LWP):12346

执行ps -aL | grep 12345

复制代码
PID   LWP   TTY          TIME CMD
12345 12345 pts/0    00:00:00 thread_id
12345 12346 pts/0    00:00:00 thread_id
  • PID列是线程组 ID(tgid),等于主线程的内核态 ID;
  • LWP列是每个线程的内核态 ID,主线程的LWPPID相同。

三、线程的进程地址空间布局

进程的虚拟地址空间从低地址到高地址依次为:代码段→已初始化数据段→未初始化数据段→堆→共享区(mmap 区域)→栈区,线程的内存分布遵循以下规则:

1. 所有线程共享的区域

  • 代码段(Text Segment):存储进程的执行代码,所有线程共享同一套指令;
  • 数据段(Data Segment):存储已初始化的全局变量和静态变量;
  • 未初始化数据段(BSS Segment):存储未初始化的全局变量和静态变量;
  • 堆(Heap):动态内存分配区域(malloc/new),线程可通过全局指针或参数共享;
  • 共享区(mmap 区域):存储共享库、内存映射文件等,子线程栈也在此区域。

2. 线程独占的区域

  • 栈区:
    • 主线程栈:位于虚拟地址空间的栈区,从高地址向低地址生长,可动态扩展(超出上限触发栈溢出);
    • 子线程栈:位于共享区,通过mmap分配,大小固定(默认 8MB),不可动态增长,用尽会触发栈溢出;
  • 线程局部存储(TLS):存储线程私有数据(如__thread修饰的变量),每个线程有独立副本。

3. 地址空间布局示意图(简化版)

复制代码
低地址 → 高地址
[代码段] → [已初始化数据段] → [未初始化数据段] → [堆] → [共享区(共享库+子线程栈)] → [主线程栈]

四、子线程栈的核心特性(与主线程栈的区别)

特性 主线程栈 子线程栈
分配方式 进程创建时自动分配 pthread_create时通过mmap分配
位置 栈区 共享区(mmap 区域)
大小 可动态增长(默认 8MB) 固定大小(默认 8MB,可通过属性设置)
增长方向 高地址→低地址 高地址→低地址
溢出处理 超出上限触发 SIGSEGV 用尽即溢出,触发 SIGSEGV

关键注意点:

  • 子线程栈大小可通过线程属性pthread_attr_setstacksize设置,最小不能小于PTHREAD_STACK_MIN(通常为 16KB);
  • 避免在子线程中分配大量局部变量(如大数组),容易导致栈溢出;
  • 线程间传递栈地址会导致野指针,需使用堆内存或全局变量。

五、线程控制块(struct pthread)的位置

pthread 库内部用struct pthread(线程控制块 TCB)描述线程,存储线程 ID、栈地址、退出状态等信息:

  • 位置:位于子线程栈的末端(高地址侧),pthread_t本质是指向该结构体的指针;
  • 核心字段:
    • tid:内核态线程 ID;
    • stackblock:线程栈的起始地址;
    • stackblock_size:线程栈的大小;
    • result:线程的退出状态;
    • start_routine:线程执行函数指针。

六、总结

  • 线程有两种 ID:用户态pthread_t(进程内唯一)和内核态 tid(全局唯一),ps -aL可查看内核态 ID;
  • 线程共享进程的代码段、数据段、堆等区域,仅独占栈和线程局部存储;
  • 子线程栈位于共享区,大小固定,需注意避免栈溢出;
  • 理解地址空间布局是线程编程的基础,可帮助规避野指针、数据竞争等问题。

至此,Linux 线程从入门到核心原理的系列讲解已完成,涵盖概念、内核实现、资源共享、线程控制、ID 与内存布局,希望能帮助你彻底掌握线程编程

相关推荐
以太浮标2 小时前
华为eNSP模拟器综合实验之- NAT策略配置类型全景汇总
服务器·网络·华为
北京阿法龙科技有限公司2 小时前
工业场景下AR+AI图像识别:精准选型赋能运维与质检
运维·人工智能·ar
小oo呆2 小时前
【学习心得】CMD终端设置Proxy的几个要点
运维·服务器·网络
Forget_85502 小时前
RHEL——制作母盘
linux·运维·服务器
释怀不想释怀2 小时前
Linux命令--echo~反引号符~重定向符(>>)~tail命令
linux·运维·服务器
Max_uuc3 小时前
【C++ 硬核】给单片机装上“反射”:手写极简属性系统 (Property System) 自动化 CLI 开发
运维·自动化
云小逸3 小时前
【Nmap 设备类型识别技术】整体概况
服务器·c语言·网络·c++·nmap
路由侠内网穿透.3 小时前
fnOS 飞牛云 NAS 本地部署私人影视库 MoonTV 并实现外部访问
运维·服务器·网络·数据库·网络协议
Doro再努力3 小时前
【Linux05】Linux权限管理深度解析(二)
linux·运维·服务器