linux驱动开发(10)- 互斥锁mutex

用count=1的信号量实现的互斥方法不是Linux下经典的用法,Linux内核针对count=1的信号量重新定义了一个新的数据结构struct mutex,一般都称其为互斥锁或者互斥体。同时内核根据使用场景的不同,把用于信号量的DOWN和UP操作在struct mutex上作了优化与扩展,专门用于这种新的数据类型。

互斥锁的定义与初始化

互斥锁mutex的概念本来就来自semaphore,如果去除掉那些跟调试相关的成员,struct mutex和struct semaphore并没有本质的不同:

<include/linux/mutex.h>
struct mutex {
    /* 1: unlocked, 0: locked, negative: locked, possible waiters */
    atomic_t           count;
    spinlock_t      wait_lock;
    struct list_head  wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
    struct thread_info *owner;
#endif
};

如同struct semaphore一样,对struct mutex的初始化不能直接通过操作其成员变量的方式进行,而应该利用内核提供的宏或者函数。定义一个静态的struct mutex变量同时初始化的方法是利用内核的DEFINE_MUTEX:

<include/linux/mutex.h>
#define __MUTEX_INITIALIZER(lockname) \
      { .count = ATOMIC_INIT(1), \
        .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock), \
        .wait_list = LIST_HEAD_INIT(lockname.wait_list) \
      }
#define DEFINE_MUTEX(mutexname) \
    struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)

如果在程序执行期间要初始化一个mutex变量,则可以使用mutex_init宏。去除掉那些与调试相关的操作之后,mutex_init宏可以展开成如下的函数定义形式:

void mutex_init(struct mutex *lock)
{
    atomic_set(&lock->count, 1);
    spin_lock_init(&lock->wait_lock);
    INIT_LIST_HEAD(&lock->wait_list);
}

互斥锁的DOWN操作

互斥锁mutex上的DOWN操作在Linux内核中为mutex_lock函数,定义如下:

<kernel/mutex.c>
void __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_set_owner(lock);
}

函数的设计思想体现在__mutex_fastpath_lock和__mutex_lock_slowpath两条主线上,__mutex_fastpath_lock用来快速判断当前可否获得互斥锁,如果成功获得锁,则函数直接返回,否则进入到__mutex_lock_slowpath函数中。这种设计是基于这样一个事实:想要获得某一互斥锁的代码绝大部分时候都可以成功获得。由此延伸开来在代码层面就是, mutex_lock函数进入__mutex_lock_slowpath的概率很低。__mutex_fastpath_lock是一平台相关函数,下面以ARM处理器为例,分析其代码实现:

<arch/arm/include/asm/mutex.h>
static inline void __mutex_fastpath_lock(atomic_t *count, void (*fail_fn)(atomic_t *))
{
    int __ex_flag, __res;
    __asm__ (
    L1  "ldrex%0,[%2]  \n\t"
    L2  "sub     %0,%0,#1\n\t"
    L3  "strex %1,%0,[%2]   "
        : "=&r" (__res), "=&r" (__ex_flag)
        : "r" (&(count)->counter)
        : "cc","memory" );
    __res |= __ex_flag;
    if (unlikely(__res != 0))
        fail_fn(count);
}

函数在L1处通过ldrex完成__res = count->counter,L2处完成__res = __res - 1,L3处试图用__res的当前值来更新count->counter。这里说“试图”是因为这个更新的操作未必会成功,主要是考虑到可能有别的进程也在操作count->counter,为不使这种可能的竞争引起对count->counter值更新的混乱,这里用了ARM指令中用于实现互斥访问的指令ldrex和strex (前面在spinlock的代码分析时已经提过)​。ldrex和strex保证了对count->counter的“读取—更新—写回”操作序列的原子性。如果L3处的更新操作成功,那么__ex_flag将为0。

接下来在__res |= __ex_flag执行完之后,通过if语句判断__res是否为0,有两种情况会导致__res不为0:一是在调用这个函数前count->counter=0,表明互斥锁已经被别的进程获得,这样L2处的__res = -1;二是在L3处的更新操作不成功,表明当前有另外一个进程也在对count->counter进行同样的操作。这两种情况都将导致__mutex_fastpath_lock不能直接返回,而是进入fail_fn,也就是调用__mutex_lock_slowpath。

此处if语句中的unlikely是利用GCC编译优化扩展的一个宏,这里的意思是条件语句__res != 0为真的可能性很小,编译器借此可以调整一些编译后代码的顺序达到某种程度的优化。与之对应的是likely。如果__mutex_fastpath_lock函数不能在第一时间获得互斥锁返回,那么将进入__mutex_lock_slowpath,正如其名字所预示的那样,代码将进入一段艰难坎坷的旅途。在Linux源码中,__mutex_lock_slowpath函数与信号量DOWN操作中的down函数非常相似,不过__mutex_lock_slowpath在把当前进程放入mutex的wait_list之前会试图多次询问mutex中的count是否为1,也就是说当前进程在进入wait_list之前会多次考察别的进程是否已经释放了这个互斥锁。这主要基于这样一个事实:拥有互斥锁的进程总是会在尽可能短的时间里释放。如果别的进程已经释放了该互斥锁,那么当前进程将可以获得该互斥锁而没有必要再去睡眠。

互斥锁的UP操作

互斥锁的UP操作为mutex_unlock,函数定义如下:

<kernel/mutex.c>
void __sched mutex_unlock(struct mutex *lock)
{
    /*
    * The unlocking fastpath is the 0->1 transition from 'locked'
    * into 'unlocked' state:
    */
#ifndef CONFIG_DEBUG_MUTEXES
    /*
    * When debugging is enabled we must not clear the owner before time,
    * the slow path will always be taken, and that clears the owner field
    * after verifying that it was indeed current.
    */
    mutex_clear_owner(lock);
#endif
    __mutex_fastpath_unlock(&lock->count, __mutex_unlock_slowpath);
}

和mutex_lock函数一样,mutex_unlock函数也有两条主线:__mutex_fastpath_unlock和__mutex_unlock_slowpath,分别用于对互斥锁的快速和慢速解锁操作。__mutex_fastpath_unlock定义如下:

<arch/arm/include/asm/mutex.h>
static inline void
__mutex_fastpath_unlock(atomic_t *count, void (*fail_fn)(atomic_t *))
{
    int __ex_flag, __res, __orig;
    __asm__ (
        "ldrex%0,[%3]  \n\t"
        "add     %1,%0,#1\n\t"
        "strex %2,%1,[%3]   "
        : "=&r" (__orig), "=&r" (__res), "=&r" (__ex_flag)
        : "r" (&(count)->counter)
        : "cc","memory" );
    __orig |= __ex_flag;
    if (unlikely(__orig != 0))
        fail_fn(count);
}

这里除了是将count->counter的值加1以外,代码和__mutex_fastpath_lock中的几乎完全一样。在最后的if语句中,导致代码中__orig不为0也有两种情况:一是在调用这个函数前count->counter不为0,表明在当前进程占有互斥锁期间有别的进程竞争该互斥锁;二是对count->counter的更新操作不成功,表明当前有另外一个进程也在对count->counter进行操作,这种情况主要是针对别的进程此时调用mutex_lock函数导致的竞争,因为互斥的原因别的进程此时不可能调用mutex_unlock。这种情况的处理是非常重要的,不只是关系到count->counter正确更新的问题,还涉及能否防止一个唤醒操作的丢失。在没有别的进程竞争该互斥锁的情况下,__mutex_fastpath_unlock函数要完成的工作最简单,把count->counter的值加1然后返回。如果有别的进程在竞争该互斥锁,那么函数进入__mutex_unlock_slowpath,这个函数主要用来唤醒在当前mutex的wait_list中休眠的进程,如同up函数一样。

番外:这里再简单介绍一下ldrex和strex汇编指令。

LDREX(Load-Exclusive Register)指令是ARM架构中用于实现多核环境下内存独占访问的关键指令,其工作原理如下:

  1. 指令执行流程
    LDREX指令从指定内存地址加载数据到目标寄存器,并标记该内存地址为“独占访问”状态。这一标记由硬件的独占监视器(Exclusive Monitor)维护。
    STREX指令尝试将数据写回内存,但仅在独占标记未被破坏时成功。如果独占标记有效,STREX会更新内存并清除标记;如果标记无效(例如被其他处理器修改),STREX会失败并返回非零值。

  2. 独占监视器的作用
    本地监视器:每个处理器核心内部有一个本地监视器,用于跟踪该核心对内存地址的独占访问。
    全局监视器:在多核系统中,全局监视器协调多个处理器对共享内存的独占访问。
    标记与清除:LDREX指令设置独占标记,STREX指令成功执行后清除标记。如果其他处理器修改了内存地址,所有相关处理器的独占标记会被清空。

  3. 内存属性与独占访问
    共享内存:可以被多个处理器访问,独占访问需要本地和全局监视器的协调。
    非共享内存:只能被一个处理器访问,独占访问仅需本地监视器。

  4. 失败重试机制
    如果STREX指令失败,程序需要重新执行LDREX-STREX循环,直到STREX成功为止。这种机制确保了原子操作的正确性。

  5. 关键注意事项
    独占标记限制:每个处理器核心在同一时间只能标记一个内存地址为独占访问。
    性能影响:频繁的独占访问失败可能导致性能下降,需合理设计重试逻辑。

你可能感兴趣的:(linux驱动开发,驱动开发,linux,服务器)