Linux初阶——线程(Part1)

news/2024/10/31 0:15:53/

一、线程概念

1、如何理解线程

说到线程,那么我们就要回到进程了。

1.1. 再谈进程

对一个进程来说,它在内存中是这样的:
 

图1.1-a

其中一个 task_struct 独享一个进程地址空间和一个页表。 而线程其实和进程差不多,是这样的:
 

图 1.1-b

其中多个 task_struct 共享一个进程地址空间,即共享进程地址空间里的正文代码、堆、变量等。所以,其实线程就是上图的一个个 task_struct , 然后这一个个 task_struct 分别执行进程代码的不同部分。所以也可以说因为进程的执行是由若干个线程的执行组成的。而也正因为线程执行的代码比进程的要少得多,因此我们称线程的执行粒度比进程要细得多。

所以,那真正的进程是什么样的呢?其实是这样的:

图 1.1-c

但是我们或许会产生这样一个疑问:那是不是以前进程的那些定义都错了呢,一个进程不是只有一个 PCB 吗,而在 Linux 里不就叫 task_struct 吗?其实这两者是不矛盾的,其实在只有一个 task_struct 的情况下,这个 task_struct 也是一个线程,只不过因为只有它一个,因此刚好独享了进程地址空间的全部资源罢了,所以进程的图就是图 1.1-a 样子了。所以,其实图 1.1-c 才是更普适的情况。 

1.2. 理解线程

如果我们新建一个进程(包括 fork 一个子进程),那么操作系统就要为这个新进程开辟新的 task_struct、进程地址空间和页表。而如果我们新建一个线程,那么操作系统就只需要创建一个新的 task_struct 就可以了,然后和旧的 task_struct 一起共享页表和进程地址空间,然后不同的 task_struct 各自执行不同部分的代码。

所以,新建一个线程,操作系统除了会开辟空间当作 task_struct 外,就不会额外开辟其他空间了;但是新建一个进程,操作系统就会开辟新的 task_struct、进程地址空间和页表,简称新的系统资源。所以我们可以得出一个结论:操作系统执行代码是通过一个一个线程执行的,但操作系统只会对一个个进程分配系统资源(内存),并不会对一个个线程分配系统资源(内存)。

1.3. 线程与进程的关系

因为进程的执行是由若干个线程的执行组成的,但是操作系统是对进程发信号的;因此如果其中一个线程报错发信号,那么就会导致整个进程退出。那么既然整个进程都退出了,那进程里的全部线程绝对也全都退出了。所以我们可以得出一个结论:只要有一个线程挂了,那么整个进程里的线程也会全部挂掉。

2、二级页表(32 位)

当机器字长为 32 位时,内存里的地址只能用 32 位表示。但是我们知道如果进程地址空间只用了一级页表储存映射关系的话,而且进程地址空间总共是 4GB,因此页表的总大小也会是 4GB。可是这只是一个进程的,而且只是页表的大小,还没算代码和数据呢,如果算上这些的话,那么一个进程所占的空间就太大了,操作系统是不可能支持几十个进程载入内存的。所以如何缩小页表的大小呢?那么我们就要说说二级页表了。

2.1. 重新看待 32 位地址号

二级页表对 32 位地址号进行了拆分,采用了 10-10-12 的格式。高 10 位是指在哪一个二级页表;中 10 位是指在这个二级页表的哪一个内存块;而低 12 位是指这个内存块内的哪一行。

二级页表

举个例子,如果地址为 0000000111 0000001000 000100101100,那么就说明要访问第 7 号二级页表中的第 8 号个内存块的第 300 号行。注意,所有的编号都是从零开始的。

2.2. 回到二级页表

得益于对 32 位地址的分段解读,虽然全部的二级页表的总行数还是和原来不用二级页表时的页表一样多;但通过二级页表,我们可以把原来的一级页表分成若干段,然后需要时就只把那一段的表加载进内存就行了;而不用像原来那样把整张一级页表加载进内存。这样就可以大幅减少页表所占用的内存空间了。

同时我们还可以发现,正是因为用了 10 位来表示表(一级页表和二级页表)中的编号,使得每个表的大小都是 4KB——刚好可以用一个内存块就可以装满了。因此操作系统开任何空间的大小都是按 4KB 的大小来开的,只不过我们可以用到的就不一定是 4KB 了。

3、线程周边概念

3.1. 执行流

因为 Linux 中,CPU 只认 task_struct,并不会认是不是进程或是不是线程,因此操作系统是没有进程或线程这一概念的,它只认 task_struct ,因此一个 task_struct 就是一个执行流了。

3.2. 主线程 & 新线程

其中 LWP(轻量型进程)等于 PID 的那个线程就是主线程,其他都是新线程。 

二、线程的控制

1、线程的创建

1.1. clone 函数

参数介绍

  • fn:新线程要执行那部分的代码的起始地址。
  • stack: 该线程的线程栈,用于存储该线程的数据和变量,以及该线程的函数的栈帧。

这个函数是用来新建线程的,就如同 fork 函数一样。 但是,由于这个函数的参数是在太复杂了,因此线程库就把这个函数封装了起来;即在 pthread_create 函数和 pthread_join 函数的内部调用 clone 函数。

1.2. pthread_create 函数

参数介绍

  • thread:输出型参数。把线程 ID 带出来。
  • attr:输入型参数。输入该线程的属性,如果不输入就用默认属性。
  • start_routine:就是 clone 函数的 fn。
  • arg:输入型参数。输入 start_routine 函数的实参。

这个函数就是用来创建线程的。成功返回 0,失败返回其他值。 

1.3. 线程 ID(pthread_t)

1.3.1. 为什么线程库要维护线程的概念

为什么线程库只要维护线程的概念,而不用维护线程的执行流(task_struct)?因为用户在用线程时,就指想获取线程有关的属性,比如线程 ID、线程的时间片、回调函数、独立栈等;但并不需要关注 task_struct 的编号、PID等那些 task_struct 的信息;因此线程库就没必要包含与 task_struct 的属性有关的东西了(不用描述 task_struct 了),取而代之的是,为了实现线程与执行流的概念,线程库中会把 tcb 与执行流建立连接,然后当线程要执行它的代码时,操作系统直接调用它的执行流就行了,但值得注意的是,此时用户并不关心操作系统调用的执行流,只会关心线程的属性!!!

1.3.2. 线程栈

由 clone 函数可知,创建一个线程时,不仅需要执行代码的起始地址,还要传一个栈的地址进去。而这个栈就是线程栈。

1.3.3. 线程 tid & 线程控制块 tcb

因为 Linux 是没有线程的概念的,只有 task_struct 的概念;因此在线程库内部就要维护线程的概念,而要维护线程的概念,就必须对多个线程进行管理,于是就要对线程进行“先描述,再组织”。而如何描述一个线程呢?其实 clone 函数和 pthread_create 函数就已经给出答案了:每个线程都要有自己的 ID,以及其他属性,还有自己的独享的栈。但这些都是在库内部维护的,因此如果要访问这些线程,那么就要把库加载到内存;但线程库是动态库,因此经过页表映射后是映射到进程地址空间的共享区里的;而线程这些概念是线程库在描述和组织,是线程库里的代码,因此线程的结构体和组织线程的数据结构、以及线程的栈都被加载到共享区里了,而不是进程地址空间的栈里。但主线程非常特殊,它的栈就是进程地址空间里的栈。

因为一个线程是由线程的属性(权限、时间片等)和线程栈组成的,因此在描述线程的结构体 tcb 肯定也是有这几个成分的。因此 tcb 的结构大概长这样:

而 tcb 的地址就是这个线程的 tid。 

1.3.4. pthread_self 函数

该函数用于获取线程的 tid。 

1.3.5. 线程的局部存储—— __thread 关键字

我们知道,全部线程是可以访问全局变量的。如果我线程想要一个私有的全局变量呢?那就要提到 __thread 关键字了。

__thread int count = 0; // 每个线程都有一个私有的 count 全局变量

以上面的例子为例,有了 __thread 之后,count 变量就不存在进程地址空间的数据区里了,而是存在每个线程 TCB 的线程局部存储区里了。

或许我们会想:那我在执行流的函数里定义个 count 变量不也一样吗?至于用这个私有的全局变量吗?其实,这个私有的全局变量同时适用于一个线程要执行多个函数这种情况。在这种情况下,该线程调用的所有函数都可以访问到这个 count;但如果只在执行流的函数里定义个 count 变量,该执行流调用的其他函数是无法访问这个 count 的。

注意:__thread 只能用于内置类型。

2、线程的回收

2.1. pthread_join 函数

参数介绍

  • thread:输入型参数。线程的 tid
  • value_ptr: 输出型参数。用来获取线程执行的函数的结果。如何获取呢?一般都会把线程执行的函数先强转成(void*)指针再返回。然后在给这个形参传 void* 指针变量的地址。
  • 返回值:成功返回 0,错误返回其他值。

举个例子:

void* thread_run(void* arg)
{return (void*)100;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, thread_run, nullptr);void* ret_val;pthread_join(tid, &ret_val);int ret = static_cast<int>(ret_val);return 0;
}

 这样就可以获取线程执行的函数的结果啦!

2.2. 线程分离—— pthread_detach 函数

在默认情况下,新建的线程的释放都要显式调用 pthread_join 函数。但如果我们不关心线程运行的结果,我们可以在线程执行的函数调 pthread_detach 函数,告诉系统当线程执行完后可以自动回收线程。于是我们就不用显式地调用 pthread_join 函数来释放线程了。

参数介绍

  • thread:线程的 tid。
  • 返回值:成功返回 0,失败返回其他值。

http://www.ppmy.cn/news/1543219.html

相关文章

Go 语言的函数参数传递

在编程中,函数参数的传递是一个基本概念,它决定了函数如何接收输入并如何影响原始数据。Go 语言以其简单明了的语法和高效的性能受到开发者的喜爱,而其参数传递机制在这方面尤为重要。本文将详细探讨 Go 语言中的参数传递方式,包括值传递、引用传递、可变参数和实际应用示例…

【Python数据分析系列】json.loads和json.dumps的用法和区别(案例+源码)

这是我的第370篇原创文章。 一、引言 json.loads 和 json.dumps 是 Python 标准库 json 模块中的两个函数&#xff0c;用于处理 JSON 格式数据。 二、实现过程 2.1 json.loads() json.loads&#xff1a;将 JSON 格式的字符串&#xff08;即 JSON 对象的文本表示&#xff09;转…

【万兴科技-注册_登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

vue 使用npm命令的时候报错 ERESOLVE could not resolve

vue在通过npm install element-plus --save命令安装element-plus的时候报错&#xff1a; error code ERESOLVE error ERESOLVE could not resolve While resolving: vue/eslint-config-standard6.1.0 Found: eslint-plugin-vue8.7.1[2m[22m 大概应该是eslint相关的版本冲突…

第三章 使用DQL命令查询数据

文章目录 第三章 使用DQL命令查询数据1. DQL语言1.2 指定查询字段SELECT1.3 AS子句&#xff08;别名&#xff09;1.4 DISTINCT关键字1.5 where条件语句1.6 BETWEEN AND范围查询1.7 LIKE模糊查询1.8 使用IN进行范围查询1.9 NULL空值条件查询 2. 连接查询&#xff08;多表查询&am…

Vuejs设计与实现 — 同构渲染

前言 Vue.js 是一个构建客户端应用的框架&#xff0c;组件的代码会在浏览器中运行&#xff0c;然后向页面输出 DOM 元素&#xff0c;也就是我们最常用的方式&#xff0c;即 客户端渲染&#xff08;client-side rendering&#xff0c;CSR&#xff09;. 实际上 Vue.js 还可以在…

基于JSP+Servlet+MyBatis+SQL Server实现的仓库管理系统(论文+数据库+源码)

### 基于WEB的仓库管理系统 开发工具&#xff1a;Eclipse,Java,Sql Server,MyBatis 该系统旨在实现高效的仓库出入库管理&#xff0c;主要功能包括&#xff1a; 1. **入库模块**&#xff1a;支持新增商品入库或已存在商品的再入库操作。 2. **出库模块**&#xff1a;对已入…

qt工程添加虚拟键盘插件qtvirtualkeyboard

1.主函数导入模块 qputenv("QT_IM_MODULE", QByteArray("qtvirtualkeyboard")); 这时候debug,点lineedit就会弹出虚拟键盘了。 ps:qlineedit文本类型决定输入法显示风格&#xff0c;默认是全功能键盘可以切换。 minLineEdit->setInputMethodHints(Qt…