鸿蒙内核源码分析(用户态锁篇) | 如何使用快锁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; // 锁二可申请

    如此futex1futex2有初始值并都是共享变量,想详细了解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 == 1atomic_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个,除去两个内核进程外,剩下的都归属用户进程,理论上用户进程可以创建很多快锁,这些快锁可以用于进程间(共享快锁)也可以用于线程间(私有快锁),在快锁的生命周期中该如何保存 ?
  • 无锁时,前面已经有进程在申请锁时,如何处理好新等锁进程和旧等锁进程的关系 ?
  • 释放锁时,需要唤醒已经在等锁的进程,唤醒的顺序由什么条件决定 ?

经常有很多小伙伴抱怨说:不知道学习鸿蒙开发哪些技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?

为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN

如何快速入门?

1.基本概念

2.构建第一个ArkTS应用

3.......

开发基础知识:

1.应用基础知识

2.配置文件

3.应用数据管理

4.应用安全管理

5.应用隐私保护

6.三方应用调用管控机制

7.资源分类与访问

8.学习ArkTS语言

9.......

基于ArkTS 开发

1.Ability开发

2.UI开发

3.公共事件与通知

4.窗口管理

5.媒体

6.安全

7.网络与链接

8.电话服务

9.数据管理

10.后台任务(Background Task)管理

11.设备管理

12.设备使用信息统计

13.DFX

14.国际化开发

15.折叠屏系列

16.......

鸿蒙开发面试真题(含参考答案):https://gitcode.com/HarmonyOS_MN

OpenHarmony 开发环境搭建

《OpenHarmony源码解析》 :https://gitcode.com/HarmonyOS_MN

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ......
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ......

OpenHarmony 设备开发学习手册 :https://gitcode.com/HarmonyOS_MN

写在最后

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往
相关推荐
欢乐熊嵌入式编程11 分钟前
智能手表固件升级 OTA 策略文档初稿
嵌入式硬件·学习·智能手表
欢乐熊嵌入式编程18 分钟前
智能手表 MCU 任务调度图
单片机·嵌入式硬件·智能手表
【云轩】32 分钟前
电机密集型工厂环境下的无线通信技术选型与优化策略
经验分享·嵌入式硬件
sword devil9001 小时前
将arduino开发的Marlin部署到stm32(3D打印机驱动)
stm32·单片机·嵌入式硬件
GodKK老神灭1 小时前
STM32 变量存储
stm32·单片机·嵌入式硬件
木宁kk2 小时前
51单片机引脚功能概述
单片机·嵌入式硬件
JANYI20182 小时前
嵌入式MCU和Linux开发哪个好?
linux·单片机·嵌入式硬件
晚秋大魔王2 小时前
OpenHarmony 开源鸿蒙南向开发——linux下使用make交叉编译第三方库——nettle库
linux·开源·harmonyos
sword devil9003 小时前
Arduino快速入门
stm32·单片机·嵌入式硬件
GodKK老神灭4 小时前
STM32实现循环队列
stm32·单片机·嵌入式硬件