1. 核心定义:Linux 没有真正的 "线程"
这句话可能听起来很耸人听闻,但这是 Linux 内核设计的精髓。
在 Linux 内核中,从来没有一个专门叫 Thread 的数据结构。
-
Windows:有明确的 Process 和 Thread 结构体。进程是容器,线程是执行单元。
-
Linux:只有 task_struct。线程被视为 "轻量级进程 (LWP - Light Weight Process)",Linux 把线程看作是 "共享了大部分资源的进程"。
-
当你调用 pthread_create 时,底层其实是调用了 clone () 系统调用。
-
clone () 创建了一个新的 task_struct,但它没有复制父进程的内存空间、文件描述符表等,而是直接用指针指向父进程的资源。
2.线程task_struct的创建
在 Linux 内核中,创建一个线程(其实就是调用 clone),确实是先申请一个新的 task_struct,然后把父进程 task_struct 里的很多东西拷贝过来,但这里的 "拷贝" 有两种方式:
- 浅拷贝(指针共享)------ 这是线程快的原因。
- 深拷贝(完全复制)------ 这是线程独立性的体现。
1. 内存描述符 (struct mm_struct *mm) ------【浅拷贝】
这是最重的一块(包含了页表、代码段、数据段、堆栈的映射)。
- 进程 (fork): 深拷贝。创建一个全新的 mm_struct,复制父进程的页表(Copy-on-Write)。
- 线程 (clone): 浅拷贝。新线程的 task_struct->mm 指针,直接指向父进程的 mm_struct 地址。
- 结果:所有线程看到的内存一模一样。
2. 文件描述符表 (struct files_struct *files) ------【浅拷贝】
这是打开的文件(fd 0, 1, 2...)。
- 进程:深拷贝。复制一张新的表,虽然指向同一个文件对象,但表是新的。
- 线程:浅拷贝。指针直接指向父进程的 files_struct。
- 结果:线程 A 关闭了 fd 3,线程 B 的 fd 3 也就关闭了。
3. 信号处理 (struct signal_struct *signal) ------【浅拷贝】
- 线程:浅拷贝。大家共用一套信号处理函数(Handler)。
- 结果:这就是为什么信号来了,大家可能都会受影响。
4. 寄存器、栈、上下数据 (struct pt_regs, thread_struct) ------【深拷贝 & 修改】
这是必须独立的。
- 线程:深拷贝并清零。
- PC 指针:修改为新线程的入口函数地址(比如 thread_func)。
- SP 指针 (Stack Pointer):这是最关键的修改!修改为用户态新分配的那块 8MB 栈空间的顶端。
- 结果:虽然大家共用内存,但每个线程有自己的栈帧,函数调用互不干扰。
5. 调度信息 (priority, policy) ------【深拷贝】
- 线程:继承父进程的优先级,但之后可以独立调整。
线程创建之所以快,是因为它 跳过了最耗时的内存页表复制 ,只是简单地复制了一个task_struct 壳子,并把里面的关键指针指向了老爹的资源。
3. PID 的大坑:TGID vs PID
在 Linux 下,每个 task_struct 都有一个唯一的 ID。
- 内核视角 (PID):每个线程都有自己的 ID。
- 主线程 ID = 1001
- 子线程 ID = 1002
- 用户视角 (TGID - Thread Group ID):
- 为了符合 POSIX 标准(规定一个进程内的所有线程 PID 必须相同),Linux 引入了 TGID。
- 所有线程的 task_struct 里,tgid 字段都等于主线程的 ID (1001)。
- 当你调用 getpid () 时,内核返回的是 TGID。
- 当你调用 syscall (SYS_gettid) 时,内核返回的是真实的 PID。
所以一个进程可以有多个task_struct,进程就是n个task_struct+加载到内存的数据和代码
线程就是task_struct!
4.线程的特点
1 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
- 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB(快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件 cache。
- 线程占用的资源要比进程少
- 能充分利用多处理器的可并行数量,适合计算密集型应用。
2 线程的缺点
- 健壮性降低,需要注意共享资源的保护,线程的互斥同步。
3 线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
5 线程控制
1 线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
- thread:返回线程 ID
- attr:设置线程的属性,attr 为 NULL 表示使用默认属性
- start_routine:是个函数地址,线程启动后要执行的函数
- arg:传给线程启动函数的参数
返回值
- 成功:返回 0。
- 失败:返回错误码(如 EAGAIN 表示资源不足,EINVAL 表示参数无效)。
- 注意:pthread 函数不设置 errno 变量,而是直接返回错误码。不要用 perror (),要用 strerror (ret)。
查看线程指令
ps -aL | head -1 && ps -aL | grep mythread
查看线程id
#include <pthread.h>
pthread_t pthread_self(void);
- 参数:无。
- 返回值 :返回当前线程的
pthread_t类型 ID。
2. 线程退出
2.1 pthread_exit
.这是线程主动结束自身的方式,相当于进程中的 exit。
void pthread_exit(void *retval);
参数详解:
- retval (返回值):
- 一个 void* 指针,代表线程的 "退出状态",会被 pthread_join 函数获取。
- 致命陷阱:切勿返回局部变量的地址!线程结束后栈内存会被销毁,返回局部变量地址会导致主线程拿到野指针,访问时崩溃或出现乱码。应返回:
- 堆内存(malloc 分配的内存)。
- 全局变量 / 静态变量的地址。
- 经强制类型转换的简单整数(如 (void*) 100)。
返回值:
- 该函数无返回值(执行后线程即终止,无返回机会)。
- 调用不会失败。
2.2 pthread_cancel
这是强制终止线程的手段,相当于进程中的 kill。
int pthread_cancel(pthread_t thread);
参数详解:
- thread (传入参数):
- 要取消的线程 ID。
关键机制(取消点):
- 非立即生效:调用 pthread_cancel 仅发送取消请求,目标线程需运行到 "取消点 (Cancellation Point)" 时才会真正退出。
- 取消点定义:绝大多数系统调用(如 read、write、sleep、printf 等)均为取消点。若线程在死循环中仅执行纯数学运算,无任何系统调用,则无法被取消。
- 被取消的线程,其退出码(retval)会自动设为宏 PTHREAD_CANCELED(通常为 -1)。
返回值与错误:
- 成功:返回 0(仅代表取消请求发送成功,不表示线程已终止)。
- 失败:返回错误码,常见错误:
- ESRCH:找不到指定线程 ID。
2.3 return
主线程return会导致其他线程终止
3. 等待线程:pthread_join
这是回收线程资源的手段,相当于进程中的 wait/waitpid。若不 join 也不 detach,线程结束后会变成 "僵尸线程",造成内存泄漏。
int pthread_join(pthread_t thread, void **retval);
参数详解:
- thread (传入参数):
- 要等待的线程 ID(由 pthread_create 传出的数值)。
- retval (传出参数):
- 二级指针(void**),用于接收目标线程通过 pthread_exit 传出的返回值。
- 不关心返回值时可传 NULL。
- 示例:
void* res; pthread_join(tid, &res);执行后,res 会指向子线程返回的数据。
返回值与错误:
- 成功:返回 0,该函数为阻塞式,会一直等待直到目标线程退出。
- 失败:返回错误码,常见错误:
- ESRCH:找不到指定线程 ID(线程可能已退出并被回收,或 ID 错误)。
- EINVAL:目标线程为 "分离状态"(Detached),或已有其他线程在 join 该线程(一个线程只能被 join 一次)。
- EDEADLK:死锁,典型情况是线程自身调用 pthread_join 等待自己,会导致永久卡死
4. 分离线程:pthread_detach
这是告知系统自动回收线程资源的方式,即 "线程结束后由系统自动释放资源"。
int pthread_detach(pthread_t thread);
参数详解:
- thread (传入参数):
- 要设置为分离态的线程 ID。
作用详解:
- 默认情况下,线程为 joinable 状态,必须通过 pthread_join 回收资源。
- 调用 pthread_detach 后,线程变为 detached 状态,结束后系统会自动释放其 PCB 和栈内存。
- 注意:线程被分离后,不能再调用 pthread_join,否则会报 EINVAL 错误。
- 常用用法:
- 主线程创建子线程后,立即调用
pthread_detach(tid)。 - 子线程在自身代码开头调用
pthread_detach(pthread_self())。
- 主线程创建子线程后,立即调用
返回值与错误:
- 成功:返回 0。
- 失败:返回错误码,常见错误:
- EINVAL:线程已处于分离状态。
- ESRCH:找不到指定线程 ID。
Pthread 函数出错时,绝大部分不会设置全局变量
errno,而是将错误码直接作为函数返回值返回。因此,检查 Pthread 函数的错误时,应通过接收函数返回值并判断是否为 0 来实现:而非通过查询全局变量errno来判断错误(这种方式对 Pthread 函数无效)。