2015年5月11日 星期一

LDD3 潤飾 4.2 Debugging by printing 用printk除錯

最常用的除錯技術是monitoring, 在應用程序編程當中是通過在合適的地方調用printf 來實現. 在你調試內核代碼時, 你可以通過printk 來達到這個目的.

4.2.1. printk

我們在前面幾章中使用printk 函數, 簡單地假設它如同printf 一樣使用. 現在到時候介紹一些不同的地方了.
一個不同是printk 允許你根據 message的嚴重程度對其分類, 通過附加不同的loglevel或者優先級在message上. 你常常用一個macro定義來指示loglevel. 例如, KERN_INFO, 我們之前曾在一些print statements的前綴看到過, 是其中一種可能的message loglevel. loglevel macro定義擴展成一個字串, 在編譯時與message文字連接在一起; 這就是為什麼下面的在優先級和格式串之間沒有逗號的原因. 這裡有2 個printk 命令的例子, 一個除錯message, 一個critical message:
printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE__);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
有8 種可能的loglevel字串, 在頭文件<linux/kernel.h> 裡定義; 我們按照嚴重性遞減的順序列出它們:
KERN_EMERG
用於緊急message, 常常是那些crash前的message.
KERN_ALERT
需要立刻動作的情形.
KERN_CRIT
嚴重情況, 常常與嚴重的硬體或者軟體失效有關.
KERN_ERR
用來報告錯誤情況; device driver常常使用KERN_ERR 來報告硬體故障.
KERN_WARNING
有問題的情況的警告, 這些情況自己不會引起系統的嚴重問題.
KERN_NOTICE
正常但是仍然值得注的情況 . 在這個級別會報告一些安全相關的情況.
KERN_INFO
信息型message. 在這個級別, 很多driver會print在啟動時發現的硬體的信息.
KERN_DEBUG
用作debug的message.
每個字串( 在macro定義擴展裡)代表一個在角括號中的整數. 整數的範圍從0 到7, 越小的數表示越大的優先級.
一條沒有指定優先級的printk 語句預設值是DEFAULT_MESSAGE_LOGLEVEL, 在kernel/printk.c 裡被指定為一個整數. 在2.6.10 內核中, DEFAULT_MESSAGE_LOGLEVEL 是KERN_WARNING, 但是在過去已知是改變的.
基於loglevel, 內核可能print message到當前console, 可能是一個text-mode ternimal, 序列埠, 或者是一台並列埠印表機. 如果優先級小於整型值console_loglevel, message被遞交給console, 一次一行( 提供一個新行結尾時,才會發送). 如果klogd 和syslogd 都在系統中運行, 內核消息被追加到/var/log/messages (或者另外根據你的syslogd 配置處理), 獨立於console_loglevel之外. 如果klogd沒有運行, 你只有讀/proc/kmsg ( 用dmsg 命令最易做到)將消息取到user space. 當使用klogd 時, 你應當記住, 它不會保存連續的同樣的行; 它只保留第一個這樣的行, 隨後是, 它收到的重複行數.
變數console_loglevel 初始化成DEFAULT_CONSOLE_LOGLEVEL, 並且可通過sys_syslog system call修改. 一種修改它的方法是在調用klogd 時指定-c 開關, 在klogd 的manpage 裡有指定. 注意要改變當前值, 你必須先殺掉klogd , 接著使用-c 選項重啟它. 另外, 你可寫一個program來改變console loglevel. 你會在由O' Reilly 提供的FTP 站點上的miscprogs/setlevel.c發現這樣一個program的版本. 新的級別指定未一個整數, 在1 和8 之前, 包含1 和8. 如果它設為1, 只有0 級消息( KERN_EMERG )到達console; 如果它設為8, 所有消息, 包括調試消息, 都顯示.
也可以通過文本文件/proc/sys/kernel/printk 讀寫console loglevel. 這個文件有4 個整數值: 當前loglevel,沒有明確loglevel的message的預設級別, 最小允許的loglevel, 以及啟動時預設loglevel. 寫一個值到這個文件就改變當前loglevel成這個值; 因此, 例如, 你可以使所有內核message出現在console, 通過簡單地輸入:
 # echo 8 > /proc/sys/kernel/printk 
現在應當清楚了為什麼hello.c 例子使用KERN_ALERT 標誌; 它們是要確保message會出現在console上.

4.2.2. 重新導向console messages

Linux 在console記錄策略上允許一些靈活性, 它允許你發送消息到一個指定的虛擬console(如果你的conslole使用的是text screen). 預設, 這個"console"是當前虛擬終端. 為了選擇一個不同地虛擬終端來接收消息, 你可對任何console設備調用ioctl(TIOCLINUX). 下面的程序, setconsole, 可以用來選擇哪個控制台接收kernel message; 它必須由superuser運行, 可以從misc-progs 目錄得到.
下面是全部程序. 應當使用一個參數來指定用以接收message的console的編號.
int main(int argc, char **argv)
{
    char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */
    if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
    else {

        fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1); } if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) { /* use stdin */
        fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n",
                argv[0], strerror(errno));
        exit(1);
    }
    exit(0);
}
setconsole 使用特殊的ioctl 命令TIOCLINUX, 來實現特定於linux 的功能. 為使用TIOCLINUX, 你傳遞它一個指向byte array的pointer作為參數. 數組的第byte字節是一個數, 指定需要的子命令,下面的字節是特對於子命令的. 在setconsole 裡, 使用子命令11, 下一個byte(存於bytes[1])指定虛擬console. TIOCLINUX 的完整描述在內核源碼的drivers/char/tty_io .c 裡.

4.2.3. message是如何記錄的

printk 函數將message寫入一個__LOG_BUF_LEN byte長的circule buffer, 長度值從4 KB 到1 MB, 由配置kernel時選擇. 這個函數接著喚醒任何在等待message的process, 就是說, 任何在syslog system call中sleeping或者在讀取/proc/kmsg 的process. 這2 個logging engine的interface幾乎是等同的, 但是注意, 從/proc/kmsg 中讀取是從log buffer中消耗掉data, 然而syslog system call能夠選擇地將data再還回去log buffer,同時保留它給其他process. 通常, 讀取/proc 文件容易些並且是klogd 的預設做法. dmesg 命令可用來查看buffer的內容, 不會沖掉它; 實際上, 這個命令將緩存區的整個內容返回給stdout, 不管它是否已經被讀過.
在停止klogd 後, 如果你偶爾人工讀取kernel message, 你會發現/proc 看起來像一個FIFO, 讀者阻塞在裡面, 等待更多數據. 顯然, 你無法以這種方式讀messgae, 如果klogd 或者其他進程已經在讀同樣的數據, 因為你要競爭它.
如果circule buffer填滿, printk 繞回並在buffer的開頭增加新數據, 覆蓋掉最老的數據. 因此, 這個記錄過程會丟失最老的數據. 這個問題相比於使用這樣一個circule buffer的優點是可以忽略的. 例如, circule bffer允許系統即便沒有一個logging process也可運行, 在沒有人讀它的時候可以通過覆蓋舊數據浪費最少的memory. Linux 對於message的解決方法的另一個特性是, printk 可以從任何地方調用, 甚至從一個中斷處理裡面, 沒有限制能print多少數據. 唯一的缺點是可能丟失一些數據.
如果klogd process在運行, 它獲取kernel message並分發給syslogd, syslogd 接著檢查/etc/syslog.conf 來找出如何處理它們. syslogd 根據一個facility和一個priority來區分message; 這個facility和priority的允許值在<sys/syslog.h> 中定義. kernel message由LOG_KERN facility來記錄, 在一個對應於printk 使用的priority上(例如, LOG_ERR 用於KERN_ERR message). 如果klogd 沒有運行, 數據保留在circular buffer中直到有人讀它或者buffer被覆蓋.
如果你要避免你的系統被來自你的driver的監視message擊垮,你或者給klogd指定一個-f (文件)選項來指示它保存message息到一個特定的文件,或者定制/etc/syslog.conf來適應你的要求.但是另外一種可能性是採用粗暴的方式:殺掉klogd和詳細地print message在一個沒有用到的虛擬終端上, 13 ]或者從一個沒有用到的xterm上發出命令cat /proc/kmsg.

4.2.4. 打開和關閉message

在driver開發的早期, printk 非常有助於調試和測試新代碼. 當你正式發行drver時, 換句話說, 你應當去掉, 或者至少關閉, 這些print語句. 不幸的是, 你很可能會發現,就在你認為你不再需要這些message並去掉它們時, 你要在driver中實現一個新特性(或者有人發現了一個bug), 你想要至少再打開一個消message. 有幾個方法來解決這2個問題, 全局性地打開或關閉你地調試message和打開或關閉單個message.
這裡我們展示一種編碼printk 調用的方法, 你可以單獨或全局地打開或關閉它們; 這個技術依靠定義一個macro, 在你想使用它時就轉變成一個printk (或者printf)調用.
  • 每個printk 語句可以打開或關閉, 通過去除或添加單個字符到macro定義的名字.
  • 所有消息可以馬上關閉, 通過在編譯前改變CFLAGS 變量的值.
  • 同一個print 語句可以在kernel代碼和user level代碼中使用, 因此對於格外的message,driver和測試程序能以同樣的方式被管理.
下面的代碼片斷實現了這些特性, 直接來自頭文件scull.h:
#undef PDEBUG /* undef it, just in case */
#ifdef SCULL_DEBUG
# ifdef __KERNEL__

/* This one if debugging is on, and kernel space */
# define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args)
# else

/* This one for user space */
# define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
# endif
#else
# define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif

#undef PDEBUGG #define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */
符號PDEBUG 定義和去定義, 取決於SCULL_DEBUG 是否定義, 和以何種方式顯示消息適合代碼運行的環境: 當它在內核中就使用內核調用printk, 在用戶空間運行就使用libc 調用fprintf 到標準錯誤輸出. PDEBUGG 符號, 換句話說, 什麼不作; 他可用來輕易地"註釋" print 語句, 而不用完全去掉它們.
為進一步簡化過程, 添加下面的行到你的makfile 裡:
# Comment/uncomment the following line to disable/enable debugging
DEBUG = y

# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
 DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
 DEBFLAGS = -O2
endif

CFLAGS += $(DEBFLAGS) 
本節中出現的macro定義依賴gcc 對ANSI C 預處理器的擴展, 支持帶可變數量的參數的macro定義. 這個gcc 依賴不應該是個問題, 因為無論如何kernel固有的非常依賴於gcc ​​特性. 另外, makefile 依賴GNU 版本的make; 再一次, kernel也依賴GNU make, 所以這個依賴不是問題.
如果你熟悉C 預處理器, 你可以擴展給定的定義來實現一個"debug level"的概念, 定義不同的級別, 安排一個整數(或者bit mask)值給每個級別, 以便決定它應當多麼詳細.
但是每個driver有它自己的特性和監視需求. 好的編程技巧是在靈活性和效率之間選擇最好的平衡, 我們無法告訴你什麼是最好的. 記住, 預處理器條件(連同代碼中的常數表達式)在編譯時執行, 因此你必須重新編譯來打開或改變message. 一個可能的選擇是使用C 條件句, 它在運行時執行, 因而, 能允許你在出現執行時打開或改變message機制. 這是一個好的特性, 但是它在每次代碼執行時需要額外的處理, 這樣即便message給關閉了也會影響效率. 有時這個效率損失無法接受.
本節出現的macro定義已經證明在多種情況下是有用的, 唯一的缺點是要求在任何對它的message改變後重新編譯.

4.2.5. 速率限制

如果你不小心, 你會發現自己用printk 產生了上千條message, 壓倒了控制台並且, 可能地, 使系統日誌文件溢出. 當使用一個慢速控制台設備(例如, 一個serial port), 過量的message速率也能拖慢系統或者只是使它不反應了. 非常難於著手於系統出錯的地方, 當控制台不停地輸出數據. 因此, 你應當非常注意你print什麼, 特別在driver的產品版本以及特別在初始化完成後. 通常, 產品代碼在正常操作時不應當print任何東西; print的輸出應當是指示需要注意的異常情況.
另一方面, 你可能想發出一個log message, 如果driver的device停止工作. 但是你應當小心不要做過了頭. 一個面對失敗永遠繼續的傻瓜process能產生每秒上千次的嘗試; 如果你的driver每次都打印"my device is broken", 它可能產生大量的輸出, 如果控制台設備慢就有可能霸占CPU -- 沒有中斷用來驅動控制台, 就算是一個serial port或者一個line printer.
在很多情況下, 最好的做法是設置一個flag說, "我已經抱怨過這個了", 並不print任何後來的message只要這個flag設置著. 然而, 有幾個理由偶爾發出一個"device還是壞的"的提示. kernel已經提供了一個函數幫助這個情況:
int printk_ratelimit(void); 
這個函數應當在你認為print一個可能會常常重複的message之前調用. 如果這個函數返回非零值, 繼續print你的message, 否則跳過它. 這樣, 典型的調用如這樣:
if (printk_ratelimit())
    printk(KERN_NOTICE "The printer is still on fire\n");
printk_ratelimit 通過跟踪多少message發向控制台而工作. 當輸出級別超過一個限度, printk_ratelimit 開始返回0 並使message被扔掉.
printk_ratelimit 的行為可以通過修改/proc/sys/kern/printk_ratelimit( 在重新使能message前等待的秒數) 和/proc/sys/kernel/printk_ratelimit_burst(限速前可接收的message數)來定制.

4.2.6. print設備編號

偶爾地, 當從一個driver print message, 你會想print與感興趣的硬件相關聯的device number. print major number不是特別難, 但是, 為一致性考慮, kernel提供了一些實用的macro定義( 在<linux/kdev_t.h> 中定義)用於這個目的:
int print_dev_t(char *buffer, dev_t dev); 
char *format_dev_t(char *buffer, dev_t dev);
兩個macro定義都將device number編進給定的緩衝區; 唯一的區別是print_dev_t 返回print的charcters數, 而format_dev_t 返回buffer; 因此, 它可以直接用作printk 調用的參數, 但是必須記住printk只有提供一個結尾的新行才會刷行. buffer應當足夠大以存放一個device number; 如果64 位編號在以後的kernel發行中明顯可能, 這個buffer應當可能至少是20 bytes長.

2015年4月12日 星期日

LDD3 潤飾 4.1 Debugging Support in the Kernel 內核中的除錯支援

在第2 章, 我們建議你建立並安裝你自己的內核, 而不是運行來自你的發布商的現成的內核. 運行你自己的內核的最充分的理由之一是內核開發者已經在內核自身中構建了多個debug特性. 這些特性能產生額外的輸出並降低性能, 因此發布商的產品內核中往往不會啟動它們. 但是, 作為一個內核開發者, 你有不同的優先權並且會樂於接收這些格外的內核debug支持帶來的開銷.
這裡, 我們列出用來開發的內核應當啟動的配置選項. 除了另外註明, 不管你喜歡什麼樣的內核配置工具,所有的這些選項都在"kernel hacking" 菜單, . 注意有些選項不是所有體系都支援.
CONFIG_DEBUG_KERNEL
這個選項只是使其他debug選項可用; 它應當打開, 但是它自己不啟動任何的特性.
CONFIG_DEBUG_SLAB
這個重要的選項打開了內核memory allocation函數的幾項檢查; 啟動這些檢查, 就可能探測到一些記憶體覆蓋和遺漏初始化的錯誤. 在遞交給call者之前,被allocated的每一個byte都設成0xa5,隨後在釋放時被設成0x6b. 如果你在任何時候見到任一個這種"posion"模式重複出現在你的driver輸出(或者常常在一個oops 的列表), 你會確切知道去找什麼類型的錯誤. 當啟動debug, 內核還會在每個allocated memory對象的前後放置特別的guarded values; 如果這些值曾被改動, 內核知道有人已覆蓋了一個memory allocation, 它會出現警示. 對更模糊的問題的各種檢查也給啟動了.
CONFIG_DEBUG_PAGEALLOC
full pages在釋放時被從kernel address space去除. 這個選項會顯著拖慢系統, 但是它也能快速指出某些類型的memory損壞錯誤.
CONFIG_DEBUG_SPINLOCK
啟動這個選項, 內核捕捉對未初始化的spinlcok的操作, 以及各種其他的錯誤( 例如2 次解鎖同一個鎖).
CONFIG_DEBUG_SPINLOCK_SLEEP
這個選項啟動對持有spinlcok時進入sleep的檢查. 實際上, 如果你call一個可能會sleep的函數, 它就出現警示, 即便這個有疑問的call沒有sleep.
CONFIG_INIT_DEBUG
用__init (或者__initdata) 標誌的項目在系統初始化或者module加載後都被丟棄. 這個選項啟動了對代碼的檢查, 這些代碼試圖在初始化完成後存取初始化時的memory.
CONFIG_DEBUG_INFO
這個選項使得內核在建立時包含完整的debug信息. 如果你想使用gdb debug內核, 你將需要這些信息. 如果你打算使用gdb, 你還要啟動CONFIG_FRAME_POINTER.
CONFIG_MAGIC_SYSRQ
啟動"magic SysRq"鍵. 我們在本章後面的"system hangs"一節查看這個鍵.
CONFIG_DEBUG_STACKOVERFLOW
CONFIG_DEBUG_STACK_USAGE
這些選項能幫助追緃內核stack overflows. stack overflow的一個明確特徵是一個沒有任何合理的back trace的oops 輸出, 第一個選項對內核增加了明確的溢出檢查; 第2 個使得內核??監測stack使用並做一些統計, 其中有一些統計可以用magic SysRq 鍵得到.
CONFIG_KALLSYMS
這個選項(在"Generl setup/Standard features"下)使得內核符號信息建在內核中; default是啟動的. 符號選項用在debug上下文中; 沒有它, 一個oops 列表只能以16 進制格式給你一個內核trackback, 這不是很有用.
CONFIG_IKCONFIG
CONFIG_IKCONFIG_PROC
這些選項(在"Generl setup"選單)使得完整的內核配置狀態被建立到內核中, 可以通過/proc 來使其可用. 大部分內核開發者知道他們使用的哪個配置, 並不需要這些選項(會使得內核更大). 但是如果你試著debug由其他人建立的內核中的問題, 它們可能有用.
CONFIG_ACPI_DEBUG
在"Power management/ACPI"下. 這個選項打開詳細的ACPI (Advanced Configuration and Power Interface) debug信息, 如果你懷疑一個問題和ACPI 相關它可能有用.
CONFIG_DEBUG_DRIVER
在"Device drivers"下. 打開了driver core的debug信息, 可用以追緃low level支持代碼的問題. 我們在第14 章查看driver core.
CONFIG_SCSI_CONSTANTS
這個選項, 在"Device drivers/SCSI device support"下, 建立詳細的SCSI 錯誤消息的信息. 如果你在使用SCSI driver, 你可能需要這個選項.
CONFIG_INPUT_EVBUG
這個選項(在"Device drivers/Input device support"下)打開輸入事件的詳細日誌. 如果你使用一個輸入設備的driver, 這個選項可能會有用. 然而要小心這個選項的安全性的隱含意義: 它記錄了你鍵入的任何東西, 包括你的密碼.
CONFIG_PROFILING
這個選項位於"Profiling support"之下. profiling通常用在系統性能調整, 但是在追緃一些內核hangs和相關問題上也有用.
當我們查看各種方法來追緃內核問題時.我們會再次遇到一些上面的選項, 但是首先, 我們要看一下經典的debug技術: print 語句.

LDD3 潤飾

最近開始在看Linux Device Driver 3(LDD3)這份文件, 網路上可以看到中文版跟英文版, 雖然英文單字還算是淺顯易懂,但中文還是比較親切,中文的優勢是一眼可以看整篇文章,不用像英文要一個字一個字讀.
網路上的中文很像是用google translate過來,不是很通順, 一些用語也不是台灣慣用,所以在閱讀英文版時順便潤飾一下中文版, 將來再回頭來看會比較快.

2015年3月31日 星期二

LDD3 潤飾 3.9. quick reference

3.9. 快速參考

本章介紹了下面symbols和header files. struct file_operations 和struct file 中的成員的列表這裡不重複了.
#include <linux/types.h>
dev_t
dev_t 是用來在內核裡代表device number的類型.
int MAJOR(dev_t dev);
int MINOR(dev_t dev);
從device number中抽取major number,minor number的巨集.
dev_t MKDEV(unsigned int major, unsigned int minor);
從major & minor number來建立dev_t 數據項的巨集定義.
#include <linux/fs.h>
"filesystem"header是編寫device driver需要的header. 許多重要的函數和數據結構在此定義.
int register_chrdev_region(dev_t first, unsigned int count, char *name)
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
void unregister_chrdev_region(dev_t first, unsigned int count);
允許driver分配和釋放device number的範圍的函數. register_chrdev_region 應當用在事先知道需要的major number時; 對於動態分配, 使用alloc_chrdev_region 代替.
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
老的( 2.6 之前) 字符設備註冊函數. 它在2.6 內核中被模擬, 但是不應當給新代碼使用. 如果主編號不是0, 可以不變地用它; 否則一個動態編號被分配給這個設備.
int unregister_chrdev(unsigned int major, const char *name);
恢復一個由register_chrdev 所作的註冊的函數. major 和name 字符串必須包含之前用來註冊設備時同樣的值.
struct file_operations;
struct file;
struct inode;
大部分設備驅動使用的3 個重要數據結構. file_operations 結構持有一個char driver的method; struct file 代表一個打開的文件, struct inode 代表磁盤上的一個文件.
#include <linux/cdev.h>
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *dev, struct file_operations *fops);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);
cdev 結構管理的函數, 它代表內核中的char device.
#include <linux/kernel.h>
container_of(pointer, type, field);
一個傳統巨集定義, 可用來獲取一個結構pointer, 從它裡面包含的某個其他結構的pointer.
#include <asm/uaccess.h>
這個包含文件聲明內核代碼使用的函數來移動數據到user space和從user space.
unsigned long copy_from_user (void *to, const void *from, unsigned long count);
unsigned long copy_to_user (void *to, const void *from, unsigned long count);
在user space和kernel space拷貝數據.

LDD3 潤飾 3.8 Using new device

3.8. 使用新設備

一旦你裝備好剛剛描述的4 個方法, driver可以編譯並測試了; 它保留了你寫給它的任何數據, 直到你用新數據覆蓋它. 這個設備表現如一個數據緩存器, 它的長度僅僅受限於可用的真實RAM 的數量. 你可試著使用cp, dd, 以及輸入/輸出重定向來測試這個driver.

free 命令可用來看空閒內存的數量如何縮短和擴張的, 依據有多少數據寫入scull.

為對一次讀寫一個quantum有更多信心, 你可增加一個printk 在driver的適當位置, 並且觀察當應用程序讀寫大塊數據中發生了什麼. 另一選擇是使用strace 工具來監視程序發出的system call以及它們的返回值. 跟蹤一個cp 或者一個ls -l > /dev/scull0 展示了量子化的讀和寫. 監視(以及調試)技術在第4 章詳細介紹.

LDD3 潤飾 3.7 Read and Write

3.7. 讀和寫

讀和寫方法都進行類似的任務, 就是, 從應用程序拷貝數據 或是拷貝數據到應用程序. 因此, 它們的原型相當相似, 可以同時介紹它們:

ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
對於兩個方法, filp 是file pointer, count 是請求的傳輸數據大小. buff 參數指向持有被寫入數據的緩存, 或者放入新數據的空緩存. 最後, offp 是一個pointer指向一個"long offset type"對象, 它指出用戶正在存取的文件位置. 返回值是一個"signed size type"; 它的使用在後面討論.

讓我們重複一下, read 和write 方法的buff 參數是user space pointer. 因此, 它不能被內核代碼直接 解引用. 這個限制有幾個理由:

依賴於你的driver運行的體系, 以及內核被如何配置的, user space pointer當運行於內核模式可能根本是無效的. 可能沒有那個地址的映射, 或者它可能指向一些其他的隨機數據.

就算這個pointer在內核空間是同樣的東西, user space內存是分頁的, 在做system call時這個內存可能沒有在RAM 中. 試圖直接引用user space內存可:能產生一個頁面錯, 這是內核代碼不允許做的事情. 結果可能是一個"oops", 導致進行system call的進程死亡.

置疑中的pointer由一個用戶程序提供, 它可能是錯誤的或者惡意的. 如果你的driver盲目地解引用一個用戶提供的pointer, 它提供了一個打開的門路使user space程序存取或覆蓋系統任何地方的內存. 如果你不想負責你的用戶的系統的安全危險, 你就不能直接解引用user space pointer.

顯然, 你的driver必須能夠存取user space緩存以完成它的工作. 但是, 為安全起見這個存取必須使用特殊的, 內核提供的函數. 我們介紹幾個這樣的函數(定義於<asm/uaccess .h>), 剩下的在第一章"使用ioctl 參數"一節中. 它們使用一些特殊的, 依賴體系的技巧來確保內核和user space的數據傳輸安全和正確.

scull 中的read, write需要拷貝一整段數據到或者從user address space. 這個能力由下列內核函數提供, 它們拷貝一個任意的字節數組, 並且位於大部分讀寫實現的核心中.

unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);
儘管這些函數表現象正常的memcpy 函數, 必須特別小心在從內核代碼中存取user space. 要存取的user pages可能當前不在memory裡, 在這個page被傳送到位時virtual memory子系統會使進程sleep.例如, 這發生在必須從swap space獲取page的時候. 對於driver編寫者來說, 最終結果是任何存取user space的函數必須是可重入的, 必須能夠和其他driver函數並行執行, 並且, 特別的, 必須在一個它能夠合法地sleep的位置. 我們在第5 章再回到這個主題.

這2 個函數的角色不限於拷貝數據到和從user space: 它們還檢查user space pointer是否有效. 如果pointer無效, 不進行拷貝; 如果在拷貝中遇到一個無效地址, 另一方面, 只拷貝部分數據. 在2 種情況下, 返回值是還要拷貝的數據量. scull 代碼查看這個錯誤返回, 並且如果它不是0 就返回-EFAULT 給用戶.

user space存取和無效user space pointer的主題有些進階, 在第6 章討論. 然而, 值得注意的是如果你不需要檢查user space pointer, 你可以調用__copy_to_user 和__copy_from_user 來代替. 這是有用處的, 例如, 如果你知道你已經檢查了這些參數. 但是, 要小心; 事實上, 如果你不檢查你傳遞給這些函數的user space pointer, 那麼你可能造成內核崩潰和/或安全漏洞.

至於實際的設備方法, read 方法的任務是從設備拷貝數據到user space(使用copy_to_user), 而write 方法必須從user space拷貝數據到設備(使用copy_from_user). 每個read 或write system call請求一個特定數目字節的傳送, 但是driver可自由傳送較少數據-- 對讀和寫這確切的規則稍微不同, 在本章後面描述.

不管這些方法傳送多少數據, 它們通常應當更新*offp 中的文件位置來表示在system call成功完成後當前的文件位置. 內核接著在適當時候傳播文件位置的改變到文件結構. pread 和pwrite system call有不同的語義; 它們從一個給定的文件偏移操作, 並且不改變其他的system call看到的文件位置. 這些調用傳遞一個指向用戶提供的位置的pointer, 並且放棄你的driver所做的改變.

圖給read的參數表示了一個典型讀實現是如何使用它的參數.
給read 的參數
圖 3.2. 給read 的參數

給read 的參數
read 和write 方法都在發生錯誤時返回一個負值. 相反, 大於或等於0 的返回值告知調用程序有多少字節已經成功傳送. 如果一些數據成功傳送接著發生錯誤, 返回值必須是成功傳送的字節數, error不會報告直到函數下一次調用. 當然, 為了實現這個慣例,要求你的driver記住錯誤已經發生, 以便它們可以在以後返回錯誤狀態.

儘管內核函數返回一個負數指示一個錯誤, 這個數的值指出所發生的錯誤類型( 如第2 章介紹), user space運行的程序常常看到-1 作為錯誤返回值. 它們需要存取errno 變量來找出發生了什麼. user space的行為由POSIX 標準來規定, 但是這個標準沒有規定內核內部如何操作.

3.7.1. read 方法

read 的返回值由調用的應用程序解釋:

如果這個值等於傳遞給read system call的count 參數, 代表請求的字節數已經被傳送. 這是最好的情況.

如果是正數, 但是小於count, 則只有部分數據被傳送. 這可能由於幾個原因, 與設備有關. 一般而言,應用程序重新試著讀取. 例如, 假如你使用fread 函數來讀取, library function重新發出system call直到請求的數據傳送完成.

如果值為0, 代表到達了文件末尾(沒有讀取數據).

一個負值表示有一個錯誤. , 根據<linux/errno.h>這個值指出了什麼錯誤. 出錯的典型返回值包括-EINTR( 被打斷的system call) 或者-EFAULT( 錯誤地址).

前面列表中漏掉的是這種情況"沒有數據, 但是可能之後會有". 在這種情況下, read system call應當blocking. 我們將在第6 章涉及blocking.

scull 代碼利用了這些規則. 特別是它利用了partial-read規則. 每個scull_read 調用只處理單個數據quantum, 不實現一個循環來收集所有的數據; 這使得代碼更短更易讀. 如果確實需要更多數據, 它重複重新調用. 如果標準I/O library(例如, fread)用來讀取設備, 應用程序甚至不會注意到數據傳送的量子化.

如果當前讀取位置大於設備大小, scull 的read 方法返回0 來表示沒有可用的數據(換句話說, 我們在文件尾). 這個情況發生在如果進程A 在讀取設備, 同時進程B 打開它來寫入, 這樣將設備截短為0. 進程A 突然發現自己過了文件尾, 下一個read call返回0.

這是read 的代碼( 忽略對down_interruptible 的調用並且現在為up; 我們在下一章中討論它們):

ssiz??e_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
        struct scull_dev *dev = filp->private_data;
        struct scull_qset *dptr; /* the first listitem */
        int quantum = dev->quantum, qset = dev->qset;
        int itemsize = quantum * qset; /* how many bytes in the listitem */
        int item, s_pos, q_pos, rest;
        ssiz??e_t retval = 0;

        if (down_interruptible(&dev->sem))
                return -ERESTARTSYS;
        if (*f_pos >= dev->size)
                goto out;
        if (*f_pos + count > dev->size)
                count = dev->size - *f_pos;

        /* find listitem, qset index, and offset in the quantum */
        item = (long)*f_pos / itemsize;
        rest = (long)*f_pos % itemsize;
        s_pos = rest / quantum;
        q_pos = rest % quantum;

        /* follow the list up to the right position (defined elsewhere) */
        dptr = scull_follow(dev, item);
        if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
                goto out; /* don't fill holes */

        /* read only up to the end of this quantum */
        if (count > quantum - q_pos)
                count = quantum - q_pos;

        if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count))
        {
                retval = -EFAULT;
                goto out;

        }
        *f_pos += count;
        retval = count;

out:
        up(&dev->sem);
        return retval;
}
3.7.2. write 方法

write, 像read, 可以傳送少於要求的數據, 根據返回值的下列規則:

如果值等於count, 要求的字節數已被傳送.

如果正值, 但是小於count, 只有部分數據被傳送. 程序最可能會重試寫入剩下的數據.

如果值為0, 什麼都沒有寫入. 這個結果不是一個錯誤, 因此沒有理由返回一個錯誤碼. 再一次, 標準庫重試寫入調用. 我們將在第6 章查看這種情況的確切含義, 那裡介紹了blocking.

一個負值表示發生一個錯誤; 如同對於讀, 有效的錯誤值是定義於<linux/errno.h>中.

不幸的是, 仍然可能有些不當行為程序, 它在進行了部分傳送時,發出error message 並且終止程序. 這是因為一些程序員習慣看寫調用要么完全失敗要么完全成功, 這實際上是大部分時間的情況, 應當也被設備支持. scull 實現的這個限制可以修改, 但是我們不想使代碼不必要地複雜.

write 的scull 代碼一次處理單個quantum, 如read 方法做的:

ssiz??e_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
        struct scull_dev *dev = filp->private_data;
        struct scull_qset *dptr;
        int quantum = dev->quantum, qset = dev->qset;
        int itemsize = quantum * qset;
        int item, s_pos, q_pos, rest;
        ssiz??e_t retval = -ENOMEM; /* value used in "goto out" statements */
        if (down_interruptible(&dev->sem))
                return -ERESTARTSYS;

        /* find listitem, qset index and offset in the quantum */
        item = (long)*f_pos / itemsize;
        rest = (long)*f_pos % itemsize;
        s_pos = rest / quantum;
        q_pos = rest % quantum;
        /* follow the list up to the right position */
        dptr = scull_follow(dev, item);
        if (dptr == NULL)
                goto out;
        if (!dptr->data)
        {
                dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
                if (!dptr->data)
                        goto out;
                memset(dptr->data, 0, qset * sizeof(char *));
        }
        if (!dptr->data[s_pos])
        {
                dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
                if (!dptr->data[s_pos])

                        goto out;
        }
        /* write only up to the end of this quantum */
        if (count > quantum - q_pos)

                count = quantum - q_pos;
        if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count))
        {
                retval = -EFAULT;
                goto out;

        }
        *f_pos += count;
        retval = count;

        /* update the size */
        if (dev->size < *f_pos)
                dev->size = *f_pos;

out:
        up(&dev->sem);
        return retval;

}
3.7.3. readv 和writev

Unix 系統已經長時間支持名為readv 和writev 的2 個system call. 這些read 和write 的"vector"版本使用一個array structure, 每個包含一個緩存的pointer和一個長度值. 一個readv 調用被期望來輪流讀取指示的數量到每個緩存. 相反, writev 要收集每個緩存的內容到一起並且作為單個write操作送出它們.

如果你的driver不提供方法來處理vector操作, readv 和writev 由多次調用你的read 和write 方法來實現. 但是在許多情況, 直接實現readv 和writev 能獲得更大的效率.

vector操作的原型是:

ssize_t (*readv) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
ssize_t (*writev) (struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
這裡, filp 和ppos 參數與read 和write 的相同. iovec 結構, 定義於<linux/uio.h>, 如同:

struct iovec
{
    void __user *iov_base; __kernel_size_t iov_len;
};
每個iovec 描述了一塊要傳送的數據; 它開始於iov_base (在user space)並且有iov_len 字節長. count 參數告訴有多少iovec 結構. 這些結構由應用程序創建, 但是內核在調用driver之前拷貝它們到內核空間.

矢量操作的最簡單實現是一個直接的循環, 只是傳遞出去每個iovec 的地址和長度給driver的read 和write 函數. 然而, 有效的和正確的行為常常需要driver更聰明. 例如, 一個磁帶driver上的writev 應當將全部iovec 結構中的內容作為磁帶上的單個記錄.

但是很多driver 沒有從自己實現這些方法中獲益. 因此, scull 省略它們. 內核使用read 和write 來模擬它們, 最終結果是相同的.

2015年3月30日 星期一

LDD3 潤飾 3.6 scull’s Memory Usage

3.6. scull 的memory使用

在介紹read, write operation前, 我們最好看看如何以及為什麼scull 進行memory分配. "如何"是需要全面理解代碼, "為什麼"演示了driver編寫者需要做的選擇, 儘管scull 明確地不是典型設備.

本節只處理scull 中的memory分配策略, 不展示給你編寫真正driver需要的硬件管理技能. 這些技能在第9 章和第10 章介紹. 因此, 你可跳過本章, 如果你不感興趣於理解memory-oriented的scull 驅動的內部工作.

scull 使用的memory區, 也稱為一個設備, 長度可變. 你寫的越多, 它增長越多; 通過使用一個短文件覆蓋設備來進行修整.

scull 驅動引入2 個核心函數來管理Linux 內核中的memory. 這些函數, 定義在<linux/slab.h>, 是:

void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
對kmalloc 的調用試圖分配size 字節的memory; 返回值是指向那個memory的pointer或者如果分配失敗為NULL. flags 參數用來描述memory應當如何分配; 我們在第8 章詳細查看這些flags. 對於現在,我們一直使用GFP_KERNEL. 分配的memory應當用kfree 來釋放. 你不能傳遞任何不是從kmalloc 獲得的東西給kfree. 但是, 傳遞一個NULL pointer給kfree 是合法的.

kmalloc 不是分配大memory區最有效的方法(見第8 章), 所以挑選給scull 的實現不是一個特別巧妙的. 一個巧妙的源碼實現可能更難閱讀, 而本節的目標是展示讀和寫, 不是memory管理. 這是為什麼代碼只是使用kmalloc 和kfree 而不依靠整頁的分配, 儘管這個方法會更有效.

另一方面, 我們不想限制"設備"區的大小, 由於理論上的和實踐上的理由. 理論上, 在被管理的數據項目施加武斷的限制總是個壞想法. 實踐上, scull 可用來暫時地吃光你係統中的memory, 以便運行在低memory條件下的測試. 運行這樣的測試可能會幫助你理解系統的內部. 你可以使用命令cp /dev/zero /dev/scull0 來用scull 吃掉所有的真實RAM, 並且你可以使用dd 工具來選擇拷貝多少數據給scull 設備.

在scull,每個設備是一個pointer的linked list,每個都指向一個scull_dev結構.每個這樣的結構,預設地,指向最多4兆字節,通過一個中間pointer陣列.發行代碼使用一個1000個pointer的array指向每個4000字節的區域.我們稱每個memory區域為一個quantum,陣列(或者它的長度)為一個quantum set.一個scull設備和它的memory區如圖一個scull設備的佈局所示.
一個scull 設備的佈局
圖 3.1. 一個scull 設備的佈局

一個scull 設備的佈局
選定的數字是這樣, 在scull 中寫單個一個字節消耗8000 或12,000 KB memory: 4000 是quantum, 4000 或者8000 是quantum set(根據pointer在目標平台上是用32位還是64位表示). 相反, 如果你寫入大量數據, linked list的開銷不是太壞. 每4 MB 數據只有一個list元素, 設備的最大尺寸受限於計算機的memory大小.

為quantum和quantum set選擇合適的值是一個策略問題, 而不是機制, 並且優化的值依賴於設備如何使用. 因此, scull driver不應當強制給quantum和quantum set使用任何特別的值. 在scull 中,用戶有幾個途徑可以改變這些值:編譯時通過改變scull.h 中的巨集SCULL_QUANTUM 和SCULL_QSET, 在模塊加載時設定整數值scull_quantum 和scull_qset, 或者使用ioctl 在運行時改變當前值和預設值.

使用巨集定義和一個整數值來進行編譯時和加載時配置, 是讓人聯想起major number如何選擇的. 我們在driver中任何與策略相關或專斷的值上運用這個技術.

餘下的唯一問題是如果選擇預設值. 在這個特殊情況下, 問題是在下列兩種情況中找到最好的平衡, 由填充了一半的quantum和quantum set導致memory浪費, 以及quantum和quantum set很小的情況下allocation ,deallocation,和pointer連接 的開銷. 另外, kmalloc 的內部設計應當考慮進去. (現在我們不追求這點, 不過; kmalloc 的內部在第8 章探索.)預設值的選擇來自假設測試時可能有大量數據寫進scull, 儘管設備的正常使用最可能只傳送幾KB 數據.

我們已經見過內部代表我們設備的scull_dev 結構. 結構的quantum 和qset 分別代表設備的quantum和quantum set大小. 實際數據, 但是, 是由一個不同的結構跟蹤, 我們稱為struct scull_qset:

struct scull_qset {
 void **data;
 struct scull_qset *next;
};
下一個代碼片段展示了實際中struct scull_dev 和struct scull_qset 是如何被用來持有數據的. sucll_trim 函數負責釋放整個數據區, 由scull_open 在文件為寫而打開時調用. 它簡單地遍歷列表並且釋放它發現的任何quantum和quantum set.

int scull_trim(struct scull_dev *dev)
{
        struct scull_qset *next, *dptr;
        int qset = dev->qset; /* "dev" is not-null */
        int i;
        for (dptr = dev->data; dptr; dptr = next)
        { /* all the list items */
                if (dptr->data) {
                        for (i = 0; i < qset; i++)
                                kfree(dptr->data[i]);
                        kfree(dptr->data);
                        dptr->data = NULL;
                }

                next = dptr->next;
                kfree(dptr);
        }
        dev->size = 0;
        dev->quantum = scull_quantum;
        dev->qset = scull_qset;
        dev->data = NULL;
        return 0;
}
scull_trim 也用在模塊清理函數中, 來歸還scull 使用的memory給系統.