linux内核同步问题
[手把手教Linux驱动5-自旋锁、信号量、互斥体概述](https://www.cnblogs.com/yikoulinux/p/13558924.html)
基础概念:
并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行
竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竟态状态。
临界资源:多个进程访问的资源
临界区:多个进程访问的代码段
并发场合:
1、单CPU之间进程间的并发:时间片轮转,调度进程。 A进程访问打印机,时间片用完,OS调度B进程访问打印机。
2、单cpu上进程和中断之间并发:CPU必须停止当前进程的执行中断;
3、多cpu之间
4、单CPU上中断之间的并发
使用偏向:
需求 建议加锁方式 低开销、短期加锁 优先自旋锁 长期锁定 优先互斥锁 中断上下文加锁 自旋锁 需要睡眠的持有锁(单线程) 互斥锁 需要睡眠的持有锁(多线程) 信号量
1、信号量(semaphore)
信号量用于进程之间的同步,进程在信号量保护的临界区代码里面是可以睡眠的(需要进行进程调度),这是与自旋锁最大的区别。
信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。它负责协调各个进程,以保证他们能够正确、合理的使用公共资源。它和spin lock最大的不同之处就是:无法获取信号量的进程可以睡眠,因此会导致系统调度。
1.1、特点
1、用于进程与进程之间的同步
2、允许多个进程进入临界区代码执行,临界区代码允许睡眠;
3、信号量本质是基于调度器的,在UP和SMP下没有区别;进程获取不到信号量将陷入休眠,并让出CPU;
4、不支持进程和中断之间的同步
5、进程调度也是会消耗系统资源的,如果一个int型共享变量就需要使用信号量,将极大的浪费系统资源
6、信号量可以用于多个线程,用于资源的计数(有多种状态)
1.2、常用函数
信号量加锁以及解锁过程:
sema_init(&sp->dead_sem, 0); / 初始化/
down(&sema);
临界区代码
up(&sema);
信号量定义:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量初始化:
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
dowm函数实现:
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;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
void down(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);/*自旋锁*/
if (likely(sem->count > 0))
sem->count--;
else
__down(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
</=>
up函数实现:
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);/*自旋锁*/
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
1.3、实现原理
信号量一般可以用来标记可用资源的个数。
举2个生活中的例子:
- 我们要坐火车从南京到新疆,这个’任务’特别的耗时,只能在车上等着车到站,但是我们没有必要一直睁着眼睛等着车到站,最好的情况就是我们上车就直接睡觉,醒来就到站,这样从人(用户)的角度来说,体验是最好的,对比于进程,程序在等待一个耗时的任务的时候,没有必须要占用CPU,可以暂停当前任务使其进入休眠状态,当等待的事件发生之后再由其他任务唤醒,这种场景采用信号量比较合适。
- 我们在等待电梯、等待洗手间,这种场景需要等待的事件并不是很多,如果我们还要找个地方睡一觉,然后等电梯到了或者洗手间可以用了再醒来,那很显然这也没有必要,我们只需要排好队,刷一刷抖音就可以了,对比于计算机程序,比如驱动在进入中断例程,在等待某个寄存器被置位,这种场景需要等待的时间很短暂,系统开销远小于进入休眠的开销,所以这种场景采用自旋锁比较合适。
dowm函数实现原理解析:
(1)down
判断sem->count是否 > 0,大于0则说明系统资源够用,分配一个给该进程,否则进入__down(sem);
(2)__down
调用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);其中TASK_UNINTERRUPTIBLE=2代表进入睡眠,且不可以打断;MAX_SCHEDULE_TIMEOUT休眠最长LONG_MAX时间;
(3)list_add_tail(&waiter.list, &sem->wait_list);
把当前进程加入到sem->wait_list中;
(3)先解锁后加锁;
进入__down_common前已经加锁了,先把解锁,调用schedule_timeout(timeout),当waiter.up=1后跳出for循环;退出函数之前再加锁;
2、原子变量(atomic)
原子变量适用于只共享一个int型变量;
2.1、特点
1、原子操作是指不被打断的操作,即它是最小的执行单位。
2、最简单的原子操作就是一条条的汇编指令(不包括一些伪指令,伪指令会被汇编器解释成多条汇编指令)
2.2、常用函数
常见函数:
#define ATOMIC_INIT(i) { (i) } /*初始化原子变量*/
#define atomic_inc(v) atomic_add(1, v) /*原子变量加1*/
#define atomic_dec(v) atomic_sub(1, v) /*原子变量减1*/
#define atomic_inc_and_test(v) (atomic_add_return(1, v) == 0) /*原子变量加1并测试是否等于0*/
#define atomic_dec_and_test(v) (atomic_sub_return(1, v) == 0) /*原子变量减1并测试是否等于0*/
2.3、实现原理
以atomic_inc为例介绍实现过程
在Linux内核文件arch\arm\include\asm\atomic.h中。
执行 atomic_read、atomic_set
这些操作都只需要一条汇编指令,所以它们本身就是不可打断的。
需要特别研究的是 atomic_inc、atomic_dec
这类读出、修改、写回的函数。
但是atomic_add在内核中是很难找到的,因为没有这个直接的声明。而是一种宏实现。
所以atomic_add的原型是下面这个宏:
#define ATOMIC_OPS(op, c_op, asm_op) \
ATOMIC_OP(op, c_op, asm_op) \
ATOMIC_OP_RETURN(op, c_op, asm_op) \
ATOMIC_FETCH_OP(op, c_op, asm_op)
ATOMIC_OPS(add, +=, add)
#define ATOMIC_OP(op, c_op, asm_op) \
static inline void atomic_##op(int i, atomic_t *v) \
{ \
unsigned long tmp; \
int result; \
\
prefetchw(&v->counter); \
__asm__ __volatile__("@ atomic_" #op "\n" \
"1: ldrex %0, [%3]\n" \
" " #asm_op " %0, %0, %4\n" \
" strex %1, %0, [%3]\n" \
" teq %1, #0\n" \
" bne 1b" \
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) \
: "r" (&v->counter), "Ir" (i) \
: "cc"); \
}
atomic_add等效于:
static inline void atomic_add(int i, atomic_t *v) \
{ \
unsigned long tmp; \
int result; \
\
prefetchw(&v->counter); \
__asm__ __volatile__("@ atomic_" #op "\n" \
"1: ldrex %0, [%3]\n" \
" " #asm_op " %0, %0, %4\n" \
" strex %1, %0, [%3]\n" \
" teq %1, #0\n" \
" bne 1b" \
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) \
: "r" (&v->counter), "Ir" (i) \
: "cc"); \
}
result(%0) tmp(%1) (v->counter)(%2) (&v->counter)(%3) i(%4)
注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中操作。如果出现上下文切换,切换机制会做寄存器上下文保护。
(1)ldrex %0, [%3]
意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。
(2)add %0, %0, %4
result = result + i
(3)strex %1, %0, [%3]
意思是将result保存到&v->counter指向的内存中, 此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。
(4) teq %1, #0
测试strex是否成功(tmp == 0 ??)
(5)bne 1b
如果发现strex失败,从(1)再次执行。
3、自旋锁(spinlock)
Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是”原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地”打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源),一般应用在中断上下文。
3.1、特点
1、spinlock是一种死等机制
2、信号量可以允许多个执行单元进入,spinlock不行,一次只能允许一个执行单元获取锁,并且进入临界区,其他执行单元都是在门口不断的死等
3、由于不休眠,因此spinlock可以应用在中断上下文中;
4、由于spinlock死等的特性,因此临界区执行代码尽可能的短;
3.2、常用函数
spinlock加锁以及解锁过程:
spin_lock(&devices_lock);
临界区代码
spin_unlock(&devices_lock);
spinlock初始化
#define spin_lock_init(_lock) \
do { \
spinlock_check(_lock); \
raw_spin_lock_init(&(_lock)->rlock); \
} while (0)
进程和进程之间同步
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
本地软中断之间同步
static __always_inline void spin_lock_bh(spinlock_t *lock)
{
raw_spin_lock_bh(&lock->rlock);
}
本地硬中断之间同步
static __always_inline void spin_lock_irq(spinlock_t *lock)
{
raw_spin_lock_irq(&lock->rlock);
}
本地硬中断之间同步并且保存本地中断状态
#define spin_lock_irqsave(lock, flags) \
do { \
raw_spin_lock_irqsave(spinlock_check(lock), flags); \
} while (0)
尝试获取锁
static __always_inline int spin_trylock(spinlock_t *lock)
{
return raw_spin_trylock(&lock->rlock);
}
3.3、实现原理
arch_spinlock_t结构体定义如下:
#define TICKET_SHIFT 16
typedef struct {
union {
u32 slock;/*union*/
struct __raw_tickets {
#ifdef __ARMEB__ /*大端模式*/
u16 next;
u16 owner;
#else/*小端模式*/
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t;
arch_spin_lock的实现如下:
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;
prefetchw(&lock->slock);/*从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();
}
lockval(%0) newval(%1) tmp(%2) &lock->slock(%3) 1 << TICKET_SHIFT(%4)
(1)ldrex %0, [%3]
把lock->slock的值赋值给lockval;并且(分别在Local monitor和Global monitor中)设置独占标志。
(2)add %1, %0, %4
newval =lockval +(1<
Original: https://www.cnblogs.com/agui125/p/16132406.html
Author: 风御之举
Title: 内核同步问题
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/607301/
转载文章受原作者版权保护。转载请注明原作者出处!