内核同步-锁机制

好久不见. 提交于 2019-12-09 20:44:18

    Linux系统上,多个进程可以同时运行,以及各种中断发生的中断也在同时得到处理,这种多个上下文宏观上同时运行的情况称为并发。并发具体包括如下几种可能:

    1) UP平台上,一个进程正在执行时被另一个进程抢占;

    2) UP平台上,一个进程正在执行时发生了中断,内核转而执行中断处理程序;

    3) SMP平台上,每个处理器都会发生UP平台上的情况;

    4) SMP平台上,多个进程或中断同时在多个CPU上执行;

多个并发的上下文同时使用同一个资源的情况称为竞态,而可能发生竞态的这一段代码称为临界区。内核编程时的临界区,比较多的情况是:

1) 代码访问了全局变量,并且这段代码可被多个进程执行;

2) 代码访问了全局变量,并且这段代码可被进程执行,也可被中断处理程序执行;

针对上述情况,内核提供了如下手段来解决竟态问题:

1)锁机制:

2)院子操作:

下面会先介绍锁机制。Linux内核提供了多种锁机制,这些锁机制的区别在于,当获取不到锁时,执行程序是否发生睡眠并进行系统调度。具体包括自旋锁、互斥体、信号量。

一、自旋锁:spinlock_t

自旋锁有两个基本操作:获取与释放。获取自旋锁时,当判断锁的状态为未锁,则会马上加锁,如果已经是锁定的状态,当期执行流程则会执行“忙等待”,中间没有任何的调度操作。也就说执行流程从判断锁的状态到完成加锁,是一个原子操作,在执行上是不可分割的。

自旋锁的实现是平台相关的,在使用的时候,只需统一包含如下头文件:

#include <linux/spinlock.h>

自旋锁的变量类型是spinlock_t,定义如下:

typedef struct {
    raw_spinlock_t raw_lock;
    #if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
    unsigned int break_lock;
    #endif
} spinlock_t;
 
typedef struct {
    volatile unsigned int lock;
} raw_spinlock_t;

1)自旋锁需要初始化才能使用,自旋锁的初始化接口如下:

# define spin_lock_init(lock)     \
do { *(lock) = SPIN_LOCK_UNLOCKED; } while (0)

2)获取自旋锁的接口如下:

void spin_lock(spinlock_t *lock);

3)释放自旋锁的接口如下:

void spin_unlock(spinlock_t *lock);


    获取自旋锁的时候,内部会首先禁止抢占,然后来时循环判断锁的状态。在UP版本中,唯一的操作就是禁止抢占,如果是UP版本且非抢占式内核,则进一步退化为无操作。可以粗略的看一下内核源码的实现,因为自旋锁的实现是与平台相关的,这里以arm平台为例:

void __lockfunc _spin_lock(spinlock_t *lock)
{
    preempt_disable(); //关闭抢占
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); //这个如果没有打开调试自旋锁的宏的话,相当于无操作。
    _raw_spin_lock(lock); //这里调用平台相关的代码来轮询锁的状态
}

static inline void __raw_spin_lock(raw_spinlock_t *lock)

    {

        unsigned long tmp;

        __asm__ __volatile__(

        "1: ldrex   %0, [%1]\n"  //取lock->lock放在 tmp里,并且设置&lock->lock这个内存地址为独占访问

        "   teq %0, #0\n"   // 测试lock->lock是否为0,影响标志位z

        #ifdef CONFIG_CPU_32v6K

            "   wfene\n"

        #endif

        "   strexeq %0, %2, [%1]\n"  //如果lock->lock是0,并且是独占访问这个内存,就向lock->lock里 写入1,并向tmp返回0,同时清除独占标记

        "   teqeq   %0, #0\n" //如 果lock->lock是0,并且strexeq返回了0,表示加锁成功,返回

        "   bne 1b"   //如果加锁失败,则会向后跳转到上面的标号1处,再次重新执行

         : "=&r" (tmp)

        : "r" (&lock->lock), "r" (1) 

        : "cc");

        smp_mb();

    }

上面那段汇编代码就是循环判断lock的值,最后的那个bne 1b,就可以明白为什么自旋锁在获取不到锁的情况下,会进行所谓的“自旋”了!

4) 此外,内核还提供了一个用于尝试获取自旋锁的接口:

Int spin_trylock(spinlock_t *lock);

到这里为止,上面介绍的接口都没有考虑到在获取锁以后又发生中断的问题,如果要解决与中断的互斥问题,则需要使用以下接口:

Void spin_lock_ireq(spinlock_t *lock); // 禁止中断并获取自旋锁
Void spin_unlock_irq(spinlock_t *lock); // 释放自旋锁并使能中断
Void spin_lock_irq_save(spin_lock_t *lock, unsigned long flags); // 禁止中断并获取保存中断状态,然后获取自旋锁
Void spin_unlok_irq_store(spinlock_t *lock, unsigned long flags); // 释放自旋锁,并将中断状态恢复为已保存的状态值。
Int spin_trylock_irq(spinlock_t *lock); // 禁止中断并尝试获取自旋锁,成功返回非0值,返回值为0表示获取失败,则中断状态恢复为使能状态。
Int spin_trylock_irqsave(spinlock_t *lock, unsigned long flags); // 禁止中断并保存中断状态,然后尝试获取自旋锁,返回值为非0表示获取成功,0表示获取失败,则会恢复中断状态。

    这里简单的看一下内核是如何实现上述接口的:在kernel/spinlock.c文件中

void __lockfunc _spin_lock_irq(spinlock_t *lock)
{
    local_irq_disable(); // 关闭当前CPU上的中断
    // 下面的执行流程和spin_lock接口一样的
    preempt_disable(); 
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    _raw_spin_lock(lock);
}
EXPORT_SYMBOL(_spin_lock_irq);

# define spin_unlock_irq(lock)  _spin_unlock_irq(lock)
void __lockfunc _spin_unlock_irq(spinlock_t *lock)
{
    spin_release(&lock->dep_map, 1, _RET_IP_);
    _raw_spin_unlock(lock);
    local_irq_enable();
    preempt_enable();
}
EXPORT_SYMBOL(_spin_unlock_irq);

    自旋锁的使用原则:

    1) 能不用尽量不用,并且持有锁的时间应尽可能的短。因为持有锁以后,其他CPU要获取同一把锁的执行流程会陷入空循环,消耗CPU资源;

    2) 持有锁以后尽量不要再去获取另一把锁,如果需要则代码各处获取锁的顺序要一致,否则容易引起死锁        

    3) 从性能的角度考虑,如果不需要与中断互斥,则不要使用禁止中断的接口;

    4) 在获取自旋锁以后,到释放自旋锁之前,不允许调用或者是间接调用引起系统调度的操作。因为一旦获取锁以后,就进入一种特殊状态--原子上下文,即在此状态下不允许被打断,而系统调度会打断当前的执行流程。

    二、互斥体:

互斥体与信号量都是属于非原子操作的同步手段,共同的特点是:当获取失败需要将当前进程挂起,进入等待状态,进程将进入睡眠状态。

    Linux内核互斥体的定义和声明是在linux/mutex.h头文件中,主要包括如下接口:

// 初始化:
void mutex_init(struct mutex *lock);
// 定义并初始化互斥体变量lock
DEFINE_MUTEX(lock);
 
// 获取mutex
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(strut mutex *lock);
int mutex_trylock(struct mutex *lock);
 
// 释放mutex
void mutex_unlock(struct mutex *lock);
 
// 销毁mutex
void mutex_destroy(struct mutex *lock)

看到这些接口,会发现与我们编写应用程序使用的mutex接口很类似,用法也很类似。

        mutex_lock()如果不能够获取mutex,则当前进程会进入睡眠,直至有其他的进程释放这个mutex,才会被唤醒。函数返回时说明互斥体已经获取成功。如果希望进程在等待互斥体时任然可以响应信号,则应使用mutex_lock_interruptible()

    在使用的过程中要注意如下几点:

    1) mutex的获取有可能导致进程睡眠,所以不能够用于中断上下文中,只可以用在进程上下文中;

    2) mutex的获取与释放必须是同一个进程,不能够在A进程获取mutex,然后在B进程释放mutex

    下面简单的对其实现源码逻辑大致的走一下:

    首先看看mutex的定义:

struct mutex {
    /* 1: unlocked, 0: locked, negative: locked, possible waiters */
     atomic_t  count;
     spinlock_t  wait_lock;
     struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
    struct thread_info *owner;
    const char   *name;
    void   *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
};

        如果我们不进行调试,那么就只有count、wait_lock和wait_list成员需要关注的。

    可以发现一个mutex就是一个原子计数器count(保存互斥体的状态),以及一个用于存放获取Mutex失败的进程链表wait_listwali_lock是为了保证原子操作wait_list。所以说互斥体是在自旋锁的基础上实现的!

    然后看看mutex的初始化,主要是初始化mutex对象的成员,将原子计数器初始化为1,表示处于unlocked状态。

# define mutex_init(mutex) \
do {       \
static struct lock_class_key __key;  \
\
__mutex_init((mutex), #mutex, &__key);  \
} while (0)
 
void  __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key)
{
atomic_set(&lock->count, 1); // 原子计数器初始化为1,表示处于unlocked状态
spin_lock_init(&lock->wait_lock);
INIT_LIST_HEAD(&lock->wait_list);
 
debug_mutex_init(lock, name, key);
}

        接着再看看互斥体的获取mutex_lock()的实现,是如何让当前进程在获取mutex失败的情况下进入睡眠等待的:

void inline fastcall __sched mutex_lock(struct mutex *lock)
{
    might_sleep();
/*
 * The locking fastpath is the 1->0 transition from
 * 'unlocked' into 'locked' state.
 */
    __mutex_fastpath_lock(&lock->count, __mutex_lock_slowpath);
}

// __mutex_fastpath_lock()的作用是对原子计数器进行减1,并判断原子计数器是否为0,如果为0则直接返回,表示获取锁成功,否则表示获取失败,会调用__mutex_lock_slowpath()进行后续的阻塞睡眠处理。
static void fastcall noinline __sched
__mutex_lock_slowpath(atomic_t *lock_count)
{
    struct mutex *lock = container_of(lock_count, struct mutex, count);
 
    __mutex_lock_common(lock, TASK_UNINTERRUPTIBLE, 0);
}

// 最后还是通过__mutex_lock_common()函数来完成阻塞睡眠处理:
static inline int __sched
__mutex_lock_common(struct mutex *lock, long state, unsigned int subclass)
{
    struct task_struct *task = current;
    struct mutex_waiter waiter;
    unsigned int old_val;
    unsigned long flags;
 
    spin_lock_mutex(&lock->wait_lock, flags);
 
    debug_mutex_lock_common(lock, &waiter);
    mutex_acquire(&lock->dep_map, subclass, 0, _RET_IP_);
    debug_mutex_add_waiter(lock, &waiter, task->thread_info);
 
    /* add waiting tasks to the end of the waitqueue (FIFO): */
     list_add_tail(&waiter.list, &lock->wait_list);
     waiter.task = task;
 
    for (;;) {
    /*
     * Lets try to take the lock again - this is needed even if
     * we get here for the first time (shortly after failing to
     * acquire the lock), to make sure that we get a wakeup once
     * it's unlocked. Later on, if we sleep, this is the
     * operation that gives us the lock. We xchg it to -1, so
     * that when we release the lock, we properly wake up the
     * other waiters:
     */
    old_val = atomic_xchg(&lock->count, -1);
      if (old_val == 1)
       break;
 
    /*
     * got a signal? (This code gets eliminated in the
     * TASK_UNINTERRUPTIBLE case.)
     */
    if (unlikely(state == TASK_INTERRUPTIBLE &&
        signal_pending(task))) {
        mutex_remove_waiter(lock, &waiter, task->thread_info);
        mutex_release(&lock->dep_map, 1, _RET_IP_);
        spin_unlock_mutex(&lock->wait_lock, flags);
 
        debug_mutex_free_waiter(&waiter);
        return -EINTR;
    }
    __set_task_state(task, state);
 
    /* didnt get the lock, go to sleep: */
    spin_unlock_mutex(&lock->wait_lock, flags);
    schedule();
    spin_lock_mutex(&lock->wait_lock, flags);
  }
 
    /* got the lock - rejoice! */
     mutex_remove_waiter(lock, &waiter, task->thread_info);
    debug_mutex_set_owner(lock, task->thread_info);
 
    /* set it to 0 if there are no waiters left: */
     if (likely(list_empty(&lock->wait_list)))
      atomic_set(&lock->count, 0);
 
    spin_unlock_mutex(&lock->wait_lock, flags);
    debug_mutex_free_waiter(&waiter);
    return 0;
}

    __mutex_lock_common()函数代码比较长,大致的代码逻辑是:

    1)先将当前进程current放入到mutex的wait_list链表中;

    2)然后是执行schedule(),执行进程切换调度,CPU就会从run queue中选取一个优先级最高的任务来运行;

    这个时候,获取mutex的进程就已经挂起了suspend,需要有其他的进程调用mutex_unlock(),才能将此进程唤醒,并重新加入到run queue中继续运行。

    上面的代码会把schedule()放在一个for循环内部,这是因为进程被唤醒后,需要先检查条件是否满足,如果不满足,则会再次挂起。


    下面就分析下时如何唤醒等待在此mutex上的进程的: mutex_unlock()

void fastcall __sched mutex_unlock(struct mutex *lock)
{
/*
 * The unlocking fastpath is the 0->1 transition from 'locked'
 * into 'unlocked' state:
 */
    __mutex_fastpath_unlock(&lock->count, __mutex_unlock_slowpath);
}
// __mutex_fastpath_unlock会对原子计数器count进行加1,然后调用__mutex_unlock_slowpath()来唤醒等待此mutex的进程。
static fastcall inline void
__mutex_unlock_common_slowpath(atomic_t *lock_count, int nested)
{
    struct mutex *lock = container_of(lock_count, struct mutex, count);
    unsigned long flags;
 
    spin_lock_mutex(&lock->wait_lock, flags);
    mutex_release(&lock->dep_map, nested, _RET_IP_);
    debug_mutex_unlock(lock);
 
/*
 * some architectures leave the lock unlocked in the fastpath failure
 * case, others need to leave it locked. In the later case we have to
 * unlock it here
 */
    if (__mutex_slowpath_needs_to_unlock())
        atomic_set(&lock->count, 1);
 
     if (!list_empty(&lock->wait_list)) {
        /* get the first entry from the wait-list: */
        struct mutex_waiter *waiter = list_entry(lock->wait_list.next, struct mutex_waiter, list);
 
        debug_mutex_wake_waiter(lock, waiter);
        wake_up_process(waiter->task);
     }
 
    debug_mutex_clear_owner(lock);
    spin_unlock_mutex(&lock->wait_lock, flags);
}

        从上面的代码可以看出,释放mutex的过程就是把等待链表的第一个进程取出来,然后将其放入run-queue运行队列中,这样就能够被调度器调度,得到运行。其中wake_up_process()函数的作用就是把一个指定的进程放入到run queue中,并将进程的状态修改为TASK_RUNNING.

    代码分析到这里,应该基本上就清楚了mutex的实现逻辑了。使用mutex就是有一点需要特别注意的,必须同一个进程对其lock和unlock操作,仔细分析下代码,就可以发现了,这里就不再说明了。


三、信号量:

信号量与互斥体都是作为同步的手段,共同点是当获取失败时会导致进程睡眠,所以都是不可以用在中断上下文中,只可以用在进程上下文中。

不同点是:

1) mutex的获取与释放必须是由同一个进程执行,而semaphore则可以跨进程使用,即可以在A进程获取信号量,在B进程释放信号量。

2) mutex只有lockedunloked两种状态值,semaphore的计数器可以大于1.

简单的看一下信号量的使用接口:

#include <linux/semaphore.h>
 
// 初始化信号量对象
struct semaphore sem;
void sema_init(struct semapthore *sem, int val);
 
// 获取信号量
void down(struct semaphore *sem); //如果获取失败,进程将进入不可被信号打断的睡眠状态
int down_interruptible(struct semaphore *sem); //如果获取失败,进程将进入可被信号打断的睡眠状态
int down_killable(strut semaphore *sem); // 如果获取失败,进程在睡眠等待的过程中,只响应致命信号
int down_timeout(struct semaphore *sem,  long jiffies); // 进程对信号量的等待操作有时间限制
 
// 释放信号量
void up(struct semaphpre *sem);

down_interruptible()down_killable()的返回值说明,0表示获取成功,-EINTR表示在等待信号量的过程中被信号打断,信号量获取失败。

down_timeout()的返回值说明,0表示成功,-ETIME表示在等待信号量的过程中超时,获取信号量失败。









易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!