Linux内核同步方法

当多个进程、线程或中断、正常用户程序同时访问同一个资源,可能导致错误,因此内核需要提供并发控制机制,对公共资源的访问进行同步控制,确保共享资源的安全访问。
linux中包含了众多的互斥与同步机制,包括信号量、互斥体、自旋锁、原子操作、读写锁等。

在讨论之前我们引入几个概念:

进程上下文:应用程序陷入内核运行时所处的内核环境。
中断上下文:中断服务程序执行时所处的内核环境。
抢占式内核:用户程序在执行系统调用期间可以被高优先级进程抢占。
非抢占式内核:用户程序执行系统调用不能被其他进成抢占。
对称多处理器(SMP):一个计算机上汇集了多个处理器,他们共享内存和总线,可并行处理数据。
单处理器:只有一个CPU。

1.自旋锁

自旋锁结构

typedef struct {
    /**
     * 该字段表示自旋锁的状态,值为1表示未加锁,任何负数和0都表示加锁
     */
    volatile unsigned int slock;
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned magic;
#endif
#ifdef CONFIG_PREEMPT
    /**
     * 表示进程正在忙等待自旋锁。
     * 只有内核支持SMP和内核抢占时才使用本标志。
     */
    unsigned int break_lock;
#endif
} spinlock_t;

特点

  1. 是一种轻量级的多处理器间的同步机制。
  2. 它是一种忙等待机制,当一个线程获取了锁之后,其他试图获取这个锁的线程进行原地打转等待锁的释放,会造成CPU时间的浪费。因此它要求持有锁的线程或应用程序所占用的时间尽可能短。
  3. 进程在获得自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的。

注意事项

  1. 自旋锁是不可递归的,递归的请求同一个自旋锁会自己锁死自己。
  2. 线程获取自旋锁之前,要禁止当前处理器上的中断。(防止获取锁的线程和中断形成竞争条件)
    比如:当前线程获取自旋锁后,在临界区中被中断处理程序打断,中断处理程序正好也要获取这个锁,
    于是中断处理程序会等待当前线程释放锁,而当前线程也在等待中断执行完后再执行临界区和释放锁的代码。

应用例子

#include <linux/spinlock.h>

spinlock_t my_spinlock;
spin_lock_init(&my_spinlock);
//或者spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;

spin_lock(&my_spinlock);
 .......    //临界区代码
spin_unlock(&my_spinlock);

注意:存在于中断和进程上下文的临界区,必须使用禁止中断的形式:spin_lock_irqsave()

自旋锁方法列表

方法 描述 其它
spin_lock_init() 动态初始化指定的spinlock_t 将自旋锁设置为1,表示有一个资源可用。
spin_lock() 获取指定的自旋锁 循环等待直到自旋锁解锁(置为1),然后,将自旋锁锁上(置为0)。可自旋等待,可被软、硬件中断
spin_unlock() 释放指定的锁 将自旋锁解锁(置为1)。
spin_lock_irq() 禁止本地中断并获取指定的锁 可自旋等待,不保存中断状态关闭软、硬件中断
spin_unlock_irq() 释放指定的锁,并激活本地中断 置为1
spin_lock_irqsave() 保存本地中断的当前状态,禁止本地中断,并获取指定的锁 可自旋等待,保存中断状态并关闭软、硬件中断
spin_unlock_irqstore() 释放指定的锁,并让本地中断恢复到以前状态 置为1
spin_trylock() 试图获取指定的锁,如果未获取,则返回0 不自旋等待,成功返回1、失败则返回0
spin_is_locked() 如果指定的锁当前正在被获取,则返回非0,否则返回0

2.信号量

信号量结构

// 信号量结构体类型
struct semaphore {
    spinlock_t      lock;       //自旋锁变量
    unsigned int        count;      //信号量的值
    struct list_head    wait_list;  //链表,推入等待队列时使用
};

特点

  1. 互斥体和信号量都是一种睡眠锁,不可在中断当中使用。
  2. 如果有一个线程试图获得一个已经被占用的信号量或互斥体时,该线程都会被推进一个等待队列,然后被睡眠。
  3. 当持有信号量或互斥体的进程将其释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量或互斥体。
  4. 互斥体功能上基本上与信号量一样,互斥体占用空间比信号量小,运行效率比信号量高,二者都通过schedule相关函数将线程调度出cpu,进入睡眠。

注意事项

  1. 信号量也是一种锁,和自旋锁不同的是,线程获取不到信号量的时候,不会像自旋锁一样循环的去试图获取锁,
    而是进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。
  2. 由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区。
  3. 信号量消耗的CPU时间的地方在于使线程睡眠和唤醒线程,如果 (使线程睡眠 + 唤醒线程)的CPU时间 > 线程自旋等待的CPU时间,那么可以考虑使用自旋锁。

应用例子

#include <linux/semaphore.h>
struct semaphore my_sema;   //信号量
sema_init(&my_sema, 1); //信号量初始化为1,与init_MUTEX(my_sema)等效
//定义一个semaphore变量,并初始化为1
//DECLARE_MUTEX(my_sema);
down_interruptible(&my_sema);   //获取信号量(减操作)  这个方法把线程状态置为 TASK_INTERRUPTIBLE 后睡眠
 .....  //临界区代码
up(&bufflock);      //释放信号量(加操作)

信号量方法列表

方法 描述 其它
sema_init(struct semaphore *, int) 以指定的计数值初始化动态创建的信号量 将信号量设置为1,表示有一个资源可用。
down_interruptible(struct semaphore *) 以试图获得指定的信号量,如果信号量已被争用,则进入可中断睡眠状态 减操作(置为0)
down(struct semaphore *) 以试图获得指定的信号量,如果信号量已被争用,则进入不可中断睡眠状态
down_trylock(struct semaphore *) 以试图获得指定的信号量,如果信号量已被争用,则立即返回非0值
up(struct semaphore *) 以释放指定的信号量,如果睡眠队列不空,则唤醒其中一个任务 加操作(置为1)

一般用的比较多的是down_interruptible()方法,因为以 TASK_UNINTERRUPTIBLE 方式睡眠无法被信号(比如Ctrl+C)唤醒。
对于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 补充说明一下:
1. TASK_INTERRUPTIBLE - 可打断睡眠,可以接受信号并被唤醒,也可以在等待条件全部达成后被显式唤醒(比如wake_up()函数)。
2. TASK_UNINTERRUPTIBLE - 不可打断睡眠,只能在等待条件全部达成后被显式唤醒(比如wake_up()函数)。

函数说明

int down_interruptible(struct semaphore *sem)

这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步。

一个进程在调用down_interruptible()之后,如果sem<0,那么就进入到可中断的睡眠状态并调度其它进程运行, 但是一旦该进程收到信号(比如Ctrl+C发出软中断),那么就会从down_interruptible函数中返回。并标记错误号为:-EINTR。

一个形象的如:小强下午放学回家,回家了就要开始吃饭嘛,这时就会有两种情况:情况一:饭做好了,可以开始吃(已经置过1);情况二:当他到厨房去的时候发现妈妈还在做,妈妈就对他说:“你先去睡会,待会做好了叫你(先前执行减操作,已经置过0)。”小强就答应去睡会,不过又说了一句:“睡的这段时间要是小红(比喻信号)来找我玩,你可以叫醒我。”小强就是down_interruptible,想吃饭就是获取信号量,睡觉对应这里的休眠,而小红来找我玩就是中断休眠。

猜你喜欢

转载自blog.csdn.net/zhangzheng_1986/article/details/81735829