1. 进程内存分布回顾
-
进程的内存布局与 C 语言程序的内存分布高度一致,主要包括以下区域(从低地址到高地址):
- 代码区(Text Segment):存放程序的机器指令。
- 初始化数据区(Initialized Data Segment):存放已显式初始化的全局变量和静态变量(如
static int a = 5;)。 - 未初始化数据区(BSS Segment, Block Started by Symbol):存放未显式初始化的全局变量和静态变量(如
static int a;),程序加载时由内核自动清零。- 初始化数据区与 BSS 段统称为 静态存储区。
- 堆区(Heap):通过
malloc、calloc等函数动态分配的内存区域,向上增长。 - 栈区(Stack):存放函数调用时的局部变量(自动变量)、函数参数、返回地址等,向下增长。
- 内核空间(Kernel Space):位于最高地址区域,在学习 C 语言时通常被忽略,但它是操作系统内核代码和数据的驻留地。
-
关键概念:
- 用户态代码只能访问用户空间(从代码区到栈区)。
- 当程序执行系统调用(如
read,write)时,会触发硬件中断,CPU 切换到内核态,此时才能访问内核空间。
2. 线程(Thread)的基本概念
- 线程是进程内的一个 执行流(Execution Stream)。
- 一个单线程进程可以看作是只有一个执行流的进程。
- 多线程进程则包含多个并发的执行流。
2.1 多线程进程的内存共享模型
-
在一个多线程进程中,所有线程 共享 以下内存区域:
- 堆区(Heap)
- 静态存储区(Static Storage Area),包括:
- 初始化数据段
- 未初始化数据段(BSS)
- 代码区(Text Segment):所有线程执行的代码都来自同一个可执行文件。
-
每个线程拥有自己 私有 的内存区域:
- 栈区(Stack):每个线程在创建时都会分配独立的栈空间,用于存放其自身的局部变量和函数调用信息。
-
核心结论:多线程进程本质上是 共享同一个地址空间 的多个执行流。
2.2 并发(Concurrency)与并行(Parallelism)
-
并发(Concurrency):
- 指多个执行流在宏观上看起来是同时运行的。
- 在单核 CPU 上,通过操作系统快速切换(时间片轮转)实现,给用户“同时”的错觉。
-
并行(Parallelism):
- 指多个执行流在物理上真正同时运行。
- 需要多核 CPU 或多处理器支持,每个核心可以独立执行一个线程。
-
I/O 阻塞:当一个线程因等待 I/O 操作(如读取文件、网络请求)而阻塞时,其他线程可以继续执行,提高了程序的整体效率。
3. POSIX 标准与 Pthreads
- POSIX(Portable Operating System Interface) 是一套由 IEEE 制定的标准,旨在提高应用程序在不同 Unix-like 系统(如 Linux, macOS)之间的可移植性。
- Pthreads(POSIX Threads) 是 POSIX 标准中定义的线程 API,也常被称为
pthread。
3.1 线程共享的进程属性
根据 POSIX 标准,同一进程内的所有线程共享以下进程级资源:
- 进程 ID (PID)
- 父进程 ID (PPID)
- 进程组 ID
- 会话 ID
- 打开的文件描述符表
- 信号处理设置(Signal Disposition)
- 文件模式创建掩码(umask)
- 环境变量(Environment Variables)
3.2 线程私有的资源
每个线程拥有自己独立的资源,其中最重要的是:
- 线程 ID (TID)
errno变量:这是一个关键点。errno是一个全局变量,用于记录最近一次系统调用或库函数的错误码。如果errno是进程共享的,那么当多个线程同时发生错误时,它们会互相覆盖errno的值,导致无法准确判断哪个线程发生了何种错误。因此,POSIX 要求errno是 线程私有 的,确保每个线程都能独立获取自己的错误状态。
3.3 Pthreads 函数的返回值约定
- 成功:返回
0。 - 失败:不返回
-1,而是直接返回一个 错误码(Error Number)。 - 重要区别:与传统的系统调用(如
open,fork)不同,Pthreads 函数 不会设置全局的errno变量。错误信息直接通过返回值传递。
4. Linux 中线程的实现方式
Linux 系统对 POSIX 线程标准的实现经历了两个主要阶段:
4.1 LinuxThreads(已废弃)
- 这是一个早期的实现,存在诸多不符合 POSIX 标准的问题:
- 线程实际上是通过轻量级进程(LWP)实现的。
getpid()在不同线程中可能返回不同的值(即线程ID),而不是统一的进程ID。- 线程不共享
uid、gid等进程属性。
- 由于这些问题,LinuxThreads 已被现代 Linux 发行版弃用。
4.2 NPTL(Native POSIX Thread Library)
- NPTL 是当前 Linux 系统(自 glibc 2.3.2 和 Linux kernel 2.6 起)采用的标准实现。
- 优点:
- 更好地符合 POSIX 标准。
- 性能更优。
- 解决了 LinuxThreads 的大部分问题,例如所有线程现在共享同一个 PID。
- 验证方法:可以通过命令
getconf GNU_LIBPTHREAD_VERSION查看当前系统使用的线程库版本。输出通常为NPTL x.x.x。
5. 线程的底层系统调用:clone
- 线程的创建最终依赖于 Linux 内核提供的
clone系统调用。 clone比fork更加灵活,它允许子任务(可以是进程或线程)与父任务共享特定的资源(如内存空间、文件描述符表等)。pthread_create等高级 Pthreads API 在内部封装了对clone的调用,并处理了符合 POSIX 标准所需的额外逻辑。
6. 线程的创建与管理
6.1 pthread_create 函数
-
功能:创建一个新的线程。
-
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); -
参数详解:
thread:指向pthread_t类型的指针,用于存储新创建线程的 ID。attr:指向线程属性对象的指针。若为NULL,则使用默认属性。start_routine:新线程开始执行的函数地址。该函数必须接受一个void*参数并返回一个void*。arg:传递给start_routine函数的参数,类型为void*,可用来传递任意数据。
-
返回值:
- 成功:返回
0。 - 失败:返回一个非零的错误码(而非
-1)。
- 成功:返回
6.2 线程的终止方式
线程可以通过以下四种方式终止:
- 从启动例程中返回:线程函数执行
return语句。 - 调用
pthread_exit:线程主动调用此函数退出,并可传递一个返回值。 - 被其他线程取消:通过
pthread_cancel向目标线程发送取消请求。 - 整个进程终止:如果主线程(或其他任何线程)调用了
exit或从main函数返回,整个进程及其所有线程都会被终止。
6.3 线程的连接(Joining)与分离(Detaching)
-
可连接线程(Joinable Thread):
- 默认状态下,线程是可连接的。
- 其他线程(通常是主线程)必须调用
pthread_join来等待该线程结束,并回收其资源和获取其返回值。 - 如果不进行
join,该线程终止后会变成“僵尸线程”,占用系统资源。
-
分离线程(Detached Thread):
- 分离线程在终止时会自动释放其资源,无需其他线程调用
pthread_join。 - 可以通过两种方式将线程设为分离状态:
- 在创建后调用
pthread_detach(thread_id)。 - 在创建前,通过
pthread_attr_setdetachstate设置线程属性为PTHREAD_CREATE_DETACHED。
- 在创建后调用
- 分离线程在终止时会自动释放其资源,无需其他线程调用
6.4 线程间的参数传递
- 由于
pthread_create的arg参数和线程函数的返回值都是void*类型,可以通过指针传递任意复杂的数据结构。 - 常见技巧:
- 传递栈上变量的地址需谨慎:如果主线程在子线程有机会使用该地址前就修改或销毁了该变量,会导致未定义行为。安全的做法是传递堆上分配的内存或使用数组/结构体来保存每个线程的独立数据。
- 直接传递整数值:在 64 位系统上,可以将一个整数强制转换为
void*进行传递(反之亦然),这是一种常见的“投机取巧”方法,但不够规范。
7. 多线程编程实践与性能
- 性能优势:在多核 CPU 上,多线程可以实现真正的并行计算,显著提升 CPU 密集型任务的执行效率。
- 潜在问题:多线程共享内存也引入了新的挑战,最主要的是 竞态条件(Race Condition)。
- 示例:多个线程同时对一个全局变量
global++进行操作。由于global++不是原子操作(包含读取、加一、写回三个步骤),最终结果可能小于预期,因为线程间的操作可能会相互覆盖。
- 示例:多个线程同时对一个全局变量
- 解决方案:为了解决竞态条件等问题,需要使用同步机制,如互斥锁(Mutex)、信号量(Semaphore)等,这些内容将在后续课程中讲解。