由securecrt光标异常想到的

一、终端编辑

之前的终端都是为了便于人机交互而设计,交互性比较强。用户输入过程中,通常不是一蹴而就,也不可能没有笔误。所以终端通常是基于行为单位进行处理,在用户输入了回车之后才向用户态返回整个输入内容,也就是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, "101010 world33[K101010101010", 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个字符)。
内核中对于33[K的处理为linux-2.6.21driverscharvt.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 etcorefilter.c。
二、内核对伪终端两侧处理的一个细节
这个问题放在这里有些突兀,是看着一片问题时才注意到的这个问题,虽然简单,但是比较有意思。
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模式)。
举一个简单的例子,程序向终端写入' ',在每个echo指令的最后都有这个个结束符,在opost处理中,内核默认会把它转换为' '两个字符,然后放入tty_driver的接受队列中,即执行tty->driver->put_char(tty, c);,由于伪终端的另一端也是一个相同的终端结构,如果它的termios也使用,那么这个转换后的内容会被再次转换,也就是用户写入的 会被转换为 ,这明显不是我们期望的结果。
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);
}
三、远程连接的问题
使用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;
四、不一致的情况
前面说如果窗口大小调整的信号发送时前端进程不是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自动调整窗口大小,以达到自动修正的目的。
五、复现一下场景
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显示等一些持续时间相对较长的耗时操作。
六、按键处理的一些问题
1、键盘编码表处理
这个小内容和这篇日志同样没有什么直观联系,只是顺带看到的问题,略作笔记,便于之后再查找。
内核中的键盘扫描码向用户输入码的转换使用的是用户态的内核中linux-2.6.21driverss390chardefkeymap.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命令查看。当然这个格式我没有细看,因为几乎不会用到。
2、文件结束字符处理
在输出一些特殊字符时,可能需要在终端中实时输入一些内容,例如输入一个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;
3、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
原因未知。
原文地址:https://www.cnblogs.com/tsecer/p/10487607.html