62、内核同步 - 第1部分

内核同步 - 第1部分

1. 技术要求

在开始之前,需要完成以下准备工作:
- 完成在线章节和内核工作区的设置。
- 准备一个运行Ubuntu 22.04 LTS(或更高版本的稳定发行版,或最近的Fedora发行版)的虚拟机,并安装所有必需的软件包。
- 强烈建议先设置工作区环境,包括克隆代码的GitHub仓库,仓库地址为:https://github.com/PacktPublishing/Linux-Kernel-Programming_2E 。
- 如果对基本的Linux驱动概念不熟悉,可以参考相关资料进行学习。

2. 关键概念
2.1 临界区、独占执行和原子性

在多核系统中编写软件时,并行运行多个代码路径通常是安全且可取的。但当多个并发代码路径访问共享可写数据(共享状态)时,必须保证在任何时刻只有一个线程可以对该数据进行操作,否则可能会导致数据损坏(数据竞争)。

临界区是指满足以下两个条件的代码路径:
- 条件一:代码路径可能是并发的,即存在并行运行的可能性。
- 条件二:该代码路径对共享可写数据进行读取和/或写入操作。

临界区的代码必须独占执行,有时还需要原子执行:
- 独占执行意味着在任何时刻,只有一个线程可以运行临界区的代码。
- 原子执行意味着操作不可分割,能够不间断地完成。

如果两个或多个线程可以同时执行临界区的代码,这就是一个错误,通常称为竞争条件或数据竞争。正确识别和保护临界区是软件设计和开发中必须确保的。

下面通过几个例子来帮助理解临界区的概念:

void qux(int factor)
{
    int nok;
    [ … ]
    //------------------------ t1
    nok += 10*PI;
    //------------------------ t2
    printf("..."); [ … ]
}

练习1 :在上述用户模式Pthreads(伪)代码片段中,假设函数代码可以由多个线程并行运行,那么时间t1和t2之间的代码区域是否构成临界区?
解决方案1 :判断一个代码区域是否为临界区,需要看它是否满足两个条件:可能并行执行且操作共享可写数据。在这个例子中,代码可以并行运行,但变量 nok 是局部变量,不是共享可写数据,每个线程都会在自己的栈上获得该变量的一个副本。因此,答案是否,该代码区域不是临界区,可以并行运行,无需显式保护。

再看另外两个内核模块(伪)代码片段:

static struct quux_drvctx {
        ... 
} *mydrv;
write_quux()      /*  driver write method   */
{
    [ … ]
    //-------------------- t1
    mydrv->sensor2 = 1;
    mydrv->hw = hw_zoom;
    //-------------------- t2
               [ … ]
}
static int glob;
static __init my_kmod_init(void) /* init method */
{
        [ … ]
        //-------------------- t1
    glob += 14;
    pr_info(...);
        //-------------------- t2
        [ … ]
}
[ … ]
module_init(my_kmod_init);
module_exit(my_kmod_cleanup);

练习2和3 :在上述内核模块(伪)代码片段中,假设代码可以由多个用户模式线程切换到内核空间并行运行,那么时间t1和t2之间的代码区域是否构成临界区?(解决方案可在后续查找,建议先自行尝试解决。)

2.2 原子性

原子操作是不可分割的操作。在现代处理器中,通常认为以下两种操作是原子的:
- 单条机器语言指令的执行。
- 对处理器字长(通常为32或64位)内的对齐基本数据类型的读写操作。例如,在64位系统上读写32位或64位整数是原子的,线程读取该变量时不会看到中间、撕裂或脏数据。但32位处理器对64位数据的操作不一定是原子的,可能会导致撕裂或脏读(或写)。

如果代码操作共享(全局或静态)可写数据,在没有显式同步机制的情况下,不能保证其独占执行。有时,临界区的代码需要原子执行,但并非总是如此,这取决于代码的运行上下文:
- 当临界区的代码在可能阻塞的安全睡眠进程上下文中运行时(如通过用户应用程序对驱动程序进行的典型文件操作,或内核线程或工作队列的执行路径),临界区不一定要真正原子执行,但需要独占执行。
- 当临界区的代码在非阻塞的原子上下文中运行时(如硬件中断:硬中断、小任务或软中断),则必须原子且独占执行。

下面通过一个概念示例来进一步说明:假设有三个线程从用户空间应用程序发出 open() 和 read() 系统调用,在多核系统上几乎同时对驱动程序进行操作。如果没有干预,它们可能会并行运行临界区的代码,从而对共享可写数据进行并行操作,导致数据竞争和损坏。

从数据访问的角度来看,不同时间段的操作情况如下:
| 时间段 | 数据访问情况 | 是否需要保护 | 能否并行运行 |
| ---- | ---- | ---- | ---- |
| t0 - t1 | 无或仅访问局部变量数据 | 否 | 是 |
| t1 - t2 | 访问全局/静态共享可写数据 | 是 | 否 |
| t2 - t3 | 无或仅访问局部变量数据 | 否 | 是 |

综上所述,临界区的代码必须满足以下要求:
- 始终独占执行。
- 在原子上下文中原子执行。

3. 经典示例:全局变量i++

考虑一个经典的例子:在可能并发的代码路径中对全局整数 i 进行自增操作。很多人可能会认为这个操作是原子的,但实际上现代硬件和软件(编译器和操作系统)会进行各种优化,使得情况变得复杂。

static int i = 5;
[ ... ]
foo()
{
    [ ... ]
    i ++;     /* Is this safe? Yes, if this code path is truly exclusive or atomic... 
               * Alternately, if this code path's not exclusive, is the i ++ truly atomic?? */
}

这个自增操作是否安全呢?简短的答案是不安全,需要进行保护。因为这是一个临界区,在可能并发的代码路径中访问(读写)共享可写数据。更详细的答案取决于:
- 函数 foo() 的代码是否能保证独占执行。
- 这里的自增操作是否真正原子(不可分割)。

现代处理器采用了多种技术来提高性能,如超标量和超流水线执行、指令和内存重排序、复杂的CPU缓存等,这些都会影响操作的原子性。

要判断 i++ 操作是否原子,可以通过以下步骤:
1. 打开编译器探索网站:https://godbolt.org/ 。
2. 选择C作为编程语言。
3. 在左窗格中声明全局整数 i ,并在函数中对其进行自增操作。
4. 在右窗格中使用适当的编译器和编译器选项进行编译。
5. 查看生成的实际机器代码。如果是单条机器指令,则操作是安全且原子的;否则,需要进行锁定保护。

通常,在基于CISC的机器(如x86[_64])上,编译器优化级别为2及以上时,代码可能会是原子的,但在基于RISC的机器(如ARM)上不一定。因此,默认情况下应假设 i++ 操作是不安全的,需要进行保护。

例如,在没有优化的情况下, i++ 通常会变成三条机器指令:将 i 从内存加载到寄存器、自增操作、将寄存器的值存储回内存。这不是原子操作,可能会导致数据竞争。而使用 -O2 优化选项时, i++ 可能会变成单条机器指令,实现原子操作,但这并不总是可靠的。

4. 锁的概念

由于线程可以并发执行访问共享可写数据的临界区代码,因此需要进行同步,消除并行性,使临界区的代码串行执行。

一种常见的技术是使用锁。锁的基本原理是在任何时刻,只有一个线程可以“获取”或拥有锁,获取锁的线程成为“获胜者”,可以继续执行临界区的代码。其他线程则需要等待锁被释放。

锁的工作过程可以用以下流程图表示:

graph TD;
    A[多个线程竞争锁] --> B{是否获取到锁};
    B -- 是 --> C[执行临界区代码];
    C --> D[释放锁];
    D --> E[其他线程继续竞争锁];
    B -- 否 --> F[等待锁释放];
    F --> E;

使用锁可以保证临界区代码的独占执行,但会带来较大的开销,因为它消除了并行性,使执行流程串行化。锁就像一个漏斗,只有一个线程可以通过,其他线程需要等待,会造成瓶颈。因此,作为软件架构师,应尽量减少锁的使用,优化和减少全局变量的使用。

需要注意的是,新手程序员可能会认为对共享可写数据对象的读取操作是安全的,不需要显式保护,但实际上除了处理器总线大小内的对齐基本数据类型外,这种情况可能会导致脏读或撕裂读。

内核同步 - 第1部分

5. 锁的开销与优化思路

锁虽然能保证临界区代码的独占执行,但会带来显著的开销,就像前文提到的,它会破坏并行性,使执行流程串行化,产生瓶颈。为了更直观地理解,我们可以将其类比为高速公路上多车道合并为一条拥堵车道,或者一个狭窄的漏斗,只有一个线程能通过,其他线程只能等待。

为了减少锁带来的负面影响,软件架构师需要在设计产品或项目时尽量减少锁的使用。虽然在大多数实际项目中完全消除全局变量不太现实,但可以通过优化和减少其使用来降低锁的需求。以下是一些具体的思路:
- 优化全局变量使用 :尽量减少全局变量的数量,将数据封装在更小的作用域内,降低数据共享的范围。
- 使用锁粒度控制 :根据实际情况选择合适的锁粒度。如果锁的范围过大,会导致更多的线程等待,降低并发性能;如果锁的范围过小,会增加锁的管理开销。
- 探索无锁编程技术 :在某些场景下,可以使用无锁编程技术,避免使用锁带来的开销。例如,使用原子操作、CAS(Compare-And-Swap)操作等。

6. 不同类型锁的特点及适用场景

在实际编程中,有多种类型的锁可供选择,不同类型的锁具有不同的特点和适用场景。以下是一些常见的锁及其特点:
| 锁类型 | 特点 | 适用场景 |
| ---- | ---- | ---- |
| 互斥锁(Mutex) | 用于保护临界区,同一时间只允许一个线程访问。线程获取锁时如果锁已被占用,会进入阻塞状态。 | 适用于可能会睡眠的上下文,如在用户应用程序对驱动程序进行的典型文件操作中。 |
| 自旋锁(Spinlock) | 线程在获取锁时,如果锁已被占用,会不断自旋等待,不会睡眠。 | 适用于非阻塞的原子上下文,如硬件中断处理程序中,因为在这种上下文中不允许睡眠。 |
| 读写锁(Read-Write Lock) | 允许多个线程同时进行读操作,但在写操作时会独占锁。 | 适用于读多写少的场景,提高并发性能。 |

下面通过一个简单的示例来对比互斥锁和自旋锁的使用:

// 互斥锁示例
#include <linux/mutex.h>

static DEFINE_MUTEX(my_mutex);

void my_function()
{
    mutex_lock(&my_mutex);
    // 临界区代码
    // ...
    mutex_unlock(&my_mutex);
}

// 自旋锁示例
#include <linux/spinlock.h>

static spinlock_t my_spinlock;

void my_function_spin()
{
    unsigned long flags;
    spin_lock_irqsave(&my_spinlock, flags);
    // 临界区代码
    // ...
    spin_unlock_irqrestore(&my_spinlock, flags);
}

在上述示例中, my_function 使用了互斥锁,适用于可能会睡眠的上下文; my_function_spin 使用了自旋锁,适用于非阻塞的原子上下文。

7. 锁的使用注意事项

在使用锁时,需要注意以下几点:
- 避免死锁 :死锁是指两个或多个线程相互等待对方释放锁,导致程序无法继续执行的情况。为了避免死锁,需要确保线程按照相同的顺序获取锁,或者使用超时机制。
- 锁的粒度控制 :如前文所述,需要根据实际情况选择合适的锁粒度,避免锁的范围过大或过小。
- 中断处理 :在中断处理程序中使用锁时,需要特别注意。例如,在使用自旋锁时,需要使用 spin_lock_irqsave 和 spin_unlock_irqrestore 来保存和恢复中断状态,避免中断处理程序和普通线程之间的竞争。

8. 总结

内核同步是多核系统编程中非常重要的一部分,尤其是在处理共享可写数据时,需要正确识别和保护临界区,避免数据竞争和损坏。锁是一种常用的同步机制,但会带来一定的开销,因此需要合理使用。

在实际编程中,需要根据不同的场景选择合适的锁类型,并注意锁的使用注意事项,避免出现死锁等问题。同时,还可以探索无锁编程技术,优化和减少全局变量的使用,提高程序的并发性能。

通过本文的介绍,相信你对内核同步的基本概念、锁的使用等有了更深入的理解。在后续的编程实践中,希望你能够灵活运用这些知识,编写出高效、稳定的代码。

相关推荐

ARM GIC中断路由机制解析:从原理到AM62L实战配置

通用中断控制器(GIC)是现代多核处理器中管理中断分发的核心硬件模块,其工作原理类似于一个高效的交通指挥系统。GIC通过Distributor(分发器)和CPU Interface(CPU接口)的协同工作,将外部中断请求精准路由到目标CPU核心。中断路由机制是GIC的关键功能,它决定了每个中断源(特别是SPI共享外设中断)由哪个或哪些核心处理,直接影响系统的实时性、负载均衡和性能表现。在ARM Cortex-A/M架构的SoC中,如AM62L,GICD_IROUTER寄存器族是实现中断路由配置的底层硬件接口

weixin_30591551的博客 477

AM62L DEBUGSS调试子系统:从CoreSight原理到多核实战

在嵌入式系统开发中,片上调试子系统是进行实时问题诊断与性能分析的核心基础设施。其原理基于Arm CoreSight架构,该架构定义了一套标准化的调试与追踪组件互联体系,包括追踪源、ATB总线、交叉触发接口(CTI)及追踪缓冲区等。这项技术的核心价值在于,它允许开发者以非侵入式的方式,实时捕获处理器执行流、数据访问及系统事件,从而将黑盒式的调试转变为可观测、可控制的透明化分析。在实际应用场景中,它广泛用于定位偶发性内存损坏、分析多核间同步延迟、优化中断响应时间等复杂问题。本文以TI AM62L SoC的DEB

weixin_34342207的博客 296

62、内核同步 - 第一部分

本文深入探讨了Linux内核同步的核心概念,包括临界区、原子性以及锁的使用与优化。文章解释了并发编程中可能出现的问题,如数据竞争和竞态条件,并介绍了如何通过锁机制、原子操作以及无锁编程技术来确保代码的并发安全性和性能。此外,还分析了不同锁类型的适用场景及其优缺点,帮助开发者在多线程环境下编写高效稳定的内核代码。

4k5l6j7h8的博客 48

62、内核同步技术深入解析

本博客深入解析了Linux内核中的同步技术,包括原子操作、锁机制、内存屏障、无锁算法等核心概念,同时结合代码示例讲解了如何在多核和多线程环境下避免数据竞争和死锁问题。文章还探讨了内核同步与调度、内存管理、模块开发的关系,并提供了调试与性能优化的方法。适用于Linux内核开发者和系统编程人员深入学习内核并发控制机制。

year5的博客 79

62、内核同步相关知识详解

本文详细介绍了Linux内核开发中的关键主题,包括内核同步机制、内存屏障的使用、各种锁的适用场景与优化建议、内核模块开发流程、内存管理机制、调度策略以及调试与性能分析工具的使用。通过实例和流程图展示了相关开发流程和选择策略,并提供了进一步学习的资源链接和实践建议。

xray4的博客 72

安卓设备安全相关技术

当前安卓设备使用的加密方案是基于Soc中一块可写入的rom实现,称为熔丝,其结果不可逆。 1、secureboot 2、AVB(Android Verify Boot) 3、dm-verity(device mapper) 4、keymaster 5、crypto 6、rpmb(replay private memory block) 7、TEE(Trust execute environment) 8、SELinux .........

weixin_43797328的博客 4838

STC8单片机高精度EV1527解码实战:定时器捕获+状态机抗干扰方案

EV1527是一种广泛应用于433MHz无线遥控的曼彻斯特编码协议,其核心在于对脉宽时序(如同步头、逻辑0/1)的微秒级精确识别。传统外部中断+延时判别方式受MCU中断延迟、编译优化及环境噪声影响,极易导致误码;而基于硬件定时器输入捕获的解码方案,将时间测量交由硬件完成,显著提升时序鲁棒性与温度/电压适应性。该技术适用于STC8等无专用无线解码外设的8位单片机,在智能开关、晾衣架、车库门控等IoT终端中实现低至0.003%的稳定误码率。本文聚焦STC8H系列利用T1定时器下降沿捕获与有限状态机协同解码EV1

weixin_29188997的博客 181

drm 之modes

#define _GNU_SOURCE #include <errno.h> #include <fcntl.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <time.h&...

u012794472的专栏 3718

Design Compiler时序约束实战:如何用set_clock_uncertainty解决时钟抖动问题?

本文深入探讨了在Design Compiler时序约束实战中,如何精准运用set_clock_uncertainty命令解决时钟抖动等非理想性问题。文章详细解析了该命令在时钟树综合前后的不同应用策略、建立时间与保持时间的差异化设置,并对比了其与set_clock_jitter的适用场景,旨在帮助数字IC设计工程师制定稳健的时序约束,为后端物理实现预留合理的安全裕量,从而高效达成时序收敛。

weixin_29196891的博客 300

手把手教你学Simulink——基于Simulink的蚁群算法路径规划仿真建模示例

路径规划是机器人导航系统中的核心任务之一,旨在为机器人找到一条安全且高效的路径,使其能够从起点到达目标点,同时避免与障碍物发生碰撞。在开始之前,需要定义工作环境(例如地图),包括起点、终点以及可能存在的障碍物位置。通过本指南,我们介绍了如何基于Simulink结合MATLAB实现蚁群算法的路径规划仿真建模。

MHD0815的博客 222

GaussDB本地安装手册(主备模式)

如果实例状态是Normal,则安装成功。三节点集中式配置文件。

Sebastien23的博客 2635

在 ABAP Cloud 中玩转 CL_ABAP_PARALLEL 并行处理:从原理到实战

ABAP并行处理框架CL_ABAP_PARALLEL解析 本文系统介绍了SAP ABAP环境中实现并行处理的CL_ABAP_PARALLEL框架。该框架通过封装异步RFC和对话进程,为开发者提供了面向对象的API,显著提升大批量数据处理的效率。 文章首先分析了ABAP环境需要并行处理的原因:传统的串行处理在面对百万级数据时效率低下。然后详细介绍了CL_ABAP_PARALLEL的设计思路,它是一个"小型线程池"式的同步并行框架,适用于批量计算场景。此外,文章重点讲解了控制并行任务的关键参

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 341

YOLOv11改进策略【卷积层】| ICCV-2023 引入Dynamic Snake Convolution动态蛇形卷积,改进C3k2

基于拓扑几何约束的动态蛇卷积用于管状结构分割DSConv(Dynamic Snake Convolution,动态蛇形卷积)模块的设计主要是为了更好地处理管状结构的分割任务,解决传统卷积在处理细管状结构时的不足。

Limiiiing的博客 1629

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

上一篇: 18、Web API与客户端集成开发指南
下一篇: 17、非确定性状态复杂度的定向删除
grass
博客等级 码龄25年 1030粉丝 954原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值