简介

ptmx,pts pseudo terminal master and slave
ptmx与pts配合实现pty(伪终端)
在telnet,ssh等远程终端工具中会使用到pty,通常的数据流是这样的
​​​telnetd进程 ---> /dev/ptmx(master) ---> /dev/pts/?(slave) ---> getty​​​
telnetd进程收到网络中的数据后,将数据丢给ptmx,ptmx像管道一样将数据丢给pts/?,getty进程从pts/?读取数据传递给shell去执行。

linux支持的两种pty
a. UNIX98 pseudoterminal,使用的是devpts文件系统,挂载在/dev /pts目录
b. 在UNIX98 pseudoterminal之前,master pseudoterminal名字为/dev/ptyp0,…,slave pseudoterminal名字为/dev/ttyp0,…,这个方法需要预先分配好很多的设备节点。

只有在open /dev/ptmx程序不退出的情况下,/dev/pts/目录下才会有对应的设备节点
在程序执行”open /dev/ptmx”的时候会在/dev/pts/目录下生成一个设备节点,比如0,1…,但是当程序退出的时候这个设备节点就消失了。可以通过如下一个例子演示在”open /dev/ptmx”的时候在/dev/pts目录下生成的设备节点

$ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx

可见在重定向/dev/ptmx的时候在/dev/pts目录下多了个设备节点3,而当上面这个shell结束的时候再次ls /dev/pts目录,设备节点3又消失了。


程序

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pty.h>

int main()
{
int fd_m, fd_s;
int len;
const char *pts_name;
char send_buf[64] = "abc\ndefghijk\nlmn";
char recv_buf[64] = {0};

fd_m = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (fd_m < 0) {
printf("open /dev/ptmx fail\n");
return -1;
}

if (grantpt(fd_m) < 0 || unlockpt(fd_m) < 0) {
printf("grantpt and unlockpt fail\n");
goto err;
}

pts_name = (const char *)ptsname(fd_m);
fd_s = open(pts_name, O_RDONLY | O_NOCTTY);
if (fd_s < 0) {
printf("open /dev/ptmx fail\n");
goto err;
}

len = write(fd_m, send_buf, strlen(send_buf));
printf("write len=%d\n", len);

len = read(fd_s, recv_buf, sizeof(recv_buf));
printf("read len=%d, recv_buf=[%s]\n", len, recv_buf);

len = read(fd_s, recv_buf, sizeof(recv_buf));
printf("read len=%d, recv_buf=[%s]\n", len, recv_buf);

close(fd_m);
close(fd_s);
return 0;

err:
if (fd_m)
close(fd_m);
if (fd_s)
close(fd_s);

return -1;
}

上面这段程序的输出如下:

ptmx/pts_#include


read只有遇到换行符’\n’的时候才会返回,否则遇不到的话一直阻塞在那里。

每open /dev/ptmx就会得到一个新的文件描述符,并且在/dev/pts/目录下生成一个与这个文件描述符对应的新的设备节点
当进程open “/dev/ptmx”的时候,获得了一个新的pseudoterminal master(PTM)的文件描述符,同时会在/dev/pts目录下自动生成一个新的pseudoterminal slave(PTS)设备。每次open “/dev/ptmx”会得到一个不同的PTM文件描述符(多次open会得到多个文件描述符),并且有和这个PTM描述符关联的PTS。

grantpt, unlockpt: 在每次打开pseudoterminal slave的时候,必须传递对应的PTM的文件描述符。grantpt以获得权限,然后调用unlockpt解锁
ptsname: 将PTM的文件描述符作为参数,会得到该描述符对应的PTS的路径

向PTM写的数据可以从PTS读出来,向PTS写的数据可以从PTM读出来。


原理

对ptmx执行open操作,将创建一对tty主从设备

tty_init
cdev_init(&ptmx_cdev, &ptmx_fops);//创建了/dev/ptmx设备节点
//此时/dev/ptmx设备节点的open函数为ptmx_fops.ptmx_open()

static int ptmx_open(struct inode *inode, struct file *filp)
{
...
idr_ret = idr_get_new(&allocated_ptys, NULL, &index);
...
//NR_UNIX98_PTY_DEFAULT也就是4096个
if (index >= pty_limit) {
...
}
...
mutex_lock(&tty_mutex);
//以index为pts的设备索引号,创建成对的主从设备ptmx和pts
retval = init_dev(ptm_driver, index, &tty);
mutex_unlock(&tty_mutex);
...
retval = ptm_driver->open(tty, filp);
...

所以在​​fd_m = open("/dev/ptmx", O_RDWR)​​操作之后,将产生一个成对的ptmx和pts主从pty设备,并返回ptmx对应的文件描述符。

调用ioctl获得与ptmx对应的pts的设备节点

tty_ioctl
tty->driver->ioctl
//在unix98_pty_init()中,仅对ptmx主设备赋予了ioctl操作,`ptm_driver->ioctl =pty_unix98_ioctl;`
pty_unix98_ioctl

static int pty_unix98_ioctl(struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg)
{
switch (cmd) {
case TIOCSPTLCK: /* Set PT Lock (disallow slave open) */
return pty_set_lock(tty, (int __user *)arg);
case TIOCGPTN: /* Get PT Number */
//当前tty对应的为ptmx结构,它的index就是与之配对的pts
return put_user(tty->index, (unsigned int __user *)arg);
}

return -ENOIOCTLCMD;
}

看看glibc库中如何封装ptsname函数

char* ptsname( int fd )
{
unsigned int pty_num;
static char buff[64];

//最终调用上面的pty_unix98_ioctl获取当前ptmx主设备对应的pty从设备号.
if ( ioctl( fd, TIOCGPTN, &pty_num ) != 0 )
return NULL;

//格式化为/dev/pts/0,/dev/pts/1等,即:pts对应的文件全路径.
snprintf( buff, sizeof(buff), "/dev/pts/%u", pty_num );
return buff;
}

adb中遇到的场景

我在移植adb到linux平台的时候,涉及到pts、ptmx,需要在内核中配置如下:

Device driver`
Character devices
[*]Enable TTY
[*]Unix98 PTY support
[*]Support multiple instances of devpts
Config busybox setting
Busybox Setting
General Configuration
Use the devpts filesystem for unix98 PTYs

选项选择好编译,再次去除这些选项之后再编译会出问题,报如下错误:

WARNING: arch/rlx/bsp/built-in.o(.text+0x4): Section mismatch in reference from the function >disable_early_printk() to the variable .init.data:promcons_output
The function disable_early_printk() references
the variable __initdata promcons_output.
This is often because disable_early_printk lacks a __initdata
annotation or the annotation of promcons_output is wrong.

WARNING: arch/rlx/bsp/built-in.o(.text+0x10): Section mismatch in reference from the function >disable_early_printk() to the variable .init.data:promcons_output
The function disable_early_printk() references
the variable __initdata promcons_output.
This is often because disable_early_printk lacks a __initdata
annotation or the annotation of promcons_output is wrong.

WARNING: arch/rlx/bsp/built-in.o(.data+0x1710): Section mismatch in reference from the variable >rts1_camera_device to the variable .init.rodata:rts1_soc_camera_pdata
The variable rts1_camera_device references
the variable __initconst rts1_soc_camera_pdata
If the reference is valid then annotate the
variable with __init* or __refdata (see linux/init.h) or name the variable:
_template, _timer, _sht, _ops, _probe, _probe_one, *_console

解决办法:
不用去修改这些文件的错误,虽然这些文件本身编译可能有问题。
可以这么做,在busybox和linux内核目录下做make clean和make distclean
然后再重新编译。
实际上后来发现,busybox中不需要配置,只要在内核中配置就行了。

内核配置并且编译之后,系统支持pts
进入终端执行:mount -t devpts none /dev/pts
如果没有/dev/pts这个目录的话,执行mkdir -p /dev/pts创建一个目录


参考文章

  1. ​ptmx(4) - Linux man page​
  2. ​Linux中的伪终端编程​
  3. ​​浅析terminal创建时ptmx和pts关系​​
  4. ​linux-3.14/Documentation/filesystems/devpts.txt​
  5. 5.