Linux 終端初始化 console_init 及 tty 驅動框架
先前分析了 Linux 入口地址和 Linux 系統啟動流程,本文詳細分析一下 Linux 啟動流程中的 console_init 終端初始化函數。
上兩篇文章如下:
Linux 內核入口分析
手把手教你分析 Linux 啟動流程
講解終端初始化之前我們先講解一個概念:tty
在Linux系統中,終端是一類字符型設備,它包括多種類型,通常使用tty來簡稱各種類型的終端設備。我們一般分為三類:
串口終端(/dev/ttyS*)
串口終端是使用計算機串口連接的終端設備。Linux 把每個串行端口都看作是一個字符設備。這些串行端口所對應的設備名稱是 /dev/ttySAC0;/dev/ttySAC1……
控制臺終端(/dev/console)
在Linux系統中,計算機的輸出設備通常被稱為控制臺終端(Console),這里特指printk信息輸出到的設備。/dev/console是一個虛擬的設備,它需要映射到真正的tty(物理終端)上,比如通過內核啟動參數” console=ttySAC0”就把console映射到了串口0。
虛擬終端(/dev/tty*)
當用戶登錄時,使用的是虛擬終端。使用Ctcl+Alt+[F1—F6]組合鍵時,我們就可以切換到tty1、tty2、tty3等上面去。tty1–tty6等稱為虛擬終端,而tty0則是當前所使用虛擬終端的一個別名。
console_init 分析
Linux 啟動函數 start_kernel 會調用 console_init 函數。
linux4.14/kernel/printk/printk.c
linux4.14/drivers/tty/n_tty.c
我們可以看到,console_init 主要做了兩件事情:
1、n_tty_init 主要調用 tty_register_ldisc(N_TTY, &n_tty_ops) 注冊 tty 線路規程。
- call = __con_initcall_start;
- while (call < __con_initcall_end) {
- (*call)();
- call++;
- }
這里主要是調用 __con_initcall_start 到 __con_initcall_end 之間的函數。
__con_initcall_start 和 __con_initcall_end 定義在:
linux4.14/include/asm-generic/vmlinux.lds.h
中間包含了 .con_initcall.init 段:
linux4.14/include/linux/init.h
我們通過 console_init 聲明的驅動模塊,就會出現在這個段中,被調用。普通我們聲明的驅動模塊都是使用 module_init,如果我們寫的是串口驅動,可以使用console_init 聲明。
如果要看具體中間有什么函數,可以查看編譯 Linux 內核的輸出 System.map 文件,這個文件記載了從頭到尾 Linux 干了什么,具體的地址存儲了什么東西。
System.map 文件默認在編譯后的 Linux 內核根目錄下, 當然我們也可以修改到其他目錄。
這里會有三列:地址,區,函數名字。
如果后面我們使用 console_init(serial_5685_xxxx)去聲明我們的驅動,那么這個 serial_5685_xxxx 就會出現在 __con_initcall_start 和 __con_initcall_end 之間,就會被調用。
initcall機制
注意上述流程,我們來理解一下 initcall 機制:
普通我們寫一個程序,想要它被調用,需要在主流程中調用這個函數,才算被調用。
那么這種方式如果放在 Linux 中,是難以想象的,我們自己寫的代碼要在多少個地方聲明。
而你如果采用initcall機制,意思就是說,你使用一個字符串聲明你的驅動初始化函數,那么所有的驅動初始化函數都存在內存中一個連續的段中,系統啟動以后,會從這個段的第一個函數開始,一個一個遍歷,進而一個一個調用,這就是 initcall 機制。這就是為什么我們寫驅動只需要使用 module_init 聲明,編譯進去即可自動被調用的原因!!!
System.map
編譯后的內核根目錄 System.map 文件記載了所有的驅動加載順序,如果你不確定驅動的加載順序,在這里查看就可以,每次編譯 Linux 內核就會產生一個新的 System.map。
tty 驅動
我們不要把 tty 驅動和 串口驅動 弄混了,tty 驅動架構如下:
其中 tty driver 等價于我們普通寫的驅動,可以自己寫。
也就是說,在 tty 驅動框架主要有三層:tty core、tty line discipline、tty driver,另外最上層是用戶空間,最下層是硬件。
tty core 稱之為 tty 核心,主要作用是向用戶提供統一的接口。
tty line discipline 稱之為 tty 線路規程,主要從上下兩層接收數據,并按照一定協議進行轉換,比如 ppp 或者藍牙協議,這樣你的 tty 終端就不止可以用普通的串口,還可以通過其他協議訪問到我們的系統。比如手機鏈接 PCB 板子的 WiFi 接入系統控制終端,輸入 ls、cd 等命令。這一層并不是必須的,你可以直接使用驅動和 tty core 進行通信,但一般這一層都會有。
tty driver 就是我們常說的串口驅動。
在 console_init 函數中,它做的兩件事,就是注冊 tty 線路規程,注冊 tty 驅動,tty 核心是包含在內核當中的。tty 線路規程和 tty 驅動可以有很多個。
有的人會有疑問,為什么有了 tty 驅動了,還會有一個 tty 線路規程。得益于 Linux 模塊化的思想,這里主要是為了分層與隔離。tty 驅動只和硬件相關,只解析基本的硬件信息,把硬件信息轉換成字符。所有的對字符的進一步處理包括加入藍牙協議傳輸,監控數據等都放在 tty 線路規程當中。這樣 tty 驅動是可以完美復用和移植的。
分享一張彭大佬的圖,本文我只講了概念,彭大佬講解過 tty 源碼:
這里只需要注意一點,在右下角,tty driver 是沒有 read 函數的,tty driver 層有 buffer,輸入的數據會存儲在 buffer 中,被讀取。
原因很簡單,對于 tty 來說,輸入設備和輸出設備不是同一個設備,輸入設備是鍵盤,輸出設備是屏幕,這和普通的 IIC、SPI 驅動同一個設備不一樣。因此在設計上 tty driver 沒有 read 函數。
本文轉載自微信公眾號「嵌入式Linux系統開發」

































