linux内核设计与实现 - 内核同步介绍

本文深入探讨了内核同步的各种方法,包括原子操作、自旋锁、读写自旋锁、信号量、读写信号量、互斥体、完成变量、BKL、顺序锁、禁止抢占和屏障的概念及应用场景,帮助理解内核中如何避免竞争条件和死锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第九章 内核同步介绍

小结:
内核同步方法:

  • 原子操作
  • 自旋锁
  • 读写自旋锁
  • 信号量:和mutex不是一个
  • 读写信号量
  • mutex
  • 完成变量
  • BKL
  • 顺序锁

顺序和屏障

9.1 临界区和竞争条件

临界区:访问和操作共享数据的代码。竞争条件:两个执行线程有可能在同一个临界区中同时执行。
同步:避免并发和防止竞争条件。

9.2 加锁

锁的形式和锁的粒度各不相同,各个锁机制之前的主要区别在于:当锁被其他线程持有时,其他的行为表现。

  1. 造成并发执行的原因
    用户空间:因为会被抢占或重新调度。
    信号处理:异步发生
    中断:任何时刻异步发生
    软中断和tasklet:任何时刻唤醒或调度
    内核抢占
    睡眠及与用户空间的同步:内核执行进程可能睡眠,会唤醒调度程序调度一个新的用户进程
    对称多处理

辨识出真正需要共享的数据和相应的临界区才是真正的挑战。
如:一段内核代码操作某资源时产生系统中断,而该中断的处理函数还要访问这个资源。

中断安全代码:在中断处理程序中能避免并发访问的安全代码
SMP安全代码:在SMP中。。。。。。。。。。。。。。。
抢占安全代码:在内核抢占时。。。。。。。。。。。。。

  1. 要保护什么
    要在一开始设计时就要仔细考虑
    不需要保护的:(1). 执行线程的局部数据,如局部自动变量(包括动态分配的数据结构,其地址仅存在栈上);(2).数据只会被特定的进程访问。

需要保护:大多数内核数据结构需要加锁。记住:给数据而不是给代码加锁。
编写内核代码时,要问自己:
(1). 这个数据是不是全局的?除了当前线程外,其他线程能不能访问它?
(2). 这个数据会不会在进程上下文和中断上下文中共享?是不是要在两个不同的中断处理程序中共享?
(3). 进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
(4). 当前进程是不是会睡眠在某些资源上?如果是,它会让共享数据处于何种状态?
(5). 怎样防止数据失控?
(6). 如果这个函数又在另一个处理器上被调度了将会发生什么?
(7). 如何确保代码远离并发威胁呢?

9.3 死锁

条件
死锁避免:

  • 按顺序加锁:使用嵌套锁时必须保证以相同的顺序获得锁
  • 防止发生饥饿
  • 不要重复请求同一个锁
  • 设计应力求简单 - 越复杂的加锁越有可能死锁。

9.4 争用和扩展性

当锁争用严重时,加锁太粗会降低可扩展性;而锁争用不明显时,加锁过细会加大系统开销,造成浪费。


第10章 内核同步方法

10.1 原子操作

原子操作:不可分割的指令。
内核提供了2组原子操作接口 - 一组对整数进行操作;另一组针对单独的位进行操作。
大多数系统结构会支持原子操作的简单算术指令,或者通过锁内存总线的方式实现。
最常见用户:实现计数器
特性:开销小

  1. 原子整数操作
    atomic_t:
    atomic_t u = ATOMIC_INIT(0);

    atomic_set()
    atomic_add()
    atomic_inc()
    atomic_read()
    atomic_dec_and_test()

原子性:确保指令执行期间不被打断(通过原子操作等)
顺序性:确保多条指令出现,本该的顺序性依然要保持(通过屏障barrier)

  1. 64位原子操作
    atomic64_t

  2. 原子位操作
    set_bit()
    clear_bit()
    test_and_set_bit()

对应的非原子位函数,多了两个下划线
__test_bit()

  1. 内核提供从指定地址开始搜索第一个被设置(或未设置)的位。
    int find_first_bit(unsigned long *addr, unsigned int size)
    int find_first_zero_bit(unsigned long *addr, unsigned int size)

10.2 自旋锁

等待锁时,一直循环-旋转-等待。
场景:适合短时间内轻量级锁

  1. 自旋锁方法
    自旋锁的实现和体系结构相关,代码往往通过汇编实现。

警告:自旋锁是不可递归的!

  • 自旋锁和中断处理程序:
    自旋锁在中断处理程序中使用时,一定要先禁止本地中断然后获取锁,否则可能打断当前持有锁的进程,而导致其他进程不能获取锁。

    内核提供禁止中断同时请求锁的接口:
    DEFINE_SPINLOCK(mr_lock);
    unsigned long flags;
    spin_lock_irqsave(&mr_lock, flags); //保存中断当前状态,并禁止本地中断,然后获取指定的锁

    spin_unlock_irqrestore(&mr_lock, flags); // 对指定的锁解锁,然后让中断恢复

  • 调试自旋锁
    CONFIG_DEBUG_SPINLOCK选项

  • 其他自旋锁的方法
    spin_lock_init():动态创建自旋锁
    spin_try_lock():试图获得特定的自旋锁,如锁已被争用,立即返回非0,不等待自旋锁被释放
    spin_is_locked():检查锁是否被占用

  1. 自旋锁和下半部
    spin_lock_bh(): 获取指定锁,并禁止所有下半部的执行
    (1). 下半部和进程上下文共享数据时,必须对进程上下文中共享数据保护,需要加锁同时禁止下半部。【下半部可抢占进程】
    (2). 中断处理程序和下半部共享数据时,下半部必须获取恰当的锁的同时禁止中断。【中断可抢占下半部】
    (3). 不同tasklet共享数据时,需要加普通自旋锁,这里不需要禁止下半部。【tasklet不会相互抢占】
    (4). 软中断共享时,需要加普通锁,也不需要禁止下半部【软中断之间不会抢占】

10.3 读-写自旋锁

一个或多个任务可以并发持有读者锁,但写者锁只能有一个。
特点:照顾读。
DEFINE_RWLOCK(mr_rwlock);
read_lock(&mr_rwlock);

read_unlock(&mr_rwlock);

write_lock(&mr_rwlock);
write_unlock(&mr_rwlock);

10.4 信号量

  1. 信号量特征:
    (1). 信号量适用于锁会被长时间持有的情况,睡眠、维护等待队列及唤醒的开销很大。
    (2). 只能在进程上下文中获取信号量锁
    (3). 占用信号量的同时不能占用自旋锁。
    (4). 往往在需要和用户空间同步时,你的代码需要睡眠,此时使用信号量是唯一选择。
    (5). 信号量不同于自旋锁,它不会禁止内核抢占,所有持信号量的代码可以被抢占

  2. 计数信号量和二值信号量
    内核使用信号量时基本用到的都是互斥信号量。

  3. 创建和初始化信号量
    信号量的实现和体系结构相关。
    静态:static DECLARE_MUTEX(name)
    动态:sema_init(sem, count); 或 init_MUTEX(sem);

  4. 使用信号量
    down_interruptible():睡眠时可唤醒。TASK_INTERRUPTIBLE
    down() :睡眠时不可唤醒。TASK_UNINTERRUPTIBLE
    down_trylock():试图获得指定信号量,如果被征用,不等待,直接返回非0值
    up()

10.5 读-写信号量

rw_semaphore,区分读写的信号量。所有读写信号量都是互斥信号量,所有读写锁的睡眠都不会被信号打断
静态初始化:static DECLARE_RWSEM(name);
动态初始化:init_rwsem(sem);
down()
down_read_trylock()
down_write_trylock()
downgradge_write():动态地将写锁转换成读锁。

10.6 互斥体

mutex,类似计数为1的信号量,但操作接口更简单,实现更高效,使用限制更强。
静态:DEFINE_MUTEX(name);
动态:mutex_init(&mutex);
锁定:mutex_lock(&mutex);
解锁:mutex_unlock(&mutex);

mutex相比信号量的场景更严格:

  • mutex上锁者必须负责给其解锁,常用方式:在同一上下文中上锁和解锁。
  • 递归地上锁和解锁是不允许的。
  • 当持有一个mutex时,进程不可以退出。
  • mutex不能在中断或下半部使用,即使使用mutex_trylock()也不行

内核配置:CONFIG_DEBUG_MUTEXES

需求建议加锁方式
低开销加锁优先使用自旋锁
短期锁定优先使用自旋锁
长期加锁优先使用互斥体
中断上下文加锁使用自旋锁
持有锁需睡眠使用互斥体

10.7 完成变量

如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量。如子进程执行或退出时,vfork()系统调用使用完成变量唤醒父进程

  • 用法:
    静态创建并初始化:DECLARE_COMPLETION(mr_comp);
    动态创建并初始化:init_completion()

    在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。

10.8 BKL:大内核锁

BKL是一个全局自旋锁,特性:
(1). 持有BKL的任务仍然可以睡眠。因为当任务无法被调度时,锁会被自动丢弃;当任务被调度时,锁会被重新获得。睡眠不会造成任务死锁。
(2). BKL是一种递归锁。
(3). BKL只能在进程上下文中。
(4). BKL在持有时会禁止内核抢占。

  • 用法:
    lock_kernel()
    unlock_kernel()
    kernel_locked():检测是否被持有

10.9 顺序锁

用于读写共享数据。实现这种锁主要依靠一个序列计数器,当有疑义的数据被写入时会得到一个锁,并且序列值会增加。在读取数据之前和之后,序列号都被读取,如果序列号相同,说明没有被写操作打断过。此外,如果读数据是偶数,也表明写没发生过。

  • 用法:
    DEFINE_SEQLOCK(mr_seq_lock);
    write_seqlock(&mr_seq_lock);
    write_sequnlock(…)
    读时区别很大:
    do{
    seq = read_seqbegin(&mr_seq_lock);
    }while(read_seqretry(&mr_seq_lock,seq));

  • 特点:
    (1). 多个读者和少数写者时,seq锁提供轻量级访问
    (2). seq锁对写者更有利。

  • 适用场景:
    (1). 数据存在很多读者,数据写者很少
    (2). 写优先于读
    (3). 数据很简单
    如jiffies

10.10 禁止抢占

内核抢占可以使用自旋锁作为非抢占区域的标记。当然,每个处理器上的数据不要锁保护。
禁止内核抢占:preempt_disable()和preempt_enable()
每个处理器上的数据访问问题:get_cpu()和put_cpu():在返回当前处理器号前首先关闭内核抢占。

10.11 顺序和屏障

屏障:保证顺序要求,指示编译器不要对给定点周围的指令进行重新排序。

  • 内存屏障
    rmb():提供一个读内存屏障,确保跨越rmb()的载入动作不会发生重排。
    wmb():提供一个写内存屏障
    mb():提供读写屏障
    read_barrier_depends():rmb的变种,只针对后续读操作锁依靠的那些载入。保证屏障前的读操作在屏障后的读操作之前完成。【只针对特定的读】

    对应的,宏smp_rmb()、smp_wmb()、smp_mb()、smp_read_barrier_depends()提供了 一个有用的优化。

  • 编译器屏障
    barrier():可以防止编译器跨屏障对load和store进行优化。前面的内存屏障可以实现编译器屏障的功能,但编译器屏障更轻量级,它只防止编译器可能重排指令

内容概要:本文介绍了多种开发者工具及其对开发效率的提升作用。首先,介绍了两款集成开发环境(IDE):IntelliJ IDEA 以其智能代码补全、强大的调试工具和项目管理功能适用于Java开发者;VS Code 则凭借轻量级和多种编程语言的插件支持成为前端开发者的常用工具。其次,提到了基于 GPT-4 的智能代码生成工具 Cursor,它通过对话式编程显著提高了开发效率。接着,阐述了版本控制系统 Git 的重要性,包括记录代码修改、分支管理和协作功能。然后,介绍了 Postman 作为 API 全生命周期管理工具,可创建、测试和文档化 API,缩短前后端联调时间。再者,提到 SonarQube 这款代码质量管理工具,能自动扫描代码并检测潜在的质量问题。还介绍了 Docker 容器化工具,通过定义应用的运行环境和依赖,确保环境一致性。最后,提及了线上诊断工具 Arthas 和性能调优工具 JProfiler,分别用于生产环境排障和性能优化。 适合人群:所有希望提高开发效率的程序员,尤其是有一定开发经验的软件工程师和技术团队。 使用场景及目标:①选择合适的 IDE 提升编码速度和代码质量;②利用 AI 编程助手加快开发进程;③通过 Git 实现高效的版本控制和团队协作;④使用 Postman 管理 API 的全生命周期;⑤借助 SonarQube 提高代码质量;⑥采用 Docker 实现环境一致性;⑦运用 Arthas 和 JProfiler 进行线上诊断和性能调优。 阅读建议:根据个人或团队的需求选择适合的工具,深入理解每种工具的功能特点,并在实际开发中不断实践和优化。
内容概要:本文围绕低轨(LEO)卫星通信系统的星间切换策略展开研究,针对现有研究忽略终端运动影响导致切换失败率高的问题,提出了两种改进策略。第一种是基于预测的多属性无偏好切换策略,通过预测终端位置建立切换有向图,并利用NPGA算法综合服务时长、通信仰角和空闲信道数优化切换路径。第二种是多业务切换策略,根据不同业务需求使用层次分析法设置属性权重,并采用遗传算法筛选切换路径,同时引入多业务切换管理方法保障实时业务。仿真结果显示,这两种策略能有效降低切换失败率和新呼叫阻塞率,均衡卫星负载。 适合人群:从事卫星通信系统研究的科研人员、通信工程领域的研究生及工程师。 使用场景及目标:①研究和优化低轨卫星通信系统中的星间切换策略;②提高卫星通信系统的可靠性和效率;③保障不同类型业务的服务质量(QoS),特别是实时业务的需求。 其他说明:文章不仅详细介绍了两种策略的具体实现方法,还提供了Python代码示例,包括终端位置预测、有向图构建、多目标优化算法以及业务感知的资源分配等关键环节。此外,还设计了完整的仿真测试框架,用于验证所提策略的有效性,并提供了自动化验证脚本和创新点技术验证方案。部署建议方面,推荐使用Docker容器化仿真环境、Redis缓存卫星位置数据、GPU加速遗传算法运算等措施,以提升系统的实时性和计算效率。
内容概要:该论文深入研究了光纤陀螺(FOG)的温度特性及其补偿方法。首先分析了光纤陀螺各主要光学和电子器件的温度特性,通过有限元方法模拟温度场对陀螺的影响,进行了稳态和瞬态热分析。接着提出了高阶多项式算法和RBF神经网络算法两种温度补偿方法,并建立了相应的数学模型。论文还设计了不同温度条件下的实验以验证补偿效果,研究表明结合这两种算法能有效补偿光纤陀螺的温度漂移误差。此外,论文提供了详细的Python代码实现,包括数据预处理、补偿算法实现、有限元热分析模拟以及补偿效果的可视化。 适合人群:具备一定编程基础和物理基础知识的研究人员或工程师,尤其是从事惯性导航系统、光纤传感技术领域工作的人员。 使用场景及目标:①研究光纤陀螺在不同温度条件下的性能变化;②开发和优化温度补偿算法以提高光纤陀螺的精度;③利用提供的代码框架进行实验设计和数据分析,探索更有效的补偿策略。 其他说明:论文不仅提供了理论分析,还有具体的代码实现,有助于读者更好地理解和应用。文中涉及的补偿算法和有限元分析方法可以为其他相关领域的研究提供参考。此外,论文还讨论了温度误差的多物理场耦合机理、静态动态补偿的综合效果以及工程实现中的关键技术瓶颈和解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值