非预期结果的全局变量
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 进行解锁。
代码执行流开始执行相关操作,例如读取键盘缓冲区。
释放信号量