[Z] Linux 内核同步机制

Linux内核同步机制,挺复杂的一个东西,常用的有自旋锁,信号量,互斥体,原子操作,顺序锁,RCU,内存屏障等。这里就说说它们的特点和基本用法。

自旋锁 :通用的 和读写的

特点:
1. 处理的时间很短。
2. 尝试获取锁时,不能睡眠,但是有trylock接口可以直接退出。
3. 多用在中断中。
4. 任何时候只有一个保持者能够访问临界区。
5. 可以被中断打断的(硬件和软件的)
6. 获取自旋锁后首先就是关闭了抢占

spin_lock使用接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   void spin_lock_init(spinlock_t *lock); //init
   void spin_lock(spinlock_t *lock); // 获取锁
  void spin_unlock(spinlock_t *lock); //释放锁
   其他变体
typedef struct spinlock {
     union {
         struct raw_spinlock rlock;
 
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
         struct {
             u8 __padding[LOCK_PADSIZE];
             struct lockdep_map dep_map;
         };
#endif
     };
} spinlock_t;

Rwlock: 读写自旋锁基本特点和通用自旋锁一样,但是有时候多线程频繁读取临界区如果同时只能一个那么效率会很低,它的特点就是在读的时候获取读锁,可以同时有N个线程同时读,在写时需要获得写锁(不能有读和写锁)。

在读操作时,写操作必须等待;写操作时,读操作也需要的等待。这样虽然避免了数据的不一致,但是某些操作要等待,后面还会出现顺序锁,是对读写锁的优化,把写的优先级调高了

使用接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
rwlock_init(lock); //init
  read_lock(lock); //获取读锁
  read_unlock(lock) ;
  write_lock (lock); //获取写锁
  write_unlock(lock);
 
/*
  * include/linux/rwlock_types.h - generic rwlock type definitions
  *                 and initializers
  *
  * portions Copyright 2005, Red Hat, Inc., Ingo Molnar
  * Released under the General Public License (GPL).
  */
typedef struct {
     arch_rwlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
     unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
     unsigned int magic, owner_cpu;
     void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
     struct lockdep_map dep_map;
#endif
} rwlock_t;

而关于自旋锁的缺点?这里找到ibm一个文章

信号量(semaphore):通用的 和读写的

相对于自旋锁,它最大的特点就是允许调用它的线程进入睡眠

1
2
3
4
5
6
/* Please don't access any members of this structure directly */
struct semaphore {
     raw_spinlock_t        lock;
     unsigned int        count;
     struct list_head    wait_list;
};

void sema_init(struct semaphore *sem, int val); // val值代表了同时多少个线程可以进入临界区,一般为1 即作为互斥体使用;当然>1 时,并发操作同一资源会引发什么呢?
down_interruptible(struct semaphore *sem); // 获取信号量 ,它是可以中断的。
up(struct semaphore *sem); // 释放信号量,一般配对使用,当然也可以在别的线程里释放它。

读写信号量:rwsem 它和读写自旋锁类似 除了线程可以睡眠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* the rw-semaphore definition
  * - if activity is 0 then there are no active readers or writers
  * - if activity is +ve then that is the number of active readers
  * - if activity is -1 then there is one active writer
  * - if wait_list is not empty, then there are processes waiting for the semaphore
  */
struct rw_semaphore {
     __s32            activity;
     raw_spinlock_t        wait_lock;
     struct list_head    wait_list;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
     struct lockdep_map dep_map;
#endif
};
 
init_rwsem(sem)    ; // 初始化
down_read( struct rw_semaphore *sem); // 获取读信号量
up_read( struct rw_semaphore *sem); //释放读信号量
down_write( struct rw_semaphore *sem); //获取写信号量
up_write( struct rw_semaphore *sem); // 释放写信号量

互斥体(mutex):和count=1的信号量几乎没有区别,当然拥有互斥锁的进程总是尽可能的在短时间内释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct mutex {
     /* 1: unlocked, 0: locked, negative: locked, possible waiters */
     atomic_t        count;
     spinlock_t        wait_lock;
     struct list_head    wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
     struct task_struct    *owner;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
     const char         *name;
     void            *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
     struct lockdep_map    dep_map;
#endif
};
 
mutex_init(mutex); // init 互斥锁
mutex_lock(); //获取互斥锁,几乎都能获取
mutex_unlock();        //释放互斥锁

原子操作(atomic)(和架构相关,就是多条指令相当于一条指令执行,多用于计数)

组要是在smp上有意义,防止多条指令被多cpu执行。也是为了实现互斥。

顺序锁(sequence)

特点:

和读写自旋锁锁类似,但是它的写不会等待。写的时候持有自旋锁。首先读者的代码应该尽可能短且写者不能频繁获得锁,其次被保护的数据结构不包括被写 修改的指针或被读间接引用的指针。当要保护的资源很小很简单,会很频繁被访问并且写入操作很少发生且必须快速时,就可以用seqlock。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Seqlock:
typedef struct {
     unsigned sequence;
     spinlock_t lock;
} seqlock_t;
 
seqlock_init(x) / DEFINE_SEQLOCK(x) // init
write_seqlock(seqlock_t *sl) ; // 获取写锁
write_sequnlock(seqlock_t *sl);
read_seqbegin 和 read_seqretry 结合使用 //读时如果有写锁,则循环等待直到锁释放.
应用实例drivers/md/md.c
retry:
             seq = read_seqbegin(&bb->lock);
 
             memset (bbp, 0xff, PAGE_SIZE);
 
             for (i = 0 ; i < bb->count ; i++) {
                 u64 internal_bb = p[i];
                 u64 store_bb = ((BB_OFFSET(internal_bb) << 10)
                         | BB_LEN(internal_bb));
                 bbp[i] = cpu_to_le64(store_bb);
             }
             bb->changed = 0;
             if (read_seqretry(&bb->lock, seq))
                 goto retry;

RCU:read-copy-update

在linux提供的所有内核互斥设施当中属于一种免锁机制。Rcu无需考虑读和写的互斥问题。

它实际上是rwlock的一种优化。读取者不必关心写入者。所以RCU可以让多个读取者与写入者同时工作。写入者的操作比例在10%以上,需要考虑其他互斥方法。并且必须要以指针的方式来访问被保护资源。

Rcu_read_lock //仅仅是关闭抢占
Rcu_read_unlock //打开抢占
Rcu_assign_pointer(ptr,new_ptr)
//等待队列:它并不是一种互斥机制。它辅助comletion。
//它主要用来实现进程的睡眠等待。
//操作接口:wait/ wake_up

1
2
3
4
5
struct __wait_queue_head {
          spinlock_t lock;
          struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

完成接口(completion) :该机制被用来在多个执行路径间作同步使用,即协调多个执行路径的执行顺序。如果没有完成体,则睡眠在wait_list上。这里usb 在提交urb时会用到。

如果驱动程序要在执行后面操作之前等待某个过程的完成,它可以调用wait_for_completion,以要完成的事件为参数:

Completion机制是线程间通信的一种轻量级机制:允许一个线程告诉另一个线程工作已经完成

1
2
3
4
5
6
7
8
9
10
11
12
struct completion {
     unsigned int done;
     wait_queue_head_t wait;
};
 
接口:
DECLARE_COMPLETION(x) // 静态定义completion
init_completion( struct completion *x); // 动态init
INIT_COMPLETION(x); // 初始化一个已经使用过的completion
Wait_for_completion( struct completion *x);
complete( struct completion *); //done +1,唤醒等待的一个。
Complete_all // 唤醒所有的,一般不会用。

内存屏障

内存屏障主要有:读屏障、写屏障、通用屏障、优化屏障

内存屏障主要解决了两个问题:单处理器下的乱序问题和多处理器下的内存同步问题

编译器优化以保证程序上下文因果关系为前提。

以 读屏障为例,它用于保证读操作有序。屏障之前的读操作一定会先于屏障之后的读操作完成,写操作不受影响,同属于屏障的某一侧的读操作也不受影响。类似的, 写屏障用于限制写操作。而通用屏障则对读写操作都有作用。而优化屏障则用于限制编译器的指令重排,不区分读写。前三种屏障都隐含了优化屏障的功能。比如:
tmp = ttt; *addr = 5; mb(); val = *data;

有了内存屏障就了确保先设置地址端口,再读数据端口。而至于设置地址端口与tmp的赋值孰先孰后,屏障则不做干预。有了内存屏障,就可以在隐式因果关系的场景中,保证因果关系逻辑正确。

在Linux中,优化屏障就是barrier()宏,它展开为asm volatile(“”:::”memory”)
smp_rmb(); // 读屏障
smp_wmb(); //写屏障
smp_mb(); // 通用屏障

Blk:大内核锁

BKL(大内核锁)是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过度到细粒度加锁机制。它终将退出历史舞台。

BKL的特性:
持有BKL的任务仍然可以睡眠 。因为当任务无法调度时,所加的锁会自动被抛弃;当任务被调度时,锁又会被重新获得。当然,并不是说,当任务持有BKL时,睡眠是安全的,紧急是可以这样做,因为睡眠不会造成任务死锁。

BKL是一种递归锁。一个进程可以多次请求一个锁,并不会像自旋锁那么产生死锁。BKL可以在进程上下文中。

BKL是有害的:
在内核中不鼓励使用BKL。一个执行线程可以递归的请求锁lock_kernel(),但是释放锁时也必须调用同样次数的unlock_kernel() 操作,在最后一个解锁操作完成之后,锁才会被释放。BKL在被持有时同样会禁止内核抢占。多数情况下,BKL更像是保护代码而不是保护数据.

备注:单核不可抢占内核 唯一的异步事件就是硬件中断 ,所以想要同步即关闭中断即可。对于单核可抢占和多核可抢占的 ,除了中断 还有进程调度(即优先级高的进程抢占cpu资源),而上述所有这些机制都是为了防止并发。

 

参考书籍《linux内核设计与实现》 ,《深入linux设备驱动内核机制》等。
参考代码 linux3.18.3

你可能感兴趣的:([Z] Linux 内核同步机制)