4.3. 用查詢來調試

2018-02-24 15:49 更新

4.3.?用查詢來調試

前面一節(jié)描述了 printk 是任何工作的以及怎樣使用它. 沒有談到的是它的缺點.

大量使用 printk 能夠顯著地拖慢系統(tǒng), 即便你降低 cosole_loglevel 來避免加載控制臺設備, 因為 syslogd 會不停地同步它的輸出文件; 因此, 要打印的每一行都引起一次磁盤操作. 從 syslogd 的角度這是正確的實現(xiàn). 它試圖將所有東西寫到磁盤上, 防止系統(tǒng)剛好在打印消息后崩潰; 然而, 你不想只是為了調試信息的原因而拖慢你的系統(tǒng). 可以在出現(xiàn)于 /etc/syslogd.conf 中的你的日志文件名前加一個連字號來解決這個問題[14]. 改變配置文件帶來的問題是, 這個改變可能在你結束調試后保留在那里, 即便在正常系統(tǒng)操作中你確實想盡快刷新消息到磁盤. 這樣永久改變的另外的選擇是運行一個非 klogd 程序( 例如 cat /proc/kmsg, 如之前建議的), 但是這可能不會提供一個合適的環(huán)境給正常的系統(tǒng)操作.

經常地, 最好的獲得相關信息的方法是查詢系統(tǒng), 在你需要消息時, 不是連續(xù)地產生數(shù)據(jù). 實際上, 每個 Unix 系統(tǒng)提供許多工具來獲取系統(tǒng)消息: ps, netstat, vmstat, 等等.

有幾個技術給驅動開發(fā)者來查詢系統(tǒng): 創(chuàng)建一個文件在 /proc 文件系統(tǒng)下, 使用 ioctl 驅動方法, 借助 sysfs 輸出屬性. 使用 sysfs 需要不少關于驅動模型的背景知識. 在 14 章討論.

4.3.1.?使用 /proc 文件系統(tǒng)

/proc文件系統(tǒng)是一個特殊的軟件創(chuàng)建的文件系統(tǒng), 內核用來輸出消息到外界. /proc 下的每個文件都綁到一個內核函數(shù)上, 當文件被讀的時候即時產生文件內容. 我們已經見到一些這樣的文件起作用; 例如, /proc/modules, 常常返回當前已加載的模塊列表.

/proc 在 Linux 系統(tǒng)中非常多地應用. 很多現(xiàn)代 Linux 發(fā)布中的工具, 例如 ps, top, 以及 uptime, 從 /proc 中獲取它們的信息. 一些設備驅動也通過 /proc 輸出信息, 你的也可以這樣做. /proc 文件系統(tǒng)是動態(tài)的, 因此你的模塊可以在任何時候添加或去除條目.

完全特性的 /proc 條目可能是復雜的野獸; 另外, 它們可寫也可讀, 但是, 大部分時間, /proc 條目是只讀的文件. 本節(jié)只涉及簡單的只讀情況. 那些感興趣于實現(xiàn)更復雜的東西的人可以從這里獲取基本知識; 接下來可參考內核源碼來獲知完整的信息.

在我們繼續(xù)之前, 我們應當提及在 /proc 下添加文件是不鼓勵的. /proc 文件系統(tǒng)在內核開發(fā)者看作是有點無法控制的混亂, 它已經遠離它的本來目的了(是提供關于系統(tǒng)中運行的進程的信息). 建議新代碼中使信息可獲取的方法是利用 sysfs. 如同建議的, 使用 sysfs 需要對 Linux 設備模型的理解, 然而, 我們直到 14 章才接觸它. 同時, /proc 下的文件稍稍容易創(chuàng)建, 并且它們完全適合調試目的, 所以我們在這里包含它們.

4.3.1.1.?在 /proc 里實現(xiàn)文件

所有使用 /proc 的模塊應當包含 <linux/proc_fs.h> 來定義正確的函數(shù).

要創(chuàng)建一個只讀 /proc 文件, 你的驅動必須實現(xiàn)一個函數(shù)來在文件被讀時產生數(shù)據(jù). 當某個進程讀文件時(使用 read 系統(tǒng)調用), 這個請求通過這個函數(shù)到達你的模塊. 我們先看看這個函數(shù)并在本章后面討論注冊接口.

當一個進程讀你的 /proc 文件, 內核分配了一頁內存(就是說, PAGE_SIZE 字節(jié)), 驅動可以寫入數(shù)據(jù)來返回給用戶空間. 那個緩存區(qū)傳遞給你的函數(shù), 是一個稱為 read_proc 的方法:


                int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);

page 指針是你寫你的數(shù)據(jù)的緩存區(qū); start 是這個函數(shù)用來說有關的數(shù)據(jù)寫在頁中哪里(下面更多關于這個); offset 和 count 對于 read 方法有同樣的含義. eof 參數(shù)指向一個整數(shù), 必須由驅動設置來指示它不再有數(shù)據(jù)返回, data 是驅動特定的數(shù)據(jù)指針, 你可以用做內部用途.

這個函數(shù)應當返回實際擺放于 page 緩存區(qū)的數(shù)據(jù)的字節(jié)數(shù), 就象 read 方法對別的文件所作一樣. 別的輸出值是 eof 和 start. eof 是一個簡單的標志, 但是 start 值的使用有些復雜; 它的目的是幫助實現(xiàn)大的(超過一頁) /proc 文件.

start 參數(shù)有些非傳統(tǒng)的用法. 它的目的是指示哪里(哪一頁)找到返回給用戶的數(shù)據(jù). 當調用你的 proc_read 方法, start 將會是 NULL. 如果你保持它為 NULL, 內核假定數(shù)據(jù)已放進 page 偏移是 0; 換句話說, 它假定一個頭腦簡單的 proc_read 版本, 它安放虛擬文件的整個內容到 page, 沒有注意 offset 參數(shù). 如果, 相反, 你設置 start 為一個 非NULL 值, 內核認為由 start 指向的數(shù)據(jù)考慮了 offset, 并且準備好直接返回給用戶. 通常, 返回少量數(shù)據(jù)的簡單 proc_read 方法只是忽略 start. 更復雜的方法設置 start 為 page 并且只從請求的 offset 那里開始安放數(shù)據(jù).

還有一段距離到 /proc 文件的另一個主要問題, 它也打算解答 start. 有時內核數(shù)據(jù)結構的 ASCII 表示在連續(xù)的 read 調用中改變, 因此讀進程可能發(fā)現(xiàn)從一個調用到下一個有不一致的數(shù)據(jù). 如果 start 設成一個小的整數(shù)值, 調用者用它來遞增 filp-<f_pos 不依賴你返回的數(shù)據(jù)量, 因此使 f_pos 成為你的 read_proc 過程的一個內部記錄數(shù). 如果, 例如, 如果你的 read_proc 函數(shù)從一個大結構數(shù)組返回信息并且第一次調用返回了 5 個結構, start可設成5. 下一個調用提供同一個數(shù)作為 offset; 驅動就知道從數(shù)組中第 6 個結構返回數(shù)據(jù). 這是被它的作者承認的一個" hack ", 可以在 fs/proc/generic.c 見到.

注意, 有更好的方法實現(xiàn)大的 /proc 文件; 它稱為 seq_file, 我們很快會討論它. 首先, 然而, 是時間舉個例子了. 下面是一個簡單的(有點丑陋) read_proc 實現(xiàn), 為 scull 設備:


int scull_read_procmem(char *buf, char **start, off_t offset, int count, int *eof, void *data)
{
    int i, j, len = 0;
    int limit = count - 80; /* Don't print more than this */

    for (i = 0; i < scull_nr_devs && len <= limit; i++) {
        struct scull_dev *d = &scull_devices[i];
        struct scull_qset *qs = d->data;
        if (down_interruptible(&d->sem))
            return -ERESTARTSYS;
        len += sprintf(buf+len,"\nDevice %i: qset %i, q %i, sz %li\n", i, d->qset, d->quantum, d->size);
        for (; qs && len <= limit; qs = qs->next) { /* scan the list */
            len += sprintf(buf + len, " item at %p, qset at %p\n", qs, qs->data);
            if (qs->data && !qs->next) /* dump only the last item */
                for (j = 0; j < d->qset; j++) {
                    if (qs->data[j])
                        len += sprintf(buf + len, " % 4i: %8p\n", j, qs->data[j]);
                }
        }
        up(&scull_devices[i].sem);

    }
    *eof = 1;
    return len;

}

這是一個相當?shù)湫偷?read_proc 實現(xiàn). 它假定不會有必要產生超過一頁數(shù)據(jù)并且因此忽略了 start 和 offset 值. 它是, 但是, 小心地不覆蓋它的緩存, 只是以防萬一.

4.3.1.2.?老接口

如果你閱覽內核源碼, 你會遇到使用老接口實現(xiàn) /proc 的代碼:


int (*get_info)(char *page, char **start, off_t offset, int count); 

所有的參數(shù)的含義同 read_proc 的相同, 但是沒有 eof 和 data 參數(shù). 這個接口仍然支持, 但是將來會消失; 新代碼應當使用 read_proc 接口來代替.

4.3.1.3.?創(chuàng)建你的 /proc 文件

一旦你有一個定義好的 read_proc 函數(shù), 你應當連接它到 /proc 層次中的一個入口項. 使用一個 creat_proc_read_entry 調用:


struct proc_dir_entry *create_proc_read_entry(const char *name,mode_t mode, struct proc_dir_entry *base, read_proc_t *read_proc, void *data); 

這里, name 是要創(chuàng)建的文件名子, mod 是文件的保護掩碼(缺省系統(tǒng)范圍時可以作為 0 傳遞), base 指出要創(chuàng)建的文件的目錄( 如果 base 是 NULL, 文件在 /proc 根下創(chuàng)建 ), read_proc 是實現(xiàn)文件的 read_proc 函數(shù), data 被內核忽略( 但是傳遞給 read_proc). 這就是 scull 使用的調用, 來使它的 /proc 函數(shù)可用做 /proc/scullmem:


create_proc_read_entry("scullmem", 0 /* default mode */,
                       NULL /* parent dir */, scull_read_procmem,
                       NULL /* client data */);

這里, 我們創(chuàng)建了一個名為 scullmem 的文件, 直接在 /proc 下, 帶有缺省的, 全局可讀的保護.

目錄入口指針可用來在 /proc 下創(chuàng)建整個目錄層次. 但是, 注意, 一個入口放在 /proc 的子目錄下會更容易, 通過簡單地給出目錄名子作為這個入口名子的一部分 -- 只要這個目錄自身已經存在. 例如, 一個(常常被忽略)傳統(tǒng)的是 /proc 中與設備驅動相連的入口應當在 driver/ 子目錄下; scull 能夠安放它的入口在那里, 簡單地通過指定它為名子 driver/scullmem.

/proc 中的入口, 當然, 應當在模塊卸載后去除. remove_proc_entry 是恢復 create_proc_read_entry 所做的事情的函數(shù):


remove_proc_entry("scullmem", NULL /* parent dir */); 

去除入口失敗會導致在不希望的時間調用, 或者, 如果你的模塊已被卸載, 內核崩掉.

當如展示的使用 /proc 文件, 你必須記住幾個實現(xiàn)的麻煩事 -- 不要奇怪現(xiàn)在不鼓勵使用它.

最重要的問題是關于去除 /proc 入口. 這樣的去除很可能在文件使用時發(fā)生, 因為沒有所有者關聯(lián)到 /proc 入口, 因此使用它們不會作用到模塊的引用計數(shù). 這個問題可以簡單的觸發(fā), 例如通過運行 sleep 100 < /proc/myfile, 剛好在去除模塊之前.

另外一個問題時關于用同樣的名子注冊兩個入口. 內核信任驅動, 不會檢查名子是否已經注冊了, 因此如果你不小心, 你可能會使用同樣的名子注冊兩個或多個入口. 這是一個已知發(fā)生在教室中的問題, 這樣的入口是不能區(qū)分的, 不但在你存取它們時, 而且在你調用 remove_proc_entry 時.

4.3.1.4.?seq_file 接口

如我們上面提到的, 在 /proc 下的大文件的實現(xiàn)有點麻煩. 一直以來, /proc 方法因為當輸出數(shù)量變大時的錯誤實現(xiàn)變得聲名狼藉. 作為一種清理 /proc 代碼以及使內核開發(fā)者活得輕松些的方法, 添加了 seq_file 接口. 這個接口提供了簡單的一套函數(shù)來實現(xiàn)大內核虛擬文件.

set_file 接口假定你在創(chuàng)建一個虛擬文件, 它涉及一系列的必須返回給用戶空間的項. 為使用 seq_file, 你必須創(chuàng)建一個簡單的 "iterator" 對象, 它能在序列里建立一個位置, 向前進, 并且輸出序列里的一個項. 它可能聽起來復雜, 但是, 實際上, 過程非常簡單. 我們一步步來創(chuàng)建 /proc 文件在 scull 驅動里, 來展示它是如何做的.

第一步, 不可避免地, 是包含 <linux/seq_file.h>. 接著你必須創(chuàng)建 4 個 iterator 方法, 稱為 start, next, stop, 和 show.

start 方法一直是首先調用. 這個函數(shù)的原型是:


void *start(struct seq_file *sfile, loff_t *pos);

sfile 參數(shù)可以幾乎是一直被忽略. pos 是一個整型位置值, 指示應當從哪里讀. 位置的解釋完全取決于實現(xiàn); 在結果文件里不需要是一個字節(jié)位置. 因為 seq_file 實現(xiàn)典型地步進一系列感興趣的項, position 常常被解釋為指向序列中下一個項的指針. scull 驅動解釋每個設備作為系列中的一項, 因此進入的 pos 簡單地是一個 scull_device 數(shù)組的索引. 因此, scull 使用的 start 方法是:


static void *scull_seq_start(struct seq_file *s, loff_t *pos)
{
    if (*pos >= scull_nr_devs)
        return NULL;  /* No more to read */
    return scull_devices + *pos;
}

返回值, 如果非NULL, 是一個可以被 iterator 實現(xiàn)使用的私有值.

next 函數(shù)應當移動 iterator 到下一個位置, 如果序列里什么都沒有剩下就返回 NULL. 這個方法的原型是:


void *next(struct seq_file *sfile, void *v, loff_t *pos); 

這里, v 是從前一個對 start 或者 next 的調用返回的 iterator, pos 是文件的當前位置. next 應當遞增有 pos 指向的值; 根據(jù)你的 iterator 是如何工作的, 你可能(盡管可能不會)需要遞增 pos 不止是 1. 這是 scull 所做的:


static void *scull_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
    (*pos)++;
    if (*pos >= scull_nr_devs)
        return NULL;
    return scull_devices + *pos;
}

當內核處理完 iterator, 它調用 stop 來清理:


void stop(struct seq_file *sfile, void *v); 

scull 實現(xiàn)沒有清理工作要做, 所以它的 stop 方法是空的.

設計上, 值得注意 seq_file 代碼在調用 start 和 stop 之間不睡眠或者進行其他非原子性任務. 你也肯定會看到在調用 start 后馬上有一個 stop 調用. 因此, 對你的 start 方法來說請求信號量或自旋鎖是安全的. 只要你的其他 seq_file 方法是原子的, 調用的整個序列是原子的. (如果這一段對你沒有意義, 在你讀了下一章后再回到這.)

在這些調用中, 內核調用 show 方法來真正輸出有用的東西給用戶空間. 這個方法的原型是:


int show(struct seq_file *sfile, void *v); 

這個方法應當創(chuàng)建序列中由 iterator v 指示的項的輸出. 不應當使用 printk, 但是; 有一套特殊的用作 seq_file 輸出的函數(shù):

int seq_printf(struct seq_file sfile, const char fmt, ...);
這是給 seq_file 實現(xiàn)的 printf 對等體; 它采用常用的格式串和附加值參數(shù). 你必須也將給 show 函數(shù)的 set_file 結構傳遞給它, 然而. 如果seq_printf 返回非零值, 意思是緩存區(qū)已填充, 輸出被丟棄. 大部分實現(xiàn)忽略了返回值, 但是.

int seq_putc(struct seq_file sfile, char c);int seq_puts(struct seq_file sfile, const char *s);
它們是用戶空間 putc 和 puts 函數(shù)的對等體.

int seq_escape(struct seq_file m, const char s, const char *esc);
這個函數(shù)是 seq_puts 的對等體, 除了 s 中的任何也在 esc 中出現(xiàn)的字符以八進制格式打印. esc 的一個通用值是"\t\n\", 它使內嵌的空格不會搞亂輸出和可能搞亂 shell 腳本.

int seq_path(struct seq_file sfile, struct vfsmount m, struct dentry dentry, char esc);
這個函數(shù)能夠用來輸出和給定命令項關聯(lián)的文件名子. 它在設備驅動中不可能有用; 我們是為了完整在此包含它.

回到我們的例子; 在 scull 使用的 show 方法是:


static int scull_seq_show(struct seq_file *s, void *v)
{
    struct scull_dev *dev = (struct scull_dev *) v;
    struct scull_qset *d;
    int i;

    if (down_interruptible (&dev->sem))
        return -ERESTARTSYS;

    seq_printf(s, "\nDevice %i: qset %i, q %i, sz %li\n",
               (int) (dev - scull_devices), dev->qset,
               dev->quantum, dev->size);

    for (d = dev->data; d; d = d->next) { /* scan the list */
        seq_printf(s, " item at %p, qset at %p\n", d, d->data);
        if (d->data && !d->next) /* dump only the last item */

            for (i = 0; i < dev->qset; i++) {
                if (d->data[i])
                    seq_printf(s, " % 4i: %8p\n",
                               i, d->data[i]);
            }
    }
    up(&dev->sem);
    return 0;
}

這里, 我們最終解釋我們的" iterator" 值, 簡單地是一個 scull_dev 結構指針.

現(xiàn)在已有了一個完整的 iterator 操作的集合, scull 必須包裝起它們, 并且連接它們到 /proc 中的一個文件. 第一步是填充一個 seq_operations 結構:


static struct seq_operations scull_seq_ops = {
 .start = scull_seq_start,
 .next = scull_seq_next,
 .stop = scull_seq_stop,
 .show = scull_seq_show
}; 

有那個結構在, 我們必須創(chuàng)建一個內核理解的文件實現(xiàn). 我們不使用前面描述過的 read_proc 方法; 在使用 seq_file 時, 最好在一個稍低的級別上連接到 /proc. 那意味著創(chuàng)建一個 file_operations 結構(是的, 和字符驅動使用的同樣結構) 來實現(xiàn)所有內核需要的操作, 來處理文件上的讀和移動. 幸運的是, 這個任務是簡單的. 第一步是創(chuàng)建一個 open 方法連接文件到 seq_file 操作:


static int scull_proc_open(struct inode *inode, struct file *file)
{
    return seq_open(file, &scull_seq_ops);
}

調用 seq_open 連接文件結構和我們上面定義的序列操作. 事實證明, open 是我們必須自己實現(xiàn)的唯一文件操作, 因此我們現(xiàn)在可以建立我們的 file_operations 結構:


static struct file_operations scull_proc_ops = {
 .owner = THIS_MODULE,
 .open = scull_proc_open,
 .read = seq_read,
 .llseek = seq_lseek,
 .release = seq_release 
}; 

這里我們指定我們自己的 open 方法, 但是使用預裝好的方法 seq_read, seq_lseek, 和 seq_release 給其他.

最后的步驟是創(chuàng)建 /proc 中的實際文件:


entry = create_proc_entry("scullseq", 0, NULL);
if (entry)
    entry->proc_fops = &scull_proc_ops;

不是使用 create_proc_read_entry, 我們調用低層的 create_proc_entry, 我們有這個原型:


struct proc_dir_entry *create_proc_entry(const char *name,mode_t mode,struct proc_dir_entry *parent); 

參數(shù)和它們的在 create_proc_read_entry 中的對等體相同: 文件名子, 它的位置, 以及父目錄.

有了上面代碼, scull 有一個新的 /proc 入口, 看來很象前面的一個. 但是, 它是高級的, 因為它不管它的輸出有多么大, 它正確處理移動, 并且通常它是易讀和易維護的. 我們建議使用 seq_file , 來實現(xiàn)包含多個非常小數(shù)目的輸出行數(shù)的文件.

4.3.2.?ioctl 方法

ioctl, 我們在第 1 章展示給你如何使用, 是一個系統(tǒng)調用, 作用于一個文件描述符; 它接收一個確定要進行的命令的數(shù)字和(可選地)另一個參數(shù), 常常是一個指針. 作為一個使用 /proc 文件系統(tǒng)的替代, 你可以實現(xiàn)幾個用來調試用的 ioctl 命令. 這些命令可以從驅動拷貝相關的數(shù)據(jù)結構到用戶空間, 這里你可以檢查它們.

這種方式使用 ioctl 來獲取信息有些比使用 /proc 困難, 因為你需要另一個程序來發(fā)出 ioctl 并且顯示結果. 必須編寫這個程序, 編譯, 并且與你在測試的模塊保持同步. 另一方面, 驅動側代碼可能容易過需要實現(xiàn)一個 /proc 文件的代碼.

有時候 ioctl 是獲取信息最好的方法, 因為它運行比讀取 /proc 快. 如果在數(shù)據(jù)寫到屏幕之前必須做一些事情, 獲取二進制形式的數(shù)據(jù)比讀取一個文本文件要更有效. 另外, ioctl 不要求劃分數(shù)據(jù)為小于一頁的片段.

ioctl 方法的另一個有趣的優(yōu)點是信息獲取命令可留在驅動中, 當調試被禁止時. 不象對任何查看目錄的人(并且太多人可能奇怪"這個怪文件是什么")都可見的 /proc 文件, 不記入文檔的 ioctl 命令可能保持不為人知. 另外, 如果驅動發(fā)生了怪異的事情, 它們仍將在那里. 唯一的缺點是模塊可能會稍微大些.

[14] 連字號, 或者減號, 是一個"魔術"標識以阻止 syslogd 刷新文件到磁盤在每個新消息, 有關文檔在 syslog.conf(5), 一個值得一讀的 manpage.

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號