1.2 内存屏障
1.2.1 概念理解
原理部分比较苦涩难懂,我们先不过多详细介绍这部分的由来和经过,接下来着重讲解什么用途和实现;
ARM64架构中提供了3条内存屏障指令。
- 数据存储屏障(Data Memory Barrier, DMB)指令。
- 数据同步屏障(Data Synchronization Barrier, DSB)指令。
- 指令同步屏障(Instruction Synchronization Barrier, ISB)指令。
学习过程中对这三条指令的含义还是有所困惑的,接下来介绍下:
数据存储屏障(Data Memory Barrier, DMB)指令: 仅当所有在它前面的存储器访问操作都执行完毕后,才提交在它后面的访问指令,DMB指令保证的是DMB指令之前的所有内存访问指令和DMB指令之后的所有内存访问指令的顺序。也就是说,DMB指令之后的内存访问不会被处理器重排到DMB指令前面。DMB指令不会保证内存访问指令在内存屏障指令之前必须完成,它仅仅保证内存屏障指令前后的内存访问指令的执行顺序。DMB指令仅仅影响内存访问指令、数据高速缓存指令,以及高速缓存管理指令等,并不会影响其他指令的顺序。
**数据同步屏障(Data Synchronization Barrier, DSB)指令:**比DMB指令要严格一些,仅当所有在它前面的访问指令都执行完毕后,才会执行在它后面的指令,即任何指令都要等待DSB指令前面的访问指令完成。位于此指令前的所有缓存,如分支预测和TLB维护操作需全部完成。
**指令同步屏障(Instruction Synchronization Barrier, ISB)指令:**比DMB指令和DSB指令严格,刷新流水线和预取缓冲区后,才会从高速缓存或者内存中预取ISB指令之后的指令。ISB指令通常用来保证上下文切换的效果,如ASID(address space ID)更改,TLB维护操作和C15寄存器的修改等。
Note: DMB和DSB指令可以带参数,后续有遇到我们再补充说明,感兴趣的也可自行查阅;
1.2.2 接口说明
在ARM64 Linux内核中实现内存屏障函数的代码如下:
<kernel/linux/linux-5.15.73/arch/arm64/include/asm/barrier.h>
#define isb() asm volatile("isb" : : : "memory")
#define dmb(opt) asm volatile("dmb " #opt : : : "memory")
#define dsb(opt) asm volatile("dsb " #opt : : : "memory")
#define mb() dsb(sy)
#define rmb() dsb(ld)
#define wmb() dsb(st)
#define dma_mb() dmb(osh)
#define dma_rmb() dmb(oshld)
#define dma_wmb() dmb(oshst)
1.2.3 linux中案例说明
例1:在一个网卡驱动中发送数据包。把网络数据包写入缓冲区后,由DMA引擎负责发送,wmb()函数保证在DMA传输之前,数据被完全写入缓冲区中。
<kernel/linux/linux-5.15.73/drivers/net/ethernet/realtek/8139too.c>
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]); // 写入TxStatus以触发DMA传输
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);
/*
* Writing to TxStatus triggers a DMA transfer of the data
* copied to tp->tx_buf[entry] above. Use a memory barrier
* to make sure that the device sees the updated data.
*/
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;
}
例2: Linux内核里面的睡眠和唤醒接口函数也运用了内存屏障指令,通常一个进程因为等待某些事件需要睡眠,如调用wait_event()函数。睡眠者的代码片段如下。
for (;;) {
set_current_state(TASKJJNINTERRUPTIBLE);
if (event_indicated)
break;
schedule();
/****************************************************************************/
#define set_current_state(state_value) \
do { \
debug_normal_state_change((state_value)); \
smp_store_mb(current->__state, (state_value)); \
} while (0)
set_current_state()函数在修改进程的状态时隐含插入了内存屏障函数smp_mb()。smp_store_mb函数最终调用的是mb();
唤醒者通常会调用wake_up()函数,它在修改task状态之前也隐含地插入内存屏障函数 smp_rmb()。
wake_up() —> __wake_up_common() -----> wq_entry->func—> autoremove_wake_function() —> try_to_wake_up() ------> smp_rmb()
- 睡眠者:CPUI在更改当前进程current->state后,插入一条内存屏障指令,保证加载唤醒标记load event_indicated不会出现在修改current->state之前。
- 唤醒者:CPU2在唤醒标记store操作和把进程状态修改成RUNNING的store操作之间插入写屏障,保证唤醒标记event indicated的修改能被其他CPU看到。
感谢学习,有什么问题可以评论区讨论学习。