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