嵌入式C语言关键字volatile以及cache对数据一致性的影响

本文深入探讨了数据一致性问题及其在编程中的表现,特别是编译器优化和缓存对数据一致性的影响。文章详细解释了如何通过使用volatile关键字来解决这些问题,并介绍了在开数据cache情况下,如何通过设置特定内存地址为不使用cache来确保数据一致性。此外,还提供了几种常见场景下的数据一致性处理方法,帮助开发者避免逻辑错误。

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

转自:http://m.blog.youkuaiyun.com/blog/a747lulu747/12423031 cache部分加入自己的一些理解

1、数据一致性是一个重要的问题,它定义了不同的CPU、系统总线所有的master看到的是相同的一片内存。

 

2、因为cache的存在,以及编译器对某些C语言语句的优化,使得CPU对某个内存变量的修改不能立刻更新到内存,或者其他系统的master修改了内存变量,但是CPU仍然使用cache中的值或者寄存器中的值来代表变量,此时就发生了数据一致性的问题:不同的系统总线master对同一个变量看到不同的值(CPU也可以看做是系统总线的master)。


3、先看看编译器优化对数据一致性的影响:

有如下语句:

int i = 0; 

while(1)

{

    i++;

    if(i > DELAY)

      break;

}

 

假设上述代码片段用来实现延时,或者其他功能。此时编译器会将变量i的值读入CPU内部寄存器,初始化为0,在while循环体中,对i++的操作就是对寄存器的操作:

	MOV      R7,#+0
	LDR      R6,=0xFF
	B        ??main_0
??main_0:
	ADD      R7,R7,#+1
	CMP      R7,R6
	BLT      ??main_1

以上是ARM中对应的汇编语言。可以看到编译器使用R7来保存i,R6来保存DELAY(值为0xFF)常量,然后在while循环中,只是对存i变量的寄存器R7加1,并没有对i变量的内存操作。如果其他CPU,或者总线master依赖于变量i来控制一些功能,此时就会出错,因为i的最新值只是存在于CPU寄存器中。

 

针对这样的情况,我们可以使用volatile关键字告诉编译器,对i变量的读写,每次都要老老实实地从内存取,并且修改后,还要马上更新到内存:

</pre><p style="padding-top: 0px; padding-bottom: 0px; margin-top: 10px; margin-bottom: 10px; font-family: Arial; font-size: 16px; line-height: 24px; -webkit-text-size-adjust: none; background-color: rgb(237, 237, 237);"></p><pre code_snippet_id="115567" snippet_file_name="blog_20131217_3_3245740" class="cpp" name="code" style="border: 1px solid rgb(255, 255, 204); background-color: rgb(255, 255, 252); font-family: 'Courier New'; overflow: auto; font-size: 16px; line-height: 24px; -webkit-text-size-adjust: none;">volatile int i = 0; 

while(1)

{

    i++;

    if(i > DELAY)

      break;

}

对应的汇编语言是:

	MOV      R1,#+0
	STR      R1,[SP, #+0]
	LDR      R6,=0xFF
	B        ??main_0
??main_0:
	LDR      R0,[SP, #+0]
	ADD      R0,R0,#+1
	STR      R0,[SP, #+0]
	LDR      R0,[SP, #+0]
	CMP      R0,R6
	BLT      ??main_1

上面的汇编语言中,R6是常量DELAY的值0xFF,而SP是变量i的内存地址。由此可见,每次都是先SP指向的内存中(即i的地址)LDR到R0,R0++,然后再将R0的值更新到SP指向的内存中(即i的内存位置)。判断i是否大于DELAY时,也是先将i的值从SP指向的地址中LDR到R0,然后在和R6(DELAY的值)比较大小。

 

4、再看看cache对数据一致性的影响

如果开启了数据cache,那么CPU对内存的读写都要经过cache缓冲。读就是读cache,写也是写cache。

考虑一下情况:CPUwhile循环退出依赖于一个内存地址的值,并且这个内存地址的值由另外一个外设负责更新。如果开启数据cache,那么CPU总是从cache中读取数据,这时,cache中的数据和内存中的数据出现不一致,程序执行出现逻辑错误。注意,此时CPU是使用LDR访存指令来访问内存,但是仍然没有得到正确的内存数据。即使使用volatile关键字也无济于事,因为volatile是在指令级上影响C语言到汇编语言的关键字,但是CPU在访问内存时,仍然需要经过cache的缓冲。

在开数据cache的情况下,可以将特定的内存地址设置为不使用cache,以确保CPU访问的是内存。具体就是页表项的Cache属性。

个人理解:

cache是夹在CPU和内存之间的一个缓存,当cache开启时,CPU从内存读写数据时会经过cache,并且留下备份,当涉及到有DMA等外设也会从内存读取数据时,容易产生错误,这时候需要手动去invalidate或者flush缓存(cache)。

常见的有三种情况:

第一种:写操作

当我们配置某些参数或者数据需要最终写到外设上时,CPU会先把他们写到cache上,假如有时序要求,cache没有马上刷新,这时候就存在你的内存和cache之间数据不一致的情况,这时候DMA去内存就拿到了错误的数据,正确的做法是在设置参数或者写入数据时,手动的flush一下cache,可以是全部或者几行,具体视情况而定,保证内存和cache之间数据的一致性。

第二种:读操作

当外设写某些数据到我们的内存上,我们需要读入时,数据也会经过cache,尤其是对相同寄存器的多次配置,或有值的变化时,很容易产生cache和内存数据不一致的情况,正确的方法是在读数据之前invalidate cache然后进行读操作,这时候数据不经过cache,保证是正确的数据。

第三种:写/读操作

当有读写混合的操作时,应该invalidate和flush联合起来使用,具体的方式需要视程序的情况而定,cache的一致性问题有时候很隐蔽,通过常规的gdb或者Jlink都是看到的经过cache的值(cache打开的情况下),不容易debug问题所在。无视程序性能的情况下,可以考虑关闭cache


 

当然,这里是一个使用C语言,在UCOSIII嵌入式实时操作系统环境下,针对缓存一致性进行测试的简化版本。由于UCOSIII的具体API可能会有所不同,这里假设有一个简单的共享内存区域以及原子操作支持。 ```c #include "ucosiii.h" #include "semphr.h" // 假设ucosiii有信号量库 #include <string.h> #include <stdio.h> // 假设有一个全局的共享内存结构体 typedef struct SharedMem { int* data; sem_t write_lock; // 锁用于保护数据写入 } SharedMem; void *cache_inconsistency_test(void *arg) { SharedMem *sm = (SharedMem *) arg; int data_size = sizeof(int); volatile int local_data; for (;;) { // 使用锁保护数据读取 sem_wait(sm->write_lock); // 获取写锁 local_data = *sm->data; sem_post(sm->write_lock); // 释放锁 // 进行处理并更新数据 local_data = process_data(local_data); // 写回数据,同样需要获取锁 sem_wait(sm->write_lock); *sm->data = local_data; sem_post(sm->write_lock); } } int main() { SharedMem sm; sm.data = (int*) malloc(sizeof(int)); // 分配共享内存 sem_init(&sm.write_lock, 0, 1); // 初始化写锁 // 创建两个任务,假设task_1和task_2分别对应两个线程 task_tcb_t *task_1_tcb = task_create("Task 1", 10, cache_inconsistency_test, &sm, NULL, 0, TCB_FLAG_STK_CHK | TCB_FLAG_STK_CLR); task_tcb_t *task_2_tcb = task_create("Task 2", 10, cache_inconsistency_test, &sm, NULL, 0, TCB_FLAG_STK_CHK | TCB_FLAG_STK_CLR); // 启动任务 task_start(task_1_tcb); task_start(task_2_tcb); // 等待一段时间观察是否会出现缓存一致性问题 osDelay(5000); // 假设这里的延迟是毫秒级别 // 通过信号量或任务同步机制检查缓存一致性 if (!verify_cache_consistency()) { printf("Cache inconsistency detected.\n"); } else { printf("Cache consistency passed.\n"); } // 清理资源 free(sm.data); sem_destroy(&sm.write_lock); task_delete(task_1_tcb); task_delete(task_2_tcb); return 0; } // 假设这是一个简单的数据处理函数 int process_data(int data) { // ...你的数据处理逻辑... return data + 1; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值