信号量环形队列与线程池起步-把临界资源拆成很多份

信号量、环形队列与线程池起步:把临界资源拆成很多份(8-16 ~ 8-20)

我的github:(https://github.com/xcx55/ubuntu-linux-project)

感谢各位大佬参观我的github!!

上一篇的锁把临界资源当"整体"保护------一次只许一个人进。但很多资源天生就该拆开用(环形队列的每一个格子、线程池的每一个任务)。这几天笔记的主线就是:用信号量把"预定"变成计数器,用环形队列让生产和消费真正并行,最后用线程池把这套机制变成基础设施。文末再补两篇小笔记(fork 拷贝页表、动态链接最终章)。


一、POSIX 信号量:一个描述资源数目的计数器(8-19)

1. 本质

信号量本质是一个计数器 ,是对资源的预定机制,描述"临界资源的数目"。

和互斥锁的关系一句话说清:

信号量为 1,就是二元信号量 ------功能上完全等价于互斥锁!

区别在于定位:锁把资源当整体(0 或 1),信号量把资源拆成 N 份(计数器 0~N),允许 N 个执行流同时进入,每个人预定一份。

2. P/V 操作与接口

  • P 操作(sem_wait) :申请资源,计数器 --;申请失败会阻塞;
  • V 操作(sem_post) :归还资源,计数器 ++。
c 复制代码
sem_t s;
sem_init(&s, pshared, value);  // pshared: 0=线程间共享, 非0=进程间共享; value=初始值
sem_wait(&s);                  // P
sem_post(&s);                  // V
sem_destroy(&s);

底层可靠性:信号量的加减本身靠 lock 前缀指令保护 ------和锁用一条交换指令一个思路。但注意笔记的提醒:信号量不保证多并发的完整正确性 (计数器只管数量,不管数据的读写安全),除非配合"资源被拆分、各份独立"的用法------这正是环形队列登场的理由。


二、环形队列:单生产单消费的天生优雅与多生产多消费的改造(8-20)

1. 为什么环形队列配信号量是绝配

普通阻塞队列:整条队列一把锁,放数据、取数据互斥------生产和消费其实串行访问队列。

环形队列:生产者一个下标、消费者一个下标 ,各自往前走,只要"生产者不套圈消费者、消费者不追上生产者",两边就可以同时操作队列的不同格子。这正是信号量表达的场景:

  • space_sem:剩余空格子数(生产者的资源);
  • data_sem:已有数据格子数(消费者的资源)。

信号量还保证了不会"超越彼此"------生产者没格子就不能放,消费者没数据就不能取。

2. 单生产单消费 → 多生产多消费

单生产单消费下这套逻辑天衣无缝,但有缺陷:现实里生产者消费者都是一群。改造时笔记回答了两个好问题:

为什么是两把锁? ------因为上一个队列模型是"以栈顶(队列口)作为资源",天然互斥;而环形队列以生产者下标和消费者下标为依靠:生产者之间竞争生产者下标(一把锁),消费者之间竞争消费者下标(另一把锁)------两把锁保护两个下标,生产和消费之间靠信号量协调,互不干扰。

先加锁还是先预定资源?

先预定资源(P 操作),再竞争锁!

理由:先 P 一次信号量,就把"进不进得来"的问题在锁外解决了,锁的竞争范围缩小到"已经预定位子的人"------只有最小范围的资源才使用锁。反过来先加锁再 P,所有线程挤在一个锁上,信号量就被架空了。

笔记最后总结的适用边界:当资源不被整体使用时,就上信号量;每个最小单元才用锁。


三、线程池起步:预先创建,空间换时间(8-20)

1. 动机

预先创建一批线程,任务来了直接指派------用预先的时间和空间,换未来的响应时间(空间换时间)。

线程的创建销毁虽然比进程便宜,但高频创建依然是开销;线程池把"创建"和"使用"解耦,开机即有一批待命线程。

2. 顺带的两课

  • 设计模式:就是"代码设计的套路"------解耦、效率更好。线程池本身就是一种设计模式的落地(池化);
  • 日志 :线程池的运行必须配日志,日志字段是必须齐全的(时间、级别、内容......),否则多线程出了问题根本无从追查。

(笔记里还留了作业:要写好线程池的回调注册,得回去补 C++ 的 lambda、虚函数、右值绑定------这是下一篇的事了。)


四、补遗一:fork 拷贝页表的本质(8-16)

四行笔记,一句话收掉旧悬念:

fork 之后,一级页表是新的,但内容和父进程的一级页表一样,其余(物理页映射)也一样------直到写时拷贝才真正分家。

也就是说:fork 复制的是页表这套"地图" ,不是物理内存本身。地图相同 → 父子看同一批物理页;写时拷贝只改发生写入的那一条页表项。和之前"PCB 以父为模板"完全同构------进程的复制,从头到尾都是结构体和表的复制。


五、补遗二:动态链接与加载的最终章(8-16)

这篇《线程 ELF .o mmap》信息量最大,把整个链接加载的知识做了一次总收束。

1. 链接期:桩、解析函数、.got 的诞生顺序

静态链接器拿到 .o 和动态库后(为什么要看一眼动态库符号表?------因为要拿到函数名对应的偏移),按顺序产出:

  1. 代码桩 (PLT):依据动态符号表生成,里面是一条 jmp 占位;
  2. 解析函数 的生成:它运行期需要两样东西------动态库基地址 + 动态表(相对偏移);
  3. .got 表的大小确定与占位。

2. 运行期:第一次调用与后续调用的分界

  • 第一次调用:走桩 → 进解析函数 → 用动态库虚拟基地址 + 动态表算出真实函数地址 → 写入 .got → call 过去;
  • 第二次起:桩里第一段 jmp 直接跳到 .got 里存好的地址,解析只发生一次------这就是之前 PLT/GOT 那篇"按需重定位"的底层细节补全。

3. 两条规则:生成靠规则 1,加载靠规则 1+2

  • 规则 1(ELF ↔ 磁盘) :exe/.so 都是 ELF 架构,看 PHT (.o 才看 section 表);数据和代码依据虚拟地址偏移存入块区,块号 = 偏移 / 4096(头指针由链接器决定,内核记住);.so 有自己的 inode,懒加载时按 inode 去磁盘对应位置读;
  • 规则 2(磁盘 ↔ 内存):读到的数据按 4KB 存入页帧------用位图找空闲页帧,虚拟地址 32 位按 10/10/12 拆分偏移做映射(和页表那篇完全对上);
  • 关键结论:生成需要规则 1,加载需要规则 1+2,且两条规则是解耦的。MMU 实例化的就是"怎么放的,就怎么拿"。

动态库代码并没有在链接期加载进来 ,而是运行期按需(缺页)加载------动态库加载行为结束,才代表该进程的虚拟地址空间真正全部生成完毕。

4. 回到线程:pthread 为什么看中 mmap

线程使用 mmap,就是看中它的两个性质:每一次 mmap 生成一个段;map_private 的空间一个进程独有,但线程之间相互可见(同地址空间)。

线程需要自己的用户栈、管理信息(TCB)、线程局部存储------本来"线程用户栈 + clone 出的 PCB"就够了,再加上管理信息和 TLS,管理更灵活。线程库的全部家当,就是几块 mmap 出来的共享区映射------把前面页表、动态库、线程三条线的知识在 mmap 上拧成一股。


六、小结

主题 一句话
信号量 描述临界资源数目的计数器;为 1 即互斥锁
P/V sem_wait 申请(可阻塞)/ sem_post 归还,靠 lock 指令保原子
环形队列 生产者下标、消费者下标各自前进,两个信号量防套圈
多生产多消费 两把锁管两个下标;先 P 再加锁
线程池 预创建 + 任务队列,空间换时间,配日志
fork 页表 复制的是地图(页表),不是地(物理页)
动态链接终章 桩 → 解析函数 → .got 只解析一次;生成规则 1、加载规则 1+2

整个线程章到这里的逻辑链:共享地址空间(红利)→ 数据不一致(事故)→ 锁(安全)→ 忙轮询(浪费)→ 条件变量(有序)→ 信号量(拆分资源)→ 环形队列(真并行)→ 线程池(基础设施)。下一步就是线程池的 C++ 实战了。

相关推荐
Huangjin007_2 小时前
【Linux 系统篇(二十五)】文件(二) 系统文件I/O详解
linux·运维·服务器
ShineWinsu3 小时前
对于ProtoBuffer:ProtoBuffer的介绍以及安装
linux·网络·google·协议·序列化·反序列化·protobuffer
FlightYe3 小时前
视界原理之2D视频(二):视频文件里有什么
android·linux·网络·c++·ffmpeg·音视频·aac
GeW5 小时前
从代码到共识:技术突围如何映照大国相处之道
linux
承渊政道6 小时前
Linux系统学习【进程信号详细解析——认识、产生、保存以及捕捉信号】
linux·学习·ubuntu·ssh·vs code·进程信号
硅基手札6 小时前
【linux内核专栏01】Linux 内核心智模型与设计哲学
linux·运维·驱动开发·开源软件
码完就睡6 小时前
Linux——socket网络编程
linux·运维·网络
道尔柯南6 小时前
【Linux】网络基础概念
linux·运维·网络
傲世仙尊7 小时前
从抢票事故到锁的原理-互斥条件变量与生产消费模型
linux·驱动开发