当前位置: 首页 > news >正文

Linux 线程

线程概念

1. 线程与进程的区别

进程:拥有独立的地址空间和PCB(进程控制块),是操作系统资源分配的基本单位。

线程:没有独立的地址空间,而是共享其所属进程的地址空间,但每个线程都有自己的PCB,是CPU调度的基本单位。

从控制角度时说。控制回路有3条路,反馈,前馈,前向,输入以及输出。

整个控制回路系统就是一个进程,PCB管理这个系统的整体资源(内存、IO设备、定时器等),

控制回路中的三条路径(反馈、前馈、前向)是三个线程,它们:

  • 共享同一个控制系统的资源(传感器数据、执行器、共享变量)

  • 并行执行各自的计算任务

  • 每个线程有自己的TCB来记录执行状态

PCB管理整个控制系统的"家当",TCB管理每个控制路径的"执行状态""

想象一个自动化工厂

  • 整个工厂 = 进程(有完整的生产系统)

  • 工厂档案(PCB) = 记录工厂的整体状态、设备清单、原料库存

  • 三条生产线 = 三个线程

    • 反馈控制线程(监控产品质量)

    • 前馈控制线程(预测原料需求)

    • 前向控制线程(执行生产指令)

  • 工人工作卡(TCB) = 记录每条生产线当前的工作状态

工作流程

  1. 三条生产线(线程)在同一个工厂(进程)内并行工作

  2. 它们共享工厂的传感器数据、控制参数、执行机构

  3. 调度器通过查看每条生产线的工作卡(TCB)来决定CPU时间分配

  4. 工厂档案(PCB)记录整个系统的资源使用情况

场景1:单进程模型

+-------------------------------+
|          Process A            |
|  +--------------------------+ |
|  |        Address Space     | |
|  |  +--------------------+  | |
|  |  |       Code         |  | |
|  |  +--------------------+  | |
|  |  |       Data         |  | |
|  |  +--------------------+  | |
|  |  |        Heap        |  | |
|  |  +--------------------+  | |
|  |  |       Stack        |  | |
|  |  +--------------------+  | |
|  +--------------------------+ |
|  +--------------------------+ |
|  |           PCB           | |
|  |   - PID: 1000          | |
|  |   - State: Running     | |
|  |   - ...                | |
|  +--------------------------+ |
+-------------------------------+

场景2:多线程模型(同一进程内)

+---------------------------------------------------+
|                 Process A                         |
|   +---------------------------------------------+ |
|   |              Shared Address Space           | |
|   |  +-------------------+ +-----------------+  | |
|   |  |      Code         | |      Code       |  | |
|   |  +-------------------+ +-----------------+  | |
|   |  |      Data         | |      Data       |  | |
|   |  +-------------------+ +-----------------+  | |
|   |  |       Heap        | |       Heap      |  | |
|   |  +-------------------+ +-----------------+  | |
|   |  | Thread 1's Stack  | | Thread 2's Stack|  | |
|   |  +-------------------+ +-----------------+  | |
|   +---------------------------------------------+ |
|   +-------------------+   +-------------------+   |
|   |      PCB 1        |   |      PCB 2        |   |
|   |  - PID: 1000      |   |  - PID: 1001      |   |
|   |  - TGID: 1000     |   |  - TGID: 1000     |   |
|   |  - State: Running |   |  - State: Ready   |   |
|   +-------------------+   +-------------------+   |
+---------------------------------------------------+

三级映射详细解析

映射流程(修正版)

线程PCB → 页目录(PD) → 页表(PT) → 物理页面(PP) → 内存单元

各级结构说明

1. 页目录 (Page Directory)

  • 位置:位于进程的PCB中

  • 大小:4KB,包含1024个表项

  • 作用:每个表项指向一个页表

  • 特点:同一进程的所有线程共享同一个页目录

2. 页表 (Page Table)

  • 大小:4KB,包含1024个表项

  • 作用:每个表项指向一个物理页面

  • 映射范围:一个页表映射 1024 × 4KB = 4MB 地址空间

3. 物理页面 (Physical Page)

  • 大小:4KB

  • 作用:实际的物理内存块

  • 包含:1024个内存单元(每个单元1字节)

地址转换过程

虚拟地址 = [10位页目录索引] + [10位页表索引] + [12位页内偏移]1. 通过页目录索引找到页表
2. 通过页表索引找到物理页面  
3. 通过页内偏移找到具体内存单元

线程共享原理详解

关键机制

// 线程创建时共享地址空间的关键
clone(..., CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...);

CLONE_VM 标志:让新线程与父线程共享同一个页目录和地址空间映射。

线程共享示意图

进程A
├── 页目录 (所有线程共享)
├── 线程1-PCB → 共享页目录
├── 线程2-PCB → 共享页目录  
└── 线程3-PCB → 共享页目录↓
相同的页表映射 → 相同的物理页面

与进程的对比

进程A:独立页目录A → 页表集A → 物理页面集X
进程B:独立页目录B → 页表集B → 物理页面集Y线程A1、A2、A3:共享页目录A → 共享页表集A → 共享物理页面集X

实际内存映射流程

完整路径

线程执行指令↓
CPU遇到虚拟地址↓
查线程PCB中的CR3寄存器 → 找到页目录物理地址↓  
通过虚拟地址前10位索引页目录 → 找到页表↓
通过虚拟地址中间10位索引页表 → 找到物理页面↓  
物理页面基址 + 页内偏移(12位) = 物理地址↓
访问内存单元

关键点说明

  1. CR3寄存器:每个线程的PCB中都保存着CR3值,指向页目录的物理地址

  2. 共享原理:同一进程的所有线程,其CR3指向同一个页目录

  3. TLB加速:频繁的地址转换通过TLB(快表)缓存,提高性能


为什么线程比进程快?

1. 创建开销小

  • 线程:只需创建PCB和栈,共享现有页目录

  • 进程:需要创建完整的页目录、页表结构

2. 切换开销小

  • 线程切换:主要保存寄存器状态,地址空间不变

  • 进程切换:需要切换整个页目录(刷新CR3和TLB)

3. 通信成本低

  • 线程:直接通过共享内存通信

  • 进程:需要IPC机制,涉及内核拷贝

个人理解

我这么理解。cpu和内核以及内存资,都是计算机资源,我们要充分利用。 但是显示当中内存难以分配以及计算效率不高,甚至会引起信息传输不安全。所以发展出了mmu以及pcb,通过创建一个虚拟内存空间,将虚拟的内存空间与现实空间进行映射,创建映射表,这样充分利用资源,同时还将信息进行隔离保证安全。 除上述之外,这个操作还不够细腻。一个进程分配一个pcb,但是pcb的内存也不小,为了充分利用资源也要对pcb的资源进一步规划以及分配,针对pcb的内存也相当于使用一个表格进行管理。

从物理内存到虚拟内存

原始状态:程序直接操作物理内存↓
问题:内存碎片、安全风险、效率低下↓
解决方案:引入MMU + 虚拟地址空间↓
效果:每个进程有独立的虚拟世界,通过映射表访问真实物理内存

PCB的精细化管理的演进

第一阶段:粗粒度 - 仅进程

一个程序 = 一个进程 = 一个PCB↓
问题:一个程序内部无法并行,创建进程开销大↓
解决方案:引入线程概念

第二阶段:细粒度 - 进程+线程

// Linux中的实现:task_struct 既可以是进程也可以是线程
struct task_struct {pid_t pid;          // 进程IDpid_t tgid;         // 线程组ID(进程ID)struct mm_struct *mm; // 内存管理结构(线程间共享)// ... 其他字段
};

PCB管理的"表格化"理解

您说的"针对PCB的内存也相当于使用一个表格进行管理"非常准确

操作系统内核维护:
┌─────────────────┐
│  进程/线程表    │ ← 这就是管理PCB的"表格"
│  - task_struct *│
│  - 状态         │
│  - 调度信息     │
│  - ...         │
└─────────────────┘↓ 指向各个PCB
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  线程1-PCB  │  │  线程2-PCB  │  │  进程B-PCB  │
│  - pid      │  │  - pid      │  │  - pid      │
│  - tgid     │  │  - tgid     │  │  - tgid     │  
│  - mm       │  │  - mm       │  │  - mm       │
└─────────────┘  └─────────────┘  └─────────────┘共享同一mm       共享同一mm        独立mm

完整的技术栈视图

资源管理的层次化

最底层:物理资源CPU核心、物理内存条、硬件设备↓
第一层抽象:MMU虚拟化  └── 虚拟地址空间 + 页表映射↓
第二层抽象:进程管理└── PCB + 进程调度↓  
第三层抽象:线程管理└── 轻量级PCB + 线程调度↓
最高层:应用程序└── 看到的是统一的编程接口

创建线程:pthread_create()

1. 函数原型

int pthread_create(pthread_t *thread,               // 传出参数:保存新线程IDconst pthread_attr_t *attr,      // 线程属性(通常 NULL)void *(*start_routine)(void *),  // 线程要执行的函数(入口)void *arg                        // 传给该函数的参数
);

2. 参数详解

参数说明
thread传出参数,函数成功后会把新线程的 ID 写入这里。类型是 pthread_t*
attr线程属性(如栈大小、是否分离等)。初学者传 NULL 表示使用默认属性
start_routine回调函数,新线程启动后执行这个函数。必须是 void* func(void*) 形式。
arg传给回调函数的参数,类型是 void*,可以传任意类型的指针(如 int*struct* 等)。

3. 返回值

  • 成功:返回 0
  • 失败直接返回错误码(如 EAGAINEINVAL),不是通过 errno
    • 这是和传统系统调用(如 openfork)的重要区别!
必须注意:
  • 回调函数签名必须严格匹配

    void* my_func(void* arg) { ... }

    不能是 void my_func()int my_func(void*)

  • 线程 ID 类型是 pthread_t,在 Linux 下通常是 unsigned long,但不要假设,直接用 %lu 打印时要强转:

    printf("Thread ID: %lu\n", (unsigned long)tid);

示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* my_thread_func(void* arg) {printf("Child thread running! ID = %lu\n", (unsigned long)pthread_self());sleep(1);return NULL;
}int main() {pthread_t tid;printf("Main thread ID = %lu\n", (unsigned long)pthread_self());int ret = pthread_create(&tid, NULL, my_thread_func, NULL);if (ret != 0) {fprintf(stderr, "Failed to create thread: %d\n", ret);return 1;}sleep(2); // 等待子线程执行完(临时方案,后续用 pthread_join)return 0;
}

获取线程 ID:pthread_self()

1. 函数原型

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

2. 特点

  • 总是成功:不会失败,也不返回错误码。
  • 返回当前线程的 ID:就像 getpid() 返回当前进程 ID 一样。
  • 线程 ID 是进程内部的标识:不同进程中的线程 ID 可能相同,但它们互不影响。

3. 线程 ID vs LWP(轻量级进程号)

项目线程 ID (pthread_self())LWP(ps -Lf 中看到的)
作用进程内部标识线程内核调度单位(CPU 时间片依据)
类型pthread_t(在 Linux 中通常是 unsigned long整数 PID(其实是内核线程的 PID)
是否相同?❌ 不同!不要混淆!

打印建议:在 Linux 下,pthread_t 本质是 unsigned long,所以用 %lu 打印:

printf("Thread ID = %lu\n", (unsigned long)pthread_self());

4. 示例代码

#include <stdio.h>
#include <pthread.h>int main() {printf("Main thread ID = %lu\n", (unsigned long)pthread_self());return 0;
}

线程中的错误处理:为什么不能用 perror

1. 问题背景:errno 在多线程中不安全!

  • 在传统单线程程序中,系统调用失败时会设置全局变量 errno,然后你可以用 perror() 打印错误。
  • 但在多线程环境中,多个线程共享同一个 errno(虽然现代 glibc 已将其改为线程局部存储,但为了代码可移植性和明确性,仍建议避免依赖 errno)。
  • 更重要的是:pthread_create 等线程函数不会设置 errno

关键事实
pthread_create() 失败时直接返回错误码(如 EAGAIN, EINVAL),不是通过 errnoerrno是输出错误原因,但是pthread_create是返回错误码。

怎么解决报错处理?

不碰 errno,只看返回值;用 strerror_r 解析信息;根据错误码(如 EAGAINEINVAL)针对性处理,确保线程安全和问题可定位。

小测试:

循环创建 5 个线程,每个打印自己的序号

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>// 子线程函数
void* tfn(void* arg) {// 将 void* 转回整数(注意 long 中转)int idx = (int)(long)arg;// 打印:序号从1开始,所以 idx+1printf("I'm %dth thread: pid=%d, tid=%lu\n",idx + 1,getpid(),(unsigned long)pthread_self());// 模拟不同执行时间(让输出更有序)sleep(idx + 1);return NULL; // 必须返回!
}int main() {const int N = 5;pthread_t tids[N];// 创建 N 个线程for (int i = 0; i < N; i++) {int ret = pthread_create(&tids[i], NULL, tfn, (void*)(long)i);if (ret != 0) {fprintf(stderr, "Create thread %d failed: %s\n", i, strerror(ret));exit(EXIT_FAILURE);}}// 主线程等待(临时方案:usleep)usleep(100000); // 100ms,确保子线程有时间输出printf("main thread: pid=%d, tid=%lu\n",getpid(),(unsigned long)pthread_self());return 0;
}

erro:循环变量传地址导致线程参数错乱

void* tfn(void* arg) {int *p = (int*)arg;printf("Thread %d\n", *p); // 解引用指针return NULL;
}int main() {pthread_t tids[5];for (int i = 0; i < 5; i++) {pthread_create(&tids[i], NULL, tfn, &i); // ⚠️ 传的是 &i!}sleep(1);return 0;
}

 输出可能为:

Thread 3
Thread 3
Thread 5
Thread 5
Thread 5

根本原因:所有线程共享同一个地址 &i

内存图解析:

主线程栈帧:
+------------------+
| int i = ?        | ← 地址固定,比如 0x7fff1234
+------------------+循环过程:
i=0 → 创建线程0,传 &i(0x7fff1234)
i=1 → 创建线程1,传 &i(还是 0x7fff1234!)
i=2 → 创建线程2,传 &i(仍是 0x7fff1234)
...
  • 所有线程的 arg 都指向同一个地址 &i
  • 主线程在 for 循环中不断执行 i++
  • 子线程启动后,什么时候执行 *p 是不确定的(由调度器决定)。
  • 当子线程终于执行到 printf 时,i 可能已经变成 3、5 甚至循环结束了(i=5)!

正确做法:传值,不传地址!

✅ 正确代码:

void* tfn(void* arg) {int num = (int)(long)arg; // 从 void* 还原整数printf("Thread %d\n", num);return NULL;
}int main() {pthread_t tids[5];for (int i = 0; i < 5; i++) {pthread_create(&tids[i], NULL, tfn, (void*)(long)(i + 1)); // 传值!}sleep(1);return 0;
}

✅ 输出:

Thread 1
Thread 2
Thread 3
Thread 4
Thread 5

为什么这样安全?

  • 每个线程收到的是 i 的一个独立副本(数值被编码进指针值中)。
  • 主线程后续修改 i完全不影响子线程拿到的值
  • 没有共享内存,没有竞争条件,线程安全!

关于 (void*)(long)i 的深度解释

问题:为什么不能直接 (void*)i

在 64 位系统:

  • int i = 3; → 4 字节:0x00000003
  • (void*)i → 8 字节指针:0x0000000000000003
  • 看似没问题,但编译器会警告:
    warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]

为什么用 long 中转?

  • 在 Linux 64 位系统,long 是 8 字节,和指针同宽。
  • (void*)(long)i 先把 int 扩展为 8 字节 long,再转指针,消除宽度不匹配警告
  • 更标准的做法是用 intptr_t(定义在 <stdint.h>):
    #include <stdint.h>
    pthread_create(..., (void*)(intptr_t)i);
    int num = (int)(intptr_t)arg;

✅ 初学记住:(void*)(long)i 传整数,用 (int)(long)arg 取回,即可安全又少警告。

扩展思考:如果必须传结构体怎么办?

typedef struct { int id; char name[20]; } Task;// 正确做法:每个线程分配独立内存
for (int i = 0; i < 5; i++) {Task *task = malloc(sizeof(Task));task->id = i + 1;strcpy(task->name, "worker");pthread_create(&tid, NULL, tfn, task); // 传堆地址
}// 子线程中:
void* tfn(void* arg) {Task *t = (Task*)arg;printf("Task %d\n", t->id);free(t); // 谁分配,谁释放!return NULL;
}

http://www.dtcms.com/a/565447.html

相关文章:

  • 【Android项目】KMMV项目随笔
  • vmware windows和linux系统共享和映射物理机目录
  • 机器学习日报11
  • 宿州品牌网站建设公司淘宝网站建设单子好接吗
  • 大数据成矿预测系列(六) | 从“看图像”到“读结构”:图卷积神经网络如何赋能地质“图谱”推理
  • AI研究-118 具身智能 Mobile-ALOHA 解读:移动+双臂模仿学习的开源方案(含论文/代码/套件链接)
  • 超越“盒子”:虚拟机在云计算与AI时代的颠覆性未来应用展望
  • 外国人可以在中国做网站吗cnzz网站建设
  • 网站建设色彩搭配做黄图网站接广告好赚吗
  • 云手机运行 技术革新
  • 安徽省建设厅网站电话网站开发明细
  • 电脑手机蓝牙远程控制系统代码三篇
  • nacos增加配置时报错
  • SQL Schema Compare:一款免费开源的数据库结构比较和同步工具
  • 北京电信备案网站做茶道网站
  • C语言实现状态模式
  • SQLite 常用函数
  • 青岛seo网站推广广告电商
  • app软件小程序网站建设wordpress jetpack 慢
  • 2G2核服务器安装ES
  • 大规模图片列表性能优化:基于 IntersectionObserver 的懒加载与滚动加载方案
  • CANN算子开发实战:从矩阵乘法到高性能优化
  • 网站推广教程分享wordpress 阴影
  • 从协议规范和使用场景探讨为什么SmartMediaKit没有支持DASH
  • 【工程开发】GLM-4.1V调试
  • Fiddler抓包手机和部分app无法连接网络问题
  • 【开题答辩全过程】以 二手咸鱼手机交易平台为例,包含答辩的问题和答案
  • 云真机和云手机的区别
  • 成都市那里有网站建设制作公司Wordpress 启动邮件
  • 东莞建网站的公司数据分析师资格证书怎么考