赞
踩
原理部分比较苦涩难懂,我们先不过多详细介绍这部分的由来和经过,接下来着重讲解什么用途和实现;
ARM64架构中提供了3条内存屏障指令。
学习过程中对这三条指令的含义还是有所困惑的,接下来介绍下:
数据存储屏障(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指令可以带参数,后续有遇到我们再补充说明,感兴趣的也可自行查阅;
在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:在一个网卡驱动中发送数据包。把网络数据包写入缓冲区后,由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()
感谢学习,有什么问题可以评论区讨论学习。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。