教你面试Linux内核秒杀通过,并发与同步8大核心技术维度

428 阅读6分钟

常见高频率Linux内核面试题?

  1. 为什么自旋锁的临界区不能睡眠?

  2. ARM 64处理器当中,我们如何实现独占访问内存?

  3. 排队自旋锁是如何实现MCS锁的?

  4. 乐观自旋锁等待的判断条件是什么?

  5. 请你说出MCS锁机制的实现原理?

  • 临界区(critical region)是指访问和操作共享数据的代码段,其中的资源无法同时被多个执行线程访问,访问临界区的执行线程或代码路径称为并发源。我们为了避免并发访问临界区,软件工程师必须保证访问临界区的原子性,即在临界区内不能有多个并发源同时执行,整个临界区就像一个不可分割的整体。

  • 在Linux内核当中产生访问的并发源主要有哪些:中断和异常、内核抢占、多处理器并发执行、软中断和tasklet。

  • 考虑SMP系统:

  1. 同一类型的中断处理程序不会并发执行,但是不同类型的中断可能送达不同的CPU,因此不同类型的中断处理程序可能会并发执行。

  2. 同一类型的软中断会在不同的CPU上并发执行。

  3. 同一类型的tasklet是串行执行的,不会在多个CPU上并发执行。

  4. 不同CPU上的进程上下文会并发执行。

  • 我们要保护资源或数据,而不是保护代码,保护对象(缓存、链表、红黑树、全局变量等等)。Linux内核提供多种并发访问保护机制(原子操作、自旋锁、信号量、互斥锁、读写锁、RCU等)。

  • 我们Linux内核开发工程师考虑清楚什么哪些地方为临界区,我们使用什么样的机制来保护这些临界区。

原文地址:教你面试Linux内核秒杀通过,并发与同步8大核心技术维度 - 知乎 (zhihu.com)

一、原子操作与内存屏障

1、原子操作

  • 是指保证指令以原子的方式执行,执行过程中不会被打断。假设有两个线程threada_func和threadb_func,他们都会尝试进行i++操作,请问threada_func和threadb_func执行完成之后,i的值是多少???

    static int i=0;

    线程函数threada_func

    void threada_func(){

    i++;

    }

    线程函数threadb_func

    void threadb_func(){

    i++;

    }

  • 有的读者可能认为是2,但也有可能不是2,代码过程流程如下:

    CPU0 CPU1

    threada_func

    load i=0 threadb_func

    load i=0

    i++

    i++

    store i (i=1)

    store i (i=1)

  • 如果我们使用自旋锁来保证i++操作的原子性,导致开销比较大。内核提供atomic_t类型的原子变量。atomic_t类型在内核源码定义如下:

  • Linux内核提供多种原子变量操作的函数如下:

  • a.基本原子操作函数(atomic_read/atomic_set)

  • 2个原子操作函数直接调用上面2个宏来实现,不包括(读-修书-回写)机制,直接使用上面函数易容引发并发访问。

  • b.不带返回值的原子操作函数

2、内存屏障

  • ARM架构中3条内存屏障指令:
  1. 数据存储屏障指令(Data Memory Barrier,DMB)

  2. 数据同步屏障指令(Data Synchronization Barrier,DSB)

  3. 指令同步屏障指令 (Instruction Synchronization,ISB)

  • Linux内核内存屏障常用函数如下:
  1. barrier()------>编译优化屏障,阻止编译器为性能优化而进行指令重排

  2. mb()------>内存屏障(包括读和写),用于SMP

  3. rmb()------>读内存屏障,用于SMP

  4. wmb()------>写内存屏障,用于SMP

  5. smp_rmb()------>用于SMP的读写内存屏障

  6. smp_wmb()------>用于SMP的写内存屏障

  • smp_mb_before_atomic/smp_mb_after_atomic------>用于在原子操作中插入一个通用内存屏障

  • ARM64 Linux内核实现内存屏障具体源码如下:

  • 例如:在一个网络接口卡驱动中发送数据包。把网络数据信息包写入缓冲区后,由DMA引擎负责发送,wmb() 函数保存在DMA传输之前,数据被完全写入缓冲区当中。

    static netdev_tx_t rtl8139_start_xmit (struct sk_buff *skb,

    struct net_device *dev)

    {

    struct rtl8139_private *tp = netdev_priv(dev);

    void __iomem *ioaddr = tp->mmio_addr;

    unsigned int entry;

    unsigned int len = skb->len;

    unsigned long flags;

    /* Calculate the next Tx descriptor entry. */

    entry = tp->cur_tx % NUM_TX_DESC;

    /* Note: the chip doesn't have auto-pad! */

    if (likely(len < TX_BUF_SIZE)) {

    if (len < ETH_ZLEN)

    memset(tp->tx_buf[entry], 0, ETH_ZLEN);

    skb_copy_and_csum_dev(skb, tp->tx_buf[entry]);

    dev_kfree_skb_any(skb);

    } else {

    dev_kfree_skb_any(skb);

    dev->stats.tx_dropped++;

    return NETDEV_TX_OK;

    }

    spin_lock_irqsave(&tp->lock, flags);

    /*

    写入TxStatus以触发DMA传输,使用一条内存屏障指令确保设备可以看到这些更新后的数据

    */

    wmb();

    RTL_W32_F (TxStatus0 + (entry * sizeof (u32)),

    tp->tx_flag | max(len, (unsigned int)ETH_ZLEN));

    tp->cur_tx++;

    if ((tp->cur_tx - NUM_TX_DESC) == tp->dirty_tx)

    netif_stop_queue (dev);

    spin_unlock_irqrestore(&tp->lock, flags);

    netif_dbg(tp, tx_queued, dev, "Queued Tx packet size %u to slot %d\n",

    len, entry);

    return NETDEV_TX_OK;

    }

  • Linux内核睡眠和唤醒接口函数使用内存屏障:

    // 通过set_current_state函数在修改进程的状态时隐含插入内存屏障函数smp_mb

    #define set_current_state(state_value) \

    smp_store_mb(current->state, (state_value))

    #ifndef __smp_store_mb

    #define __smp_store_mb(var, value) do { WRITE_ONCE(var, value); __smp_mb(); } while (0)

    #endif

  • 在SMP观察睡眠者和唤醒者之间关系如下:

    CPU1 CPU2

    set_current_state STORE event_indicate

    wake_up()

    STORE current->state

    STORE current->state

    LOAD event_indicated

    if(event_indicate)

    break;

  • 睡眠者:CPU1在更新当前进程current->state后,插入一条内存屏障指令,保存加载唤醒标记LOAD event_indicated不会出现在修改current->state之前。

  • 唤醒者:CPU2在唤醒标记store操作和把进程状态改成RUNNING的sotre操作之间插入写屏障,保证唤醒记录event_indicate的修改能被其他CPU看到。

  • 内存屏障扩展API函数,内核里面提供一个自旋等待接口函数,它在排队自旋锁机制的实现中广泛应用。

    /**

    • smp_cond_load_relaxed() - (Spin) wait for cond with no ordering guarantees

    • @ptr: pointer to the variable to wait on

    • @cond: boolean expression to wait for

    • Equivalent to using READ_ONCE() on the condition variable.

    • Due to C lacking lambda expressions we load the value of *ptr into a

    • pre-named variable @VAL to be used in @cond.

    */

    // ptr表示要加载的地址,cond_expr表示判断条件,这个函数一直原子地加载并判断条件是否成立

    #ifndef smp_cond_load_relaxed // 执行完成之后插入一条加载获取内存屏障指令

    #define smp_cond_load_relaxed(ptr, cond_expr) ({ \

    typeof(ptr) __PTR = (ptr); \

    typeof(*ptr) VAL; \

    for (;;) { \

    VAL = READ_ONCE(*__PTR); \

    if (cond_expr) \

    break; \

    cpu_relax(); \

    } \

    VAL; \

    })

    #endif

    /**

    • smp_cond_load_acquire() - (Spin) wait for cond with ACQUIRE ordering

    • @ptr: pointer to the variable to wait on

    • @cond: boolean expression to wait for

    • Equivalent to using smp_load_acquire() on the condition variable but employs

    • the control dependency of the wait to reduce the barrier on many platforms.

    */

    #ifndef smp_cond_load_acquire // 没有隐含任何的内存屏障指令

    #define smp_cond_load_acquire(ptr, cond_expr) ({ \

    typeof(*ptr) _val; \

    _val = smp_cond_load_relaxed(ptr, cond_expr); \

    smp_acquire__after_ctrl_dep(); \

    _val; \

    })

    #endif

二、自旋锁与MCS锁

1、自旋锁

  • 自旋锁(spinlock)是Linux内核中最常见的锁机制,自旋锁特性如下:
  1. 同一时刻只能有一个内核代码路径可以获得这个锁;

  2. 自旋锁可以在中断上下文中使用;

  3. 要求自旋锁持有者尽快完成临界区的执行任务;

  4. 忙等待的锁机制

  • 自旋锁实现对应数据结构源码如下:

typedef struct spinlock {

union {

struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC

# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))

struct {

u8 __padding[LOCK_PADSIZE];

struct lockdep_map dep_map;

};

#endif

};

} spinlock_t;

typedef struct raw_spinlock {

arch_spinlock_t raw_lock;

#ifdef CONFIG_DEBUG_SPINLOCK

unsigned int magic, owner_cpu;

void *owner;

#endif

#ifdef CONFIG_DEBUG_LOCK_ALLOC

struct lockdep_map dep_map;

#endif

} raw_spinlock_t;