从零手写操作系统之RVOS任务同步和锁实现-07

news2025/7/16 0:05:14

从零手写操作系统之RVOS任务同步和锁实现-07

  • 并发与同步
  • 临界区、锁、死锁
  • 自旋锁
    • 1.0 版本
    • 2.0 版本
      • 原子指令
      • 思路
      • 测试
    • 3.0 版本
      • 测试
  • 小结
  • 其他同步技术


本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。

RVOS是本课程基于RISC-V搭建的简易操作系统名称。

课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md

前置知识:

  • RVOS环境搭建-01
  • RVOS操作系统内存管理简单实现-02
  • RVOS操作系统协作式多任务切换实现-03
  • RISC-V 学习篇之特权架构下的中断异常处理
  • 从零手写操作系统之RVOS外设中断实现-04
  • 从零手写操作系统之RVOS硬件定时器-05
  • 从零手写操作系统之RVOS抢占式多任务实现-06

并发与同步

并发 指多个控制流同时执行,可能存在下面几种情况

  • 多处理器多任务
  • 单处理器多任务
  • 单处理器任务+中断

同步是为了保证在并行执行的环境中,各个控制流可以有效执行而采用的一种编程技术。


临界区、锁、死锁

  • 临界区:在并发的程序执行环境中,所谓临界区(Critical Section)指的是一个会访问共享资源的指令片段,而且当这样的多个指令片段同时访问某个共享资源时可能会引发问题。
    • 例如:一个共享设备或者一块共享存储内存

在并发环境下为了有效控制临界区的执行(同步),我们要做的是当有一个控制流进入临界区时,其他相关控制流必须 等待。

锁是一种最常见的用来实现同步的技术:

  • 不可睡眠的锁
  • 可睡眠的锁

可睡眠与不可睡眠的主要区别在于指令流获取不到锁时,是进入等待挂起状态,还是不断轮询锁查看锁是否被释放,也就是我们常说的自旋锁

在这里插入图片描述

死锁(Deadlock)问题:

  • 当控制流执行路径中会涉及多个锁,并且这些控制流执行路径获取(aquire)锁的顺序不同时就可能会发生死锁问题。
  • 如何解决死锁:
    • 调整获取(aquire)锁的顺序,譬如保持一致。
    • 尽可能防止任务在持有一把锁的同时申请其它的锁。
    • 尽可能少用锁,尽可能少并发。
      在这里插入图片描述

自旋锁

在这里插入图片描述

1.0 版本

在这里插入图片描述
这段逻辑很简单,关键在于spin_lock函数中,获取不到锁,就死循环尝试。

上面这段代码存在什么问题?

  • 让我们把spin_lock函数翻译为汇编代码看看
    在这里插入图片描述

可以看到,问题在于我们读取锁状态和上锁的操作并非原子性的,所以在并发环境下,会产生不一致性问题。


2.0 版本

1.0版本的问题是,由于读取锁和上锁操作非原子性,所以在并发环境下,可能会存在多个指令流同时看见锁处于空闲状态,随后都重复上锁,也就是一把锁同时被多个任务持有。

为了解决这个问题,我们需要引入原子性指令,该指令由硬件提供支持。

原子指令

RV32A 有两种类型的原子操作:

  • 内存原子操作(AMO)
  • 加载保留/条件存储(load reserved / store conditional)

图 6.1 是 RV32A 扩展指令集的示意图,图 6.2 列出了它们的操作码和指令格式。
在这里插入图片描述
AMO 指令对内存中的操作数执行一个原子操作,并将目标寄存器设置为操作前的内存值。原子表示内存读写之间的过程不会被打断,内存值也不会被其它处理器修改。

AMO 和 LR/SC 指令要求内存地址对齐,因为保证跨 cache 行的原子读写的难度很大。

加载保留和条件存储保证了它们两条指令之间的操作的原子性。

  • 加载保留读取一个内存字,存入目标寄存器中,并留下这个字的保留记录。
  • 而如果条件存储的目标地址上存在保留记录,它就把字存入这个地址。
  • 如果存入成功,它向目标寄存器中写入 0;否则写入一个非0 的错误代码。

为什么 RV32A 要提供两种原子操作呢?

  • 因为实际中存在两种不同的使用场景。

编程语言的开发者会假定体系结构提供了原子的比较-交换(compare-and-swap)操作:

  • 比较一个寄存器中的值和另一个寄存器中的内存地址指向的值,如果它们相等,将第三个寄
    存器中的值和内存中的值进行交换。这是一条通用的同步原语,其它的同步操作可以以它为基础来完成。
  • 尽管将这样一条指令加入 ISA 看起来十分有必要,它在一条指令中却需要 3 个源寄存器和 1 个目标寄存器。源操作数从两个增加到三个,会使得整数数据通路、控制逻辑和指令格式都变得复杂许多。
  • 不过,加载保留和条件存储只需要两个源寄存器,用它们可以实现原子的比较交换。

使用加载保留和条件存储两个寄存器实现原子的比较交换案例:
在这里插入图片描述

a3存放内存中的值,a1存放当前内存中期望的值,a2希望设置到内存中的新值

  • 首先,执行加载保留指令LR.W, 从地址寄存器a0指定的内存位置加载一个32位的字(word)到寄存器a3,并在加载期间保留该内存位置的锁定状态。(将当前内存中的值加载到a3保存)

这条指令执行后,如果成功加载了内存数据并保留了锁定状态,则a3寄存器将存储加载的值。如果加载失败,a3寄存器的内容将保持不变。加载保留指令常用于原子操作和同步原语的实现,以确保在多线程或多核环境下的数据一致性。

  • 如果加载成功,下面比较寄存器a3和寄存器a1的值,如果不相等,则进行一个相对偏移为80的条件跳转。 (也就是比较内存中的值和我们传入的值是否相等)
  • 然后,将寄存器a2的值存储到寄存器a0指定的内存位置中,但仅当寄存器a3的值等于内存位置中的值时才执行存储操作。 (确保内存中的值没有变化)
  • 将结果写入a3寄存器中,0表示成功,非0表示失败,如果写入失败,跳回到0地址处重试

使用amo原子指令,实现临界区保护案例:

a0可以看做是一个锁指针 lk->locked,其指向的内存中存放locked值

在这里插入图片描述

  • 初始化锁: 将立即数1加载到寄存器t0中
  • 尝试获取锁: 将内存位置a0的值加载到寄存器t1中,然后将寄存器t0的值存储到内存位置a0中 (不由分说,先上锁,然后把锁的原始值返回,由t1寄存器保存)

注意:

  1. 如果锁已经被任务A加上了,那么任务B此时通过amoswap交换拿到的锁值为1,此时我们设置锁为1是没有影响的,因为锁的值本来就为1
  2. 如果锁还没被加上,那么任务B此时通过amswap交换拿到的锁值为0,此时我们设置锁为1相当于任务A抢到了锁
  • 获取锁失败,则进行重试: 如果t1不为0,则跳到地址4处重试 (如果当前内存中locked值不为0,说明锁已经被其他任务加上了,那么重新尝试获取锁)

注意:

  1. 对于上面第一种情况而言,由于t1寄存器保存的锁值不为0,说明抢锁失败,需要重试
  2. 对于上面第二种情况而言,由于t1寄存器保存的锁值为0,说抢锁成功,继续往下执行临界区代码
  • 释放锁: 将内存位置a0处的值设置为0 (与x0寄存器做交换,等同于置零,向x0写入是没有意义的,最终表达意思即为: 释放锁)

另外还提供 AMO 指令的原因是,它们在多处理器系统中拥有比加载保留/条件存储更好的可扩展性,例如可以用它们来实现高效的归约。

AMO 指令在于 I/O 设备通信时也很有用,可以实现总线事务的原子读写。这种原子性可以简化设备驱动,并提高 I/O 性能。


思路

经过上面原子指令的介绍,想必各位也知道如何对1.0版本的漏洞进行改进了,下面给出代码:
在这里插入图片描述

typedef struct{
	int locked;
} spinlock;
int spin_lock(spinlock *lk)
{
	while(__sync_lock_test_and_set(&lk->locked,1)!=0);
	return 0;
}

int spin_unlock(spinlock *lk)
{
	lk->locked=0;
	return 0;
}

_sync_lock_test_and_set是c编程库提供的一个原子操作函数,常用于多线程编程中实现互斥锁(mutex)的功能,这个函数在不同的编程语言和平台上可能会有不同的实现方式,但它的目的是原子地测试并设置一个锁的状态。

右边是该函数在RISC-V架构下的汇编实现,其利用了我们上面介绍的amoswap原子交换指令:

  • 首先,使用a4寄存器存放指向lk->locked值的指针
  • 然后将a5寄存器设置为1
  • 原子交换a5寄存器和内存位置a4处的值,也就是locked的值
  • 将a5寄存器的值赋值给a3
  • 判断a3寄存器是否不为0,如果满足条件,就跳到loop处重新执行该流程

上面的流程简单而言就是:

  • 获取锁的当前值,同时将锁进行锁定
  • 通过锁的当前值,判断锁是否已经被抢占,如果被抢占了,那么上面的锁定其实没有影响,则进入重试阶段
  • 如果还没有被抢占,则上面的锁定生效
  • 执行临界区代码
  • 释放锁

测试

在user.c文件中,我们针对user_task0进行代码改造,来测试对临界区加锁运行的效果:

我们期望的是通过加锁,来确保任务0中五个语句的输出总体来看是连续的

#include "os.h"

#define DELAY 4000

#define USE_LOCK

spinlock lk;

void user_task0(void)
{
	uart_puts("Task 0: Created!\n");
	while (1) {
#ifdef USE_LOCK
		spin_lock(&lk);
#endif
		uart_puts("Task 0: Begin ... \n");
		for (int i = 0; i < 5; i++) {
			uart_puts("Task 0: Running... \n");
			task_delay(DELAY);
		}
		uart_puts("Task 0: End ... \n");
#ifdef USE_LOCK
		spin_unlock(&lk);
		task_yield();
#endif
	}
}

void user_task1(void)
{
	uart_puts("Task 1: Created!\n");
	while (1) {
#ifdef USE_LOCK
		spin_lock(&lk);
#endif
		uart_puts("Task 1: Begin ... \n");
		for (int i = 0; i < 5; i++) {
			uart_puts("Task 1: Running... \n");
			task_delay(DELAY);
		}
		uart_puts("Task 1: End ... \n");
#ifdef USE_LOCK
		spin_unlock(&lk);
		task_yield();
#endif		
	}
}

/* NOTICE: DON'T LOOP INFINITELY IN main() */
void os_main(void)
{
	task_create(user_task0);
	task_create(user_task1);
}

只有在任务0输出完五句后,任务1才会开始输出:
在这里插入图片描述


3.0 版本

2.0 版本引入的原子指令实现思路很好,但是对于本门课程而言,由于只使用到了单个hart, 并且导致读取锁和上锁操作非原子性的根本原因在于中断打断我们任务的执行。

所以,在单hart架构下,最简单的保护临界区代码并发安全的方法就是关中断。

#include "os.h"

int spin_lock()
{ 
    //关闭全局中断
	w_mstatus(r_mstatus() & ~MSTATUS_MIE);
	return 0;
}

int spin_unlock()
{
   //开启全局中断
	w_mstatus(r_mstatus() | MSTATUS_MIE);
	return 0;
}

测试

在user.c文件中,我们针对user_task0进行代码改造,来测试对临界区加锁和不加锁的两种运行效果:

我们期望的是通过加锁,来确保任务0中五个语句的输出总体来看是连续的

  • 首先测试不加锁
//将USE_LOCK宏定义注释掉
//#define USE_LOCK

void user_task0(void)
{
	uart_puts("Task 0: Created!\n");
	while (1) {
//如果不存在USER_LOCK宏定义,那么下面的加锁和解锁代码都会在预处理环节被丢弃	
#ifdef USE_LOCK
		spin_lock();
#endif
		uart_puts("Task 0: Begin ... \n");
		for (int i = 0; i < 5; i++) {
			uart_puts("Task 0: Running... \n");
			task_delay(DELAY);
		}
		uart_puts("Task 0: End ... \n");
#ifdef USE_LOCK
		spin_unlock();
#endif
	}
}

void user_task1(void)
{
	uart_puts("Task 1: Created!\n");
	while (1) {
		uart_puts("Task 1: Begin ... \n");
		for (int i = 0; i < 5; i++) {
			uart_puts("Task 1: Running... \n");
			task_delay(DELAY);
		}
		uart_puts("Task 1: End ... \n");
	}
}

从输出结果可以观察到,任务0和任务1此时都没加锁,临界区代码总是会被中断打断:
在这里插入图片描述

  • 测试加锁
#define USE_LOCK

void user_task0(void)
{
	uart_puts("Task 0: Created!\n");
	while (1) {
#ifdef USE_LOCK
		spin_lock();
#endif
		uart_puts("Task 0: Begin ... \n");
		for (int i = 0; i < 5; i++) {
			uart_puts("Task 0: Running... \n");
			task_delay(DELAY);
		}
		uart_puts("Task 0: End ... \n");
#ifdef USE_LOCK
		spin_unlock();
#endif
	}
}

void user_task1(void)
{
	uart_puts("Task 1: Created!\n");
	while (1) {
		uart_puts("Task 1: Begin ... \n");
		for (int i = 0; i < 5; i++) {
			uart_puts("Task 1: Running... \n");
			task_delay(DELAY);
		}
		uart_puts("Task 1: End ... \n");
	}
}

由于任务0的临界区代码都加上了锁,所以输出是连续的,没有受到中断影响:
在这里插入图片描述


小结

自旋锁的使用:

• 自旋锁可以防止多个任务同时进入临界区(critical region)

• 在自旋锁保护的临界区中不能执行长时间的操作

• 在自旋锁保护的临界区中不能主动放弃CPU


其他同步技术

在这里插入图片描述

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

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

相关文章

练习:程序切片

练习&#xff1a;程序切片 1 简介 注&#xff1a;问题3提到了conditioned slicing。 我们没有给出计算条件切片的算法&#xff0c;但你不应该要求这样的算法。 2 问题 1 对于以下每个代码片段&#xff0c;绘制程序依赖图&#xff08;没有定义顺序边&#xff09;。 基于这些&am…

什么是 Vue.js 中的 v-if 和 v-show 指令?

什么是 Vue.js 中的 v-if 和 v-show 指令&#xff1f; Vue.js 是一种用于构建交互式用户界面的渐进式框架。它采用了响应式的数据绑定机制和组件化的开发模式&#xff0c;让开发者能够更加高效地构建复杂的 Web 应用。在 Vue.js 中&#xff0c;v-if 和 v-show 是两个常用的指令…

STM32开发——电动车报警装置

1.项目简介 1.1项目需求 点击遥控器 A 按键&#xff0c;系统进入警戒模式&#xff0c;一旦检测到震动&#xff08;小偷偷车&#xff09;&#xff0c;则喇叭发出声响报警&#xff0c; 吓退小偷。 点击遥控器 B 按键&#xff0c;系统退出警戒模式&#xff0c;再怎么摇晃系统都不…

通过项目驱动的学习方法快速掌握Java编程

摘要 Java作为一种广泛应用于软件开发领域的编程语言&#xff0c;对于零基础的学习者来说&#xff0c;学习Java编程可能存在一定的难度。本文将介绍如何通过项目驱动的学习方法&#xff0c;帮助零起点的学习者快速掌握Java编程。通过以项目为核心的学习路径、结合实践和理论的…

【Pytorch】自定义模型、自定义损失函数及模型删除修改层的常用操作

目录 问题一&#xff1a;更改模型最后一层&#xff0c;删除最后一层问题二&#xff1a;自定义模型及参数冻结问题三&#xff1a;自定义损失函数及广播机制 问题1&#xff1a;更改模型最后一层&#xff0c;删除最后一层&#xff0c;添加层。 改变模型最后一层 # Load the mo…

Jmter自动化

一、接口测试流程 1、拿到api接口文档&#xff0c;熟悉接口业务。 2、编写测试用例。 正例&#xff1a;正常参数&#xff0c;是否接口正常 反例&#xff1a;鉴权异常情况、参数异常、兼容性、黑名单、调用次数异常 3、使用接口测试用具&#xff08;Jmeter&#xff09; 4、…

chatgpt赋能python:Python安装gym:入门指南

Python安装gym: 入门指南 如果您是一位正在学习强化学习的学生&#xff0c;或者是一位研究者、开发人员&#xff0c;那么您一定会对OpenAI出品的gym库感兴趣。该库为编写和比较强化学习算法提供了一组标准环境。但是&#xff0c;在使用gym之前&#xff0c;您需要将其安装到您的…

ThinkPad无法进系统的解决方案(实测)

ThinkPad无法进系统如何解决&#xff1f; 不一样的笔记本进到BIOS的方法是不太一样的&#xff0c;下面就和大伙儿具体解读电脑上进到thinkpad的bios设置启动项的方法吧。 在开机或重启的Lenovo画面自检处&#xff0c;快速、连续多次按键盘的“F1”按键&#xff0c;即可进入BI…

基于html+css的图展示112

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

PHP实现文件上传

上传文件的必备三个条件&#xff1a; 1、上传到后台的文件 2、method "post";&#xff08;不可以为get方法&#xff09; 3、enctype "multipart/form-data";&#xff08;注意哦&#xff0c;是data&#xff0c;不是date&#xff09; 三者缺一不可 后台…

抛弃传统网络?SDN协议、标准、接口对比分析!

概要&#xff1a; 随着网络规模的不断扩大和复杂性的增加&#xff0c;传统的网络架构已经难以满足日益增长的网络需求。SDN&#xff08;Software Defined Networking&#xff09;技术的出现&#xff0c;为网络的管理和控制带来了革命性的变化。SDN的核心思想是将网络的控制和管…

chatgpt赋能python:Python如何访问文件

Python如何访问文件 Python是一种优秀的编程语言&#xff0c;被广泛应用于各种领域&#xff0c;包括文件处理。在Python中&#xff0c;我们可以使用内置的文件处理功能访问文件。 什么是文件&#xff1f; 文件是计算机系统中的一种数据存储形式。它们可以包含任何类型的信息…

u盘视频丢失怎么找回?居然还得靠它

u盘视频丢失怎么找回&#xff1f;U盘作为一款常用的存储数据的工具&#xff0c;因为其自身的小巧便携&#xff0c;方便我们随身携带&#xff0c;深受广大用户的喜爱。在使用U盘的过程中&#xff0c;我们也会遇到一些文件丢失的麻烦&#xff0c;比如误删除里面的视频文件。当遇到…

浅谈 Tarjan 算法

在了解 Tarjan 算法之前&#xff0c;我们先来了解 dfs 搜索树。 1 dfs 生成树 定义&#xff1a; dfs 遍历整张图&#xff0c;按照 dfs 序构成一棵树。 1.1 有向图的 dfs 生成树 有向图的 dfs 生成树包括四种边&#xff1a; 树边&#xff08;tree edge&#xff09;&#xff…

CDC是什么?有没有合适的技术方案?

CDC 是 Change Data Capture(变更数据获取)的简称。核心思想是&#xff0c;监测并捕获数据库的 变动&#xff08;包括数据或数据表的插入、更新以及删除等&#xff09;&#xff0c;将这些变更按发生的顺序完整记录下 来&#xff0c;写入到消息中间件中以供其他服务进行订阅及…

阿里、百度、值得买齐发声,电商的“AIGC式”进化

配图来自Canva可画 一年一度618要来了&#xff0c;和往年一样折扣力度、明星直播等话题被炒得火热&#xff0c;不同的是今年618的科技属性更强。 究其原因&#xff0c;过去半年AIGC技术被电商平台应用到实际运营中&#xff0c;“AIGC选品”、“虚拟货场”、“智能客服”成为电商…

《MySQL(六):基础篇- 事务》

文章目录 6. 事务6.1 事务简介6.2 事务操作6.2.1 未控制事务6.2.2 控制事务一6.2.3 控制事务二 6.3 事务四大特性6.4 并发事务问题6.5 事务隔离级别 6. 事务 6.1 事务简介 事务 是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff0c;事务会把所有的操作作为一…

【机器学习】神经网络入门

神经网络 非线性假设 如果对于下图使用Logistics回归算法&#xff0c;如果只有x1和x2两个特征的时候&#xff0c;Logistics回归还是可以较好地处理的。它可以将x1和x2包含到多项式中 但是有很多问题所具有的特征远不止两个&#xff0c;甚至是上万个&#xff0c;如果我们想要…

MySQL数据库给表添加索引

说明&#xff1a;当数据库中的记录数过多时&#xff0c;查询速度会显著变慢。此时可以给表创建索引&#xff0c;提高查询速度。 一、创建索引前 我现在有一张表&#xff0c;有1000万条记录&#xff0c;根据username值&#xff0c;查询一条记录&#xff0c;测试下查询时间&…

赛宁网安助力智能网联汽车发展 | “饶派杯”XCTF车联网安全挑战赛圆满收官

​​ 2023年5月31日&#xff0c;“饶派杯”XCTF车联网安全挑战赛在江西省上饶市圆满落幕。本次大赛特邀国内21支精英战队参与比拼&#xff0c;参赛选手覆盖全国知名高校、自动驾驶汽车和科研院所等车联网安全人才。最终&#xff0c;经过9个小时激烈角逐&#xff0c;来自南京邮电…