【Linux操作系统】简学深悟启示录:线程概念与控制
本文介绍了线程的基本概念和控制方法。线程作为轻量级进程,共享进程资源但拥有独立执行流,是操作系统的调度基本单位。文章详细讲解了线程与进程的区别,线程的创建、终止和等待操作,包括使用pthread_create创建线程、pthread_exit终止线程以及pthread_join等待线程回收资源。同时介绍了线程特有的ID、栈和寄存器等私有数据,以及共享的文件描述符等资源。通过对比用户态TID和内核态
文章目录
1.线程概念
1.1 理解线程

要理解线程,直接从进程入手,每个进程都是一个个得task_struct,当我们需要执行一个新任务时,操作系统以进程为单位,给我们分配资源,但是有时候执行得任务不需要重新调配资源那么麻烦,就需要线程了
对于Linux系统,并没有真正意义上的线程,我们将线程称作轻量级进程,地址空间作为进程的资源窗口,线程就可以看作进程里的一个个执行流,共享资源各自执行各自的执行流
简单类比: 进程像独立的 “工厂车间”(有专属内存、文件句柄等资源),线程像车间里的 “工人”(共享车间资源,只占少量执行上下文),工人干活(执行任务)的粒度更细,但必须依托车间存在
1.2 重新定义线程和进程
线程: 操作系统的调度基本单位,是进程内部的执行流资源
进程: 进程是承担系统资源分配的基本实体
1.3 进程地址空间第四讲

在理解进程地址空间时,我们并不知道页表的转换机制,那么虚拟地址是如何转换到物理地址的呢?

这里我们以 32 位的虚拟地址为例,那么通常将其分为 32 = 10 + 10 + 12,第一个 10 位表示一级页目录(通常页目录有 1024 个条目),每个条目存放一个二级页表的条目地址,第二个 10 位就表示二级页表的条目地址,二级页表的条目里存放的是物理内存页框的起始地址,最后通过页框加上最后 12 位的偏移量查找到特定地址内容,由于是 12 位地址(大约 2¹² 的大小),恰好符合页框的 4KB 大小,所以偏移量不存在超出页框的情况
对于一个变量 int a = 10,是 4 个字节,难道要拿四个地址吗,其实不是的,只需要找到其实地址,编译器本身就知道内置类型的偏移量(自定义类型也是由内置类型组成的),所以起始地址+类型就能知道整个变量的内容了
单级页表对连续内存要求高,于是引入了多级页表,但是多级页表也是一把双刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率

有没有提升效率的办法呢?计算机科学中,很多问题都能通过添加中间层解决,MMU(内存管理单元)就引入了这样一个“新武器”—— TLB(转译后备缓冲器,Translation Lookaside Buffer,江湖人称快表,本质是一种缓存)
具体工作流程如下:
CPU向MMU传输新的虚拟地址后,MMU会优先查询TLB中是否存在对应的映射关系- 若
TLB中存在该映射(即Cache Hit),则直接获取物理地址,通过总线发送给内存,快速完成地址转译 - 由于
TLB容量较小,难免出现缓存未命中(Cache Miss)的情况。此时MMU会启用“老武器”页表,在页表中查找对应的物理地址 - 找到物理地址后,
MMU除了将地址通过总线传给内存,还会把该虚拟地址与物理地址的映射关系写入TLB,完成缓存刷新,方便后续快速查询
1.4 进程VS线程
线程共享进程数据,但也拥有自己的⼀部分"私有"数据:
- 线程
ID - ⼀组寄存器,线程的上下文数据
- 栈
errno- 信号屏蔽字
- 调度优先级

同一地址空间,因此 Text Segment 、Data Segment 都是共享的,如果定义⼀个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(
SIG_ IGN、SIG_ DFL或者⾃定义的信号处理函数) - 当前工作目录
- 用户
id和组id
2.线程控制
2.1 线程的链接与查看
mythread:mythread.cpp
g++ -o $@ $^ -lpthread
.PHONY:clean
clean:
rm -f mythread
与线程有关的函数构成了⼀个完整的系列,绝大多数函数的名字都是以 pthread_ 打头的,要使用这些函数库,要通过引入头文 <pthread.h> 链接这些线程函数库时要使用编译器命令的 -lpthread 选项

用 ldd 命令查看确实链接上了,那么如何查看线程呢?

使用 ps 命令带上 -L 就表示查看 LWP,这是轻量级进程的缩写,LWP 的 id 也叫 tid,还可以通过 pthread 库中有函数 pthread_self() 得到,它返回⼀个 pthread_t 类型的变量
pthread_self()得到的是「用户态TID」:属于POSIX线程库层面,仅当前进程内唯一,类型是pthread_t(可能是指针),用于用户态线程间识别ps查到的LWP(Light Weight Process)是「内核态TID」:属于操作系统内核层面,全系统唯一,是整数(如12345),是内核调度的实际标识
2.2 创建线程

参数:
thread:返回线程IDattr:设置线程的属性,attr为NULL表示使用默认属性start_routine:是个函数地址,线程启动后要执行的函数arg:传给线程启动函数的参数
返回值: 成功返回 0;失败返回错误码
pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这样做)。而是将错
误代码通过返回值返回
2.3 终止线程


线程退出有三种方法:
- 线程可通过从线程函数
return的方式终止自身 - ⼀个线程可以调用
pthread_ cancel()终止同一进程中的另一个线程,一般由主线程来调用 - 线程也可主动调用
pthread_exit()终止自身
🔥值得注意的是: pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用 malloc 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时该线程函数已经退出了
2.4 等待线程

参数:
thread:线程IDvalue_ptr:它指向⼀个指针,后者指向线程的返回值
返回值: 成功返回 0;失败返回错误码
阻塞调用方(一般是主线程),默认是阻塞等待,等待目标线程退出,然后回收其内核资源(如线程描述符),并获取退出状态。其实和进程等待的 waitpid 很像
| 维度 | pthread_exit(void *retval) | pthread_cancel(pthread_t thread) | pthread_join(pthread_t thread, void **retval) |
|---|---|---|---|
| 核心作用 | 线程主动终止自身(正常退出) | 向目标线程发送「终止请求」(请求他杀) | 阻塞当前线程,等待目标线程退出 + 回收资源 + 获取返回值 |
| 调用方 | 线程自身(自己退出自己) | 其他线程(如主线程终止子线程) | 其他线程(如主线程等待子线程) |
| 线程状态影响 | 调用线程立即退出(生命周期结束) | 目标线程若响应,会被终止(生命周期结束) | 不影响目标线程,仅阻塞调用方线程 |
| 是否阻塞 | 不阻塞(调用后线程直接退出,不返回) | 非阻塞(发送请求后立即返回,不等待结果) | 阻塞(直到目标线程退出才返回) |
| 返回值/结果 | 通过 retval 传递退出状态给 pthread_join() |
返回 0 = 请求发送成功(不代表线程已退出) | 返回 0 = 线程退出且资源回收成功;retval 接收目标线程退出状态 |
| 目标线程要求 | 无(自身调用,无需指定目标) | 不能是分离线程(PTHREAD_CREATE_DETACHED) |
不能是分离线程(分离线程自动回收资源) |
| 资源回收 | 不主动回收自身资源,需 pthread_join() 处理 |
不回收目标线程资源,需 pthread_join() 处理 |
核心职责:回收目标线程内核资源(避免僵尸线程) |
| 典型场景 | 线程完成任务后正常退出 | 终止超时、卡住或无需继续运行的线程 | 确保子线程完成任务 + 回收资源 + 获取退出状态 |
2.5 分离线程

这个函数其实和 pthread_join 很像,但是这个函数是给线程定义为分离状态,让线程在执行完之后自动回收资源释放,适用于不需要拿线程返回结果的情况,joinable 和分离是冲突的,⼀个线程不能既是 joinable 又是分离的
核心区别: detach 是 “自动回收、不等待、无结果”,join 是 “手动回收、阻塞等、拿结果”
3.线程ID及地址空间布局
我们知道 pthread_self 能够返回 tid,即线程自身的 ID,也就是 pthread_create 的那个(用户态层面),可是为什么 tid 是一串很长的数字呢?

其实 pthread 库也是个动态库,存放于共享区里,每个线程在库里都有自己的独立栈,即 TCB,将 tid 用 %p 打印出来就会发现和每个独立栈的地址是一样的,因此我们可知每一个线程库独立栈的起始地址就是 tid
#include <iostream>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
#define NUM 10
//int g_val = 0;
//__thread int g_val = 0;
struct threadData
{
string threadname;
};
string toHex(pthread_t tid)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%lx", tid);
return buffer;
}
void InitThreadData(threadData* td, int number)
{
td->threadname = "thread-" + to_string(number);
}
void* threadRoutine(void* args)
{
threadData* td = static_cast<threadData*>(args);
int i = 0;
while(i < 10)
{
cout << "pid: " << getpid() << ", tid: " << toHex(pthread_self()) << ", threadname: " << td->threadname << ", i: " << toHex((pthread_t)i) << endl;
sleep(1);
i++;
}
delete td;
return nullptr;
}
int main()
{
vector<pthread_t> tids;
for(int i = 0; i < NUM; i++)
{
pthread_t tid;
threadData* td = new threadData();
InitThreadData(td, i);
pthread_create(&tid, nullptr, threadRoutine, td);
tids.push_back(tid);
sleep(1);
}
for(int i = 0; i < tids.size(); ++i)
{
pthread_join(tids[i], nullptr);
}
}
假设我们创建一个多线程,然后每个线程共用一个threadRoutine方法函数,分为三种情况:
- 每个
threadRoutine方法函数里都有一个i变量,那么查看地址可以发现,每个线程都有自己的独立栈,i都有自己的地址,i和线程一一对应 - 如果不是自己的局部变量呢?那么使用全局变量int g_val = 0就是操作同一个变量了
- 如果每个线程想要有自己的全局变量呢,可以使用
__thread int g_val = 0,__thread是个编译选项,这叫线程的局部存储,前提是只能使用内置类型,不能使用自定义类型
希望读者们多多三连支持
小编会继续更新
你们的鼓励就是我前进的动力!

更多推荐



所有评论(0)