## 4.3.?用查詢來調試
前面一節描述了 printk 是任何工作的以及怎樣使用它. 沒有談到的是它的缺點.
大量使用 printk 能夠顯著地拖慢系統, 即便你降低 cosole_loglevel 來避免加載控制臺設備, 因為 syslogd 會不停地同步它的輸出文件; 因此, 要打印的每一行都引起一次磁盤操作. 從 syslogd 的角度這是正確的實現. 它試圖將所有東西寫到磁盤上, 防止系統剛好在打印消息后崩潰; 然而, 你不想只是為了調試信息的原因而拖慢你的系統. 可以在出現于 /etc/syslogd.conf 中的你的日志文件名前加一個連字號來解決這個問題[[14](#)]. 改變配置文件帶來的問題是, 這個改變可能在你結束調試后保留在那里, 即便在正常系統操作中你確實想盡快刷新消息到磁盤. 這樣永久改變的另外的選擇是運行一個非 klogd 程序( 例如 cat /proc/kmsg, 如之前建議的), 但是這可能不會提供一個合適的環境給正常的系統操作.
經常地, 最好的獲得相關信息的方法是查詢系統, 在你需要消息時, 不是連續地產生數據. 實際上, 每個 Unix 系統提供許多工具來獲取系統消息: ps, netstat, vmstat, 等等.
有幾個技術給驅動開發者來查詢系統: 創建一個文件在 /proc 文件系統下, 使用 ioctl 驅動方法, 借助 sysfs 輸出屬性. 使用 sysfs 需要不少關于驅動模型的背景知識. 在 14 章討論.
### 4.3.1.?使用 /proc 文件系統
/proc文件系統是一個特殊的軟件創建的文件系統, 內核用來輸出消息到外界. /proc 下的每個文件都綁到一個內核函數上, 當文件被讀的時候即時產生文件內容. 我們已經見到一些這樣的文件起作用; 例如, /proc/modules, 常常返回當前已加載的模塊列表.
/proc 在 Linux 系統中非常多地應用. 很多現代 Linux 發布中的工具, 例如 ps, top, 以及 uptime, 從 /proc 中獲取它們的信息. 一些設備驅動也通過 /proc 輸出信息, 你的也可以這樣做. /proc 文件系統是動態的, 因此你的模塊可以在任何時候添加或去除條目.
完全特性的 /proc 條目可能是復雜的野獸; 另外, 它們可寫也可讀, 但是, 大部分時間, /proc 條目是只讀的文件. 本節只涉及簡單的只讀情況. 那些感興趣于實現更復雜的東西的人可以從這里獲取基本知識; 接下來可參考內核源碼來獲知完整的信息.
在我們繼續之前, 我們應當提及在 /proc 下添加文件是不鼓勵的. /proc 文件系統在內核開發者看作是有點無法控制的混亂, 它已經遠離它的本來目的了(是提供關于系統中運行的進程的信息). 建議新代碼中使信息可獲取的方法是利用 sysfs. 如同建議的, 使用 sysfs 需要對 Linux 設備模型的理解, 然而, 我們直到 14 章才接觸它. 同時, /proc 下的文件稍稍容易創建, 并且它們完全適合調試目的, 所以我們在這里包含它們.
#### 4.3.1.1.?在 /proc 里實現文件
所有使用 /proc 的模塊應當包含 <linux/proc_fs.h> 來定義正確的函數.
要創建一個只讀 /proc 文件, 你的驅動必須實現一個函數來在文件被讀時產生數據. 當某個進程讀文件時(使用 read 系統調用), 這個請求通過這個函數到達你的模塊. 我們先看看這個函數并在本章后面討論注冊接口.
當一個進程讀你的 /proc 文件, 內核分配了一頁內存(就是說, PAGE_SIZE 字節), 驅動可以寫入數據來返回給用戶空間. 那個緩存區傳遞給你的函數, 是一個稱為 read_proc 的方法:
~~~
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
~~~
page 指針是你寫你的數據的緩存區; start 是這個函數用來說有關的數據寫在頁中哪里(下面更多關于這個); offset 和 count 對于 read 方法有同樣的含義. eof 參數指向一個整數, 必須由驅動設置來指示它不再有數據返回, data 是驅動特定的數據指針, 你可以用做內部用途.
這個函數應當返回實際擺放于 page 緩存區的數據的字節數, 就象 read 方法對別的文件所作一樣. 別的輸出值是 *eof 和 *start. eof 是一個簡單的標志, 但是 start 值的使用有些復雜; 它的目的是幫助實現大的(超過一頁) /proc 文件.
start 參數有些非傳統的用法. 它的目的是指示哪里(哪一頁)找到返回給用戶的數據. 當調用你的 proc_read 方法, *start 將會是 NULL. 如果你保持它為 NULL, 內核假定數據已放進 page 偏移是 0; 換句話說, 它假定一個頭腦簡單的 proc_read 版本, 它安放虛擬文件的整個內容到 page, 沒有注意 offset 參數. 如果, 相反, 你設置 *start 為一個 非NULL 值, 內核認為由 *start 指向的數據考慮了 offset, 并且準備好直接返回給用戶. 通常, 返回少量數據的簡單 proc_read 方法只是忽略 start. 更復雜的方法設置 *start 為 page 并且只從請求的 offset 那里開始安放數據.
還有一段距離到 /proc 文件的另一個主要問題, 它也打算解答 start. 有時內核數據結構的 ASCII 表示在連續的 read 調用中改變, 因此讀進程可能發現從一個調用到下一個有不一致的數據. 如果 *start 設成一個小的整數值, 調用者用它來遞增 filp-<f_pos 不依賴你返回的數據量, 因此使 f_pos 成為你的 read_proc 過程的一個內部記錄數. 如果, 例如, 如果你的 read_proc 函數從一個大結構數組返回信息并且第一次調用返回了 5 個結構, *start可設成5. 下一個調用提供同一個數作為 offset; 驅動就知道從數組中第 6 個結構返回數據. 這是被它的作者承認的一個" hack ", 可以在 fs/proc/generic.c 見到.
注意, 有更好的方法實現大的 /proc 文件; 它稱為 seq_file, 我們很快會討論它. 首先, 然而, 是時間舉個例子了. 下面是一個簡單的(有點丑陋) read_proc 實現, 為 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;
}
~~~
這是一個相當典型的 read_proc 實現. 它假定不會有必要產生超過一頁數據并且因此忽略了 start 和 offset 值. 它是, 但是, 小心地不覆蓋它的緩存, 只是以防萬一.
#### 4.3.1.2.?老接口
如果你閱覽內核源碼, 你會遇到使用老接口實現 /proc 的代碼:
~~~
int (*get_info)(char *page, char **start, off_t offset, int count);
~~~
所有的參數的含義同 read_proc 的相同, 但是沒有 eof 和 data 參數. 這個接口仍然支持, 但是將來會消失; 新代碼應當使用 read_proc 接口來代替.
#### 4.3.1.3.?創建你的 /proc 文件
一旦你有一個定義好的 read_proc 函數, 你應當連接它到 /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 是要創建的文件名子, mod 是文件的保護掩碼(缺省系統范圍時可以作為 0 傳遞), base 指出要創建的文件的目錄( 如果 base 是 NULL, 文件在 /proc 根下創建 ), read_proc 是實現文件的 read_proc 函數, data 被內核忽略( 但是傳遞給 read_proc). 這就是 scull 使用的調用, 來使它的 /proc 函數可用做 /proc/scullmem:
~~~
create_proc_read_entry("scullmem", 0 /* default mode */,
NULL /* parent dir */, scull_read_procmem,
NULL /* client data */);
~~~
這里, 我們創建了一個名為 scullmem 的文件, 直接在 /proc 下, 帶有缺省的, 全局可讀的保護.
目錄入口指針可用來在 /proc 下創建整個目錄層次. 但是, 注意, 一個入口放在 /proc 的子目錄下會更容易, 通過簡單地給出目錄名子作為這個入口名子的一部分 -- 只要這個目錄自身已經存在. 例如, 一個(常常被忽略)傳統的是 /proc 中與設備驅動相連的入口應當在 driver/ 子目錄下; scull 能夠安放它的入口在那里, 簡單地通過指定它為名子 driver/scullmem.
/proc 中的入口, 當然, 應當在模塊卸載后去除. remove_proc_entry 是恢復 create_proc_read_entry 所做的事情的函數:
~~~
remove_proc_entry("scullmem", NULL /* parent dir */);
~~~
去除入口失敗會導致在不希望的時間調用, 或者, 如果你的模塊已被卸載, 內核崩掉.
當如展示的使用 /proc 文件, 你必須記住幾個實現的麻煩事 -- 不要奇怪現在不鼓勵使用它.
最重要的問題是關于去除 /proc 入口. 這樣的去除很可能在文件使用時發生, 因為沒有所有者關聯到 /proc 入口, 因此使用它們不會作用到模塊的引用計數. 這個問題可以簡單的觸發, 例如通過運行 sleep 100 < /proc/myfile, 剛好在去除模塊之前.
另外一個問題時關于用同樣的名子注冊兩個入口. 內核信任驅動, 不會檢查名子是否已經注冊了, 因此如果你不小心, 你可能會使用同樣的名子注冊兩個或多個入口. 這是一個已知發生在教室中的問題, 這樣的入口是不能區分的, 不但在你存取它們時, 而且在你調用 remove_proc_entry 時.
#### 4.3.1.4.?seq_file 接口
如我們上面提到的, 在 /proc 下的大文件的實現有點麻煩. 一直以來, /proc 方法因為當輸出數量變大時的錯誤實現變得聲名狼藉. 作為一種清理 /proc 代碼以及使內核開發者活得輕松些的方法, 添加了 seq_file 接口. 這個接口提供了簡單的一套函數來實現大內核虛擬文件.
set_file 接口假定你在創建一個虛擬文件, 它涉及一系列的必須返回給用戶空間的項. 為使用 seq_file, 你必須創建一個簡單的 "iterator" 對象, 它能在序列里建立一個位置, 向前進, 并且輸出序列里的一個項. 它可能聽起來復雜, 但是, 實際上, 過程非常簡單. 我們一步步來創建 /proc 文件在 scull 驅動里, 來展示它是如何做的.
第一步, 不可避免地, 是包含 <linux/seq_file.h>. 接著你必須創建 4 個 iterator 方法, 稱為 start, next, stop, 和 show.
start 方法一直是首先調用. 這個函數的原型是:
~~~
void *start(struct seq_file *sfile, loff_t *pos);
~~~
sfile 參數可以幾乎是一直被忽略. pos 是一個整型位置值, 指示應當從哪里讀. 位置的解釋完全取決于實現; 在結果文件里不需要是一個字節位置. 因為 seq_file 實現典型地步進一系列感興趣的項, position 常常被解釋為指向序列中下一個項的指針. scull 驅動解釋每個設備作為系列中的一項, 因此進入的 pos 簡單地是一個 scull_device 數組的索引. 因此, 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 實現使用的私有值.
next 函數應當移動 iterator 到下一個位置, 如果序列里什么都沒有剩下就返回 NULL. 這個方法的原型是:
~~~
void *next(struct seq_file *sfile, void *v, loff_t *pos);
~~~
這里, v 是從前一個對 start 或者 next 的調用返回的 iterator, pos 是文件的當前位置. next 應當遞增有 pos 指向的值; 根據你的 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 實現沒有清理工作要做, 所以它的 stop 方法是空的.
設計上, 值得注意 seq_file 代碼在調用 start 和 stop 之間不睡眠或者進行其他非原子性任務. 你也肯定會看到在調用 start 后馬上有一個 stop 調用. 因此, 對你的 start 方法來說請求信號量或自旋鎖是安全的. 只要你的其他 seq_file 方法是原子的, 調用的整個序列是原子的. (如果這一段對你沒有意義, 在你讀了下一章后再回到這.)
在這些調用中, 內核調用 show 方法來真正輸出有用的東西給用戶空間. 這個方法的原型是:
~~~
int show(struct seq_file *sfile, void *v);
~~~
這個方法應當創建序列中由 iterator v 指示的項的輸出. 不應當使用 printk, 但是; 有一套特殊的用作 seq_file 輸出的函數:
int seq_printf(struct seq_file *sfile, const char *fmt, ...);
這是給 seq_file 實現的 printf 對等體; 它采用常用的格式串和附加值參數. 你必須也將給 show 函數的 set_file 結構傳遞給它, 然而. 如果seq_printf 返回非零值, 意思是緩存區已填充, 輸出被丟棄. 大部分實現忽略了返回值, 但是.
int seq_putc(struct seq_file *sfile, char c);int seq_puts(struct seq_file *sfile, const char *s);
它們是用戶空間 putc 和 puts 函數的對等體.
int seq_escape(struct seq_file *m, const char *s, const char *esc);
這個函數是 seq_puts 的對等體, 除了 s 中的任何也在 esc 中出現的字符以八進制格式打印. esc 的一個通用值是"\t\n\\", 它使內嵌的空格不會搞亂輸出和可能搞亂 shell 腳本.
int seq_path(struct seq_file *sfile, struct vfsmount *m, struct dentry *dentry, char *esc);
這個函數能夠用來輸出和給定命令項關聯的文件名子. 它在設備驅動中不可能有用; 我們是為了完整在此包含它.
回到我們的例子; 在 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 結構指針.
現在已有了一個完整的 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
};
~~~
有那個結構在, 我們必須創建一個內核理解的文件實現. 我們不使用前面描述過的 read_proc 方法; 在使用 seq_file 時, 最好在一個稍低的級別上連接到 /proc. 那意味著創建一個 file_operations 結構(是的, 和字符驅動使用的同樣結構) 來實現所有內核需要的操作, 來處理文件上的讀和移動. 幸運的是, 這個任務是簡單的. 第一步是創建一個 open 方法連接文件到 seq_file 操作:
~~~
static int scull_proc_open(struct inode *inode, struct file *file)
{
return seq_open(file, &scull_seq_ops);
}
~~~
調用 seq_open 連接文件結構和我們上面定義的序列操作. 事實證明, open 是我們必須自己實現的唯一文件操作, 因此我們現在可以建立我們的 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 給其他.
最后的步驟是創建 /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);
~~~
參數和它們的在 create_proc_read_entry 中的對等體相同: 文件名子, 它的位置, 以及父目錄.
有了上面代碼, scull 有一個新的 /proc 入口, 看來很象前面的一個. 但是, 它是高級的, 因為它不管它的輸出有多么大, 它正確處理移動, 并且通常它是易讀和易維護的. 我們建議使用 seq_file , 來實現包含多個非常小數目的輸出行數的文件.
### 4.3.2.?ioctl 方法
ioctl, 我們在第 1 章展示給你如何使用, 是一個系統調用, 作用于一個文件描述符; 它接收一個確定要進行的命令的數字和(可選地)另一個參數, 常常是一個指針. 作為一個使用 /proc 文件系統的替代, 你可以實現幾個用來調試用的 ioctl 命令. 這些命令可以從驅動拷貝相關的數據結構到用戶空間, 這里你可以檢查它們.
這種方式使用 ioctl 來獲取信息有些比使用 /proc 困難, 因為你需要另一個程序來發出 ioctl 并且顯示結果. 必須編寫這個程序, 編譯, 并且與你在測試的模塊保持同步. 另一方面, 驅動側代碼可能容易過需要實現一個 /proc 文件的代碼.
有時候 ioctl 是獲取信息最好的方法, 因為它運行比讀取 /proc 快. 如果在數據寫到屏幕之前必須做一些事情, 獲取二進制形式的數據比讀取一個文本文件要更有效. 另外, ioctl 不要求劃分數據為小于一頁的片段.
ioctl 方法的另一個有趣的優點是信息獲取命令可留在驅動中, 當調試被禁止時. 不象對任何查看目錄的人(并且太多人可能奇怪"這個怪文件是什么")都可見的 /proc 文件, 不記入文檔的 ioctl 命令可能保持不為人知. 另外, 如果驅動發生了怪異的事情, 它們仍將在那里. 唯一的缺點是模塊可能會稍微大些.
[[14](#)] 連字號, 或者減號, 是一個"魔術"標識以阻止 syslogd 刷新文件到磁盤在每個新消息, 有關文檔在 syslog.conf(5), 一個值得一讀的 manpage.
- 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. 快速參考