VirtIO實現原理——PCI基礎

virtio設備可以基於不同總線來實現,本文介紹基於pci實現的virtio-pci設備。以virtio-blk爲例,首先介紹PCI配置空間內容,virtio-pci實現的硬件基礎——capability,最後分析PIC設備的初始化以及virtio-pci設備的初始化。

PCI配置空間

  • virtio設備作爲pci設備,必須實現pci local bus spec規定的配置空間(最大256字節),前64字節是spec中定義好的,稱預定義空間,其中前16字節對所有類型的pci設備都相同,之後的空間格式因類型而不同,對前16字節空間,我稱它爲通用配置空間

通用配置空間

在這裏插入圖片描述

  • 前16字節中有4個地方用來識別virtio設備
  1. vendor id:廠商ID,用來標識pci設備出自哪個廠商,這裏是0x1af4,來自Red Hat。
  2. device id:廠商下的產品ID,傳統virtio-blk設備,這裏是0x1001
  3. revision id:廠商決定是否使用,設備版本ID,這裏未使用
  4. header type:pci設備類型,0x00(普通設備),0x01(pci bridge),0x02(CardBus bridge)。virtio是普通設備,這裏是0x00
  • command字段用來控制pci設備,打開某些功能的開關,virtio-blk設備是(0x0507 = 0b1010111),command的各字段含義如下圖,低三位的含義如下:
  1. I/O Space:如果PCI設備實現了IO空間,該字段用來控制是否接收總線上對IO空間的訪問。如果PCI設備沒有IO空間,該字段不可寫。
  2. Memory Space:如果PCI設備實現了內存空間,該字段用來控制是否接收總線上對內存空間的訪問。如果PCI設備沒有內存空間,該字段不可寫。
  3. Bus Master:控制pci設備是否具有作爲Master角色的權限。
    在這裏插入圖片描述
  • status字段用來記錄pci設備的狀態信息,virtio-blk是(0x10 = 0x10000),status各字段含義如下圖:
    在這裏插入圖片描述
    其中有一位是Capabilities List,它是pci規範定義的附加空間標誌位,Capabilities List的意義是允許在pci設備配置空間之後加上額外的寄存器,這些寄存器由Capability List組織起來,用來實現特定的功能,附加空間在64字節配置空間之後,最大不能超過256字節。以virtio-blk爲例,它標記了這個位,因此在virtio-blk設備配置空間之後,還有一段空間用來實現virtio-blk的一些特有功能。1表示capabilities pointer字段(0x34)存放了附加寄存器組的起始地址。這裏的地址表示附加空間在pci設備空間內的偏移
  • virtio-blk配置空間的內容可以通過lspci命令查看到,如下
    G4ubmV0L2h1YW5nOTg3MjQ2NTEw,size_16,color_FFFFFF,t_70)

virtio配置空間

  • virtio-pci設備實現pci spec規定的通用配置空間後,設計了自己的配置空間,用來實現virtio-pci的功能。pci通過status字段的capabilities listbit標記自己在64字節預定義配置空間之後有附加的寄存器組,capabilities pointer會存放寄存器組鏈表的頭部指針,這裏的指針代表寄存器在配置空間內的偏移
    在這裏插入圖片描述
  • pci spec中描述的capabilities list格式如下,第1個字節存放capability ID,標識後面配置空間實現的是哪種capability,第2個字節存放下一個capability的地址。capability ID查閱參見pci spec3.0 附錄H。virtio-blk實現的capability有兩種,一種是MSI-X( Message Signaled Interrupts - Extension),ID爲0x11,一種是Vendor Specific,ID爲0x9,後面一種capability設計目的就是讓廠商實現自己的功能。virtio-blk的實現以此爲基礎
    在這裏插入圖片描述
  • virtio-pci根據自己的功能需求,設計瞭如下的capabilities佈局,右側是6個capability,其中5個用做描述virtio-pci的capability,1個用作描述MSI-X的capability,這裏我們只介紹用作virtio-pci的capability。再看下圖,右邊描述了virtio-blk的capability佈局,左邊是每個capability指向的物理地址空間佈局。virtio-pci設備的初始化,前後端通知,數據傳遞等核心功能,就在這5個capability中實現
    在這裏插入圖片描述
  • 根據virtio spec的規範,要實現virtio-pci的capabilty,其佈局應該如下
    在這裏插入圖片描述
    vndr表示capability類型,next表示下一個capability在pci配置空間的位置,len表示capability這個數據結構的長度,type有如下取值,將virtio-pci的capability又細分成幾類
    在這裏插入圖片描述

virtio通用配置空間

  • capability中最核心的內容是virtio_pci_common_cfg,它是virtio前後端溝通的主要橋樑,common config分兩部分,第一部分用於設備配置,第二部分用於virtqueue使用。virtio驅動初始化利用第一部分來和後端進行溝通協商,比如支持的特性(guest_feature),初始化時設備的狀態(device_status),設備的virtqueue個數(num_queues)。第二部分用來實現前後段數據傳輸。後面會詳細提到兩部分在virtio初始化和數據傳輸中的作用。virtio_pci_common_cfg數據結構如下
    在這裏插入圖片描述

virtio磁盤配置空間

  • TODO

VirtIO-PCI初始化

  • virtio-blk基於virtio-pci,virtio-pci基於pci,所以virtio-blk初始化要從pci設備初始化說起

PCI初始化

  • PCI驅動框架初始化在內核中有兩個入口,分別如下:
  1. arch_initcall(pci_arch_init),體系結構的初始化,包括初始化IO地址0XCF8
  2. subsys_initcall(pci_subsys_init),PCI子系統初始化,這個過程會完成PIC總線樹上設備的枚舉,Host bridge會爲PCI設備分配地址空間並將其寫入BAR寄存器。體系結構初始化不做介紹,這張主要介紹PCI總線樹的枚舉和配置

枚舉

  • 枚舉的前提該PCI設備可以訪問,PCI規範規定,設備在還沒有配置地址前,CPU往兩個IO端口分別寫入地址和數據,實現對PCI設備的讀寫。如下圖所示,這兩個IO端口對應的是兩個Host橋上的寄存器,它們可以直接通過io指令訪問
    在這裏插入圖片描述
  • 這兩個寄存器位於Host橋上,翻看Host橋(Intel 5000X MCH 3.5章節)的手冊可以找到寄存器各字段具體含義,如下圖所示。當cpu要訪問某個pci設備時,先往0XCF8寫入4字節的bus/slot/function地址,然後通過0XCFC的IO空間讀取或者寫入數據。地址空間0XCF8的初始化發生在pci_arch_init裏面。
    在這裏插入圖片描述
    在這裏插入圖片描述
  • 有了讀寫pci設備寄存器的方法,cpu可以讀取任意pci總線上任意設備配置空間的任意寄存器。讀取slot設備配置空間的vendor id,如果返回全1表示沒有設備,如果返回具體值表示slot上存在function,繼續判斷是否爲multifunction。通過這樣的方式逐總線搜索下去,枚舉每一個slot上存在的pci設備,直到遍歷完總線樹。其中兩處涉及到配置空間寄存器的讀寫,一是識別設備的時候需要讀取vendor id和device id,一是讀取bar空間大小時需要讀寫bar寄存器。枚舉發生在pci_subsys_init,如下:
pci_subsys_init
	x86_init.pci.init	=>	x86_default_pci_init	
	pci_legacy_init
		pcibios_scan_root
			x86_pci_root_bus_resources	// 爲Host bridge分配資源,通常情況下就是64K IO空間地址和內存空間地址就在這裏劃分
			pci_scan_root_bus			// 枚舉總線樹上的設備
				pci_create_root_bus		// 創建Host bridge
				pci_scan_child_bus		// 掃描總線樹上所有設備,如果有pci橋,遞歸掃描下去
					pci_scan_slot
						pci_scan_single_device	// 掃描設備,讀取vendor id和device id
							pci_scan_device
								pci_setup_device
									pci_read_bases
										__pci_read_base	// 讀取bar空間大小
  • 讀取到PCI設備BAR空間大小後,就可以向Host bridge申請物理地址區間了,如果成功,PCI設備就得到了一段PCI空間的,大於等於BAR空間大小的物理地址。注意,Host bridge掌握着PCI總線上所有設備可以使用的IO資源和存儲資源,這裏說的資源,就是物理地址空間。下面是兩個關鍵的數據結構
/*
 * Resources are tree-like, allowing
 * nesting etc..
 */
struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    unsigned long desc;
    struct resource *parent, *sibling, *child;
};

resource代表一個資源,可以是一段IO地址區間,或者Mem地址區間,總線樹上每枚舉一個設備,Host bridge就根據設備的BAR空間大小分配合適的資源給這個PCI設備用,這裏的資源就是IO或者內存空間的物理地址。PCI設備BAR寄存器的值就是從這裏申請得來的。申請的流程如下

pci_read_bases
	/* 遍歷每個BAR寄存器,讀取其內容,併爲其申請物理地址空間 */
	for (pos = 0; pos < howmany; pos++) {
        struct resource *res = &dev->resource[pos];	// 申請的地址空間放在這裏面
        reg = PCI_BASE_ADDRESS_0 + (pos << 2);
        pos += __pci_read_base(dev, pci_bar_unknown, res, reg);
    }
    	region.start = l64;	
    	region.end = l64 + sz64;
		/* 申請資源,將申請到的資源放在res中, region存放PCI設備BAR空間區間 */
    	pcibios_bus_to_resource(dev->bus, res, &region);  	

分析資源申請函數,它首先取出PCI設備所在的Host bridge,pci_host_bridge.windows鏈表維護了Host bridge管理的所有資源,遍歷其windows成員鏈表,找到合適的區間,然後分給PCI設備。
在這裏插入圖片描述
至此,PCI設備有了PCI域的物理地址,當掃描結束後,內核會逐一爲這些PCI設備配置這個物理地址

  • 枚舉過程中識別設備的代碼如下
    在這裏插入圖片描述
  • 枚舉過程中獲取bar佔用的空間大小步驟如下,代碼如下
  1. 讀取BAR空間32bit的原始內容,保存
  2. BAR空間所有bit寫1
  3. 再次讀取BAR空間內容,右起第一個非0位所在的bit位,它的值就是BAR空間大小
    假設是右起第12bit爲1,那麼BAR空間的大小就是2^12 = 4KB
  4. 將原始內容寫入BAR空間,恢復其原始狀態,留待下一次讀取
    在這裏插入圖片描述
  • 枚舉之後,內核記錄PCI總線樹上所有PCI設備的基本硬件信息,比如vendor id,device id,同時也建立了一張所有設備的內存拓撲圖,填充了大部分pci_dev數據結構,餘下部分比如PCI設備的驅動(pci_dev.driver)在這個時候還沒有找到
/*
 * The pci_dev structure is used to describe PCI devices.
 */
struct pci_dev {
    struct list_head bus_list;  /* node in per-bus list */
    struct pci_bus  *bus;       /* bus this device is on */
    struct pci_bus  *subordinate;   /* bus this device bridges to */

    void        *sysdata;   /* hook for sys-specific extension */
    struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */
    struct pci_slot *slot;      /* Physical slot this device is in */

    unsigned int    devfn;      /* encoded device & function index */
    unsigned short  vendor;
    unsigned short  device;
    unsigned short  subsystem_vendor;
    unsigned short  subsystem_device;
    unsigned int    class;      /* 3 bytes: (base,sub,prog-if) */
    u8      revision;   /* PCI revision, low byte of class word */
    u8      hdr_type;   /* PCI header type (`multi' flag masked out) */
#ifdef CONFIG_PCIEAER
    u16     aer_cap;    /* AER capability offset */
#endif
    u8      pcie_cap;   /* PCIe capability offset */
    u8      msi_cap;    /* MSI capability offset */
    u8      msix_cap;   /* MSI-X capability offset */
    u8      pcie_mpss:3;    /* PCIe Max Payload Size Supported */
    u8      rom_base_reg;   /* which config register controls the ROM */
    u8      pin;        /* which interrupt pin this device uses */
    u16     pcie_flags_reg; /* cached PCIe Capabilities Register */
    unsigned long   *dma_alias_mask;/* mask of enabled devfn aliases */

    struct pci_driver *driver;  /* which driver has allocated this device */
    u64     dma_mask;   /* Mask of the bits of bus address this
                       device implements.  Normally this is
                       0xffffffff.  You only need to change
                       this if your device has broken DMA
                       or supports 64-bit transfers.  */
 	......
}

配置

  • 枚舉完成之後,內核得到pci總線樹的拓撲圖,然後開始配置總線樹上的所有設備。Host bridge按照枚舉過程中讀取的pci設備的bar空間大小,將這段地址空間分成大小不同的地址空間。簡單說就是爲每個pci設備分配一個能夠滿足它使用的pci總線域地址段。確立了每個pci設備擁有的地址空間後,Host bridge將其首地址寫到bar寄存器中,這就是配置,注意,bar空間寫入的起始地址是pci總線域的,不是cpu域的。配置過程在pci_subsys_init中完成,首先遍歷總線樹上所有設備,統一檢查在枚舉階段設備申請的資源,判斷多個設備之前是否有資源衝突,內核要保證資源統一併且正確的分配給每一個PCI設備。檢查完成後分配資源。最後向每個PCI設備的BAR寄存器寫入分配到的地址空間起始值,完成配置。流程如下:
pci_subsys_init
	pcibios_resource_survey             
    	pcibios_allocate_bus_resources(&pci_root_buses);		// 首先將整個資源按照總線再分成一段段空間
    	pcibios_allocate_resources(0);							// 檢查資源是否統一併且不衝突
    	pcibios_allocate_resources(1);
    	pcibios_assign_resources();								// 寫入地址到BAR寄存器
    		pci_assign_resource
    			_pci_assign_resource
    				__pci_assign_resource
    					pci_bus_alloc_resource
    					pci_update_resource
    						pci_std_update_resource
    							pci_write_config_dword(dev, reg, new)	// 往BAR寄存器寫入起始地址

BAR寄存器中寫入的地址,乍一看就是系統的物理地址,但實際上,它與CPU域的物理地址有所不同,它是PCI域的物理地址。兩個域的地址需要通過Host bridge的轉換。只不過,X86上Host bridge偷懶了,直接採用了一一映射的方式。因此兩個域的地址空間看起來一樣。在別的結構上(PowerPC)這個地址不一樣。

  • 下面是virtio-blk設備配置空間的格式,截圖自pci3.0 spec 6.2.5,bar寄存器配置之後,它的內容是bar空間的pci總線域起始地址,其中低4bit描述了這段空間的屬性,在取地址的時候要作與0操作。最低位表示這個pci設備的bar空間,映射到的是cpu總線域的哪類空間,爲1是IO空間,爲0是內存空間。當映射到內存空間時,還用[2-1]這兩位區分cpu域地址總線的寬度,爲00表示映射到32位寬總線的cpu域內存空間,爲10時表示映射到64位寬總線的cpu域內存空間。
    在這裏插入圖片描述
  • 下面是一個典型的virtio-blk設備配置空間bar寄存器內容
  1. BAR0:0XC041,將低4位與0,BAR0空間的起始地址0XC040,從最低位可以看出來這映射的是IO空間
  2. BAR1:0XFEBD2000,BAR1空間的起始地址0XFEBD2000,這是個內存空間
  3. BAR2:0XFE00800C,將低4位與0,BAR2空間的起始地址0XFE008000,這是個內存空間,64-bit,支持prefetch
    在這裏插入圖片描述
  • 在系統的IO地址空間中,可以看到BAR0佔用的IO地址,cat /proc/ioports
    在這裏插入圖片描述
  • 在系統的內存空間中,可以看到BAR1和BAR4佔用的內存空間,cat /proc/iomem
    在這裏插入圖片描述
  • 從主機上可以看到qemu統計的virtio-blk設備BAR空間的使用情況,virsh qemu-monitor-command vm --hmp info pci
    在這裏插入圖片描述
  • pci設備的配置有幾個地方容易混淆,特此說明自己的理解,如有不對,請留言指出:
  1. 枚舉過程中訪問pci配置空間寄存器,軟件接口是往特定寄存器寫東西,真正發起讀寫請求的,是host bridge,它在pci總線上傳輸的是command type爲configuration wirte/read的transaction。
    配置完成後,軟件對pci bar空間關聯的內存進行讀寫,可以直接使用mov內存訪問指令,這時候host bridge在pci總線上傳輸的是command type爲IO Read/Write或者Memory Read/Write的transaction。
  2. pci設備被配置之後,bar寄存器中存放了關聯的內存起始地址,這個地址是pci總線域的物理地址,不是cpu域的物理地址,雖然這兩個值一樣,但這只是x86這種架構的特殊情況,因爲host bridge轉換的時候是一一映射的,才造成這種假象。在別的架構上,比如power pc,cpu域的地址想要訪問pci總線域的地址,需要通過host bridge進行地址轉換才能進行,兩種地址並不相同。
  3. pci設備被配置之後,雖然看上去軟件可以像訪問內存一樣訪問pci設備的bar的空間,但cpu真正訪問的時候,是要把地址交給host bridge,讓它去訪問纔可以。可以說,訪問pci設備bar空間的不是cpu,而是host bridge。至始至終,cpu都不能直接管理pci設備的bar空間,它只能通過host bridge來間接管理。

加載virtio-pci驅動

  • virtio-blk最底層是pci,其上是virtio-pci,因此初始化流程從pci bus的註冊開始,然後是virtio-pci,然後是virtio-blk逐級往上介紹,virtio-blk的驅動加載會留到下一章介紹。
  • linux設備驅動有一套bus,device,driver基礎框架,這種設計將設備與板載信息解耦,使得設備和驅動可以靈活加載,屏蔽了板載信息的變化。當內核從一個平臺移植到另外一個平臺時,bus,device,driver這套機制的實現使得設備驅動的加載可以自適應。驅動不會因爲板載設備的變化而重寫代碼。
  • 設備驅動框架中,bus是連接device和driver的橋樑,因此在系統啓動過程中它首先被註冊。bus的作用,就是讓系統啓動時註冊的設備可以找到對應的驅動,註冊的驅動可以找到對應的設備,這個過程稱爲設備與驅動的綁定。
  • 總線上發生兩類事件可以導致設備與驅動綁定行爲的發生:一是通過device_register函數向某一bus上註冊設備,這種情況下內核除了將該設備加入到bus上設備鏈表的尾端,同時會試圖將此設備與總線上所有驅動對象進行綁定操作;二是通過driver_register將某一驅動註冊到其所屬的bus上,內核此時除了將驅動對象加入到bus所有驅動對象構成的鏈表的尾部,也會試圖將驅動與器上的所有設備進行綁定操作。
  • virtio-pci設備是註冊到pci總線上的,因此加載它的驅動應該時pci總線上的驅動,首先介紹PCI總線的註冊,然後介紹總線如何將設備和驅動綁定,最後介紹設備的探測。

PCI總線的註冊

  • PCI總線聲明瞭一個bus_type的結構,稱爲pci_bus_type,它由下面的值初始化:
struct bus_type pci_bus_type = {
    .name       = "pci",
    .match      = pci_bus_match,
    .uevent     = pci_uevent,
    .probe      = pci_device_probe,
    .remove     = pci_device_remove,
	......
};
  • 將PCI模塊加載到內核時,通過調用bus_register將pci_bus_type註冊到驅動程序核心的頂層bus中,這個過程將在/sys/bus下創建pci目錄,然後在/sys/bus/pci中創建兩個目錄:devices和drivers,代碼如下:
pci_driver_init
	bus_register(&pci_bus_type)	// pci總線數據結構
		priv->subsys.kobj.kset = bus_kset;	// 指向代表頂層bus的kset
		priv->devices_kset = kset_create_and_add("devices", NULL, &priv->subsys.kobj);
    	priv->drivers_kset = kset_create_and_add("drivers", NULL,  &priv->subsys.kobj); 

生成的pci目錄如下:
在這裏插入圖片描述
創建的devices和drivers如下:
在這裏插入圖片描述

  • PCI總線註冊後,重要的一個功能就是綁定設備和驅動,當由PCI設備註冊到總線上時,它遍歷總線上的所有驅動,調用match函數判斷這對驅動和設備是否匹配。下一節具體介紹match的流程。

PCI驅動的註冊

  • 所有pci總線上的驅動都必須實現一個pci_driver的變量,這個結構中有一個device_driver結構,在註冊PCI驅動程序時,這個結構將被初始化,如下:
static struct pci_driver virtio_pci_driver = {
    .name       = "virtio-pci",
    .id_table   = virtio_pci_id_table,
    .probe      = virtio_pci_probe, 
    .remove     = virtio_pci_remove,
	......
}

module_pci_driver(virtio_pci_driver)
	pci_register_driver
		__pci_register_driver

int __pci_register_driver(struct pci_driver *drv, struct module *owner,
              const char *mod_name)
{
    /* initialize common driver fields */
    drv->driver.name = drv->name;
    drv->driver.bus = &pci_bus_type;	// 將驅動程序的總線指向了pci_bus_type
    drv->driver.owner = owner;
    drv->driver.mod_name = mod_name;
    drv->driver.groups = drv->groups;

    spin_lock_init(&drv->dynids.lock);
    INIT_LIST_HEAD(&drv->dynids.list);

    /* register with core */
    return driver_register(&drv->driver);	// 向驅動核心註冊
}
  • PCI驅動註冊調用內核驅動核心接口driver_register,該接口會在sysfs目錄建立相應的目錄和屬性文件,爲用戶空間提供控制驅動卸載和加載的接口,具體流程如下:
driver_register
	driver_find											// 查找是否總線上已存在相同驅動,防止重複註冊
	bus_add_driver
		driver_create_file(drv, &driver_attr_uevent)	// 在virtio-pci目錄下創建uevent屬性文件
		add_bind_files(drv)								// 在virtio-pci目錄下創建bind/unbind屬性文件
		
/* driver_attr_uevent 變量通過以下宏定義,其餘driver屬性文件類似 */
static DRIVER_ATTR_WO(uevent)
#define DRIVER_ATTR_WO(_name) \
    struct driver_attribute driver_attr_##_name = __ATTR_WO(_name)

重點看unbind屬性文件的創建,它爲用戶提供了卸載驅動的接口,當用戶向unbind屬性文件裏面寫入pci設備的地址時,內核會將該設備與其驅動解綁,相當於綁定的逆操作,對應的解邦操作函數unbind_store,如下:

/* Manually detach a device from its associated driver. */
static ssize_t unbind_store(struct device_driver *drv, const char *buf,
                size_t count)
{
    struct bus_type *bus = bus_get(drv->bus);		// 找到驅動所在總線
    struct device *dev;
    int err = -ENODEV;

    dev = bus_find_device_by_name(bus, NULL, buf);	// 通過buf中存放的設備名字找到其在內核中對應的device
    if (dev && dev->driver == drv) {				// 確認設備的驅動就是自己
        if (dev->parent)    /* Needed for USB */
            device_lock(dev->parent);
        device_release_driver(dev);					// 解綁定!!!
        if (dev->parent)
            device_unlock(dev->parent);
        err = count;
    }
    put_device(dev);
    bus_put(bus);
    return err;
}

static DRIVER_ATTR_IGNORE_LOCKDEP(unbind, S_IWUSR, NULL, unbind_store)

#define DRIVER_ATTR_IGNORE_LOCKDEP(_name, _mode, _show, _store) \
    struct driver_attribute driver_attr_##_name =       \
        __ATTR_IGNORE_LOCKDEP(_name, _mode, _show, _store)       

回到virtio-pci驅動的註冊流程都走完之後,sysfs中多了virtio-pci驅動的目錄和屬性文件,如下:
在這裏插入圖片描述

PCI設備的match

  • PCI設備註冊後,驅動核心遍歷總線上每個驅動,依次執行match函數,查看是否能配對,下面這個函數時內核提供的遍歷總線上所有驅動的工具函數,遍歷PCI總線似乎沒有用這個函數,但動作應該類似。
int bus_for_each_drv(struct bus_type * bus, struct device_driver * start,
             void * data, int (*fn)(struct device_driver *, void *));
  • match動作的具體流程如下,它判斷設備和驅動能否綁定的標誌,對virtio-pci設備來說,就是vendor id相等就可以了
pci_bus_match
	pci_match_device
		pci_match_id
			pci_match_one_device
			
static inline const struct pci_device_id *
pci_match_one_device(const struct pci_device_id *id, const struct pci_dev *dev)
{   
    if ((id->vendor == PCI_ANY_ID || id->vendor == dev->vendor) &&
        (id->device == PCI_ANY_ID || id->device == dev->device) &&
        (id->subvendor == PCI_ANY_ID || id->subvendor == dev->subsystem_vendor) &&
        (id->subdevice == PCI_ANY_ID || id->subdevice == dev->subsystem_device) &&
        !((id->class ^ dev->class) & id->class_mask)) 
        return id;
    return NULL;
} 
  • pci_match_one_device函數中,第一個參數是設備驅動註冊時硬編碼的ID結構體,第二個參數是pci設備,當PCI驅動指定的ID爲PCI_ANY_ID時,表示可以匹配任何的ID,查看virtio_pci_driver註冊時設置的virtio_pci_id_table,如下,可以看到,驅動只設置了vendor id,所有隻要vendor id爲0x1af4,都可以match成功。在系統枚舉PCI設備時,已經從PCI設備的配置空間中讀到了vendor id。因此,如果是virtio設備,不論是哪一種,都可以成功綁定virtio-pci驅動
static const struct pci_device_id virtio_pci_id_table[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_REDHAT_QUMRANET, PCI_ANY_ID) },
    { 0 }
};  

#define PCI_VENDOR_ID_REDHAT_QUMRANET    0x1af4

#define PCI_DEVICE(vend,dev) \
    .vendor = (vend), .device = (dev), \
    .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID

PCI設備的探測

PCI總線match設備和驅動成功後,驅動程序核心會把device結構中的driver指針指向這個驅動程序,兩者就聯繫起來,然後調用device_driver結構中的probe函數探測PCI設備。這裏就是virtio_pci_driver指定的virtio_pci_probe函數。probe的主要動作包含:

  1. 使能PCI設備BAR空間的映射能力,設置command寄存器的I/O space和memory space爲1,將pci設備映射IO空間和內存空間的開關打開
  2. 讀取virtio-pci設備配置空間的附加寄存器組,識別capability,讀取PCI設備每個BAR內容到內存,並映射BAR空間到內核高地址
  3. 完善內核驅動核心數據結構,將virtio-pci設備註冊到virtio總線上,這一步會觸發virtio總線的match動作

映射BAR空間

  • PCI設備的BAR寄存器已經被寫入了PCI域的起始地址,但BAR空間還不能被驅動程序使用,需要將其映射到內核虛擬地址空間(3G-4G),才能使用
使能BAR空間
  • PCI配置空間的command寄存器,控制着PCI設備對來自總線上的訪問是否響應的開關。使能BAR空間,就是讓PCI設備開放總線對PCI設備中IO或Memory空間的訪問權限
virtio_pci_driver.probe
	virtio_pci_probe
		pci_enable_device
			pci_enable_device
				pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO)	// 打開內存和IO訪問權限
					do_pci_enable_device
						pcibios_enable_device
							pci_enable_resources
								pci_write_config_word(dev, PCI_COMMAND, cmd)	//向command寄存器字段寫1
識別cap
  • 使能BAR空間之後,開始查找BAR空間在設備上的位置,根據PCI的規範,我們首先要查看設備是否開啓capability,如果開啓再到PCI規範中約定的地方獲取cap鏈表的起始偏移,virtio-pci cap佈局如下:
    在這裏插入圖片描述
    在這裏插入圖片描述

cap探測入口在virtio_pci_modern_probe,如果是傳統模式,入口在virtio_pci_legacy_probe,這裏以modern probe爲例

virtio_pci_probe
	virtio_pci_modern_probe
		virtio_pci_find_capability
			pci_find_capability(dev, PCI_CAP_ID_VNDR)
				pos = __pci_bus_find_cap_start	// 判斷入口點,如果是普通pci設備,返回0x34,這個地方存放cap鏈表的入口偏移
				pos = __pci_find_next_cap	// 依次搜索每一條cap,找到類型爲PCI_CAP_ID_VNDR的第一個cap,返回它在配置空間的偏移
					__pci_find_next_cap_ttl

函數首先通過pci_find_capability查找類型爲PCI_CAP_ID_VNDR(0x9)的capability bar位置,這是PCI規範中定義的擴展capability類型,在查找前首先確定capability在配置空間的位置入口,檢查PCI設備是否實現capabilty,如果實現了,是普通設備或者pci橋,它在配置空間偏移0x34的地方,如果是Card Bus,它在配置空間偏移0x14的地方,找到capabitliy起始位置後依次查找鏈表上每個cap,直到找到PCI_CAP_ID_VNDR類型的cap(檢查cap空間type字段是否爲PCI_CAP_ID_VNDR),找到後返回cap在配置空間中的偏移。整個過程關鍵代碼和示意圖如下

/**
 * virtio_pci_find_capability - walk capabilities to find device info.
 * @dev: the pci device
 * @cfg_type: the VIRTIO_PCI_CAP_* value we seek
 * @ioresource_types: IORESOURCE_MEM and/or IORESOURCE_IO.
 *
 * Returns offset of the capability, or 0.
 */
static inline int virtio_pci_find_capability(struct pci_dev *dev, u8 cfg_type,
                         u32 ioresource_types, int *bars)
{           
    int pos;
    /* 查找cap結構的在配置空間中的偏移地址 */
    for (pos = pci_find_capability(dev, PCI_CAP_ID_VNDR);
         pos > 0;   
         pos = pci_find_next_capability(dev, pos, PCI_CAP_ID_VNDR)) {
        u8 type, bar;
        /* 取出virtio_pci_cap數據結構中type成員的值 */
        pci_read_config_byte(dev, pos + offsetof(struct virtio_pci_cap, cfg_type), &type);
         /* 取出virtio_pci_cap數據結構中bar成員的值 */
        pci_read_config_byte(dev, pos + offsetof(struct virtio_pci_cap, bar), &bar);
                            
        /* Ignore structures with reserved BAR values */
        if (bar > 0x5)
            continue;
		/* 如果是我們想要的type,返回該cap在配置空間中的偏移 */
        if (type == cfg_type) {
            if (pci_resource_len(dev, bar) &&
                pci_resource_flags(dev, bar) & ioresource_types) {
                *bars |= (1 << bar);
                return pos;
            }
        }
    }
    return 0;
}

在這裏插入圖片描述

  • cap偏移確認後,就可以找到virtio-pci存放在BAR空間的數據,cap中有4個成員是描述這個數據,bar指示數據放在第幾個bar空間(取值0-5),cfg_type指示數據的類型,offset指示數據在bar空間的偏移,length存放數據長度。
/* This is the PCI capability header: */
struct virtio_pci_cap {
    __u8 cap_vndr;      /* Generic PCI field: PCI_CAP_ID_VNDR */
    __u8 cap_next;      /* Generic PCI field: next ptr. */
    __u8 cap_len;       /* Generic PCI field: capability length */
    __u8 cfg_type;      /* Identifies the structure. */
    __u8 bar;       /* Where to find it. */
    __u8 padding[3];    /* Pad to full dword. */
    __le32 offset;      /* Offset within bar. */
    __le32 length;      /* Length of the structure, in bytes. */
};
映射
  • 回到virtio_pci_modern_probe,依次找到virtio規範定義的幾種類型的cap在配置空間的偏移,注意common,isr,notify和device變量存放的是配置空間的偏移,最後的device結構與具體virtio設備有關,每種virtio設備實現自己的配置空間,virtio-blk的device-specific結構爲virtio_blk_config
    common = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_COMMON_CFG,
                        		IORESOURCE_IO | IORESOURCE_MEM,
                        		&vp_dev->modern_bars);
    isr = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_ISR_CFG,
                     			IORESOURCE_IO | IORESOURCE_MEM,
                     			&vp_dev->modern_bars);
    notify = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_NOTIFY_CFG,
                        		IORESOURCE_IO | IORESOURCE_MEM,
                        		&vp_dev->modern_bars);
    /* Device capability is only mandatory for devices that have
     * device-specific configuration.
     */
    device = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_DEVICE_CFG,
                        		IORESOURCE_IO | IORESOURCE_MEM,
                        		&vp_dev->modern_bars);
  • map_capability將BAR空間映射到內核的虛擬地址空間(3G - 4G)
vp_dev->common = map_capability(pci_dev, common,
                    			sizeof(struct virtio_pci_common_cfg), 4,
                    			0, sizeof(struct virtio_pci_common_cfg),
                    			NULL);
                    			
vp_dev->device = map_capability(pci_dev, device, 0, 4,
                        		0, PAGE_SIZE,
                        		&vp_dev->device_len);

map_capability    			
	pci_iomap_range(dev, bar, offset, length)
		if (flags & IORESOURCE_IO)	// 如果BAR空間實現的是IO空間,將其映射到CPU的IO地址空間
        	return __pci_ioport_map(dev, start, len);
    	if (flags & IORESOURCE_MEM)	// 如果BAR空間實現的內存空間,將其映射到CPU的內存地址空間
        	return ioremap(start, len);

註冊virtio-pci方法

  • 當PCI設備的物理探測完成後,virtio-pci設備的探測接近尾聲,最後是註冊所有virtio-pci設備都需要基本方法,包括操作virtqueue和virtio-pci配置空間的
    /* Again, we don't know how much we should map, but PAGE_SIZE
     * is more than enough for all existing devices.
     */
    if (device) {
        vp_dev->device = map_capability(pci_dev, device, 0, 4,
                        0, PAGE_SIZE,
                        &vp_dev->device_len);
        if (!vp_dev->device)
            goto err_map_device;

        vp_dev->vdev.config = &virtio_pci_config_ops;	// 註冊配置空間操作函數
    } else {
        vp_dev->vdev.config = &virtio_pci_config_nodev_ops;
    }
    
    vp_dev->config_vector = vp_config_vector;
    vp_dev->setup_vq = setup_vq;						// 註冊virtqueue初始化函數
    vp_dev->del_vq = del_vq; 

註冊設備到virtio總線

  • virtio-pci設備的probe過程,完成了BAR空間映射,註冊config操作函數和virtqueue操作函數。作爲virtio設備的父類,virtio-pci設備爲它們完成了這些通用操作,接下來就是每個virtio設備的probe。通過register_virtio_device函數向virtio總線註冊設備,可以觸發virtio總線上的match操作,然後進行virtio設備的探測,這裏我們以virtio-blk設備爲例,流程如下:
virtio_pci_probe
	pci_enable_device
	virtio_pci_modern_probe
	register_virtio_device
		dev->dev.bus = &virtio_bus		// 將virtio_device.dev.bus設置成virito總線!!!
		dev->config->reset(dev)			// 復位virtio設備
		virtio_add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE)	// 設置設備狀態爲ACKNOWLEDGE,表示我們已經發現這個virtio設備
		device_register(&dev->dev)		// 向virtio總線註冊設備,觸發總線上的match操作
  • 將PCI設備註冊到virtio總線後,關於virtio-pci設備的初始化就告一段落,後面就是具體virtio設備的驅動加載和探測,包括以下設備,下面一章我們選取block device繼續進行分析
1af4:1041  network device (modern)
1af4:1042  block device (modern)
1af4:1043  console device (modern)
1af4:1044  entropy generator device (modern)
1af4:1045  balloon device (modern)
1af4:1048  SCSI host bus adapter device (modern)
1af4:1049  9p filesystem device (modern)
1af4:1050  virtio gpu device (modern)
1af4:1052  virtio input device (modern)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章