Java与C语言中的锁

Java与C语言中的锁

C

嵌入式汇编的语法格式是:
asm(code
: output operand list
: input operand list
: clobber list)

__asm__是GCC关键字asm的宏定义

寄存器其添加%,例如%0,就是0号寄存器

b,w,l分别表示字节,字,双字

output operand list 和 input operand list是c代码和嵌入式汇编代码的接口,clobber list描述了汇编代码对寄存器的修改情况

volatile关键字

易变性

被volatile修饰的变量被认为是易变的,CPU在读取该类型的变量时不能直接使用寄存器中的值,而是需要重新从主内存中读取。在对该类型的变量做出修改后,也需要立即同步会主内存。

其原理是在编译后的汇编代码中,会多出两条内存读写的指令。

不可优化性

被volatile修饰的变量,在编译阶段不会被编译器优化掉。例如替换成常量等。

顺序性

被volatile修饰的变量,在编译阶段保证不会被执行指令重排序。
指令重排序的含义是指,在保证函数输出不变的前提下,编译器会调整指令的前后顺序,已达到最优的执行速度。

这里的顺序性是多个volatile类型的变量之间保证的,在一个普通变量和一个volatile变量之间的编译顺序是不被保证的。

原子操作

原子操作就是保证指令以原子的方式执行(执行过程不被打断)。
那么操作系统是怎么保证原子操作的呢?

  1. 大部分原子操作的实现就是将变量的读取和修改的行为包含在一个单步执行中,更简单的说就是CPU提供的指令集中支持单指令的变量修改。
  2. 如果不支持单独修改的话,就为单步执行提供了锁内存总线的指令。

内核提供了两组原子操作接口— 一组针对整数操作,另一组针对单独的位操作。

整数操作

atomic_t结构体定义如下:

// goldfish/include/linux/types.h

typedef struct {
	int counter;
} atomic_t;

atomic_t使用很简单:

atomic_t v;//声明
atomic_t u = ATOMIC_INIT(0);//定义u,并初始化为0

atomic_set(&v, 4);//设置v的值为4
atomic_add(2, &v);//v=v+2=6
atomic_inc(&v);//v=v+1=7

int ret = atomic_read(&v);//转换为整型

int atomic_dec_and_test(atomic_t *v);//执行v减一并检查结果是否为0。如果结果为0就返回真,否则返回假

原子操作的实现依赖不同架构的指令集,在x86上atomic_add的实现为:

//通过内联汇编指令实现
#define LOCK_PREFIX "\n\tlock; "
static __always_inline void atomic_add(int i, atomic_t *v)
{
	asm volatile(LOCK_PREFIX "addl %1,%0"
		     : "+m" (v->counter)
		     : "ir" (i));
}

在多处理器环境下,通过再addl指令前添加lock指令,锁住对应内存,防止在多处理系统或多线程竞争的环境下互斥使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

位操作

原子位操作是对普通指针进行的操作,不想原子整型对应atomic_t,这里没有特殊的数据类型。

unsigned long word = 0;

set_bit(0, &word);//第0位被设置为1
set_bit(1, &word);//第1位被设置为1
printk("%ul\n", word);//因为第0位和第1位都是1,所以打印出3

clear_bit(1, &word);//清空第1位,即变成0
change_bit(0, &word);//反转第0位,及变为0

//设置第0位,并返回设置之前的值
if(test_and_set_bit(0, &word)) {
    //因为之前第0位为0,所以if条件永远为假
}

自旋锁 pin lock

自旋锁,顾名思义,当获取不到锁时,会一直在原地打转,不断尝试去获得锁,直到成功。

自旋锁的使用方法:

//定义一个自旋锁
spinlock_t lock;
spin_lock_init(&lock);

//获取自旋锁,保护临界区
spin_lock(&lock);
...
//释放锁
spin_unlock(&lock);

自旋锁结构体定义如下:

// goldfish/include/linux/spinlock_types.h

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;
	};
} spinlock_t;

typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
} raw_spinlock_t;

typedef struct {
	volatile unsigned int lock;
} arch_spinlock_t;

获取自旋锁spin_lock函数的实现如下:

// goldfish/include/linux/spinlock.h

static __always_inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}

经过一系列调用最终调用函数为:

// goldfish/include/linux/spinlock_api_smp.h

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
  // 禁止CPU抢占,同时添加内存栅栏,防止指令重排序
  //CPU抢占控制由thread_info结构体中的preempt_count整型变量控制,0可抢占,大于0不可抢占
	preempt_disable();
	//debug时执行
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	//真正的加锁操作do_raw_spin_lock
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

//goldfish/include/linux/spinlock.h
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
	__acquire(lock);
	//该函数实现要区分不同的CPU架构
	arch_spin_lock(&lock->raw_lock);
}

在ia64架构上的实现如下:

//arch/ia64/include/asm/spinlock.h
static __always_inline void arch_spin_lock(arch_spinlock_t *lock)
{
	__ticket_spin_lock(lock);
}

static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)
{
	int	*p = (int *)&lock->lock, ticket, serve;

	ticket = ia64_fetchadd(1, p, acq);

	if (!(((ticket >> TICKET_SHIFT) ^ ticket) & TICKET_MASK))
		return;

	ia64_invala();

	for (;;) {
		asm volatile ("ld4.c.nc %0=[%1]" : "=r"(serve) : "r"(p) : "memory");

		if (!(((serve >> TICKET_SHIFT) ^ ticket) & TICKET_MASK))
			return;
		cpu_relax();
	}
}

arm64上的实现为:

//arch/arm64/include/asm/spinlock_types.h
typedef struct {
#ifdef __AARCH64EB__     /* 大端字节序(高位存放在低地址) */
     u16 next;
     u16 owner;
#else                    /* 小端字节序(低位存放在低地址) */
     u16 owner;
     u16 next;
#endif
} __aligned(4) arch_spinlock_t;

//arch/arm64/include/asm/spinlock.h
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned int tmp;
	arch_spinlock_t lockval, newval;

	asm volatile(
	/* Atomically increment the next ticket. */
	ARM64_LSE_ATOMIC_INSN(
	/* prefetch memory,将参数*lock代表的32位数据写到3号寄存器中 */
"	prfm	pstl1strm, %3\n"
/* 独占加载3号寄存器,到w0寄存器中*/
"1:	ldaxr	%w0, %3\n" 
/* 将w0寄存器的值(next值)加上(1 << 16)后存储到w1存储器*/
"	add	%w1, %w0, %w5\n"
/* 尝试将加1后的值保存到为3号寄存器中,如果写入失败,向w2写入非0,如果写入成功向w2写入0。*/
"	stxr	%w2, %w1, %3\n"
/*compare and branch on Non-Zero,判断是w2是不是非0,如果是非0的则跳转到1号执行分支,直到加1成功。*/
"	cbnz	%w2, 1b\n",
/* 将w5中的数据写到w2寄存器中 */
"	mov	%w2, %w5\n"
/* 0号寄存器与3号寄存器的原子相加存到2号寄存器中*/
"	ldadda	%w2, %w0, %3\n"
	__nops(3)
	)

	/* 判断是否能获取到锁 */
"	eor	%w1, %w0, %w0, ror #16\n"
/*判断w1号寄存器是不是0,如果是0,则跳转到3号执行分支处*/
"	cbz	%w1, 3f\n"
	/*
	 * 到这里说明没有拿到锁,sevl(send local event)发送一个本地事件,避免错过
	 * 其他处理器释放自旋锁时发出的事件
	 */
"	sevl\n"
/* wfe(wait for event) 使处理器进入低功耗状态,等待事件*/
"2:	wfe\n"
/* 判断是否拿到锁,没有的话重复执行2号逻辑分支*/
"	ldaxrh	%w2, %4\n"
"	eor	%w1, %w2, %w0, lsr #16\n"
"	cbnz	%w1, 2b\n"
	/* We got the lock. Critical section starts here. */
"3:"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
	: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
	: "memory");
}
// arch/arm/include/asm/spinlock.h

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned long tmp;
	u32 newval;
	arch_spinlock_t lockval;

	prefetchw(&lock->slock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%3]\n"
"	add	%1, %0, %4\n"
"	strex	%2, %1, [%3]\n"
"	teq	%2, #0\n"
"	bne	1b"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
	: "cc");

	while (lockval.tickets.next != lockval.tickets.owner) {
		wfe();
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
	}

	smp_mb();
}

自旋锁的原理是:通过循环不停的尝试获取锁,直到成功或超时。

读写锁 write read pin lock

读写锁也是自旋锁,是更细粒度的锁。它允许多个读操作同时执行。同时只可以存在一个写操作,读和写不能同时进行。
因为读写锁是互斥的,当读锁没有被释放时,写操作是无法获取到锁的,但是读操作是继续执行。所以在读操作比较多的时候,读锁一直被占用,导致写锁无法获取,导致写操作处于饥饿状态。

下面是使用方法:

rwlock mr_rwlock = RW_LOCK_UNLOCKED;

read_lock(&mr_rwlock);//获取读锁
//... 临界区
read_unlock(&mr_rwlock);//释放读锁

write_lock(&mr_rwlock);//获取写锁
//... 临界区
write_unlock(&mr_rwlock);释放写锁

其源码实现如下:

读写锁结构体如下:

typedef struct {
	volatile unsigned int lock;
} arch_rwlock_t;

获取读锁:

//glodfish/arch/arm/include/asm/spinlock.h

static inline void arch_read_lock(arch_rwlock_t *rw)
{
	unsigned long tmp, tmp2;

	prefetchw(&rw->lock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%2]\n"
"	adds	%0, %0, #1\n"
"	strexpl	%1, %0, [%2]\n"
	WFE("mi")
"	rsbpls	%0, %1, #0\n"
"	bmi	1b"
	: "=&r" (tmp), "=&r" (tmp2)
	: "r" (&rw->lock)
	: "cc");

  //添加内存屏障barrier()
	smp_mb();
}

获取写锁:

static inline void arch_write_lock(arch_rwlock_t *rw)
{
	unsigned long tmp;

	prefetchw(&rw->lock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%1]\n"
"	teq	%0, #0\n"
	WFE("ne")
"	strexeq	%0, %2, [%1]\n"
"	teq	%0, #0\n"
"	bne	1b"
	: "=&r" (tmp)
	: "r" (&rw->lock), "r" (0x80000000)
	: "cc");

  //添加内存屏障barrier()
	smp_mb();
}

顺序锁 seq pin lock

读写锁的缺点是读写互斥,读读不互斥,当存在大量读操作时会使写操作饥饿。
顺序锁是读写锁的优化。相对于读写锁,顺序锁中的读写不再互斥,当在读操作期间,写操作完成了,那么读操作必须重新执行。另外,顺序锁中的写写仍是互斥的。

信号量 semphore

信号量与自旋锁不同的是,当获取不到信号量时,进程不会原地打转,而是进入休眠等待状态。
信号量的使用场景为:
1、锁定时间比较长,线程的切换、维护等待队列以及唤醒的开销相对于CPU自旋较小
2、超过一个以上的锁持有者。因为信号量可以设定初始值,初始值为1叫做互斥信号量,大于1则称为计数信号量

信号量的使用方法如下:

//定义一个信号量
static DECLARE_MUTEX(mr_sem);

//获取一个信号
if(down_interruptible(&mr_sem)) {
    //没有获取到信号量,进入休眠等待状态
}

// 否则进入临界区代码

//释放信号量
up(&mr_sem);

信号量源码定义如下:

//信号量结构体
struct semaphore {
	raw_spinlock_t		lock;
	unsigned int		count;
	struct list_head	wait_list;
};

//获取信号,获取成功返回0,否则线程休眠,被信号唤醒后,返回-4
int down_interruptible(struct semaphore *sem)
{
	unsigned long flags;
	int result = 0;

   //加锁
	raw_spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--;
	else
		result = __down_interruptible(sem);
		
	//解锁
	raw_spin_unlock_irqrestore(&sem->lock, flags);

	return result;
}

//线程休眠
static inline int __sched __down_common(struct semaphore *sem, long state,
								long timeout)
{
	struct task_struct *task = current;
	struct semaphore_waiter waiter;

  //创建一个信号等待者,添加到信号的等待队列中
	list_add_tail(&waiter.list, &sem->wait_list);
	waiter.task = task;
	waiter.up = false;

	for (;;) {
		if (signal_pending_state(state, task))
			goto interrupted;
		if (unlikely(timeout <= 0))
			goto timed_out;
		__set_task_state(task, state);
		raw_spin_unlock_irq(&sem->lock);
		//调度当前线程
		timeout = schedule_timeout(timeout);
		raw_spin_lock_irq(&sem->lock);
		if (waiter.up)
			return 0;
	}

//进入
 timed_out:
	list_del(&waiter.list);
	return -ETIME;

//被中断唤醒后,将当前任务从等待队列中删除,并返回-4
 interrupted:
	list_del(&waiter.list);
	return -EINTR;
}

//调度当前线程,如果超时时间为MAX_SCHEDULE_TIMEOUT,则当前线程直接休眠,等待被唤醒
signed long __sched schedule_timeout(signed long timeout)
{
	struct timer_list timer;
	unsigned long expire;

	switch (timeout)
	{
	case MAX_SCHEDULE_TIMEOUT:
		schedule();
		goto out;

    ....		
}

//进程调度函数
asmlinkage __visible void __sched schedule(void)
{
	struct task_struct *tsk = current;

	sched_submit_work(tsk);
	do {
		preempt_disable();
		__schedule(false);
		sched_preempt_enable_no_resched();
	} while (need_resched());
}

//进程调度核心实现
static void __sched notrace __schedule(bool preempt)
{
    ...
    
    /*  更新全局状态,
     *  标识当前CPU发生上下文的切换  */
    rcu_note_context_switch();
    
    ...
    
    /*  挑选一个优先级最高的任务将其排进队列  */
    next = pick_next_task(rq, prev);
    
    /*  进程之间上下文切换    */
    rq = context_switch(rq, prev, next); /* unlocks the rq */
    
    ...
}

//进程切换操作
//1.切换到新进程中的虚拟内存映射 2.切换到新进程的处理器状态,包含栈信息、寄存器信息

// kernel/sched/core.c
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct pin_cookie cookie)
{
    ...
}

互斥体 mutex

互斥体在使用上和信号量差不多,

mutex使用:

//创建并初始化mutex
struct mutex my_mutex;
mutex_init(&my_mutex);

//加锁
void mutex_lock(&my_mutex);
int mutex_lock_interruptible(&my_mutex);
int mutex_trylock(&my_mutex);

//解锁
void mutex_unlock(&my_mutex);

mutex定义如下:

//结构体
struct mutex {
	/* 1: unlocked, 0: locked, negative: locked, possible waiters */
	atomic_t		count;
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值