## 8.2.?后備緩存
一個設備驅動常常以反復分配許多相同大小的對象而結束. 如果內核已經維護了一套相同大小對象的內存池, 為什么不增加一些特殊的內存池給這些高容量的對象? 實際上, 內核確實實現了一個設施來創建這類內存池, 它常常被稱為一個后備緩存. 設備驅動常常不展示這類的內存行為, 它們證明使用一個后備緩存是對的, 但是, 有例外; 在 Linux 2.6 中 USB 和 SCSI 驅動使用緩存.
Linux 內核的緩存管理者有時稱為" slab 分配器". 因此, 它的功能和類型在 <linux/slab.h> 中聲明. slab 分配器實現有一個 kmem_cache_t 類型的緩存; 使用一個對 kmem_cache_create 的調用來創建它們:
~~~
kmem_cache_t *kmem_cache_create(const char *name, size_t size,
size_t offset,
unsigned long flags,
void (*constructor)(void *, kmem_cache_t *,
unsigned long flags), void (*destructor)(void *, kmem_cache_t *, unsigned long flags));
~~~
這個函數創建一個新的可以駐留任意數目全部同樣大小的內存區的緩存對象, 大小由 size 參數指定. name 參數和這個緩存關聯并且作為一個在追蹤問題時有用的管理信息; 通常, 它被設置為被緩存的結構類型的名子. 這個緩存保留一個指向 name 的指針, 而不是拷貝它, 因此驅動應當傳遞一個指向在靜態存儲中的名子的指針(常常這個名子只是一個文字字串). 這個名子不能包含空格.
offset 是頁內的第一個對象的偏移; 它可被用來確保一個對被分配的對象的特殊對齊, 但是你最可能會使用 0 來請求缺省值. flags 控制如何進行分配并且是下列標志的一個位掩碼:
SLAB_NO_REAP
設置這個標志保護緩存在系統查找內存時被削減. 設置這個標志通常是個壞主意; 重要的是避免不必要地限制內存分配器的行動自由.
SLAB_HWCACHE_ALIGN
這個標志需要每個數據對象被對齊到一個緩存行; 實際對齊依賴主機平臺的緩存分布. 這個選項可以是一個好的選擇, 如果在 SMP 機器上你的緩存包含頻繁存取的項. 但是, 用來獲得緩存行對齊的填充可以浪費可觀的內存量.
SLAB_CACHE_DMA
這個標志要求每個數據對象在 DMA 內存區分配.
還有一套標志用來調試緩存分配; 詳情見 mm/slab.c. 但是, 常常地, 在用來開發的系統中, 這些標志通過一個內核配置選項被全局性地設置
函數的 constructor 和 destructor 參數是可選函數( 但是可能沒有 destructor, 如果沒有 constructor ); 前者可以用來初始化新分配的對象, 后者可以用來"清理"對象在它們的內存被作為一個整體釋放回給系統之前.
構造函數和析構函數會有用, 但是有幾個限制你必須記住. 一個構造函數在分配一系列對象的內存時被調用; 因為內存可能持有幾個對象, 構造函數可能被多次調用. 你不能假設構造函數作為分配一個對象的一個立即的結果而被調用. 同樣地, 析構函數可能在以后某個未知的時間中調用, 不是立刻在一個對象被釋放后. 析構函數和構造函數可能或不可能被允許睡眠, 根據它們是否被傳遞 SLAB_CTOR_ATOMIC 標志(這里 CTOR 是 constructor 的縮寫).
為方便, 一個程序員可以使用相同的函數給析構函數和構造函數; slab 分配器常常傳遞 SLAB_CTOR_CONSTRUCTOR 標志當被調用者是一個構造函數.
一旦一個對象的緩存被創建, 你可以通過調用 kmem_cache_alloc 從它分配對象.
~~~
void *kmem_cache_alloc(kmem_cache_t *cache, int flags);
~~~
這里, cache 參數是你之前已經創建的緩存; flags 是你會傳遞給 kmalloc 的相同, 并且被參考如果 kmem_cache_alloc 需要出去并分配更多內存.
為釋放一個對象, 使用 kmem_cache_free:
~~~
void kmem_cache_free(kmem_cache_t *cache, const void *obj);
~~~
當驅動代碼用完這個緩存, 典型地當模塊被卸載, 它應當如下釋放它的緩存:
~~~
int kmem_cache_destroy(kmem_cache_t *cache);
~~~
這個銷毀操作只在從這個緩存中分配的所有的對象都已返回給它時才成功. 因此, 一個模塊應當檢查從 kmem_cache_destroy 的返回值; 一個失敗指示某類在模塊中的內存泄漏(因為某些對象已被丟失.)
使用后備緩存的一方面益處是內核維護緩沖使用的統計. 這些統計可從 /proc/slabinfo 獲得.
### 8.2.1.?一個基于 Slab 緩存的 scull: scullc
是時候給個例子了. scullc 是一個簡化的 scull 模塊的版本, 它只實現空設備 -- 永久的內存區. 不象 scull, 它使用 kmalloc, scullc 使用內存緩存. 量子的大小可在編譯時和加載時修改, 但是不是在運行時 -- 這可能需要創建一個新內存區, 并且我們不想處理這些不必要的細節.
scullc 使用一個完整的例子, 可用來試驗 slab 分配器. 它區別于 scull 只在幾行代碼. 首先, 我們必須聲明我們自己的 slab 緩存:
~~~
/* declare one cache pointer: use it for all devices */
kmem_cache_t *scullc_cache;
~~~
slab 緩存的創建以這樣的方式處理( 在模塊加載時 ):
~~~
/* scullc_init: create a cache for our quanta */
scullc_cache = kmem_cache_create("scullc", scullc_quantum,
0, SLAB_HWCACHE_ALIGN, NULL, NULL); /* no ctor/dtor */
if (!scullc_cache)
{
scullc_cleanup();
return -ENOMEM;
}
~~~
這是它如何分配內存量子:
~~~
/* Allocate a quantum using the memory cache */
if (!dptr->data[s_pos])
{
dptr->data[s_pos] = kmem_cache_alloc(scullc_cache, GFP_KERNEL);
if (!dptr->data[s_pos])
goto nomem;
memset(dptr->data[s_pos], 0, scullc_quantum);
}
~~~
還有這些代碼行釋放內存:
~~~
for (i = 0; i < qset; i++)
if (dptr->data[i])
kmem_cache_free(scullc_cache, dptr->data[i]);
~~~
最后, 在模塊卸載時, 我們不得不返回緩存給系統:
~~~
/* scullc_cleanup: release the cache of our quanta */
if (scullc_cache)
kmem_cache_destroy(scullc_cache);
~~~
從 scull 到 scullc 的主要不同是稍稍的速度提升以及更好的內存使用. 因為量子從一個恰好是合適大小的內存片的池中分配, 它們在內存中的排列是盡可能的密集, 與 scull 量子的相反, 它帶來一個不可預測的內存碎片.
### 8.2.2.?內存池
在內核中有不少地方內存分配不允許失敗. 作為一個在這些情況下確保分配的方式, 內核開發者創建了一個已知為內存池(或者是 "mempool" )的抽象. 一個內存池真實地只是一類后備緩存, 它盡力一直保持一個空閑內存列表給緊急時使用.
一個內存池有一個類型 mempool_t ( 在 <linux/mempool.h> 中定義); 你可以使用 mempool_create 創建一個:
~~~
mempool_t *mempool_create(int min_nr,
mempool_alloc_t *alloc_fn,
mempool_free_t *free_fn,
void *pool_data);
~~~
min_nr 參數是內存池應當一直保留的最小數量的分配的對象. 實際的分配和釋放對象由 alloc_fn 和 free_fn 處理, 它們有這些原型:
~~~
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
~~~
給 mempool_create 最后的參數 ( pool_data ) 被傳遞給 alloc_fn 和 free_fn.
如果需要, 你可編寫特殊用途的函數來處理 mempool 的內存分配. 常常, 但是, 你只需要使內核 slab 分配器為你處理這個任務. 有 2 個函數 ( mempool_alloc_slab 和 mempool_free_slab) 來進行在內存池分配原型和 kmem_cache_alloc 和 kmem_cache_free 之間的感應淬火. 因此, 設置內存池的代碼常常看來如此:
~~~
cache = kmem_cache_create(. . .);
pool = mempool_create(MY_POOL_MINIMUM,mempool_alloc_slab, mempool_free_slab, cache);
~~~
一旦已創建了內存池, 可以分配和釋放對象,使用:
~~~
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
~~~
當內存池創建了, 分配函數將被調用足夠的次數來創建一個預先分配的對象池. 因此, 對 mempool_alloc 的調用試圖從分配函數請求額外的對象; 如果那個分配失敗, 一個預先分配的對象(如果有剩下的)被返回. 當一個對象被用 mempool_free 釋放, 它保留在池中, 如果對齊預分配的對象數目小于最小量; 否則, 它將被返回給系統.
一個 mempool 可被重新定大小, 使用:
~~~
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);
~~~
這個調用, 如果成功, 調整內存池的大小至少有 new_min_nr 個對象. 如果你不再需要一個內存池, 返回給系統使用:
~~~
void mempool_destroy(mempool_t *pool);
~~~
你編寫返回所有的分配的對象, 在銷毀 mempool 之前, 否則會產生一個內核 oops.
如果你考慮在你的驅動中使用一個 mempool, 請記住一件事: mempools 分配一塊內存在一個鏈表中, 對任何真實的使用是空閑和無用的. 容易使用 mempools 消耗大量的內存. 在幾乎每個情況下, 首選的可選項是不使用 mempool 并且代替以簡單處理分配失敗的可能性. 如果你的驅動有任何方法以不危害到系統完整性的方式來響應一個分配失敗, 就這樣做. 驅動代碼中的 mempools 的使用應當少.
- 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. 快速參考