锁:并发操作中,解决数据同步的四种方法

非预期结果的全局变量

int a = 0;
/* 中断处理程序 */
void interrupt_handle() {
    
    
	a++;
}
/* 线程处理函数 */
void thread_func() {
    
    
	a++;
}

对于一个a++(即a = a + 1)操作,CPU一般是这么做的

  • 把a从内存中加载到某个寄存器(mov eax, ebx)
  • 这个寄存器加一(inc eax)
  • 把这个寄存器写回内存(mov ebx, eax)

当还没运行完第二条指令的时候中断就来了,CPU转而去处理中断,即interrupt_handle函数,此时a=1,CPU继续回去执行第3条指令,此时a=1,这显然是错的。

原子操作 拿下单体变量

要解决上述场景中的问题,有两种方法,一种是把a++变成原子操作,原子操作是不可分隔的,要么不执行a++,要么一口气执行完;另一种是控制中断,比如关中断,不允许中断发生,执行完了之后再打开中断。

x86平台支持许多原子操作,所以此时我们得写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"
                     : "+m" (v->a_count)
                     : "ir" (i));
}
//原子加1
static inline void atomic_inc(atomic_t *v)
{
    
    
        __asm__ __volatile__("lock;" "incl %0"
                       : "+m" (v->a_count));
}
//原子减1
static inline void atomic_dec(atomic_t *v)
{
    
    
       __asm__ __volatile__("lock;" "decl %0"
                     : "+m" (v->a_count));
}

其中加上lock前缀的addl,sbl,incl,decl指令都是原子操作,lock前缀表示锁定总线

注意!原子操作只适合单体变量,操作系统的数据结果可能有几百字节大小,其中包含不同的数据类型,此时就要用中断!

中断控制 搞定复杂变量

开中断指令sti和关中断指令cli都要特权级为0的时候才能执行,主要是对标志寄存器eflags的IF位进行设置和清除

有一点要注意开关中断是完全取决于IF位的,sti和cli也是对IF位进行修改

//关闭中断
void hal_cli()
{
    
    
    __asm__ __volatile__("cli": : :"memory");
}
//开启中断
void hal_sti()
{
    
    
    __asm__ __volatile__("sti": : :"memory");
}
//使用场景
void foo()
{
    
    
    hal_cli();
    //操作数据……
    hal_sti();
}
void bar()
{
    
    
    hal_cli();
    //操作数据……
    hal_sti();
}
void foo()
{
    
    
    hal_cli();
    //操作数据第一步……
    hal_sti();
}
void bar()
{
    
    
    hal_cli();
    foo();
    //操作数据第二步……
    hal_sti();
}

但是中断是不允许嵌套使用的,因此此时会产生bug,我们可以这么改,在关中断之前把eflags寄存器的值的入栈,在开中断之前恢复eflags寄存器的值即可。

typedef u32_t cpuflg_t;
static inline void hal_save_flags_cli(cpuflg_t* flags)
{
    
    
     __asm__ __volatile__(
            "pushfl \t\n" //把eflags寄存器压入当前栈顶
            "cli    \t\n" //关闭中断
            "popl %0 \t\n"//把当前栈顶弹出到eflags为地址的内存中        
            : "=m"(*flags)
            :
            : "memory"
          );
}
static inline void hal_restore_flags_sti(cpuflg_t* flags)
{
    
    
    __asm__ __volatile__(
              "pushl %0 \t\n"//把flags为地址处的值寄存器压入当前栈顶
              "popfl \t\n"   //把当前栈顶弹出到eflags寄存器中
              :
              : "m"(*flags)
              : "memory"
              );
}
自旋锁 协调多核心CPU

在多核CPU中,又是另外一种情况。中断控制只能控制本地CPU,无法控制其他CPU,于是自旋锁登场了。

自旋锁的原理
读取锁变量,判断其值是否已经加锁,如果未加锁则执行加锁,然后返回,表示加锁成功;
如果已经锁了,就要返回第一步继续执行后续步骤

这个算法的一个必要条件是:必须保证读取锁变量和判断并加锁的操作是原子执行

x86有一个原子交换指令xchg,他可以让寄存器里的一个值跟内存可见中的一个值做交换

//自旋锁结构
typedef struct
{
    
    
     volatile u32_t lock;//volatile可以防止编译器优化,保证其它代码始终从内存加载lock变量的值 
} spinlock_t;
//锁初始化函数
static inline void x86_spin_lock_init(spinlock_t * lock)
{
    
    
     lock->lock = 0;//锁值初始化为0是未加锁状态
}
//加锁函数
static inline void x86_spin_lock(spinlock_t * lock)
{
    
    
    __asm__ __volatile__ (
    "1: \n"
    "lock; xchg  %0, %1 \n"//把值为1的寄存器和lock内存中的值进行交换
    "cmpl   $0, %0 \n" //用0和交换回来的值进行比较
    "jnz    2f \n"  //不等于0则跳转后面2标号处运行
    "jmp 3f \n"     //若等于0则跳转后面3标号处返回
    "2:         \n" 
    "cmpl   $0, %1  \n"//用0和lock内存中的值进行比较
    "jne    2b      \n"//若不等于0则跳转到前面2标号处运行继续比较  
    "jmp    1b      \n"//若等于0则跳转到前面1标号处运行,交换并加锁
    "3:  \n"     :
    : "r"(1), "m"(*lock));
}
//解锁函数
static inline void x86_spin_unlock(spinlock_t * lock)
{
    
    
    __asm__ __volatile__(
    "movl   $0, %0\n"//解锁把lock内存中的值设为0就行
    :
    : "m"(*lock));
}
信号量 CPU时间管理大师

无论是原子操作,还是自旋锁,都不适合长时间等待的情况,因为有很多资源有一定的时间性,你想去获取他,他不会立即返回给你。

下面来看看另一种同步机制,既能对资源数据进行保护,又能在资源无法满足的情况下,让CPU可以执行其他任务

信号量无非就是三个关键词:

  • 等待(数据被锁住了)
  • 互斥(数据释放,各个CPU的进程进行抢夺)
  • 唤醒(重新激活等待的代码执行流)

获取信号量

  • 首先对用于保护信号量自身的自旋锁 sem_lock 进行加锁。(这里加锁可能有人觉得是非必要的,但是这个加锁是原子操作,可以防止在一个进程获取信号量的时候被其他进程给中断)
  • 对信号值 sem_count 执行“减 1”操作,并检查其值是否小于 0。
  • 上步中检查 sem_count 如果小于 0,就让进程进入等待状态并且将其挂入 sem_waitlst 中,然后调度其它进程运行。否则表示获取信号量成功。当然最后别忘了对自旋锁 sem_lock 进行解锁。

代码执行流开始执行相关操作,例如读取键盘缓冲区。

释放信号量

Guess you like

Origin blog.csdn.net/qq_48322523/article/details/120815487