深入 Linux 线程:从内核实现到用户态实践,解锁线程创建、同步、调度与性能优化的完整指南

发布于:2025-08-14 ⋅ 阅读:(18) ⋅ 点赞:(0)

一.浅谈线程

线程是操作系统能够进行运算调度的最小单位 ,它被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件描述符等,但每个线程有自己独立的程序计数器、栈和寄存器等。

从特点来看,线程具有以下几个显著特性:

  • 轻量级:线程的创建和销毁成本相对较低,切换速度也比进程快,这是因为线程共享进程资源,不需要像进程那样进行复杂的资源分配和回收。
  • 共享性同一进程内的线程共享进程的地址空间和其他资源,这使得线程之间的通信更加便捷高效,它们可以通过直接访问共享内存来交换数据。
  • 并发性:多个线程可以在同一时间间隔内交替执行,在多核处理器环境下,还能实现真正的并行执行,从而提高程序的运行效率。

线程的应用场景非常广泛。在需要同时处理多个任务的程序中,使用线程可以带来很大的好处。例如,在网络服务器中,一个服务器进程可以创建多个线程,每个线程负责处理一个客户端的请求,这样就能够同时为多个客户端提供服务,而不会因为一个客户端的请求处理缓慢而影响其他客户端。这是后面会讲到的CP模型。
不过,线程也带来了一些挑战。由于多个线程共享资源,可能会出现线程安全问题。当多个线程同时访问和修改共享资源时,如果没有适当的同步机制,就可能导致数据不一致、死锁等问题。因此,在多线程编程中,需要使用互斥锁、信号量等同步工具来保证线程之间的协调工作

下面是线程的一些性质:

  • 线程是进程的一个执行分支,线程的执行粒度,比进程要细,可以看作一个进程内有多个线程;
  • 线程是操作系统调度的基本单位,操作系统以进程为单位分配资源,进程将资源细分给线程;
  • 管理线程先描述后组织,用 struct tcb 结构体描述,但重写一套与进程相似的操作耗时费力,Linux 将线程嵌入到PCB中随进程一同管理(windows选择单独管理);
  • 线程需要分配进程的虚拟地址空间,只需要不同线程撰写不同函数即可达到目的
  • 线程更加轻量级,体现在创建和释放以及切换,线程切换cache命中率高,进程切换cache需要重新缓存

二.深入线程

线程会对虚拟地址空间进行资源划分:本质是划分地址空间,获得一定范围的合法虚拟地址,就是在划分页表,进行资源共享,本质是对地址空间的共享,对页表条目的共享。

线程的优点:

1.创建一个新线程的代价要比创建一个新进程小得多;

2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多(比如:线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的,需要保存进程的上下文,对系统的开销要更多);

3.能充分利用多处理器的可并行数量:
①在等待慢速I/O操作结束的同时,程序可执行其他的计算任务:即多线程任务。
②计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现,cpu核数越多;线程就能越多。
③I/O密集型应用,为了提高性能,将I/0操作重叠。线程可以同时等待不同的I/O操作。多线程操作不会完全闲下来。

线程的缺点:

1.性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性线程多难控制能损失指的是增加了额外的同步和调度开销,而可用的资源不变。(线程多难控制)

2.健壮性降低: 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

3.缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成比如可重入函数;原子性;进行的快慢不同影响。(比如可重入函数;原子性;进行的快慢不同)

4.编程难度提高编写与调试一个多线程程序比单线程程序困难得多。

线程异常:

1.单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃等;如信号处理都交给了它所属的进程了。(故线程不能像进程那样查看退出信息等;因为都交给它所属于的进程了,因此使用线程要更加严谨)

2.线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

线程用途:

1.合理的使用多线程,能提高CPU密集型程序的执行效率。

2.合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

三.控制进程

Linux系统在安装时,不管是Centos或是ubunto还是其他的版本,都会默认带有pthread线程库,但不属于系统调用,使用时要引⼊头文件 <pthread.h>,编译时由于头文件和动态库在环境变量中留有默认路径,故只需要指明库即可,-lpthread。

线程创建:

在这里插入图片描述

 int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)

pthread_t *thread 输出型thread id
const pthread_attr_t *attr 线程的属性,通常nullptr
void *(*start_routine) (void *) 函数指针
void *arg 线程函数的参数列表
返回值:成功返回0;失败返回错误码

线程ID及LWP:
对于进程可以使用pa ajx查看一样,线程的查看也有一套类似进程的指令:

ps -aL #查看轻量级进程

在这里插入图片描述
这里的LWP和线程ID不是一个概念:

线程ID只是在线程库的一个线程的虚拟起始地址,通常显示为十六进制值。而LWP就是linux底层模拟线程的这个轻量级进程的进程ID;主线程的LWP与进程的PID相同!

  • LWP ID:是内核级的唯一标识,在整个系统中唯一(与进程 ID 同属一个命名空间)。内核通过 LWP ID 调度线程、管理资源,类似进程 ID(PID)的作用。
  • 线程 ID( pthread_t ):是用户态的标识,仅在创建它的进程内唯一,用于线程库函数(如 pthread_join、pthread_kill)中指定线程。

因此我们可以理解为LWP 是内核对线程的 “身份证”,线程 ID 是用户态对线程的 “昵称”。

线程获取:

在这里插入图片描述
这里的函数返回值就是上面提到的线程ID(thread’s ID);

线程终止:

终止线程可以用三种方法:

  • return
  • pthread_exit
  • pthread_cancel

return:

void* threadFunc(void* arg)
{
    //完成线程分配到的任务
    return nullptr;
}

pthread_exit:

在这里插入图片描述

pthread_exit((void*)114514); // 线程结束时返回值为114514

pthread_cancel:

在这里插入图片描述

#include <pthread.h>
int pthread_cancel(pthread_t thread);

参数: thread:线程ID
返回值:成功返回0;失败返回错误码
对于pthread_join获得返回值:如果线程被别的线程调⽤pthread_cancel异常终止,retval中保存的是PTHREAD_CANCELED,值为-1

线程等待:

线程也需要进行等待,有类似于僵尸进程的情况出现,通常都是主线程最后退出:

pthread_join:

在这里插入图片描述

#include <pthread.h>
 int pthread_join(pthread_t thread, void **retval);

thread : 线程ID
retval :它指向一个指针,指向线程的返回值
返回值:成功返回0;失败返回错误码

调⽤该函数的线程将阻塞等待tid的线程终⽌,thread线程以不同的⽅法终⽌,通过pthread_join得到的终⽌状态是不同的,总结如下:

  1. 如果thread线程通过return返回,retval 所指向的单元⾥存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调⽤pthread_cancel异常终掉,retval 所指向的单元⾥存放的是常 数PTHREAD_ CANCELED (即-1) 。
  3. 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,retval 所指向的单元存放的是传给 pthread_exit的参数。
  4. 如果对thread线程的终⽌状态不感兴趣,可以传nullptr给retval参数。

线程分离:

在这里插入图片描述

 #include <pthread.h>
 int pthread_detach(pthread_t thread);

请注意:如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。如果线程已经分离,就不能join了,否则会报错!

注意:
1.编译时,由于使用第三方库,请带上-lpthread
2.ps -aL可以查看LWP轻量进程id
3.主线程的LWP与进程的pid相同
4.信号是从进程的层面上发送的,结束一个进程时所有线程都会结束
5.所有线程可以共用一个函数或全局变量
6.线程也需要进行等待,有类似于僵尸进程的情况出现,通常都是主线程最后退出

四.从虚拟地址空间理解线程

结合操作系统内核以及虚拟地址空间来看看线程的运行机制:
在这里插入图片描述
性质如下:
1.栈区提供主线程栈,专门给主线程使用的,共享区中处了有动态库还有许多副线程的tcb,(除了主线程,所有其他线程的独立栈,都在共享区,具体来讲是在pthread库中,tid指向的用户tcb的起始地址)tcb就是描述线程的结构体,其中包含struct_pthread,线程局部存储,线程栈(每一个线程需要拥有自己的栈以及临时变量) ,创建线程返回的tid就是那个线程tcb的起始地址!
2.线程的概念是pthread库维护的,线程库需要管理线程
3.创建多个线程并传递对象时,不能在主栈区申请空间,必须在堆上开辟空间!
4.可以使用__thread来将全局变量变为局部存储(每个线程都会拥有该变量的独立副本,而非所有线程共享同一个变量),但不能将非内置类型的对象变为局部存储,因为会在堆上开辟空间
5.当多个线程同时访问一个变量时,会出现数据不一致问题,如何解决?对共享数据的任何访问,保证任何时候只有一个执行流访问——使用锁!
在这里插入图片描述

线程与系统调用clone的关系:

在 Linux 系统中,clone() 函数是创建轻量级进程(包括线程)的底层系统调用,是实现线程机制的核心基础。理解 clone() 与线程创建的关系,需要从 Linux 对进程和线程的底层设计说起:
1. Linux 内核并没有严格区分 “进程” 和 “线程”,而是将两者统一视为任务(task)。它们的区别仅在于资源共享的程度:
普通进程:拥有独立的地址空间、文件描述符表、信号处理等资源,进程间资源不共享。
线程(轻量级进程)与创建它的线程(或进程)共享大部分资源(如地址空间、代码段、全局变量等),仅保留少量私有资源(如栈、寄存器、线程局部存储等)。
2. clone() 是 Linux 特有的系统调用,其功能是创建一个新任务(task),并通过参数控制新任务与父任务的资源共享方式。函数原型简化如下:

int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);

其中最关键的是 flags 参数,它通过一系列标志位(如 CLONE_VM、CLONE_FILES 等)指定新任务与父任务共享哪些资源:CLONE_VM 共享地址空间(内存映射;CLONE_FILES 共享文件描述符表 ;CLONE_SIGHAND 共享信号处理函数;CLONE_THREAD 新任务与父任务属于同一线程组 ;CLONE_PID 共享进程 ID(仅用于内核线程)(了解即可)

  1. clone() 与线程创建的直接关联:
    用户态的线程库(POSIX 线程库 pthread)正是通过调用 clone() 实现线程创建的。例如,pthread_create() 函数的底层会调用 clone(),并传入特定的 flags 以确保新线程与主线程共享大部分资源:
// 线程创建时 clone() 的典型 flags 参数组合
int flags = CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | ...;

网站公告

今日签到

点亮在社区的每一天
去签到