了解SMMU产生背景之前,首先要了解DMA工作原理。因为SMMU的产生主要是为了解决虚拟化平台下的DMA重映射问题。
DMA,外设和内存的连接件,用于解放CPU。外设可以通过DMA,将搜集的数据批量传输到内存,然后再发送一个中断通知CPU去内存取。这样减少了CPU被中断的次数,提高了系统的效率。DMA要能够正常工作,首先要进行正确的配置(包括通道选择、DMA源外设地址、DMA目的内存地址及尺寸等信息);其次,一般需要连续的一段或多段物理内存。由于DMA不能像CPU一样通过MMU操作虚拟地址,所以DMA需要的是连续的物理地址。
这对Hypervisor+GuestOS的虚拟化系统来说必须解决一个问题:GuestOS看到的内存并非真实的物理内存,Guest OS的驱动无法正常地把连续的物理内存分配给硬件。于是,现代计算机设计了引入了IOMMU(ARM架构叫SMMU)架构,来对非CPU的外部设备,也提供一个MMU部件,对这些设备发出的地址请求进行翻译。DMA设备是其中最重要的应用。
SMMU其实就是一个MMU部件,只是它不供CPU用,而是供外设使用。它也提供虚拟地址的管理、映射和翻译工作。其通用流程如下图:
可以发现上图中有多处“Bypass”判断,因为其可以设置为全局Bypass,也可以设置为部分Bypass。因为整个翻译过程有多个步骤,所以可以设置多个Bypass选项。下图则结合具体寄存器,来更详细的说明这个过程:
SMMU_sCR0.CLIENTPD是client port disable的意思,如果设置为1,则相当于全局Bypass了。否则,则开始搜索stream mapping table(SMMU_SMRn寄存器)进行streamID的匹配。
异常情况:
正常情况下,只有一个匹配,则找到对应的SMMU_S2CRn寄存器进行后续判断处理(匹配的具体过程参照附录“stream mapping”)。
首先如果SMMU_S2CRn.TYPE=1,则表示Bypass,同时应用SMMU_S2CRn中设置的相关属性;而如果SMMU_S2CRn.TYPE=2,则表示没有有效的context可用,报错;默认情况下SMMU_S2CRn.TYPE=0,表示正常翻译。
接着会依据SMMU_CBARn.TYPE域来决定后续流程走向,其值与功能对照如下表:
0b00:Stage 2 context; ——① 0b01:Stage 1 context with stage 2 bypass ——① 0b10:Stage 1 context with stage 2 fault 0b11:Stage 1 followed by stage 2 translation context ——② |
上图说明一点:对于“Stage 2 context”情形,n=2;对于“Stage 1 context with stage 2 bypass”情形,n=1。
对“Stage 1 followed by stage 2 translation context”情形,只有两个stage的context都有效,才属于正常流程。这时,首先判断stage1的MMU翻译功能是否开启?
如果开启(SMMU_CBn_SCTLR.M=1),在判断stage2的MMU翻译功能是否开启?
如果开启,则会正常完成两个stage的翻译,获取结果。
如果没开,则stage1正常查表翻译,stage2采用默认值和属性,获取结果。
如果没开(SMMU_CBn_SCTLR.M=0),仍要判断stage2的MMU翻译功能是否开启?
如果开启,则stage1采用默认值和属性,stage2正常查表翻译,获取结果。
如果没开,则stage1、stage2均采用默认值和属性,获取结果。
注:图中上标——“a,b”,是因为stage1和stage2都有相同格式的SMMU_CBn_SCTLR寄存器。默认值为Bypass的值,默认属性从SMMU_CBn_SCTLR寄存器相应位域获取。
在fiasco内核中,设计了3个类来支持SMMU。其中,Dmar类向应用层提供bind和unbind接口,以便支持SMMU操作的设备能够启用SMMU能力。Io_mmu类主要用于初始化和配置SMMU设备,以便实现设备DMA地址重映射的能力。Dmar_space类为Io_mmu类提供了一个地址空间,以便UVMM能够配置DMA地址重映射所需的页表。
内核对smmu的初始化,主要通过静态类变量方式,在程序运行前就初始化好了。
入口为STATIC_INITIALIZE(Dmar),我们可以展开这个宏,翻译一下,就可以得到如下代码段:
class static_init_class_Dmar{ public: static_init_class_Dmar() FIASCO_INIT; }; static_init_class_Dmar::static_init_class_Dmar(){ Dmar::init(); } static static_init_class_Dmar __static_construction_of_class_Dmar__; |
也就是定义了一个“static_init_class_Dmar”类型的类,同时定义了一个静态实例——“__static_construction_of_class_Dmar__”,并在构造函数里调用Dmar::init()。下面,我们看一下Dmar::init()函数:
void Dmar::init() { Dmar_space::init(); //调用Dmar_space的init函数初始化——2
_glbl_iommu.construct(); //详见下面分析——1 initial_kobjects.register_obj(_glbl_iommu, Initial_kobjects::Iommu); //注册iommu能力权限 } |
1.我们先来看_glbl_iommu的定义:
static Static_object |
kernel/fiasco/src/types/types.h文件中
template< typename T > class Static_object { public: …… operator T * () const { return get(); } T *operator -> () const { return get(); }
T *construct() { return new (_i) T; }
template< typename... A > T *construct(A&&... args) { return new (_i) T(cxx::forward(args)...); }
private: mutable char __attribute__((aligned(sizeof(Mword)*2))) _i[sizeof(T)]; }; |
可以看出,其实就是在_glbl_iommu的私有变量“_i”中保存了一份Dmar的实例。
2.Dmar_space的init函数
void Dmar_space::init(unsigned max_did) { _max_did = max_did > (unsigned)Max_nr_did ? (unsigned)Max_nr_did : max_did; _free_dids = new Boot_object _initialized = true; } |
而_max_did、_free_dids、_initialized都是static型的,所以这里主要初始化了Dmar_space的静态变量。
内核定义了Startup类来初始化启动早期需要的串口、内存、时钟等资源:
class Startup { public: static void stage1(); static void stage2(); };
STATIC_INITIALIZEX_P(Startup, stage1, STARTUP1_INIT_PRIO); STATIC_INITIALIZEX_P(Startup, stage2, STARTUP_INIT_PRIO); |
“STATIC_INITIALIZEX_P”的定义跟前面“STATIC_INITIALIZE”的定义类似,也是定义了一个静态类,不过增加了“_P”后缀,展开后定义如下:
class static_init_class_Startup_func { public: static_init_class_Startup_func () FIASCO_INIT; }; static_init_class_Startup_func ::static_init_class_Startup_func () { Startup::stage2(); //这里也可以是Startup::stage1,看传入的func参数 } static static_init_class_Startup_func __static_construction_of_class_Startup_func__ \ __attribute__((init_priority(prio))) |
可以发现,这里增加了一个attribute属性——init_priority(prio)。
注:init_priority是GNU C++用于判断初始化顺序的一个属性,取值范围为101~65535,数值越小,优先级越高(越先初始化)。
在stage2()中会调用Platform_control::init()——》iommu_init()——》Io_mmu::init()
bool Io_mmu::init(Cpu_number cpu) { iommus.setup(cpu); //Setup函数——1 iommus.reset(cpu); //Reset函数——2
return true; } |
static Io_mmu iommus; //定义的静态变量iommus |
1.Setup函数
void Io_mmu::setup(Cpu_number cpu) { …… _version = ARM_SMMU_V2; //SMMU版本 _model = ARM_MMU500; //SMMU硬件架构
_base = Kmem::mmio_remap(Mem_layout::Mc_phys_base); //设置SMMU基地址
/* SMMU_IDR0~2,获取系统能力(包括stage1、2的支持情况、stream matching/indexing支持情况、寄存器空间占用大小、页大小配置及VA和IPA位宽等信息)*/ …… }
|
2.Reset函数
void Io_mmu::reset(Cpu_number cpu) { …… //具体代码略过 } |
Reset函数所做的动作包括:
Uvmm中对smmu的初始化主要包括两个方面:一是通过IPC调用内核相应的factory方法创建Dmar_space对象;二是映射DMA空间。如下,uvmm在启动Guest OS之前会调用Ram_ds::map_kern_dma_space来完成smmu相关初始化。
int Ram_ds::map_kern_dma_space(l4_mword_t reserve_size) { …… //获取Task的能力权限 _kern_dmar = ZCore::chkcap(ZCore::Util::cap_alloc.alloc
//创建Dmar_space内核对象 ZCore::chksys(ZCore::Env::env()->factory()->create(_kern_dmar, l4_PROTO_DMA_SPACE)); …… if(add_mem_region(local_map_addr, remote_map_addr, size) == ~0ul) //映射dma空间 return -1; return 0; } |
“ZCore::Env::env()->factory()->create(_kern_dmar, l4_PROTO_DMA_SPACE)”,就是通过IPC获取Task可用的工厂能力权限,然后调用其create函数创建对象。这里传入的msgtag为l4_PROTO_DMA_SPACE=-17L。
系统中通过“register_factory()”静态构造函数来收集Kobject_iface的工厂对象,类似于static类实例功能,在main函数执行之前就初始化好了。目前系统中有如下地方进行工厂注册:
kernel/fiasco/src/kern/arm/64/arm_dmar_space.cpp:728:register_factory() kernel/fiasco/src/kern/arm/bsp/jetsontx1/dmar_space.cpp:417:register_factory() kernel/fiasco/src/kern/arm/vm.cpp:246:register_factory() kernel/fiasco/src/kern/factory.cpp:183:register_factory() kernel/fiasco/src/kern/ia32/dmar_space.cpp:551:register_factory() kernel/fiasco/src/kern/ia32/vm_svm.cpp:741:register_factory() kernel/fiasco/src/kern/ipc_gate.cpp:431:register_factory() kernel/fiasco/src/kern/irq.cpp:933:register_factory() kernel/fiasco/src/kern/semaphore.cpp:234:register_factory() kernel/fiasco/src/kern/task.cpp:613:register_factory() kernel/fiasco/src/kern/thread_object.cpp:819:register_factory() kernel/fiasco/src/jdb/kinfo.cpp:213:register_factory() |
其中,“arm_dmar_space.cpp”文件中的“register_factory”定义如下:
static inline void __attribute__((constructor)) FIASCO_INIT register_factory() { Kobject_iface::set_factory(L4_msg_tag::Label_dma_space, //enum Protocol中定义=”-17L” &Task::generic_factory } |
注:gcc为函数提供了几种类型的属性,其中包含:构造函数(constructors)和析构函数(destructors)。
__attribute__((constructor)) // 在main函数被调用之前调用
__attribute__((destructor)) // 在main函数被调用之后调
所以,通过IPC会调用到内核“Task::generic_factory
template int UTCB_AREA_MR> TASK_TYPE * FIASCO_FLATTEN Task::create(Ram_quota *q, L4_msg_tag t, Utcb const *u, int *err) { …… typedef Kmem_slab_t *err = L4_err::ENomem; cxx::unique_ptr …… if (EXPECT_FALSE(!v->initialize())) //调用其initialize方法 return 0; …… return v.release(); //释放智能指针的控制权,返回指针 } |
initialize方法
bool Dmar_space::initialize() { …… //具体代码略过 } |
Initialize方法主要做了如下动作:
Uvmm通过add_mem_region函数对DMA空间进行映射。系统实现中传入了整个uvmm地址空间,并以4K为单位,循环调用l4::Task::map()方法来进行虚拟机的IPA地址和物理地址之间的映射。这一步完成了SMMU所需页表的构造过程。
UVMM中透传设备在create阶段会进行smmu 的绑定:
struct F : Factory { cxx::Ref_ptr Dt_node const &node) override { …… auto prop_start = node.get_prop if(prop_start) { auto src_id = node.get_prop_val(prop_start, prop_sz, 0); devs->ram()->bind_kern_iommu(src_id); } …… } |
int Ram_ds::bind_kern_iommu(l4_uint64_t src_id) { l4::Cap …… int r = l4_error(iommu->bind(src_id, _kern_dmar)); …… } |
namespace l4 { class Iommu : public Kobject_x { public: l4_INLINE_RPC( l4_msgtag_t, bind, (l4_uint64_t src_id, Ipc::Cap …… }; } |
#define l4_INLINE_RPC(res, name, args...) \ l4_INLINE_RPC_NF(res, name, args); l4::Ipc::Msg::Call |
#define l4_INLINE_RPC_NF(res, name, args...) \ struct name##_t : l4::Ipc::Msg::Rpc_inline_call { \ typedef Rpc_inline_call type; \ l4_INLINE_RPC_SRV_FORWARD(name); \ } |
#define l4_INLINE_RPC_SRV_FORWARD(name) \ template { \ OBJ *o; \ fwd(OBJ *o) : o(o) {} \ template { return o->op_##name(a...); } \ } |
可以发现,最终它会调用op_bind函数进行绑定,对应到内核对象就是:Dmar::op_bind
L4_msg_tag Dmar::op_bind(Ko::Rights, Unsigned64 src_id, Ko::Cap { …… iommu->set_context_entry(src_id, space.obj); //初始化一个CB相关寄存器 …… } |
这里会调用set_context_entry函数来初始化一个CB相关寄存器,其主要步骤为:
注:一个stream match组为一个SMMU_SMRn寄存器和一个SMMU_S2CRn寄存器,其中n相等。如SMMU_SMR1和SMMU_S2CR1为一组。
上面第5步——“初始化此CB的相关寄存器”可以细分如下:
Stream mapping
如何通过streamID找到对应的CB。这里有两种模式:
Stream matching:
利用StreamID在 Stream Match registers(SMMU_SMRn)中查找,如果找到唯一的匹配(假设是SMMU_SMRx),则对应的 Stream-to-Context register(SMMU_S2CRx)中保存的CB index就是要找的CB。
Stream indexing:
如果Stream indexing被使用,则StreamID直接指示SMMU_S2CRn的索引。比如,StreamID=m,则对应的CB index保存在SMMU_S2CRm中。
以上两种模式的支持情况可以从SMMU_IDR0.SMS位域读取。
上图中SMMU_SMRn、SMMU_S2CRn及SMMU_CBARn一一对应,配对成组。类似于cpu的mmu,Translation context bank中的TTBR0和TTBR1分别指向高低2段虚拟地址空间的页表。自然的,stage2的context bank没有TTBR1属性。
另外,共享地址空间的设备属于同一个Domain。不同StreamID可以通过映射到同一Translation context bank来共享地址空间,即实现Domain的概念,因而可使用Translation context bank在CBT中的index作为Domian ID。