由securecrt光標異常想到的

1、終端編輯前端

以前的終端都是爲了便於人機交互而設計,交互性比較強。用戶輸入過程當中,一般不是一蹴而就,也不可能沒有筆誤。因此終端一般是基於行爲單位進行處理,在用戶輸入了回車以後才向用戶態返回整個輸入內容,也就是shell的一條命令。一樣是爲了便於用戶編輯,在內核態支持簡單的編輯命令,例如刪除一個單詞,刪除整行,刪除一個字符等操做,輸入字符(ctrl+D)結束整個文件輸出等功能。這些功能集成在內核中的有點就是全部的用戶均可以無差異的使用該功能,減小用戶態不一樣工具的重複代碼和邏輯功能。
這些內核態基本編輯工具可以完成大部分基本功能,在實際應用中它們的功能遠遠不夠。例如,刪除整行的行並非以用戶輸入回車爲單位,而是以屏幕中一行大小爲單位。假設當前終端窗口中一行又80字符,在輸入81個字符以後,輸入 ctrl+U只能刪除最後一個字符,以前的80字符做爲上一行。再假設用戶輸入了hello world,若是想刪除hello(跳過world),在當前的內核模式下一樣沒法實現。
爲了知足複雜的命令行編輯功能,有一個通用的行編輯庫就叫作libreadline,這個也正是bash在編輯命令時使用的底層編輯工具。它工做在終端的raw模式下,也就是全部的用戶輸入都是由readline庫本身實現,包括內核提供的ctrl+W等基本編輯功能也放到bash中本身完成。
把這些功能放在用戶態以後,一些光標位置的控制就須要經過一些特殊的控制序列來完成,也就是常見的" esc["序列。最基本的,readline庫須要知道屏幕的大小,多殺行,多少列。假設用戶輸入光標左移按鍵,readline須要將光標在換行的地方移動到上一行。這個內核中的驅動並不會完成。事實上,計算機的顯卡中光標的位置和輸入沒有任何關係,之因此在終端中輸入一個字符光標自動後移,該功能也是內核態的vt驅動完成,而不是顯卡的硬件完成。
readline還須要考慮的問題就是字符"塌陷"的問題。對於輸入 「hello new world」,假設要刪除中間的new字符,那麼此時new字符以後的全部字符串要移動到new的位置上,屏幕顯示也須要readline來清理和管理。說道這裏,能夠看到readline實際上是一個最爲原始的「窗口管理器」,它本身維護並「記憶」整個屏幕的信息,在合適的位置顯示光標,合適的地方擦除字符,摺疊輸入等操做。
再進一步,以後的圖形管理系統XServer系統,它的模型和這個控制模型也是相似的。考慮一個終端,它可能和主機的距離很是遠,當用戶在終端中鍵入一個命令 ls,這個字符串發送給遠端的bash程序,它的readline庫讀取該命令,把字符串內容返回給bash進行語法分析,readline庫把命令的輸入回顯在屏幕上,輸出結果返回給終端。在字符編輯時,客戶端發送一個ctrl +W來刪除一個單詞,readline將該操做轉換爲一連串移動光標,擦除屏幕字符,屏幕上顯示字符等一連串指令返回給終端,終端則負責把這些指令轉換爲圖像顯示出來。以後的XServer系統中,輸入從用戶的鍵盤輸入變成了光標輸入,回傳的控制指令從字符的擦除轉換爲了更爲複雜的窗口重繪、光標移動等圖形化指令。
下面是一個readline指令控制的一個直觀例子:
一個終端中輸入 hello new world,光標移動到new以後,執行ctrl + W刪除new單詞,此時strace的輸出爲
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#strace -s 100 -p 12483
Process 12483 attached - interrupt to quit
read(0, "\27", 1)                       = 1
write(2, " \10\10\10 world\33[K\10\10\10\10\10\10", 18) = 18
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
read(0, 
其中\33[K就是對於終端的控制序列,從內核的vt.c中能夠知道這個指令序列是擦除從光標當前位置到行尾的全部屏幕字符,而\10表示將光標向前移動一個字符,對於
hello new world
刪除new來講,這個指令序列表示先將光標前移3個字符,到達"new"單詞的開始,輸出world,注意輸出world以後,光標自動移動到world的後面,\33[k 擦除光標當前位置到行尾字符全部字符,對於這種狀況,也就是world最後的rld三個單詞,最後6個\10則是將光標再次重定位到hello 和 world之間(由於輸出world時光標被內核自動後移了6個字符)。
內核中對於\033[K的處理爲linux-2.6.21\drivers\char\vt.c
static void csi_K(struct vc_data *vc, int vpar)
{
unsigned int count;
unsigned short * start;
 
switch (vpar) {
case 0: /* erase from cursor to end of line */
count = vc->vc_cols - vc->vc_x;
start = (unsigned short *)vc->vc_pos;
if (DO_UPDATE(vc))
vc->vc_sw->con_clear(vc, vc->vc_y, vc->vc_x, 1,
     vc->vc_cols - vc->vc_x);
再和linux下的抓包庫libpcap作比較,能夠發現readline也至關於一個簡單的編譯器,它可以把用戶輸入的編輯指令轉換爲內核識別的基礎指令序列,內核中對於網絡包解析處理部分代碼的解析位於linux-2.6.21\net\core\filter.c。
2、內核對僞終端兩側處理的一個細節
這個問題放在這裏有些突兀,是看着一片問題時才注意到的這個問題,雖然簡單,可是比較有意思。
static void __init unix98_pty_init(void)
{
ptm_driver = alloc_tty_driver(NR_UNIX98_PTY_MAX);
if (!ptm_driver)
panic("Couldn't allocate Unix98 ptm driver");
pts_driver = alloc_tty_driver(NR_UNIX98_PTY_MAX);
if (!pts_driver)
panic("Couldn't allocate Unix98 pts driver");
 
ptm_driver->owner = THIS_MODULE;
ptm_driver->driver_name = "pty_master";
ptm_driver->name = "ptm";
ptm_driver->major = UNIX98_PTY_MASTER_MAJOR;
ptm_driver->minor_start = 0;
ptm_driver->type = TTY_DRIVER_TYPE_PTY;
ptm_driver->subtype = PTY_TYPE_MASTER;
ptm_driver->init_termios = tty_std_termios;
ptm_driver->init_termios.c_iflag = 0;
ptm_driver->init_termios.c_oflag = 0;
ptm_driver->init_termios.c_cflag = B38400 | CS8 | CREAD;
ptm_driver->init_termios.c_lflag = 0;
ptm_driver->init_termios.c_ispeed = 38400;
ptm_driver->init_termios.c_ospeed = 38400;
ptm_driver->flags = TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW |
TTY_DRIVER_DYNAMIC_DEV | TTY_DRIVER_DEVPTS_MEM;
ptm_driver->other = pts_driver;
tty_set_operations(ptm_driver, &pty_ops);
ptm_driver->ioctl = pty_unix98_ioctl;
 
pts_driver->owner = THIS_MODULE;
pts_driver->driver_name = "pty_slave";
pts_driver->name = "pts";
pts_driver->major = UNIX98_PTY_SLAVE_MAJOR;
pts_driver->minor_start = 0;
pts_driver->type = TTY_DRIVER_TYPE_PTY;
pts_driver->subtype = PTY_TYPE_SLAVE;
pts_driver->init_termios = tty_std_termios;
pts_driver->init_termios.c_cflag = B38400 | CS8 | CREAD;
pts_driver->init_termios.c_ispeed = 38400;
pts_driver->init_termios.c_ospeed = 38400;
pts_driver->flags = TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW |
TTY_DRIVER_DYNAMIC_DEV | TTY_DRIVER_DEVPTS_MEM;
pts_driver->other = ptm_driver;
tty_set_operations(pts_driver, &pty_ops);
 
if (tty_register_driver(ptm_driver))
panic("Couldn't register Unix98 ptm driver");
if (tty_register_driver(pts_driver))
panic("Couldn't register Unix98 pts driver");
 
pty_table[1].data = &ptm_driver->refcount;
}
僞終端的兩側都是終端,而終端在標準模式下(也就是pts_driver->init_termios = tty_std_termios中的tty_std_termios)會對特殊字符作額外處理(也就是默認的cooked模式)。
舉一個簡單的例子,程序向終端寫入'\n',在每一個echo指令的最後都有這個個結束符,在opost處理中,內核默認會把它轉換爲'\r\n'兩個字符,而後放入tty_driver的接受隊列中,即執行tty->driver->put_char(tty, c);,因爲僞終端的另外一端也是一個相同的終端結構,若是它的termios也使用,那麼這個轉換後的內容會被再次轉換,也就是用戶寫入的\n會被轉換爲\r\r\n,這明顯不是咱們指望的結果。
pty沒有定義本身的put_char接口,可是通用層作了一層兼容:
unix98_pty_init--->>tty_register_driver
if (!driver->put_char)
driver->put_char = tty_default_put_char;
static void tty_default_put_char(struct tty_struct *tty, unsigned char ch)
{
tty->driver->write(tty, &ch, 1);
}
3、遠程鏈接的問題
使用secureCRT登錄遠端服務器時,若是在本地修改了secureCRT窗口的大小,爲了保證遠端的readline可以正確的調整本身的窗口大小,此時須要須要一種機制來同步給遠端服務器上該僞終端的變化。本地的sercureCRT不能直接調用遠端的系統,而只能經過遠端的代理來調整終端的大小和本身窗口大小一致。
若是搜索下一ssh代碼(我文本搜索的是freebsd的一份ssh代碼),其中的確能夠看到有協議來同步窗口的大小。這裏能夠腦補一下當本地secureCRT窗口調整以後通過的一些過程。
窗口大小調整,secureCRT經過協議告訴和本身直連的另外一端(sshd)新窗口的大小,遠端sshd一端經過網絡和secureCRT鏈接,另外一端經過僞終端和bash鏈接(bash中的前端進程有可能仍是一個ssh客戶端,考慮一下通過一些跳板機中轉鏈接目標服務器的狀況)。
當sshd受到網絡傳遞的修改協議以後,執行ioctrl系統調用來修改本身使用的終端的大小。
內核調整了該終端大小以後,經過SIGWINCH信號告訴給該終端的前端進程組(這個信號在gdb模式下默認是不作任何處理的,因此使用調試器附加到bash上不會被SIGWINCH中斷,須要執行handl SIGWINCH stop來使能中止)。假設說前端進程組bash自己,那麼bash的readline就會調整本身的窗口大小和secureCRT窗口大小一致。若是前端進行是ssh,那麼它就會進行「接力」傳遞,再把這個事件傳遞給遠端sshd。麻煩的狀況在於若是前端進程不是bash也不是ssh,那麼此時這個信號丟失,bash沒法感知窗口變化,此時會出現secureCRT大小和bash大小不一致狀況(這個地方還有細節沒有說,由於這裏太長了,稍候再說)。
內核中對於調整窗口大小的接口爲 TIOCS
tty_ioctl--->>tiocswinsz(tty, real_tty, p)
if (tty->pgrp)
kill_pgrp(tty->pgrp, SIGWINCH, 1);
if ((real_tty->pgrp != tty->pgrp) && real_tty->pgrp)
kill_pgrp(real_tty->pgrp,  SIGWINCH, 1);
tty->winsize = tmp_ws;
4、不一致的狀況
前面說若是窗口大小調整的信號發送時前端進程不是bash,那麼bash認爲的窗口大小和secureCRT的大小是不一致的,此時顯示就會出現各類詭異的顯示,具體每種詭異的狀況如何從代碼層面上詳細分析,就會涉及到readline庫代碼的分析,這個庫看起來仍是比較繁瑣的,因此省略。
在有些狀況下,即便前端進程不是bash,咱們發現調整了以後readline依然可以正確工做。看下bash的FAQ能夠知道,bash支持經過checkwinsize選項來使能shell的主動監測。當shell派生命令退出以後,shell都主動監測下當前終端的大小和本身認爲的大小是否相同,不一樣則更新。
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#shopt | grep win
checkwinsize    off
這裏有三個地方的窗口大小,secureCRT本地窗口大小,tty終端中窗口大小,bash中readline認爲的窗口大小。一般前二者是一致的,能夠經過執行stty -a命令看遠端tty的窗口大小,而secureCRT的大小在工具的左下角都有顯示。問題在於readline認爲的窗口大小和這二者是否一致,若是不一致則readline編譯出來的調整指令在secureCRT執行起來的結果就顯得詭異。對於bash內部認爲的窗口大小能夠經過內置變量$COLUMNS和$LINES來顯示。
這裏要說的時,若是出現窗口不一致的狀況,此時能夠經過使能readline的checkwinsize選項來讓bash自動調整窗口大小,以達到自動修正的目的。
5、復現一下場景
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#echo $LINES $COLUMNS
36 105
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#shopt | grep checkwinsize
checkwinsize    off
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#sleep 1000
[LOCAL] : SEND[0]: window-change (rows: 57, cols: 177)
 
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#echo $LINES $COLUMNS
36 105
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#stty -a
speed 38400 baud; rows 57; columns 177; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V;
flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
固然在現實場景中,前端進程通常不會是sleep這麼普通,它一般應該是 vim編輯器,gzip壓縮,rsync同步,rz/sz傳輸、zcat顯示等一些持續時間相對較長的耗時操做。
6、按鍵處理的一些問題
一、鍵盤編碼表處理
這個小內容和這篇日誌一樣沒有什麼直觀聯繫,只是順帶看到的問題,略做筆記,便於以後再查找。
內核中的鍵盤掃描碼向用戶輸入碼的轉換使用的是用戶態的內核中linux-2.6.21\drivers\s390\char\defkeymap.c文件的說明
/* Do not edit this file! It was automatically generated by   */
/*    loadkeys --mktable defkeymap.map > defkeymap.c          */
loadkeys是用戶態一個進程,經過man loadkeys能夠看到該工具說明,並且還能夠看到其它相關工具說明
FILES
       /usr/share/kbd/keymaps
              default directory for keymaps
 
 
       /usr/src/linux/drivers/char/defkeymap.map
              default kernel keymap
 
SEE ALSO
       dumpkeys(1), keymaps(5)
代碼註釋中說的defkeymap.map文件就在內核的源代碼樹中。該文件的格式能夠經過man keymaps命令查看。固然這個格式我沒有細看,由於幾乎不會用到。
二、文件結束字符處理
在輸出一些特殊字符時,可能須要在終端中實時輸入一些內容,例如輸入一個shell腳本
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#cat | sh
for num in `seq 1 10`
do
echo hello${num}world
done
hello1world
hello2world
hello3world
hello4world
hello5world
hello6world
hello7world
hello8world
hello9world
hello10world
內核中的處理
n_tty_receive_char
if (c == EOF_CHAR(tty)) {
        if (tty->canon_head != tty->read_head)
        set_bit(TTY_PUSH, &tty->flags);
c =  __DISABLED_CHAR;
goto handle_newline;
}
handle_newline:
spin_lock_irqsave(&tty->read_lock, flags);
set_bit(tty->read_head, tty->read_flags);
put_tty_queue_nolock(c, tty);
tty->canon_head = tty->read_head;
tty->canon_data++;
spin_unlock_irqrestore(&tty->read_lock, flags);
kill_fasync(&tty->fasync, SIGIO, POLL_IN);
if (waitqueue_active(&tty->read_wait))
wake_up_interruptible(&tty->read_wait);
return;
在讀取時
static ssize_t read_chan(struct tty_struct *tty, struct file *file,
 unsigned char __user *buf, size_t nr)
if (!eol || (c != __DISABLED_CHAR)) { 當讀取到 __DISABLED_CHAR時,沒有增長操做b的數值,接下來返回大小的 size = b - buf值爲0,或者說這個字符自己沒有影響減法的結果
if (put_user(c, b++)) {
retval = -EFAULT;
b--;
break;
}
nr--;
}
if (eol)
break;
……
size = b - buf;
if (size) {
retval = size;
if (nr)
        clear_bit(TTY_PUSH, &tty->flags);
} else if (test_and_clear_bit(TTY_PUSH, &tty->flags))
 goto do_it_again;
 
n_tty_set_room(tty);
 
return retval;
三、secureCRT下使用內核自帶編輯功能刪除單個字符
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#stty -a
speed 38400 baud; rows 57; columns 177; line = 0;
intr = ^C; quit = ^\;  erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V;
flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
tsecer@harry:/data/harry/harrytest/bash/bash-4.0#sleep 1111
dfdf
dfdsfdf sdfd^_^_^_^_^_^_^_^_dkfksdkfsdfs 
內核的說明中是經過 ctrl + ?來刪除一個字符,可是使用無效,我逐個嘗試了一下,刪除單個字符的方法是
ctrl + backspace
緣由未知。
相關文章
相關標籤/搜索