欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

打开伪终端设备

程序员文章站 2022-07-01 09:14:31
...
    在伪终端概述一节中已对 PTY进行了初步的介绍。尽管 PTY 表现得就像物理终端设备一样,不过在打开 PTY 设备文件时,应用程序并不需要设置 O_TTY_INIT 标识(见不带缓冲的文件I/O之open)。各种平台打开 PTY 设备的方法有所不同,posix_openpt 函数提供了一种可移植的方法来打开下一个可用的伪终端主设备。
#include <stdlib.h>
#include <fcntl.h>
int posix_openpt(int oflag);
       /* 返回值:若成功,返回下一个可用的 PTY 主设备的文件描述符;否则,返回 -1 */

    参数 oflag 是一个位屏蔽字,指定如何打开主设备。它类似于 open 函数的 oflag 参数,但只支持指定 O_RDWR 来打开主设备进行读、写,或指定 O_NOCTTY 来防止主设备成为调用者的控制终端,其他打开标志都会导致未定义的行为。
    在伪终端从设备可用之前,它的权限必须设置,以便应用程序可以访问它。grantpt 函数可以把从设备节点的用户 ID 设置为调用者的实际用户 ID,设置其组 ID 为一非指定值,通常是可以访问该终端设备的组。权限被设置为 0620,即对个体所有者是读/写,对组所有者是写。实现通常将 PTY 从设备的组所有者设置为 tty 组。把那些要对系统中所有活动端具有写权限的程序(如 wall(1)和 write(1))的设置组 ID 设置为 tty 组,因为在 PTY 从设备上 tty 组的写权限是被允许的。
#include <stdlib.h>
int grantpt(int fd);
int unlockpt(int fd);
                          /* 两个函数的返回值:若成功,返回 0;否则,返回 -1 */
char *ptsname(int fd);
               /* 返回值:若成功,返回指向 PTY 从设备名的指针;否则,返回 NULL */

    这几个函数中的 fd 参数都是与伪终端主设备关联的文件描述符。
    为了更改从设备节点的权限,grantpt 可能需要 fork 并 exec 一个设置用户 ID 程序(如 Solaris 中是 /usr/lib/pt_chmod),因此调用者可能捕捉到行为未定义的 SIGCHLD 信号。
    unlockpt 函数用于准予对 PTY 从设备的访问,从而允许应用程序打开该设备。阻止其他进程打开从设备后,建立该设备的应用程序有机会在使用主、从设备之前正确地初始化这些设备。
    若给定了伪终端主设备的文件描述符,则可以用 ptsname 函数找到伪终端从设备的路径名(该名字可能存储在静态存储中,因此后续的调用可能会覆盖它)。
    为了方便处理相关细节,下面自定义了两个函数:ptym_open 和 ptys_open。ptym_open 打开下一个可用的 PTY 主设备。调用者必须分配一个数组来存放主设备或从设备的名字,并且如果调用成功,相应的从设备名会通过 pts_name 返回。然后将这个名字传给用来打开该从设备的 ptys_open 函数。缓冲区的字节长度由 pts_namesz 传送,使得 ptym_open 不会复制比该缓冲区长的字符串。不过一般不直接调用这两个函数,而是由另一个自定义的函数 pty_fork 来调用,并且还会 fork 出一个子进程。一个进程调用 ptym_open 来打开一个主设备并得到从设备名,然后 fork 子进程。子进程在调用 setsid 建立新的会话后调用 ptys_open 打开从设备。这就是从设备如何成为子进程控制终端的过程。
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <termios.h>
#include <sys/ioctl.h>

int ptym_open(char *pts_name, int pts_namesz){
	int fdm = posix_openpt(O_RDWR);
	if(fdm < 0)
		return -1;
	if(grantpt(fdm) < 0)		// grant access to slave
		goto errout;
	if(unlockpt(fdm) < 0)		// clear slave's lock flag
		goto errout;
	char *ptr = ptsname(fdm);	// get slave's name
	if(ptr == NULL)
		goto errout;
	strncpy(pts_name, ptr, pts_namesz);	// save name of slave
	pts_name[pts_namesz - 1] = '\0';
	return fdm;

errout:
	int err = errno;
	close(fdm);
	errno = err;
	return -1;
}

int ptys_open(char *pts_name){
	return open(pts_name, O_RDWR);
}

pid_t pty_fork(int *ptyfdm, char *slave_name, int slave_namesz,
				const struct termios *slave_termios,
				const struct winsize *slave_winsize){
	char pts_name[20];			
	int fdm = ptym_open(pts_name, sizeof(pts_name));
	if(fdm < 0){
		printf("can't open master pty: %s, error %d\n", pts_name, fdm);
		exit(1);
	}
	if(slave_name != NULL){
		strncpy(slave_name, pts_name, slave_namesz);
		slave_name[slave_namesz-1] = '\0';
	}
	pid_t pid = fork();
	if(pid < 0)
		return -1;
	if(pid > 0){			// parent
		*ptyfdm = fdm;		// save fd of master
		return pid;		// parent return pid of child
	}
	close(fdm);			// all done with master in child
	if(setsid() < 0){
		printf("setsid error\n");
		exit(1);
	}
	/* Linux/Solaris acquires controlling terminal on open(). */
	int fds = ptys_open(pts_name);
	if(fds < 0){
		printf("can't open slave pty\n");
		exit(1);
	}
#if defined(BSD)
	/* TIOCSCTTY is the BSD way to acquire a controlling terminal. */
	if(ioctl(fds, TIOCSCTTY, (char *)0) < 0){
		printf("TIOCSCTTY error\n");
		exit(1);
	}
#endif
	if(slave_termios != NULL)
		tcsetattr(fds, TCSANOW, slave_termios);
	if(slave_winsize != NULL)
		ioctl(fds, TIOCSWINSZ, slave_winsize);
	/* Slave becomes stdin/stdout/stderr of child. */
	dup2(fds, STDIN_FILENO);
	dup2(fds, STDOUT_FILENO);
	dup2(fds, STDERR_FILENO);
	if(fds!=STDIN_FILENO && fds!=STDOUT_FILENO && fds!=STDERR_FILENO){
		printf("dup2 error to stdin/stdout/stderr\n");
		close(fds);
	}
	return 0;			// child returns 0 just like fork()
}

    在 pty_fork 函数中,PTY 主设备的文件描述符通过参数 ptrfdm 指针返回。如果参数 slave_name 不为空,则从设备名被存储在该指针指向的由调用者分配的存储区中。如果 slave_termios 不为空,则系统使用该指针引用的结构初始化从设备的终端行规程,否则系统会把从设备的 termios 结构设置成实现定义的初始状态。类似地,如果 slave_winsize 指针不为空,则按该指针引用的结构初始化从设备的窗口大小,否则 winsize 结构(见终端窗口大小和 termcap)一般初始化为 0。我们还把从设备的文件描述符复制给了子进程的标准输入、标准输出和标准错误。这意味着以后不管子进程 exec 何种程序,它都具有同 PTY 从设备联系起来的这 3 个描述符。如果该函数成功执行,它会在子进程中返回 0,在父进程中返回子进程的进程 ID,否则返回 -1。