线程概念与控制

article/2025/6/22 17:51:41

目录

 

Linux线程概念

什么是线程

分页式存储管理

虚拟地址和页表的由来

物理内存管理

页表

提问

解答

缺页异常

线程的优点

线程的缺点

线程异常

Linux进程VS线程

进程与线程

进程的多个线程共享

进程与线程关系如图

Linux线程控制

POSIX线程库

创建线程

测试

获取线程ID

线程终⽌

 

线程等待

测试

分离线程

测试


 

Linux线程概念
 

什么是线程
 

1.在⼀个程序⾥的⼀个执行路线就叫做线程(thread)。更准确的定义是:线程是“⼀个进程内部
的控制序列”
2.⼀切进程至少都有⼀个执行线程

3.线程在进程内部运行,本质是在进程地址空间内运行

4.在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化

5.透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程控制流

分页式存储管理

虚拟地址和页表的由来


如果在没有虚拟内存和分页机制的情况下,每一个用户程序在物理内存上所对应的空间必须是连续的

但是不同的程序他的代码与数据长度都是不一样的,并且一直会有进程退出,如果采用连续内存的方式就会导致存在很多的内存碎片。

因此,我们希望操作系统提供给用户的空间必须是连续的,但是物理内存最好不要连续。

由此,虚拟地址与页表就诞生了

把物理内存按照⼀个固定的长度的页框进行分割,有时叫做物理页。每个页框包含⼀个物理页

⼀个页的大小等于页框的大小。大多数 32位 体系结构支持4KB的页,而64位体系结
构⼀般会支持 8KB 的页。

页:一个数据块,存放于页框或磁盘上

页框:一个存储区域

 

有了这种机制,CPU便并非是直接访问物理内存地址,而是通过虚拟地址空间来间接的访问物理内存地址。所谓的虚拟地址空间,是操作系统为每⼀个正在执行的进程分配的⼀个逻辑地址。
 

操作系统通过将虚拟地址空间和物理内存地址之间建立映射关系,也就是页表,这张表上记录了每⼀对页和页框的映射关系,能让CPU间接的访问物理内存地址。

总结⼀下,其思想是将虚拟内存下的逻辑地址空间分为若⼲页,将物理内存空间分为若⼲页框,通过页表便能把连续的虚拟内存,映射到若干个不连续的物理内存页。这样就解决了使⽤连续的物理内存造成的碎片问题。

物理内存管理
 

假设⼀个可⽤的物理内存有4GB的空间。按照⼀个页框的大小4KB进行划分, 4GB的空间就是4GB/4KB = 1048576个页框。有这么多的物理页,操作系统肯定是要将其管理起来的,操作系统需要知道哪些页正在被使用,哪些页空闲等等。

 

内核用 struct page 结构表示系统中的每个物理页,出于节省内存的考虑, struct page 中使
用了大量的联合体union

注意的是 struct page 与物理页相关,而并非与虚拟页相关。⽽系统中的每个物理页都要分配⼀
个这样的结构体,让我们来算算对所有这些页都这么做,到底要消耗掉多少内存。

我们算一个struct page 40个字节,一个页4kb,那么一个struct page的总数大概为页总数的1/100。

系统4GB的空间,struct page大概占40M,相对于系统的4GB内存来说并不算多

 

要知道的是,页的大小对于内存利用和系统开销来说非常重要,页太大,页必然会剩余较大不能利用的空间(页内碎片)。页太小,虽然可以减小页内碎片的大小,但是页太多,会使得页表太长而占用内存,同时系统频繁地进行页转化,加重系统开销。因此,页的大小应该适中,通常512B -8KB ,windows系统的页框大小为4KB。

页表
 

页表中的每⼀个表项,指向⼀个物理页的开始地址。在 32 位系统中,虚拟内存的最大空间是 4GB ,这是每⼀个用户程序都拥有的虚拟内存空间。既然需要让 4GB 的虚拟内存全部可用,那么页表中就需要能够表示这所有的 4GB 空间,那么就一共需要 4GB/4KB = 1048576 个表项。

虚拟内存仍然是连续的,图中的虚线只是用来表示虚拟内存单元与页表每一个表项的映射关系

最终实现,虚拟地址上连续,物理地址上分散,并且解决了内存碎片化的问题

提问

在 32 位系统中,地址的长度是 4 个字节,那么页表中的每⼀个表项就是占用 4 个字节。所以页表占据的总空间大小就是: 1048576*4 = 4MB的大小。也就是说映射表自己本身,就要占用4MB /4KB = 1024 个物理页。这会存在哪些问题呢?

1.我们创建页表的目的就是为了将进程划分为可以一个个页,可以不用连续的存放在物理地址。

但是我们页表自己就需要1024个连续物理页,与我们一开始的想法冲突

2.很多时候进程都是需要访问部分物理页,没有必要让所有物理页都一直占据内存空间

解答

解决大容量页表的方法就是将页表也看作文件,对页表也进行分页,因此,多级页表的思想就产生了。

将一个大页表拆成1024个小页表(每个表1024个表项),这样,1024(表的个数)*1024(每个表中表项)个4k的小页表同样可以占据4GB的物理内存空间。

 

从总数上看,整张大页表仍然需要4M空间,似乎和之前没区别,但实际上一个应用程序不可能占据4GB的所有内存空间,或许几十个小页表就够了,一个程序的代码段,数据段,栈段一共需要10M,也就是3张小页表就够了

 

缺页异常
 

CPU给MMU的虚拟地址,在 TLB 和页表都没有找到对应的物理页,该怎么办呢?其实这就是缺页异常 Page Fault ,它是⼀个由硬件中断触发的可以由软件逻辑纠正的错误。

由于CPU没有数据就无法进行计算,CPU罢工了,用户进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler处理。

缺页中断会交给 PageFaultHandler 处理,其根据缺页中断的不同类型会进行不同的处理:
 

1.Hard Page Fault 也被称为 Major Page Fault ,翻译为硬缺页错误/主要缺页错误,这
时物理内存中没有对应的物理页,需要CPU打开磁盘设备读取到物理内存中,再让MMU建立虚拟
地址和物理地址的映射。

2.Soft Page Fault 也被称为 Minor Page Fault ,翻译为软缺页错误/次要缺页错误,这
时物理内存中是存在对应物理页的,只不过可能是其他进程调入的,发出缺页异常的进程不知道
而已,此时MMU只需要建立映射即可,无需从磁盘读取写⼊内存,⼀般出现在多进程共享内存区
域。
3.Invalid Page Fault 翻译为无效缺页错误,比如进程访问的内存地址越界访问,右比如对
空指针解引用内核就会报 segment fault 错误中断进程直接挂掉。

线程的优点
 

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

2.与进程切换相比,线程切换操作系统要做的工作也小很多

例如:1.由于线程属于同一个进程,拥有相同的虚拟地址空间。2.进程上下文的切换会扰乱处理器的缓存机制,这将导致内存的访问在一段时间内效率很低,在线程的切换中就没有这个问题。

3.线程占用的资源要比进程小很多

4.能充分利用多处理器的可并行数量

5.在等待慢速IO工作完成的同时也可以执行计算任务

6.计算密集型应用为了在多处理器系统上运行,将计算分解到多线程中运行

7.IO密集型应用,为了提高性能将I/O操作重叠。线程可同时等待不同的I/O操作

线程的缺点
 

性能损失
⼀个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同⼀个处理器。如果计
算密集型线程的数量⽐可⽤的处理器多,那么可能会有较大的性能损失,这⾥的性能损失指
的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序里,因时间分配上的细微偏差或者
因共享了不该共享的变量⽽造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护
的。
缺乏访问控制
进程是访问控制的基本粒度,在⼀个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高

线程异常
 

一个进程中的多个线程同属于该进程,那就意味着一旦某个进程出现异常例如野指针导致的不仅仅是那个线程退出,还是整个进程的退出。因为线程出了异常就是进程出异常。

 

Linux进程VS线程
 

进程与线程

1.进程是资源分配的基本单位(所有线程共享进程资源)

2.线程是调度的基本单位(这意味着每个线程分配到的时间片和进程本身无关)

3.线程共享一部分数据,同时还拥有自己的一部分数据:

线程ID、一组寄存器、栈、errno信号屏蔽字、调度优先级

进程的多个线程共享
 

同⼀地址空间,因此Text Segment、Data Segment都是共享的,如果定义⼀个函数,在各线程中都可以调用,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表、每种信号的处理方式、当前工作目录、用户ID与组ID

进程与线程关系如图

Linux线程控制
 

POSIX线程库

1.与线程有关的函数构成了⼀个完整的系列,绝大多数函数的名字都是以“pthread_”打头的

2.要使用这些函数库,要引入头文件<pthread.h>

3.连接这些函数库需要使用编译器命令的 -lpthread选项,也就是使用系统路径下的libpthread.so 动态库,头文件与库文件都在系统默认路径下

创建线程

#include<pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);thread:返回线程ID
attr:设置线程属性,设置为nullptr则采用默认配置
start_routine:是个函数地址,线程启动后要执⾏的函数
arg:传给线程启动函数的参数返回值:成功返回0
错误返回错误码

测试

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
void* run(void* arg)
{int i=0;while(true){i++;printf("我是线程1:%d\n",i);sleep(1);}
}int main()
{pthread_t tid;int ret;if(ret=pthread_create(&tid,nullptr,run,nullptr)!=0){fprintf(stderr,"pthread create:%s",strerror(ret));exit(EXIT_FAILURE);}while(true){printf("我是主线程\n");sleep(1);}
}

获取线程ID

#include<pthread.h>pthread_t pthread_self(void);

这个函数返回“进程级线程ID

这个“ID”是pthread库给每个线程定义的进程内唯⼀标识,是pthread库维持的

当然了,这个ID是进程级的,操作系统并不认识

其实pthread库也是通过内核提供的系统调⽤(例如clone)来创建线程的,而内核会为每个线程创建系统全局唯⼀的“ID”来唯⼀标识这个线程。

-L 选项:打印线程信息
 

我们可以看到,两个线程拥有相同的进程ID,但是操作系统为他们分配了不同的“LWP(即系统级线程ID)

LWP得到的是真正的线程ID。之前使⽤pthread_self得到的这个数实际上是⼀个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。

ps -aL看到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟
地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。


线程终止

 

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调⽤exit。
2. 线程可以调pthread_exit终止自己
3. ⼀个线程可以调⽤pthread_cancel终⽌同⼀进程中的另⼀个线程。

pthread_exit函数
功能:线程终止void pthread_exit(void *value_ptr);value_ptr:value_ptr不要指向⼀个局部变量,因为这个返回值是要交给其他线程查看的,
如果指向局部变量那么线程结束时,局部变量也会被一起销毁
因此,value_ptr最好是全局变量的地址或者是堆上开辟的空间⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)

如果采用return返回,那么要求也和pthread_exit相同,不要指向一个局部变量
 

功能:取消⼀个执⾏中的线程int pthread_cancel(pthread_t thread);thread:线程ID返回值:
成功返回0
失败返回错误码

线程等待
 

为什么需要线程等待

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
 

这里其实和进程等待差不多,如果不等待的话那么线程也会一直占有空间不释放,相当于“僵尸线程

功能:等待线程结束int pthread_join(pthread_t thread, void **value_ptr);thread:指定线程IDvalue_ptr:因为线程的返回值是一个一级指针,
所以我们就需要使用二级指针才能拿到线程退出时返回的值返回值:
成功返回0
失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过
pthread_join得到的终止状态是不同的,总结如下:

1.使用return 或pthread_exit函数退出,我们知道exit和return非常类似,所以使用这两种方式退出时,value_ptr指针上的值就是return的返回值或时传给pthread_exit函数的参数。

2.如果线程是被别的线程使用pthread_cancel终止掉的,value_ptr所指向的单元里存放的是
就为常数PTHREAD_CANCELED

3.如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数

测试

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)//1号线程return返回,在堆上开辟空间带出返回值
{printf("thread 1 returning ... \n");int *p = (int *)malloc(sizeof(int));*p = 1;return (void *)p;
}
void *thread2(void *arg)//2号线程pthread_exit返回,一样在堆上开辟空间
{printf("thread 2 exiting ...\n");int *p = (int *)malloc(sizeof(int));*p = 2;pthread_exit((void *)p);
}
void *thread3(void *arg)//3号线程被主线程pthread_cancel返回,返回值默认为常数PTHREAD_CANCELED
{while (1){ //printf("thread 3 is running ...\n");sleep(1);}return NULL;
}int  main(void)
{pthread_t tid;void *ret;// thread 1 returnpthread_create(&tid, NULL, thread1, NULL);pthread_join(tid, &ret);printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);free(ret);// thread 2 exitpthread_create(&tid, NULL, thread2, NULL);pthread_join(tid, &ret);printf("thread return, thread id %lX, return code:%d\n", tid, *(int *)ret);free(ret);// thread 3 cancel by otherpthread_create(&tid, NULL, thread3, NULL);sleep(3);pthread_cancel(tid);pthread_join(tid, &ret);if (ret == PTHREAD_CANCELED)printf("thread return, thread id %lX, return code:PTHREAD_CANCELED\n",tid);elseprintf("thread return, thread id %lX, return code:NULL\n", tid);
}

分离线程
 

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则
无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃
动释放线程资源。

功能:分离线程,线程退出时自动释放资源int pthread_detach(pthread_t thread);thread:指定线程ID返回值:
成功返回0
失败返回错误码

我们可以选择让线程自己分离自己,也可以选择让其他线程来进行分离

thread_detach(pthread_self());thread_datach(thread_id);


joinable和分离是冲突的,⼀个线程不能既是joinable又是分离的。


测试

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run(void *arg)
{pthread_detach(pthread_self());printf("%s\n", (char *)arg);return NULL;
}
int main(void)
{pthread_t tid;if (pthread_create(&tid, NULL, thread_run, (void*)"thread1 run...") != 0){printf("create thread error\n");return 1;}int ret = 0;sleep(1); // 很重要,要让线程先分离,再等待if (pthread_join(tid, NULL) == 0){printf("pthread wait success\n");ret = 0;}else{printf("pthread wait failed\n");ret = 1;}return ret;
}

 


http://www.hkcw.cn/article/HxBFKqxGIw.shtml

相关文章

SAR ADC 同步逻辑设计

SAR ADC的逻辑是重要的一个模块&#xff0c;可以分为同步逻辑和异步逻辑&#xff0c;对于低速SAR ADC&#xff0c;一般采用同步逻辑&#xff0c;对于高速SAR ADC&#xff0c;一般采用异步逻辑。 对于同步逻辑&#xff0c;由于架构不同&#xff0c;有先置位再比较&#xff0c;也…

用不太严谨的文字介绍遥测自跟踪天线的基本原理

前两天跟一个客户见面的时候&#xff0c;客户问我&#xff1a;遥测自跟踪天线能够跟踪目标&#xff0c;是什么原理&#xff1f;不需要目标的位置&#xff0c;怎么做到自跟踪的&#xff1f; 突然一瞬间&#xff0c;有点语塞。 难道要介绍天线、馈源、极化、左旋、右旋、和差网…

谷歌工作自动化——仙盟大衍灵机——仙盟创梦IDE

下载地址 https://chromewebstore.google.com/detail/selenium-ide/mooikfkahbdckldjjndioackbalphokd https://chrome.zzzmh.cn/info/mooikfkahbdckldjjndioackbalphokd

AI学习笔记(一)背景学习

什么是AI、机器学习、深度学习、强化学习&#xff0c;他们之间是什么关联关系&#xff1f; AI&#xff08;Artificial_intelligence&#xff09;&#xff1a;即人工智能是指计算系统执行通常与人类智能相关的任务的能力&#xff0c;例如学习、推理、解决问题、感知和决策 机器…

2000-2023年 上市公司-气候风险总词频、气候风险指数-社科经管实证数据

2000-2023年上市公司-气候风险总词频、气候风险指数-社科经管https://download.csdn.net/download/paofuluolijiang/90880454 https://download.csdn.net/download/paofuluolijiang/90880454 本数据集涵盖2000至2023年中国A股上市公司的气候风险相关文本信息及量化指标&#x…

Vue-自定义指令

自定义指令 简单写法 v-twoAge 功能&#xff1a; 当前年龄翻倍 注意&#xff1a;指令方法名称 小写 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>自定义指令</title><!-- 引入V…

力扣HOT100之动态规划:152. 乘积最大子数组

这道题并不是代码随想录里的&#xff0c;我试着用动规五部曲来做&#xff0c;然后不能通过全部测试样例&#xff0c;在第109个测试样例卡住了&#xff0c;如下所示。 原因是可能负数乘以负数会得到最大的乘积&#xff0c;不能单纯地用上一个序列的最大值乘以当前值来判断是否能…

应急响应靶机-web2-知攻善防实验室

题目&#xff1a; 前景需要&#xff1a;小李在某单位驻场值守&#xff0c;深夜12点&#xff0c;甲方已经回家了&#xff0c;小李刚偷偷摸鱼后&#xff0c;发现安全设备有告警&#xff0c;于是立刻停掉了机器开始排查。 这是他的服务器系统&#xff0c;请你找出以下内容&#…

【设计模式-4.6】行为型——状态模式

说明&#xff1a;本文介绍行为型设计模式之一的状态模式 定义 状态模式&#xff08;State Pattern&#xff09;也叫作状态机模式&#xff08;State Machine Pattern&#xff09;&#xff0c;允许对象在内部状态发生改变时改变它的行为&#xff0c;对象看起来好像修改了它的类…

proteus新建工程

1 点击新建工程 2 输入项目名&#xff0c;选择工程文件夹 3 下一步 4 不创建pcb 5 直接下一步 6 点击完成 7 创建完毕

【计算机CPU架构】ARM架构简介

引言&#xff1a;后x86时代的计算革命 2023年全球ARM芯片出货量突破300亿片&#xff0c;这个数字背后是智能手机、物联网设备、数据中心到超级计算机的全面渗透。当Apple M系列芯片以颠覆性效能震撼PC市场&#xff0c;当AWS Graviton3以40%性价比优势冲击云服务&#xff0c;一场…

Python实现P-PSO优化算法优化循环神经网络LSTM分类模型项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档&#xff09;&#xff0c;如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 随着深度学习技术的迅猛发展&#xff0c;循环神经网络&#xff08;RNN&#xff09;及其变体LSTM&#xff08;Long S…

牛客周赛94

随手写一下题解吧&#xff0c;最后一题确实有点烧脑了&#xff0c;一开始没想到&#xff0c;看完题解确实茅塞顿开了 经典校招题 思路&#xff1a;n级台阶&#xff0c;每次只能走1或2格&#xff0c;问你最少得步数&#xff0c;那肯定就是每次都走两个&#xff0c;如果是奇数就…

华为OD机试真题——硬件产品销售方案(2025A卷:100分)Java/python/JavaScript/C++/C语言/GO六种最佳实现

2025 A卷 100分 题型 本文涵盖详细的问题分析、解题思路、代码实现、代码详解、测试用例以及综合分析; 并提供Java、python、JavaScript、C++、C语言、GO六种语言的最佳实现方式! 2025华为OD真题目录+全流程解析/备考攻略/经验分享 华为OD机试真题《硬件产品销售方案》: 目录…

流媒体基础解析:视频清晰度的关键因素

在视频处理的过程中&#xff0c;编码解码及码率是影响视频清晰度的关键因素。今天&#xff0c;我们将深入探讨这些概念&#xff0c;并解析它们如何共同作用于视频质量。 编码解码概述 编码&#xff0c;简单来说&#xff0c;就是压缩。视频编码的目的是将原始视频数据压缩成较…

TDengine 集群运行监控

简介 为了确保集群稳定运行&#xff0c;TDengine 集成了多种监控指标收集机制&#xff0c;并通过 taosKeeper 进行汇总。taosKeeper 负责接收这些数据&#xff0c;并将其写入一个独立的 TDengine 实例中&#xff0c;该实例可以与被监控的 TDengine 集群保持独立。TDengine 中的…

SoftThinking:让模型学会模糊思考,同时提升准确性和推理速度!!

摘要&#xff1a;人类的认知通常涉及通过抽象、灵活的概念进行思考&#xff0c;而不是严格依赖离散的语言符号。然而&#xff0c;当前的推理模型受到人类语言边界的限制&#xff0c;只能处理代表语义空间中固定点的离散符号嵌入。这种离散性限制了推理模型的表达能力和上限潜力…

【LUT技术专题】图像自适应3DLUT

3DLUT开山之作: Learning Image-adaptive 3D Lookup Tables for High Performance Photo Enhancement in Real-time&#xff08;2020 TPAMI &#xff09; 专题介绍一、研究背景二、图像自适应3DLUT方法2.1 前置知识2.2 整体流程2.3 损失函数的设计 三、实验结果四、局限五、总结…

【计算机网络】 ARP协议和DNS协议

文章目录 【计算机网络】ARP协议和DNS协议&#xff08;知识点详细&#xff09;一、ARP协议&#xff08;地址解析协议&#xff09;1. **协议功能**2. **ARP报文结构**3. **工作流程**&#xff08;1&#xff09;**正向ARP&#xff08;已知IP&#xff0c;求MAC&#xff09;**&…

普中STM32F103ZET6开发攻略(一)

各位看官老爷们&#xff0c;点击关注不迷路哟。你的点赞、收藏&#xff0c;一键三连&#xff0c;是我持续更新的动力哟&#xff01;&#xff01;&#xff01; 目录 普中STM32F103ZET6开发攻略 1. GPIO端口实验——点亮LED灯 1.1 实验目的 1.2 实验原理 1.3 实验环境和器材…