Linux下的console和terminal

consoleterminal是很容易让人迷惑的两个概念。根据wikipedia上的定义,小型计算机的console应该就是键盘加显示器;而terminal则是输入数据进去,和显示数据来源的设备,通常是一个计算机系统。

Linux下的console除了真实的硬件设备外,还有virtual console,也就是你按alt+Fn或者alt+ctrl+Fn切换到的东西。所谓虚拟就是这些console共享同一个真实的设备,只有一个活动的console才显示在前面。这些console对应的设备是:/dev/ttyN,其中1 ≤ N ≤ 63。而/dev/tty0则是指向当前的terminal;/dev/console是指向当前console,但它现在并不是对/dev/tty0的符号链接。更多可参考console(4)。

/dev/tty是另一个特殊设备,它指向控制终端(controlling terminal)。如果某个进程的控制终端是/dev/tty3,那么/dev/tty就指向/dev/tty3了。控制终端是什么概念?它是一个进程的某个属性,是依附带该进程上的终端。比如我们在某个终端下输入ctrl+C,那么它控制的前台进程就会收到SIGINT,而后台进程会收到SIGTTIN或SIGTTOU ,如果它们读写该终端的话。被同一个终端控制的所有进程被称为一个会话(session),会话的领导就是创建改会话的进程,其子进程也会被该终端控制。所以,1) 需要交互的命令行程序通常会从/dev/tty这个设备进行读写;2) Unix后台进程都需要在fork之后调用setsid(2),3) 需要加O_NOCTTY,当你open一个可能是终端的文件时。

另外,想要确定/dev/tty究竟是指向哪个设备,可以调用TIOCCONS ioctl。参考tty(4)。

下面是另外一个概念——伪终端(pseudo-terminal),根据pty(7)的介绍,伪终端一对虚拟设备,提供端到端双向通信的通路,一端称为master,另一端称为slave。在slave那端看到的和在真实终端看到的效果一样。所以伪终端一般被ssh等网络登录程序使用。历史上,有两套伪终端接口,一个是Unix 98伪终端,另一个是BSD伪终端。

BSD提供的接口很简单:/dev/pty[p-za-e][0-9a-f] 是master; /dev/tty[p-za-e][0-9a-f] 是slave,它们都是配好对的。这样看起来很简单,但对程序员来说不容易,要找到一个合适的终端需要一个个从头尝试。所以这种方式已经被遗弃。而Unix 98伪终端则完全不同,它始终使用/dev/ptmx作为master复制设备,然后在每次打开它的时候才得到一个master设备的fd,同时在/dev/pts/目录下得到一个slave设备。这样编程就相对容易了,根据pts(4)介绍,需要三个新的API: ptsname(3),grantpt(3)和unlockpt(3)。我们可以通过一个实例看一下如何使用:

(以下代码摘自netvirt)
[c]
char mptname = “/dev/ptmx”; / master pseudo-tty device */
//…
void
getmaster()
{
struct stat stb;

if ((master = open(mptname, O_RDWR)) >= 0) { /* a pseudo-tty is free */
    (void) ioctl(0, TCGETS, (char *)&b);
    (void) ioctl(0, TIOCGWINSZ, (char *)&size);
    return;
} else {                /* out of pseudo-tty's */
    perror(mptname);
    fprintf(stderr, gettext("Out of pseudo-tty'sn"));
    fail();
}

}

void
getslave()
{
char slavename; / name of slave pseudo-tty */

grantpt(master);        /* change permissions of slave */
unlockpt(master);            /* unlock slave */
slavename = ptsname(master);        /* get name of slave */
slave = open(slavename, O_RDWR);    /* open slave */
if (slave < 0) {            /* error on opening slave */
    perror(slavename);
    fail();
}
ioctl(slave, I_PUSH, "ptem");    /* push pt hw emulation module */
ioctl(slave, I_PUSH, "ldterm");        /* push line discipline */

(void) ioctl(slave, TCSETSF, (char *)&b);
(void) ioctl(slave, TIOCSWINSZ, (char *)&size);

}
[/c]
然后我们再来看一下glibc中对ptsname(3)的实现:

(源文件sysdeps/unix/sysv/linux/ptsname.c)
[c]

define _PATH_DEVPTS "/dev/pts/"

char *
ptsname (int fd)
{
return __ptsname_r (fd, buffer, sizeof (buffer)) != 0 ? NULL : buffer;
}

int
__ptsname_r (int fd, char *buf, size_t buflen)
{

if (ioctl (fd, TIOCGPTN, &ptyno) == 0)

numbuf[sizeof (numbuf) - 1] = '';
p = _itoa_word (ptyno, &numbuf[sizeof (numbuf) - 1], 10, 0);

memcpy (
stpcpy (buf, devpts), p, &numbuf[sizeof (numbuf)] - p);
[/c]

我们可以看出,实际上是调用ioctl TIOCGPTN,通过内核,而Linux内核又是通过devpts这种文件系统实现了这一切:

$ mount | grep devpts
devpts on /dev/pts type devpts (rw,gid=5,mode=620)

这样我们终于把一切搞清楚了。:-)