v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码
百篇博客分析|本篇为:(用户态锁篇) | 如何使用快锁Futex(上)
进程通讯相关篇为:
- v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志
- v27.05 鸿蒙内核源码分析(互斥锁) | 同样是锁它却更丰满
- v28.04 鸿蒙内核源码分析(进程通讯) | 九种进程间通讯方式速揽
- v29.05 鸿蒙内核源码分析(信号量) | 谁在解决任务间的同步
- v30.07 鸿蒙内核源码分析(事件控制) | 多对多任务如何同步
- v33.03 鸿蒙内核源码分析(消息队列) | 进程间如何异步传递大数据
- v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式
- v77.02 鸿蒙内核源码分析(消息封装) | 剖析LiteIpc(上)进程通讯内容
- v78.01 鸿蒙内核源码分析(消息映射) | 剖析LiteIpc(下)进程通讯机制
- v79.01 鸿蒙内核源码分析(用户态锁) | 如何使用快锁Futex(上)
- v80.01 鸿蒙内核源码分析(内核态锁) | 如何实现快锁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) do { perror(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
(进行负载操作)。
什么意思 ? 来个直白的解释 :- 如果
futexp == 1
则atomic_compare_exchange_strong
返回真,同时将futexp
的值变成0
,1代表可以持有锁,一旦持有立即变0,别人就拿不到了。所以此处甚秒。而且这发生在用户态。 - 如果
futexp == 0
atomic_compare_exchange_strong
返回假,没有拿到锁,就需要陷入内核态去挂起任务等待锁的释放
futex(futexp, FUTEX_WAIT, 0, NULL, NULL, 0) //执行一个等锁的系统调用
参数四为
NULL
代表不在内核态停留直接返回用户态,后续将在内核态部分详细说明。- 如果
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
几个问题
以上是个简单的例子,只发生在两个进程抢一把锁的情况下,如果再多几个进程抢一把锁时情况就变复杂多了。
例如会遇到以下情况:
- 鸿蒙内核进程池默认上限是
64
个,除去两个内核进程外,剩下的都归属用户进程,理论上用户进程可以创建很多快锁,这些快锁可以用于进程间(共享快锁)也可以用于线程间(私有快锁),在快锁的生命周期中该如何保存 ? - 无锁时,前面已经有进程在申请锁时,如何处理好新等锁进程和旧等锁进程的关系 ?
- 释放锁时,需要唤醒已经在等锁的进程,唤醒的顺序由什么条件决定 ?
这些工作在用户态下肯定没办法完成,需要内核处理,请查看 (内核态锁篇) | 如何实现快锁Futex(下),详细解构其实现过程。
百文说内核 | 抓住主脉络
- 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
- 与代码需不断
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。 - 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,公众号回复 百文 可方便阅读。
按功能模块:
百万注源码 | 处处扣细节
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
< gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码,公众号中回复 百万 可方便阅读。
关注不迷路 | 代码即人生
v79.01 鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁Futex(上) | 百篇博客分析OpenHarmony源码的更多相关文章
- v80.01 鸿蒙内核源码分析(内核态锁篇) | 如何实现快锁Futex(下) | 百篇博客分析OpenHarmony源码
百篇博客分析|本篇为:(内核态锁篇) | 如何实现快锁Futex(下) 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) ...
- v74.01 鸿蒙内核源码分析(编码方式篇) | 机器指令是如何编码的 | 百篇博客分析OpenHarmony源码
本篇关键词:指令格式.条件域.类型域.操作域.数据指令.访存指令.跳转指令.SVC(软件中断) 内核汇编相关篇为: v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的 v75.03 ...
- v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main () | 百篇博客分析 OpenHarmony 源码
本篇关键词:内核重定位.MMU.SVC栈.热启动.内核映射表 内核汇编相关篇为: v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的 v75.03 鸿蒙内核源码分析(汇编基础) | ...
- 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 百篇博客分析OpenHarmony源码 | v63.01
百篇博客系列篇.本篇为: v63.xx 鸿蒙内核源码分析(文件系统篇) | 用图书管理说文件系统 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...
- 鸿蒙内核源码分析(文件概念篇) | 为什么说一切皆是文件 | 百篇博客分析OpenHarmony源码 | v62.01
百篇博客系列篇.本篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一切皆是文件 | 51.c.h.o 本篇开始说文件系统,它是内核五大模块之一,甚至有Linux的设计哲学是" ...
- 鸿蒙内核源码分析(进程镜像篇)|ELF是如何被加载运行的? | 百篇博客分析OpenHarmony源码 | v56.01
百篇博客系列篇.本篇为: v56.xx 鸿蒙内核源码分析(进程映像篇) | ELF是如何被加载运行的? | 51.c.h.o 加载运行相关篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应 ...
- v72.01 鸿蒙内核源码分析(Shell解析) | 应用窥伺内核的窗口 | 百篇博客分析OpenHarmony源码
子曰:"苟正其身矣,于从政乎何有?不能正其身,如正人何?" <论语>:子路篇 百篇博客系列篇.本篇为: v72.xx 鸿蒙内核源码分析(Shell解析篇) | 应用窥视 ...
- v75.01 鸿蒙内核源码分析(远程登录篇) | 内核如何接待远方的客人 | 百篇博客分析OpenHarmony源码
子曰:"不学礼,无以立 ; 不学诗,无以言 " <论语>:季氏篇 百篇博客分析.本篇为: (远程登录篇) | 内核如何接待远方的客人 设备驱动相关篇为: v67.03 ...
- v76.01 鸿蒙内核源码分析(共享内存) | 进程间最快通讯方式 | 百篇博客分析OpenHarmony源码
百篇博客分析|本篇为:(共享内存篇) | 进程间最快通讯方式 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁) | 同样 ...
随机推荐
- element ui 动态菜单解决方案集锦
1.<分享一个VUE Element-UI 的多级菜单动态渲染的组件> 2.<饿了么组件库,element-ui开发精美的后台管理系统系列之(一)开发伸缩菜单> 3.<V ...
- 基本的sql语法
1. SELECT: 用于从数据库中选取数据 SELECT name,value FROM table_name 2.SELECT DISTINCT 语句用于返回唯一不同的值(去重) 3.WHERE ...
- 使用.NET 6开发TodoList应用(28)——实现应用程序健康检查
系列导航及源代码 使用.NET 6开发TodoList应用文章索引 需求 应用健康检查在容器部署的微服务场景下非常常见,相比而言单体非容器部署的应用就不太关心这个特性,为了后续的内容我们在本文中简单介 ...
- Android官方文档翻译 八 2.1Setting Up the Action Bar
Setting Up the Action Bar 建立Action Bar This lesson teaches you to 这节课教给你 Support Android 3.0 and Abo ...
- nao机器人使用手册
简单使用和保养 开关机和马达 开机是按一下,后来按一下相当于重启了一次程序,3是播报IP地址,5秒是关机,8秒是强制关机. 电池 3月左右不用需要取下电池.夏天5-8小时,冬天8-10小时充电.活动时 ...
- gin框架中的渲染
各种数据格式的响应 json.结构体.XML.YAML类似于java的properties.ProtoBuf 点击查看代码 // json响应 func someJson(context *gin.C ...
- MySQL存储引擎(最全面的概括)
目录 一:MySQL存储引擎 1.什么是存储引擎? 2.查看存储引擎信息 二:MySQL支持的存储引擎 1.存储引擎 三:innoDB存储引擎 1.特性 2.存储结构 3.优缺点.适用场景 四:MyI ...
- 字的研究(3)fontTools-TrueType轮廓坐标的获取以及基于TrueType的Glyph实例的构建
前言 本文主要介绍如果使用Python第三方库fontTools提取OpenType字体文件中的TrueType轮廓坐标以及如何构建基于TrueType的Glyph实例 TrueType轮廓坐标的获取 ...
- Integer缓冲区相关问题--valueOf()方法
今天在学习过程中了解到一个现象,代码如下: Integer num1 = 100; Integer num2 = 100; System.out.println(num1==num2?true:fal ...
- Java 继承01
继承 ●示例 class Person { public String name; Person(){ System.out.println("Person Constrctor...&qu ...