19.3. Opening Pseudo-Terminal DevicesThe way we open a pseudo-terminal device differs among platforms. The Single UNIX Specification includes several functions as XSI extensions in an attempt to unify the methods. These extensions are based on the functions originally provided to manage STREAMS-based pseudo terminals in System V Release 4. The posix_openpt function is provided as a portable way to open an available pseudo-terminal master device.
The oflag argument is a bitmask that specifies how the master device is to be opened, similar to the same argument used with open(2). Not all open flags are supported, however. With posix_openpt, we can specify O_RDWR to open the master device for reading and writing, and we can specify O_NOCTTY to prevent the master device from becoming a controlling terminal for the caller. All other open flags result in unspecified behavior. Before a slave pseudo-terminal device can be used, its permissions need to be set so that it is accessible to applications. The grantpt function does just this. It sets the user ID of the slave's device node to be the caller's real user ID and sets the node's group ID to an unspecified value, usually some group that has access to terminal devices. The permissions are set to allow read and write access to individual owners and write access to group owners (0620).
To change permission on the slave device node, grantpt might need to fork and exec a set-user-ID program (/usr/lib/pt_chmod on Solaris, for example). Thus, the behavior is unspecified if the caller is catching SIGCHLD. The unlockpt function is used to grant access to the slave pseudo-terminal device, thereby allowing applications to open the device. By preventing others from opening the slave device, applications setting up the devices have an opportunity to initialize the slave and master devices properly before they can be used. Note that in both grantpt and unlockpt, the file descriptor argument is the file descriptor associated with the master pseudo-terminal device. The ptsname function is used to find the pathname of the slave pseudo-terminal device, given the file descriptor of the master. This allows applications to identify the slave independent of any particular conventions that might be followed by a given platform. Note that the name returned might be stored in static memory, so it can be overwritten on successive calls.
Figure 19.7 summarizes the pseudo-terminal functions in the Single UNIX Specification and indicates which functions are supported by the platforms discussed in this text.
Even though the Single UNIX Specification has tried to improve portability in this area, implementations are still catching up, as illustrated by Figure 19.7. Thus, we provide two functions that handle all the details: ptym_open to open the next available PTY master device and ptys_open to open the corresponding slave device.
Normally, we don't call these two functions directly; the function pty_fork (Section 19.4) calls them and also forks a child process. The ptym_open function determines the next available PTY master and opens the device. The caller must allocate an array to hold the name of either the master or the slave; if the call succeeds, the name of the corresponding slave is returned through pts_name. This name is then passed to ptys_open, which opens the slave device. The length of the buffer in bytes is passed in pts_namesz so that the ptym_open function doesn't copy a string that is longer than the buffer. The reason for providing two functions to open the two devices will become obvious when we show the pty_fork function. Normally, a process calls ptym_open to open the master and obtain the name of the slave. The process then forks, and the child calls ptys_open to open the slave after calling setsid to establish a new session. This is how the slave becomes the controlling terminal for the child. 19.3.1. STREAMS-Based Pseudo TerminalsThe details of the STREAMS implementation of pseudo terminals under Solaris are covered in Appendix C of Sun Microsystems [2002]. The next available PTY master device is accessed through a STREAMS clone device. A clone device is a special device that returns an unused device when it is opened. (STREAMS clone opens are discussed in detail in Rago [1993].) The STREAMS-based PTY master clone device is /dev/ptmx. When we open it, the clone open routine automatically determines the first unused PTY master device and opens that unused device. (We'll see in the next section that, under BSD-based systems, we have to find the first unused PTY master ourselves.) Figure 19.8. STREAMS-based pseudo-terminal open functions#include "apue.h" #include <errno.h> #include <fcntl.h> #include <stropts.h> int ptym_open(char *pts_name, int pts_namesz) { char *ptr; int fdm; /* * Return the name of the master device so that on failure * the caller can print an error message. Null terminate * to handle case where strlen("/dev/ptmx") > pts_namesz. */ strncpy(pts_name, "/dev/ptmx", pts_namesz); pts_name[pts_namesz - 1] = '\0'; if ((fdm = open(pts_name, O_RDWR)) < 0) return(-1); if (grantpt(fdm) < 0) { /* grant access to slave */ close(fdm); return(-2); } if (unlockpt(fdm) < 0) { /* clear slave's lock flag */ close(fdm); return(-3); } if ((ptr = ptsname(fdm)) == NULL) { /* get slave's name */ close(fdm); return(-4); } /* * Return name of slave. Null terminate to handle * case where strlen(ptr) > pts_namesz. */ strncpy(pts_name, ptr, pts_namesz); pts_name[pts_namesz - 1] = '\0'; return(fdm); /* return fd of master */ } int ptys_open(char *pts_name) { int fds, setup; /* * The following open should allocate a controlling terminal. */ if ((fds = open(pts_name, O_RDWR)) < 0) return(-5); /* * Check if stream is already set up by autopush facility. */ if ((setup = ioctl(fds, I_FIND, "ldterm")) < 0) { close(fds); return(-6); } if (setup == 0) { if (ioctl(fds, I_PUSH, "ptem") < 0) { close(fds); return(-7); } if (ioctl(fds, I_PUSH, "ldterm") < 0) { close(fds); return(-8); } if (ioctl(fds, I_PUSH, "ttcompat") < 0) { close(fds); return(-9); } } return(fds); } We first open the clone device /dev/ptmx to get a file descriptor for the PTY master. Opening this master device automatically locks out the corresponding slave device. We then call grantpt to change permissions of the slave device. On Solaris, it changes the ownership of the slave to the real user ID, changes the group ownership to the group tty, and changes the permissions to allow only user-read, user-write, and group-write. The reason for setting the group ownership to tty and enabling group-write permission is that the programs wall(1) and write(1) are set-group-ID to the group tty. Calling grantpt executes the program /usr/lib/pt_chmod, which is set-user-ID to root so that it can modify the ownership and permissions of the slave. The function unlockpt is called to clear an internal lock on the slave device. We have to do this before we can open the slave. Additionally, we must call ptsname to obtain the name of the slave device. This name is of the form /dev/pts/NNN. The next function in the file is ptys_open, which does the actual open of the slave device. Solaris follows the historical System V behavior: if the caller is a session leader that does not already have a controlling terminal, this call to open allocates the PTY slave as the controlling terminal. If we didn't want this to happen, we could specify the O_NOCTTY flag for open. After opening the slave device, we might need to push three STREAMS modules onto the slave's stream. Together, the pseudo terminal emulation module (ptem) and the terminal line discipline module (ldterm) act like a real terminal. The ttcompat module provides compatibility for older V7, 4BSD, and Xenix ioctl calls. It's an optional module, but since it's automatically pushed for console logins and network logins (see the output from the program shown in Figure 14.18), we push it onto the slave's stream. The reason that we might not need to push these three modules is that they might be there already. The STREAMS system supports a facility known as autopush, which allows an administrator to configure a list of modules to be pushed onto a stream whenever a particular device is opened (see Rago [1993] for more details). We use the I_FIND ioctl command to see whether ldterm is already on the stream. If so, we assume that the stream has been configured by the autopush mechanism and avoid pushing the modules a second time. The result of calling ptym_open and ptys_open is two file descriptors open in the calling process: one for the master and one for the slave. 19.3.2. BSD-Based Pseudo TerminalsUnder BSD-based systems and Linux-based systems, we provide our own versions of the XSI functions, which we can optionally include in our library, depending on which functions (if any) are provided by the underlying platform. In our version of posix_openpt, we have to determine the first available PTY master device. To do this, we start at /dev/ptyp0 and keep trying until we successfully open a PTY master or until we run out of devices. We can get two different errors from open: EIO means that the device is already in use; ENOENT means that the device doesn't exist. In the latter case, we can terminate the search, as all pseudo terminals are in use. Once we are able to open a PTY master, say /dev/ptyMN, the name of the corresponding slave is /dev/ttyMN. On Linux, if the name of the PTY master is /dev/pty/mXX, then the name of the corresponding PTY slave is /dev/pty/sXX. In our version of grantpt, we call chown and chmod but realize that these two functions won't work unless the calling process has superuser permissions. If it is important that the ownership and protection be changed, these two function calls need to be placed into a set-user-ID root executable, similar to the way Solaris implements it. The function ptys_open in Figure 19.9 simply opens the slave device. No other initialization is necessary. The open of the slave PTY under BSD-based systems does not have the side effect of allocating the device as the controlling terminal. In Section 19.4, we'll see how to allocate the controlling terminal under BSD-based systems. Figure 19.9. Pseudo-terminal open functions for BSD and Linux#include "apue.h" #include <errno.h> #include <fcntl.h> #include <grp.h> #ifndef _HAS_OPENPT int posix_openpt(int oflag) { int fdm; char *ptr1, *ptr2; char ptm_name[16]; strcpy(ptm_name, "/dev/ptyXY"); /* array index: 0123456789 (for references in following code) */ for (ptr1 = "pqrstuvwxyzPQRST"; *ptr1 != 0; ptr1++) { ptm_name[8] = *ptr1; for (ptr2 = "0123456789abcdef"; *ptr2 != 0; ptr2++) { ptm_name[9] = *ptr2; /* * Try to open the master. */ if ((fdm = open(ptm_name, oflag)) < 0) { if (errno == ENOENT) /* different from EIO */ return(-1); /* out of pty devices */ else continue; /* try next pty device */ } return(fdm); /* got it, return fd of master */ } } errno = EAGAIN; return(-1); /* out of pty devices */ } #endif #ifndef _HAS_PTSNAME char * ptsname(int fdm) { static char pts_name[16]; char *ptm_name; ptm_name = ttyname(fdm); if (ptm_name == NULL) return(NULL); strncpy(pts_name, ptm_name, sizeof(pts_name)); pts_name[sizeof(pts_name) - 1] = '\0'; if (strncmp(pts_name, "/dev/pty/", 9) == 0) pts_name[9] = 's'; /* change /dev/pty/mXX to /dev/pty/sXX */ else pts_name[5] = 't'; /* change "pty" to "tty" */ return(pts_name); } #endif #ifndef _HAS_GRANTPT int grantpt(int fdm) { struct group *grptr; int gid; char *pts_name; pts_name = ptsname(fdm); if ((grptr = getgrnam("tty")) != NULL) gid = grptr->gr_gid; else gid = -1; /* group tty is not in the group file */ /* * The following two calls won't work unless we're the superuser. */ if (chown(pts_name, getuid(), gid) < 0) return(-1); return(chmod(pts_name, S_IRUSR | S_IWUSR | S_IWGRP)); } #endif #ifndef _HAS_UNLOCKPT int unlockpt(int fdm) { return(0); /* nothing to do */ } #endif int ptym_open(char *pts_name, int pts_namesz) { char *ptr; int fdm; /* * Return the name of the master device so that on failure * the caller can print an error message. Null terminate * to handle case where string length > pts_namesz. */ strncpy(pts_name, "/dev/ptyXX", pts_namesz); pts_name[pts_namesz - 1] = '\0'; if ((fdm = posix_openpt(O_RDWR)) < 0) return(-1); if (grantpt(fdm) < 0) { /* grant access to slave */ close(fdm); return(-2); } if (unlockpt(fdm) < 0) { /* clear slave's lock flag */ close(fdm); return(-3); } if ((ptr = ptsname(fdm)) == NULL) { /* get slave's name */ close(fdm); return(-4); } /* * Return name of slave. Null terminate to handle * case where strlen(ptr) > pts_namesz. */ strncpy(pts_name, ptr, pts_namesz); pts_name[pts_namesz - 1] = '\0'; return(fdm); /* return fd of master */ } int ptys_open(char *pts_name) { int fds; if ((fds = open(pts_name, O_RDWR)) < 0) return(-5); return(fds); }
19.3.3. Linux-Based Pseudo TerminalsLinux supports the BSD method for accessing pseudo terminals, so the same functions shown in Figure 19.9 will also work on Linux. However, Linux also supports a clone-style interface to pseudo terminals using /dev/ptmx (but this is not a STREAMS device). The clone interface requires extra steps to identify and unlock a slave device. The functions we can use to access these pseudo terminals on Linux are shown in Figure 19.10. Figure 19.10. Pseudo-terminal open functions for Linux#include "apue.h" #include <fcntl.h> #ifndef _HAS_OPENPT int posix_openpt(int oflag) { int fdm; fdm = open("/dev/ptmx", oflag); return(fdm); } #endif #ifndef _HAS_PTSNAME char * ptsname(int fdm) { int sminor; static char pts_name[16]; if (ioctl(fdm, TIOCGPTN, &sminor) < 0) return(NULL); snprintf(pts_name, sizeof(pts_name), "/dev/pts/%d", sminor); return(pts_name); } #endif #ifndef _HAS_GRANTPT int grantpt(int fdm) { char *pts_name; pts_name = ptsname(fdm); return(chmod(pts_name, S_IRUSR | S_IWUSR | S_IWGRP)); } #endif #ifndef _HAS_UNLOCKPT int unlockpt(int fdm) { int lock = 0; return(ioctl(fdm, TIOCSPTLCK, &lock)); } #endif int ptym_open(char *pts_name, int pts_namesz) { char *ptr; int fdm; /* * Return the name of the master device so that on failure * the caller can print an error message. Null terminate * to handle case where string length > pts_namesz. */ strncpy(pts_name, "/dev/ptmx", pts_namesz); pts_name[pts_namesz - 1] = '\0'; fdm = posix_openpt(O_RDWR); if (fdm < 0) return(-1); if (grantpt(fdm) < 0) { /* grant access to slave */ close(fdm); return(-2); } if (unlockpt(fdm) < 0) { /* clear slave's lock flag */ close(fdm); return(-3); } if ((ptr = ptsname(fdm)) == NULL) { /* get slave's name */ close(fdm); return(-4); } /* * Return name of slave. Null terminate to handle case * where strlen(ptr) > pts_namesz. */ strncpy(pts_name, ptr, pts_namesz); pts_name[pts_namesz - 1] = '\0'; return(fdm); /* return fd of master */ } int ptys_open(char *pts_name) { int fds; if ((fds = open(pts_name, O_RDWR)) < 0) return(-5); return(fds); } On Linux, the PTY slave device is already owned by group tty, so all we need to do in grantpt is ensure that the permissions are correct. |