前言
由于在接下来的学习内容中涉及到数据的同步,我们在开发操作系统的时候必须考虑到并发同步问题,要想解决这问题,实际应用中一般采用:原子变量、关中断、信号量、自旋锁。
一、问题引入
下面的代码,描述的是一个线程中的函数和中断处理函数,它们分别对一个全局变量执行加 1 操作:
int a = 0;
void interrupt_handle()
{
a++;
}
void thread_func()
{
a++;
}
梳理一下编译器的翻译过程,通常编译器会把 a++ 语句翻译成这 3 条指令:
1.把 a 加载某个寄存器中。
2.这个寄存器加 1。
3.把这个寄存器写回内存。
那么不难推断,可能导致结果不确定的情况是这样的:thread_func 函数还没运行完第 2 条指令时,中断就来了。
因此,CPU 转而处理中断,也就是开始运行 interrupt_handle 函数,这个函数运行完 a=1,CPU 还会回去继续运行第 3 条指令,此时 a 依然是 1,这显然是错的。
下面来看一下表格,你就明白了。
显然在 t2 时刻发生了中断,导致了 t2 到 t4 运行了 interrupt_handle 函数,t5 时刻 thread_func 又恢复运行,导致 interrupt_handle 函数中 a 的操作丢失,因此出错。
要解决上述场景中的问题,有这样两种思路。
一种是把 a++ 变成原子操作,也就是说要 a++ 这个操作不可分隔,即 a++ 要么不执行,要么一口气执行完;
另一种就是控制中断,比如在执行 a++ 之前关掉中断,执行完了之后打开中断。
二、原子操作
要实现原子操作需要底层操作系统的支持,X86 平台支持很多原子指令,我们只需要直接应用这些指令,比如原子加、原子减,原子读写等,用汇编代码写出对应的原子操作函数就行了。
现代 C 语言已经支持嵌入汇编代码,可以在 C 函数中按照特定的方式嵌入汇编代码了,实现原子操作就方便了,代码如下:
//定义一个原子类型
typedef struct s_ATOMIC{
volatile s32_t a_count; //在变量前加上volatile,是为了禁止编译器优化,使其每次都从内存中加载变量
}atomic_t;
//原子读
static inline s32_t atomic_read(const atomic_t *v)
{
//x86平台取地址处是原子
return (*(volatile u32_t*)&(v)->a_count);
}
//原子写
static inline void atomic_write(atomic_t *v, int i)
{
//x86平台把一个值写入一个地址处也是原子的
v->a_count = i;
}
//原子加上一个整数
static inline void atomic_add(int i, atomic_t *v)
{
__asm__ __volatile__("lock;" "addl %1,%0"
: "+m" (v->a_count)
: "ir" (i));
}
//原子减去一个整数
static inline void atomic_sub(int i, atomic_t *v)
{
__asm__ __volatile__("lock;" "subl %1,%0"