您的位置:首页 > 运维架构 > Linux

SVR4/4.3BSD与Linux对待伪终端的不同方式

2010-07-05 21:20 399 查看
打开伪终端意味着打开了一个“终端对”,这个终端对的其中一个是主终端,另一个是从终端,简单说主终端和类似sshd,telnetd等用户空间的远程协议处理进程连接,而从终端则和shell之类的实际进程连接,在处理远程登录的时候,一般都是由远程协议处理进程打开主终端和从终端,然后就在远程网络终端和本机shell之间建立了一条双向通道--“远程网络终端-(套接字)--本机协议处理进程--主终端--从终端--shell”,在这个“打开主从终端建立连接”的语义以及其实现上,有着不同的标准,总的来说有三种方式,分别是SVR4的方式,BSD的方式以及linux的方式,在“建立连接”的语义上SVR4的方式使用“流”来建立这条连接,而BSD和linux则是自动建立的,在“打开主从终端”的语义上,SVR4和linux是自动确定主终端并打开主终端后自动确定从终端,而BSD则必须手工确定和打开主终端,可见linux处理伪终端的方式是结合SVR4和BSD两种UNIX标准的结果,linux不仅实现这种有意义的最佳组合,而且分别实现了SRV和BSD的两种方式的接口,如果编译CONFIG_LEGACY_PTYS宏,则可以使用BSD的方式,如果编译CONFIG_UNIX98_PTYS,则实现SRV4的接口。

参见《unix环境高级编程》的第19章,在用户空间,SVR4的方式为:

int ptym_open(char *pts_name)

{

strcpy(pts_name, "/dev/ptmx"); //准备主终端的文件名字

fdm = open(pts_name, O_RDWR); //打开主终端

grantpt(fdm); //连接次终端

ptr = ptsname(fdm); //得到次终端的文件名字

strcpy(pts_name, ptr);

return fdm;

}

int ptys_open(int fdm, char *pts_name)

{

fds = open(pts_name, O_RDWR); //打开次终端

ioctl(fds, I_PUSH, "ptem"); //压入一个伪终端虚拟模块

ioctl(fds, I_PUSH, "ldterm"); //压入行规程模块

return fds;

}

而BSD的方式却是:

int ptym_open(char *pts_name)

{

...

//一个for循环,从/dev/ptyp0开始一直找到/dev/ptyTf为止,寻到第一个没有被使用的作为主终端,然后将对应的文件名的ptyXY中的p改为t,即ttyXY就是次终端

pts_name[5] = 't'; ///dev/ptyXY中的第6个元素就是p,现在改为t

return fdm;

}

int ptys_open(int fdm, char *pts_name)

{

fds = open(pts_name, O_RDWR); //打开次终端

//无需压入流模块,因为对于bsd来讲,其驱动程序是基于硬编码和clist的。

}

可见SRV4和BSD的方式根本不同,通过理解这种不同,我们也能更加明白/dev目录下的关于终端文件的命名规则了。关于流机制,BSD没有实现,可能是由于BSD自最初就没有SRV经过良好的规划吧,加之流的作者直接贡献流机制于SRV,那时unix已经分裂了。从终端的行规程以及任何终端的行规程之类其实也是一种协议,只是该协议主要规定人-机界面的规则,之所以将之称为行规程就是因为该规程在标准模式下限制了一次输入的结束就是一个换行符,这就是一个行规成协议,毕竟机器并不知道何时人们会输入完毕也就不能预先读取特定大小的数据块,而只能硬性规定一个特殊的字符作为输入结束,该特殊字符就是换行,类似tcp/ip协议,只是没有后者普遍罢了,使用压入流的方式,你可以轻易的堆积一个协议栈,只要将/dev/ip|udp|tcp等协议设备文件依次用ioctl的I_PUSH命令堆积即可。行规程压入了从终端并没有压入主终端,可见主终端仅仅起到一个数据中转的作用,主终端之所以不需要行规程,那是因为从终端可以处理直接从进程写入的数据或者说可以处理数据边界以及转义问题,这是无关紧要的,完全可以重新实现一个伪终端驱动,然后在主从终端都压入行规程模块,这样就显得更加对称了,正如linux后来实现的那样,虽然linux并没有显式地实现流机制,在linux中的伪终端tty的write函数中,主从终端都是统一一致的,如此一来在linux中,主从终端就更加像一对管道了,起码比SRV的要对称:

static int pty_write(...)

{

struct tty_struct *to = tty->link; //直接获取“一对的另一半”

...

to->ldisc.receive_buf(to, temp_buffer, NULL, n);//将数据放入另一半的缓冲区

...

}

如果看一下linux实现pty的源码,就会发现实际上pts使用的行规程是tty_ldisc_N_TTY,其receive_buf对数据其实并没有做太复杂的加工,因此这条管道并不复杂。

在打开一对终端方面,linux实现了两种方式,SRV4的方式和BSD的方式,总之,实现这两种接口是之前unix标准混战的结果。以SRV4的方式为例,linux中使用了一个/dev/ptmx设备文件,该设备文件只有一个却可以集中代表所有的主终端,任何sshd,telnetd之类的进程都可以只使用者一个终端设备文件,虽然设备文件是一个,但是由于内核中file数据结构是基于进程的,因此各个进程对该设备文件的引用却可以容纳不同的数据,包括不同的从终端。在ptmx_open中,不仅系统可以自动分配一个主终端,而且还为该主终端绑定了一个从终端,主终端设置到file结构体的private_data字段上,之后诸如sshd,telnetd之类的进程读写/dev/ptmx文件时,虽然它们读写的是同一个文件,可是由于file结构体不再它们之间共享,因此它们取到的file->private_data也就不同了:

static int ptmx_open(struct inode * inode, struct file * filp)

{

struct tty_struct *tty;

int index;

idr_pre_get(&allocated_ptys, GFP_KERNEL); //和BSD的实现不同,SRV的方式在内核中查找可用的主终端

idr_ret = idr_get_new(&allocated_ptys, NULL, &index); //得到新的项

...

retval = init_dev(ptm_driver, index, &tty);//此中实现终端对的相互link

filp->private_data = tty; //重要的赋值

devpts_pty_new(tty->link); //linux中的从终端设备文件是动态生成和删除的,因此linux使用了其强大的VFS机制,通过实现一个pts文件系统来支持这种动态的增删。

...

}

SRV4使用伪终端的方式是一对多的,ptmx集合了所有的主终端,同一个文件在不同的进程空间做区分,而BSD的方式却直接将多个一对一的终端对开放给接口调用者。在init_dev中,最为重要的一段是:

tty = alloc_tty_struct(); //分配主终端

initialize_tty_struct(tty); //初始化主终端的线路规程之类,包括些许工作队列

tty->driver = driver; //指定driver

tty->index = idx;

...

o_tty = alloc_tty_struct(); //分配从终端

initialize_tty_struct(o_tty); //初始化主终端的线路规程之类

o_tty->driver = driver->other; //pty_init的时候,主从driver都将other设置为对方

o_tty->index = idx;

tty_line_name(driver->other, idx, o_tty->name);

...

tty->link = o_tty; //连接彼此,从此以后两个终端一主一从构成一个包含很多控制功能的管道

o_tty->link = tty;

linux的sshd等进程在调用完/dev/ptmx的open之后,实际上一对终端就建立起来了,由于没有实现SRV4的流机制,因此不需要在用ioctl将更多的协议模块PUSH上去了,这个过程直接在init_dev中就做了,可以猜想一切基于SRV4标准实现的OS,其ptmx的open要简单的多,因此init_dev简单得多,不需要系统做任何事,后续的所有工作几乎全靠调用者来用ioctl完成,比linux更灵活,但是对于很多凡人来讲也更繁琐。

十分欣赏SRV的那种I_PUSH实现的将ip,icmp,udp等堆积成协议栈的方式--流机制,同时更乐于使用linux这种将一切都做好,但是还可以定制的OS。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: