嵌入式Linux应用开发-驱动大全-同步与互斥①

嵌入式Linux应用开发-驱动大全-同步与互斥①

  • 第一章 同步与互斥①
    • 1.1 内联汇编
      • 1.1.1 C语言实现加法
      • 1.1.2 使用汇编函数实现加法
      • 1.1.3 内联汇编语法
      • 1.1.4 编写内联汇编实现加法
      • 1.1.5 earlyclobber的例子
    • 1.2 同步与互斥的失败例子
      • 1.2.1 失败例子1
      • 1.2.2 失败例子2
      • 1.2.3 失败例子3

第一章 同步与互斥①

在这里插入图片描述

1.1 内联汇编

要深入理解 Linux内核中的同步与互斥的实现,需要先了解一下内联汇编:在 C函数中使用汇编代码。
现代编译器已经足够优秀,大部分的 C代码转成汇编码后,效率都很高。但是有些特殊的算法需要我们手工优化,这时就需要手写汇编代码;或是有时需要调用特殊的汇编指令(比如使用 ldrex/strex实现互斥访问),这都涉及内联汇编。
实际上你完全可以不使用内联汇编,单独写一个遵守 ATPCS规则的汇编函数,让 C函数去调用它。但是在 C函数中写汇编代码,可以不用另外新建一个汇编文件,比较方便。
内联汇编的完整语法比较复杂,可以参考这 3篇文章:
① GNU C扩展汇编 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
② ARM GCC 内嵌(inline)汇编手册 http://blog.chinaunix.net/uid-20543672-id-3194385.html
③ C内联汇编 https://akaedu.github.io/book/ch19s05.html
这 3章文章写得细致而深入,也有些难以理解。你跟着我们的视频或文档,就可以掌握到足够的知识。 下面举 3个例子说明汇编函数、用 C函数中使用内联汇编的方法。

1.1.1 C语言实现加法

使用 GIT下载后,源码在“07_驱动大全\source\01_inline_assembly\01_c_code\main.c”:

01 #include <stdio.h> 
02 #include <stdlib.h> 
03 
04 int add(int a, int b) 
05 { 
06      return a+b; 
07 } 
08 
09 int main(int argc, char **argv) 
10 { 
11      int a; 
12      int b; 
13 
14      if (argc != 3) 
15      { 
16              printf("Usage: %s <val1> <val2>\n", argv[0]); 
17              return -1; 
18      } 
19 
20      a = (int)strtol(argv[1], NULL, 0); 
21      b = (int)strtol(argv[2], NULL, 0); 
22 
23      printf("%d + %d = %d\n", a, b, add(a, b)); 
24      return 0; 
25 } 

上面的 add函数代码最简单,但是对应的汇编也挺复杂:需要入栈、出栈等操作,效率不算高。看看test.dis:

266 00010404 <add>: 
267    10404:   b480            push    {r7} 
268    10406:   b083            sub     sp, #12 
269    10408:   af00            add     r7, sp, #0 
270    1040a:   6078            str     r0, [r7, #4] 
271    1040c:   6039            str     r1, [r7, #0] 
272    1040e:   687a            ldr     r2, [r7, #4] 
273    10410:   683b            ldr     r3, [r7, #0] 
274    10412:   4413            add     r3, r2                // 真正实现加法的只有这条指令 275    10414:   4618            mov     r0, r3 
276    10416:   370c            adds    r7, #12 
277    10418:   46bd            mov     sp, r7 
278    1041a:   f85d 7b04       ldr.w   r7, [sp], #4 
279    1041e:   4770            bx      lr 
280 

1.1.2 使用汇编函数实现加法

使用 GIT下载后,源码在“07_驱动大全\source\01_inline_assembly\02_assembly\add.S”:

01 .text            // 放在代码段 
02 .global  add     // 实现全局函数 add 
03 .thumb           // 使用 thumb指令, main.c默认使用 thumb指令, 所以这里也使用 thumb指令 
04 
05 add: 
06      add r0, r0, r1 
07      bx lr 
08 

根据 ATPCS规则,main函数调用 add(a, b)时,会把第一个参数存入 r0寄存器,把第二个参数存入 r1寄存器。
在上面第 06行里,把 r0、r1累加后,结果存入 r0:根据 ATPCS规则,r0用来保存返回值。
可以看到,这个 add函数连栈都没有使用,非常高效。
这只是一个很简单的例子,我们工作中并不使用汇编来进行“加法优化”,在计算量非常大的地方可以考虑单独编写汇编函数实现优化。

1.1.3 内联汇编语法

从上面例子可以看到,我们完全可以新建一个汇编文件,在 ATPCS规则之下编写代码,这样 C函数就可以直接调用汇编函数。
但是,需要新建汇编文件,有点麻烦。
使用内联汇编,可以在 C代码中内嵌汇编代码。
先看看内联汇编的语法。
在这里插入图片描述

内联汇编语法:
① asm
也可以写作“asm”,表示这是一段内联汇编。
② asm-qualifiers
有 3个取值:volatile、inline、goto。
volatile的意思是易变的、不稳定的,用来告诉编译器不要随便优化这段代码,否则可能出问题。比如汇编指令“mov r0, r0”,它把 r0的值复制到 r0,并没有实际做什么事情,你的本意可能是用这条指令来延时。编译器看到这指令后,可能就把它去掉了。加上 volatile的话,编译器就不会擅自优化。
其他 2个取值我们不关心,也比较难以理解,不讲。
③ AssemblerTemplate
汇编指令,用双引号包含起来,每条指令用“\n”分开,比如:

“mov  %0, %1\n” 
“add  %0, %1, %2\n” 

④ OutputOperands
输出操作数,内联汇编执行时,输出的结果保存在哪里。
格式如下,当有多个变量时,用逗号隔开:
[ [asmSymbolicName] ] constraint (cvariablename)
asmSymbolicName是符号名,随便取,也可以不写。 constraint表示约束,有如下常用取值:
在这里插入图片描述

constraint前还可以加上一些修饰字符,比如“=r”、“+r”、“=&r”,含义如下:
在这里插入图片描述
variablename:C语言的变量名。

示例 1如下:
[result] “=r” (sum)
它的意思是汇编代码中会通过某个寄存器把结果写入 sum变量。在汇编代码中可以使用“%[result]”来引用它。

示例 2如下:
“=r” (sum)
在汇编代码中可以使用“%0”、“%1”等来引用它,这些数值怎么确定后面再说。

⑤ InputOperands
输入操作数,内联汇编执行前,输入的数据保存在哪里。
格式如下,当有多个变量时,用逗号隔开:
[ [asmSymbolicName] ] constraint (cexpression)

asmSymbolicName是符号名,随便取,也可以不写。
constraint表示约束,参考上一小节,跟 OutputOperands类似。

cexpression:C语言的表达式。

示例 1如下:
[a_val]“r”(a), [b_val]“r”(b)
它的意思变量 a、b的值会放入某些寄存器。在汇编代码中可以使用%[a_val]、%[b_val]使用它们。

示例 2如下:
“r”(a), “r”(b) 它的意思变量 a、b的值会放入某些寄存器。在汇编代码中可以使用%0、%1等使用它们,这些数值后面再说。

⑥ Clobbers 在汇编代码中,对于“OutputOperands”所涉及的寄存器、内存,肯定是做了修改。但是汇编代码中,也许要修改的寄存器、内存会更多。比如在计算过程中可能要用到 r3保存临时结果,我们必须在“Clobbers”中声明 r3会被修改。 下面是一个例子: : “r0”, “r1”, “r2”, “r3”, “r4”, “r5”, “memory”

我们常用的是有“cc”、“memory”,意义如下:
在这里插入图片描述

1.1.4 编写内联汇编实现加法

使用 GIT下载后,源码在“07_驱动大全\source\01_inline_assembly\03_inline_assembly\main.c”:

04 int add(int a, int b) 
05 { 
06      int sum; 
07      __asm__ volatile ( 
08              "add %0, %1, %2" 
09              :"=r"(sum) 
10              :"r"(a), "r"(b) 
11              :"cc" 
12      ); 
13      return sum; 

在这里插入图片描述

所以第 08行代码就是:把第 1、2个操作数相加,存入第 0个操作数。也就是把 a、b相加,存入 sum。
还可以使用另一种写法,在 Linux内核中这种用法比较少见。
使用 GIT下载后,源码在“07_驱动大全\source\01_inline_assembly\03_inline_assembly\main2.c”:
在这里插入图片描述

1.1.5 earlyclobber的例子

OutputOperands的约束中经常可以看到“=&r”,其中的“&”表示 earlyclobber,它是最难理解的。有
一些输出操作数在汇编代码中早早就被写入了新值 A,在这之后,汇编代码才去读取某个输入操作数,这个输出操作数就被称为 earlyclobber(早早就被改了)。
这可能会有问题:假设早早写入的新值 A,写到了 r0寄存器;后面读输入操作数时得到数值 B,也可能写入 r0寄存器,这新值 A就被破坏了。
核心原因就在于输出操作数、输入操作数都用了同一个 r0寄存器。为什么要用同一个?因为编译器不知道你是 earlyclobber的,它以为是先读入了所有输入操作数,都处理完了,才去写输出操作数的。按这流程,没人来覆盖新值 A。
所以,如果汇编代码中某个输出操作数是 earlyclobber的,它的 constraint就要加上“&”,这就是告诉编译器:给我分配一个单独的寄存器,别为了省事跟输入操作数用同一个寄存器。
使用 GIT下载后,源码在“07_驱动大全\source\01_inline_assembly\04_earlyclobber\main.c”:
在这里插入图片描述

上面的代码中,输出操作数%0对应的寄存器是 r3,输入操作数%1对应的寄存器也是 r3。
第 8行更新了%0的值后,第 9行修改%1的值,由于%0、%1是同一个寄存器,所以%0的值也被修改了。 最终返回的累加值是错的,增加了 1,如下图所示:
在这里插入图片描述

怎么修改?在第 11行加“&”就可以了,这是告诉编译器,对于%0操作数它是 earlyclobber的,不能跟其他操作数共用寄存器,如下:
在这里插入图片描述

从右边的反汇编码可以知道,%0跟%1、%2使用不一样的寄存器,所以后面第 9、10行无法影响到%0的值。
程序运行结果如下图所示:
在这里插入图片描述

1.2 同步与互斥的失败例子

注意:本节在 GIT上没有源码。
一句话理解同步与互斥:我等你用完厕所,我再用厕所。
什么叫同步?就是条件不允许,我要等等。
什么是互斥?你我早起都要用厕所,谁先抢到谁先用,中途不被打扰。
同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来实现。我“等”你用完厕所,我再用厕所。这不就是用“同步”来实现“互斥”吗?
有时候看代码更容易理解,伪代码如下:

01 void  抢厕所(void) 
02 { 
03    if (有人在用) 我眯一会; 
04    用厕所; 
05    喂,醒醒,有人要用厕所吗; 
06 } 

假设有 A、B两人早起抢厕所,A先行一步占用了;B慢了一步,于是就眯一会;当 A用完后叫醒 B,B也就愉快地上厕所了。
在这个过程中,A、B是互斥地访问“厕所”,“厕所”被称之为临界资源。我们使用了“休眠-唤醒”的同步机制实现了“临界资源”的“互斥访问”。
上面是一个有“味道”的例子,回到程序员的世界,一个驱动程序同时只能有一个 APP使用,怎么实现?

1.2.1 失败例子1

01 static int valid = 1; 
02 
03 static ssize_t gpio_key_drv_open (struct inode *node, struct file *file) 04 { 
05      if (!valid) 
06      { 
07              return -EBUSY; 
08      } 
09      else 
10      { 
11              valid = 0; 
12      } 
13 
14      return 0; //成功 
15 } 
16 
17 static int gpio_key_drv_close (struct inode *node, struct file *file) 
18 { 
19      valid = 1; 
20      return 0; 21 } 
22 

看第 5行,我们使用一个全局变量 valid来实现互斥访问。这有问题吗?很大概率没问题,但是并非万无一失。
注意:编写驱动程序时,要有系统的概念,程序 A调用驱动程序时,它可能被程序 B打断,程序 B也去调用这个驱动程序。
下图是一个例子,程序 A在调用驱动程序的中途被程序 B抢占了 CPU资源:
在这里插入图片描述

程序 A执行到第 11行之前,被程序 B抢占了,这时 valid尚未被改成 0;
程序 B调用 gpio_key_drv_open时,发现 valid等于 1,所以成功返回 0;
当程序 A继续从第 11行执行时,它最终也成功返回 0;
这样程序 A、B都成功打开了驱动程序。
注意:在内核态,程序 A不是主动去休眠、主动放弃 CPU资源;而是被优先级更高的程序 B抢占了,这种行为被称为“preempt”(抢占)。

1.2.2 失败例子2

上面的例子是不是第 5行到第 11行的时间跨度大长了?再优化一下程序行不行?代码如下:

01 static int valid = 1; 
02 
03 static ssize_t gpio_key_drv_open (struct inode *node, struct file *file) 
04 { 
05      if (--valid) 
06      { 
07              valid++; 
08              return -EBUSY; 
09      } 
10      return 0; 
11 } 
12 
13 static int gpio_key_drv_close (struct inode *node, struct file *file) 
14 { 
15      valid = 1; 
16      return 0; 
17 } 
18 

第 5行先减 1再判断,这样可以更大概率地避免问题,但是还是不能确保万无一失。对数据的修改分为 3步:读出来、修改、写进去。请看下图:
在这里插入图片描述

进程 A在读出 valid时发现它是 1,减 1后为 0,这时 if不成立;但是修改后的值尚未写回内存; 假设这时被程序 B抢占,程序 B读出 valid仍为 1,减 1后为 0,这时 if不成立,最后成功返回; 轮到 A继续执行,它把 0值写到 valid变量,最后也成功返回。
这样程序 A、B都成功打开了驱动程序。

1.2.3 失败例子3

前面 2个例子,都是在修改 valid的过程中被别的进程抢占了,那么在修改 valid的时候直接关中断不就可以了吗?

01 static int valid = 1; 
02 
03 static ssize_t gpio_key_drv_open (struct inode *node, struct file *file) 
04 { 
05       unsigned long flags; 
06       raw_local_irq_save(flags); // 关中断 
07      if (--valid) 
08      { 
09              valid++; 
10              raw_local_irq_restore(flags);  // 恢复之前的状态 
11              return -EBUSY; 
12      } 
13       raw_local_irq_restore(flags);          // 恢复之前的状态 
14      return 0; 
15 } 
16 
17 static int gpio_key_drv_close (struct inode *node, struct file *file) 
18 { 
19      valid = 1; 
20      return 0; 
21 } 

第 06行直接关中断,这样别的线程、中断都不能来打扰本线程了,在它读取、修改 valid变量的过程中无人打扰。
没有问题了?

对于单 CPU核的系统上述代码是没问题的;但是对于 SMP系统,你只能关闭当前 CPU核的中断,别的CPU核还可以运行程序,它们也可以来执行这个函数,同样导致问题,如下图:
在这里插入图片描述

假设 CPU0上进程 A、CPU1上进程 B同时运行到上图中读出 valid的地方,它们同时发现 valid都是 1,减减后都等于 0,在第 07行判断条件都不成立,所以在第 14行都可以返回 0,都可以成功打开驱动。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/94256.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

互联网Java工程师面试题·MyBatis 篇·第二弹

目录 16、Xml 映射文件中&#xff0c;除了常见的 select|insert|updae|delete标签之外&#xff0c;还有哪些标签&#xff1f; 17、Mybatis 的 Xml 映射文件中&#xff0c;不同的 Xml 映射文件&#xff0c;id 是否可以重复&#xff1f; 18、为什么说 Mybatis 是半自动 ORM 映射…

2023年中国体育赛事行业现状及趋势分析:体育与科技逐步融合,推动产业高质量发展[图]

体育赛事运营是指组织体育赛事或获取赛事版权&#xff0c;并进行赛事推广营销、运营管理等一系列商业运作的运营活动。体育赛事运营相关业务主要包括赛事运营与营销、赛事版权运营两个部分。 体育赛事运营行业分类 资料来源&#xff1a;共研产业咨询&#xff08;共研网&#x…

5.外部中断

中断初始化配置步骤&#xff1a; IO口初始化配置 开启中断总允许EA 打开某个IO口的中断允许 打开IO口的某一位的中断允许 配置该位的中断触发方式 中断函数&#xff1a; #pragma vector PxINT_VECTOR __interrupt void 函数名(void){}#pragma vector PxINT_VECTOR __int…

开源白板工具 Excalidraw 架构解读

本文讲解开源白板工具 Excalidraw 的架构设计。 版本 0.16.1 技术栈 Vite React TypeScript Yarn Husky。 脚手架原来是用的是 Create React App&#xff0c;但这个脚手架已经不维护了&#xff0c;一年多没发布新版本了。 目前市面上比较流行的 React 脚手架是 Vite&…

RabbitMQ的基本介绍

什么是MQ 本质是一个队列&#xff0c;只不过队列中存放的信息是message罢了&#xff0c;还是一种跨进程的通信机制&#xff0c;用于上下游传递信息。在互联网架构中&#xff0c;MQ是一种非常常见的上下游“逻辑解耦物理解耦”的消息通信服务。使用了MQ之后&#xff0c;信息发送…

嵌入式Linux应用开发-驱动大全-同步与互斥④

嵌入式Linux应用开发-驱动大全-同步与互斥④ 第一章 同步与互斥④1.5 自旋锁spinlock的实现1.5.1 自旋锁的内核结构体1.5.2 spinlock在UP系统中的实现1.5.3 spinlock在SMP系统中的实现 1.6 信号量semaphore的实现1.6.1 semaphore的内核结构体1.6.2 down函数的实现1.6.3 up函数的…

用于工业物联网和自动化的 Apache Kafka、KSQL 和 Apache PLC4

由于单一系统和专有协议&#xff0c;数据集成和处理是工业物联网&#xff08;IIoT&#xff0c;又名工业 4.0 或自动化工业&#xff09;中的巨大挑战。Apache Kafka、其生态系统&#xff08;Kafka Connect、KSQL&#xff09;和 Apache PLC4X 是以可扩展、可靠和灵活的方式实现端…

【文献阅读】Pocket2Mol : 基于3D蛋白质口袋的高效分子采样 + CrossDocked数据集说明

Pocket2Mol: Efficient Molecular Sampling Based on 3D Protein Pockets code&#xff1a; GitHub - pengxingang/Pocket2Mol: Pocket2Mol: Efficient Molecular Sampling Based on 3D Protein Pockets 所用数据集 与“A 3D Generative Model for Structure-Based Drug Desi…

MySQL进阶 —— 超详细操作演示!!!(下)

MySQL进阶 —— 超详细操作演示&#xff01;&#xff01;&#xff01;&#xff08;下&#xff09; 五、锁5.1 概述5.2 全局锁5.3 表级锁5.4 行级锁 六、InnoDB 引擎6.1 逻辑存储结构6.2 架构6.3 事务原理6.4 MVCC 七、MySQL 管理7.1 系统数据库7.2 常用工具 MySQL— 基础语法大…

使用代理IP进行安全高效的竞争情报收集,为企业赢得竞争优势

在激烈的市场竞争中&#xff0c;知己知彼方能百战百胜。竞争对手的信息对于企业来说至关重要&#xff0c;它提供了洞察竞争环境和市场的窗口。在这个信息时代&#xff0c;代理IP是一种实用的工具&#xff0c;可以帮助企业收集竞争对手的产品信息和营销活动数据&#xff0c;为企…

python二次开发CATIA:根据已知数据点创建曲线

已知数据点存于Coords.txt文件如下&#xff1a; 8.67155477658819,20.4471021292557,0 41.2016126836927,20.4471021292557,0 15.9568941320569,-2.93388599177698,0 42.2181532110364,-6.15301746150354,0 43.0652906622083,-26.4843096139083,0 -31.6617679595947,-131.1513…

分类预测 | MATLAB实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测

分类预测 | MATLAB实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测 目录 分类预测 | MATLAB实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测&…

C++项目:【高并发内存池】

文章目录 一、项目介绍 二、什么是内存池 1.池化技术 2.内存池 3.内存池主要解决的问题 4.malloc 三、定长的内存池 四、高并发内存池整体框架设计 1.高并发内存池--thread cache 1.1申请内存&#xff1a; 1.2释放内存&#xff1a; 1.3用TLS实现thread cache无锁访…

rabbitMQ死信队列快速编写记录

文章目录 1.介绍1.1 什么是死信队列1.2 死信队列有什么用 2. 如何编码2.1 架构分析2.2 maven坐标2.3 工具类编写2.4 consumer1编写2.5 consumer2编写2.6 producer编写 3.整合springboot3.1 架构图3.2 maven坐标3.3 构建配置类&#xff0c;创建exchange&#xff0c;queue&#x…

想要精通算法和SQL的成长之路 - 二叉树的判断问题(子树判断 | 对称性 | 一致性判断)

想要精通算法和SQL的成长之路 - 二叉树的判断问题 前言一. 相同的树二. 对称二叉树三. 判断子树 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 相同的树 原题链接 这题目典型的递归题&#xff1a; 如果两个节点都是null&#xff0c;我们返回true。如果两个节点一个nul…

centos 部署nginx 并配置https

centos版本&#xff1a;centos 7.8 &#xff08;最好不要用8&#xff0c;8的很多用法和7相差很大&#xff09; 一.安装nginx 1。下载Nginx安装包&#xff1a;首先&#xff0c;访问Nginx的官方网站&#xff08;https://nginx.org/&#xff09;或您选择的镜像站点&#xff0c;找…

C#学生选课及成绩查询系统

一、项目背景 学生选课及成绩查询系统是一个学校不可缺少的部分&#xff0c;传统的人工管理档案的方式存在着很多的缺点&#xff0c;如&#xff1a;效率低、保密性差等&#xff0c;所以开发一套综合教务系统管理软件很有必要&#xff0c;它应该具有传统的手工管理所无法比拟的…

关于算法复杂度的几张表

算法在改进今天的计算机与古代的计算机的区别 去除冗余 数据点 算法复杂度 傅里叶变换

解决java.io.FileNotFoundException: HADOOP_HOME and hadoop.home.dir are unset.的错误

文章目录 1. 复现错误2. 分析错误3. 解决问题3.1 下载Hadoop3.2 配置Hadoop3.3 下载winutils3.4 配置winutils 1. 复现错误 今天在运行同事给我的项目&#xff0c;但在项目启动时&#xff0c;报出如下错误&#xff1a; java.io.FileNotFoundException: java.io.FileNotFoundEx…

嵌入式系统中C++内存管理基本方法

引言 说到 C 的内存管理&#xff0c;我们可能会想到栈空间的本地变量、堆上通过 new 动态分配的变量以及全局命名空间的变量等&#xff0c;这些变量的分配位置都是由系统来控制管理的&#xff0c;而调用者只需要考虑变量的生命周期相关内容即可&#xff0c;而无需关心变量的具…