The Design and Implementation of the FreeBSD Operating System

   

10.8. Terminal Operations

Now that we have examined the overall structure of the terminal I/O system and have described that system's data structures, as well as the hardware that the system controls, we continue with a description of the terminal I/O system operation. We examine the operation of the pseudo-terminal device driver and the default termios line discipline.

Open

Each time that the master side of a previously unused pseudo-terminal device is opened, the pseudo-terminal driver's open routine is called. The open routine initializes the tty structure; sets the default modes; sets the terminal-size information for the line to zero, indicating an unknown size; and sets the state to TS ISOPEN. The driver then passes control to the line discipline through its open entry. The setup of the terminal completes when the corresponding slave side of the pseudo-terminal is opened by the application that will be using it.

For hardware devices that support modem-control lines, the open routine takes some additional steps. It starts by enabling the DTR output line. If the CLOCAL control flag is not set for the terminal and the open call did not specify the O_NONBLOCK flag, the open routine blocks awaiting assertion of the DCD input line. Some drivers support device flags to override modem control; these flags are set in the system-configuration file and are stored in the driver data structures. If the bit corresponding to a terminal line number is set in a device's flags, modem-control lines are ignored on input. When a carrier signal is detected on the line, the TS_CARR_ON bit is set in the terminal state. The line is then marked as open (state bit TS_OPEN), and the driver then passes control to the line discipline as just described for pseudo-terminals.

Output Line Discipline

After a line has been opened, a write on the resulting file descriptor produces output to be transmitted on the terminal. Writes to character devices result in calls to the device write entry, d_write, with a device pointer, a uio structure describing the data to be written, and a flag specifying whether the I/O is nonblocking. The pseudo-terminal driver uses the device pointer to locate the correct tty structure and then call the line discipline l_write entry with the tty structure and uio structure as parameters.

The line-discipline write routine does most of the work of output translation and flow control. It is responsible for copying data into the kernel from the user process and for placing the translated data onto the pseudo-terminal's output queue. The terminal-driver write routine, ttwrite(), first checks whether the current process is allowed to write to the terminal at this time. The user may set a tty option to allow only the foreground process (see Section 10.5) to do output. If this option is set, and if the terminal line is the controlling terminal for the process, then the process should do output immediately only if it is in the foreground process group (i.e., if the process groups of the process and of the terminal are the same). If the process is not in the foreground process group, and a SIGTTOU signal would cause the process to be suspended, a SIGTTOU signal is sent to the process group of the process. Here, the write will be attempted again when the user moves the process group to the foreground. If the process is in the foreground process group, or a SIGTTOU signal would not suspend the process, the write proceeds as usual.

When ttwrite() has confirmed that the write is permitted, it enters a loop that copies the data to be written into the kernel, checks for any output translation that is required, and places the data on the output queue for the terminal. It prevents the queue from becoming overfull by blocking if the queue fills before all characters have been processed. The limit on the queue size, the high watermark, is dependent on the output line speed; for pseudo-terminals the line speed is set to the maximum baud rate so that they will get the maximum high watermark of several thousand characters. The low watermark is set to about half of the high watermark. When forced to wait for output to drain before proceeding, ttwrite() sets a flag in the tty structure state, TS_SO_OLOWAT, to request that it be awakened when the queue drops below the low watermark. The check of the queue size and following sleep must be ordered such that removal of characters from the output queue is guaranteed to occur after the sleep; see Figure 10.2. If characters could be removed after the check but before the sleep, their removal might cause the queue to drop below the low watermark, thus causing a wakeup to be issued before the thread had gone to sleep. Once asleep, the thread would be blocked forever because the event for which it would be waiting would have already occurred. As shown in Figure 10.2, the guarantee is made by acquiring the mutex that must be held while adding or removing characters from the output queue for the terminal.

Figure 10.2. Pseudocode for checking the output queue in a line discipline.

struct tty *tp; ttstart(tp); mtx_lock(&tp >t_outmtx); if (tp >t_outq.c_cc > high-water-mark) { /* Request wake up when output <= low-water-mark */ tp >t_state |= TS_SO_OLOWAT; msleep(&tp >t_outq, &tp >t_outmtx, TTOPRI, "ttywri", 0); } mtx_unlock(&tp >t_outmtx);

Once errors, permissions, and flow control have been checked, ttwrite() copies the user's data into a local buffer in chunks of at most 100 characters using uiomove(). (A value of 100 is used because the buffer is stored on the stack, and so it cannot be large.) When the terminal driver is configured in noncanonical mode, no per-character translations are done, and the entire buffer is processed at once. In canonical mode, the terminal driver locates groups of characters requiring no translation by scanning through the output string, looking up each character in turn in a table that marks characters that might need translation (e.g., newline), or characters that need expansion (e.g., tabs). Each group of characters that requires no special processing is placed into the output queue using b_to_q(). Trailing special characters are output with ttyoutput(). In either case, ttwrite() must check that enough C-list blocks are available; if they are not, it waits for a short time (by sleeping on Ibolt for up to 1 second), then retries.

The routine that does output with translation is ttyoutput(), which accepts a single character, processes that character as necessary, and places the result on the output queue. The following translations may be done, depending on the terminal mode:

  • Tabs may be expanded to spaces.

  • Newlines may be replaced with a carriage return plus a line feed.

As soon as data are placed on the output queue of a tty, ttstart() is called to start output. Unless output is already in progress or has been suspended by receipt of a stop character, ttstart() calls the start routine specified in the tty's t_oproc field. For a pseudo-terminal, the start routine checks for a thread sleeping on the master side and, if found, awakens it so that it can consume the data. For a hardware terminal, the start routine begins sending the characters out the serial line. Once all the data have been processed and have been placed into the output queue, ttwrite() returns an indication that the write completed successfully.

Terminal Output

Characters are removed from the output queue either by the thread running on the master side of the pseudo-terminal or by the hardware device driver. Whenever the number of characters on the output queue drops below the low watermark, the output routine checks to see if the TS_SO_OLOWAT flag is set to show that a thread is waiting for space in the output queue and should be awakened. In addition, selwakeup() is called, and if a thread is recorded in t_wsel as selecting for output, that thread is notified. The output continues until the output queue is empty.

Terminal Input

Unlike output, terminal input is not started by a system call but instead arrives asynchronously when the terminal line receives characters from a remote login session or locally from the keyboard. Thus, the input processing in the terminal system occurs mostly at interrupt time.

When a character arrives over the network from a remote login session, the locally running remote-login daemon writes it into the master side of the pseudo-terminal. The master side of the pseudo-terminal will pass the character as input to the terminal line discipline for the receiving tty through the latter's l_rint entry:

(*linesw[tp >t_line].l_rint)(input-character, tp);

For locally attached hardware such as a keyboard, the input character will be passed by the device driver directly to the receiving tty's l_rint entry. In either case, the input character is passed to the l_rint routine as an integer. The bottom 8 bits of the integer are the actual character. Characters received from locally connected hardware may have hardware-detected parity errors, break characters, or framing errors. Such errors are shown by setting flags in the upper bits of the integer.

The receiver-interrupt (l_rint) routine for the normal terminal line discipline is ttyinput(). When a break condition is detected (a longer-than-normal character with only 0 bits), it is ignored, or an interrupt character or a null is passed to the process, depending on the terminal mode. The interpretation of terminal input described in Section 10.1 is done here. Input characters are echoed if desired. In noncanonical mode, characters are placed into the raw input queue without interpretation. Otherwise, most of the work done by ttyinput() is to check for characters with special meanings and to take the requested actions. Other characters are placed into the raw queue. In canonical mode, if the received character is a carriage return or another character that causes the current line to be made available to the program reading the terminal, the contents of the raw queue are added to the canonicalized queue and ttwakeup() is called to notify any process waiting for input. In noncanonical mode, ttwakeup() is called when each character is processed. It will awaken any process sleeping on the raw queue awaiting input for a read and will notify processes selecting for input. If the terminal has been set for signal-driven I/O using fcntl and the FASYNC flag, a SIGIO signal is sent to the process group controlling the terminal.

Ttyinput() must also check that the input queue does not become too large, exhausting the supply of C-list blocks. For pseudo-terminals, when the input queue becomes full, the kernel simply stops reading characters from the master side, which causes the local-login daemon to stop reading from the network. Eventually the network flow control blocks the process generating the characters. For characters coming from locally attached hardware such as a keyboard, input characters are discarded when the limit (8192 characters) is reached. If the IXOFF termios flag is set, end-to-end flow control is invoked when the queue reaches half full by output of a stop character (normally XOFF or control-S).

Up to this point, all processing is asynchronous and occurs independent of whether a read call is pending on the terminal device. In this way, type-ahead is allowed to the limit of the input queues.

Eventually, a read call is made on the file descriptor for the terminal device. Like all calls to read from a character-special device, this one results in a call to the device driver's d_read entry with a device pointer, a uio structure describing the data to be read, and a flag specifying whether the I/O is nonblocking. Terminal device drivers use the device pointer to locate the tty structure for the device and then call the line discipline l_read entry to process the system call.

The l_read entry for the terminal driver is ttread(). Like ttwrite(), ttread() first checks to see whether the process is part of the session and the process group currently associated with the terminal. If the process is a member of the session currently associated with the terminal, if any, and is a member of the current process group, the read proceeds. Otherwise, if a SIGTTIN would suspend the process, a SIGTTIN is sent to that process group. Here, the read will be attempted again when the user moves the process group to the foreground. Otherwise, an error is returned. Finally, ttread() checks for data in the appropriate queue (the canonical queue in canonical mode, the raw queue in noncanonical mode). If no data are present, ttread() returns the error EAGAIN if the terminal is using non-blocking I/O; otherwise, it sleeps on the address of the raw queue. When ttread() is awakened, it restarts processing from the beginning because the terminal state or process group might have changed while it was asleep.

When characters are present in the queue for which ttread() is waiting, they are removed from the queue one at a time with getc() and are copied out to the user's buffer with ureadc(). In canonical mode, certain characters receive special processing as they are removed from the queue: The delayed-suspension character causes the current process group to be stopped with signal SIGTSTP, and the end-of-file character terminates the read without being passed back to the user program. If there was no previous character, the end-of-file character results in the read returning zero characters, and that is interpreted by user programs as indicating end-of-file. However, most special processing of input characters is done when the character is entered into the queue. For example, translating carriage returns to newlines based on the ICRNL flag must be done when the character is first received because the newline character wakes up waiting processes in canonical mode. In noncanonical mode, the characters are not examined as they are processed.

Characters are processed and returned to the user until the character count in the uio structure reaches zero, the queue is exhausted, or, if in canonical mode, a line terminator is reached. When the read call returns, the returned character count will be the amount by which the requested count was decremented as characters were processed.

After the read completes, if terminal output was blocked by a stop character being sent because the queue was filling up, and the queue is now less than 20 percent full, a start character (normally XON, control-Q) is sent.

The ioctl Routine

Section 10.3 described the user interface to terminal drivers and line disciplines, most of which is accessed via the ioctl system call. Most of these calls manipulate software options in the terminal line discipline; some of them also affect the operation of the asynchronous serial-port hardware. In particular, the hardware line speed, word size, and parity are derived from these settings. Consequently, ioctl calls are processed both by the current line discipline and by the hardware driver.

The device driver d_ioctl routine is called with a device pointer, an ioctl command, and a pointer to a data buffer when an ioctl is done on a character-special file, among other arguments. Like the read and write routines, most terminal-driver ioctl routines locate the tty structure for the device and then pass control to the line discipline. The line-discipline ioctl routine does discipline-specific actions, including change of line discipline. If the line-discipline routine fails, the driver will immediately return an error, as shown in Figure 10.3. Otherwise, the driver will then call the ttioctl() routine that does most common terminal processing, including changing terminal parameters. If ttioctl() fails, the driver will immediately return an error. Otherwise, some drivers implement additional ioctl commands that do hardware specific processing for example, manipulating modem-control outputs. These commands are not recognized by the line discipline or by common terminal processing, and thus must be handled by the driver. The ioctl routine returns an error number if an error is detected or returns zero if the command has been processed successfully. The errno variable is set to ENOTTY if the command is not recognized.

Figure 10.3. Handling of an error return from a line discipline.

error = (*linesw[tp >t_line].l_ioctl)(tp, cmd, data, flag); if (error >= 0) return (error); error = ttioctl(tp, cmd, data, flag); if (error >= 0) return (error); switch (cmd) { case TIOCSBRK: /* hardware specific commands */ ... return (0); case TIOCCBRK: ... return (0); default: return (ENOTTY); }

Modem Transitions

The way in which the system uses modem-control lines on terminal lines was introduced in Section 10.7. Most terminal hardware support at least the set of modem-control lines used by FreeBSD; those that do not act instead as though the carrier were always asserted. When a device is opened, the DTR output is enabled, and then the state of the carrier input is checked. If the state of the carrier input changes later, this change must be detected and processed by the driver. Some devices have a separate interrupt that reports changes in modem-control status; others report such changes along with other status information with received characters. Some devices do not interrupt when modem-control lines change, and the driver must check their status periodically. When a change is detected, the line discipline is notified by a call to its l_mnodem routine with the new state of the carrier input.

The normal terminal-driver modem routine, ttymodem(), maintains the state of the TS_CARR_ON flag in the tty structure and processes corresponding state changes. When carrier establishment is detected, a wakeup is issued for any process waiting for an open to complete. When carrier drops on an open line, the leader of the session associated with the terminal (if any) is sent a hangup signal, SIGHUP, and the terminal queues are flushed. The return value of ttymodem() indicates whether the driver should maintain its DTR output. If the value is zero, DTR should be turned off. Ttymodem() also implements an obscure terminal option to use the carrier line for flow-control handshaking, stopping output when carrier drops and resuming when it returns.

Closing of Terminal Devices

When the final reference to a terminal device is closed, or the revoke system call is made on the device, the device-driver close routine is called. Both the line discipline and the hardware driver may need to close down gracefully. The devicedriver routine first calls the line-discipline close routine. The standard linediscipline close entry, ttylclose(), waits for any pending output to drain (if the terminal was not opened with the O_NONBLOCK flag set and the carrier is still on), then flushes the input and output queues. (Note that the close may be interrupted by a signal while waiting for output to complete.) The hardware driver may clear any pending operations, such as transmission of a break. If the state bit TS_HUPCLS has been set with the TIOCHPCL ioctl, DTR is disabled to hang up the line. Finally, the device-driver routine calls ttyclose(), which flushes all the queues, increments the generation number so that pending reads and writes can detect reuse of the terminal, and clears the terminal state.


   
 

Категории