打开伪终端设备
程序员文章站
2022-07-01 09:14:31
...
在伪终端概述一节中已对 PTY进行了初步的介绍。尽管 PTY 表现得就像物理终端设备一样,不过在打开 PTY 设备文件时,应用程序并不需要设置 O_TTY_INIT 标识(见不带缓冲的文件I/O之open)。各种平台打开 PTY 设备的方法有所不同,posix_openpt 函数提供了一种可移植的方法来打开下一个可用的伪终端主设备。
参数 oflag 是一个位屏蔽字,指定如何打开主设备。它类似于 open 函数的 oflag 参数,但只支持指定 O_RDWR 来打开主设备进行读、写,或指定 O_NOCTTY 来防止主设备成为调用者的控制终端,其他打开标志都会导致未定义的行为。
在伪终端从设备可用之前,它的权限必须设置,以便应用程序可以访问它。grantpt 函数可以把从设备节点的用户 ID 设置为调用者的实际用户 ID,设置其组 ID 为一非指定值,通常是可以访问该终端设备的组。权限被设置为 0620,即对个体所有者是读/写,对组所有者是写。实现通常将 PTY 从设备的组所有者设置为 tty 组。把那些要对系统中所有活动端具有写权限的程序(如 wall(1)和 write(1))的设置组 ID 设置为 tty 组,因为在 PTY 从设备上 tty 组的写权限是被允许的。
这几个函数中的 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 打开从设备。这就是从设备如何成为子进程控制终端的过程。
在 pty_fork 函数中,PTY 主设备的文件描述符通过参数 ptrfdm 指针返回。如果参数 slave_name 不为空,则从设备名被存储在该指针指向的由调用者分配的存储区中。如果 slave_termios 不为空,则系统使用该指针引用的结构初始化从设备的终端行规程,否则系统会把从设备的 termios 结构设置成实现定义的初始状态。类似地,如果 slave_winsize 指针不为空,则按该指针引用的结构初始化从设备的窗口大小,否则 winsize 结构(见终端窗口大小和 termcap)一般初始化为 0。我们还把从设备的文件描述符复制给了子进程的标准输入、标准输出和标准错误。这意味着以后不管子进程 exec 何种程序,它都具有同 PTY 从设备联系起来的这 3 个描述符。如果该函数成功执行,它会在子进程中返回 0,在父进程中返回子进程的进程 ID,否则返回 -1。
#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。
上一篇: 波特率和行控制函数