07.linux内核同步

等待队列

当某个进程执行需要等待某个事件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);
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核中,有多种同步机制可用于保护共享资源,防止并发访问导致的数据竞争和不一致性。以下是一些常见的同步机制: 1. 自旋锁(Spinlock):自旋锁是一种基本的同步机制,用于保护临界区,以防止多个进程或线程同时访问。当一个进程或线程尝试获取自旋锁时,如果锁已被占用,它会不断地自旋等待,直到锁被释放。 2. 互斥锁(Mutex):互斥锁是一种更高级的同步机制,与自旋锁类似,但当锁被占用时,进程或线程会被阻塞而不是自旋等待。这样可以避免浪费CPU资源。 3. 读写锁(ReadWrite Lock):读写锁允许多个读操作同时进行,但只有一个写操作可以进行。这提供了更好的并发性能,适用于读频繁、写较少的场景。 4. 原子操作(Atomic Operations):原子操作是一种不可中断的操作,可以确保在多线程环境下对共享变量的原子性访问。常见的原子操作包括原子赋值、原子加减等。 5. 信号量(Semaphore):信号量是一种用于控制对共享资源的访问的同步机制。它可以用于限制同时访问资源的进程或线程数目。 6. 屏障(Barrier):屏障是一种同步机制,它可以使一组进程或线程在某个点上等待,直到所有进程或线程都到达该点,然后再继续执行。 这些同步机制在Linux内核中均有相应的实现,并且可以根据具体的需求选择适当的机制来保护共享资源和实现线程间的同步

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值