標籤:

Tegra TX2一瞥3:nvmap

這是這個系列最後一篇,看看TX2的GPU和host是怎麼共享內存的。主要是要單獨看看nvmap這個模塊的工作原理。

TX2的內核源代碼是這樣放的:

display kernel-4.4 nvgpu nvgpu-t18x nvhost nvhost-t18x nvmap nvmap-t18x t18x

沒有上傳的代碼不是Patch,而是一個個目錄樹的形態。但裡面非常完整,連NVIDIA-REVIEWERS這種文件都是按MAINTAINER的格式整理的,說起來應該是按隨時可以upstream的方式設計的,但整個功能通用性不強,估計upstream的難度不低。

其中gpu的驅動在nvgpu目錄下(如前所述,drm驅動是已經upstream的,在kernel-4.4目錄中)。這個Kernel外的模塊代碼量93,913Loc)。而nvmap是個獨立的目錄(代碼量6,334Loc),nvgpu強依賴於nvmap。由於沒有資料,nvgpu目錄下的程序不太好看懂,很多概念比如TSG是什麼,估計需要分析很久,所以我們從一個相對獨立邏輯空間來看看它的語義是怎麼樣的。(這也是想介紹一下,應該如何具體解構一個已經存在的設計)

在分析這個邏輯前,我們先看看計算加速器設計中的一個關鍵問題:內存管理。

比如我做一個大型的矩陣計算,一個100x100的矩陣乘以另一個100x100的矩陣,用32位整數,這樣我就有120000個位元組的空間要處理,放到4K的頁中,就需要接近30頁的內存需要訪問。我一開始在CPU一側整理這些數據,這些數據當然是放在靠近CPU的內存中比較實在,這樣CPU算起來比較快。但準備好了,我希望這個乘法讓加速器(比如GPU)給我算。那麼GPU離這些內存就比較遠了,每個乘法加法,都要經過層層匯流排節點更新到內存里,這還不如CPU自己算。所以,這是個兩難的問題,根據不同的匯流排帶寬和時延,可以考慮的方案有:

  1. 內存選擇在CPU一側(讓加速器吃虧)
  2. 內存選擇在加速器一側(讓CPU吃虧)
  3. 內存開始的時候選擇在CPU一側,但某一頁的內容被加速器訪問的時候,拷貝到GPU一側,完成計算後,如果CPU訪問這個內存,再從加速器拷貝回CPU一側。這個過程,既可以是人工的,也可以是動態的。

這裡還有一個地址空間的問題,一般的Linux內核驅動,如果一片內存要分享給設備,需要先做dma=dma_map(va),其中va是CPU的地址,dma是設備的地址。這樣一來,如果CPU要把一個地址提交給設備,就需要轉換到另一個地址空間。這種情況對於那些「弱智外設」,比如網卡,SAS等控制器,大部分時候做DMA就是為了拷貝一段數據到外設上,這當然沒有問題,但對於那些「智能設備」如加速器,很可能我的數據中就帶有指針,我的頭指針可以給你dma_map(),你不能要求我把裡面的指針也全部替換了吧?

最後是,現代加速器(特別是計算加速器),基本上要求服務於用戶態,所以,前面的問題,都要在用戶態解決。

我們關鍵看這個代碼如何處理這兩個問題的。

首先,nvmap的配置項如下(把子配置項和功能無關的項忽略了,只看大特性):

NVMAP_HIGHMEM_ONLY:分配內存時僅用HIMEM(64位系統要這東西幹嘛?)

NVMAP_PAGE_POOLS:基於內存池管理內存分配

NVMAP_CACHE_MAINT_BY_SET_WAYS:Cache分配演算法

NVMAP_DMABUF_STASH:重用DMABUF,降低重新分配成本

NVMAP_FORCE_ZEROED_USER_PAGES:安全功能,分配用戶內存

NVMAP_DEFER_FD_RECYCLE:延遲FD號的使用(為後續分配重用)

沒有什麼涉及主功能的配置項,都是優化類的設計,這對構架分析來說是個好消息。

程序入口在nvmap_init中,實現為一個名叫tegra-carveouts的platform_device。查一下carveout的含義,它是「切割出」的意思,也是CPU側切出一片內存給GPU用這個商業特性的名字。

以此為根,遍歷整個代碼樹,功能分列如下:

nvmap_init.c:平台設備驅動總入口,配置參數管理

nvmap_dev.c:platform_driver實現,註冊為misc設備/dev/nvmap,核心是提供ioctl控制

nvmap_ioctl.c:具體實現nvmap_dev.c中的ioctl功能

nvmap_alloc.c:handle分配管理

nvmap.c:基於handle進行內存分配

nvmap_cache.c:在debugfs中創建介面進行cache控制,這裡的cache控制指的是CPU一側的控制,通過調用msr等動作實現的。這個介面的存在,說明CPU和GPU間不是CC的。

nvmap_dmabuf.c:nv版本的dma_buf實現,dma_buf是內核用於用戶態直接對設備做DMA的一個封裝[1]

nvmap_fault.c:實現vma的ops,進行fault處理,主要從handle預分配的空間中取

nvmap_handle.c:handle管理,主要是做一個rb樹,建立vma和處理handle的關聯

nvmap_heap.c:kmem_cache的封裝(kmem_cache是內核一個管理固定大小內存的一個數據結構),用作CPU/GPU共享內存,這時稱為Carveout。

nvmap_mm.c:進程的mm管理,是cache管理的一部分

nvmap_pp.c:page pool管理,自行管理了一個page list,NVMAP_IOC_ALLOC要求分配的也都在這裡管理的

nvmap_tag.c:handle的名字管理,提供給handle命名和基於名字訪問的能力

如果我們把優化性的語義收縮一下,比如pp和heap如果不做池化,就是個簡單的alloc_page()和kmalloc,tag如果不做rb樹的管理,就是個handle name。cache操作是CPU一側的,不封裝就是基本流程中的其中一步……

這樣,我們可以初步猜測這個系統的功能是這樣構造的:

實現一個平台設備,註冊為misc設備,用戶態通過ioctl分配handle,基於handle提供vma的分配,分配後的頁面依靠自己管理的page進行填充,提供dma_buf介面,剩下的功能都是用戶態如何基於handle對設備進行硬體交互了。

為了確認這一點,接著看看ioctl的設計:

NVMAP_IOC_CREATE/NVMAP_IOC_FROM_FD:創建handle和dma_buf

NVMAP_IOC_FROM_VA:同上,但同時設置vma

NVMAP_IOC_FROM_IVC_ID:同上,但同時分配Carveout

NVMAP_IOC_GET_FD:查找功能,FD2handle

NVMAP_IOC_GET_IVC_ID:同上,FD2VimID

NVMAP_IOC_GET_IVM_HEAPS:列出支持IVM的所有carveout內存塊

NVMAP_IOC_ALLOC:為handle分配page

NVMAP_IOC_GET_IVC_ID:同上,但從Carveout分配

NVMAP_IOC_VPR_FLOOR_SIZE:似乎是設置特定設備的最小DMA緩衝

NVMAP_IOC_FREE:靠,這是關掉handle的fd(而不是釋放Page)

NVMAP_IOC_WRITE/READ:數據讀寫,WTF,就是說至少部分數據必須通過系統調用來訪問(看不到用戶庫的代碼,不好猜)

NVMAP_IOC_CACHE:Cache操作

NVMAP_IOC_CACHE_LIST:同上,維護類功能。

NVMAP_IOC_GUP_TEST:不用test了,這個東西邏輯上肯定是有問題的:用戶態DMA的問題

NVMAP_IOC_SET_TAG_LABEL:這是給handle命名

基本不改變前面的邏輯,關鍵是這裡有一個Carveout的概念,我猜在普通的實現上,這個就是普通內存,從CPU直接分配kmemcache來共享,如果是高性能實現,就是不同的內存,然後靠兩邊的讀寫來產生缺頁來實現拷貝過程。

再看看handle的數據結構:

struct nvmap_handle { struct rb_node node; /* entry on global handle tree */ atomic_t ref; /* reference count (i.e., # of duplications) */ atomic_t pin; /* pin count */ u32 flags; /* caching flags */ size_t size; /* padded (as-allocated) size */ size_t orig_size; /* original (as-requested) size */ size_t align; struct nvmap_client *owner; struct dma_buf *dmabuf; union { struct nvmap_pgalloc pgalloc; struct nvmap_heap_block *carveout; }; bool heap_pgalloc; /* handle is page allocated (sysmem / iovmm) */ bool alloc; /* handle has memory allocated */ bool from_va; /* handle memory is from VA */ u32 heap_type; /* handle heap is allocated from */ u32 userflags; /* flags passed from userspace */ void *vaddr; /* mapping used inside kernel */ struct list_head vmas; /* list of all user vmas */ atomic_t umap_count; /* number of outstanding maps from user */ atomic_t kmap_count; /* number of outstanding map from kernel */ atomic_t share_count; /* number of processes sharing the handle */ struct list_head lru; /* list head to track the lru */ struct mutex lock; struct list_head dmabuf_priv; u64 ivm_id; int peer; /* Peer VM number */};

rbtree,成組的vma,多個dmabuf,cpu/gpu二選一的page或者carveout……基本上和前面的邏輯一致。

再快速查一次nvgpu一側的代碼,除了by-pass-smmu以外,沒有獨立的SMMU操作,查所有的中斷處理,都是關於channel的,沒有關於SMMU的中斷處理,所以基本上可以認為,這個方案是處理不了設備一側的缺頁的。

這個方案看起來挺……簡陋的,重點還是聚焦在基本功能架構的一般優化上,沒到向上提煉構架的程度。

從功能上說,最不好的一點就是和IOMMU是結合不起來的,但它完全基於FD的管理比WrapDrive基於vfio-mdev的管理帶來一個好處,就是進程退出,通道就自動回收了,WD現在做這個功能不好做。另外,WD需要考慮這裡的另外兩個需求:

  1. 它的Host和加速器之間不是CC的,得有個辦法把Cache操作插入到語義中
  2. WD現在沒有考慮支持大頁

附錄:

[1] dma_buf的功能在內核Documentation/driver-api/dma-buf.rst中表述。如果我沒有記錯,這是當初三星在Linaro首先推的特性,它主要解決像視頻播放器這種:需要軟體動兩下,轉給硬體動兩下,然後軟體再動兩下,交給下個硬體動兩下這樣的場景的。它的核心就是讓用戶態可以分配一片DMA內存,然後讓這個內存可以在多個驅動和進程之間互相傳遞。


推薦閱讀:

知不知
PCIE匯流排的地址問題
守弱的內涵和外延
Tegra TX2一瞥2:IO子系統
氣和深度學習3:回到氣的問題

TAG:軟體架構 |