LLINUX下的tty,console與串口(整理自www.driverdevelop.com)
zlei 發(fā)表于 2005-3-23 16:36:17 LINUX|編程 ←返回版面
LLINUX下的tty,console與串口
hexf2002 :
公司作一個(gè)嵌入式產(chǎn)品,用ARM內核,LINUX操作系統(不是uclinux)。我最近的工作是把一個(gè)原來(lái)作好的模塊(用串口來(lái)通信)掛到系統上,通過(guò)串口來(lái)控制該模塊的一系列工作,并要求
作成單獨的驅動(dòng)程序(不是通過(guò)應用程序來(lái)控制)。同時(shí)也想借此熟悉LINUX下設備驅動(dòng)程序的開(kāi)發(fā)方法。我們買(mǎi)的別的公司的開(kāi)發(fā)板,LINUX現在已經(jīng)能跑起來(lái),但技術(shù)支持和文檔基本沒(méi)有。最近剛開(kāi)始學(xué)習LINUX,算是有了一些了解,但對TTY設備、CONSOLE、串口之間的關(guān)系覺(jué)得比較混亂。這里有幾個(gè)問(wèn)題請教:
1、LINUX下TTY、CONSOLE、串口之間是怎樣的層次關(guān)系?具體的函數接口是怎樣的?串口是如何被調用的?
2、printk函數是把信息發(fā)送到控制臺上吧?如何讓PRINTK把信息通過(guò)串口送出?或者說(shuō)系統在什么地方來(lái)決定是將信息送到顯示器還是串口?
3、start_kernel中一開(kāi)始就用到了printk函數(好象是printk(linux_banner什么的),在 這個(gè)時(shí)候整個(gè)內核還沒(méi)跑起來(lái)呢。那這時(shí)候的printk是如何被調用的?在我們的系統中,系統啟動(dòng)是用的現代公司的BOOTLOADER程序,后來(lái)好象跳到了LINUX下的head-armv.s, 然后跳到start_kernel,在bootloader 里串口已經(jīng)是可用的了,那么在進(jìn)入內核后是不是要重新設置?
以上問(wèn)題可能問(wèn)的比較亂,因為我自己腦子里也比較亂,主要還是對tty,console,serial之間的關(guān)系,特別是串口是如何被調用的沒(méi)搞清。這方面的資料又比較少(就情景分析中講了一點(diǎn)),希望
高手能指點(diǎn)一二,非常感謝!
/////////////////////////////////////////////////////////////////////
zooka :
我最近也在搞這方面的東西,也是寫(xiě)一個(gè)串口設備的驅動(dòng)
搞了將近一個(gè)月了,其中上網(wǎng)找資料,看源代碼,什么都做了
但還是一蹋糊涂的,有些問(wèn)題還是不明白,希望一起討論討論
在/proc/device(沒(méi)記錯應該是這個(gè)文件)
里面有一個(gè)叫serial的驅動(dòng),其主設備號是4,次設備號是64-12X(沒(méi)記錯應該是這個(gè)范圍)
大家都知道,串口的次設備號是從64開(kāi)始的,串口1 /dev/ttyS0就對應次設備號64,串口2就對應65
問(wèn)題是現在我機上只有兩個(gè)串口,它注冊這么多次設備號來(lái)干什么?
對于一個(gè)接在串口1的設備,在我注冊驅動(dòng)的時(shí)候
我是需要自己找一個(gè)主設備號呢?
還是就用主設備號4,次設備號從上面12X的后面選?
還是就用主設備號4,次設備號64?
在linux的內核中有一個(gè)tty層,我看好像有些串口驅動(dòng)是從這里開(kāi)始的
例如調用tty_register_driver()來(lái)注冊驅動(dòng)
就像在pci子系統里調用pci_register_driver()那樣的
那么,用這種機制來(lái)注冊的驅動(dòng),
它是直接對串口的端口操作呢(例如用inb(),outb()....之類(lèi)的)
還是某些更底層的驅動(dòng)接口呢?
這些問(wèn)題纏了我很久都沒(méi)解決,搞得最后不得不放棄
現在轉向用戶(hù)空間的應用程序,看能不能有些更高效的方法來(lái)實(shí)現
(在用戶(hù)空間只能用open("/dev/ttyS0", O_RDWR)來(lái)實(shí)現了)
另外還有,系統里已經(jīng)為我們實(shí)現了串口的驅動(dòng)
所以我們在用戶(hù)空間的程序里直接open("/dev/ttyS0")就可用了
但是現在要寫(xiě)的是接在串口上的設備的驅動(dòng)
在內核模塊中可不可以包含某個(gè)頭文件,然后就可以直接用串口驅動(dòng)中的接口呢?
/////////////////////////////////////////////////////////////////////
kernel_1998 :
看到你們的問(wèn)題后,感覺(jué)很有典型性,因此花了點(diǎn)工夫看了一下,做了一些心得貼在這里,歡迎討論并指正:
1、LINUX下TTY、CONSOLE、串口之間是怎樣的層次關(guān)系?具體的函數接口是怎樣的?串口是如何被調用的?
tty和console這些概念主要是一些虛設備的概念,而串口更多的是指一個(gè)真正的設備驅動(dòng)。
Tty實(shí)際是一類(lèi)終端I/O設備的抽象,它實(shí)際上更多的是一個(gè)管理的概念,它和tty_ldisc(行規程)和tty_driver(真實(shí)設備驅動(dòng))組合在一起,目的是向上層的VFS提供一個(gè)統一的接口。通過(guò)file_operations結構中的tty_ioctl可以對其進(jìn)行配置。查tty_driver,你將得到n個(gè)結果,實(shí)際都是相關(guān)芯片的驅動(dòng)。因此,可以得到的結論是(實(shí)際情況比這復雜得多):每個(gè)描述tty設備的tty_struct在初始化時(shí)必然掛如了某個(gè)具體芯片的字符設備驅動(dòng)(不一定是字符設備驅動(dòng)),可以是很多,包括顯卡或串口chip。不知道你的ARM Soc是那一款,不過(guò)看情況你們應該用的是常見(jiàn)的chip,這些驅動(dòng)實(shí)際上都有。
而console是一個(gè)緩沖的概念,它的目的有一點(diǎn)類(lèi)似于tty。實(shí)際上console不僅和tty連在一起,還和framebuffer連在一起,具體的原因看下面的鍵盤(pán)的中斷處理過(guò)程。Tty的一個(gè)子集需要使用console(典型的如主設備號4,次設備號1—64),但是要注意的是沒(méi)有console的tty是存在的。
而串口則指的是tty_driver。
舉個(gè)典型的例子:
分析一下鍵盤(pán)的中斷處理過(guò)程:
keyboard_interrupt—>handle_kbd_event—>handle_keyboard_event—>handle_scancode
void handle_scancode(unsigned char scancode, int down)
{
……..
tty = ttytab? ttytab[fg_console]: NULL;
if (tty && (!tty->driver_data)) {
……………
tty = NULL;
}
………….
schedule_console_callback();
}
這段代碼中的兩個(gè)地方很值得注意,也就是除了獲得tty外(通過(guò)全局量tty記錄),還進(jìn)行了console 回顯schedule_console_callback。Tty和console的關(guān)系在此已經(jīng)很明了?。?!
2、printk函數是把信息發(fā)送到控制臺上吧?如何讓PRINTK把信息通過(guò)串口送出?或者說(shuō)系統在什么地方來(lái)決定是將信息送到顯示器還是串口?
具體看一下printk函數的實(shí)現就知道了,printk不一定是將信息往控制臺上輸出,設置kernel的啟動(dòng)參數可能可以打到將信息送到顯示器的效果。
函數前有一段英文,很有意思:
/*This is printk. It can be called from any context. We want it to work.
*
* We try to grab the console_sem. If we succeed, it‘s easy - we log the output and
* call the console drivers. If we fail to get the semaphore we place the output
* into the log buffer and return. The current holder of the console_sem will
* notice the new output in release_console_sem() and will send it to the
* consoles before releasing the semaphore.
*
* One effect of this deferred printing is that code which calls printk() and
* then changes console_loglevel may break. This is because console_loglevel
* is inspected when the actual printing occurs.
*/
這段英文的要點(diǎn):要想對console進(jìn)行操作,必須先要獲得console_sem信號量。如果獲得console_sem信號量,則可以“log the output and call the console drivers”,反之,則“place the output into the log buffer and return”,實(shí)際上,在代碼:
asmlinkage int printk(const char *fmt, ...)
{
va_list args;
unsigned long flags;
int printed_len;
char *p;
static char printk_buf[1024];
static int log_level_unknown = 1;
if (oops_in_progress) { /*如果為1情況下,必然是系統發(fā)生crush*/
/* If a crash is occurring, make sure we can‘t deadlock */
spin_lock_init(&logbuf_lock);
/* And make sure that we print immediately */
init_MUTEX(&console_sem);
}
/* This stops the holder of console_sem just where we want him */
spin_lock_irqsave(&logbuf_lock, flags);
/* Emit the output into the temporary buffer */
va_start(args, fmt);
printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args);/*對傳入的buffer進(jìn)行處理,注意還不是
真正的對終端寫(xiě),只是對傳入的string進(jìn)行格式解析*/
va_end(args);
/*Copy the output into log_buf. If the caller didn‘t provide appropriate log level tags, we insert them here*/
/*注釋很清楚*/
for (p = printk_buf; *p; p++) {
if (log_level_unknown) {
if (p[0] != ‘<‘ || p[1] < ‘0‘ || p[1] > ‘7‘ || p[2] != ‘>‘) {
emit_log_char(‘<‘);
emit_log_char(default_message_loglevel + ‘0‘);
emit_log_char(‘>‘);
}
log_level_unknown = 0;
}
emit_log_char(*p);
if (*p == ‘n‘)
log_level_unknown = 1;
}
if (!arch_consoles_callable()) {
/*On some architectures, the consoles are not usable on secondary CPUs early in the boot process.*/
spin_unlock_irqrestore(&logbuf_lock, flags);
goto out;
}
if (!down_trylock(&console_sem)) {
/*We own the drivers. We can drop the spinlock and let release_console_sem() print the text*/
spin_unlock_irqrestore(&logbuf_lock, flags);
console_may_schedule = 0;
release_console_sem();
} else {
/*Someone else owns the drivers. We drop the spinlock, which allows the semaphore holder to
proceed and to call the console drivers with the output which we just produced.*/
spin_unlock_irqrestore(&logbuf_lock, flags);
}
out:
return printed_len;
}
實(shí)際上printk是將format后的string放到了一個(gè)buffer中,在適當的時(shí)候再加以show,這也回答了在start_kernel中一開(kāi)始就用到了printk函數的原因
3、start_kernel中一開(kāi)始就用到了printk函數(好象是printk(linux_banner什么的),在這個(gè)時(shí)候整個(gè)內核還沒(méi)跑起來(lái)呢。那這時(shí)候的printk是如何被調用的?在我們的系統中,系統啟動(dòng)是用的現代公司的BOOTLOADER程序,后來(lái)好象跳到了LINUX下的head-armv.s, 然后跳到start_kernel,在bootloader 里串口已經(jīng)是可用的了,那么在進(jìn)入內核后是不是要重新設置?
Bootloader一般會(huì )做一些基本的初始化,將kernel拷貝物理空間,然后再跳到kernel去執行??梢钥隙ǖ氖莐ernel肯定要對串口進(jìn)行重新設置,原因是Bootloader有很多種,有些不一定對串口進(jìn)行設置,內核不能依賴(lài)于bootloader而存在。
/////////////////////////////////////////////////////////////////////
hexf2002:
多謝樓上大俠,分析的很精辟。我正在看printk函數。
我們用的CPU是hynix的hms7202。在評估板上是用串口0作
控制臺,所有啟動(dòng)過(guò)程中的信息都是通過(guò)該串口送出的。
在bootloader中定義了函數ser_printf通過(guò)串口進(jìn)行交互。
但我還是沒(méi)想明白在跳轉到linux內核而console和串口尚未
初始化時(shí)printk是如何能夠工作的?我看了start_kernel
的過(guò)程(并通過(guò)超級終端作了一些跟蹤),console的初始化
是在console_init函數里,而串口的初始化實(shí)際上是在1號
進(jìn)程里(init->do_basic_setup->do_initcalls->rs_init),
那么在串口沒(méi)有初始化以前prink是如何工作的?特別的,在
start_kernel一開(kāi)始就有printk(linux_banner),而這時(shí)候
串口和console都尚未初始化呢。
/////////////////////////////////////////////////////////////////////
kernel_1998 :
1.在start_kernel一開(kāi)始就有printk(linux_banner),而這時(shí)候串口和console都尚未初始化?
仔細分析printk可以對該問(wèn)題進(jìn)行解答。代碼中的:
/* Emit the output into the temporary buffer */
va_start(args, fmt);
printed_len = vsnprintf(printk_buf, sizeof(printk_buf), fmt, args);
va_end(args);
將輸入放到了printk_buf中,接下來(lái)的
for (p = printk_buf; *p; p++) {
if (log_level_unknown) {
if (p[0] != ‘<‘ || p[1] < ‘0‘ || p[1] > ‘7‘ || p[2] != ‘>‘) {
emit_log_char(‘<‘);
emit_log_char(default_message_loglevel + ‘0‘);
emit_log_char(‘>‘);
}
log_level_unknown = 0;
}
emit_log_char(*p);
if (*p == ‘n‘)
log_level_unknown = 1;
}
則將printk_buf中的內容進(jìn)行解析并放到全局的log_buf(在emit_log_char函數)中。
而下面的
if (!down_trylock(&console_sem)) {
/*
* We own the drivers. We can drop the spinlock and let
* release_console_sem() print the text
*/
spin_unlock_irqrestore(&logbuf_lock, flags);
console_may_schedule = 0;
release_console_sem();
} else {
/*
* Someone else owns the drivers. We drop the spinlock, which
* allows the semaphore holder to proceed and to call the
* console drivers with the output which we just produced.
*/
spin_unlock_irqrestore(&logbuf_lock, flags);
}
則是根據down_trylock(&console_sem)的結果調用release_console_sem(),在release_console_sem()中才真正的對全局的log_buf中的內容相應的console設備驅動(dòng)進(jìn)行處理。
至此,可以得到如下的一些結論:
(1)printk的主操作實(shí)際上還是針對一個(gè)buffer(log_buf),該buffer中的內容是否顯示(或者說(shuō)向終端輸出),則要看是否可以獲得console_sem。
(2)printk所在的文件為printk.c,是和體系結構無(wú)關(guān)的,因此對任何平臺都一樣。
可以推測的結論是:
(1)kernel在初始化時(shí)將console_sem標為了locked,因此在start_kernel一開(kāi)始的printk(linux_banner)中實(shí)際只將輸入寫(xiě)入了緩沖,等在串口和console初始化后,對printk的調用才一次將緩沖中的內容向串口和console輸出。
(2)在串口和console的初始化過(guò)程中,必然有對console_sem的up操作。
(3)因此,在embedded的調試中,如果在console的初始化之前系統出了問(wèn)題,不會(huì )有任何的輸出。唯一可以使用的只能是led或jtag了。
(4)因此,你的問(wèn)題可以看出解答。
2.console的初始化.
不知道你用的是那一個(gè)內核版本,在我看的2.4.18和2.4.19中,都是在start_kernel中就對console進(jìn)行的初始化。從前面的分析來(lái)看,console的初始化不應該太晚,否則log_buf有可能溢出。
/////////////////////////////////////////////////////////////////////
hexf2002:
多謝樓上,分析的很精彩!
我們用的內核版本是2.4.18,console的初始化確實(shí)是在
start_kernel->console->init。
關(guān)于tty和串口,我這里還想再問(wèn)一下。tty設備的操作的總入口
是
static struct file_operations tty_fops = {
llseek: no_llseek,
read: tty_read,
write: tty_write,
poll: tty_poll,
ioctl: tty_ioctl,
open: tty_open,
release: tty_release,
fasync: tty_fasync,
};
而對串口的操作定義在:
static struct tty_driver serial_driver 這個(gè)結構中。
serial.c中的多數函數都是填充serial_driver中的函數指針。
那么在對串口操作時(shí),應該是先調用tty_fops中的操作(比如
tty_open等),然后再分流到具體的串口操作(rs_open等)吧?
但tty_driver(對串口就是serial_driver)中有很多函數指針
并不跟file_operations中的函數指針對應,不知道這些對應
不上的操作是如何被執行的?比如put_char,flush_char,read_proc,
write_proc,start,stop等。
/////////////////////////////////////////////////////////////////////
kernel_1998 :
以下是我對這個(gè)問(wèn)題的一些理解:
這實(shí)際上還是回到原先的老問(wèn)題,即tty和tty_driver之間的關(guān)系。從實(shí)現上看,tty_driver實(shí)際上是tty機制的實(shí)現組件之一,借用面向對象設計中的常用例子,這時(shí)的tty_driver就象是tty這部汽車(chē)的輪胎,tty這部汽車(chē)要正常運行,還要tty_ldisc(行規程),termios,甚至struct tq_struct tq_hangup(看tty_struct)等基礎設施。它們之間的關(guān)系并非繼承。
至于tty_driver中的函數指針,再打個(gè)C++中的比喻,它們實(shí)際上很象虛函數,也就是說(shuō),可以定義它們,但并不一定實(shí)現它們。實(shí)際上還不用說(shuō)tty_driver,只要查一下serial_driver都會(huì )發(fā)現n多個(gè)具體的實(shí)現,但對各個(gè)具體的設備,其tty_driver中的函數不一定全部實(shí)現。所以put_char,flush_char,read_proc, write_proc,start,stop這些函數的情況是有可能實(shí)現,也有可能不實(shí)現。即使被實(shí)現,也不一定為上層(VFS層)所用。