linux 终端下敲ctrl-c时,到底发生了什么?
通过telnet登录到单板,然后按ctrl-c会发生什么情况,流程是怎么样的?
在分析之前,先介绍tty的相关知识。
我们可以认为,所有跟输入输出相关的操作,最终都由tty来接管。
举例来说,当我们敲 ls /dev时得到
[root@ dev]# ls /dev
autofs fb loop0 lp3 oldmem sda1 snapshot tty12 tty22
tty32 tty42 tty52 tty62 usbmon1
block fb0 loop1 MAKEDEV port sda2 snd tty13 tty23
tty33 tty43 tty53 tty63 usbmon2
bsg fd loop2 mapper ppp sda3 stderr tty14 tty24
tty34 tty44 tty54 tty7 usbmon3
bus full loop3 mcelog ptmx sda4 stdin tty15 tty25
tty35 tty45 tty55 tty8 usbmon4
char fuse loop4 megadev0 pts sda5 stdout tty16 tty26
tty36 tty46 tty56 tty9 usbmon5
console hpet loop5 mem random sda6 systty tty17 tty27
tty37 tty47 tty57 ttyS0 vcs
core hvc0 loop6 net rfkill sequencer tty tty18 tty28
tty38 tty48 tty58 ttyS1 vcs1
cpu input loop7 network_latency root sequencer2 tty0 tty19 tty29
tty39 tty49 tty59 ttyS2 vcsa
cpu_dma_latency kmem lp0 network_throughput rtc sg0 tty1 tty2 tty3
tty4 tty5 tty6 ttyS3 vcsa1
disk kmsg lp1 null rtc0 sg1 tty10 tty20 tty30
tty40 tty50 tty60 urandom vga_arbiter
dri log lp2 nvram sda shm tty11 tty21 tty31
tty41 tty51 tty61 usbmon0 zero
[root@ dev]#
另外还可以通过 /proc/tty/drivers得到tty相关驱动信息
[root@ dev]# cat /proc/tty/drivers
/dev/tty /dev/tty 5 0 system:/dev/tty
/dev/console /dev/console 5 1 system:console
/dev/ptmx /dev/ptmx 5 2 system
/dev/vc/0 /dev/vc/0 4 0 system:vtmaster
serial /dev/ttyS 4 64-95 serial
pty_slave /dev/pts 136 0-1048575 pty:slave
pty_master /dev/ptm 128 0-1048575 pty:master
unknown /dev/tty 4 1-63 console
[root@ dev]#
tty 控制终端,control terminal 这个是跟进程相关的,可以理解为一个指针,指向具体的tty终端设备。
进程可以通过open /dev/tty来获取当前进程使用的tty终端具体是哪个,例如是ptmx,还是pts/2,还是
tty2,或者是ttyS0
linux shell下tty命令的可以查看当前进程的控制终端。例如:
[root@ dev]# tty
/dev/pts/10
具体的实现是靠ttyname(0)
即返回当前进程的标准输入,对应的tty终端设备是哪个。
另外通过ps -ax可以查看系统所有进程的控制终端,如:
18448 ? Ss 0:06 sshd: root@pts/9
18451 pts/9 Ss+ 0:00 -bash
3424 tty2 Ss+ 0:00 /sbin/mingetty /dev/tty2
这是通过进程的/proc/pid/stat的第7个字段,当前控制终端的设备号得到的。
ptmx 伪终端主
pts 伪终端从
tty0-tty63 虚拟终端,也叫虚拟控制台终端,virtual console ternimal 通常情况下是6个,这里我们查看
的服务器有63个
tty0可以理解为指针
ttyS0-ttyS3 串口终端
刚才说到,/proc/pid/stat的第7个字段是进程控制终端的设备号,可以根据此设备号得到具体的设备名。
例如:
[root@ dev]# cat /proc/1191/stat1191 (bash) S 1185 1191 1191 34849 1191 4202752 3550 51329644 0 1551 5 6 157769 26478 20 0 1 0
36325973 111104000 471 18446744073709551615 4194304 5064252 140735122662272 140735122657512
263321371088 0 0 3686404 1266761467 18446744071582166303 0 0 17 0 0 0 0 0 0
[root@ dev]#
34849即0x8821,主设备号为高4字节0x88即136,次设备号为低4字节0x21即33
对照/proc/tty/drivers得到/dev/pts/33
再根据ps -ax | grep 1191得到
1191 pts/33 Ss 0:00 -bash
确实是吻合的。
扯远了,下面来看telnet的流程
26912 ? Ss 0:00 busybox telnetd当用户通过telnet客户端连接telnetd后
26912 ? Ss 0:00 busybox telnetd
27030 ? Ss 0:00 login -- chenyu
27038 pts/6 Ss+ 0:00 -bash
可以看出telnetd先fork出gettty并执行login,登录成功后则再fork出实际的bash,
这个bash用的是伪终端pts/6,
以下数据的PPid字段可以证明:Name: busybox
PPid: 1
Name: login
Pid: 27030
PPid: 26912
Name: bash
Pid: 27038
PPid: 27030
bash并不知道这个pts/6具体是个什么设备,只知道往里面读写。
每次在telnet客户端,敲一个字符,就会把数据发给telnetd,telnetd再操作ptmx通过
tty displine将数据格式化传递给pts/6,然后bash从pts/6收到数据进行处理,再写回pts/6,
传给ptmx,由telnetd将数据再传递回telnet客户端。 可以通过每次在telnet客户端
敲一个字符,telnetd的调度次数就增加2,来大体验证这个流程(telnetd收到客户端数据被唤醒,bash通过
伪终端把数据写回唤醒telnetd,一共两次唤醒)
具体的telnet流程图如下:

说完了telnetd的原理,再来看看在telnet客户端敲ctrl-c时,系统会发生什么。
首先可以明确的是,ctrl-c传递到telnetd后,会通过写伪终端主设备ptmx的方式传递给从设备进而传给bash。
那么,到底是telnetd,还是bash端会处理这个ctrl-c?
实际上我们可以这么理解,在直接登录shell时,没有telnetd这一层,
应该是shell进程本身被ctrl-c键盘中断打断,
然后再驱动里通过tty收到了此特殊字符,然后在接收流程里,决定是否给当前进程shell发SIG信号。那么我们就认为telnetd不过是一个中转站,具体的数据处理还是要靠bash来完成。
因此当bash从pts/6收到ctrl-c特殊字符后,会进行特殊处理。来看这段代码:
首先,不管是伪终端,还是串口,亦或是控制台,接收到字符后都要推送给tty displine 的接收函数
n_tty_receive_buf做进一步处理。n_tty_receive_buf的数据来源是tty->buf。
这个tty->buf的来源,要么是伪终端主设备pty_write写入的数据,要么是中断往里面写入的数据。
如果是伪终端pty_write
static int pty_write(struct tty_struct *tty, const unsigned char *buf, int c)
{
//得到从设备
struct tty_struct *to = tty->link;
if (tty->stopped)
return 0;
if (c > 0) {
/* Stuff the data into the input queue of the other end */
//让从设备强行收数据,即将buf数据传递给从设备的tty->buf
c = tty_insert_flip_string(to, buf, c);
/* And shovel */
if (c) {
//将从设备的tty->buf临时缓冲数据提交给displine处理,即提交给tty->read_buf
//供用户空间使用
//这个一般是通过schedule_work来完成
tty_flip_buffer_push(to);
tty_wakeup(tty);
}
}
return c;
}
如果是串口中断,则是
serial8250_interrupt->serial8250_handle_port->receive_chars->
uart_insert_char->tty_insert_flip_char 将串口数据拷贝到tty->buf中之后,
再tty_flip_buffer_push(tty);将数据推送给displine处理
因此可以看出,串口和伪终端的不同之处在于,数据来源不同,一个是tty_write写入的,
一个是串口中断接受到的。
所以,我们在串口,或者是在telnet下,敲ctrl-c,实际上都会走到tty_flip_buffer_push,
进行数据的分析和接受。具体的特殊字符解析,应该也是在tty_flip_buffer_push之后的步骤完成。
那下面我们来看tty_flip_buffer_push
* This routine is called out of the software interrupt to flush data
* from the buffer chain to the line discipline.
最终调到到receive_buf->n_tty_receive_buf->n_tty_receive_break->
isig(SIGINT, tty, 1);->
if (tty->pgrp)
kill_pgrp(tty->pgrp, sig, 1);
还剩下tty的pgrp字段和kill_pgrp分析
待续