TTY設備驅動結構


Tags: Linux驅動程序

本文爲你展示基於“串口通信設備”的TTY設備驅動程序內部結構,並講述這些驅動是如何實現鏈路層通信協議(包括ppp和slip)的應用。本文的代碼基於2.4內核,大部分適用於2.2和2.0[注]。

注:本文的代碼雖然基於2.4內核,但是結構原理在現在內核版本仍然存在。

串口驅動誤解

當我們說【串口驅動】的時候,我們第一個想到的東西是/dev/ttyS0,因爲它是大家所熟知的串口通信的設備文件(至少在PC系統上如此)。由於/dev/ttyS0是一個字符型的設備文件,大家都以爲串口驅動是字符設備驅動,這種推斷不能叫錯,只是不夠準確。【串口驅動】不是一般的字符驅動,有兩點可以證明:

  • 第一,串行驅動不被拿來作字符驅動範例;
  • 第二,不同的串行設備驅動使用相同的【主設備號】。當你爲你的系統擴展一個新的串口,那這個串口的驅動程序不會被分配一個新的主設備號。

串口驅動的“串口”從分類就看出來它是一種特殊的驅動類型,歸於字符驅動大類下。

查看 /proc/devices ,你會發現ttyS驅動的主設備號是4,這是一個善意的謊言:文本模式控制檯設備的主設備號也是4。事實上,2.0版內核用了一個通用的名字“ttyp”代表主設備號4。

串口設備之所以與一般的字符設備(比如並口打印機或磁帶機)存在不同是因爲串口被用來實現高一級的抽象——tty,串口設備爲tty設備提供了串行通信信道。tty的名字來源早期的一種串口應用設備——電傳打字機(tele-type)。

tty設備驅動子系統

tty設備是什麼樣的設備呢?tty的概念其實已經被泛化,不只是指處理數字文字(alphanumeric)字符的終端設備,例如基於VGA和幀緩衝式的文本控制檯、xterm虛擬終端等。[tty設備驅動子系統]以下圖那樣的一種結構實現了對tty設備的泛化。某特定tty設備驅動的特殊部分以註冊的方式註冊入通用的子系統部分,從而實現一支特定tty設備驅動。註冊方式的好處是設備驅動的特殊部分可以以內核模塊形式(kernel module)實現。

Figure 1

圖中展示出有三部分是可註冊的,串行通信設備驅動(serial.c)、線路規則(n_tty.c)和[tty設備驅動子系統]本身(tty_io.c)。可以這樣理解,tty設備是一種特殊的字符設備,所以它需通過[字符設備驅動子系統](fs/devices.c)註冊自身。[串行通信設備驅動]是不能被用戶直接使用的,它必須抽象爲一個tty設備,然後配置使用默認的線路規則——n_tty.c,經[tty設備驅動子系統]在系統中註冊爲字符設備。

由此可見,[tty設備驅動子系統]被劃分爲三塊,數據流從用戶空間到串行設備間夾着一層tty。雖然如此,tty_io.c本身只是橋樑,實際的tty操作是線路規則完成,線路規則(line discipline)是一支定義串口線如何使用的軟件模塊。Linux默認是線路規則是N_TTY,N_TTY是標準字符終端IO處理規則。

爲何如此複雜?

這種分層設計看上去就很複雜,有必要嗎?複雜的回報是靈活性。當tty設備應用種類紛繁時,很有必要。爲新串行設備編寫驅動比一般字符設備要複雜,有了這種通用抽象模型,爲編寫串口設備驅動省下很多功夫。

更重一點是,[tty設備驅動子系統]把線路規則也卸下,進一步的抽象後,線路規則也可替換。這樣,串行設備驅動根本不知道數據是從哪來的,它只管收發就行了。

PPP和slip

如果你使用modem(使用PPP鏈路協議)撥號上網,或者用SLIP互聯PC和你的掌上PDA,你就體會到上面提到的複雜性。ppp和SLIP實現了各自的線路規則,當它們中任何一個應用要運行,tty設備必須在它們的線路規則模塊間切換,來構造不同的tty設備。

下圖展示了SLIP應用的一個概念圖。

Figure 2

有趣的是,SLIP驅動向[tty設備驅動子系統]和[網絡子系統]同時註冊,前者是線路規則N_SLIP,後者網絡設備slip0。當tty設備切換爲N_SLIP後,串行通信數據經TCP/IP協議棧與用戶空間通信。當tty設備被切爲TCP/IP網絡設備後,其它用戶進程沒法讀寫 基於/dev/ttyS的tty設備。這也說明了爲什麼網絡通道建立後,slattach 和 pppd 都不能退出的原因。

關鍵數據結構

[tty設備驅動子系統]由三個主要的數據結構構建:

struct tty_struct:這是[tty設備]的核心表徵數據結構,結構體內內嵌(通過指針)餘下的兩個關鍵數據結構。每當有新的tty設備被打開使用時,tty_struct都被創建一個新實例,直到設備被關閉。注意實際代碼裏(tty_io.c)有很多複雜的操作,例如在打開和關閉tty設備的過程中對的termios設定的保存與恢復(至少基於串口的tty設備如此)。

struct tty_driver:這是驅動本身的表徵,定義設備號,定義並實現設備的使用接口等,在設備打開的時,get_tty_driver函數負責關聯設備與設備驅動。tty_driver結構成員大略如下:

struct tty_driver {

/* the driver states which range of devices it supports */
short major;         /* major device number */
short minor_start;   /* start of minor device number*/
short   num;         /* number of devices */

/* and has its own operations */
int  (*open)();
void (*close)();
int  (*write)();
int  (*ioctl)();  /* device-specific control */

/* return information on buffer state */
int  (*write_room)();      /* how much can be written */
int  (*chars_in_buffer)(); /* how much is there to read */

/* flow control, input and output */
void (*throttle)();
void (*unthrottle)();
void (*stop)();
void (*start)();

/* and callbacks for /proc/tty/driver/ */
int (*read_proc)();
int (*write_proc)();
};

struct tty_ldisc:線路規則模塊由tty_struct的ldisc域指定。在設備打開的時候,ldisc默認指向n_tty,用戶空間程序可以通過ioctl切換tty設備的線路規則。tty_ldisc結構成員大略如下:

struct tty_ldisc {

/* routines called from above */
int     (*open)();
void    (*close)();
ssize_t (*read)();
ssize_t (*write)();
int     (*ioctl)();

/* routines called from below */
void    (*receive_buf)();
int     (*receive_room)();
void    (*write_wakeup)();
};

作爲一般的tty設備驅動程序開發者,可以不必關心tty_struct的實現,只管tty_driver 和 tty_ldisc就行了。因爲當應用系統使用新串行通信設備,我們一般實現tty_driver就行了;如果應用系統有傳統的串口,並且想開發新上層應用,那實現一支新線路規則模塊就行了。例如,假設你手頭有一塊特殊的鍵盤,使用標準RS-232串口,那麼需要實現一支新線路規則模塊就可以了,因爲整套的串口tty設備驅動有了,鍵盤驅動已經有了。新線路規則模塊完成與input子系統和通用鍵盤驅動的通信。

設備的讀寫數據流


Figure 3

圖三展示了數據是如何從用戶空間流向硬件接口並返回的。從圖可知,“寫入數據”是比較直觀,“讀出數據”需要解釋一下。“讀數據”稍微複雜,因爲“讀數據”時硬件和用戶空間沒有直接的調用關係。因爲只有用戶空間調用硬件,硬件不能調用用戶空間代碼,所以只能把收到的數據存放起來,等用戶空間程序來讀。你可能已經猜到解決辦法——使用緩衝區:硬件收到的數據會存放到內核的緩衝區並保持着,直到有用戶空間程序把它們讀走。當某用戶程序讀取緩衝區,而緩衝區爲空的時候,用戶程序被置入睡眠狀態,只有當緩衝區收到數據後才被喚醒。

注意,其實“寫數據”的操作一樣有緩衝區。只是“寫操作”是一步接一步由上而下,控制流比較簡單直接,不像讀操作有數據傳輸的延遲,寫操作的緩衝區只是爲了“軟化”硬件傳輸操作和軟件控制操作的邊界。爲了精簡圖示,圖中沒有畫出寫操作緩衝區。

對tty設備而言,[讀緩衝區]應該集成在tty的數據結構內,雖然這樣設計會讓tty_struct變得相當肥大,但是沒有理由表明緩衝區可以放在其它地方。tty設備傳輸不能沒有緩衝,而tty設備是動態創建的,所以即使緩衝佔有空間,至少不會浪費空間。

tty設備被設計使用兩種類型的緩衝區,內核開發者選擇了在線路規則模塊內使用普通的緩衝區(tty buffer),而低層硬件驅動使用翻轉式緩衝區(flip_buffer),使用翻轉式緩衝,硬件驅動可以儘可能快的接收傳入的數據,不必與上層同步操作:flip_buffers由硬件獨佔使用,flip_buffers的數據被tty_flip_buffer_push函數轉存入tty buffer。

所謂的翻轉式緩衝區其實就是兩塊一樣大小的可以來回寫入數據的物理緩衝區塊。在底層的中斷處理函數返回前,函數flush_to_ldisc會被調用來翻轉緩衝區的讀寫指針,翻轉的具體實現可參考drivers/char/tty_io.c中的flush_to_ldisc()。

如何切換自定的線路規則模塊

[tty設備驅動子系統]提供了線路規則模塊的註冊接口(tty_register_ldisc()),定義了線路規則驅動結構(tty_ldisc ),驅動開發者可以自定線程路規則。線路規則有一個數值標識,有一個符號名。例如,缺省的tty線路規則的標識值是N_TTY,PPP的標識值是N_PPP,這些值在include/linux/tty.h 內靜態定義。值得注意的是,目前的設計沒有保留可用的標識值給本地自定使用,因此你只能手動在系統中“偷”一個值來使用。例如到目前爲止,N_MOUSE還沒有被廣泛使用,你可以用它來自定一個線路規則 。

要切換到N_MOUSE,在用戶程序中使用以下代碼:

#include ....

int i = N_MOUSE;
ioctl(fd, TIOCSETD, &i);
劉建文原創,引用請註明出處。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章