1. 概念介紹:終端
在Linux系統中, 與終端相關的概念很容易讓人迷糊. 首先有終端這個概念, 然后還有各種類型的終端(串口終端, 偽終端, 控制台終端, 控制終端), 還有一個概念叫console.
那么什么是終端? 什么是控制台終端? 什么是console?
為了理清這些疑問, 我們來依次介紹這些概念.
1.1 終端
大家都知道, 最初的計算機由於價格昂貴, 因此, 一台計算機一般是由多個人同時使用的. 在以前專門有這種可以連上一台電腦的設備, 只有顯示器和鍵盤,還有簡單的處理電路,本身不具有處理計算機信息的能力, 他是負責連接到一台正常的計算機上(通常是通過串口) ,然后登陸計算機,並對該計算機進行操作。當然,那時候的計算機操作系統都是多任務多用戶的操作系統。這樣一台只有顯示器和鍵盤能夠通過串口連接到計算機的設備就叫做終端.
終端的主要目的是提供人機交互的接口, 讓他人可以通過終端控制本機.
在Linux系統中, tty就是終端子系統. tty一詞源於Teletypes, 是最早出現的一種終端設備,類似電傳打字機。tty-core是終端子系統的核心. tty-core上層是字符設備驅動, 通過字符設備驅動, 終端子系統會在/dev目錄下創建各種各樣的tty節點, 下文會具體介紹這些節點. 有了這些節點, 就可以通過終端來控制本機了.
怎么控制呢?
想象一下現在有一塊樹莓派的板子, 系統啟動之后, 在/dev下創建了一個節點. 然后有一個程序提供了控制本機的能力, 比如getty, 它運行在板子上. getty首先會提示用戶登錄, 比如它會往終端節點輸出一個 "login:" 字符串, 然后該字符串通過節點進入到tty-core. 注意, 這個時候"login:"還只存在於板上的Linux內核中, 沒有任何人可以看到它. tty-core收到字符串之后改怎么辦呢? 它需要把該字符串發送給用戶, 怎么發送? 可以選擇樹莓派的串口, 然后用戶在某個別的機器比如XP電腦上, 通過某個工具比如SecureCRT打開這個串口, 就可以看見"login:"了. 然后用戶輸入登陸用戶名, 就會沿原路反饋給getty程序, getty驗證輸入的用戶名是否為一個有效的名字, 然后提示用戶輸入密碼 ……, 這樣就實現了人機交互, 控制本機的功能.
串口在這樣一個過程中扮演了什么角色呢? 它是一個傳輸數據的載體. 根據載體的不同, 終端可以分為串行端口終端, 偽終端, 控制台終端.
1.2 控制台
理解了終端的概念之后, 在來看看什么是控制台.
簡單來講, 控制台就是Linux的顯示子系統+輸入子系統.
還是以樹莓派的板子為例, 假如板子上接了HDMI顯示器, 插上了USB鼠標鍵盤, 那么HDMI+鼠標鍵盤就是板子的控制台了.
控制台與終端一樣, 都具有輸入/輸出的功能. 例如系統可以通過串口打印/獲取信息; 也可以通過控制台的顯示系統打印信息, 通過輸入系統獲取信息.
理解了控制台的概念之后, 就好理解后面的控制台終端了.
另外, 在Linux內核中還存在一個概念叫console子系統, 雖然console翻譯過來就是控制台, 但是在本文的語義環境中, 請區分console和控制台: console是與內核中的printk機制相關的, 而控制台則代指(顯示+輸入)子系統.
1.3 不同類型的終端
根據載體的不同, 終端可分為多種類型. 下面分別介紹
串行端口終端(/dev/ttySn)
很好理解, 它就是載體為串口的終端. 設備節點名通常是/dev/ttyS0等, 也有USB轉串口類型的終端, 節點名通常是/dev/ttyUSB0等.
控制台終端(/dev/console, /dev/tty0, /dev/tty1 ...)
上面我們理解了什么是控制台, 控制台本身就有接收輸入和顯示輸出的功能, 只不過它的輸入一般是輸入子系統(鍵盤, 鼠標等). 它的輸出一般是顯示系統. 控制台終端就是把控制台的輸入/輸出功能做為載體, 借助它來創建終端.
控制台終端的節點名是: /dev/tty0, /dev/tty1 …, /dev/tty6
以樹莓派的板子為例, 接上USB鼠標, HDMI顯示器, 啟動系統. 系統啟動完畢之后, 就會在HDMI上看到一個登陸界面. 這個登陸界面就是getty程序在/dev/tty1上創建的. 按下Alt+F1 - F6, 可以看到6個登陸界面, 這些登陸界面是getty分別在/dev/tty1-6上創建的. /etc/inittab中會控制getty程序在哪些控制台終端上登陸.
/dev/tty0可以理解為1個鏈接, 它鏈接到當前正在使用的控制台終端, 比如現在通過Alt+F2切換到/dev/tty2對應的控制台終端, 然后輸入命令echo test > /dev/tty0, 就會在/dev/tty2對應的控制台終端上看到test. 如果用Alt+F3切換到另一個終端, 在做同樣的動作, 也會在F3對應的終端上看到test. 但是不論在哪個終端, 輸入命令echo test > /dev/tty2, 都只會在Alt+F2對應的終端上看到test. 不管當前系統使用的是哪個控制台終端, “系統相關信息”都會發送到/dev/tty0. 只有”系統”或超級用戶root可以向/dev/tty0進行寫操作.
/dev/console也可以理解為一個鏈接. 只有在單用戶模式下可以在/dev/console上登陸.
通過bootargs,可以告訴Linux內核,在系統啟動階段,printk的信息將會打印至何處。
在樹莓派上, 如果console=/dev/ttyS0, 115200 console=/dev/tty1,則串口和HDMI上都會看到printk打印的信息。
不過當Linux內核啟動完畢之后,有應用程序再去open /dev/console時,得到的是最后一次傳入的值。比如上例中就是/dev/tty1. 以上例為基礎,不管你在任何終端上輸入echo test > /dev/console, 最終都會在HDMI的tty1終端上顯示出來。 內核啟動完畢之后,在文件系統的啟動過程中,會初始化一些程序(比如ssh, alsa-lib等),此時這些程序的輸出信息會定位到/dev/console上,這也是為什么我們只能在HDMI上看到這些信息的原因。
偽終端(pty)
偽終端主要是用於通過網絡來控制本機.
以telnet為例. 在樹莓派的板子上, 會有一個telnet的守護進程, 該守護進程通過網絡(TCP/IP協議)與其它機器通信, 監聽是否有其它機器想通過telnet連接到本機, 當收到連接請求之后, 守護進程會fork出一個子進程, 在子進程上運行控制本機的程序(比如getty). 接着getty就會打開一個偽終端節點(/dev/ttyp2), 我們把該節點稱為從設備節點(s2). 然后getty就會往s2發送一個"login:"字符串. 當這個字符串被傳遞到tty-core里面之后, 下一步該送往何地呢? 如果是串行端口終端, 可以通過串口發出去. 不過在偽終端中, 字符串會發送到另一個節點(/dev/ptyp2), 我們把這個節點稱為主設備節點(m2). telnet守護進程會讀取m2中的數據, 然后通過TCP/IP協議發給其它機器.
因此: 偽終端是成對出現的邏輯設備(s2/m2), 偽終端的載體不是真實的硬件, 而是一個軟件編寫的邏輯設備(m2).
偽終端與前面說的終端在表現形式上最大的不同, 就是它總是成對出現, 而不是單一的一個。它分為“偽終端主設備(/dev/ptyMN)”和“偽終端從設備”。(/dev/ttyMN)。其中,M與N的命名方式如下:
M: p q r s t u v w x y z a b c d e 共16 個
N: 0 1 2 3 4 5 6 7 8 9 a b c d e f 共16 個
這樣,默認支持最大是256個。這種命名方式有一些問題,同時終端的最大個數也被限制了,因此Linux內核引入了一種新的命名方式:UNIX98_PTYS
UNIX98_PTYS
在這種命名方式下,有一個設備節點(/dev/ptmx)作為所有偽終端的主設備。當有進程打開/dev/ptmx時,就會在/dev/pts/目錄下生成一個對應的從設備。這時的主設備(1)和從設備(N)存在一對多的關系.
控制終端(tty)
/dev/tty這個終端沒有任何載體,可以把它理解成一個鏈接,會鏈接到當前進程所打開的實際的終端。在當前進程的命令行里面輸入tty可以查看/dev/tty所對應的終端。比如getty這個程序運行在為終端的從設備/dev/pts/5上,那么輸入tty命令的時候,顯示的就是/dev/pts/5
1.4 了解系統中存在的終端
/proc/tty/drivers:
showing the name of the driver, the default node name, the major number for the driver, the range of minors used by the driver, and the type of the tty driver
cat /proc/tty/drivers
name of the driver |
default node name |
major number |
range of minors |
type of the tty driver |
/dev/tty |
/dev/tty |
5 |
0 |
system:/dev/tty |
/dev/console |
/dev/console |
5 |
1 |
system:console |
/dev/ptmx |
/dev/ptmx |
5 |
2 |
system |
/dev/vc/0 |
/dev/vc/0 |
4 |
0 |
system:vtmaster |
usbserial |
/dev/ttyUSB |
188 |
0-254 |
serial |
serial |
/dev/ttyS |
4 |
64-67 |
serial |
pty_slave |
/dev/pts |
136 |
0-255 |
pty:slave |
pty_master |
/dev/ptm |
128 |
0-255 |
pty:master |
pty_slave |
/dev/ttyp |
3 |
0-255 |
pty:slave |
pty_master |
/dev/pty |
2 |
0-255 |
pty:master |
unknown |
/dev/tty |
4 |
1-63 |
console |
/proc/tty/driver/files
contains individual files for some of the tty drivers, if they implement that functionality. The default serial driver creates a file in this directory that shows a lot of serial-port-specific information about the hardware
/sys/class/tty
All of the tty devices currently registered and present in the kernel have their own subdirectory under /sys/class/tty. Within that subdirectory, there is a "dev" file that contains the major and minor number assigned to that tty device. If the driver tells the kernel the locations of the physical device and driver associated with the tty device, it creates symlinks back to them
2. tty子系統架構介紹
所有的終端節點都是字符設備驅動, 因此最上層是字符設備驅動.
字符設備驅動下面是tty子系統, 先貼一張圖
tty core是對終端這個概念的抽象, 它實現了各種不同類型的終端的通用功能
tty driver是載體的驅動程序,比如我們用串口作為載體,則tty driver就是串口的驅動。
driver只用關心如何把數據發給硬件(比如串口, 就是發送寄存器)以及如何從硬件接收數據,core會考慮如何以統一的形式與用戶空間交互,交互的數據格式是怎樣的。這里的數據格式是指軟件上的概念,可以理解成協議,比如是否需要封裝頭部,頭部信息是怎樣的。
當core收到數據之后,它會傳遞給tty line discipline, discipline發給driver,driver在把數據變成硬件可以接受的格式,從硬件發送出去。反過來當硬件收到數據之后,driver會把這個數據寫到一個緩沖區, 然后把緩沖區的數據推送到discipline的緩沖區里面,用戶空間會通過read接口從discipline的緩沖區里面讀取數據。
core也可以直接和driver交互而不用通過discipline。不過通常都會有一個discipline存在。
tty line discipline的主要目的是對傳輸的數據進行一些協議上的解封/封裝, 比如PPP或者Bluetooth。
從driver的角度來看,它不知道數據是core直接給它的還是經過discipline之后再給它的。driver只知道把收到的數據發給硬件和從硬件中讀取數據,不清楚數據是否封裝了一些協議。這種設計也是符合邏輯的,硬件只知道一個bit一個bit的傳輸和接收數據,才不管傳輸的數據代表什么意思
理解了這3個概念, 我們就知道如果要添加一個串行端口終端,那么就需要做一個串口驅動,這個驅動要符合tty driver的規范,也就是按照tty driver的要求,實現必要的接口函數,然后向tty core注冊,接下來就萬事大吉了。對於其他類型的載體,比如虛擬終端或者控制台終端,也是一樣,實現一個tty driver並注冊即可。
嵌入式SOC中,串口一般叫UART或者USART,每個芯片的數據手冊里面一般都有一章節來描述這個模塊。不同的芯片廠商,比如Atmel和TI,它們的UART模塊多少有點不一樣,但是絕大部分都是一樣的,比如都有start/stop bit,波特率,等。因此Linux內核中又抽象出了一個概念: Serial core
Serial core: Serial core在tty driver下面, 它把串口設備的一些通用的東西抽象出來了,這樣對於不同的廠商的UART模塊,就不需要從頭到尾完全實現一遍tty driver要求的接口,只需要定義一個簡單的UART driver,然后向Serial Core注冊,接下來Serial Core就會把自己封裝成tty driver的形式,向tty core進行注冊,從而完成添加一個串行端口終端的動作。簡化了串行端口終端驅動的開發。
接下來, 我們首先介紹一下tty driver, 它是一個承上啟下的模塊:
對上, 它與tty core交互
對下, 它提供接口給serial core
然后我們在介紹tty core, 接着是serial core, 最后是tty line discipline.
3. tty driver
3.1 簡介
如果你想編寫一個終端驅動, 需要遵循如下步驟:
首先, 創建一個struct tty_driver結構體.
內核代碼提供了一個API (alloc_tty_driver), 專門用於創建這個結構體, 給該結構體分配內存.
struct tty_driver tiny_tty_driver = alloc_tty_driver(TINY_TTY_MINORS);
然后, 定義一個tty_operations結構體, 並編寫相應的實現函數:
static struct tty_operations serial_ops = {
.open = tiny_open,
.close = tiny_close,
.write = tiny_write,
.write_room = tiny_write_room,
.set_termios = tiny_set_termios,
};
然后, 初始化剛剛創建的tiny_tty_driver
/* initialize the tty driver */
tiny_tty_driver->owner = THIS_MODULE;
tiny_tty_driver->driver_name = "tiny_tty";
tiny_tty_driver->name = "ttty";
tiny_tty_driver->devfs_name = "tts/ttty%d";
tiny_tty_driver->major = TINY_TTY_MAJOR,
tiny_tty_driver->type = TTY_DRIVER_TYPE_SERIAL,
tiny_tty_driver->subtype = SERIAL_TYPE_NORMAL,
tiny_tty_driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_NO_DEVFS,
tiny_tty_driver->init_termios = tty_std_termios;
tiny_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
tty_set_operations(tiny_tty_driver, &serial_ops);
然后, 調用tty driver子系統提供的API, 注冊該driver.
/* register the tty driver */
retval = tty_register_driver(tiny_tty_driver);
當注冊完成無誤之后, 你會發現如下變化:
/dev/
可能為在/dev/下面出現多個tiny_tty_driver->name開頭的設備節點, 例如/dev/ttty0, /dev/ttty1.
為什么說可能而不是一定? /dev/下面到底會出現幾個節點? 為什么是以tiny_tty_driver->name開頭的? 這些疑問將在本節詳細分析中找到答案.
該文件中會多出一行
$ cat /proc/tty/drivers
tiny_tty /dev/ttty 240 0-3 serial
此行數據依次是: tiny_tty_driver->driver_name , tiny_tty_driver->name , 主設備號 , 次設備號范圍 , tiny_tty_driver->type
/sys/class/tty
會在該目錄下出現多個子目錄, 子目錄的名稱以tiny_tty_driver->name打頭.
同樣, 本節后面的內容會介紹為什么.
上述就是編寫終端驅動的基本步驟. 不管你是編寫串行終端驅動, 還是虛擬終端驅動, 或是控制台終端驅動, 都應遵循上述步驟.
不過對於串行終端驅動, serial core已經幫你完成了上述步驟, 你只需要向serial core子系統進行注冊即可.
3.2 主要數據結構
根據前一節的簡介, 我們提煉出幾個主要的數據結構, 分別介紹它們: tty_driver, tty_operations.
tty_driver
在tty子系統中, tty_driver用於描述一個tty驅動. 要編寫一個終端驅動, 必須定義一個tty_driver結構體. 然后用此結構體向tty子系統進行注冊.
頭文件: include/linux/tty_driver.h
Comment |
|
intmagic |
magic number for this structure |
struct kref kref |
引用計數 |
struct cdev *cdevs |
描述一個字符設備驅動的結構體. 在本文開頭介紹過, 所有的終端節點(/dev/ttyxx)都是字符設備驅動 |
struct module*owner |
|
const char*driver_name |
會出現在/proc/tty/drivers中的第一列. The driver_name variable should be set to something short, descriptive, and unique among all tty drivers in the kernel |
const char*name |
會出現在/dev/ , /sys/class/tty/ , /proc/tty/drivers的第二列. |
intname_base |
name的起始編號, 一般情況下默認是0 /dev/下的節點名和/sys/class/tty/下的目錄名是由(name+name_base)組成的. 例如name=ttty, name_base=0, 組合之后就是ttty0 |
該driver的主設備號. 與字符設備驅動相關. |
|
intminor_start |
該driver的次設備號的起始值. 與字符設備驅動相關. |
unsigned intnum |
表示該driver在注冊字符設備驅動的時候, 可以注冊幾個次設備. 次設備的設備號從(major+minor_start)開始遞增. 假如num = 3, 如果使能了創建設備節點, 則/dev/下會多出來3個節點, /sys/class/tty/下也會多出來3個文件夾 |
shorttype |
該driver的類型, 以下幾種類型之一: /* tty driver types */ #define TTY_DRIVER_TYPE_SYSTEM0x0001 #define TTY_DRIVER_TYPE_CONSOLE0x0002 #define TTY_DRIVER_TYPE_SERIAL0x0003 #define TTY_DRIVER_TYPE_PTY0x0004 #define TTY_DRIVER_TYPE_SCC0x0005/* scc driver */ #define TTY_DRIVER_TYPE_SYSCONS0x0006 |
shortsubtype |
該driver的子類型, 以下幾種子類型之一 include/linux/tty_driver.h /* system subtypes (magic, used by tty_io.c) */ #define SYSTEM_TYPE_TTY0x0001 #define SYSTEM_TYPE_CONSOLE0x0002 #define SYSTEM_TYPE_SYSCONS0x0003 #define SYSTEM_TYPE_SYSPTMX0x0004
/* pty subtypes (magic, used by tty_io.c) */ #define PTY_TYPE_MASTER0x0001 #define PTY_TYPE_SLAVE0x0002
/* serial subtype definitions */ #define SERIAL_TYPE_NORMAL1 |
struct ktermios init_termios |
如果用戶空間要配置一個終端的波特率, 起始/停止位, 奇偶校驗等參數時, 一般會准備一個termios結構體, 然后把這個結構體設置到內核驅動里面. init_termios代表該driver的初始termios |
unsigned longflags |
該driver的flags, 可以用以下幾種類型相或(|) flags決定了在向tty子系統進行注冊時, 系統會采取何種動作, 例如是否創建/dev/節點等等. flags的定義在include/linux/tty_driver.h, 針對每一個flag的意思, 該文件中也有詳細的注釋: #define TTY_DRIVER_INSTALLED0x0001 #define TTY_DRIVER_RESET_TERMIOS0x0002 #define TTY_DRIVER_REAL_RAW0x0004 #define TTY_DRIVER_DYNAMIC_DEV0x0008 #define TTY_DRIVER_DEVPTS_MEM0x0010 #define TTY_DRIVER_HARDWARE_BREAK0x0020 #define TTY_DRIVER_DYNAMIC_ALLOC0x0040 #define TTY_DRIVER_UNNUMBERED_NODE0x0080 |
struct proc_dir_entry *proc_entry |
proc系統相關, 用於生成/proc/ tty/driver/下的文件 |
struct tty_driver *other |
only used for the PTY driver |
struct tty_struct **ttys |
指針, 指向tty_struct結構體, tty_struct將會在tty core一節中詳細介紹 |
struct tty_port **ports |
指針, 指向tty_port結構體, tty_port將會在tty core一節中詳細介紹 |
struct ktermios **termios |
用於鏈接與該driver相關的所有的termios |
void *driver_state |
|
const struct tty_operations *ops |
與該driver關聯的ops, 后面會專門介紹這個結構體 |
struct list_head tty_drivers |
tty driver子系統中會有一個全局鏈表頭, 掛載所有注冊的driver. 這里的tty_drivers用於把自己掛接到全局的鏈表頭下 |
tty_operations
tty_operations用於描述一個tty_driver的操作函數.
仔細觀察這些操作函數的參數, 會發現它們都與struct tty_struct這個結構體有關系. tty_struct是用於描述當一個tty設備被open之后, 所有與之相關的狀態. 換言之, tty_struct是一個run-time階段的數據結構. 我們會在tty core一節詳細介紹這個結構體.
頭文件: include/linux/tty_driver.h
struct tty_operations |
Comment |
struct tty_struct * (*lookup)(struct tty_driver *driver,struct inode *inode, int idx) |
這些接口函數的意思就現在暫時不詳細介紹了, 只是簡單列出來 |
int (*install)(struct tty_driver *driver, struct tty_struct *tty) |
|
void (*remove)(struct tty_driver *driver, struct tty_struct *tty) |
|
int (*open)(struct tty_struct * tty, struct file * filp) |
|
void (*close)(struct tty_struct * tty, struct file * filp) |
|
void (*shutdown)(struct tty_struct *tty) |
|
void (*cleanup)(struct tty_struct *tty) |
|
int (*write)(struct tty_struct * tty, const unsigned char *buf, int count) |
|
int (*put_char)(struct tty_struct *tty, unsigned char ch) |
|
void (*flush_chars)(struct tty_struct *tty) |
|
int (*write_room)(struct tty_struct *tty) |
|
int (*chars_in_buffer)(struct tty_struct *tty) |
|
int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg) |
|
long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg) |
|
void (*set_termios)(struct tty_struct *tty, struct ktermios * old) |
|
void (*throttle)(struct tty_struct * tty) |
|
void (*unthrottle)(struct tty_struct * tty) |
|
void (*stop)(struct tty_struct *tty) |
|
void (*start)(struct tty_struct *tty) |
|
void (*hangup)(struct tty_struct *tty) |
|
int (*break_ctl)(struct tty_struct *tty, int state) |
|
void (*flush_buffer)(struct tty_struct *tty) |
|
void (*set_ldisc)(struct tty_struct *tty) |
|
void (*wait_until_sent)(struct tty_struct *tty, int timeout) |
|
void (*send_xchar)(struct tty_struct *tty, char ch) |
|
int (*tiocmget)(struct tty_struct *tty) |
|
int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear) |
|
int (*resize)(struct tty_struct *tty, struct winsize *ws) |
|
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew) |
|
int (*get_icount)(struct tty_struct *tty,struct serial_icounter_struct *icount) |
|
#ifdef CONFIG_CONSOLE_POLL int (*poll_init)(struct tty_driver *driver, int line, char *options); int (*poll_get_char)(struct tty_driver *driver, int line); void (*poll_put_char)(struct tty_driver *driver, int line, char ch); #endif |
|
const struct file_operations *proc_fops |
|
3.3 主要API說明
根據3.1節的簡介, 我們知道最主要的一個API: tty_register_driver.
該API里面還會調用其它幾個API : tty_register_device, proc_tty_register_driver
下面我們分別介紹它們.
tty_register_driver
若想編寫一個終端驅動, 首先會准備好tty_driver和tty_operations結構體, 然后調用tty_register_driver向tty driver子系統進行注冊.
下面我們詳細分析一下, tty_register_driver里面到底做了哪些事情.
頭文件: include/linux/tty.h
實現文件: drivers/tty/tty_io.c
int tty_register_driver(struct tty_driver *driver)
用動態/靜態的方式分配主次設備號, 並賦值給driver-> major, driver-> minor_start.
if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC), 則調用tty_cdev_add向字符設備驅動子系統注冊一個字符設備驅動.
tty_cdev_add封裝了字符設備驅動的一些API, 我們看下這個函數的細節:
static int tty_cdev_add(struct tty_driver *driver, dev_t dev,
unsigned int index, unsigned int count)
{
/* init here, since reused cdevs cause crashes */
cdev_init(&driver->cdevs[index], &tty_fops);
driver->cdevs[index].owner = driver->owner;
return cdev_add(&driver->cdevs[index], dev, count);
}
當cdev_add成功返回之后, 字符設備驅動就已經注冊成功了. 不過請注意, 此時並不會自動在/dev/創建設備節點. 后面會有其它的代碼來創建設備節點.
另外需要特別注意tty_fops這個結構體, 它是內核系統定義的一個結構體(在drivers/tty/tty_io.c中). 假設/dev/下已經創建了設備節點, 當我們在用戶空間調用open/read/write/close等操作時, 最終就會映射到tty_fops這個結構體上.
list_add(&driver->tty_drivers, &tty_drivers);
tty_drivers是drivers/tty/tty_io.c中定義的一個全局鏈表頭, 這里把”driver”掛接到這個全局鏈表頭下.
if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)), (如果flags沒有定義TTY_DRIVER_DYNAMIC_DEV)
for (i = 0; i < driver->num; i++) (針對每一個num)
調用一次tty_register_device.
tty_register_device會在/dev/目錄下創建1個對應的字符設備驅動節點, 同時也會在/sys/class/tty目錄下創建1個對應的子目錄. for循環總共調用(driver->num)次tty_register_device, 所以/dev/下就會出現(driver->num)個設備節點, /sys/class/tty下也會出現(driver->num)個子目錄.
至於為什么tty_register_device為什么能創建設備節點, 節點名是什么? 以及為什么會在/sys/class/tty下創建子目錄, 目錄名是什么? 后文會詳解分析這個API, 屆時會找到答案.
proc_tty_register_driver(driver)
proc_tty_register_driver會在/proc/tty/driver/目錄下創建一個子目錄, 子目錄的名稱是tty_driver->driver_name.
后文會專門介紹一下proc_tty_register_driver這個API.
driver->flags |= TTY_DRIVER_INSTALLED
設置flags標志, 代表該driver已經被正確注冊了.
tty_register_device
tty_register_device主要用於生成設備節點和/sys/class/tty下的子目錄.
在編寫終端驅動時, 當調用tty_register_driver向tty子系統注冊時, 如果沒有設置TTY_DRIVER_DYNAMIC_DEV, 則會自動調用tty_register_device; 如果設置了TTY_DRIVER_DYNAMIC_DEV, 也可以在后面再手動調用tty_register_device來創建設備節點和class下的子目錄.
下面我們詳細分析一下, tty_register_device里面到底做了哪些事情.
頭文件: include/linux/tty.h
實現文件: drivers/tty/tty_io.c
struct device *tty_register_device(struct tty_driver *driver, unsigned index,
struct device *device)
{
return tty_register_device_attr(driver, index, device, NULL, NULL);
}
直接調用tty_register_device_attr, 來看看tty_register_device_attr:
struct device *tty_register_device_attr(struct tty_driver *driver,
unsigned index, struct device *device,
void *drvdata,
const struct attribute_group **attr_grp)
if (index >= driver->num), 則返回錯誤. 說明傳過來的index參數不能大於driver本身的num數
if (driver->type == TTY_DRIVER_TYPE_PTY)
pty_line_name(driver, index, name);
else
tty_line_name(driver, index, name)
根據driver的type, 若是PTY, 則調用pty_line_name; 若是TTY, 則調用tty_line_name.
pty_line_name和tty_line_name的目的就是設置名稱, 結果存儲在name變量中. 它們內部會調用sprintf, 格式化輸出名稱. 具體細節可以看代碼.
例如, 如果type是TTY, 則name最終可能是結果是 ("%s%d", driver->name, index + driver->name_base).
這個name很重要, /dev/下的節點名和/sys/class/tty/下的子目錄名都是靠它決定的.
if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)), 則調用tty_cdev_add注冊字符設備驅動. 這里與tty_register_driver里面的if (driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)遙相呼應. 如果tty_register_driver中已經注冊了字符設備驅動, 那么這里就不需要再次注冊了.
接着分配一個struct device結構體, 給dev->devt, dev->class等賦值, 設置dev的name. 然后調用device_register(dev)注冊該device.
device_register在《設備模型》一文中詳細介紹過, 它會創建設備節點, 創建class下的子目錄. 具體細節請看《設備模型》中的對應章節.
proc_tty_register_driver & /proc/tty/drivers
proc是Linux系統中的一個子模塊, 跟sysfs有點類似, 也算一個虛擬的文件系統. 如果你向proc進行注冊, 注冊成功之后, 用戶空間就可以通過/proc/xxx與你的”proc driver”交互.
一般情況下, 我們會通過系統提供一些調試信息給到用戶空間, 因此我們把proc界定為調試技術, 會在《調試技術》一文中詳細介紹它.
這里, 我們只是簡單看下tty子系統向proc注冊了些什么東西.
頭文件: include/linux/tty.h
實現文件: fs/proc/proc_tty.c
/proc/tty/drivers文件是如何生成的?
proc_tty.c的初始化函數里面會創建這個文件, 代碼如下:
/*
* Called by proc_root_init() to initialize the /proc/tty subtree
*/
void __init proc_tty_init(void)
{
if (!proc_mkdir("tty", NULL))
return;
proc_mkdir("tty/ldisc", NULL); /* Preserved: it's userspace visible */
/*
* /proc/tty/driver/serial reveals the exact character counts for
* serial links which is just too easy to abuse for inferring
* password lengths and inter-keystroke timings during password
* entry.
*/
proc_tty_driver = proc_mkdir_mode("tty/driver", S_IRUSR|S_IXUSR, NULL);
proc_create("tty/ldiscs", 0, NULL, &tty_ldiscs_proc_fops);
proc_create("tty/drivers", 0, NULL, &proc_tty_drivers_operations);
}
前文說過, 我們可以通過cat /proc/tty/drivers, 來查看系統中注冊了多少個tty driver.
如何實現的呢? cat操作最終會映射到proc_tty_drivers_operations, 該operations最終會掃描tty_drivers這個全局鏈表頭下的所有driver, 然后把它們的信息反饋到用戶空間.
proc_tty_register_driver
tty_register_driver里面會調用此API, 此API的代碼細節如下:
/*
* This function is called by tty_register_driver() to handle
* registering the driver's /proc handler into /proc/tty/driver/<foo>
*/
void proc_tty_register_driver(struct tty_driver *driver)
{
struct proc_dir_entry *ent;
if (!driver->driver_name || driver->proc_entry ||
!driver->ops->proc_fops)
return;
ent = proc_create_data(driver->driver_name, 0, proc_tty_driver,
driver->ops->proc_fops, driver);
driver->proc_entry = ent;
}
如果tty_driver結構體定義了ops->proc_fops, 則會在/proc/tty/driver/目錄下創建一個文件, 文件的名稱是driver->driver_name. 我們可以cat此文件, 以便獲取一些必要的信息. cat操作最終會映射到driver->ops->proc_fops.
4. tty core
前一節我們介紹了如何向tty driver子系統注冊一個終端驅動.
驅動注冊成功之后, 用戶空間就可以通過tty core子系統提供的接口與驅動交互了.
本小節, 我們從用戶空間的角度, 來看看tty core子系統的內部邏輯.
4.1 簡介
前文說過, 所有的終端設備, 從用戶空間的角度來看, 都是字符設備驅動.
在注冊tty_driver時, tty_register_driver會調用tty_cdev_add來注冊字符設備驅動, 然后在tty_register_device中會創建設備節點.
tty_cdev_add在注冊字符設備驅動時, 使用的ops是drivers/tty/tty_io.c中實現的struct file_operations tty_fops. 用戶空間的open/read等操作, 最終就會映射到tty_fops上.
另外, 在介紹tty_driver這個結構體時, 我們也提到了tty_struct, tty_port, ktermios這幾個結構體.
上述這些數據結構我們划歸到tty core子系統, 將在本節詳細介紹它們.
4.2 主要數據結構
tty_struct
關於tty_struct和ktermios, 先看看官方代碼中的一段解釋:
* Where all of the state associated with a tty is kept while the tty
* is open. Since the termios state should be kept even if the tty
* has been closed --- for things like the baud rate, etc --- it is
* not stored here, but rather a pointer to the real state is stored
* here
tty_struct是用於表示一個tty設備被open之后的狀態, 當設備被close之后, 這個結構體就消失了. 這是它與tty_driver的區別.
termios在tty_driver注冊的時候, 就已經有一個初始值了. tty設備被open之后, 可以修改termios的值. 設備被close之后, 它並不會消失. 舉個例子: 假設我們open了一個tty設備, 然后把它的波特率設置為了9600, 如果我們close了這個設備, 然后在重新打開, 波特率不變, 還是9600.
struct tty_struct |
Comment |
intmagic |
magic number for this structure |
struct kref kref |
引用計數 |
struct device *dev |
tty_register_device中會創建一個device, 這里的dev指向那個被創建的device |
struct tty_driver *driver |
對應的tty_driver |
const struct tty_operations *ops |
tty_driver對應的那個tty_operations. 應該可以直接通過driver->ops訪問到它啊, 為什么要在這里把它單獨提出來呢? 在tty設備被open的時候, 會把driver->ops 賦值給 tty_struct->ops. 在需要的時候, 可以將tty_struct->ops重新賦值而不必更改driver->ops. |
int index |
一個tty_driver可以對應tty_driver->num個設備. 這里的 0 <= index <= tty_driver->num |
struct ld_semaphore ldisc_sem; struct tty_ldisc *ldisc |
ldisc指向該tty_struct對應的tty line discipline. ldisc_sem是一個互斥鎖, 用於互斥對ldisc的訪問. 例如假設我們想更改tty_struct->ldisc, 則需要先獲取鎖ldisc_sem |
struct mutex atomic_write_lock; struct mutex legacy_mutex; struct mutex throttle_mutex; struct rw_semaphore termios_rwsem; struct mutex winsize_mutex; spinlock_t ctrl_lock; spinlock_t flow_lock; |
定義了各種鎖, 用於互斥訪問. 我們在《競爭與阻塞》一文中介紹了各種鎖機制, 細節可以查看原文. |
/* Termios values are protected by the termios rwsem */ struct ktermios termios, termios_locked; struct termiox *termiox;/* May be NULL for unsupported */ |
該tty_struct對應的ktermios |
char name[64] |
該tty_struct的name, 它的值是 sprintf(p, "%s%d", driver->name, index + driver->name_base) |
struct pid *pgrp;/* Protected by ctrl lock */ struct pid *session; |
pid相關 |
unsigned long flags |
該tty_struct對應的flag, 以下幾種子類型之一 注意對flags的修改必須使用set_bit/clear_bit這樣的原子操作, 以避免並發訪問導致的各種問題 include/linux/tty.h #define TTY_THROTTLED 0/* Call unthrottle() at threshold min */ #define TTY_IO_ERROR 1/* Cause an I/O error (may be no ldisc too) */ #define TTY_OTHER_CLOSED 2/* Other side (if any) has closed */ #define TTY_EXCLUSIVE 3/* Exclusive open mode */ #define TTY_DEBUG 4/* Debugging */ #define TTY_DO_WRITE_WAKEUP 5/* Call write_wakeup after queuing new */ #define TTY_OTHER_DONE6/* Closed pty has completed input processing */ #define TTY_LDISC_OPEN 11/* Line discipline is open */ #define TTY_PTY_LOCK 16/* pty private */ #define TTY_NO_WRITE_SPLIT 17/* Preserve write boundaries to driver */ #define TTY_HUPPED 18/* Post driver->hangup() */ #define TTY_LDISC_HALTED22/* Line discipline is halted */ |
int count |
在用戶空間, 可以對一個tty設備節點open多次, 多次open在內核空間只對應1個tty_struct. count代表被open的次數. 在open/re-open時++, 在close時-- |
struct winsize winsize;/* winsize_mutex */ |
該tty_struct對應的窗口的size. 注意這個參數不像ktermios, 在tty_struct消失的時候, 它也會消失. 為什么不把它單獨拿出呢? 因為應用程序幾乎每次在open時, 都會設置winsize, 因此這里沒必要保存. |
unsigned long stopped:1,/* flow_lock */ flow_stopped:1, unused:BITS_PER_LONG - 2; |
一些變量的定義, 具體目的暫不清楚 |
int hw_stopped |
具體目的暫不清楚 |
unsigned long ctrl_status:8,/* ctrl_lock */ packet:1, unused_ctrl:BITS_PER_LONG - 9; |
一些變量的定義, 具體目的暫不清楚 |
unsigned int receive_room;/* Bytes free for queue */ |
一般會有一個buffer用來存儲用戶空間給過來的數據, 這個參數應該是指的buffer的剩余size. |
int flow_change |
具體目的暫不清楚 |
struct tty_struct *link |
具體目的暫不清楚 |
struct fasync_struct *fasync |
異步通知機制相關. 例如用戶空間可以丟一段數據下來, 但是不用再那里等着, 可以繼續執行其它程序. 當內核把這段數據傳輸完之后, 通知用戶空間. |
int alt_speed;/* For magic substitution of 38400 bps */ |
一個變量, 具體意義暫不清楚 |
wait_queue_head_t write_wait; wait_queue_head_t read_wait; |
兩個等待隊列. 在《競爭與阻塞》一文中有詳細介紹等待隊列 |
struct work_struct hangup_work |
一個工作隊列 |
void *disc_data |
與tty line discipline相關 |
void *driver_data |
|
struct list_head tty_files |
在《字符設備驅動》一文中我們講過, 每次open操作, 內核空間都會創建一個對應的struct file. 但是對tty設備的多次open操作, 內核只會有一個struct tty_struct. 這里的tty_files是一個鏈表頭, 所有的struct file都會掛接在這個鏈表頭下 |
int closing |
|
unsigned char *write_buf |
|
int write_cnt |
|
/* If the tty has a pending do_SAK, queue it here - akpm */ struct work_struct SAK_work; |
工作隊列, 具體目的看注釋 |
struct tty_port *port |
指向一個tty_port |
ktermios
Ktermios主要用於用戶空間配置tty設備, 配置其波特率, 奇偶校驗等等.
頭文件: include/uapi/asm-generic/termbits.h
具體的細節和每個字段的意思, 直接查看源代碼即可, 這里不多說了.
struct ktermios {
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
tty_port
回頭看一下tty_operations這個結構體, 會發現它只有write函數, 但是沒有read函數. 當用戶空間想發送數據時, write函數會被調用, 它會操作硬件(例如串口)把數據發送出去. 但是當硬件收到數據的時候, 它是如何傳遞給用戶空間的呢?
tty_port的作用就在於此, 你可以簡單的把它理解為一塊buffer. 當硬件收到數據之后, 它會把收到的數據存儲在tty_port的buffer里面, 然后用戶空間會從tty_port的buffer讀取數據.
在繼續下面的文章之前, 讓我們先梳理一下 /dev/設備節點, tty_driver, tty_struct, tty_port這幾者之間的關系.
一個tty_driver對應(tty_driver->num)個設備節點
一個設備節點在被open之后, 對應一個tty_struct
一個tty_struct對應一個tty_port
與tty_port相關的主要代碼和數據結構如下:
頭文件: include/linux/tty.h
struct tty_port數據結構的細節就不仔細介紹了, 直接放下代碼在這里.
struct tty_port_operations {
/* Return 1 if the carrier is raised */
int (*carrier_raised)(struct tty_port *port);
/* Control the DTR line */
void (*dtr_rts)(struct tty_port *port, int raise);
/* Called when the last close completes or a hangup finishes
IFF the port was initialized. Do not use to free resources. Called
under the port mutex to serialize against activate/shutdowns */
void (*shutdown)(struct tty_port *port);
/* Called under the port mutex from tty_port_open, serialized using
the port mutex */
/* FIXME: long term getting the tty argument *out* of this would be
good for consoles */
int (*activate)(struct tty_port *port, struct tty_struct *tty);
/* Called on the final put of a port */
void (*destruct)(struct tty_port *port);
};
struct tty_port {
struct tty_bufhead buf; /* Locked internally */
struct tty_struct *tty; /* Back pointer */
struct tty_struct *itty; /* internal back ptr */
const struct tty_port_operations *ops; /* Port operations */
spinlock_t lock; /* Lock protecting tty field */
int blocked_open; /* Waiting to open */
int count; /* Usage count */
wait_queue_head_t open_wait; /* Open waiters */
wait_queue_head_t close_wait; /* Close waiters */
wait_queue_head_t delta_msr_wait; /* Modem status change */
unsigned long flags; /* TTY flags ASY_*/
unsigned char console:1, /* port is a console */
low_latency:1; /* optional: tune for latency */
struct mutex mutex; /* Locking */
struct mutex buf_mutex; /* Buffer alloc lock */
unsigned char *xmit_buf; /* Optional buffer */
unsigned int close_delay; /* Close port delay */
unsigned int closing_wait; /* Delay for output */
int drain_delay; /* Set to zero if no pure time
based drain is needed else
set to size of fifo */
struct kref kref; /* Ref counter */
};
源文件 :
drivers/tty/tty_port.c : 該C文件提供了對於tty_port的一些API, 包括:
void tty_port_init(struct tty_port *port) : *port參數指向一塊已經分配好的空間, 該API用於初始化這塊空間
tty_port_link_device : 用於把tty_port和tty_driver關聯起來, 也就是讓tty_driver-> ports[index] = tty_port
tty_port_register_device
tty_port_register_device_attr
…….
drivers/tty/tty_buffer.c : 該C文件提供了操作tty_port->buf的一些API, 主要就是對buffer的處理, 該C文件對應的頭文件主要是include/linux/tty_flip.h:
void tty_buffer_init(struct tty_port *port): 主要用於初始化tty_port->buf, 注意這里並沒有給buffer分配空間
tty_buffer_request_room: 它會調用tty_buffer_alloc, 給buffer分配存儲空間
tty_buffer_set_limit : 設置buffer的size限制
tty_insert_flip_char : 往buffer里面插入一個字符
tty_insert_flip_string : 往buffer里面插入字符串
tty_buffer_space_avail : 獲取buffer的剩余空間
tty_flip_buffer_push : 把數據從tty_port->buf搬移到tty line discipline. 前文提到過, 用戶空間會從tty_port讀取硬件收到的數據, 實際上是從tty line discipline里面讀取的
4.3 主要API說明
tty core這個子模塊好像沒有向內核其它子模塊提供什么接口, 它主要的功能是向用戶空間提供了字符設備驅動接口. 因此本節我們主要看看這些字符設備驅動的接口函數.
由於這些接口函數的細節實現太過繁瑣, 加之我們在項目中主要工作是集中在驅動這塊, 對tty core只需大致了解即可, 因此本節只會做些粗略介紹, 大致理清代碼邏輯.
tty core提供的字符設備驅動, 最重要的是下面這個ops:
代碼路徑: drivers/tty/tty_io.c
static const struct file_operations tty_fops = {
.llseek = no_llseek,
.read = tty_read,
.write = tty_write,
.poll = tty_poll,
.unlocked_ioctl = tty_ioctl,
.compat_ioctl = tty_compat_ioctl,
.open = tty_open,
.release = tty_release,
.fasync = tty_fasync,
};
我們主要分析一下open/read/write/release這幾個函數.
open
當我們在用戶空間open一個tty的設備節點時, 此處的open函數將會被調用.
open函數的主要功能是創建並初始化tty_struct結構體.
如果用戶空間重復打開同一個tty設備節點, open函數並不會創建新的tty_struct, 只會執行tty_reopen操作, 在reopen里面, tty_struct->count++.
還有一個特殊情況: /dev/tty這個設備節點, 還記得它的作用嗎? 如果你對這個設備節點執行open操作, 內核空間會創建一個新的tty_struct結構體嗎? (答案是不會). 並且內核空間會直接通過(current->signal->tty)獲取已經創建好的tty_struct. 還記得current嗎? 它是struct task_struct類型的指針, 通過current, 我們可以得到進程的詳細信息.
下面我們來看看這個函數的代碼細節:
static int tty_open(struct inode *inode, struct file *filp)
{
struct tty_struct *tty;
struct tty_driver *driver = NULL;
dev_t device = inode->i_rdev;
......
tty = tty_open_current_tty(device, filp);
if (!tty) {
mutex_lock(&tty_mutex);
driver = tty_lookup_driver(device, filp, &noctty, &index);
if (IS_ERR(driver)) {
retval = PTR_ERR(driver);
goto err_unlock;
}
/* check whether we're reopening an existing tty */
tty = tty_driver_lookup_tty(driver, inode, index);
if (IS_ERR(tty)) {
retval = PTR_ERR(tty);
goto err_unlock;
}
if (tty) {
mutex_unlock(&tty_mutex);
tty_lock(tty);
/* safe to drop the kref from tty_driver_lookup_tty() */
tty_kref_put(tty);
retval = tty_reopen(tty);
if (retval < 0) {
tty_unlock(tty);
tty = ERR_PTR(retval);
}
} else { /* Returns with the tty_lock held for now */
tty = tty_init_dev(driver, index);
mutex_unlock(&tty_mutex);
}
tty_driver_kref_put(driver);
}
......
}
tty_open_current_tty : 當對/dev/tty節點執行open操作時, 這個函數就會起作用, 它會調用get_current_tty, 從(current->signal->tty)獲取已經創建好的tty_struct
tty_lookup_driver : 通過設備號找到對應的tty_driver. 能猜到實現邏輯嗎? 也很簡單, 首先我們已知設備號, 然后所有的tty_driver都被掛載到全局鏈表頭tty_drivers下面了, 逐個掃描鏈表頭下面掛接的所有的tty_driver, 對比設備號, 即可找到對應的tty_driver.
tty_driver_lookup_tty : 查看tty_deriver->ttys[idx]是否為NULL, 如果不為空, 則證明是重復open同一個tty設備節點, 直接執行tty_reopen操作即可, 不用創建新的tty_struct.
tty_init_dev : 創建tty_struct結構體. 它會調用alloc_tty_struct分配並初始化tty_struct.
read
當我們在用戶空間執行read操作時, 此處的read函數將會被調用. read的主要目的是把數據從內核空間返回給用戶空間.
前文我們說過, 當我們的硬件(例如串口)收到了數據之后, 會通過tty_port存儲到tty line discipline里面. 這里的read操作就是從tty_ldisc獲取數據, 然后返回給用戶空間.
直接貼一下代碼吧:
static ssize_t tty_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
int i;
struct inode *inode = file_inode(file);
struct tty_struct *tty = file_tty(file);
struct tty_ldisc *ld;
if (tty_paranoia_check(tty, inode, "tty_read"))
return -EIO;
if (!tty || (test_bit(TTY_IO_ERROR, &tty->flags)))
return -EIO;
/* We want to wait for the line discipline to sort out in this
situation */
ld = tty_ldisc_ref_wait(tty);
if (ld->ops->read)
i = ld->ops->read(tty, file, buf, count);
else
i = -EIO;
tty_ldisc_deref(ld);
if (i > 0)
tty_update_time(&inode->i_atime);
return i;
}
代碼很簡單, 調用ld->ops->read讀取數據. 這里簡單是因為的主要的邏輯都是在tty line discipline中處理的, 我們在介紹這一節時在詳細描述.
write
當我們在用戶空間執行write操作時, 此處的write函數將會被調用. write的主要目的是把數據從用戶空間傳遞到內核空間, 然后通過硬件發送出去.
write的邏輯也很簡單, 收到用戶空間的數據之后, 調用tty line discipline發送數據, tty line discipline會調用tty_driver->ops->write函數把數據通過硬件發送出去.
代碼如下:
static ssize_t tty_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct tty_struct *tty = file_tty(file);
struct tty_ldisc *ld;
ssize_t ret;
......
ld = tty_ldisc_ref_wait(tty);
if (!ld->ops->write)
ret = -EIO;
else
ret = do_tty_write(ld->ops->write, tty, file, buf, count);
tty_ldisc_deref(ld);
return ret;
}
do_tty_write會調用ld->ops->write, ld->ops->write最終會調用tty_driver->ops->write函數把數據通過硬件發送出去.
release
用戶空間執行close操作時, 會導致release函數被調用.
不過不是每次close操作都會導致release函數被調用, 如果設備節點被open了多次, 那么只有最后一次close操作才會導致release函數被調用, 這個邏輯是字符設備驅動控制的, 具體細節可以回頭看看《字符設備驅動》一文.
所以一旦這里的release函數被執行, 就代表所有的open操作都已經close了. release函數里面會釋放tty_struct這個結構體.
代碼就不貼了.
5. tty line discipline
5.1 簡介
tty line discipline(后文簡稱ldis)的作用我們在第2章大致介紹過, 現在我們再來它在tty子系統中的地位.
tty子系統中, tty core以字符設備驅動的形式負責與用戶空間交互; tty driver則負責操作底層硬件, 以一個個bit的方式通過硬件收發數據.
當用戶空間想要發送數據時, 會先把數據傳遞給tty core, tty core會把數據轉交給ldis; ldis此時可以對數據做一些封裝(一般是軟件協議上的頭的封裝), ldis封裝完數據之后, 會把數據交給tty driver通過硬件外發; tty driver不關心數據的具體內容, 它只管一個個bit的把數據發出去.
反過來, 當硬件收到數據之后, tty driver會把這些數據存儲在tty_port->buffer中, 然后在某個時候, 在把tty_port->buffer中的數據搬移到ldis中; ldis收到數據后, 會對數據做一些解封處理(一般是軟件協議上的頭的解封); 當用戶空間調用read接口獲取數據時, tty core會從ldis中取出數據, 然后交給用戶空間.
綜合來看, ldis的作用就是對數據進行一些軟件協議層面的處理, 與具體硬件無法, 主要與協議相關. 所以說一個ldis就是用來實現一種協議的, 例如PPP 或者 Bluetooth.
理解了ldis的地位, 接下來我們分析一下軟件層面上的架構.
首先, 你可以把ldis理解為一個池子, 你可以通過ldis子模塊提供的API向這個池子添加(注冊)ldis. 這個池子會存儲很多個ldis, 每個ldis對應一種協議的實現, 當我們想使用ldis的時候, 就會從這個池子里面選擇一種ldis.
那么, 接下來的一個問題是誰會負責從這個池子里面選取ldis呢? 答案是tty core, 原因是它會通過ldis發送數據, 從ldis接收數據, 因此它必須知道該用哪個ldis.
還有一個問題, tty core是在何時來選擇ldis的呢? 默認情況下, 當用戶空間調用open操作打開某一設備節點時, tty core的open函數將會被調用, 在tty core的open函數里面, 會從ldis的池子里面選擇一個ldis. 另外, 用戶空間也可以通過ioctl指定tty core使用某一個ldis.
問題繼續, tty core知道用哪個ldis了, 那么它應該要把這個ldis存儲起來, 以便后面隨時使用. 存儲在哪里呢? 你應該已經知道了, 答案是tty_struct-> ldisc, 在tty core的open函數里面, 會創建tty_struct結構體, 然后選取默認的ldis, 然后把獲取到的ldis存儲在tty_struct-> ldisc.
對於內核的這種設計, 有啥想說的?
只能說設計的very good! 一方面是體現了軟件分層的思想, 不同的子模塊負責不同的事情; 另一方面, 模塊與模塊之前低耦合, tty core與ldis並沒有必然的聯系, 它可以選擇使用任何一個ldis, 用戶空間也設置tty core所使用的ldis.
這樣的設計也符合邏輯, 例如用戶空間想發送一個字符A, 那么用戶空間可以選擇通過藍牙(其中一個ldis)發送出去, 也可以選擇通過其它方式(另外一個ldis)發送出去.
5.2 主要數據結構
tty_ldiscs[NR_LDISCS]
實現文件: drivers/tty/tty_ldisc.c
/* Line disc dispatch table */
static struct tty_ldisc_ops *tty_ldiscs[NR_LDISCS];
它就是我們前文說的那個池子, 其實就是一個全局結構體數組. 池子的大小是數組大小, 也就是NR_LDISCS.
當我們用tty ldis子模塊提供的API注冊一個ldis時, 被注冊的ldis就是存儲在這個數組里面.
NR_LDISCS是在include/uapi/linux/tty.h中定義的. uapi說明用戶空間也會引用此頭文件, 用戶空間引用此頭文件的目的是為了設置tty core使用哪一個ldis, 在設置的時候, 用戶空間指定一個ID即可, 這個ID對應數組的某個元素.
#define NR_LDISCS 30
/* line disciplines */
#define N_TTY 0
#define N_SLIP 1
#define N_MOUSE 2
#define N_PPP 3
#define N_STRIP 4
#define N_AX25 5
#define N_X25 6 /* X.25 async */
#define N_6PACK 7
#define N_MASC 8 /* Reserved for Mobitex module <kaz@cafe.net> */
#define N_R3964 9 /* Reserved for Simatic R3964 module */
#define N_PROFIBUS_FDL 10 /* Reserved for Profibus */
#define N_IRDA 11 /* Linux IrDa - http://irda.sourceforge.net/ */
#define N_SMSBLOCK 12 /* SMS block mode - for talking to GSM data */
/* cards about SMS messages */
#define N_HDLC 13 /* synchronous HDLC */
#define N_SYNC_PPP 14 /* synchronous PPP */
#define N_HCI 15 /* Bluetooth HCI UART */
#define N_GIGASET_M101 16 /* Siemens Gigaset M101 serial DECT adapter */
#define N_SLCAN 17 /* Serial / USB serial CAN Adaptors */
#define N_PPS 18 /* Pulse per Second */
#define N_V253 19 /* Codec control over voice modem */
#define N_CAIF 20 /* CAIF protocol for talking to modems */
#define N_GSM0710 21 /* GSM 0710 Mux */
#define N_TI_WL 22 /* for TI's WL BT, FM, GPS combo chips */
#define N_TRACESINK 23 /* Trace data routing for MIPI P1149.7 */
#define N_TRACEROUTER 24 /* Trace data routing for MIPI P1149.7 */
#define NR_LDISCS30, 說明這個池子最多可以存儲30個ldis
另外內核已經規定了大部分ldis對應的ID, 例如#define N_PPP 3, 說明實現PPP這個協議的ldis對應的ID是3.
tty_ldisc
如果我們想自己編寫一個ldis, 則必須實現一個tty_ldisc_ops結構體, 然后向tty ldis子模塊注冊. 注冊過程中, tty ldis子模塊會創建一個tty_ldisc結構體, 並把這個結構體存入tty_ldiscs[NR_LDISCS]這個數組的某個位置.
頭文件: include/linux/tty_ldisc.h
struct tty_ldisc {
struct tty_ldisc_ops *ops;
struct tty_struct *tty;
};
這個結構體很簡單, *tty是用來指向tty_struct結構體的, 主要是要實現tty_ldisc_ops這個結構體.
tty_ldisc_ops
頭文件: include/linux/tty_ldisc.h
struct tty_ldisc_ops {
int magic;
char *name;
int num;
int flags;
/*
* The following routines are called from above.
*/
int (*open)(struct tty_struct *);
void (*close)(struct tty_struct *);
void (*flush_buffer)(struct tty_struct *tty);
ssize_t (*chars_in_buffer)(struct tty_struct *tty);
ssize_t (*read)(struct tty_struct *tty, struct file *file,
unsigned char __user *buf, size_t nr);
ssize_t (*write)(struct tty_struct *tty, struct file *file,
const unsigned char *buf, size_t nr);
int (*ioctl)(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty, struct file *file,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios *old);
unsigned int (*poll)(struct tty_struct *, struct file *,
struct poll_table_struct *);
int (*hangup)(struct tty_struct *tty);
/*
* The following routines are called from below.
*/
void (*receive_buf)(struct tty_struct *, const unsigned char *cp,
char *fp, int count);
void (*write_wakeup)(struct tty_struct *);
void (*dcd_change)(struct tty_struct *, unsigned int);
void (*fasync)(struct tty_struct *tty, int on);
int (*receive_buf2)(struct tty_struct *, const unsigned char *cp,
char *fp, int count);
struct module *owner;
int refcount;
};
各個接口函數的意思在tty_ldisc.h里面已經給出了說明, 我們直接貼出來:
/*
* This structure defines the interface between the tty line discipline
* implementation and the tty routines. The following routines can be
* defined; unless noted otherwise, they are optional, and can be
* filled in with a null pointer.
*
* int(*open)(struct tty_struct *);
*
*This function is called when the line discipline is associated
*with the tty. The line discipline can use this as an
*opportunity to initialize any state needed by the ldisc routines.
*
* void(*close)(struct tty_struct *);
*
*This function is called when the line discipline is being
*shutdown, either because the tty is being closed or because
*the tty is being changed to use a new line discipline
*
* void(*flush_buffer)(struct tty_struct *tty);
*
*This function instructs the line discipline to clear its
*buffers of any input characters it may have queued to be
*delivered to the user mode process.
*
* ssize_t (*chars_in_buffer)(struct tty_struct *tty);
*
*This function returns the number of input characters the line
*discipline may have queued up to be delivered to the user mode
*process.
*
* ssize_t (*read)(struct tty_struct * tty, struct file * file,
* unsigned char * buf, size_t nr);
*
*This function is called when the user requests to read from
*the tty. The line discipline will return whatever characters
*it has buffered up for the user. If this function is not
*defined, the user will receive an EIO error.
*
* ssize_t (*write)(struct tty_struct * tty, struct file * file,
* const unsigned char * buf, size_t nr);
*
*This function is called when the user requests to write to the
*tty. The line discipline will deliver the characters to the
*low-level tty device for transmission, optionally performing
*some processing on the characters first. If this function is
*not defined, the user will receive an EIO error.
*
* int(*ioctl)(struct tty_struct * tty, struct file * file,
* unsigned int cmd, unsigned long arg);
*
*This function is called when the user requests an ioctl which
*is not handled by the tty layer or the low-level tty driver.
*It is intended for ioctls which affect line discpline
*operation. Note that the search order for ioctls is (1) tty
*layer, (2) tty low-level driver, (3) line discpline. So a
*low-level driver can "grab" an ioctl request before the line
*discpline has a chance to see it.
*
* long(*compat_ioctl)(struct tty_struct * tty, struct file * file,
* unsigned int cmd, unsigned long arg);
*
*Process ioctl calls from 32-bit process on 64-bit system
*
* void(*set_termios)(struct tty_struct *tty, struct ktermios * old);
*
*This function notifies the line discpline that a change has
*been made to the termios structure.
*
* int(*poll)(struct tty_struct * tty, struct file * file,
* poll_table *wait);
*
*This function is called when a user attempts to select/poll on a
*tty device. It is solely the responsibility of the line
*discipline to handle poll requests.
*
* void(*receive_buf)(struct tty_struct *, const unsigned char *cp,
* char *fp, int count);
*
*This function is called by the low-level tty driver to send
*characters received by the hardware to the line discpline for
*processing. <cp> is a pointer to the buffer of input
*character received by the device. <fp> is a pointer to a
*pointer of flag bytes which indicate whether a character was
*received with a parity error, etc. <fp> may be NULL to indicate
*all data received is TTY_NORMAL.
*
* void(*write_wakeup)(struct tty_struct *);
*
*This function is called by the low-level tty driver to signal
*that line discpline should try to send more characters to the
*low-level driver for transmission. If the line discpline does
*not have any more data to send, it can just return. If the line
*discipline does have some data to send, please arise a tasklet
*or workqueue to do the real data transfer. Do not send data in
*this hook, it may leads to a deadlock.
*
* int (*hangup)(struct tty_struct *)
*
*Called on a hangup. Tells the discipline that it should
*cease I/O to the tty driver. Can sleep. The driver should
*seek to perform this action quickly but should wait until
*any pending driver I/O is completed.
*
* void (*fasync)(struct tty_struct *, int on)
*
*Notify line discipline when signal-driven I/O is enabled or
*disabled.
*
* void (*dcd_change)(struct tty_struct *tty, unsigned int status)
*
*Tells the discipline that the DCD pin has changed its status.
*Used exclusively by the N_PPS (Pulse-Per-Second) line discipline.
*
* int(*receive_buf2)(struct tty_struct *, const unsigned char *cp,
*char *fp, int count);
*
*This function is called by the low-level tty driver to send
*characters received by the hardware to the line discpline for
*processing. <cp> is a pointer to the buffer of input
*character received by the device. <fp> is a pointer to a
*pointer of flag bytes which indicate whether a character was
*received with a parity error, etc. <fp> may be NULL to indicate
*all data received is TTY_NORMAL.
*If assigned, prefer this function for automatic flow control.
*/
5.3 主要API說明
tty_register_ldisc
頭文件: include/linux/tty.h
實現文件: drivers/tty/tty_ldisc.c
int tty_register_ldisc(int disc, struct tty_ldisc_ops *new_ldisc)
代碼邏輯很簡單, 把new_ldisc存儲到tty_ldiscs數組中, 參數disc代表new_ldisc在數組中的ID.
tty_set_ldisc
頭文件: include/linux/tty.h
實現文件: drivers/tty/tty_ldisc.c
int tty_set_ldisc(struct tty_struct *tty, int ldisc)
當用戶空間調用ioctl設置ldis時, 該函數會被調用.
函數的邏輯也很簡單, 根據參數ldisc, 從池子里面選擇對應的ldis, 然后替換tty_struct->ldisc.
tty_ldisc_N_TTY
tty_ldisc_N_TTY並不是一個API, 它是內核系統默認的ldis. 這個ldis並不會對數據做額外的處理, 它就像一個管道, 聯通tty core和tty driver.
如果用戶空間沒有顯示配置用哪一個ldis, 默認使用的就是tty_ldisc_N_TTY.
這里有兩個問題:
-
- tty_ldisc_N_TTY是誰定義的, 何時會把自己添加到池子里面?
- 代碼邏輯上是怎么把tty_ldisc_N_TTY做為默認的ldis的?
首先看第一個問題: tty_ldisc_N_TTY是誰定義的, 誰注冊的:
實現文件: drivers/tty/n_tty.c
struct tty_ldisc_ops tty_ldisc_N_TTY = {
.magic = TTY_LDISC_MAGIC,
.name = "n_tty",
.open = n_tty_open,
.close = n_tty_close,
.flush_buffer = n_tty_flush_buffer,
.chars_in_buffer = n_tty_chars_in_buffer,
.read = n_tty_read,
.write = n_tty_write,
.ioctl = n_tty_ioctl,
.set_termios = n_tty_set_termios,
.poll = n_tty_poll,
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup,
.fasync = n_tty_fasync,
.receive_buf2 = n_tty_receive_buf2,
};
注冊函數: drivers/tty/tty_ldisc.c
void tty_ldisc_begin(void)
{
/* Setup the default TTY line discipline. */
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
}
drivers/tty/tty_io.c中的void __init console_init(void)函數會調用這里的tty_ldisc_begin, 然后tty_ldisc_begin調用tty_register_ldisc向池子里面添加一個ldis
再來看第二個問題: 代碼邏輯上是怎么把tty_ldisc_N_TTY做為默認的ldis的:
我們知道, 當用戶空間對一個tty節點執行open操作時, tty core里面對應的open函數會被調用.
tty core的open函數會創建一個tty_struct結構體, 並且會在此時選擇默認的ldis, 並把這個ldis賦值給tty_struct->ldisc.
具體的代碼流程是:
drivers/tty/tty_io.c : tty_open -> tty_init_dev -> alloc_tty_struct -> tty_ldisc_init.
tty_ldisc_init是在drivers/tty/tty_ldisc.c中定義的, 代碼如下:
void tty_ldisc_init(struct tty_struct *tty)
{
struct tty_ldisc *ld = tty_ldisc_get(tty, N_TTY);
if (IS_ERR(ld))
panic("n_tty: init_tty");
}
tty_ldisc_get(tty, N_TTY): 根據參數N_TTY(其實就是一個ID, 對應數組中的某個元素), 獲取到對應的ldis, 然后賦值給tty->ldisc.
6. serial core
6.1 簡介
serial core主要是針對串口驅動. 絕大多數ARM的CPU, 都有串口控制器, 在CPU的芯片手冊里面, 一般叫UART或者USART.
為什么會有serial core的存在呢? 主要目的是為了讓編寫串口驅動變得更加容易.
當你需要編寫一個串口驅動時, 你只需要向serial core子系統注冊即可, serial core會幫你向tty driver子系統進行注冊. 當然你也可以直接向tty driver子系統注冊一個串口驅動, 這樣就相當於繞過了serial core, 一般不推薦這樣做.
serial core里面也涉及到幾個自己的數據結構, 為了理清這些數據結構的意義, 我們先來看看串口在硬件上的特點:
一般一個ARM的CPU上會有多個UART/USART控制器, 它們在芯片手冊上一般叫做USART1, USART2, USART3…
在Serial core里面, 用一個struct uart_driver結構體代表一個CPU的所有USART控制器; 用一個struct uart_state結構體代表CPU的某一個具體的控制器(例如USART1).
先來一張數據結構的關系圖, 然后我們再來理清這些數據結構的對應關系:
好多數據結構…, 別着急, 我們來慢慢分析:
一個CPU(假設有N個USART控制器)對應一個uart_driver, 也對應一個tty_driver.
其中uart_driver->nr = tty_driver->num = N.
一個USART控制器對應一個uart_state, 也對應一個tty_port, 也對應一個uart_port.
tty_driver->major + tty_driver->minor_start定義了起始設備號, 一個控制器也對應一個設備號, 同時也對應一個/dev/下的節點.
理解了上面描述的對應關系, 再來看下面關於數據結構的介紹就會輕松很多了.
6.2 主要數據結構
uart_driver
頭文件: include/linux/serial_core.h
struct uart_driver |
Comment |
struct module*owner |
|
const char*driver_name |
最終會賦值給tty_driver->driver_name, 此字段的作用如果忘記了, 請回頭看看tty_driver數據結構的介紹 |
const char*dev_name |
最終會賦值給tty_driver->name, 此字段的作用如果忘記了, 請回頭看看tty_driver數據結構的介紹 |
int major |
最終會賦值給tty_driver->major |
int minor |
最終會賦值給tty_driver->minor_start |
int nr |
最終會賦值給tty_driver->num |
struct console*cons |
暫時不清楚具體作用 |
/* * these are private; the low level driver should not * touch these; they should be initialised to NULL */ struct uart_state*state |
注意代碼注釋, 當我們打算實現一個uart_driver結構體時, 這個字段應該為NULL. serial core會負責幫我們分配/釋放uart_state空間. 注意, 一個uart_driver下可以”掛接”多個uart_state |
struct tty_driver*tty_driver |
同樣, 這個字段應該設置為NULL serial core會負責調用alloc_tty_driver來創建tty_driver結構體. |
uart_state
頭文件: include/linux/serial_core.h
uart_state, 前文說過, 它對應CPU上某個具體的控制器, 例如USART1.
struct uart_state |
Comment |
struct tty_portport |
一個uart_state對應一個tty_port, 也就是說每個USART控制器都會對應一個tty_port. tty_port的作用是當硬件收到數據時, 把數據存儲在tty_port里面, 然后再轉移到ldis里面. 每個USART控制器, 從硬件的角度來說, 都可以收到自己的數據, 因此每個控制器都需要一個tty_port, 否則多個硬件往一個tty_port里面存儲數據就會出現數據混亂 |
struct uart_port*uart_port |
一個uart_state也對應一個uart_port. 是不是在想uart_port和tty_port有什么關系? uart_port是不是類似tty_port, 負責把硬件的收到的數據送給ldis? 答案是否定的, 在uart_state中, 已經有tty_port這個結構體來負責把硬件收到的數據轉送給ldis, 沒有必要再設計一個uart_port來做同樣的事情. uart_port的主要作用是跟硬件控制器打交道, CPU上會有多個USART, 每個USART多少有點不一樣, 至少每個USART的寄存器地址就不一樣, 中斷號不一樣. 而且不同的USART也可能有不同的波特率等等, 因此每一個硬件控制器, 都需要一個uart_port的結構體來描述它. 因為uart_port是負責跟具體的硬件控制器打交道, 因此往硬件控制器發送數據和從硬件控制器獲取數據, 都需要經過uart_port. uart_port接收到數據之后, 會通過uart_port->uart_state->tty_port(看見了嗎, uart_port並不能直接獲取到tty_port, 從這里也可以看出, 它與tty_port屬於同等級別, 都隸屬於uart_state)找到tty_port, 然后把數據存儲到tty_port中. 后文會詳細介紹uart_port結構體 |
struct circ_bufxmit |
一個uart_state也對應一個xmit. xmit就是一個環形緩沖區, 大小一般是(#define UART_XMIT_SIZE PAGE_SIZE) xmit的作用是當用戶空間調用write操作往串口發送數據時, tty子系統經過層層調用, 會把數據存儲在xmit里面, 然后通知硬件開始發送數據. 使用緩沖區的原因是串口采用的是串行傳輸, 速度比較慢, 用緩沖區的話用戶空間就不會被阻塞. |
enum uart_pm_statepm_state |
休眠相關, 暫不分析 |
uart_port
一個具體的USART硬件控制器對應一個uart_port.
uart_port是用來描述硬件的, 主要作用是負責與硬件控制器打交道, 控制硬件發送數據, 從硬件接收數據等.
既然uart_port是用來描述硬件的, 那么它應該描述硬件的哪些信息呢?
首先, 得有硬件的寄存器地址, 中斷號等.
其次, 還得有操作硬件的接口函數, 例如操作硬件發送數據, 從硬件接收數據等等, 這一部分功能其實是用uart_ops描述的.
下面我們來看看uart_port的數據結構, 挺長的, 我們只截取幾個重點:
頭文件: include/linux/serial_core.h
Comment |
|
spinlock_tlock |
/* port lock */ |
unsigned longiobase unsigned char __iomem*membase |
硬件寄存器地址, 我們可以用I/O Port標准提供的in/out, read/write方式來訪問硬件 |
resource_size_tmapbase resource_size_tmapsize |
除了上述方式, 我們也可以通過I/O Memory的方式來訪問硬件寄存器. 事實上, I/O Port方式在X86架構中比較流行, 在ARM中, 更多的時候用的是I/O Memory的方式 |
unsigned intirq;/* irq number */ unsigned longirqflags;/* irq flags */ (*handle_irq)(struct uart_port *) |
中斷相關 中斷號, 中斷標志 中斷處理函數 |
(*handle_break)(struct uart_port *) |
處理break信號 |
struct serial_rs485 rs485 (*rs485_config)(struct uart_port *, struct serial_rs485 *rs485) |
485相關 485的標志位, (是否使能485等等) 485的配置函數 |
const struct uart_ops*ops |
指向uart_ops |
…….. |
還有很多, 就不一一細說了 |
(*serial_in)(struct uart_port *, int) (*serial_out)(struct uart_port *, int, int) (*set_termios)(struct uart_port *, …..) (*set_mctrl)(struct uart_port *, unsigned int) (*startup)(struct uart_port *port) (*shutdown)(struct uart_port *port) (*throttle)(struct uart_port *port) (*unthrottle)(struct uart_port *port) (*pm)(struct uart_port *, unsigned int state, ..) |
這些接口函數的功能與uart_ops里面重復了, 基本上都是使用uart_ops里面的函數, 這里都沒有用到 |
uart_ops
uart_ops算是uart_port的子結構, 它是用於描述如何操作一個USART硬件控制器來收發數據的.
頭文件: include/linux/serial_core.h
/*
* This structure describes all the operations that can be done on the
* physical hardware. See Documentation/serial/driver for details.
*/
struct uart_ops {
unsigned int (*tx_empty)(struct uart_port *);
void (*set_mctrl)(struct uart_port *, unsigned int mctrl);
unsigned int (*get_mctrl)(struct uart_port *);
void (*stop_tx)(struct uart_port *);
void (*start_tx)(struct uart_port *);
void (*throttle)(struct uart_port *);
void (*unthrottle)(struct uart_port *);
void (*send_xchar)(struct uart_port *, char ch);
void (*stop_rx)(struct uart_port *);
void (*enable_ms)(struct uart_port *);
void (*break_ctl)(struct uart_port *, int ctl);
int (*startup)(struct uart_port *);
void (*shutdown)(struct uart_port *);
void (*flush_buffer)(struct uart_port *);
void (*set_termios)(struct uart_port *, struct ktermios *new,
struct ktermios *old);
void (*set_ldisc)(struct uart_port *, struct ktermios *);
void (*pm)(struct uart_port *, unsigned int state,
unsigned int oldstate);
/*
* Return a string describing the type of the port
*/
const char *(*type)(struct uart_port *);
/*
* Release IO and memory resources used by the port.
* This includes iounmap if necessary.
*/
void (*release_port)(struct uart_port *);
/*
* Request IO and memory resources used by the port.
* This includes iomapping the port if necessary.
*/
int (*request_port)(struct uart_port *);
void (*config_port)(struct uart_port *, int);
int (*verify_port)(struct uart_port *, struct serial_struct *);
int (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct uart_port *);
void (*poll_put_char)(struct uart_port *, unsigned char);
int (*poll_get_char)(struct uart_port *);
#endif
};
ops的調用流程梳理
從第二章開始到現在, 我們已經介紹過很多種ops, 下面我們梳理一下它們的調用流程.
從用戶空間到硬件:
用戶空間 -> tty core(file_operations tty_fops) -> tty line discipline(tty_ldisc_ops) -> tty driver(tty_operations) -> uart port(uart_ops)
從硬件到用戶空間:
上述流程反過來即可
6.3 主要API說明
uart_register_driver
如果我們想編寫一個串口驅動, 需要准備好uart_driver結構體, 然后調用該API向serial core進行注冊.
該API會進一步向tty driver子系統注冊, 下面我們分析一下該API的實現細節
頭文件: include/linux/serial_core.h
實現文件: drivers/tty/serial/serial_core.c
int uart_register_driver(struct uart_driver *drv)
drv->state = kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL)
分配drv->nr個uart_state空間
normal = alloc_tty_driver(drv->nr)
申請一個tty_driver結構體normal, 隨后會初始化normal, 我們看看幾個重要的地方:
normal->flags= TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV
flags設置了TTY_DRIVER_DYNAMIC_DEV標志, 設置這個標准意味着什么? 不記得了可以回頭看看第3章. 它意味着當調用tty_register_driver向tty driver子系統進行注冊時, 並不會創建字符設備驅動, 也不會生成設備節點, 也不會在/sys/class/tty下創建子目錄.
為什么? 回想一下uart_driver這個結構體作用: 它並不對應某個具體的USART硬件控制器, 它好像一個容器, 里面可以存放多個硬件控制器(也就是uart_port). 而設備節點一般就代表某一個硬件控制器, 例如/dev/ttyS0對應的是USART0這個控制器, /dev/ttyS1對應的USART1這個控制器, 因此設備節點不應該在此時創建, 而應該在uart_port注冊的時候創建.
tty_set_operations(normal, &uart_ops)
設置normal的tty_operations, 也就是uart_ops. uart_ops是在serial_core.c中實現的. tty line discipline就是與這個uart_ops打交道的.
for (i = 0; i < drv->nr; i++) {
….
tty_port_init(port);
port->ops = &uart_port_ops;
}
初始化tty_port, 並設置其ops為uart_port_ops, uart_port_ops也是在serial_core.c中實現的. 當硬件收到數據之后, 就是通過這個uart_port_ops把數據轉存到tty line discipline里面的
retval = tty_register_driver(normal)
向tty driver子系統注冊
uart_add_one_port
針對某一個具體的USART硬件控制器, 當我們准備好uart_port和uart_port->uart_ops結構體之后, 就可以調用該API向serial core子系統添加一個port了.
頭文件: include/linux/serial_core.h
實現文件: drivers/tty/serial/serial_core.c
int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
當對uart_port做完一系列的初始化之后, 最終會調用tty_port_register_device_attr, 這是tty driver子系統的一個API, 該API最終會導致注冊字符設備驅動, 生成設備節點, 創建/sys/class/tty下的子目錄
7. 驅動設計指導規范
在大多情況下, 我們的驅動開發工作主要都是開發串口驅動, 即向serial core子系統進行注冊. 因為本節主要介紹編寫串口驅動的主要步驟.
在3.1節我們介紹過如何編寫一個tty driver, 其實編寫串口驅動和編寫tty driver很相似.
只不過當你編寫tty driver時, 是直接向tty driver子系統進行注冊. 而編寫串口驅動時, 是向serial core子系統注冊, 然后serial core再向tty driver子系統進行注冊.
7.1 代碼文件結構
編寫串口驅動, 你一定需要引用serial_core.h這個頭文件.
文件路徑: include/linux/serial_core.h, 這個頭文件定義了你需要實現的數據結構, 定義了serial core子系統提供給你的API.
同時, 你可能需要看看drivers/tty/serial/serial_core.c這個C文件, 以輔助理解API的意義.
7.2 串口驅動編寫流程
- 注冊uart_driver
定義一個struct uart_driver結構體, 給結構體的相應變量賦值, 然后調用uart_register_driver進行注冊即可.
思考一個問題? 在什么時機調用uart_register_driver, 是不是弄個platform_driver, 然后在driver的probe函數里面調用uart_register_driver? 答案是否定的.
- 添加一個uart_port
定義一個struct uart_port和struct uart_ops結構體, 然后調用uart_add_one_port進行注冊.
同樣, 思考一個問題, 何時調用uart_add_one_port, 會不會有platform_driver?
答案是肯定的. 因為uart_port是用於描述一個具體的USART控制器的. USART控制器屬於ARM CPU的片上外設, 所有的片上外設都會掛接在platform bus下面, 有一個paltform device描述此外設的硬件資源, 還有一個platform driver用於描述如何操作這個外設.
所以, 對於每一個USART控制器, 都會有一個與之對於的platfrom device, 這些device共用同一個platfrom driver, 在driver的probe函數里面, 調用uart_add_one_port向serial core子系統添加一個USART控制器.
不過uart_register_driver就不是在probe函數里面被調用的, 因為它並不對應某一個具體的片上外設, 不會有與之對於的platform device. 一般我們會定義一個module_init函數, 在module_init函數里面調用uart_register_driver