mutex互斥体-1

文章详细探讨了Linux内核中信号量、mutex和MCS自旋锁的区别与应用。信号量可以睡眠,但当初始化为1时可实现互斥访问。mutex作为轻量级锁,在锁竞争激烈时比信号量更快,且具备MCS机制,确保只有一个线程自旋等待,其余线程休眠。MCS锁通过链表结构保证了只有一个节点自旋,其他节点睡眠,避免了锁的持有者重复添加到链表中。文章还介绍了MCS锁的初始化、申请和释放过程。
摘要由CSDN通过智能技术生成

linux已经有了信号量,为什么还需要mutex机制?(自旋锁呢?自旋锁不也是独占访问的吗?但是mutex和信号量是可以睡眠的)

当信号量被初始化为1时,信号量就可以实现互斥访问。那为什么还需要mutex呢?书上说mutex更加轻便简单,在锁竞争激烈的情况下,mutex比信号量执行速度更快。另外mutex还有一个mcs机制,能够保证只有一个人在自旋等待锁被释放,而其他人进行休眠。(自旋等待机制核心原理是当发现锁持有者正在临界区执行,且没有其他优先级更高的进程要被调度(need_resched),那么当前进程相信锁持有者很快就会离开临界区,释放锁。因此不如直接自旋等待,而不进入睡眠)

MCS锁

struct optimistic_spin_queue {
    /*
     * Stores an encoded value of the CPU # of the tail node in the queue.
     * If the queue is empty, then it's set to OSQ_UNLOCKED_VAL.
     */
    atomic_t tail;//保存编码后的cpu号,初始化为OSQ_UNLOCKED_VAL(0)
};
struct optimistic_spin_node {
    struct optimistic_spin_node *next, *prev;
    int locked; /* 1 if lock acquired */
    int cpu; /* encoded CPU # value */
};

每个MCS锁有一个optimistic_spin_queue。optimistic_spin_node表示本cpu上的节点,它组成了一个双向链表,locked成员表示加锁状态,cpu表示被重新编码后的cpu编号,用于表示该node属于哪个cpu。如果locked=1,说明编号为cpu的节点获得了该锁。optimistic_spin_node是一个Per-CPU变量

static DEFINE_PER_CPU_SHARED_ALIGNED(struct optimistic_spin_node, osq_node);

MCS锁初始化:osq_lock_init

static inline void osq_lock_init(struct optimistic_spin_queue *lock)
{
    atomic_set(&lock->tail, OSQ_UNLOCKED_VAL);//初始化为0,后面会被设置为偏码后的cpu号
}
#define OSQ_UNLOCKED_VAL (0)

MCS锁申请:osq_lock

osq_lock:看书上说MCS机制能够保证只有一个人自旋,其他的都会睡眠。还以为是进来就判断是否有人原地自旋,没有自己自旋,如果有就去休眠。结果是想多了。和代码注释中写的一样没有想明白怎么保证链表里面不会将同一个node加入多次的?

感觉主要就是区分不同的cpu,即node。对于同一个cpu,同一时刻只能有一个执行路径。如果另外一个执行路径需要使用osq_lock尝试获取锁,那么之前在自旋的人,肯定是因为进程调度,被换下cpu。在代码中对于need_resched,因此在退出自旋的时候,已经将当前node从链表中摘出了。所以即使第二个人进来,又将该节点加入到链表中,该节点在链表中也只占用一个位置(所以链表长度,应该就是cpu个数)。

另外我在内核里面搜索调用osq_lock的地方,都显示的禁用了内核抢占,这意味着即使在osq_lock中自旋--》while (!smp_load_acquire(&node->locked))的时候被中断打断,中断执行完之后,必然还是会回到这里继续执行。所以当前进程被换下cpu的唯一路径只有need_resched触发才行。

因此感觉自旋锁的始终是最后一个尝试获取锁osq的人在进行自旋。但是感觉每个cpu上都可以有一个原地自旋的人

bool osq_lock(struct optimistic_spin_queue *lock)
{
    struct optimistic_spin_node *node = this_cpu_ptr(&osq_node);
    struct optimistic_spin_node *prev, *next;
    int curr = encode_cpu(smp_processor_id());
    int old;

    node->locked = 0;
    node->next = NULL;
    node->cpu = curr;
    /* 
    将lock->tail设置为curr,并且返回tail在替换前的值
    */
    old = atomic_xchg(&lock->tail, curr);
    /*
    cpu号编码规则为cpu0 + 1
    编码后的为 1,2,3....
    OSQ_UNLOCKED_VAL=0表示还无人获得该锁
    */
    if (old == OSQ_UNLOCKED_VAL)//成功获取锁,直接返回
        return true;
    /*
    old != OSQ_UNLOCKED_VAL说明有人获得了锁
    且此时的old执行了锁持有者所在的cpu号
    */
    /*
    通过old获得锁持有者的node:optimistic_spin_node
    现在有两种情况:
    1、持有者在其他cpu上
    2、持有者在当前cpu上;会出现这种情况吗??
    从书上的图看,应该链表节点个数为cpu个数,应该是不会重复加入节点的
    后面会把当前的node和prev连接起来,将当前node加入到链表中
    针对情况2,那不是自己指向了自己??
    */
    prev = decode_cpu(old);
    node->prev = prev;
    ACCESS_ONCE(prev->next) = node;

    /*
    一直查询当前节点的locked是否变为1,因为前驱节点释放锁时,
    会将它下一个节点的lock成员设置为1
    理想情况下前驱节点释放锁,那么当前进程也退出自旋
    那是怎么确定是自己的前驱节点得到锁,而不是更前面的节点获得了锁呢
    这样怎么保证只有一个人睡眠呢?

    另外如果出现了当前节点被重复挂到了链表上,那么只需要修改一个节点
    其他节点都能被唤醒咯.这个疑问先给出了解答:

    下面这段代码是自旋,考虑下面两种情况
    1、在同一个cpu上,会不会有两个人同时走到这里?
        肯定不会,如果有第二个人到这里,前一个人一定会因为need_resched,
        将node从链表中摘除
    2、不同cpu上,多个人走到这里?
        走到这里说明他们的node是不同的。

    所以可以回答上面的问题了,链表中同一个node只会占用一个节点,
    不会出现一个node在链表中出现多次的问题
    */
    /* 锁持有者在释放锁的时候会将下一个节点的locked设置为1 */
    while (!smp_load_acquire(&node->locked)) {
        /*
         * If we need to reschedule bail... so we can block.
         */
        if (need_resched())//如果有更高优先级的进程调度,需要退出自旋
            goto unqueue;

        arch_mutex_cpu_relax();
    }
    return true;

unqueue:
    /*
     * Step - A  -- stabilize @prev
     *
     * Undo our @prev->next assignment; this will make @prev's
     * unlock()/unqueue() wait for a next pointer since @lock points to us
     * (or later).
     */
    //1 将prev->next设置为null
    for (;;) {
        /*
         说明自旋期间没有人来修改链表.这种关系其他人能来修改? 
         好像是锁持有者可以改变这种关系。例如所释放的时候,会把自己的next置空
         这个链表的关系,不就改变了嘛。后面确认一下锁释放是不是这样
        后面看了,确实是这样的,表头节点(即锁持有者释放的时候会将自己的next置空)
        */
        if (prev->next == node &&
            cmpxchg(&prev->next, node, NULL) == node)
            break;

        /*
         * We can only fail the cmpxchg() racing against an unlock(),
         * in which case we should observe @node->locked becomming
         * true.
         */
        /*
         锁持者释放的时候会将下一个节点的node->locked设置为1
        表示当前节点获得了锁,直接返回即可,不用摘除节点了 
        */
        if (smp_load_acquire(&node->locked))
            return true;

        arch_mutex_cpu_relax();

        /*
         * Or we race against a concurrent unqueue()'s step-B, in which
         * case its step-C will write us a new @node->prev pointer.
         */
        /* prev节点可能发生变化,重新加载prev */
        prev = ACCESS_ONCE(node->prev);
    }

    /*
     * Step - B -- stabilize @next
     *
     * Similar to unlock(), wait for @node->next or move @lock from @node
     * back to @prev.
     */
    //获取curr_node的下一个节点
    next = osq_wait_next(lock, node, prev);
    if (!next)
        return false;

    /*
     * Step - C -- unlink
     *
     * @prev is stable because its still waiting for a new @prev->next
     * pointer, @next is stable because our @node->next pointer is NULL and
     * it will wait in Step-A.
     */

    ACCESS_ONCE(next->prev) = prev;
    ACCESS_ONCE(prev->next) = next;

    return false;
}

osq_lock:查看lock->tail是否为OSQ_UNLOCKED_VAL。如果是则表示没有无人获取锁。反转就需要将本cpu的节点加入链表中,一直进行自旋,直到成功或者退出。

在退出时,需要将本节点从MCS链表中删除。主要分为3个步骤:

  1. 解除前继节点prev的next指针指向

  1. 解除当前节点的next指针指向,并找出当前节点的下一节点

  1. 让前继节点prev->next指向next_node,next_node->prev指向prev_node

osq_wait_next:寻找当前节点的下一个节点

static inline struct optimistic_spin_node *
osq_wait_next(struct optimistic_spin_queue *lock,
          struct optimistic_spin_node *node,
          struct optimistic_spin_node *prev)
{
    struct optimistic_spin_node *next = NULL;
    int curr = encode_cpu(smp_processor_id());
    int old;

    /*
     * If there is a prev node in queue, then the 'old' value will be
     * the prev node's CPU #, else it's set to OSQ_UNLOCKED_VAL since if
     * we're currently last in queue, then the queue will then become empty.
     */
    old = prev ? prev->cpu : OSQ_UNLOCKED_VAL;//让old执行prev节点所在的cpu编号

    for (;;) {
        /*
        atomic_cmpxchg(atomic_t * v, int old, int new)
        如果*v和old相等,则*v = new,并且返回v所在的原始内容
        */
        if (atomic_read(&lock->tail) == curr &&
            atomic_cmpxchg(&lock->tail, curr, old) == curr) {
            /*
             * We were the last queued, we moved @lock back. @prev
             * will now observe @lock and will complete its
             * unlock()/unqueue().
             */
            /*
            这里直接退出循环,返回空值。说明当前节点就是最后一个节点
            为什么呢?
            因为在osq_lock进来的时候old = atomic_xchg(&lock->tail, curr);
            如果此时tail仍然是curr,说明这段时间没有其他节点尝试获取锁
            不然tail应该是其他节点的cpu号
            */
            break;
        }

        /*
         * We must xchg() the @node->next value, because if we were to
         * leave it in, a concurrent unlock()/unqueue() from
         * @node->next might complete Step-A and think its @prev is
         * still valid.
         *
         * If the concurrent unlock()/unqueue() wins the race, we'll
         * wait for either @lock to point to us, through its Step-B, or
         * wait for a new @node->next from its Step-C.
         */
        /* 如果node->next不为空,那么我们只需要返回node->next即可 */
        if (node->next) {
            next = xchg(&node->next, NULL);
            if (next)
                break;
        }
        /* 
        为什么会出现node不是最后一个节点,但是node->next为空的情况呢?
        说明情况会突然让next指针变化呢??
        */
        arch_mutex_cpu_relax();
    }

    return next;
}

osq_unlock:将当前节点从链表中删除,并将锁传递给当前节点的下一个节点。按照代码这样写,我感觉锁持有者始终都是表头节点。因为摘除的时候只是修改next指针,说明没有人在它前面。这样这个锁的传递方式就先进先出了。

void osq_unlock(struct optimistic_spin_queue *lock)
{
    struct optimistic_spin_node *node, *next;
    int curr = encode_cpu(smp_processor_id());

    /*
     * Fast path for the uncontended case.
     */
    /*
    本来就是当前节点获得了锁,说明没有其他竞争锁,直接设置tail为OSQ_UNLOCKED_VAL
    */
    if (likely(atomic_cmpxchg(&lock->tail, curr, OSQ_UNLOCKED_VAL) == curr))
        return;

    /*
     * Second most likely case.
     */
    node = this_cpu_ptr(&osq_node);
    /* 将当前节点的next设置为空,并且返回下一节点 */
    next = xchg(&node->next, NULL);
    if (next) {//如果next不为空,则将locked设置为1,表示节点next获得了锁
        ACCESS_ONCE(next->locked) = 1;
        return;
    }
    /* 同样是找到当前节点的下一个节点,并且将锁传递给它 */
    next = osq_wait_next(lock, node, NULL);
    if (next)
        ACCESS_ONCE(next->locked) = 1;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值