等待队列
当某个进程执行需要等待某个事件ready后才可以继续时,可以在此阻塞(进入休眠状态,并在适当的时候唤醒,涉及到进程的调度和进程状态的转换)。内核提供了等待队列来完成阻塞式 IO,等待队列是内核基于更基础的 互斥机制+进程调度 来实现的。
eg.内核驱动read/write()例程在设备状态尚未ready时,应该检查 filep->f_flags 上的O_NONBLOCK位是否有设置,如果设置则应该立即返回-EAGAIN,否则可以使用等待队列进入阻塞(然后在 isr 中唤醒)。
DECLARE_WAIT_QUEUE_HEAD(wq) init_waitqueue_head(wait_queue_head_t *) | 单独操作entry的接口: DECLARE_WAIT_QUEUE(name,tsk)声明并init init_waitqueue_entry(wait_queue_t *,struct task_struct *) add_wait_queue(wait_queue_head_t *,wait_queue_t *) remove_wait_queue(wait_queue_head_t *,wait_queue_t *) //设置wake_up时将被触发的函数 init_waitqueue_entry_func(wait_queue_t *,wait_queue_func_t ) |
wait_event_interuptible(wq, condition) //宏函数,内部所做的工作就是 操作waitqueue_entry + __set_current_state(TASK_INTERRUPTIBLE) + shcedule() wake_up_interruptible(wq) //等待队列唤醒函数 | 与wait_event()类似的还有completion机制(深入linux设备驱动内核机制 p165),也是通过极为类似的策略实现的同步: void init_completion(struct completion *x) void wait_for_completion(struct completion *x) void complete(struct completion *x) |
1、并发可能带来竞态(竞争状态),所以需要互斥与同步机制(防止竞态访问某些资源/标志的手段叫互斥,因为资源暂时不能访问而等待(或其他应对措施)是同步的结果)。
2、并发源:中断、进程调度(多进程)、多处理器。
3、竞态,是指多个执行路径有可能对同一资源进行操作、可能导致资源数据紊乱的行为(通常需要保护的是数据而不是代码)。
4、对共享资源进行访问的代码片段被称为临界区。
中断
单处理器不可抢占系统中(并发源只有中断了),使用local_irq_enable() + local_irq_disable()(或者 local_irq_save() + local_irq_restore(),保存开关中断前的中断状态)简单有效,但关中断时间太长会影响系统性能。调用者不会因为 local_irq_save() 而睡眠。
spin_lock
spin_lock()+spin_unlock()使用了处理器专用的互斥访问指令,会关闭系统可抢占性并访问两个处理器之间共享的某个变量(使用此变量判断是否是上锁状态)。特点:没有拿到自旋锁则忙等待,拿到自旋锁后不可以休眠(不可被换出处理器,要求不可调用可导致休眠的函数,如kmalloc的某些掩码)并应该尽快处理,调用spin_lock前最好关闭中断防止中断中再次spin_lock死锁(spin_lock的某些变体中已经内部实现)。调用者不会因为 spin_lock()而睡眠。spin_lock()原是为多处理器设计,后被引申到单处理器上(仅做禁止调度和关中断的操作)。读取者与写入者自旋锁:只要有写,就要互斥;但读与读之间可以同时进行。
信号量
信号量,是使用spinlock+schedule实现的可导致睡眠的、灵活的(因为count可以大于1)lock。特点:没有拿到信号量则sleep。底层实现方式如下。
struct semaphore {
spinlock_t lock; //用于互斥访问count变量
unsigned int count; //信号量值
//当调用down的函数无法获得信号量时,其task将被链入此list并schedule其他线程运行
//当调用up的函数发现有等待此信号量的task时,将从此list上取第一个task节点唤醒
struct list_head wait_list;
}
//初始化信号量
sema_init(struct semaphore *sem, int val) //初始化一个信号量(需要自己来定义)
//拿取信号量
down_interruptiable(struct semaphore *sem) //当收到信号或拿到信号量时被唤醒,最常用
down(struct semaphore *sem) //只有拿到信号量时才可以被唤醒,很少使用
down_killable(struct semaphore *sem) // 当收到致命信号时会被唤醒,很少使用
down_trylock(struct semaphore *sem) //无法获取信号量时,直接返回>0而非进入睡眠状态
down_timeout(struct semaphore *sem, long jiffies) //当无法拿到信号量或超时都被唤醒
//返还信号量
up(struct semaphore *sem)
同样也存在读取者与写入者信号量。
mutex
linux单独为count=1的信号量定义了一个新的数据结构mutex,mutex可以认为是信号量的简化、强约束版本的、可导致睡眠的锁。中断上下文中不可以使用可能导致睡眠的锁,因此常用spinlock。
DEFINE_MUTEX(mutexname) //定义并初始化一个mutex
mutex_init(struct mutex *lock) //初始化一个mutex
mutex_lock(struct mutex *lock) //down,获取mutexlock
mutex_unlock(struct mutex *lock) //up,返还mutexlock
原子变量
顺序锁 seqlock 和 rcu(read-copy-update)和上面提到的读取者写入者类似,用在读取者和写入者共存的系统。
若共享资源只是简单的整型变量,使用前面的lock未免大材小用,linux提供atomic机制来实现原子操作。类似的还有对变量上的bit进行原子性的操作和测试。
atomic_t abc = ATOMIC_INIT(int)
void atomic_inc(atomic_t *v)
void atomic_dec(atomic_t *v)
void atomic_add(int I, atomic_t *v)
void atomic_sub(int I, atomic_t *v)
void atomic_set(atomic_t *v, int i)
int atomic_read(atomic_t *v)
用户层通过 pthread_mutex_t + pthread_cond_t 实现灵活的同步(说他灵活是因为我们可以自己定义需要检验的flag而不一定是linux内定的count变量。使用mutex拿不到锁则睡眠,拿到锁发现我们的flag不符合要求也会调用 pthread_cond_wait 原子解锁并在cond的等待队列上睡眠,即 pthread_cond_wait帮我们实现了类似down的策略但flag我们自己定)
/* Binary semaphore */
typedef struct bsem {
pthread_mutex_t mutex; //用于互斥访问 v
pthread_cond_t cond; //其操作api 封装了mutex操作和schedule操作
int v;
} bsem;
/* Init semaphore to 1 or 0 */
static void bsem_init(bsem *pBsem, int value)
{
if (value < 0 || value > 1)
{
//err("bsem_init(): Binary semaphore can take only values 1 or 0");
exit(1);
}
pthread_mutex_init(&(pBsem->mutex), NULL);
pthread_cond_init(&(pBsem->cond), NULL);
pBsem->v = value;
}
/* Post to at least one thread */
static void bsem_post(bsem *pBsem)
{
pthread_mutex_lock(&pBsem->mutex);
pBsem->v = 1;
//这只会唤醒一个处于 pthread_cond_wait 的线程
pthread_cond_signal(&pBsem->cond);
pthread_mutex_unlock(&pBsem->mutex);
}
/* Wait on semaphore until semaphore has value 0 */
static void bsem_wait(bsem *pBsem)
{
//mutex 用于保护 cond,上锁过程可能导致阻塞
pthread_mutex_lock(&pBsem->mutex);
while (pBsem->v != 1) {
//1.pthread_cond_wait 需要在已经 lock mutex 的环境下进行
//2.因为 pthread_cond_wait 会自动 unlock()
//3.这么做是为了"在本task会因为调用pthread_cond_wait 而被放入等待队列休眠时,别人有机会拿到锁来唤醒本task"
//4."unlock+将task放入等待队列" 通常是原子化的
//5.因为unlock瞬间若别人lock并signal了(那么此唤醒机被错过了!),本task可能再无机会被唤醒
//6.当条件 cond 满足,本task从等待队列移除并返回时,pthread_cond_wait 又会自动 lock
//7.因为等待cond的task可能有很多,本task从休眠状态出来的第一件事就是再去check value,并操作value
pthread_cond_wait(&pBsem->cond, &pBsem->mutex); //这家伙内部有schedule()+mutex_lock()的操作
}
pBsem->v = 0;
pthread_mutex_unlock(&pBsem->mutex);
}