smmu 除了完成 iommu 的統一的ops 之外,有自己獨特的一些地方。
1、Stream Table
Stream Table是存在內存中的一張表,在SMMU設備初始化的時候由驅動程序創建好。
Stream Table支持2種格式,Linear Stream Table 和 2-level Stream Table, Linear Stream Table就是將整個Stream Table在內存中線性展開為一個數組,
優點是索引方便快捷,缺點是當平台上外設較少的時候浪費連續的內存空間。 2-level Stream Table則是將Stream Table拆成2級去索引,優點是更加節省內存。
但是請注意的是,Stream Table 的2-level的構造和 iommu支持的兩個階段的翻譯不是一個概念,
SMMU支持2階段地址翻譯,這和內存虛擬化場景下MMU支持2階段地址翻譯類似, 第一階段的地址翻譯被用做進程(software entity)之間的隔離或者OS內的DMA隔離,
第二階段的地址翻譯被用來做DMA重映射,即將Guest發起的DMA映射到Guest的地址空間內。
第一階段主要處理的是VA到IPA,第二階段是IPA到PA
2、Stream Table的查找屬於smmu的第一階段查找么?
答案是肯定的,主要從看的角度來說。
我們經常表述的SMMU支持兩階段地址翻譯,是指va到gpa為第一階段,而gpa到hpa是第二階段,
而stream table的查找是 屬於前置查找,當然它可以實現為線性的,也可以實現為 2-level查找。
從術語上說,它都不屬於前面所說的一階段查找的一部分,但是它的結果又決定了stage1的行為。
3、Stream Table 是怎么申請內存的?
smmuv3對Stream Table申請內存的實現如下:
static int arm_smmu_init_strtab(struct arm_smmu_device *smmu)
{
u64 reg;
int ret;
if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)//二級表模式,類似於二級mmu頁表來理解
ret = arm_smmu_init_strtab_2lvl(smmu);
else
ret = arm_smmu_init_strtab_linear(smmu);//否則是線性模式,所謂線性就是可以理解為一維數組
....
如果是線性查找,則
static int arm_smmu_init_strtab_linear(struct arm_smmu_device *smmu)
{
void *strtab;
u64 reg;
u32 size;
struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
size = (1 << smmu->sid_bits) * (STRTAB_STE_DWORDS << 3);//caq:ste為64字節,8<<3為64
strtab = dmam_alloc_coherent(smmu->dev, size, &cfg->strtab_dma,
GFP_KERNEL | __GFP_ZERO);//條目數*64,然后申請內存,**注意這里是一次性申請完**,和二級不一樣
if (!strtab) {
dev_err(smmu->dev,
"failed to allocate linear stream table (%u bytes)\n",
size);
return -ENOMEM;
}
cfg->strtab = strtab;//caq:申請的內存地址記錄下來,stream table的基地址
cfg->num_l1_ents = 1 << smmu->sid_bits;//條目數
/* Configure strtab_base_cfg for a linear table covering all SIDs */
reg = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_LINEAR);//caq:配置stream table(STRTAB_BASE_CFG)的log2size, ste的entry數目是2 ^ log2size
reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, smmu->sid_bits);
cfg->strtab_base_cfg = reg;//
arm_smmu_init_bypass_stes(strtab, cfg->num_l1_ents);
return 0;
}
如果是2-level查找,則
//caq:二級表的初始化,查找的索引為SID,也就是streamid
static int arm_smmu_init_strtab_2lvl(struct arm_smmu_device *smmu)
{
void *strtab;
u64 reg;
u32 size, l1size;
struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
/* Calculate the L1 size, capped to the SIDSIZE. */
size = STRTAB_L1_SZ_SHIFT - (ilog2(STRTAB_L1_DESC_DWORDS) + 3);
size = min(size, smmu->sid_bits - STRTAB_SPLIT);
cfg->num_l1_ents = 1 << size;//caq:計算有多少一級條目
size += STRTAB_SPLIT;
if (size < smmu->sid_bits)
dev_warn(smmu->dev,
"2-level strtab only covers %u/%u bits of SID\n",
size, smmu->sid_bits);
l1size = cfg->num_l1_ents * (STRTAB_L1_DESC_DWORDS << 3);//一級表*8,就是一個指針大小
strtab = dmam_alloc_coherent(smmu->dev, l1size, &cfg->strtab_dma,
GFP_KERNEL | __GFP_ZERO);//caq:先申請一級表大小,請注意,看起來二級表是通過pagefault
if (!strtab) {
dev_err(smmu->dev,
"failed to allocate l1 stream table (%u bytes)\n",
size);
return -ENOMEM;
}
cfg->strtab = strtab;//caq:記錄一級表位置,也就是stream table的基地址,是一個指針數組
/* Configure strtab_base_cfg for 2 levels */
reg = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_2LVL);//caq:fmt為2level
reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, size);
reg |= FIELD_PREP(STRTAB_BASE_CFG_SPLIT, STRTAB_SPLIT);
cfg->strtab_base_cfg = reg;//caq:要按照二級查找的模式構造這個值,最終會將這個值刷到對應的寄存器
return arm_smmu_init_l1_strtab(smmu);
}
那么,streamtable什么時候使用線性查找,什么時候使用二階查找呢?
#define IDR1_SSIDSIZE GENMASK(10, 6)//硬件支持substreamID的bit數,0表示不支持substreamid
#define IDR1_SIDSIZE GENMASK(5, 0)//硬件支持streamID的bit數,0表示支持一個stream
/* SID/SSID sizes */
smmu->ssid_bits = FIELD_GET(IDR1_SSIDSIZE, reg);//caq:從硬件上支持的位數
smmu->sid_bits = FIELD_GET(IDR1_SIDSIZE, reg);//caq:從bit看支持64,但spec里面就是20bit,
/*
* If the SMMU supports fewer bits than would fill a single L2 stream
* table, use a linear table instead.
*/
if (smmu->sid_bits <= STRTAB_SPLIT)//caq:如果小於8bit,則沒必要用二階表
smmu->features &= ~ARM_SMMU_FEAT_2_LVL_STRTAB;//caq:直接用線性表
#define STRTAB_L1_SZ_SHIFT 20//caq:20位的streamid,按照高 STRTAB_SPLIT 位來拆分
#define STRTAB_SPLIT 8//caq:20位的streamid,高8位用來查找ste,低12位用來查找二層真正的ste entry
如果硬件支持的streamid的bit小於8,則只使用線性查找。
3、streamid,substreamid到底代表啥?
一個smmu可以有多個設備連着,他們的頁表除非共用,否則肯定不一樣,SMMU 用stream id作區分,注意,此時區分的是設備。
【StreamID去索引Stream Table中的STE(Stream Table Entry)】
一個設備有多個進程會使用,所以smmu單元也要支持多頁表,smmu使用substream id區分多進程的io頁表。
同樣x86上也有類似的區分機制,不同的是x86是使用Request ID來區分的,Request ID默認是PCI設備分配到的BDF號。
看SMMUv3 Spec,又有說明:對於PCI設備StreamID就是PCI設備的RequestID, 兩者其實就是同一個東西。
只是一個是從SMMU的角度去看就成為StreamID,從PCIe的角度去看就稱之為RequestID。但arm內有很多platform類設備
這類設備沒有bdf號,所以arm抽象成StreamID來區分設備也比較合理。
同時,一個設備可能被多個進程使用,
多個進程有多個頁表,設備需要對其進行區分,SMMU使用SubstreamID來對其進行表示。 SubstreamID的概念和PCIe PASID是等效的,
這只不過又是在ARM上的另外一種稱呼而已。 SubstreamID最大支持20bit和PCIe PASID的最大寬度是一致的。
設備master發起一筆DMA請求,請求的總線信息中帶有streamid、subtreamid、iova(也就是大家理解的address)等參數,
smmu硬件收到該請求會自動通過streamid檢索到STE表(目前我們N2是采用二級STE表),再通過substreamid檢索到這個STE表指向的CD表
(就是context description表),從CD表中找到頁表基地址,開始頁表查找過程,找到IOVA對應的物理地址,再對這個物理地址發起一次DMA請求。
由於smmu是為master服務的,可以稱其為秘書,這個master可以是一個pcie的設備,也可能是其他設備。
todo。。。。。。
同X86上一樣,ARM上的設備直通關鍵也是要解決DMA重映射和直通設備中斷投遞的問題。 但和X86上不一樣的是,
ARMv8-A上使用的是SMMU v3.1來處理設備的DMA重映射,這個在smmuv3的驅動代碼就能看出,但是中斷的話,
由於arm進入服務器領域較晚,所以arm迭代的支持虛擬化的中斷是在GICv3中斷控制器來完成的,
並沒有像x86那樣使用一個 **虛擬的 irq_chip **來完成。
SMMUv3和GICv3在設計的時候考慮了更多跟虛擬化相關的實現, 針對虛擬化場景有一定的改進和優化。