從printXX看tty設備(6)tty框架及串口O_NONBLOCK何時丟失數據
阿新 • • 發佈:2019-03-07
ole serial 鼠標 模式 -i 終端配置 for 內容 ctrl+s 這個函數簇的主要功能就是作為所有tty設備的一個入口,它的主要功能就是根據設備號查找到該設備對應的tty_driver,然後在VFS中的struct file 變量的private_data中安裝一個struct tty_struct變量,這個變量對於每個具體的tty設備有一個,可以保存會話相關內容以及tty設備終端配置信息等。
為什麽說這個是所有tty設備的VFS接口呢?因為所有的tty_driver的註冊都是通過tty_register_driver函數來實現的,而這個函數內通過
cdev_init(&driver->cdev, &tty_fops);
將設備的VFS接口註冊為tty_fops。
在tty_open接口中,會通過
driver = get_tty_driver(device, &index);
來查找設備文件對應的驅動程序。
2、tty_driver
不同的tty設備會對應不同的tty_driver,一個tty_driver可以對應若幹個tty設備。例如虛擬控制臺設備可以占到主設備為1,次設備從1到63個設備,而串口則從65開始,而8250串口則可以占據主設備4,次設備從64開始的若幹設備號。這些驅動通過tty_register_driver來把自己註冊為驅動,在註冊的過程中,在一個tty_driver中,它裏面明確說明了自己覆蓋的設備編號範圍
int major; /* major device number */
int minor_start; /* start of minor device number */
int minor_num; /* number of *possible* devices */
int num; /* number of devices allocated */
這些都是驅動在註冊的時候確定好的。
3、tty_struct結構
這個是tty對應的struct file中private_data變量指向的一個內容,這個變量的分配是在某個設備文件第一次打開的時候按需分配的。為什麽這麽做呢?因為很多的tty設備可能一直沒有被用到,所以就不需要分配。例如我們剛才說的虛擬tty設備,它雖然占有主設備號4,次設備為1到63個設備編號,但是通常只有前幾個作為系統inittab中getty設備的控制臺串口,其它之後大部分都是沒有使用的,所以沒有必要在一開始都分配這些tty_struct,而是在需要的時候分配,這和內核中的Copy On Write的思想一致,都是lazy算法思想。這個結構的分配和初始化流程為:
tty_open--->>>init_dev--->>alloc_tty_struct & initialize_tty_struct
中進行分配,當然在init_dev函數中會判斷設備對應的tty_struct是否已經被分配。然後在tty_open中通過
filp->private_data = tty;
將這個新分配的tty_struct實例安裝到VFS中struct file的private_data指針中。
4、tty_ldisc
這個現在只見到了對N_TTY類型的鏈路層的使用,其它的不是很清楚,但是從這個名字上來看應該是一個邏輯的概念,就是說對接收和發送的數據如何額外轉化的一個過程,然後將這個轉換的結果發送給具體的tty驅動。例如,當我們在串口上輸入CTRL+C的時候,是否要把這個組合按鍵轉換為一個SIGINT信號發送給前臺回話(而不是在tty的輸入隊列裏放一個CTRL的按鍵碼和一個C的按鍵碼),比方說,是否在接收到一個回車的時候才喚醒對tty的read等待(相對於每接收到一個字符就喚醒)等。這個成員的初始化是在
tty_open--->>>init_dev--->>initialize_tty_struct
tty_ldisc_assign(tty, tty_ldisc_get(N_TTY));
這裏的tty_ldisc是個內嵌在tty_struct中的結構,鏈路規則為console_init函數中註冊的
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
5、struct ktermios
這個結構是用戶態可以配置的一個接口,對於不同的tty_struct的不同表現具有重要影響。例如,要配置一個tty設備是否回顯功能(密碼輸入的時候是不能回顯的),最少接收到多少個字符進行read喚醒等都可以配置,用戶態的stty程序可以顯示和配置一個tty設備的這些屬性。這個結構的分配同樣是在init_dev中分配並初始化,而且大部分的tty_driver設備都設置自己的初始配置為tty_std_termios結構,例如串口的初始化為uart_register_driver中
normal->init_termios = tty_std_termios;
三、tty的鏈路層
這個tty_read接口比較簡單,其實主要工作就是轉發向了鏈路層,這個鏈路層也就是前面說的tty_ldisc_N_TTY的read_chan/write_chan接口.前面可以看到,當使用initialize_tty_struct結構中默認就是用的這個N_TTY鏈路層。但是系統中還的確存在其他的鏈路層,例如鼠標消息的發送也是通過自己專用的一個鏈路層N_MOUSE來實現的。當然還有其他常見但是無緣拜會的一些鏈路結構,例如N_SLIP,N_HDLC等。如果作為了解,這些內容還是應該看一下,作為一個對比,從而理解這個層次存在的意義,可以沒有這方面的應用,精力和智力都不允許,所以咱就不趟這趟水了。
但是鏈路層的作用還是比較重要的,很多的字符轉化都是在這一層來完成的。最為常見的就是當鍵盤上按下Ctrl+C的時候,此時鏈路層會向當前tty設備的前臺發送一個SIGINT信號,而這個C字符就不會放入接收隊列。或者說CTRL+K可以刪除當前鏈路層中所有輸入信息,這一點有一個重要的啟示和思路,那就是N_TTY鏈路默認是按照行來進行喚醒的,也就是說,當用戶沒有按下回車鍵的時候,所有的輸入都將會放在鏈路層的緩沖區中。這一點對於read的行為影響是很大的,比方說,read要求讀入長度為100字符,那麽在用戶輸入下面幾個字符
ls
之後回車,這個read調用會馬上返回,並且read的返回值為3,緩沖區內為“ls\n”。這裏有很多值得註意的事情,之後有機會再展開。
為了說明鏈路層,這裏看一下當我們在鍵盤上按下CTRL+C組合鍵之後,這個組合是如何轉換為SIGINT被我們接收到的。
對於一個CTRL+C,雖然在我們看來是一個組合鍵,也就是兩次按鍵,但是在發給鏈路層之後是一個字符,ASCII碼為3(推廣情況就是,CTRL+A為1,CTRL+Z為26,那麽ASCII碼的0還有27到31之間的非鍵盤顯示字符如何打印呢?這個其實從ASCII的排表順序中也可以得到,那就是第64個字符還是的32個可打印字符和前32個非可打印字符是一一對應的,只是在這些可打印字符加上CTRL既可以得到對應的前32個非可顯示字符)。
在串口中輸入CTRL+S
linux-2.6.21\drivers\char\n_tty.c
static inline void n_tty_receive_char(struct tty_struct *tty, unsigned char c)
if (I_IXON(tty)) {
if (c == START_CHAR(tty)) {這裏的START_CHAR就是CTRL+S,而對於開始則為CTRL+Q,當然這裏說的都收默認值,用戶可以修改。
start_tty(tty);
return;
}
if (c == STOP_CHAR(tty)) {
stop_tty(tty);
return;
}
}
linux-2.6.21\include\asm-i386\termios.h
/* intr=^C quit=^\ erase=del kill=^U
eof=^D vtime=\0 vmin=\1 sxtc=\0
start=^Q stop=^S susp=^Z eol=\0
reprint=^R discard=^U werase=^W lnext=^V
eol2=\0
*/
#define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0"
void stop_tty(struct tty_struct *tty)
{
if (tty->stopped)
return;
tty->stopped = 1;
if (tty->link && tty->link->packet) {
tty->ctrl_status &= ~TIOCPKT_START;
tty->ctrl_status |= TIOCPKT_STOP;
wake_up_interruptible(&tty->link->read_wait);
}
if (tty->driver->stop)
(tty->driver->stop)(tty);
}
以我們常見的偽終端為例,它的驅動pty_ops並沒有定義自己的stop方法,所以它只是執行了 tty->stopped = 1操作。當一個串口輸入被終止時候,當用戶通過tty_write寫入數據的時候,它執行的操作為
static int pty_write(struct tty_struct * tty, const unsigned char *buf, int count)
{
struct tty_struct *to = tty->link;
int c;
if (!to || tty->stopped)
return 0;
這裏判斷如果tty->stopped為1,那麽此時發送失敗,發送返回值為零,此時就會在write_chan函數中判斷這個返回值,其中有
while (nr > 0) {
c = tty->driver->write(tty, b, nr);
if (c < 0) {
retval = c;
goto break_out;
}
if (!c)
break;
b += c;
nr -= c;
}
}
if (!nr)
break;
if (file->f_flags & O_NONBLOCK) {如果串口設置了非阻塞,那麽此次打印將會丟失。
retval = -EAGAIN;
break;
四、8520串口驅動丟數據
現在假設說用戶沒有執行CTRL+S來暫停串口,然後用戶態對串口設置了非阻塞模式,然後在物理串口發送的時候何時丟失數據。對於所有的uart設備,它們將會共享一個上層tty驅動,也就是一個tty_operations uart_ops結構,這是一個虛擬的串口控制器,然後對於每個具體的設備,在這個統一的uart設備的管理之上成為一個uart_port,每個具體的端口對應一個結構,例如,對於我們通常的PC上就有兩個串口,所以就會有兩個對應的uart_port結構,然後每種不同的port有自己對應的uart_ops實現,這樣就是一個從VFS到具體串口端口的映射和轉換過程。
現在假設用戶通過tty_write寫入數據的時候,它大致流程為
tty_write--->>>write_chan-->>uart_write--->>>uart_start--->>__uart_start--->>>serial8250_start_tx
其中在__uart_start接口中主要就是將write_chan中傳入的數據放入一個uart設備可以識別的環形緩沖區中,等待串口來發送。這裏的發送並不是同步的,當從write_chan返回的時候,這些數據並沒有發送,串口的發送使用的是中斷方式,當緩沖區中的一個數據發送完之後,串口將會產生一個中斷,然後在中斷程序中再次從緩沖區摘取一個字符放入串口控制器,如此反復。
在uart_write函數中
while (1) {
c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);
if (count < c)
c = count;
if (c <= 0)這裏如果打印非常多,那麽緩沖區的空閑空間為零,此處將會直接返回。也就是在write_chan中返回零,和前一節類似。
break;
memcpy(circ->buf + circ->head, buf, c);
circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);
buf += c;
count -= c;
ret += c;
}
返回之後如果設置的是非阻塞模式,那麽此時打印數據將會丟失,並且從用戶空間中返回,並且返回值為-EAGAIN。
五、printk為什麽不會丟數據及於write_chan的串口共享問題
當串口作為控制臺接口的時候,它並不是通過uart_write這一套接口來實現的,而是通過serial8250_console中註冊的綠色通道來完成,在這個結構中,其實現方法為serial8250_console_write。
/*
* First save the IER then disable the interrupts當控制臺打印時,此時將會關掉中斷,而tty_write使用的就是中斷,所以用戶打印將暫停。
*/
ier = serial_in(up, UART_IER);
if (up->capabilities & UART_CAP_UUE)
serial_out(up, UART_IER, UART_IER_UUE);
else
serial_out(up, UART_IER, 0);
uart_console_write(&up->port, s, count, serial8250_console_putchar);
static void serial8250_console_putchar(struct uart_port *port, int ch)
{
struct uart_8250_port *up = (struct uart_8250_port *)port;
wait_for_xmitr(up, UART_LSR_THRE);然後逐個字符進行輪詢,從而保證發送是同步的。
serial_out(up, UART_TX, ch);
}
有時候我們看到,用戶態的printf比printk的顯示要慢,也正是這個原因,因為用戶態的tty_write是使用中斷的異步方式,而內核的printk則是同步方式。
一、內核tty實現
這個模塊在內核的實現中占有濃重的一筆,我甚至經常覺得,經常搞的是串口還是網口是嵌入式工程師和網絡工程師的一重要區別標誌。所以作為一個嵌入式工程師,對這個tty設備接觸的比較多,所以感情也比較深一些。在2011年11月份(偉大的聖光棍節月份),我在博客裏對tty設備做了一個簡單的總結,後來就把這一頁揭過去,但是最近又遇到一些東西,感覺還是有必要再補充一下。當時對於tty設備的理解還是比較膚淺的(當然現在依然如此),所以其實沒有跳出來看一下這個實現的框架及背後的一些原因。
二、tty設備與VFS接口
1、tty_fops變量中的tty_open/tty_read/tty_write接口簇
這組接口是所有tty設備所共享的一個接口簇,這個主要是對應設備文件和VFS系統接口實現,這就是tty設備和整個Linux內核中虛擬文件系統的一層接口,這組接口是和塊設備(例如硬盤/dev/hda之類)的操作接口並列的一個概念;也是通過設備文件查找設備的第一次分層。
為什麽說這個是所有tty設備的VFS接口呢?因為所有的tty_driver的註冊都是通過tty_register_driver函數來實現的,而這個函數內通過
cdev_init(&driver->cdev, &tty_fops);
將設備的VFS接口註冊為tty_fops。
driver = get_tty_driver(device, &index);
來查找設備文件對應的驅動程序。
2、tty_driver
不同的tty設備會對應不同的tty_driver,一個tty_driver可以對應若幹個tty設備。例如虛擬控制臺設備可以占到主設備為1,次設備從1到63個設備,而串口則從65開始,而8250串口則可以占據主設備4,次設備從64開始的若幹設備號。這些驅動通過tty_register_driver來把自己註冊為驅動,在註冊的過程中,在一個tty_driver中,它裏面明確說明了自己覆蓋的設備編號範圍
int major; /* major device number */
int minor_num; /* number of *possible* devices */
int num; /* number of devices allocated */
這些都是驅動在註冊的時候確定好的。
3、tty_struct結構
這個是tty對應的struct file中private_data變量指向的一個內容,這個變量的分配是在某個設備文件第一次打開的時候按需分配的。為什麽這麽做呢?因為很多的tty設備可能一直沒有被用到,所以就不需要分配。例如我們剛才說的虛擬tty設備,它雖然占有主設備號4,次設備為1到63個設備編號,但是通常只有前幾個作為系統inittab中getty設備的控制臺串口,其它之後大部分都是沒有使用的,所以沒有必要在一開始都分配這些tty_struct,而是在需要的時候分配,這和內核中的Copy On Write的思想一致,都是lazy算法思想。這個結構的分配和初始化流程為:
tty_open--->>>init_dev--->>alloc_tty_struct & initialize_tty_struct
中進行分配,當然在init_dev函數中會判斷設備對應的tty_struct是否已經被分配。然後在tty_open中通過
filp->private_data = tty;
將這個新分配的tty_struct實例安裝到VFS中struct file的private_data指針中。
4、tty_ldisc
這個現在只見到了對N_TTY類型的鏈路層的使用,其它的不是很清楚,但是從這個名字上來看應該是一個邏輯的概念,就是說對接收和發送的數據如何額外轉化的一個過程,然後將這個轉換的結果發送給具體的tty驅動。例如,當我們在串口上輸入CTRL+C的時候,是否要把這個組合按鍵轉換為一個SIGINT信號發送給前臺回話(而不是在tty的輸入隊列裏放一個CTRL的按鍵碼和一個C的按鍵碼),比方說,是否在接收到一個回車的時候才喚醒對tty的read等待(相對於每接收到一個字符就喚醒)等。這個成員的初始化是在
tty_open--->>>init_dev--->>initialize_tty_struct
tty_ldisc_assign(tty, tty_ldisc_get(N_TTY));
這裏的tty_ldisc是個內嵌在tty_struct中的結構,鏈路規則為console_init函數中註冊的
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
5、struct ktermios
這個結構是用戶態可以配置的一個接口,對於不同的tty_struct的不同表現具有重要影響。例如,要配置一個tty設備是否回顯功能(密碼輸入的時候是不能回顯的),最少接收到多少個字符進行read喚醒等都可以配置,用戶態的stty程序可以顯示和配置一個tty設備的這些屬性。這個結構的分配同樣是在init_dev中分配並初始化,而且大部分的tty_driver設備都設置自己的初始配置為tty_std_termios結構,例如串口的初始化為uart_register_driver中
normal->init_termios = tty_std_termios;
三、tty的鏈路層
這個tty_read接口比較簡單,其實主要工作就是轉發向了鏈路層,這個鏈路層也就是前面說的tty_ldisc_N_TTY的read_chan/write_chan接口.前面可以看到,當使用initialize_tty_struct結構中默認就是用的這個N_TTY鏈路層。但是系統中還的確存在其他的鏈路層,例如鼠標消息的發送也是通過自己專用的一個鏈路層N_MOUSE來實現的。當然還有其他常見但是無緣拜會的一些鏈路結構,例如N_SLIP,N_HDLC等。如果作為了解,這些內容還是應該看一下,作為一個對比,從而理解這個層次存在的意義,可以沒有這方面的應用,精力和智力都不允許,所以咱就不趟這趟水了。
但是鏈路層的作用還是比較重要的,很多的字符轉化都是在這一層來完成的。最為常見的就是當鍵盤上按下Ctrl+C的時候,此時鏈路層會向當前tty設備的前臺發送一個SIGINT信號,而這個C字符就不會放入接收隊列。或者說CTRL+K可以刪除當前鏈路層中所有輸入信息,這一點有一個重要的啟示和思路,那就是N_TTY鏈路默認是按照行來進行喚醒的,也就是說,當用戶沒有按下回車鍵的時候,所有的輸入都將會放在鏈路層的緩沖區中。這一點對於read的行為影響是很大的,比方說,read要求讀入長度為100字符,那麽在用戶輸入下面幾個字符
ls
之後回車,這個read調用會馬上返回,並且read的返回值為3,緩沖區內為“ls\n”。這裏有很多值得註意的事情,之後有機會再展開。
為了說明鏈路層,這裏看一下當我們在鍵盤上按下CTRL+C組合鍵之後,這個組合是如何轉換為SIGINT被我們接收到的。
對於一個CTRL+C,雖然在我們看來是一個組合鍵,也就是兩次按鍵,但是在發給鏈路層之後是一個字符,ASCII碼為3(推廣情況就是,CTRL+A為1,CTRL+Z為26,那麽ASCII碼的0還有27到31之間的非鍵盤顯示字符如何打印呢?這個其實從ASCII的排表順序中也可以得到,那就是第64個字符還是的32個可打印字符和前32個非可打印字符是一一對應的,只是在這些可打印字符加上CTRL既可以得到對應的前32個非可顯示字符)。
在串口中輸入CTRL+S
linux-2.6.21\drivers\char\n_tty.c
static inline void n_tty_receive_char(struct tty_struct *tty, unsigned char c)
if (I_IXON(tty)) {
if (c == START_CHAR(tty)) {這裏的START_CHAR就是CTRL+S,而對於開始則為CTRL+Q,當然這裏說的都收默認值,用戶可以修改。
start_tty(tty);
return;
}
if (c == STOP_CHAR(tty)) {
stop_tty(tty);
return;
}
}
linux-2.6.21\include\asm-i386\termios.h
/* intr=^C quit=^\ erase=del kill=^U
eof=^D vtime=\0 vmin=\1 sxtc=\0
start=^Q stop=^S susp=^Z eol=\0
reprint=^R discard=^U werase=^W lnext=^V
eol2=\0
*/
#define INIT_C_CC "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0"
void stop_tty(struct tty_struct *tty)
{
if (tty->stopped)
return;
tty->stopped = 1;
if (tty->link && tty->link->packet) {
tty->ctrl_status &= ~TIOCPKT_START;
tty->ctrl_status |= TIOCPKT_STOP;
wake_up_interruptible(&tty->link->read_wait);
}
if (tty->driver->stop)
(tty->driver->stop)(tty);
}
以我們常見的偽終端為例,它的驅動pty_ops並沒有定義自己的stop方法,所以它只是執行了 tty->stopped = 1操作。當一個串口輸入被終止時候,當用戶通過tty_write寫入數據的時候,它執行的操作為
static int pty_write(struct tty_struct * tty, const unsigned char *buf, int count)
{
struct tty_struct *to = tty->link;
int c;
if (!to || tty->stopped)
return 0;
這裏判斷如果tty->stopped為1,那麽此時發送失敗,發送返回值為零,此時就會在write_chan函數中判斷這個返回值,其中有
while (nr > 0) {
c = tty->driver->write(tty, b, nr);
if (c < 0) {
retval = c;
goto break_out;
}
if (!c)
break;
b += c;
nr -= c;
}
}
if (!nr)
break;
if (file->f_flags & O_NONBLOCK) {如果串口設置了非阻塞,那麽此次打印將會丟失。
retval = -EAGAIN;
break;
四、8520串口驅動丟數據
現在假設說用戶沒有執行CTRL+S來暫停串口,然後用戶態對串口設置了非阻塞模式,然後在物理串口發送的時候何時丟失數據。對於所有的uart設備,它們將會共享一個上層tty驅動,也就是一個tty_operations uart_ops結構,這是一個虛擬的串口控制器,然後對於每個具體的設備,在這個統一的uart設備的管理之上成為一個uart_port,每個具體的端口對應一個結構,例如,對於我們通常的PC上就有兩個串口,所以就會有兩個對應的uart_port結構,然後每種不同的port有自己對應的uart_ops實現,這樣就是一個從VFS到具體串口端口的映射和轉換過程。
現在假設用戶通過tty_write寫入數據的時候,它大致流程為
tty_write--->>>write_chan-->>uart_write--->>>uart_start--->>__uart_start--->>>serial8250_start_tx
其中在__uart_start接口中主要就是將write_chan中傳入的數據放入一個uart設備可以識別的環形緩沖區中,等待串口來發送。這裏的發送並不是同步的,當從write_chan返回的時候,這些數據並沒有發送,串口的發送使用的是中斷方式,當緩沖區中的一個數據發送完之後,串口將會產生一個中斷,然後在中斷程序中再次從緩沖區摘取一個字符放入串口控制器,如此反復。
在uart_write函數中
while (1) {
c = CIRC_SPACE_TO_END(circ->head, circ->tail, UART_XMIT_SIZE);
if (count < c)
c = count;
if (c <= 0)這裏如果打印非常多,那麽緩沖區的空閑空間為零,此處將會直接返回。也就是在write_chan中返回零,和前一節類似。
break;
memcpy(circ->buf + circ->head, buf, c);
circ->head = (circ->head + c) & (UART_XMIT_SIZE - 1);
buf += c;
count -= c;
ret += c;
}
返回之後如果設置的是非阻塞模式,那麽此時打印數據將會丟失,並且從用戶空間中返回,並且返回值為-EAGAIN。
五、printk為什麽不會丟數據及於write_chan的串口共享問題
當串口作為控制臺接口的時候,它並不是通過uart_write這一套接口來實現的,而是通過serial8250_console中註冊的綠色通道來完成,在這個結構中,其實現方法為serial8250_console_write。
/*
* First save the IER then disable the interrupts當控制臺打印時,此時將會關掉中斷,而tty_write使用的就是中斷,所以用戶打印將暫停。
*/
ier = serial_in(up, UART_IER);
if (up->capabilities & UART_CAP_UUE)
serial_out(up, UART_IER, UART_IER_UUE);
else
serial_out(up, UART_IER, 0);
uart_console_write(&up->port, s, count, serial8250_console_putchar);
static void serial8250_console_putchar(struct uart_port *port, int ch)
{
struct uart_8250_port *up = (struct uart_8250_port *)port;
wait_for_xmitr(up, UART_LSR_THRE);然後逐個字符進行輪詢,從而保證發送是同步的。
serial_out(up, UART_TX, ch);
}
有時候我們看到,用戶態的printf比printk的顯示要慢,也正是這個原因,因為用戶態的tty_write是使用中斷的異步方式,而內核的printk則是同步方式。
從printXX看tty設備(6)tty框架及串口O_NONBLOCK何時丟失數據