1. 同步与互斥
(1)互斥与同步机制是计算机系统中,用于控制进程对某些特定资源(共享资源)的访问的机制
(2)同步是指用于实现控制多个进程按照一定的规则或顺序访问某些系统资源的机制。
(3)互斥是指用于实现控制某些系统资源在任意时刻只能允许一个进程访问的机制。互斥是同步机制中的一种特殊情况。
(4)同步机制是linux操作系统可以高效稳定运行的重要机制
2. Linux系统并发主要来源
在操作系统引入了进程概念,进程成为调度实体后,系统就具备了并发执行多个进程的能力,但也导致了系统中各个进程之间的资源竞争和共享。另外,由于中断、异常机制的引入,以及内核态抢占都导致了这些内核执行路径(进程)以交错的方式运行。对于这些交错路径执行的内核路径,如不采取必要的同步措施,将会对一些关键数据结构进行交错访问和修改,从而导致这些数据结构状态的不一致,进而导致系统崩溃。因此,为了确保系统高效稳定有序地运行,linux必须要采用同步机制。
(1)中断处理:
(2)内核态抢占:
(3)多处理器的并发:
注:采用同步机制的目的就是避免多个进程并发访问统一临界资源
3. Linux内核同步机制
(1)禁用中断 (单处理器不可抢占系统)
(2)原子操作:Linux内核提供了一个专门的atomic_t类型(一个原子访问计数器)和一些专门的函数和宏,这些函数和宏作用于atomic_t类型的变量,并当作单独的、原子的汇编语言来使用。
①Linux中的原子操作
atomic_read(v) //返回*v atomic_set(v) //把*v置成i atomic_add(i, v) //把*v增加i atomic_sub(i, v) //把*v减去i atomic_sub_and_test(i, v) //从*v中减去i,如果结果为0,则返回1,否则,返回0 atomic_inc(v) //把1加到*v atomic_dec(v) //从*v减1 atomic_dec_and_test(v) //从*v减1,如果结果为0,则返回1,否则,返回0 atomic_inc_and_test(v) //把1加到*v,如果结果为0,则返回1,否则,返回0 atomic_add_negative(i, v) //把i加到*v,如果结果为负,则返回1,否则,返回0 atomic_inc_return(v) //把1加到*v,返回*v的新值 atomic_dec_return(v) //从*v减1,返回*v的新值 atomic_add_return(i, v) //把i加到*v,返回*v的新值 atomic_sub_return(i, v) //从*v减i,返回*v的新值
②Linux中原子位处理函数
test_bit(nr, addr) set_bit(nr, addr) clear_bit(nr, addr) change_bit(nr, addr) test_and_set_bit(nr, addr) //设置*addr的第nr位,并返回它(第nr位)的原值 test_and_clear_bit(nr, addr) test_and_change_bit(nr, addr) atomic_clear_mask(mask, addr) atomic_set_mask(mask, addr)
4. 自旋锁
(1)普通自旋锁
在Linux内核中,自旋锁用spinlock_t结构来表示,
typedef struct { raw_spinlock_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 } spinlock_t;
① 在单处理器系统下:CONFIG_SMP没有选中时,变量类型raw_spinlock_t退化为一个空结构体。相应的接口函数也发生了退化。相应的加锁函数spin_lock()和解锁函数spin_unlock()退化为只完成禁止内核态抢占、使能内核态抢占。
② 在多处理器系统下:选中CONFIG_SMP时,核心变量raw_lock的数据类型raw_lock_t定义如下:
typedef struct { volatile unsigned int lock; } raw_spinlock_t;
从定义中可以看出该数据结构定义了一个内核变量,用于计数工作。当结构中成员变量slock的数值为1时,表示自旋锁处于非锁定状态,可以使用。否则,表示处于锁定状态,不可以使用。
③ 普通自旋锁接口函数
spin_lock_init(lock) //把自旋锁置为1(未锁) spin_lock(lock) //循环,直到自旋锁变为1(未锁),然后把自旋锁置位为0(锁上) spin_unlock(lock) //把自旋锁置位1(未锁) spin_unlock_wait(lock) //等待,直到自旋锁变为1(未锁) spin_is_locked(lock) //如果自旋锁被置位为1(未锁),返回0;否则,返回1. spin_trylock(lock) //把自旋锁置为0,如果原来的锁值是1(未锁),则返回1;否则,返回0
(2)自旋锁的变种
spin_lock_irq(lock) spin_unlock_irq(lock)
相比于前面的普通自旋锁,它在上锁前增加了禁用中断的功能,在解锁后,使能了中断。
(3)读写自旋锁(rwlock)
① 读写自旋锁的引入是为了增加内核的并发能力。只要没有内核控制路径对数据结构进行修改,读写自旋锁就允许多个内核控制路径同时读取同一数据结构。如果一个内核控制路径想对这个数据结构进行写操作,那么它必须首先获取读写锁的写锁,写锁授权独占访问这个资源。
② 每个读写锁都是一个rwlock_t结构
typedef struct { raw_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;
③读写自旋锁的接口函数
DEFINE_RWLOCK(lock) //声明读写自旋锁lock,并初始化为未锁定状态 write_lock(lock) //以写方式锁定,若成功则返回,否则循环等待 write_unlock(lock) //解除写方式的锁定,重设为未锁定状态 read_lock(lock) //以读方式锁定,若成功则返回,否则循环等待 read_unlock(lock) //解除读方式的锁定,重设为未锁定状态
④读写自旋锁工作原理:
对于读写自旋锁rwlock,它允许任意数量的读取者同时进入临界区,但写入者必须进行互斥访问。一个进程要进行读,必须要先检查是否有进程正在写入,如果有,则自旋(忙等),否则获得锁。一个进程要进程写,必须要先检查是否有进程正在读取或者写入,如果有,则自旋(忙等)否则获得锁。即读写自旋锁的应用规则如下:
* 如果当前有进程正在写,那么其他进程就不能读也不能写。
* 如果当前有进程正在读,那么其他程序可以读,但是不能写。
(4)顺序自旋锁(seqlock)
① Linux2.6内核引入顺序自旋锁,主要用于解决自旋锁同步机制中,在拥有大量读者进程时,写进程由于长时间无法持有锁而被饿死的情况,其主要思想是:为写进程提高更高的优先级,在写锁定请求出现时,立即满足写锁定的请求,无论此时是否有读进程正在访问临界资源。但是新的写锁定请求不会,也不能抢占已有写进程的写锁定。
② 每个顺序锁都是包含两个字段的seqlock_t结构:一个类型为spinlock_t的lock字段和一个无符号整形的sequence字段,第二个字段是一个顺序计数器。每个读者都必须在读数据前后两次读顺序计数器,并检查两次读到的值是否相同,如果不相同,说明新的写者已经开始写并增加了顺序计数器,因此暗示读者刚读到的数据是无效的。
typedef struct { unsigned sequence; spinlock_t lock; } seqlock_t;
③ 顺序锁访问接口函数
seqlock_init(seqlock) //初始化为未锁定状态 read_seqbgin()、read_seqretry() //保证数据的一致性 write_seqlock(lock) //尝试以写锁定方式锁定顺序锁 write_sequnlock(lock) //解除对顺序锁的写方式锁定,重设为未锁定状态。
④ 顺序收访问接口使用说明:
通过把SEQLOCK_UNLOCKED赋值给变量seqlock_t或者执行seqlock_init宏,把seqlock_t变量初始化为“未上锁”。写者通过调用write_seqlock() 和write_sequnlock()获取和释放顺序锁。第一个函数获取seqlock_t数据结构中的自旋锁,然后使顺序计数器加1;第二个函数再次增加顺序计数器,然后释放自旋锁。这样可以保证写者在写的过程中,计数器的值是奇数,并且当没有写者在改变数据的时候,计数器值是偶数。
unsigned int seq; do{ seq = read_seqbegin(&seqlock); /* 临界区 */ }while(read_seqretry(&seqlock, seq));
read_seqbegin()返回顺序锁的当前顺序号;如果局部变量seq的值是奇数(写者在read_seqbegin()函数调用后,正更新数据结构),或者seq的值与顺序锁的顺序计数器的当前值不匹配(读者正执行临界区代码时,写者开始工作),read_seqretry()就返回1。
5. 信号量
(1) Linux提供两种信号量
① 内核信号量,由内核控制路径使用
② System V IPC信号量,由用户态进程使用
(2) 信号量应用背景:前面介绍的自旋锁同步机制是一种“忙等”机制,在临界资源被锁定的时间很短的情况下很有效。但是在临界资源被持有时间很长或者不确定的情况下,忙等机制则会浪费很多宝贵的处理器时间。针对这种情况,linux内核中提供了信号量机制,此类型的同步机制在进程无法获取到临界资源的情况下,立即释放处理器的使用权,并睡眠在所访问的临界资源上对应的等待队列上;在临界资源被释放时,再唤醒阻塞在该临界资源上的进程。另外,信号量机制不会禁用内核态抢占,所以持有信号量的进程一样可以被抢占,这意味着信号量机制不会给系统的响应能力,实时能力带来负面的影响。
(3) 信号量设计思想:除了初始化之外,信号量只能通过两个原子操作P()和V()访问,也称为down()和up()。down()原子操作通过对信号量的计数器减1,来请求获得一个信号量。如果操作后结果是0或者大于0,获得信号量锁,任务就可以进入临界区。如果操作后结果是负数,任务会放入等待队列,处理器执行其他任务;对临界资源访问完毕后,可以调用原子操作up()来释放信号量,该操作会增加信号量的计数器。如果该信号量上的等待队列不为空,则唤醒阻塞在该信号量上的进程。
(4)普通信号量:由数据结构struct semaphore来描述
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };