## 8.3.?get_free_page 和其友
如果一個模塊需要分配大塊的內存, 它常常最好是使用一個面向頁的技術. 請求整個頁也有其他的優點, 這個在 15 章介紹.
為分配頁, 下列函數可用:
get_zeroed_page(unsigned int flags);
返回一個指向新頁的指針并且用零填充了該頁.
__get_free_page(unsigned int flags);
類似于 get_zeroed_page, 但是沒有清零該頁.
__get_free_pages(unsigned int flags, unsigned int order);
分配并返回一個指向一個內存區第一個字節的指針, 內存區可能是幾個(物理上連續)頁長但是沒有清零.
flags 參數同 kmalloc 的用法相同; 常常使用 GFP_KERNEL 或者 GFP_ATOMIC, 可能帶有 __GFP_DMA 標志( 給可能用在 ISA DMA 操作的內存 ) 或者 __GFP_HIGHMEM 當可能使用高端內存時. [[29](#)]order 是你在請求的或釋放的頁數的以 2 為底的對數(即, log2N). 例如, 如果你要一個頁 order 為 0, 如果你請求 8 頁就是 3. 如果 order 太大(沒有那個大小的連續區可用), 頁分配失敗. get_order 函數, 它使用一個整數參數, 可以用來從一個 size 中提取 order(它必須是 2 的冪)給主機平臺. order 允許的最大值是 10 或者 11 (對應于 1024 或者 2048 頁), 依賴于體系. 但是, 一個 order-10 的分配在除了一個剛剛啟動的有很多內存的系統中成功的機會是小的.
如果你好奇, /proc/buddyinfo 告訴你系統中每個內存區中的每個 order 有多少塊可用.
當一個程序用完這些頁, 它可以使用下列函數之一來釋放它們. 第一個函數是一個落回第二個函數的宏:
~~~
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
~~~
如果你試圖釋放和你分配的頁數不同的頁數, 內存圖變亂, 系統在后面時間中有麻煩.
值得強調一下, __get_free_pages 和其他的函數可以在任何時候調用, 遵循我們看到的 kmalloc 的相同規則. 這些函數不能在某些情況下分配內存, 特別當使用 GFP_ATOMIC 時. 因此, 調用這些分配函數的程序必須準備處理分配失敗.
盡管 kmalloc( GFP_KERNEL )有時失敗當沒有可用內存時, 內核盡力滿足分配請求. 因此, 容易通過分配太多的內存降低系統的響應. 例如, 你可以通過塞入一個 scull 設備大量數據使計算機關機; 系統開始爬行當它試圖換出盡可能多的內存來滿足 kmalloc 的請求. 因為每個資源在被增長的設備所吞食, 計算機很快就被說無法用; 在這點上, 你甚至不能啟動一個新進程來試圖處理這個問題. 我們在 scull 不解釋這個問題, 因為它只是一個例子模塊并且不是一個真正的放入多用戶系統的工具. 作為一個程序員, 你必須小心, 因為一個模塊是特權代碼并且可能在系統中開啟新的安全漏洞(最可能是一個服務拒絕漏洞好像剛剛描述過的.)
### 8.3.1.?一個使用整頁的 scull: scullp
為了真實地測試頁分配, 我們已隨其他例子代碼發布了 scullp 模塊. 它是一個簡化的 scull, 就像前面介紹過的 scullc.
scullp 分配的內存量子是整頁或者頁集合: scullp_order 變量缺省是 0, 但是可以在編譯或加載時改變.
下列代碼行顯示了它如何分配內存:
~~~
/* Here's the allocation of a single quantum */
if (!dptr->data[s_pos])
{
dptr->data[s_pos] =
(void *)__get_free_pages(GFP_KERNEL, dptr->order);
if (!dptr->data[s_pos])
goto nomem;
memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
}
~~~
scullp 中釋放內存的代碼看來如此:
~~~
/* This code frees a whole quantum-set */
for (i = 0; i < qset; i++)
if (dptr->data[i])
free_pages((unsigned long)(dptr->data[i]), dptr->order);
~~~
在用戶級別, 被感覺到的區別主要是一個速度提高和更好的內存使用, 因為沒有內部的內存碎片. 我們運行一些測試從 scull0 拷貝 4 MB 到 scull1, 并且接著從 scullp0 到 scullp1; 結果顯示了在內核空間處理器使用率有輕微上升.
性能的提高不是激動人心的, 因為 kmalloc 被設計為快的. 頁級別分配的主要優勢實際上不是速度, 而是更有效的內存使用. 按頁分配不浪費內存, 而使用 kmalloc 由于分配的粒度會浪費無法預測數量的內存.
但是 __get_free_page 函數的最大優勢是獲得的頁完全是你的, 并且你可以, 理論上, 可以通過適當的設置頁表來組合這些頁為一個線性的區域. 例如, 你可以允許一個用戶進程 mmap 作為單個不聯系的頁而獲得的內存區. 我們在 15 章討論這種操作, 那里我們展示 scullp 如何提供內存映射, 一些 scull 無法提供的東西.
### 8.3.2.?alloc_pages 接口
為完整起見, 我們介紹另一個內存分配的接口, 盡管我們不會準備使用它直到 15 章. 現在, 能夠說 struct page 是一個描述一個內存頁的內部內核結構. 如同我們將見到的, 在內核中有許多地方有必要使用頁結構; 它們是特別有用的, 在任何你可能處理高端內存的情況下, 高端內存在內核空間中沒有一個常量地址.
Linux 頁分配器的真正核心是一個稱為 alloc_pages_node 的函數:
~~~
struct page *alloc_pages_node(int nid, unsigned int flags,
unsigned int order);
~~~
這個函數頁有 2 個變體(是簡單的宏); 它們是你最可能用到的版本:
~~~
struct page *alloc_pages(unsigned int flags, unsigned int order);
struct page *alloc_page(unsigned int flags);
~~~
核心函數, alloc_pages_node, 使用 3 個參數, nid 是要分配內存的 NUMA 節點 ID[[30](#)], flags 是通常的 GFP_ 分配標志, 以及 order 是分配的大小. 返回值是一個指向描述分配的內存的第一個(可能許多)頁結構的指針, 或者, 如常, NULL 在失敗時.
alloc_pages 簡化了情況, 通過在當前 NUMA 節點分配內存( 它使用 numa_node_id 的返回值作為 nid 參數調用 alloc_pages_node). 并且, 當然, alloc_pages 省略了 order 參數并且分配一個單個頁.
為釋放這種方式分配的頁, 你應當使用下列一個:
~~~
void __free_page(struct page *page);
void __free_pages(struct page *page, unsigned int order);
void free_hot_page(struct page *page);
void free_cold_page(struct page *page);
~~~
如果你對一個單個頁的內容是否可能駐留在處理器緩存中有特殊的認識, 你應當使用 free_hot_page (對于緩存駐留的頁) 或者 free_cold_page 通知內核. 這個信息幫助內存分配器在系統中優化它的內存使用.
### 8.3.3.?vmalloc 和 其友
我們展示給你的下一個內存分配函數是 vmlloc, 它在虛擬內存空間分配一塊連續的內存區. 盡管這些頁在物理內存中不連續 (使用一個單獨的對 alloc_page 的調用來獲得每個頁), 內核看它們作為一個一個連續的地址范圍. vmalloc 返回 0 ( NULL 地址 ) 如果發生一個錯誤, 否則, 它返回一個指向一個大小至少為 size 的連續內存區.
我們這里描述 vmalloc 因為它是一個基本的 Linux 內存分配機制. 我們應當注意, 但是, vmalloc 的使用在大部分情況下不鼓勵. 從 vmalloc 獲得的內存用起來稍微低效些, 并且, 在某些體系上, 留給 vmalloc 的地址空間的數量相對小. 使用 vmalloc 的代碼如果被提交來包含到內核中可能會受到冷遇. 如果可能, 你應當直接使用單個頁而不是試圖使用 vmalloc 來掩飾事情.
讓我們看看 vmalloc 如何工作的. 這個函數的原型和它相關的東西(ioremap, 嚴格地不是一個分配函數, 在本節后面討論)是下列:
~~~
#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(void * addr);
void *ioremap(unsigned long offset, unsigned long size);
void iounmap(void * addr);
~~~
值得強調的是 kmalloc 和 _get_free_pages 返回的內存地址也是虛擬地址. 它們的實際值在它用在尋址物理地址前仍然由 MMU (內存管理單元, 常常是 CPU 的一部分)管理.[[31](#)] vmalloc 在它如何使用硬件上沒有不同, 不同是在內核如何進行分配任務上.
kmalloc 和 __get_free_pages 使用的(虛擬)地址范圍特有一個一對一映射到物理內存, 可能移位一個常量 PAGE_OFFSET 值; 這些函數不需要給這個地址范圍修改頁表. vmalloc 和 ioremap 使用的地址范圍, 另一方面, 是完全地合成的, 并且每個分配建立(虛擬)內存區域, 通過適當地設置頁表.
這個區別可以通過比較分配函數返回的指針來獲知. 在一些平臺(例如, x86), vmalloc 返回的地址只是遠離 kmalloc 使用的地址. 在其他平臺上(例如, MIPS, IA-64, 以及 x86_64 ), 它們屬于一個完全不同的地址范圍. 對 vmalloc 可用的地址在從 VMALLOC_START 到 VAMLLOC_END 的范圍中. 2 個符號都定義在 <asm/patable.h> 中.
vmalloc 分配的地址不能用于微處理器之外, 因為它們只在處理器的 MMU 之上才有意義. 當一個驅動需要一個真正的物理地址(例如一個 DMA 地址, 被外設硬件用來驅動系統的總線), 你無法輕易使用 vmalloc. 調用 vmalloc 的正確時機是當你在為一個大的只存在于軟件中的順序緩沖分配內存時. 重要的是注意 vamlloc 比 __get_free_pages 有更多開銷, 因為它必須獲取內存并且建立頁表. 因此, 調用 vmalloc 來分配僅僅一頁是無意義的.
在內核中使用 vmalloc 的一個例子函數是 create_module 系統調用, 它使用 vmalloc 為在創建的模塊獲得空間. 模塊的代碼和數據之后被拷貝到分配的空間中, 使用 copy_from_user. 在這個方式中, 模塊看來是加載到連續的內存. 你可以驗證, 同過看 /proc/kallsyms, 模塊輸出的內核符號位于一個不同于內核自身輸出的符號的內存范圍.
使用 vmalloc 分配的內存由 vfree 釋放, 采用和 kfree 釋放由 kmalloc 分配的內存的相同方式.
如同 vmalloc, ioremap 建立新頁表; 不同于 vmalloc, 但是, 它實際上不分配任何內存. ioremap 的返回值是一個特殊的虛擬地址可用來存取特定的物理地址范圍; 獲得的虛擬地址應當最終通過調用 iounmap 來釋放.
ioremap 對于映射一個 PCI 緩沖的(物理)地址到(虛擬)內核空間是非常有用的. 例如, 它可用來存取一個 PCI 視頻設備的幀緩沖; 這樣的緩沖常常被映射在高端物理地址, 在內核啟動時建立的頁表的地址范圍之外. PCI 問題在 12 章有詳細解釋.
由于可移植性, 值得注意的是你不應當直接存取由 ioremap 返回的地址好像是內存指針.你應當一直使用 readb 和 其他的在第 9 章介紹的 I/O 函數. 這個要求適用因為一些平臺, 例如 Alpha, 無法直接映射 PCI 內存區到處理器地址空間, 由于在 PCI 規格和 Alpha 處理器之間的在數據如何傳送方面的不同.
ioremap 和 vmalloc 是面向頁的(它通過修改頁表來工作); 結果, 重分配的或者分配的大小被調整到最近的頁邊界. ioremap 模擬一個非對齊的映射通過"向下調整"被重映射的地址以及通過返回第一個被重映射頁內的偏移.
vmalloc 的一個小的缺點在于它無法在原子上下文中使用, 因為, 內部地, 它使用 kmalloc(GFP_KERNEL) 來獲取頁表的存儲, 并且因此可能睡眠. 這不應當是一個問題 -- 如果 __get_free_page 的使用對于一個中斷處理不足夠好, 軟件設計需要一些清理.
### 8.3.4.?一個使用虛擬地址的 scull : scullv
使用 vmalloc 的例子代碼在 scullv 模塊中提供. 如同 scullp, 這個模塊是一個 scull 的簡化版本, 它使用一個不同的分配函數來為設備存儲數據獲得空間.
這個模塊分配內存一次 16 頁. 分配以大塊方式進行來獲得比 scullp 更好的性能, 并且來展示一些使用其他分配技術要花很長時間的東西是可行的. 使用 __get_free_pages 來分配多于一頁是易于失敗的, 并且就算它成功了, 它可能是慢的. 如同我們前面見到的, vmalloc 在分配幾個頁時比其他函數更快, 但是當獲取單個頁時有些慢, 因為頁表建立的開銷. scullv 被設計象 scullp 一樣. order 指定每個分配的"級數"并且缺省為 4. scullv 和 scullp 之間的位于不同是在分配管理上. 這些代碼行使用 vmalloc 來獲得新內存:
~~~
/* Allocate a quantum using virtual addresses */
if (!dptr->data[s_pos])
{
dptr->data[s_pos] =
(void *)vmalloc(PAGE_SIZE << dptr->order);
if (!dptr->data[s_pos])
goto nomem;
memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
}
~~~
以及這些代碼行釋放內存:
~~~
/* Release the quantum-set */
for (i = 0; i < qset; i++)
if (dptr->data[i])
vfree(dptr->data[i]);
~~~
如果你在使能調試的情況下編譯 2 個模塊, 你能看到它們的數據分配通過讀取它們在 /proc 創建的文件. 這個快照從一套 x86_64 系統上獲得:
~~~
salma% cat /tmp/bigfile > /dev/scullp0; head -5 /proc/scullpmem
Device 0: qset 500, order 0, sz 1535135
item at 000001001847da58, qset at 000001001db4c000
0:1001db56000
1:1003d1c7000
salma% cat /tmp/bigfile > /dev/scullv0; head -5 /proc/scullvmem
Device 0: qset 500, order 4, sz 1535135
item at 000001001847da58, qset at 0000010013dea000
0:ffffff0001177000
1:ffffff0001188000
~~~
下面的輸出, 相反, 來自 x86 系統:
~~~
rudo% cat /tmp/bigfile > /dev/scullp0; head -5 /proc/scullpmem
Device 0: qset 500, order 0, sz 1535135
item at ccf80e00, qset at cf7b9800
0:ccc58000
1:cccdd000
rudo% cat /tmp/bigfile > /dev/scullv0; head -5 /proc/scullvmem
Device 0: qset 500, order 4, sz 1535135
item at cfab4800, qset at cf8e4000
0:d087a000
1:d08d2000
~~~
這些值顯示了 2 個不同的行為. 在 x86_64, 物理地址和虛擬地址是完全映射到不同的地址范圍( 0x100 和 0xffffff00), 而在 x86 計算機上, vmalloc ;虛擬地址只在物理地址使用的映射之上.
[[29](#)] 盡管 alloc_pages (稍后描述)應當真正地用作分配高端內存頁, 由于某些理由我們直到 15 章才真正涉及.
[[30](#)] NUMA (非統一內存存取) 計算機是多處理器系統, 這里內存對于特定的處理器組("節點")是"局部的". 對局部內存的存取比存取非局部內存更快. 在這樣的系統, 在當前節點分配內存是重要的. 驅動作者通常不必擔心 NUMA 問題, 但是.
[[31](#)] 實際上, 一些體系結構定義"虛擬"地址為保留給尋址物理內存. 當這個發生了, Linux 內核利用這個特性, 并且 kernel 和 __get_free_pages 地址都位于這些地址范圍中的一個. 這個區別對設備驅動和其他的不直接包含到內存管理內核子系統中的代碼是透明的
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考