v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码

卧疾丰暇豫,翰墨时间作。这篇文章主要讲述v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码相关的知识,希望能为你提供帮助。
?

v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码

文章图片
??
百篇博客分析|本篇为:(用户态锁篇) | 如何使用快锁Futex(上)
?
快锁三篇
鸿蒙内核实现了??Futex??,系列篇将用三篇来介绍快锁,主要两个原因:

  • 网上介绍??Futex??的文章很少,全面深入内核介绍的就更少,所以来一次详细整理和挖透。
  • 涉及用户态和内核态打配合,共同作用既要说用户态的使用(一篇)又要说清楚内核态的实现(两篇)。 本篇为第一篇,用户态下如何使用??Futex???,并借助一个??demo??来说清楚整个过程。

基本概念
??Futex???(??Fast userspace mutex??,用户态快速互斥锁),系列篇简称 ?快锁? ,是一个在Linux上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具,它第一次出现在??linux??内核开发的2.5.7版;其语义在2.5.40固定下来,然后在2.6.x系列稳定版内核中出现,是内核提供的一种系统调用能力。通常作为基础组件与用户态的相关锁逻辑结合组成用户态锁,是一种用户态与内核态共同作用的锁,其用户态部分负责锁逻辑,内核态部分负责锁调度。
当用户态线程请求锁时,先在用户态进行锁状态的判断维护,若此时不产生锁的竞争,则直接在用户态进行上锁返回;反之,则需要进行线程的挂起操作,通过??Futex??系统调用请求内核介入来挂起线程,并维护阻塞队列。
当用户态线程释放锁时,先在用户态进行锁状态的判断维护,若此时没有其他线程被该锁阻塞,则直接在用户态进行解锁返回;反之,则需要进行阻塞线程的唤醒操作,通过??Futex??系统调用请求内核介入来唤醒阻塞队列中的线程。
存在意义

  • ?互斥锁?(??mutex??)是必须进入内核态才知道锁可不可用,没人跟你争就拿走锁回到用户态,有人争就得干等 (包括 有限时间等和无限等待两种,都需让出??CPU??执行权) 或者放弃本次申请回到用户态继续执行。那为何?互斥锁?一定要陷入内核态检查呢? 互斥锁(??mutex??) 本质是竞争内核空间的某个全局变量(??LosMux??结构体)。应用程序也有全局变量,但其作用域只在自己的用户空间中有效,属于内部资源,有竞争也是应用程序自己内部解决。而应用之间的资源竞争(即内核资源)就需要内核程序来解决,内核空间只有一个,内核的全局变量当然要由内核来管理。应用程序想用内核资源就必须经过系统调用陷入内核态,由内核程序接管??CPU??,所谓接管本质是要改变程序状态寄存器,??CPU??将从用户态栈切换至内核态栈运行,执行完成后又要切回用户态栈中继续执行,如此一来栈间上下文的切换就存在系统性能的损耗。没看明白的请前往系列篇 ?(互斥锁篇)? 翻看。
  • ?快锁? 解决思路是能否在用户态下就知道锁可不可用,因为竞争并不是时刻出现,跑到内核态一看其实往往没人给你争,白跑一趟来回太浪费性能。那问题来了,用户态下如何知道锁可不可用呢? 因为不陷入内核态就访问不到内核的全局变量。而自己私有空间的变量对别的进程又失效不能用。越深入研究内核越有一种这样的感觉,内核的实现可以像数学一样推导出来,非常有意思。数学其实是基于几个常识公理推导出了整个数学体系,因为不如此逻辑就无法自洽。如果对内核有一定程度的了解,这里自然能推导出可以借助 ?共享内存? 来实现!

使用过程
看个??linux futex???官方??demo???详细说明下用户态下使用??Futex??的整个过程,代码不多,但涉及内核的知识点很多,通过它可以检验出内核基本功扎实程度。
//futex_demo.c
#define _GNU_SOURCE
#include < stdio.h>
#include < errno.h>
#include < stdatomic.h>
#include < stdint.h>
#include < stdlib.h>
#include < unistd.h>
#include < sys/wait.h>
#include < sys/mman.h>
#include < sys/syscall.h>
#include < linux/futex.h>
#include < sys/time.h>
#define errExit(msg)doperror(msg); exit(EXIT_FAILURE); \\
while (0)
static uint32_t *futex1, *futex2, *iaddr;
/// 快速系统调用
static int futex(uint32_t *uaddr, int futex_op, uint32_t val,
const struct timespec *timeout, uint32_t *uaddr2, uint32_t val3)

return syscall(SYS_futex, uaddr, futex_op, val,
timeout, uaddr2, val3);

/// 申请快锁
static void fwait(uint32_t *futexp)

long s;
while (1)
const uint32_t one = 1;
if (atomic_compare_exchange_strong(futexp, & one, 0))
break; //申请快锁成功
//申请快锁失败,需等待
s = futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0);
if (s == -1 & & errno != EAGAIN)
errExit("futex-FUTEX_WAIT");


/// 释放快锁
static void fpost(uint32_t *futexp)

long s;
const uint32_t zero = 0;
if (atomic_compare_exchange_strong(futexp, & zero, 1)) //释放快锁成功
s = futex(futexp, FUTEX_WAKE, 1, NULL, NULL, 0); //唤醒等锁 进程/线程
if (s== -1)
errExit("futex-FUTEX_WAKE");


/// 父子进程竞争快锁
int main(int argc, char *argv[])

pid_t childPid;
int nloops;
setbuf(stdout, NULL);
nloops = (argc > 1) ? atoi(argv[1]) : 3;
iaddr = mmap(NULL, sizeof(*iaddr) * 2, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_SHARED, -1, 0); //创建可读可写匿名共享内存
if (iaddr == MAP_FAILED)
errExit("mmap");
futex1 = & iaddr[0]; //绑定锁一地址
futex2 = & iaddr[1]; //绑定锁二地址
*futex1 = 0; // 锁一不可申请
*futex2 = 1; // 锁二可申请
childPid = fork();
if (childPid == -1)
errExit("fork");
if (childPid == 0) //子进程返回
for (int j = 0; j < nloops; j++)
fwait(futex1); //申请锁一
printf("子进程(%jd) %d\\n", (intmax_t) getpid(), j);
fpost(futex2); //释放锁二

exit(EXIT_SUCCESS);

// 父进程返回执行
for (int j = 0; j < nloops; j++)
fwait(futex2); //申请锁二
printf("父进程 (%jd) %d\\n", (intmax_t) getpid(), j);
fpost(futex1); //释放锁一

wait(NULL);
exit(EXIT_SUCCESS);

代码在??wsl2??上编译运行结果如下:
root@DESKTOP-5PBPDNG:/home/turing# gcc ./futex_demo.c -o futex_demo
root@DESKTOP-5PBPDNG:/home/turing# ./futex_demo
父进程 (283) 0
子进程 (284) 0
父进程 (283) 1
子进程 (284) 1
父进程 (283) 2
子进程 (284) 2

?解读?

  • 通过系统调用??mmap??? 创建一个可读可写的共享内存??iaddr[2]???整型数组,完成两个??futex???锁的初始化。内核会在内存分配一个共享线性区(??MAP_ANONYMOUS??? | ??MAP_SHARED???),该线性区可读可写( ??PROT_READ??? | ??PROT_WRITE???) ??futex1 = & iaddr[0]; //绑定锁一地址 futex2 = & iaddr[1]; //绑定锁二地址 *futex1 = 0; // 锁一不可申请 *futex2 = 1; // 锁二可申请 ?? 如此??futex1???和??futex2???有初始值并都是共享变量,想详细了解??mmap??内核实现的可查看系列篇 ?(线性区篇)? 和 ?(共享内存篇)? 有详细介绍。
  • ??childPid = fork(); ??? 创建了一个子进程,fork会拷贝父进程线性区的映射给子进程,导致的结果就是父进程的共享线性区到子进程这也是共享线性区,映射的都是相同的物理地址。对??fork??不熟悉的请前往翻看,系列篇 ?(fork篇)| 一次调用,两次返回? 专门说它。
  • ??fwait???(申请锁)与??fpost???(释放锁)成对出现,单独看下申请锁过程 ??/// 申请快锁 static void fwait(uint32_t *futexp)long s; while (1) const uint32_t one = 1; if (atomic_compare_exchange_strong(futexp, & one, 0)) break; //申请快锁成功 //申请快锁失败,需等待 s = futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0); if (s == -1 & & errno != EAGAIN) errExit("futex-FUTEX_WAIT"); ?? 死循环的break条件是 ??atomic_compare_exchange_strong??为真,这是个原子比较操作,此处必须这么用,至于为什么请前往翻看系列篇 ?(原子操作篇)| 谁在为完整性保驾护航? ,注意它是理解??Futex???的关键所在,它的含义是 ??在头文件< stdatomic.h> 中定义 _Bool atomic_compare_exchange_strong(volatile A * obj,C * expected,C desired); ?? 将所指向的值obj与所指向的值进行原子比较??expected???,如果相等,则用前者替换前者??desired???(执行读取 - 修改 - 写入操作)。否则,加载实际值所指向的??obj???进入??*expected??(进行负载操作)。 什么意思 ? 来个直白的解释 :
  • ??futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0) //执行一个等锁的系统调用 ?? 最后一个参数为??0??代表不在内核态停留直接返回用户态,后续将在内核态部分详细说明。

  • 如果 ??futexp == 1?? 则 ??atomic_compare_exchange_strong??返回真,同时将 ??futexp??的值变成??0??,1代表可以持有锁,一旦持有立即变0,别人就拿不到了。所以此处甚秒。而且这发生在用户态。
  • 如果??futexp == 0?? ??atomic_compare_exchange_strong??返回假,没有拿到锁,就需要陷入内核态去挂起任务等待锁的释放


  • ??childPid == 0???是子进程的返回。不断地申请??futex1??? 释放??futex2??? ??if (childPid == 0) //子进程返回 for (int j = 0; j < nloops; j++) fwait(futex1); printf("子进程(%jd) %d\\n", (intmax_t) getpid(), j); fpost(futex2); exit(EXIT_SUCCESS); ??
  • 最后的父进程的返回,不断地申请??futex2??? 释放??futex1??? ??// 父进程返回执行 for (int j = 0; j < nloops; j++) fwait(futex2); printf("父进程 (%jd) %d\\n", (intmax_t) getpid(), j); fpost(futex1); wait(NULL); exit(EXIT_SUCCESS); ??
  • 两把锁的初值为 ??*futex1 = 0; *futex2 = 1; ???,父进程在 ??fwait(futex2)???所以父进程的??printf???将先执行,??*futex2 = 0; ???锁二变成不可申请,打印完成后释放??fpost(futex1)???使其结果为??*futex1 = 1; ???表示锁一可以申请了,而子进程在等??fwait(futex1)???,交替下来执行的结果为 ??父进程 (283) 0 子进程 (284) 0 父进程 (283) 1 子进程 (284) 1 父进程 (283) 2 子进程 (284) 2 ??

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断??debug???一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,??v**.xx?? 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,公众号回复 ?百文? 可方便阅读。

按功能模块:

  • 前因后果 > > ??总目录??? | ??调度故事??? | ??内存主奴??? | ??源码注释??? | ??源码结构??? | ??静态站点??? | ??参考文档?? |
  • 基础工具 > > ??双向链表??? | ??位图管理??? | ??用栈方式??? | ??定时器??? | ??原子操作??? | ??时间管理?? |
  • 加载运行 > > ??ELF格式??? | ??ELF解析??? | ??静态链接??? | ??重定位??? | ??进程映像?? |
  • 进程管理 > > ??进程管理??? | ??进程概念??? | ??Fork??? | ??特殊进程??? | ??进程回收??? | ??信号生产??? | ??信号消费??? | ??Shell编辑??? | ??Shell解析?? |
  • 编译构建 > > ??编译环境??? | ??编译过程??? | ??环境脚本??? | ??构建工具??? | ??gn应用??? | ??忍者ninja?? |
  • 进程通讯 > > ??自旋锁??? | ??互斥锁??? | ??进程通讯??? | ??信号量??? | ??事件控制??? | ??消息队列??? | ??共享内存??? | ??消息封装??? | ??消息映射??? | ??用户态锁?? |
  • 内存管理 > > ??内存分配??? | ??内存管理??? | ??内存汇编??? | ??内存映射??? | ??内存规则??? | ??物理内存?? |
  • 任务管理 > > ??时钟任务??? | ??任务调度??? | ??任务管理??? | ??调度队列??? | ??调度机制??? | ??线程概念??? | ??并发并行??? | ??CPU??? | ??系统调用??? | ??任务切换?? |
  • 文件系统 > > ??文件概念??? | ??文件系统??? | ??索引节点??? | ??挂载目录??? | ??根文件系统??? | ??VFS??? | ??文件句柄??? | ??管道文件?? |
  • 硬件架构 > > ??汇编基础??? | ??汇编传参??? | ??工作模式??? | ??寄存器??? | ??异常接管??? | ??汇编汇总??? | ??中断切换??? | ??中断概念??? | ??中断管理?? |
  • 设备驱动 > > ??字符设备??? | ??控制台??? | ??远程登录?? |

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
  • ??< gitee??? | ??github??? | ??coding??? | ??codechina > ?? 四大码仓推送 | 同步官方源码,公众号中回复 ?百万? 可方便阅读。
    ?????

关注不迷路 | 代码即人生
【v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码】
v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码

文章图片


    推荐阅读