linux內核筆記之SMMU代碼分析

2020/06/10: first version, 主要介紹smmu驅動的初始化流程

在前一篇博文ARM SMMU學習筆記中, 介紹了SMMU的一些基本概念以及SMMU地址轉換的基本流程,本文主要分析linux kernel中SMMUv3的代碼(drivers/iommu/arm-smmu-v3.c)
linux kernel版本是linux 5.7, 體系結構是aarch64

smmu的位置

在這裏插入圖片描述
SMMU的作用是把CPU提交給設備的VA地址,直接作爲設備發出的地址,變成正確的物理地址,訪問到物理內存上。
和mmu不同的是,一個smmu可以有多個設備連着,他們的頁表不可能複用,SMMU 用stream id作區分。
一個設備有多個進程,所以smmu單元也要支持多頁表,smmu使用substream id區分多進程的頁表。

smmu的設備節點定義

在討論smmu的代碼前,先看下smmu的設備節點是怎麼定義的:
Example:

smmu@2b400000 {
                compatible = "arm,smmu-v3";
                reg = <0x0 0x2b400000 0x0 0x20000>;
                interrupts = <GIC_SPI 74 IRQ_TYPE_EDGE_RISING>,
                             <GIC_SPI 75 IRQ_TYPE_EDGE_RISING>,
                             <GIC_SPI 77 IRQ_TYPE_EDGE_RISING>,
                             <GIC_SPI 79 IRQ_TYPE_EDGE_RISING>;
                interrupt-names = "eventq", "priq", "cmdq-sync", "gerror";
                dma-coherent;
                #iommu-cells = <1>;
                msi-parent = <&its 0xff0000>;
        };

compatible: 用於匹配smmu驅動。
reg:smmu設備的物理基地址。
interrupts: 描述與中斷名稱對應的smmu中斷源,上述分別對應中斷類型,中斷號以及中斷觸發方式。
interrupt-names: 中斷名稱。
eventq,當event queue從空變爲非空狀態時上報中斷。
priq, 當pri queue從空變爲非空狀態時上報中斷。
cmdq-sync, command queue中CMDQ_SYNC命令完成時產生中斷。
gerror,event記錄到event queue過程中產生的錯誤會記錄在SMMU_GERROR寄存器中,併產生中斷。
combined,組合中斷,需要硬件支持,如果提供了組合中斷,則將優先使用組合中斷。
dma-coherent:表示設備通過smmu進行的DMA訪問是否cache coherent的,假設DMA把外設的數據搬運到內存的某個位置,cpu去讀那段地址,因爲cache命中了,讀到的還是舊的值,這就是cache的不coherent。
#iommu-cells: 一個cell代表一個streamid, smmu-v3必須定義爲1。
msi-parent:指定msi中斷控制器。

SMMU結構體

struct arm_smmu_domain {
	struct arm_smmu_device		*smmu;
	struct mutex			init_mutex; /* Protects smmu pointer */

	struct io_pgtable_ops		*pgtbl_ops;
	bool				non_strict;
	atomic_t			nr_ats_masters;

	enum arm_smmu_domain_stage	stage;
	union {
		struct arm_smmu_s1_cfg	s1_cfg;
		struct arm_smmu_s2_cfg	s2_cfg;
	};

	struct iommu_domain		domain;

	struct list_head		devices;
	spinlock_t			devices_lock;
};

arm_smmu_device: 指定smmu設備
io_pgtable_ops: io頁表映射定義的一系列操作
non_strict: smmu non-strict模式,在該補丁集中引入 add non-strict mode support for arm-smmu-v3
主要是爲了解決開啓smmu後,頻繁的unmap,需要頻繁的invalid tlb帶來的性能損失, 所以不在每一次unmap後都進行tlb invalidate操作,而是累計一定次數或者時間後執行invalid all操作,但這樣是有一定的安全風險(頁表雖然釋放了但是還是在tlb中有殘留,可能被利用到)。可以通過啓動參數控制。
nr_ats_masters: ats的設備數量,enable_ats時數量+1, disable ats時數量減1
arm_smmu_domain_stage: 代表smmu支持的方式,支持stage1的轉換,stage2的轉換,stage1 + stage2的轉換,以及bypass模式。
arm_smmu_s1_cfg: stage1轉換需要的數據結構
arm_smmu_s2_cfg: stage2轉換需要的數據結構

smmu驅動初始化

從smmu驅動的probe函數開始分析

+->arm_smmu_device_probe() //smmu設備驅動probe入口函數
	+-> arm_smmu_device_dt_probe() //smmu設備樹解析
	+-> platform_get_irq_byname() // smmu設備中斷解析
	+-> arm_smmu_device_hw_probe() // smmu硬件規格探測
	+-> arm_smmu_init_structures() //smmu 數據結構初始化
	+-> arm_smmu_device_reset() // smmu設備復位, 硬件初始化配置
	+-> iommu_device_register() // iommu設備註冊
	+-> arm_smmu_set_bus_ops() // 給支持的總線設置bus->iommu_ops

對probe中調用的這些函數進行詳細分析
(1)arm_smmu_device_dt_probe

static int arm_smmu_device_dt_probe(struct platform_device *pdev,
				    struct arm_smmu_device *smmu)
{
	int ret = -EINVAL;
	if (of_property_read_u32(dev->of_node, "#iommu-cells", &cells))   ---- (a)
		dev_err(dev, "missing #iommu-cells property\n");
	else if (cells != 1)
		dev_err(dev, "invalid #iommu-cells value (%d)\n", cells);
	else
		ret = 0;
		
	parse_driver_options(smmu);          ----- (b)
	
	if (of_dma_is_coherent(dev->of_node))         ---- (c)
		smmu->features |= ARM_SMMU_FEAT_COHERENCY;

	return ret;
}

a. 讀取設備樹,看smmu的設備節點定義中#iommu-cells是否爲1, 如果不爲1則直接bypass 掉smmu

b. parse_driver_options, 主要解析smmu是否有需要規避的硬件bug

c. 解析smmu設備中的dma-coherent屬性

(2) platform_get_irq_byname

/* Interrupt lines */

	irq = platform_get_irq_byname_optional(pdev, "combined");  
	if (irq > 0)
		smmu->combined_irq = irq;
	else {
		irq = platform_get_irq_byname_optional(pdev, "eventq"); 
		if (irq > 0)
			smmu->evtq.q.irq = irq;

		irq = platform_get_irq_byname_optional(pdev, "priq");
		if (irq > 0)
			smmu->priq.q.irq = irq;

		irq = platform_get_irq_byname_optional(pdev, "gerror");
		if (irq > 0)
			smmu->gerr_irq = irq;
	}

分別獲取dts節點中定義的"combined", “eventq”, “priq”, "gerror"中斷號

(3) arm_smmu_device_hw_probe
該函數主要探測smmu設備的硬件規格,主要是通過讀SMMU的IDR0,IDR1,IDR5寄存器確認
SMMU_IDR0:

域段 offset 作用
ST_LEVEL 28: 27 確認stream table格式是線性table還是2-level table
TERM_MODEL 26 fault的處理方式,
STALL_MODEL 25: 24 確認是否是stall mode。 該模式下smmu會暫停引發stall的transaction, 然後stall,之後根據軟件的commad是resume還是stall_term來決定stall命令是retry還是terminate。當前只允許4種fault類型被stall: F_TRANSLATION, F_ACCESS, F_ADDR_SIZE. F_PERMISSION.【spec 5.5 Fault configuration (A,R,S bits)】
TTENDIAN 22: 21 確認traslation table支持的大小端模式
CD2L 19 確認是否支持2-level 的CD table
VMW 17 用於控制vmid wildcards的功能和範圍。 vmid wildcard, vmid的模糊匹配,是爲了方便tlb無效化, 兩種tlb無效化的方式:command和廣播tlb無效都會使用到vmid wildcards
VMID16 18 確認是否支持16bit VMID。 每一個虛擬機都被分配一個ID號,這個ID號用於標記某個特定的TLB項屬於哪一個VM。每一個VM有它自己的ASID空間。例如兩個不同的VMs同時使用ASID 5,但指的是不同的東西。對於虛擬機而言,通常VMID會結合ASID同時使用。
PRI 16 確認是否支持page request interface。 屬於pcie 硬件特性,PCIe設備可以發出缺頁請求,SMMU硬件在解析到缺頁請求後可以直接將缺頁請求寫入PRI queueu, 軟件在建立好頁表後,可以通過CMD queue發送PRI response給PCIe設備。 [Linux SVA特性分析]
SEV 14 確認是否支持WFE wake-up事件的產生。 當SEV == 1時,command queue從滿到非滿狀態會觸發WFE wake-up event。 此外,當CMD_SYNC完成且要求SIG_SEV時也會產生WFE wake-up event
MSI 13 確認是否支持MSI消息中斷
ASID16 12 確認是否支持16bit ASID.。 在TLB的表項中,每個項都有一個ASID,當切換進程的時候,TLB中可以存在多個進程的頁表項, 不再需要清空TLB,因爲B進程用不了裏面A進程的頁表項,可以帶來性能提升。[TLB flush操作]
ATS 10 Address Translation Service, 也是pcie的硬件特性,有ATS能力的PCIE,自帶地址翻譯功能,如果它要發出一個地址,進入PCIE總線的時候,一定程度上可以認爲就是物理地址。ats會在設備中緩存va對應的pa, 設備隨後使用pa做內存訪問時無需經過SMMU頁錶轉換,可以提高性能。 【PCIe/SMMU ATS analysis note
HTTU 7:6 Hardware Translation Table Update,在訪問或寫入相關頁面後,硬件自動更新相關頁面的Access flag、Dirty state。該特性和armv8.1的tthm特性一樣,在沒有tthm 特性之前,軟件去更新頁表的young和dirty page, 會有一定的開銷。
S1P 1 確認是否支持stage1轉換,va->pa
S2P 0 確認是否支持stage2轉換,ipa->pa

SMMU_IDR1:

域段 offset 作用
TABLES_PRESET 30 確認smmu_strtab_base和smmu_strtab_base_cfg的基地址是否固定, smmu_strtab_base是stream table的物理基地址,smmu_strtab_base_cfg是stream table的配置寄存器
QUEUES_PRESET 29 確認smmu queue的基地址是否固定,queue指的是smmu event queue, smmu cmd queue, smmu pri queue
CMDQS 25:21 cmd queue 最大entry數目, 等於log2(entries), 最大是19
EVENTQS 20:16 event queue的最大entry數目,等於log2(entries), 最大是19
PRIQS 15:11 pri queue 最大entry數目,等於log2(entries), 最大是19
SSIDSIZE 10:6 確認硬件支持substreamID的bit數,範圍爲【0,20】, 0表示不支持substreamid
SIDSIZE 5:0 確認硬件支持streamID的bit數,範圍爲【0,32】, 0表示支持一個stream

IDR1主要用來設置smmu 各個queue的entry數量, 設置ssid和sid的size.

SMMU_IDR5:

域段 offset 作用
STALL_MAX 31:16 smmu支持的最大未完成stall 事務數
VAX 11:10 表示smmu支持的va地址是48bit還是52bit
GRAN64K 6 支持64KB翻譯粒度, Translation Granule表示translation table的size大小, 頁表粒度是smmu管理的最小地址空間
GRAN16K 5 支持16KB翻譯粒度
GRAN4K 4 支持4KB翻譯粒度
OAS 2:0 表示output address size, 32bit ~ 52bit

IDR5主要設置smmu ias(input address size) 和 oas (output address size), ias代表IPA,oas代表PA。

(4)arm_smmu_init_structures()
smmu相關的數據結構的內存申請和初始化

static int arm_smmu_init_structures(struct arm_smmu_device *smmu)
{
	int ret;

	ret = arm_smmu_init_queues(smmu); ----------------- (a)
	if (ret)
		return ret;

	return arm_smmu_init_strtab(smmu);  ----------------- (b)
}

(a) arm_smmu_init_queues() 會初始化三個queue, 分別爲cmd queue, event queue, pri queue.
SMMU使用這3個隊列做基本的事件管理。
event queue用於記錄軟件配置錯誤的狀態信息,smmu將配置錯誤信息記錄到event queue中,軟件會通過從event queue讀取配置錯誤信息,然後進行相應的配置錯誤處理。
軟件使用command queue和smmu 硬件進行交互,軟件寫命令發送到command queue中,smmu會從command queue中讀取命令進行處理。
pri queue需要硬件支持pri 特性,和event queue類似,當有相應硬件事件發生時,硬件把相應的描述符寫入pri queue, 然後上報中斷。

(b) arm_smmu_init_strtab

static int arm_smmu_init_strtab(struct arm_smmu_device *smmu)
{
	u64 reg;
	int ret;

	if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)  
		ret = arm_smmu_init_strtab_2lvl(smmu);
	else
		ret = arm_smmu_init_strtab_linear(smmu);

	if (ret)
		return ret;

	/* Set the strtab base address */
	reg  = smmu->strtab_cfg.strtab_dma & STRTAB_BASE_ADDR_MASK; 
	reg |= STRTAB_BASE_RA;
	smmu->strtab_cfg.strtab_base = reg;

	/* Allocate the first VMID for stage-2 bypass STEs */
	set_bit(0, smmu->vmid_map);
	return 0;
}

首先確認SMMU的stream table的組織方式是線性table還是2-level table.
如果是linear table:
在這裏插入圖片描述
使用STRTAB_BASE + sid * 64(一個STE的大小爲64B)找到STE

+-> arm_smmu_init_strtab_linear
	// 計算stream table的size, 如果使用linear 查找,stream table的size = sid * 64(sid表示有多少個ste, 一個STE的大小爲64B)
	+-> size = (1 << smmu->sid_bits) * (STRTAB_STE_DWORDS << 3); 
	// 申請Stream table的內存
	+-> strtab = dmam_alloc_coherent()
	// 配置stream table(STRTAB_BASE_CFG)的format, 決定stream table的格式是linear
	+-> reg  = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_LINEAR);
	// 配置stream table(STRTAB_BASE_CFG)的log2size, ste的entry數目是2 ^ log2size
	+-> reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, smmu->sid_bits);  
	// cfg->num_l1_ents對應的是sid, 對SMMU下的所有sid逐一調用arm_smmu_write_strtab_ent
	+-> arm_smmu_init_bypass_stes(strtab, cfg->num_l1_ents) 
		+-> arm_smmu_write_strtab_ent()
			// 發送CMDQ_OP_PREFETCH_CFG
			+-> arm_smmu_cmdq_issue_cmd()

如果是2-level table:在這裏插入圖片描述
先通過sid的高位找到L1_STD(STRTAB_BASE + sid[9:8] * 8, 一個L1_STD的大小爲8B), L1_STD定義了下一級查找的基地址,然後通過sid 找到具體的STE(l2ptr + sid[7:0] * 64).

結合代碼分析:

+-> arm_smmu_init_strtab_2lvl()
	/* 計算l1的大小, 一個l1 std的大小爲8byte, 對應的l1_std = sid[maxbit:split], maxbit是log2Size - 1, 所以l1的大小等於2 ^ (log2Size - split) * 8  */
	+-> l1size = cfg->num_l1_ents * (STRTAB_L1_DESC_DWORDS << 3);
	// 申請L1 stream table的空間
	+->  strtab = dmam_alloc_coherent()
	// 配置stream table(STRTAB_BASE_CFG)的format, 決定stream table的格式是2-level
	+-> reg  = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_2LVL);
	/* 配置stream table(STRTAB_BASE_CFG)的log2size,2級ste的entry是2 ^ log2size, l1 std的
	entry大小爲2 ^ (log2size - split) */
	+-> reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, size);
	/* 配置stream table(STRTAB_BASE_CFG)的split, split的值可以被配置爲6/8/10, 
	分別對應l1 std能夠指向的最大二級ste的空間爲4k/16k/64k*/
	+-> reg |= FIELD_PREP(STRTAB_BASE_CFG_SPLIT, STRTAB_SPLIT);
	/* 分配L1STD的內存, 並配置L1 descriptor的SPAN,SPAN表示L2 table包含多少個STE */
	+-> arm_smmu_init_l1_strtab()
  • 申請l1 Stream table的內存,內存大小爲2 ^ (log2Size - split) * 8
  • 申請L1 STD的內存, L1 STD在stream table的索引是streamID[maxbit: split]

配置完stream table的結構和各級大小後,再配置stream table的基地址:

	/* 配置stream table(STRTAB_BASE_CFG)的RA和addr, addr對應的是stream table的物理地址
	   ra爲read allocate hint */
	+-> reg  = smmu->strtab_cfg.strtab_dma & STRTAB_BASE_ADDR_MASK;
	+-> reg |= STRTAB_BASE_RA;

至此stream table的初始化流程結束

(5) arm_smmu_device_reset
該函數主要是進行smmu的硬件配置
主要流程如下:

+-> arm_smmu_device_reset()
	// 寫SMMU_CR0來disable smmu,並通過SMMU_CR0ACK檢查CR0是否被clear
	+-> arm_smmu_device_disable()
	// 配置讀取ste和command queue的屬性
	+-> writel_relaxed(ARM_SMMU_CR1);
	// random crap
	+-> writel_relaxed(ARM_SMMU_CR2);
	/* 配置ARM_SMMU_STRTAB_BASE和ARM_SMMU_STRTAB_BASE寄存器,分別對應stream table的物理基地址
	  以及格式,大小等*/
	+->writeq_relaxed(smmu->strtab_cfg.strtab_base, ARM_SMMU_STRTAB_BASE);
	+->writel_relaxed(smmu->strtab_cfg.strtab_base_cfg, ARM_SMMU_STRTAB_BASE);
	/* 配置cmd queue相關寄存器
	 * ARM_SMMU_CMDQ_BASE是配置command queue的基地址
	 * ARM_SMMU_CMDQ_PROD, 可以表示取出命令的位置
	 * ARM_SMMU_CMDQ_CONS, 可以表示輸入命令的位置
	 * ARM_SMMU_CMDQ_PROD和ARM_SMMU_CMDQ_CONS初始化時配置爲相同的值,都爲0
	 * 通過CMDQ_PROD和CMDQ_CONS, 可以判斷command queue是否還有空間
	 */
	+-> writeq_relaxed(smmu->cmdq.q.q_base, smmu->base + ARM_SMMU_CMDQ_BASE);
	+-> writel_relaxed(smmu->cmdq.q.llq.prod, smmu->base + ARM_SMMU_CMDQ_PROD);
	+-> writel_relaxed(smmu->cmdq.q.llq.cons, smmu->base + ARM_SMMU_CMDQ_CONS);
	// 最後配置command queue的en,對command queue進行使能
	+-> enables = CR0_CMDQEN;
	// 配置event queue相關寄存器, 流程和command queue類似
	+-> config event queue
	// 如果支持pri, 則配置pri queue相關寄存器, 流程和上面一致
	+-> config pri queue
	// 申請並使能smmu支持的相關中斷(eventq irq, priq irq, gerror irq)
	+-> arm_smmu_setup_irqs()
	// enable smmu, 寫SMMU_CR0,並通過SMMU_CR0ACK檢查CR0是否被enable
	+-> arm_smmu_write_reg_sync(smmu, enables, ARM_SMMU_CR0, ARM_SMMU_CR0ACK);

再着重講下smmu的中斷註冊:arm_smmu_setup_irqs()

+-> arm_smmu_setup_irqs()
	+-> arm_smmu_setup_unique_irqs()
		+-> arm_smmu_setup_msis(smmu);
			+->  arm_smmu_write_msi_msg()
		+-> devm_request_irq(smmu->dev, irq, arm_smmu_gerror_handler,
				       0, "arm-smmu-v3-gerror", smmu);

arm_smmu_write_msi_msg()函數裏會去:

  • 配置MSI中斷的目的地址
  • 配置MSI的中斷數據
  • 配置MSI中斷的寫地址的屬性
    配置完成後,當中斷產生時,最終會進入中斷註冊的處理函數, 以gerror的中斷處理爲例:
 arm_smmu_gerror_handler()
 	// 讀gerror和gerrorrn寄存器,確認gerror中斷髮生的錯誤類型
 	+-> gerror = readl_relaxed(smmu->base + ARM_SMMU_GERROR);
 	+-> gerrorn = readl_relaxed(smmu->base + ARM_SMMU_GERRORN);
 	// 完成中斷處理後,寫gerror和gerrorn對應的的位一致,global中斷處理完成
 	+-> writel(gerror, smmu->base + ARM_SMMU_GERRORN);

(6) iommu_device_register
註冊iommu設備,主要設計一個操作,就是將smmu設備添加到iommu_device_list中

int iommu_device_register(struct iommu_device *iommu)
{
	spin_lock(&iommu_device_lock);
	list_add_tail(&iommu->list, &iommu_device_list);
	spin_unlock(&iommu_device_lock);
	return 0;
}

着重講下和iommu_device相關的兩個重要數據結構iommu_group 和 iommu_domain
看下iommu_device結構體的定義

struct iommu_device {
	struct list_head list;
	const struct iommu_ops *ops;
	struct fwnode_handle *fwnode;
	struct device *dev;
};

iommu_device中定義了iommu_ops以及struct device,
在struct device中,有iommu_group的成員,iommu_group 又包含了iommu_domain。
iommu_device->device->iommu_group->iommu_domain

iommu_domain的具體定義:

struct iommu_domain {
	unsigned type;
	const struct iommu_ops *ops;
	unsigned long pgsize_bitmap;	/* Bitmap of page sizes in use */
	iommu_fault_handler_t handler;
	void *handler_token;
	struct iommu_domain_geometry geometry;
	void *iova_cookie;
};

每一個domain 代表一個具體的設備使用iommu的詳細spec
在arm_smmu_domain結構體中,又將arm_smmu_domain和iommu_domain關聯, 所以iommu_ops指向SMMU驅動。所以最終ARM是用arm_smmu_domain來管理驅動和設備之間的關聯的。

iommu_group的具體定義:[drivers/iommu/iommu.c: iommu_group]

struct iommu_group {
	struct kobject kobj;
	struct kobject *devices_kobj;
	struct list_head devices;
	struct mutex mutex;
	struct blocking_notifier_head notifier;
	void *iommu_data;
	void (*iommu_data_release)(void *iommu_data);
	char *name;
	int id;
	struct iommu_domain *default_domain;
	struct iommu_domain *domain;
};

爲什麼會有一個iommu_group的概念,直接將device和iommu_domain關聯不香嗎?
假設我們通過iommu提供設備的DMA能力,當發起dma_map的時候,設備設置了streamid, 但是多個設備的streamid有可能是一樣的。 那麼這時候修改其中一個設備的頁表體系,也就相當於修改了另一個設備的頁表體系。所以,修改頁表的最小單位不是設備,而是streamid。
因此,爲了避免這種情況,增加了一個iommu_group的概念,iommu_group代表共享同一個streamid的一組device(表述在/sys/kernel/iommu_group中)。
有了iommu_group, 設備發起dma_map操作時,會定位streamid和iommu_group, group定位了iommu_device和iommu_domain,iommu_domain定位了asid,這樣,硬件要求的全部信息都齊了。
(Linux iommu和vfio概念空間解構)

(7) arm_smmu_set_bus_ops
給smmu支持的總線設置bus->iommu_ops, 讓總線具有了iommu attach的能力。

	arm_smmu_set_bus_ops(&arm_smmu_ops)
	+-> bus_set_iommu(&pci_bus_type, ops);
	+-> bus_set_iommu(&amba_bustype, ops);
	+-> bus_set_iommu(&platform_bus_type, ops);

arm_smmu_ops結構體定義如下:

static struct iommu_ops arm_smmu_ops = {
	.capable		= arm_smmu_capable,
	.domain_alloc		= arm_smmu_domain_alloc,
	.domain_free		= arm_smmu_domain_free,
	.attach_dev		= arm_smmu_attach_dev,             ----- 
	.map			= arm_smmu_map,
	.unmap			= arm_smmu_unmap,
	.flush_iotlb_all	= arm_smmu_flush_iotlb_all,
	.iotlb_sync		= arm_smmu_iotlb_sync,
	.iova_to_phys		= arm_smmu_iova_to_phys,
	.add_device		= arm_smmu_add_device,
	.remove_device		= arm_smmu_remove_device,
	.device_group		= arm_smmu_device_group,
	.domain_get_attr	= arm_smmu_domain_get_attr,
	.domain_set_attr	= arm_smmu_domain_set_attr,
	.of_xlate		= arm_smmu_of_xlate,
	.get_resv_regions	= arm_smmu_get_resv_regions,
	.put_resv_regions	= generic_iommu_put_resv_regions,
	.pgsize_bitmap		= -1UL, /* Restricted during device attach */
};

主要分析smmu的兩個關鍵操作:arm_smmu_attach_dev和arm_smmu_add_device
arm_smmu_add_device: 將smmu設備添加到總線

arm_smmu_add_device()
	
	+-> smmu = arm_smmu_get_by_fwnode(fwspec->iommu_fwnode);
	/* for each sid, 如果是2-level ste, 爲l2 ste分配內存
	 *在之前的init_l1_strtab, 已經初始化了L1_std, L1_STD定義了下一級查找的基地址,
	 * 現在可以通過sid 找到具體的STE(l2ptr + sid[7:0] * 64)
	 * 這個函數先爲每一個sid分配L2_STE的內存, 分配完成後在爲每一個SID進行cfg配置
	 */
	+-> arm_smmu_init_l2_strtab()
	// 將device和group關聯起來
	+-> iommu_device_link()

總線掃描發現了設備,總線的發現流程負責調用iommu_ops(arm_smmu_ops )給這個設備加上iommu_group,然後讓iommu_group指向對應的iommu控制器

arm_smmu_attach_dev, 嘗試爲設備尋找到驅動

arm_smmu_attach_dev()
	// 從iommu_domain 中得到arm_smmu_domain
	+-> smmu_domain = to_smmu_domain(iommu_domain );
	// 一般情況下smmu_domain->smmu = NULL
	// 在arm_smmu_add_device中,我們已經爲STE項分配了內存
	+-> arm_smmu_domain_finalise(domain, master);
		// 分配asid
		+-> asids = arm_smmu_bitmap_alloc()
		// 根據smmu stage是stage1還是stage2, 如果smmu domain是stage1
		+-> arm_smmu_domain_finalise_s1()
			// 分配CD table的空間
			+-> arm_smmu_alloc_cd_tables(smmu_domain);
			// 配置CD descriptor的cfg
			+-> cfg->cd.tcr	= FIELD_PREP(CTXDESC_CD_0_XXX)...
		// 如果smmu domain是stage2, STE已經包含了頁表的s2ttb基地址和vmid,結束
		+-> arm_smmu_domain_finalise_s2()
		+-> finalise_stage_fn(smmu_domain, master, &pgtbl_cfg);
		

支持了2-leveli的CD或linear格式的CD, 方式和SID查找ste類似。
在這裏插入圖片描述
結合代碼分析:

static int arm_smmu_alloc_cd_tables(struct arm_smmu_domain *smmu_domain)
{
	int ret;
	size_t l1size;
	size_t max_contexts;
	struct arm_smmu_device *smmu = smmu_domain->smmu;
	struct arm_smmu_s1_cfg *cfg = &smmu_domain->s1_cfg;
	struct arm_smmu_ctx_desc_cfg *cdcfg = &cfg->cdcfg;

	max_contexts = 1 << cfg->s1cdmax;                -------------- (a)      

	if (!(smmu->features & ARM_SMMU_FEAT_2_LVL_CDTAB) ||
	    max_contexts <= CTXDESC_L2_ENTRIES) {
		cfg->s1fmt = STRTAB_STE_0_S1FMT_LINEAR;                -------- (b)
		cdcfg->num_l1_ents = max_contexts;						

		l1size = max_contexts * (CTXDESC_CD_DWORDS << 3);      ------- (c)
	} else {
		cfg->s1fmt = STRTAB_STE_0_S1FMT_64K_L2;               
		cdcfg->num_l1_ents = DIV_ROUND_UP(max_contexts,
						  CTXDESC_L2_ENTRIES);

		cdcfg->l1_desc = devm_kcalloc(smmu->dev, cdcfg->num_l1_ents,
					      sizeof(*cdcfg->l1_desc),
					      GFP_KERNEL);
		if (!cdcfg->l1_desc)
			return -ENOMEM;

		l1size = cdcfg->num_l1_ents * (CTXDESC_L1_DESC_DWORDS << 3);
	}

	cdcfg->cdtab = dmam_alloc_coherent(smmu->dev, l1size, &cdcfg->cdtab_dma,
					   GFP_KERNEL);
	if (!cdcfg->cdtab) {
		dev_warn(smmu->dev, "failed to allocate context descriptor\n");
		ret = -ENOMEM;
		goto err_free_l1;
	}

	return 0;

err_free_l1:
	if (cdcfg->l1_desc) {
		devm_kfree(smmu->dev, cdcfg->l1_desc);
		cdcfg->l1_desc = NULL;
	}
	return ret;
}

在CD的建立過程中,主要涉及到以下幾點:
ste.S1Contextptr中定義了CD的基地址,CD的大小爲64byte
a. 需要配置ste.S1CDMax, cdmax爲0表示這個ste只有一個CD, 不需要使用到substreamid, 如果cdmax不爲0, 那麼CD的數目是2 ^ S1CDMax;
b. 需要配置ste.S1Fmt, 如果是linear結構的CD,CD的獲取方法爲S1ContextPTR + 64 * ssid; 如果是2-level結構的CD, L1CD的索引爲ssid[s1cdmax - 1: 6], L2CD的索引爲ssid[5:0]

attach_dev完成後,如果是stage1相關,CD的結構,大小和基地址已經成功建立,成功獲取STE後,可以通過substreamid找到CD(S1ContextPTR + 64 * ssid)。找到的CD中包含頁表PTW需要的TTBR寄存器,所以每一個CD對應一個頁表, 這樣一個SMMU單元,就可以有多張頁表。

總結:

smmu驅動的初始化流程就是一個探測硬件規格,初始化硬件配置,分配STD/STE/CD等空間的過程。
在這裏插入圖片描述

參考資料

Linux iommu和vfio概念空間解構
IHI0070_System_Memory_Management_Unit_Arm_Architecture_Specification

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章