Chapter 6. UNIX Special Files

This chapter discusses UNIX special files that represent devices. Two important examples of special files are pipes and FIFOs, interprocess communication mechanisms that allow processes running on the same system to share information and hence cooperate. The chapter introduces the client-server model and also discusses how to handle special files representing devices such as terminals.

Pipes

The capacity to communicate is essential for processes that cooperate to solve a problem. The simplest UNIX interprocess communication mechanism is the pipe, which is represented by a special file. The pipe function creates a communication buffer that the caller can access through the file descriptors fildes[0] and fildes[1]. The data written to fildes[1] can be read from fildes[0] on a first-in-first-out basis.

SYNOPSIS

  #include <unistd.h>

  int pipe(int fildes[2]);
                                      POSIX

If successful, pipe returns 0. If unsuccessful, pipe returns –1 and sets errno. The following table lists the mandatory errors for pipe.

errno

cause

EMFILE

more than MAX_OPEN-2 file descriptors already in use by this process

ENFILE

number of simultaneously open files in system would exceed system-imposed limit

A pipe has no external or permanent name, so a program can access it only through its two descriptors. For this reason, a pipe can be used only by the process that created it and by descendants that inherit the descriptors on fork. The pipe function described here creates a traditional unidirectional communication buffer. The POSIX standard does not specify what happens if a process tries to write to fildes[0] or read from fildes[1].

When a process calls read on a pipe, the read returns immediately if the pipe is not empty. If the pipe is empty, the read blocks until something is written to the pipe, as long as some process has the pipe open for writing. On the other hand, if no process has the pipe open for writing, a read from an empty pipe returns 0, indicating an end-of-file condition. (This description assumes that access to the pipe uses blocking I/O.)

Example 6.1. 

The following code segment creates a pipe.

int fd[2];
if (pipe(fd) == -1)
   perror("Failed to create the pipe");

If the pipe call executes successfully, the process can read from fd[0] and write to fd[1].

A single process with a pipe is not very useful. Usually a parent process uses pipes to communicate with its children. Program 6.1 shows a simple program in which the parent creates a pipe before forking a child. The parent then writes a string to the pipe and prints a message to standard error. The child reads a message from the pipe and then prints to standard error. This program does not check for errors on the read or write operations.

Example 6.1. parentwritepipe.c

A program in which a parent writes a string to a pipe and the child reads the string. The program does not check for I/O errors.

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define BUFSIZE 10

int main(void) {
   char bufin[BUFSIZE] = "empty";
   char bufout[] = "hello";
   int bytesin;
   pid_t childpid;
   int fd[2];

   if (pipe(fd) == -1) {
      perror("Failed to create the pipe");
      return 1;
   }
   bytesin = strlen(bufin);
   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid)                                       /* parent code */
      write(fd[1], bufout, strlen(bufout)+1);
   else                                                 /* child code */
      bytesin = read(fd[0], bufin, BUFSIZE);
   fprintf(stderr, "[%ld]:my bufin is {%.*s}, my bufout is {%s}
",
           (long)getpid(), bytesin, bufin, bufout);
   return 0;
}

Example 6.2. 

Run Program 6.1 and explain the results. Does the child always read the full string?

Answer:

The parent’s bufin always contains the string "empty". The child’s bufin most likely contains the string "hello". However, reads from pipes are not atomic. That is, there is no guarantee that a single read call actually retrieves everything written by a single write call. It is possible (though not likely in this case) that the child’s bufin could contain something like "helty" if read retrieves only partial results. If the parent’s write operation fails, the child’s bufin contains "empty".

Example 6.3. 

Consider the following code segment from Program 6.1.

if (childpid)
   write(fd[1], bufout, strlen(bufout)+1);
else
   bytesin = read(fd[0], bufin, BUFSIZE);

What happens if you replace it with the following code?

if (childpid)
   copyfile(STDIN_FILENO, fd[1]);
else
   copyfile(fd[0], STDOUT_FILENO);

(The copyfile function is shown in Program 4.6 on page 100.)

Answer:

The parent process reads from standard input and writes to the pipe, while the child reads from the pipe and echoes to standard output. The parent echoes everything entered at the keyboard as it is typed, and the child writes to the screen as it reads each entered line from the pipe. A difficulty arises, however, when you enter the end-of-file character (usually Ctrl-D) at the terminal. The parent detects the end of the input, displays the message written by its fprintf, and exits with no problem, closing its descriptors to the pipe. Unfortunately, the child still has fd[1] open, so the copyfile function does not detect that input has ended. The child hangs, waiting for input, and does not exit. Since the parent has exited, the prompt appears, but the child process is still running. Unless you execute ps you might think that the child terminated also. To fix the problem, replace the substitute code with the following.

if (childpid && (close(fd[0]) != -1))
   copyfile(STDIN_FILENO, fd[1]);
else if (close(fd[1]) != -1)
   copyfile(fd[0], STDOUT_FILENO);

Program 6.2 shows a modification of Program 3.2 from page 68. The modification demonstrates how to use reading from pipes for synchronization. The parent creates a pipe before creating n-1 children. After creating all its children, the parent writes n characters to the pipe. Each process, including the parent, reads a character from the pipe before proceeding to output its information to standard error. Since the read from the pipe blocks until there is something to read, each child waits until the parent writes to the pipe, thereby providing a synchronization point called a barrier. None of the processes can do any writing to standard error until all of the processes have been created. Section 6.8 gives another example of barrier synchronization. Notice that Program 6.2 uses r_write and r_read rather than write and read to ensure that the parent actually writes everything and that the children actually perform their reads. The children do not synchronize after the barrier.

Example 6.2. synchronizefan.c

A synchronized process fan. Processes wait until all have been created before echoing their messages to standard error.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "restart.h"

int main  (int argc, char *argv[]) {
   char buf[] = "g";
   pid_t childpid = 0;
   int fd[2];
   int i, n;

   if (argc != 2){      /* check for valid number of command-line arguments */
      fprintf (stderr, "Usage: %s processes
", argv[0]);
      return 1;
   }
   n = atoi(argv[1]);
   if (pipe(fd) == -1) {                 /* create pipe for synchronization */
      perror("Failed to create the synchronization pipe");
      return 1;
   }
   for (i = 1; i < n;  i++)                  /* parent creates all children */
       if ((childpid = fork()) <= 0)
           break;
   if (childpid > 0) {          /* write synchronization characters to pipe */
      for (i = 0; i < n; i++)
         if (r_write(fd[1], buf, 1) != 1)
            perror("Failed to write synchronization characters");
   }
   if (r_read(fd[0], buf, 1) != 1)                      /* synchronize here */
      perror("Failed to read synchronization characters");
   fprintf(stderr, "i:%d  process ID:%ld  parent ID:%ld  child ID:%ld
",
           i, (long)getpid(), (long)getppid(), (long)childpid);
   return (childpid == -1);
}

Pipelines

Section 4.7 explains how a process can redirect standard input or output to a file. Redirection allows programs that are written as filters to be used very generally. This section describes how to use redirection with pipes to connect processes together. (You may want to review Section 4.7, which explains how a process can redirect standard input or output to a file.)

Example 6.4. 

The following commands use the sort filter in conjunction with ls to output a directory listing sorted by size.

ls -l > my.file
sort -n +4 < my.file

The first option to sort gives the type of sort (n means numeric). The second option instructs the program to find the sort key by skipping four fields.

The first command of Example 6.4 causes the process that runs the ls -l to redirect its standard output to the disk file my.file. Upon completion, my.file contains the unsorted directory listing. At this point, the second command creates a process to run the sort with its standard input redirected from my.file. Since sort is a filter, the sorted listing appears on standard output. Unfortunately, when the pair of commands completes, my.file remains on disk until explicitly deleted.

An alternative approach for outputting a sorted directory listing is to use an interprocess communication (IPC) mechanism such as a pipe to send information directly from the ls process to the sort process.

Example 6.5. 

The following alternative to the commands of Example 6.4 produces a sorted directory listing without creating the intermediate file my.file.

ls -l | sort -n +4

The vertical bar (|) of Example 6.5 represents a pipe. A programmer can build complicated transformations from simple filters by feeding the standard output of one filter into the standard input of the other filter through an intermediate pipe. The pipe acts as a buffer between the processes, allowing them to read and write at different speeds. The blocking nature of read and write effectively synchronize the processes.

The connection between ls and sort in Example 6.5 differs from redirection because no permanent file is created. The standard output of ls is “connected” to the standard input of sort through the intermediate communication buffer. Figure 6.1 shows a schematic of the connection and the corresponding file descriptor tables after the processes representing ls and sort establish the connection. The ls process redirects its standard output to the write descriptor of the pipe, and sort redirects its standard input to the read descriptor of the pipe. The sort process reads the data that ls writes on a first-in-first-out basis. The sort process does not have to consume data at the same rate as ls writes it to the pipe.

Status of the file descriptor table during execution of Example 6.5.

Figure 6.1. Status of the file descriptor table during execution of Example 6.5.

Program 6.3 shows a program that implements the equivalent of Example 6.5. Figures 6.2 to 6.4 depict the state of the file descriptor table for Program 6.3. In Figure 6.2, the child process inherits a copy of the file descriptor table of the parent. Both processes have read and write descriptors for the pipe. Figure 6.3 shows the file descriptor table after the child redirects its standard output and the parent redirects its standard input, but before either process closes unneeded file descriptors. Figure 6.4 shows the configuration after each process completes the close calls. This is the configuration inherited by execl.

Status of the file descriptor table after the fork in Program 6.3.

Figure 6.2. Status of the file descriptor table after the fork in Program 6.3.

Status of the file descriptor table after both dup2 functions of Program 6.3.

Figure 6.3. Status of the file descriptor table after both dup2 functions of Program 6.3.

Status of the file descriptor table after all close calls of Program 6.3.

Figure 6.4. Status of the file descriptor table after all close calls of Program 6.3.

Example 6.6. 

Explain why the only return values in Program 6.3 indicate error conditions. Under what circumstances does this program execute successfully?

Answer:

The program executes successfully when both parent and child successfully run execl on their respective programs and these programs complete successfully. If execution reaches one of the return statements of Program 6.3, at least one of the execl calls failed. Once an execl call completes successfully, the program on which execl was run is responsible for the error handling.

Example 6.3. simpleredirect.c

A program to execute the equivalent of ls -l | sort -n +4.

#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
   pid_t childpid;
   int fd[2];

   if ((pipe(fd) == -1) || ((childpid = fork()) == -1)) {
      perror("Failed to setup pipeline");
      return 1;
   }

   if (childpid == 0) {                                  /* ls is the child */
      if (dup2(fd[1], STDOUT_FILENO) == -1)
         perror("Failed to redirect stdout of ls");
      else if ((close(fd[0]) == -1) || (close(fd[1]) == -1))
         perror("Failed to close extra pipe descriptors on ls");
      else {
         execl("/bin/ls", "ls", "-l", NULL);
         perror("Failed to exec ls");
      }
      return 1;
   }
   if (dup2(fd[0], STDIN_FILENO) == -1)               /* sort is the parent */
       perror("Failed to redirect stdin of sort");
   else if ((close(fd[0]) == -1) || (close(fd[1]) == -1))
       perror("Failed to close extra pipe file descriptors on sort");
   else {
      execl("/bin/sort", "sort", "-n", "+4", NULL);
      perror("Failed to exec sort");
   }
   return 1;
}

Example 6.7. 

What output would be generated if the file descriptors fd[0] and fd[1] were not closed before the calls to execl?

Answer:

No output would be generated. The sort process reads from standard input until an end-of-file occurs. Since it is reading from a pipe, sort detects an end-of-file (read returns 0) only when the pipe is empty and no processes have the pipe open for writing. As illustrated in Figure 6.4, only the ls program (the child) can write to the pipe. Eventually, this program terminates, and sort (the parent) detects end-of-file. If Program 6.3 omits the close calls, the situation looks like Figure 6.3. When the child terminates, the parent still has file descriptor [4] open for writing to the pipe. The parent blocks indefinitely, waiting for more data.

FIFOs

Pipes are temporary in the sense that they disappear when no process has them open. POSIX represents FIFOs or named pipes by special files that persist even after all processes have closed them. A FIFO has a name and permissions just like an ordinary file and appears in the directory listing given by ls. Any process with the appropriate permissions can access a FIFO. Create a FIFO by executing the mkfifo command from a shell or by calling the mkfifo function from a program.

The mkfifo function creates a new FIFO special file corresponding to the pathname specified by path. The mode argument specifies the permissions for the newly created FIFO.

SYNOPSIS

   #include <sys/stat.h>

   int mkfifo(const char *path, mode_t mode);
                                                       POSIX

If successful, mkfifo returns 0. If unsuccessful, mkfifo returns –1 and sets errno. A return value of –1 means that the FIFO was not created. The following table lists the mandatory errors for mkfifo.

errno

cause

EACCES

search permission on a component of path prefix denied, or write permission on parent directory of FIFO denied

EEXIST

named file already exists

ELOOP

a loop exists in resolution of path

ENAMETOOLONG

length of path exceeds PATH_MAX, or a pathname component is longer than NAME_MAX

ENOENT

component of path prefix specified by path does not name existing file, or path is an empty string

ENOSPC

directory to contain new file cannot be extended, or the file system is out of resources

ENOTDIR

component of path prefix is not a directory

EROFS

the named file would reside on a read-only file system

Unlike many other I/O functions, mkfifo does not set errno to EINTR.

Example 6.8. 

The following code segment creates a FIFO, myfifo, in the current working directory. This FIFO can be read by everybody but is writable only by the owner.

#define FIFO_PERMS  (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

if (mkfifo("myfifo", FIFO_PERMS) == -1)
   perror("Failed to create myfifo");

Remove a FIFO the same way you remove a file. Either execute the rm command from a shell or call unlink from a program. Example 6.9 shows a code segment that removes the FIFO that Example 6.8 created. The code assumes that the current working directory of the calling program contains myfifo.

Example 6.9. 

The following code segment removes myfifo from the current working directory.

if (unlink("myfifo") == -1)
   perror("Failed to remove myfifo");

Program 6.4 creates a named pipe from a path specified on the command line. It then forks a child. The child process writes to the named pipe, and the parent reads what the child has written. Program 6.4 includes error checking, identifying each message with the process ID. This identification of messages is important because the parent and child share standard error.

Example 6.4. parentchildfifo.c

The parent reads what its child has written to a named pipe.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#define BUFSIZE 256
#define FIFO_PERM  (S_IRUSR | S_IWUSR)

int dofifochild(const char *fifoname, const char *idstring);
int dofifoparent(const char *fifoname);

int main (int argc, char *argv[]) {
   pid_t childpid;

   if (argc != 2) {                           /* command line has pipe name */
      fprintf(stderr, "Usage: %s pipename
", argv[0]);
      return 1;
   }
   if (mkfifo(argv[1], FIFO_PERM) == -1) {           /* create a named pipe */
      if (errno != EEXIST) {
         fprintf(stderr, "[%ld]:failed to create named pipe %s: %s
",
              (long)getpid(), argv[1], strerror(errno));
         return 1;
      }
   }
   if ((childpid = fork()) == -1){
      perror("Failed to fork");
      return 1;
   }
   if (childpid == 0)                                   /* The child writes */
      return dofifochild(argv[1], "this was written by the child");
   else
      return dofifoparent(argv[1]);
}

The dofifochild function of Program 6.5 shows the actions taken by the child to write to the pipe. Notice that Program 6.5 uses snprintf rather than sprintf to construct the message. The first three parameters to snprintf are the buffer address, the buffer size and the format string. The snprintf does not write beyond the specified size and always inserts a null character to terminate what it has inserted. Program 6.5 also uses r_write instead of write to make sure that the child writes the entire message.

Example 6.5. dofifochild.c

The child writes to the pipe and returns.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define BUFSIZE 256

int dofifochild(const char *fifoname, const char *idstring) {
   char buf[BUFSIZE];
   int fd;
   int rval;
   ssize_t strsize;

   fprintf(stderr, "[%ld]:(child) about to open FIFO %s...
",
          (long)getpid(), fifoname);
   while (((fd = open(fifoname, O_WRONLY)) == -1) && (errno == EINTR)) ;
   if (fd == -1) {
      fprintf(stderr, "[%ld]:failed to open named pipe %s for write: %s
",
             (long)getpid(), fifoname, strerror(errno));
      return 1;
   }
   rval = snprintf(buf, BUFSIZE, "[%ld]:%s
", (long)getpid(), idstring);
   if (rval < 0) {
      fprintf(stderr, "[%ld]:failed to make the string:
", (long)getpid());
      return 1;
   }
   strsize = strlen(buf) + 1;
   fprintf(stderr, "[%ld]:about to write...
", (long)getpid());
   rval = r_write(fd, buf, strsize);
   if (rval != strsize) {
      fprintf(stderr, "[%ld]:failed to write to pipe: %s
",
             (long)getpid(), strerror(errno));
      return 1;
   }
   fprintf(stderr, "[%ld]:finishing...
", (long)getpid());
   return 0;
}

The dofifoparent function of Program 6.6 shows the actions taken by the parent to read from the pipe.

Example 6.10. 

What happens to the named pipe after the processes of Program 6.4 exit?

Answer:

Since neither process called unlink for the FIFO, it still exists and appears in the directory listing of its path.

Example 6.6. dofifoparent.c

The parent reads what was written to a named pipe.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define BUFSIZE 256
#define FIFO_MODES O_RDONLY

int dofifoparent(const char *fifoname) {
   char buf[BUFSIZE];
   int fd;
   int rval;

   fprintf(stderr, "[%ld]:(parent) about to open FIFO %s...
",
                       (long)getpid(), fifoname);
   while (((fd = open(fifoname, FIFO_MODES)) == -1) && (errno == EINTR))  ;
   if (fd == -1) {
      fprintf(stderr, "[%ld]:failed to open named pipe %s for read: %s
",
             (long)getpid(), fifoname, strerror(errno));
      return 1;
   }
   fprintf(stderr, "[%ld]:about to read...
", (long)getpid());
   rval = r_read(fd, buf, BUFSIZE);
   if (rval == -1) {
      fprintf(stderr, "[%ld]:failed to read from pipe: %s
",
             (long)getpid(), strerror(errno));
      return 1;
   }
   fprintf(stderr, "[%ld]:read %.*s
", (long)getpid(), rval, buf);
   return 0;
}

Pipes and the Client-Server Model

The client-server model is a standard pattern for process interaction. One process, designated the client, requests a service from another process, called the server. The chapters in Part 4 of the book develop and analyze applications that are based on the client-server model with network communication. This section introduces client-server applications that use named pipes as the communication vehicle. We look at two types of client-server communication—simple-request and request-reply. In simple-request, the client sends information to the server in a one-way transmission; in request-reply the client sends a request and the server sends a reply.

Programs 6.7 and 6.8 illustrate how the simple-request protocol can be useful in logging. The client writes logging information to a named pipe rather than to standard error. A server reads from the named pipe and writes to a file. At first glance, the use of the named pipe appears to have added an extra step with no benefit. However, pipes and FIFOs have a very important property—writes of no more than PIPE_BUF bytes are guaranteed to be atomic. That is, the information is written as a unit with no intervening bytes from other writes. In contrast, an fprintf is not atomic, so pieces of the messages from multiple clients might be interspersed.

The server of Program 6.7 creates the pipe if it does not already exist. The server opens the pipe for both reading and writing, even though it will not write to the pipe. When an attempt is made to open a pipe for reading, open blocks until another process opens the pipe for writing. Because the server opens the pipe for reading and writing, open does not block. The server uses copyfile to read from the pipe and to write to standard output. To write to a file, just redirect standard output when the server is started. Since the server has the pipe open for writing as well as reading, copyfile will never detect an end-of-file. This technique allows the server to keep running even when no clients are currently writing to the pipe. Barring errors, the server runs forever.

Example 6.7. pipeserver.c

The program reads what is written to a named pipe and writes it to standard output.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define FIFOARG 1
#define FIFO_PERMS (S_IRWXU | S_IWGRP| S_IWOTH)

int main (int argc, char *argv[]) {
   int requestfd;

   if (argc != 2) {    /* name of server fifo is passed on the command line */
      fprintf(stderr, "Usage: %s fifoname > logfile
", argv[0]);
      return 1;
   }
                         /* create a named pipe to handle incoming requests */
   if ((mkfifo(argv[FIFOARG], FIFO_PERMS) == -1) && (errno != EEXIST)) {
       perror("Server failed to create a FIFO");
       return 1;
   }
                    /* open a read/write communication endpoint to the pipe */
   if ((requestfd = open(argv[FIFOARG], O_RDWR)) == -1) {
       perror("Server failed to open its FIFO");
       return 1;
   }
   copyfile(requestfd, STDOUT_FILENO);
   return 1;
}

The client in Program 6.8 writes a single line to the pipe. The line contains the process ID of the client and the current time. Multiple copies of Program 6.8 can run concurrently. Because of the atomic nature of writes to the pipe, pieces of the messages from different clients are not interleaved.

Example 6.8. pipeclient.c

The client writes an informative message to a named pipe.

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define FIFOARG 1

int main (int argc, char *argv[]) {
   time_t curtime;
   int len;
   char requestbuf[PIPE_BUF];
   int requestfd;

   if (argc != 2) {  /* name of server fifo is passed on the command line */
      fprintf(stderr, "Usage: %s fifoname", argv[0]);
      return 1;
   }

   if ((requestfd = open(argv[FIFOARG], O_WRONLY)) == -1) {
       perror("Client failed to open log fifo for writing");
       return 1;
   }

   curtime = time(NULL);
   snprintf(requestbuf, PIPE_BUF, "%d: %s", (int)getpid(), ctime(&curtime));
   len = strlen(requestbuf);
   if (r_write(requestfd, requestbuf, len) != len) {
      perror("Client failed to write");
      return 1;
   }
   r_close(requestfd);
   return 0;
}

Example 6.11. 

How would you start Program 6.7 so that it uses the pipe mypipe and the log file it creates is called mylog? When will the program terminate?

Answer:

pipeserver mypipe > mylog

The program does not terminate unless it is killed. You can kill it by typing Ctrl-C at the keyboard. No client error can cause the server to terminate.

Example 6.12. 

Start the pipeserver of Program 6.7 and run several copies of the pipeclient of Program 6.8 and observe the results.

We now consider a second example of the client-server model with named pipes, a simple time (sequence number) server that illustrates some of the difficulties in using the client-server model with pipes and FIFOs.

The implementation uses two named pipes—a request pipe and a sequence pipe. Clients write a byte to a request pipe (e.g., 'g'). The server responds by writing a sequence number to the sequence pipe and incrementing the sequence number. Unfortunately, reading from a pipe is not an atomic operation. Since the sequence number is more than one byte, it is possible (though unlikely) that a client may not get all of the bytes of a sequence number in one read. Depending on the interleaving of the client processes, the next client may get part of the previous sequence number. To handle this possibility, a client that does a partial read of the sequence number immediately transmits an error designator (e.g., 'e') on the request pipe. When the server encounters the error character, it closes and unlinks the pipes. The other clients then detect an error.

As before, the server opens both pipes for reading and writing. The server terminates only when it receives an 'e' byte from a client. When that happens, future clients block when they try to open the request pipe for writing. Pending clients receive an error when they try to write to the request pipe since no process has this pipe open. When a process writes to a pipe or FIFO that no process has open for reading, write generates a SIGPIPE signal. Unless the process has specifically prevented it, the signal causes the process to terminate immediately. Section 8.4 explains how to respond to these types of signals.

Programs 6.9 and 6.10 illustrate the difficulties of implementing a request-reply protocol by using named pipes. When multiple clients make requests, the server replies can be read by any client. This allows a sequence number meant for one process to be read by another process. Second, because reads are not atomic, a partial read by one client causes the next client to receive incorrect results. The solution in Program 6.9 and Program 6.10 is for the client to send an error code, which causes the server to terminate. This strategy may suffice for closely cooperating processes, but it is not applicable in general. A malicious client could cause the protocol to behave incorrectly without detecting an error. In most cases, the client should never be able to cause the server to fail or exit. The exercise of Section 6.10 explores an alternative strategy in which the server creates a separate named pipe for each distinct client. Now each pipe only has a single reader, eliminating the two problems described above.

Example 6.9. seqserverbad.c

A sequence server reads a character from the request pipe and transmits a sequence number to the sequence pipe. (See text for a discussion.)

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define ERROR_CHAR'e'
#define OK_CHAR 'g'
#define REQUEST_FIFO 1
#define REQ_PERMS (S_IRUSR | S_IWUSR | S_IWGRP | S_IWOTH)
#define SEQUENCE_FIFO 2
#define SEQ_PERMS (S_IRUSR | S_IWUSR | S_IRGRP| S_IROTH)

int main (int argc, char *argv[]) {
   char buf[1];
   int reqfd, seqfd; long seqnum = 1;
   if (argc != 3) {            /* names of fifos passed on the command line */
      fprintf(stderr, "Usage: %s requestfifo sequencefifo
", argv[0]);
      return 1;
   }

                         /* create a named pipe to handle incoming requests */
   if ((mkfifo(argv[REQUEST_FIFO], REQ_PERMS) == -1) && (errno != EEXIST)) {
       perror("Server failed to create request FIFO");
       return 1;
   }
   if ((mkfifo(argv[SEQUENCE_FIFO], SEQ_PERMS) == -1) && (errno != EEXIST)){
       perror("Server failed to create sequence FIFO");
       if (unlink(argv[REQUEST_FIFO]) == -1)
          perror("Server failed to unlink request FIFO");
       return 1;
   }
   if (((reqfd = open(argv[REQUEST_FIFO], O_RDWR)) == -1) ||
       ((seqfd = open(argv[SEQUENCE_FIFO], O_RDWR)) == -1)) {
      perror("Server failed to open one of the FIFOs");
      return 1;
   }
   for ( ; ; ) {
      if (r_read(reqfd, buf, 1) == 1) {
         if ((buf[0] == OK_CHAR) &&
             (r_write(seqfd, &seqnum, sizeof(seqnum)) == sizeof(seqnum)))
            seqnum++;
         else if (buf[0] == ERROR_CHAR)
            break;
      }
   }
   if (unlink(argv[REQUEST_FIFO]) == -1)
      perror("Server failed to unlink request FIFO");
   if (unlink(argv[SEQUENCE_FIFO]) == -1)
      perror("Server failed to unlink sequence FIFO");
   return 0;
}

Example 6.10. seqclientbad.c

The client writes a request to a request pipe and reads the sequence number from the sequence pipe. This client can cause the server to exit.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <sys/stat.h>
#include "restart.h"
#define ERROR_CHAR 'e'
#define OK_CHAR 'g'
#define REPEAT_MAX 100
#define REQUEST_FIFO 1
#define SEQUENCE_FIFO 2
#define SLEEP_MAX 5

int main (int argc, char *argv[]) {
   int i;
   char reqbuf[1];
   int reqfd, seqfd;
   long seqnum;

   if (argc != 3) {            /* names of pipes are command-line arguments */
      fprintf(stderr, "Usage: %s requestfifo sequencefifo
", argv[0]);
      return 1;
   }
   if (((reqfd = open(argv[REQUEST_FIFO], O_WRONLY)) == -1) ||
       ((seqfd = open(argv[SEQUENCE_FIFO], O_RDONLY)) == -1)) {
       perror("Client failed to open a FIFO");
       return 1;
   }
   for (i = 0; i < REPEAT_MAX; i++) {
       reqbuf[0] = OK_CHAR;
       sleep((int)(SLEEP_MAX*drand48()));
       if (r_write(reqfd, reqbuf, 1) == -1) {
          perror("Client failed to write request");
          break;
       }
       if (r_read(seqfd, &seqnum, sizeof(seqnum)) != sizeof(seqnum) ) {
           fprintf(stderr, "Client failed to read full sequence number
");
           reqbuf[0] = ERROR_CHAR;
           r_write(reqfd, reqbuf, 1);
           break;
       }
       fprintf(stderr, "[%ld]:received sequence number %ld
",
               (long)getpid(), seqnum);
    }
   return 0;
}

The situation with nonatomic reads from pipes can actually be worse than described here. We have assumed that a read becomes nonatomic as follows.

  1. The server gets two requests and writes two sequence numbers (4-byte integers) to the pipe.

  2. One client calls read for the sequence pipe requesting four bytes, but read returns only two bytes.

  3. The second client calls read for the sequence pipe to read the next four bytes. These four bytes consist of the last two bytes from the first sequence number and the first two bytes of the second sequence number.

Under these circumstances the first client detects an error, and the server shuts down. The second client may or may not know an error occurred.

However, another scenario is technically possible, although it is very unlikely. Suppose the server writes two 4-byte integer sequence numbers and the bytes in the pipe are abcdefgh. The POSIX standard does not exclude the possibility that the first client will read the bytes abgh and the second one will read the bytes cdef. In this case, the sequence numbers are incorrect and the error is not detected at all.

Example 6.13. 

Try running one copy of Program 6.9 (seqserverbad) and two copies of Program 6.10 (seqclientbad). What happens?

Answer:

This should work correctly. The two copies of seqclientbad should get disjoint sets of sequence numbers.

Example 6.14. 

Try running two copies of Program 6.9 (seqserverbad) and one copy of Program 6.10 (seqclientbad). What happens?

Answer:

Either server can respond to a request for a sequence number. It is possible that the client will get the same sequence number twice.

Example 6.15. 

Change the seqclientbad to have a SLEEP_MAX of 0 and a REPEAT_MAX of 1,000,000. Comment out the last fprintf line. Run two copies of the client with one copy of the server. What happens?

Answer:

It is possible, but unlikely, that the server will terminate because one of the clients received an incorrect number of bytes when requesting the sequence number.

Terminal Control

Many special files represent devices with characteristics that are platform dependent, making standardization difficult. However, since terminal control was thought to be essential on all systems, the POSIX standards committee decided to include library functions for manipulating special files representing terminals and asynchronous communication ports. This section describes these functions and the way to use them.

The stty command reports or sets terminal I/O characteristics. When executed without any arguments or with the -a or -g options, the stty command outputs information about the current terminal to standard output. The -a produces a longer form of the readable information produced by stty without arguments; the -g option produces the information in a form that can be used by a program. The second form of stty allows operands to change the behavior of the terminal associated with a shell.

SYNOPSIS

   stty [-a | -g]
   stty operands
                           POSIX:Shell and Utilities

Example 6.16. 

Execute stty, stty -a and stty -g on your system. Try to interpret the results.

Answer:

The stty command outputs the following under Sun Solaris 9.

speed 9600 baud; -parity
rows = 34; columns = 80; ypixels = 680; xpixels = 808;
swtch = <undef>;
brkint -inpck -istrip icrnl -ixany imaxbel onlcr tab3
echo echoe echok echoctl echoke iexten

The stty -a command on the same system outputs a more complete listing of the terminal settings.

speed 9600 baud;
rows = 34; columns = 80; ypixels = 680; xpixels = 808;
csdata ?
eucw 1:0:0:0, scrw 1:0:0:0
intr = ^c; quit = ^; erase = ^?; kill = ^u;
eof = ^d; eol = <undef>; eol2 = <undef>; swtch = <undef>;
start = ^q; stop = ^s; susp = ^z; dsusp = ^y;
rprnt = ^r; flush = ^o; werase = ^w; lnext = ^v;
-parenb -parodd cs8 -cstopb -hupcl cread -clocal -loblk
-crtscts -crtsxoff -parext -ignbrk brkint ignpar -parmrk
-inpck -istrip -inlcr -igncr icrnl -iuclc ixon -ixany -ixoff
imaxbel isig icanon -xcase echo echoe echok -echonl -noflsh
-tostop echoctl -echoprt echoke -defecho -flusho -pendin iexten
opost -olcuc onlcr -ocrnl -onocr -onlret -ofill -ofdel tab3

The stty -g command outputs the following on a single line.

2506:1805:d00bd:8a3b:3:1c:7f:15:4:0:0:0:11:13:1a:19:12:f:
17:16:0:0:1:1:0:00:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:
0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0

The interpretation of the fields closely follows the flags in the struct termios structure described below.

The stty -a command displays the current terminal settings, and the second form of stty allows you to change them. One important operand of stty is sane. This operand sets all modes to reasonable values and is useful if you terminate a program that has set the modes in an inconvenient way. You can use stty sane to recover when, for example, local echo has been turned off and you cannot see what you are typing. Sometimes you will have to terminate the line containing the stty command with a Ctrl-J rather than pressing the Return key if Return has been set to send a carriage return rather than a newline.

Programs access terminal characteristics through the struct termios structure, which includes at least the following members.

tcflag_t  c_iflag;      /* input modes */
tcflag_t  c_oflag;      /* output modes */
tcflag_t  c_cflag;      /* control modes */
tcflag_t  c_lflag;      /* local modes */
cc_t      c_cc[NCCS];   /* control characters */

The c_cc array of the struct termios structure holds the values of the characters that have special meaning to the terminal device drivers, for example, the end of input or program break characters. Table 6.1 on page 206 lists the special characters and their default settings.

The c_iflag member of the struct termios structure controls the way a terminal handles input; the c_oflag controls the way a terminal handles output. The c_cflag specifies hardware control information for the terminal, and the c_lflag controls the editing functions of the terminal. Table 6.2 on page 210 lists the POSIX values that these flags can take on. You can set an action by performing a bitwise OR of the appropriate struct termios field with the corresponding flag, and you can clear it by performing a bitwise AND with the complement of the flag.

Example 6.17. 

The ECHO value of the c_lflag field of struct termios specifies that characters typed at standard input should be echoed to standard output of the terminal. The following code segment clears the ECHO flag in a struct termios structure.

struct termio term;
term.c_lflag &= ~ECHO;

The tcgetattr function retrieves the attributes associated with the terminal referenced by the open file descriptor fildes. The attributes are returned in a struct termios structure pointed to by termios_p. The tcsetattr function sets the parameters of the terminal referenced by the open file descriptor fildes from the struct termios structure pointed to by termios_p. The optional_actions parameter controls the point at which the changes take effect: TCSANOW signifies that changes occur immediately, and TCSADRAIN signifies that changes occur after all output to fildes is transmitted. If optional_actions is TCSAFLUSH, the changes occur after all output to fildes is transmitted. In this case, all input received but not read is discarded.

SYNOPSIS

  #include <termios.h>

  int tcgetattr(int fildes, struct termios *termios_p);
  int tcsetattr(int fildes, int optional_actions,
                const struct termios *termios_p);
                                                              POSIX

These functions return 0 if successful. If unsuccessful, these functions return –1 and set errno. The following table lists the mandatory errors for these functions.

errno

cause

EBADF

fildes is not a valid file descriptor

EINTR

a signal interrupted tcsetattr

EINVAL

optional_actions is not a supported value, or attempt to change attribute represented in struct termios to an unsupported value

ENOTTY

file associated with fildes is not a terminal

Program 6.11 shows a ttysetchar function that sets a particular character. The ttsetchar function first calls tcgetattr to read the current settings of the terminal into a struct termios structure. After modifying the desired characters, ttysetchar calls tcsetattr to change the actual terminal settings. It is possible for tcsetattr to be interrupted by a signal while it is waiting for output to drain, so we restart it in this case.

Example 6.18. 

The following code segment calls the ttysetchar function of Program 6.11 to set the character that indicates end of terminal input to Ctrl-G. (The usual default is Ctrl-D.)

if (ttysetchar(STDIN_FILENO, VEOF, 0x07) == -1)
   perror("Failed to change end-of-file character");

Table 6.1. The POSIX special control characters

canonical mode

noncanonical mode

description

usual default

VEOF

 

EOF character

Ctrl-D

VEOL

 

EOL character

none

VERASE

 

ERASE character

backspace or delete

VINTR

VINTR

INTR character

Ctrl-C

VKILL

 

KILL character

Ctrl-U

 

VMIN

MIN value

1

VQUIT

VQUIT

QUIT character

Ctrl-

VSUSP

VSUSP

SUSP character

Ctrl-Z

 

VTIME

TIME value

0

VSTART

VSTART

START character

Ctrl-Q

VSTOP

VSTOP

STOP character

Ctrl-S

Example 6.11. ttysetchar.c

A function that sets a particular terminal control character to be a particular value.

#include <errno.h>
#include <termios.h>
#include <unistd.h>

int ttysetchar(int fd, int flagname, char c) {
   int error;
   struct termios term;

   if (tcgetattr(fd, &term) == -1)
      return -1;
   term.c_cc[flagname] = (cc_t)c;
   while (((error = tcsetattr(fd, TCSAFLUSH, &term)) == -1) &&
           (errno == EINTR)) ;
   return error;
}

Program 6.12 shows a function that uses tcgetattr and tcsetattr to turn echoing on or off. When echoing is turned off, the characters that you type do not appear on the screen.

Example 6.19. 

Why did Program 6.12 use tcgetattr to read the existing struct termios structure before setting the echo flags?

Answer:

The code shouldn’t change any of the other settings, so it reads the existing struct termios structure before modifying it.

Example 6.12. setecho.c

A function to turn terminal echo on or off.

#include <errno.h>
#include <termios.h>
#include <unistd.h>
#define ECHOFLAGS (ECHO | ECHOE | ECHOK | ECHONL)

int setecho(int fd, int onflag) {
   int error;
   struct termios term;

   if (tcgetattr(fd, &term) == -1)
      return -1;
   if (onflag)                                        /* turn echo on */
      term.c_lflag |= ECHOFLAGS;
   else                                              /* turn echo off */
      term.c_lflag &= ~ECHOFLAGS;
   while (((error = tcsetattr(fd, TCSAFLUSH, &term)) == -1) &&
           (errno == EINTR)) ;
   return error;
}

Example 6.20. 

What happens when you run the following program? Under what circumstances might such behavior be useful?

#include <unistd.h>
int setecho(int fd, int onflag);

int main(void) {
   setecho(STDIN_FILENO, 0);
   return 0;
}

Answer:

After you run this program, you will not see anything that you type on the computer screen. You can log out or use stty sane to set the echo back on. Turning off echoing is used for entering passwords and other secrets.

Program 6.13 shows the passwordnosigs function that retrieves the password entered at the controlling terminal of a process. It returns 0 if successful. On failure it returns –1 and sets errno. Notice that passwordnosigs sets the errno based on the first error that occurs. While most functions return immediately after an error, functions that must always restore state have to clean up before they return. The program calls the setecho function of Program 6.12 to turn echoing off and on. It must turn the terminal echo back on before returning or the user won’t be able to see what is typed.

Example 6.13. passwordnosigs.c

A function that prompts for and reads a password, assuming that no signals will occur.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include "restart.h"

int readline(int fd, char *buf, int nbytes);
int setecho(int fd, int onflag);

int passwordnosigs(char *prompt, char *passbuf, int passmax) {
   int fd; int firsterrno = 0;
   int passlen;
   char termbuf[L_ctermid];

   if (ctermid(termbuf) == NULL) {                /* find the terminal name */
      errno = ENODEV;
      return -1;
   }
   if ((fd = r_open2(termbuf, O_RDWR)) == -1)         /* open the terminal  */
      return -1;
   if (setecho(fd, 0) == -1)                               /* turn echo off */
      firsterrno = errno;
   else if (r_write(fd, prompt, strlen(prompt)) == -1)      /* write prompt */
      firsterrno = errno;
   else if ((passlen = readline(fd, passbuf, passmax)) == 0)
      firsterrno = EINVAL;
   else if (passlen == -1)
      firsterrno = errno;
   else
      passbuf[passlen-1] = ’’;                     /* remove newline */
   if ((setecho(fd, 1) == -1) && !firsterrno)  /* always turn echo back on */
      firsterrno = errno;
   if ((r_write(fd,"
",1) == -1) && !firsterrno)
      firsterrno = errno;
   if ((r_close(fd) == -1) && !firsterrno)
      firsterrno = errno;
   if (firsterrno)
      errno = firsterrno;
   return firsterrno ? -1 : 0;
}

The passwordnosigs uses readline of Program 4.1 on page 95 to read in a line from the terminal. We were able to use it here because it was written to use a general file descriptor rather than just reading from standard input.

The passwordnosigs function uses the controlling terminal as determined by the ctermid function rather than using standard input. The controlling terminal is usually something like /dev/tty and often shares the same physical devices as standard input and standard output, which are usually the keyboard and screen. One of the consequences of using a controlling terminal rather than standard input and standard output is that controlling terminals cannot be redirected from the command line. This is often used for passwords to discourage users from storing passwords in a file.

Example 6.21. 

What happens if a signal aborts a program that is executing passwordnosigs? This could happen if the user enters Ctrl-C after being prompted for the password.

Answer:

If the signal comes in after passwordnosigs turns off echoing, the user won’t be able to see subsequent typing at the terminal. If you do this, try typing stty sane followed by Return to get the terminal back to echo mode. Chapter 8 addresses this issue more carefully in Program 8.4 on page 266.

Table 6.2 lists the flags for terminal control. Chapter 8 discusses some of the issues related to terminals and signals. The project of Chapter 11 explores many aspects of terminal configuration and the interaction of terminal devices with user processes.

Canonical and noncanonical input processing

A common misconception is that somehow the keyboard and screen are connected, so everything that you type automatically appears on the screen. The keyboard and screen are, in fact, separate devices that communicate with terminal device drivers running on the computer. The device drivers receive bytes from the keyboard, buffering and editing them as specified by the settings for these devices.

The usual method of handling terminal input, canonical mode, processes input one line at a time. The special characters of Table 6.1 are used for terminating input and simple editing such as erasing the last character typed. A line is a sequence of bytes delimited by a newline (NL), an end-of-file (EOF) or an end-of-line (EOL).

In canonical mode, read requests do not return until the user enters a line delimiter (or the process receives a signal). The ERASE and KILL characters work only on the portion of a line that has not yet been delimited. A read request can return only one line, regardless of the number of bytes requested. If the system defines the POSIX constant MAX_CANON for the terminal, input lines cannot be longer than MAX_CANON.

A consequence of canonical mode processing is that input from a terminal behaves differently from input from other devices such as disks. In noncanonical mode, input is not assembled into lines. The device driver does not respond to the ERASE and KILL characters. Noncanonical input processing has two controlling parameters—MIN and TIME. The MIN parameter controls the smallest number of bytes that should be gathered before read returns. The TIME parameter refers to a timer with a 0.1-second granularity used for timing out bursty transmissions. Table 6.3 summarizes the settings for MIN and TIME.

Table 6.2. The POSIX values of flags for terminal control.

field

flag

description

c_iflag

BRKINT

signal interrupt on break

 

ICRNL

map CR to NL on input

 

IGNBRK

ignore break condition

 

IGNCR

ignore CR

 

IGNPAR

ignore characters with parity errors

 

INLCR

map NL to CR on input

 

INPCK

enable input parity check

 

ISTRIP

strip character

 

IXOFF

enable start/stop input control

 

IXON

enable start/stop output control

 

PARMRK

mark parity errors

c_oflag

OPOST

postprocess output

 

OCRNL

map CR to NL on output (POSIX:XSI Extension)

 

ONOCR

no CR output at column 0 (POSIX:XSI Extension)

 

ONLRET

NL performs CR function (POSIX:XSI Extension)

c_cflag

CSIZE

character size (CS5—CS8 for 5 to 8 bits, respectively)

 

CSTOPB

send two stop bits, else one

 

CREAD

enable receiver

 

PARENB

enable parity

 

PARODD

odd parity, else even

 

HUPCL

hang up on last close

 

CLOCAL

ignore modem status lines

c_lflag

ECHO

enable echo

 

ECHOE

echo ERASE as an error-correcting backspace

 

ECHOK

enable KILL

 

ECHONL

echo a newline

 

ICANON

canonical input (erase and kill processing)

 

IEXTEN

enable extended (implementation-defined) functions

 

ISIG

enable signals

 

NOFLSH

disable flush after interrupt, quit, or suspend

 

TOSTOP

send SIGTTOU for background output

Program 6.14 shows a function that sets the current terminal to be in noncanonical mode with single-character input. After a setnoncanonical call, the terminal device driver delivers each character as typed, treating the ERASE and KILL characters as ordinary characters. The function returns 0 on success. If an error occurs, setnoncanonical returns –1 and sets errno.

Example 6.22. 

How would you set the terminal back to canonical mode after a call to the function setnoncanonical?

Answer:

This may be a problem on some systems. POSIX allows c_cc[MIN] and c_cc[TIME] to be used for VEOF and VEOL in canonical mode. On some systems, a call to setnoncanonical will overwrite these values. Unless these values have been saved, there is no way to restore them to their original values. If you just set the ICANON bit in the c_lflag of the struct termios structure, it may not return the terminal to the previous canonical mode state. Program 6.15 provides a method for handling this.

Table 6.3. Parameters for noncanonical mode processing.

case

meaning

 

MIN > 0, TIME > 0

TIME is an interbyte timer If TIME expires or MIN bytes are received, read is satisfied.

 

MIN > 0, TIME = 0

read blocks until at least MIN bytes received

 

MIN = 0, TIME > 0

read is satisfied when a single byte arrives or TIME expires

 

MIN = 0, TIME = 0

minimum of number of bytes requested or number of bytes available returned

 

Example 6.23. 

Suppose that standard input has been set to noncanonical mode. Five characters have been typed at the keyboard. You try to read 10 bytes from standard input. What happens in each of the following cases?

  1. MIN = 5 and TIME = 0

  2. MIN = 0 and TIME = 100

  3. MIN = 20 and TIME = 100

  4. MIN = 3 and TIME = 100

  5. MIN = 20 and TIME = 0

  6. MIN = 0 and TIME = 0

Answer:

  1. You receive 5 bytes immediately.

  2. You receive 5 bytes immediately.

  3. You receive 5 bytes after a delay of 10 seconds.

  4. You receive 5 bytes immediately.

  5. You block until at least 5 more characters are entered.

  6. You receive 5 bytes immediately.

Example 6.14. setnoncanonical.c

A function that sets the terminal associated with the caller to perform single character input (rather than line processing).

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include "restart.h"

int ttysetchar(int fd, int flagname, char c);

int setnoncanonical(void) {
   int error;
   int fd;
   int firsterrno = 0;
   struct termios term;
   char termbuf[L_ctermid];

   if (ctermid(termbuf) == NULL) {               /* find the terminal name */
      errno = ENODEV;
      return -1;
   }
   if ((fd = r_open2(termbuf, O_RDONLY)) == -1)       /* open the terminal */
      return -1;
   if (tcgetattr(fd, &term) == -1)                  /* get its termios */
      firsterrno = errno;
   else {
      term.c_lflag &= ~ICANON;
      while (((error = tcsetattr(fd, TCSAFLUSH, &term)) == -1) &&
              (errno == EINTR)) ;
      if (error)
         firsterrno = errno;
   }
   if (!firsterrno && (ttysetchar(fd, VMIN, 1) || ttysetchar(fd, VTIME, 0)))
      firsterrno = errno;
   if ((r_close(fd) == -1) && !firsterrno)
      firsterrno = errno;
   if (firsterrno)
      errno = firsterrno;
   return firsterrno ? -1 : 0;
}

Program 6.15 shows two functions for saving and restoring the struct termios structure. Each takes a pointer to a struct termios structure as a parameter and returns 0 on success. On error these functions return –1 with errno set. The correct way to temporarily set noncanonical mode is as follows.

  1. Call gettermios to save struct termios structure in a local variable.

  2. Call setnoncanonical.

  3. Do the noncanonical mode processing.

  4. Restore the original terminal mode by calling settermios.

Example 6.15. savetermios.c

Functions for saving and restoring the terminal mode.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include "restart.h"

int gettermios(struct termios *termp) {
   int fd;
   int firsterrno = 0;
   char termbuf[L_ctermid];

   if (ctermid(termbuf) == NULL) {                /* find the terminal name */
      errno = ENODEV;
      return -1;
   }
   if ((fd = r_open2(termbuf, O_RDONLY)) == -1)        /* open the terminal */
      return -1;
   if (tcgetattr(fd, termp) == -1)                       /* get its termios */
      firsterrno = errno;
   if ((r_close(fd) == -1) && !firsterrno)
      firsterrno = errno;
   if (firsterrno) {
      errno = firsterrno;
      return -1;
   }
   return 0;
}

int settermios(struct termios *termp) {
   int error;
   int fd;
   int firsterrno = 0;
   char termbuf[L_ctermid];

   if (ctermid(termbuf) == NULL) {                /* find the terminal name */
      errno = ENODEV;
      return -1;
   }
   if ((fd = r_open2(termbuf, O_RDONLY)) == -1)        /* open the terminal */
      return -1;
   while (((error = tcsetattr(fd, TCSAFLUSH, termp)) == -1) &&
           (errno == EINTR)) ;
   if (error)
      firsterrno = errno;
   if ((r_close(fd) == -1) && !firsterrno)
      firsterrno = errno;
   if (firsterrno) {
      errno = firsterrno;
      return -1;
   }
   return 0;
}

Audio Device

An audio device (microphone, speaker) is an example of a peripheral device represented by a special file. The device designation for this device on many systems is /dev/audio. The discussion in this section illustrates the nature of special files, but it is specific to Sun systems. The audio device may behave differently on different systems. Note: If you logged in from an ASCII terminal or X-terminal, you cannot use the audio device even if the system has one.

Example 6.24. 

The following command plays the audio file sample.au on the speaker of a Sun workstation.

cat sample.au > /dev/audio

The audio device may support several audio formats, and you may have to set the audio device for the proper format before Example 6.24 works correctly. Audio files typically contain a header giving information about the format of the audio file. Sending the file directly to the audio device, as in this example, may cause the header to be interpreted as audio data. You will probably hear a series of clicks at the beginning of the playback. Many systems have a utility for playing audio. The utility reads the header and uses this information to program the audio device for the correct format. This command utility may be called audioplay or just play.

In this section, we assume that we are using audio files in a fixed format and that the audio device has already been set for that format.

Program 6.16 contains a library of functions for reading and writing from the audio device. None of these library functions pass the file descriptor corresponding to the audio device. Rather, the audio library is treated as an object that calling programs access through the provided interface (open_audio, close_audio, read_audio and write_audio).

The open_audio opens /dev/audio for read or write access, using blocking I/O. If the audio device has already been opened, open hangs until the device is closed. If the audio device had been opened with the O_NONBLOCK flag, open would have returned with an error if the device were busy.

The open_audio function attempts to open both the microphone and the speaker. A process that will only record can call open with O_RDONLY; a process that will only play can call open with O_WRONLY. If it is interrupted by a signal, open_audio restarts open.

The speaker can handle data only at a predetermined rate, so write_audio may not send the entire buffer to the speaker in one write function. Similarly, read_audio reads only the data currently available from the microphone and returns the number of bytes actually read. The get_record_buffer_size function uses ioctl to retrieve the size of the blocks that the audio device driver reads from the audio device.

Example 6.16. audiolib.c

The audio device object and its basic operations.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stropts.h>
#include <unistd.h>
#include <sys/audio.h>
#include "restart.h";
#define AUDIO "/dev/audio"

static int audio_fd = -1;   /* audio device file descriptor */

int open_audio(void) {
   while (((audio_fd = open(AUDIO, O_RDWR)) == -1) && (errno == EINTR)) ;
   if (audio_fd == -1)
      return -1;
   return 0;
}

void close_audio(void) {
   r_close(audio_fd);
   audio_fd = -1;
}

int read_audio(char *buffer, int maxcnt) {
   return r_read(audio_fd, buffer, maxcnt);
}

int write_audio(char *buffer, int maxcnt) {
   return r_write(audio_fd, buffer, maxcnt);
}

int get_record_buffer_size(void) {
   audio_info_t myaudio;
   if (audio_fd == -1)
      return -1;
   if (ioctl(audio_fd, AUDIO_GETINFO, &myaudio) == -1)
      return -1;
   else
      return myaudio.record.buffer_size;
}

The ioctl function provides a means of obtaining device status information or setting device control options. The ioctl function has variable syntax. Its first two parameters are an open file descriptor and an integer specifying the type of request. Different requests may require different additional parameters.

SYNOPSIS

  #include <stropts.h>

  int ioctl(int fildes, int request, .... /* arg */);
                                                            POSIX

If successful, ioctl returns a value other than –1 that depends on the request value. If unsuccessful, ioctl returns –1 and sets errno. The mandatory errors depend on the value of request. See the man page for ioctl for further information.

The ioctl function provides a means of obtaining device status information or setting device control options. The Sun Solaris operating environment uses the AUDIO_GETINFO request of ioctl to retrieve information about the audio device. The audio_info_t type defined in audioio.h holds configuration information about the audio device.

typedef struct audio_info {
   audio_prinfo_t   play;          /* output status information */
   audio_prinfo_t   record;        /* input status information */
   uint_t           monitor_gain;  /* input to output mix */
   uchar_t          output_muted;  /* nonzero if output muted */
   uchar_t _xxx[3];                /* Reserved for future use */
   uint_t _yyy[3];                 /* Reserved for future use */
} audio_info_t;

The audio_prinfo_t member of the preceding structure is defined as follows.

struct audio_prinfo {
   /* The following values describe the audio data encoding */
   uint_t   sample_rate;  /* samples per second */
   uint_t   channels;     /* number of interleaved channels */
   uint_t   precision;    /* number of bits per sample */
   uint_t   encoding;     /* data encoding method */

   /* The following values control audio device configuration */
   uint_t   gain;         /* volume level */
   uint_t   port;         /* selected I/O port */
   uint_t   avail_ports;  /* available I/O ports */
   uint_t   _xxx[2];      /* reserved for future use */
   uint_t   buffer_size;  /* I/O buffer size */

   /* The following values describe the current device state */
   uint_t   samples;      /* number of samples converted */
   uint_t   eof;          /* end-of-file counter (play only) */
   uchar_t  pause;        /* nonzero if paused, zero to resume */
   uchar_t  error;        /* nonzero if overflow/underflow */
   uchar_t  waiting;      /* nonzero if a process wants access */
   uchar_t  balance;      /* stereo channel balance */
   ushort_t minordev;

   /* The following values are read-only device state flags */
   uchar_t  open;         /* nonzero if open access granted */
   uchar_t  active;       /* nonzero if I/O active */
} audio_prinfo_t;

The buffer_size member of the audio_prinfo_t structure specifies how large a chunk of audio data the device driver accumulates before passing the data to a read request. The buffer_size for play specifies how large a chunk the device driver accumulates before sending the data to the speaker. Audio tends to sound better if the program sends and receives chunks that match the corresponding buffer_size settings. Use ioctl to determine these sizes in an audio application program. The get_record_buffer_size function in Program 6.16 returns the appropriate block size to use when reading from the microphone, or –1 if an error occurs.

Program 6.17 reads from the microphone and writes to the speaker. Terminate the program by entering Ctrl-C from the keyboard. It is best to use headphones when trying this program to avoid feedback caused by a microphone and speaker in close proximity. The audiolib.h header file contains the following audio function prototypes.

int open_audio(void);
void close_audio(void);
int read_audio(char *buffer, int maxcnt);
int write_audio(char *buffer, int length);

Example 6.17. audiocopy.c

A simple program that reads from the microphone and sends the results to the speaker.

#include <stdio.h>
#include <stdlib.h>
#include "audiolib.h"

#define BUFSIZE 1024
int main (void) {
   char buffer[BUFSIZE];
   int bytesread;

   if (open_audio() == -1) {
      perror("Failed to open audio");
      return 1;
   }
   for( ; ; ) {
      if ((bytesread = read_audio(buffer, BUFSIZE)) == -1) {
          perror("Failed to read microphone");
          break;
      } else if (write_audio(buffer, bytesread) == -1) {
          perror("Failed to write to speaker");
          break;
      }
   }
   close_audio();
   return 1;
}

The implementation of Program 6.16 opens the audio device for blocking I/O. Nonblocking reads are complicated by the fact that read can return –1 either if there is an error or if the audio device is not ready with the data. The latter case has an errno value of EAGAIN and should not be treated as an error. The primary reason for opening the audio device in nonblocking mode is so that open does not hang when the device is already open. An alternative is to open the audio device in nonblocking mode and then to use fcntl to change the mode to blocking.

Example 6.25. nonblockingaudio.c

The following program opens the audio device for nonblocking I/O. It then reads BLKSIZE bytes from the audio device into a buffer. It does nothing with the audio that is read in other than display the number of bytes read.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "restart.h"
#define AUDIO_DEVICE "/dev/audio"
#define BLKSIZE 1024

int main(void) {
   int audiofd;
   char *bp;
   char buffer[BLKSIZE];
   unsigned bytesneeded;
   int bytesread;

   if ((audiofd = open(AUDIO_DEVICE, O_NONBLOCK | O_RDWR)) == -1) {
      perror("Failed to open audio device");
      return 1;
    }

   bp = buffer;
   bytesneeded = BLKSIZE;
   while(bytesneeded != 0) {
      bytesread = r_read(audiofd, bp, bytesneeded);
      if ((bytesread == -1) && (errno != EAGAIN))
         break;
      if (bytesread > 0) {
         bp += bytesread;
         bytesneeded -= bytesread;
      }
   }
   fprintf(stderr, "%d bytes read
", BLKSIZE - bytesneeded);
   return 0;
}

In testing audio programs, keep in mind that the audio device is closed when the program exits. If the audio buffer still holds data that has not yet reached the speakers, that data may be lost. The draining of a device after a close is system dependent, so read the man page before deciding how to handle the situation.

Exercise: Audio

The exercises in this section assume that the operating system handles the audio device in a way similar to how the Solaris operating environment handles it.

  1. Add the following access functions to the audio object of Program 6.16.

    1. The play_file function plays an audio file. It has the following prototype.

      int play_file(char *filename);
      

      The play_file outputs the audio file specified by filename to the audio device, assuming that the speaker has already been opened. If successful, play_file returns the total number of bytes output. If unsuccessful, play_file returns –1 and sets errno.

    2. The record_file function saves incoming audio data to a disk file. It has the following prototype.

      int record_file(char *filename, int seconds);
      

      The record_file function saves audio information for a time interval of seconds in the file given by filename, assuming that the microphone has already been opened. If successful, record_file returns the total number of bytes recorded. If unsuccessful, record_file returns –1 and sets errno.

    3. The get_record_sample_rate function determines the sampling rate for recording. It has the following prototype.

      int get_record_sample_rate(void);
      

      If successful, get_record_sample_rate returns the sampling rate for recording. If unsuccessful, get_record_sample_rate returns –1 and sets errno.

    4. The get_play_buffer_size returns the buffer size that the audio device driver uses to transfer information to the audio output device. It has the following prototype.

      int get_play_buffer_size(void);
      

      If successful, get_play_buffer_size returns the buffer size for recording. If unsuccessful, get_play_buffer_size returns –1 and sets errno.

    5. The get_play_sample_rate function determines the sampling rate for playing. It has the following prototype.

      int get_play_sample_rate(void);
      

      If successful, get_play_sample_rate returns the sampling rate used for playing audio files on the speaker. If unsuccessful, get_play_sample_rate returns –1 and sets errno. A rate of 8000 samples/second is considered voice quality.

    6. The set_play_volume function changes the volume at which sound plays on the speaker. It has the following prototype.

      int set_play_volume(double volume);
      

      The set_play_volume sets the gain on the speaker. The volume must be between 0.0 and 1.0. If successful, set_play_volume returns 0. If unsuccessful, set_play_volume returns –1 and sets errno.

    7. The set_record_volume function changes the volume of incoming sound from the microphone. It has the following prototype.

      int set_record_volume(double volume);
      

      The set_record_volume function sets the gain on the microphone. The volume value must be between 0.0 and 1.0. If successful, set_record_volume returns 0. If unsuccessful, it returns –1 and sets errno.

  2. Rewrite Program 6.17 to copy from the microphone to the speaker, using the preferred buffer size of each of these devices. Call get_record_buffer_size and get_play_buffer_size to determine the respective sizes. Do not assume that they are the same in your implementation.

  3. Use the record_file function to create eight audio files, each of which is ten seconds in duration: pid1.au, pid2.au, and so on. In the file pid1.au, record the following message (in your voice): “I am process 1 sending to standard error”. Record similar messages in the remaining files. Play the files back by using the play_file function.

  4. Be sure to create a header file (say, audiolib.h) with the prototypes of the functions in the audio library. Include this header file in any program that calls functions from this library.

  5. Record your speaking of the individual numerical digits (from 0 to 9) in ten different files. Write a function called speak_number that takes a string representing an integer and speaks the number corresponding to the string by calling play_file to play the files for the individual digits. (How does the program sound compared to the computer-generated messages of the phone company?)

  6. Replace the fprintf statement that outputs the various IDs in Program 3.1 on page 67 with a call to play_file. For the process with i having value 1, play the file pid1.au, and so on. Listen to the results for different numbers of processes when the speaker is opened before the fork loop. What happens when the speaker is opened after the fork? Be sure to use snprintf to construct the filenames from the i value. Do not hardcode the filenames into the program.

  7. Make a recording of the following statement in file pid.au: “My process ID is”. Instead of having each process in the previous part play a pidi.au file corresponding to its i number, use speak_number to speak the process ID. Handle the parent and child IDs similarly.

  8. Redesign the audio object representation and access functions so that processes have the option of opening separately for read and for write. Replace audio_fd with the descriptors play_fd and record_fd. Change the open_audio so that it sets both play_fd and record_fd to the file descriptor value returned by open. Add the following access functions to the audio object of Program 6.16.

    1. The open_audio_for_record function opens the audio device for read (O_RDONLY). It has the following prototype.

      int open_audio_for_record(void);
      

      The function returns 0 if successful or –1 if an error occurs.

    2. The open_audio_for_play function opens the audio device for write (O_WRONLY). It has the following prototype.

      int open_audio_for_play(void);
      

      The open_audio_for_play function returns 0 if successful or –1 if an error occurs.

Exercise: Barriers

A barrier is a synchronization construct used by cooperating processes to block until all processes reach a particular point. The exercises in this section use a FIFO to implement a barrier. They extend the simple barrier of Program 6.2.

Write a barrier server that takes two command-line arguments: the name of a barrier (name) and the size of the barrier (n). The size represents the number of processes that need to block at that barrier. The server creates a named pipe, name.request, to handle requests for the barrier and a named pipe, name.release, for writing the release characters. For example, if the barrier name is mybarrier, the server creates pipes called mybarrier.request and mybarrier.release. The server then does the following in a loop.

  1. Open name.request for reading.

  2. Read exactly n characters from name.request.

  3. Close name.request.

  4. Open name.release for writing.

  5. Write exactly n characters to name.release.

  6. Close name.release.

Write the following barrier function for use by the clients.

int waitatbarrier(char *name);

The function blocks at the barrier with the given name. If successful, the waitatbarrier function returns 0. If unsuccessful, waitatbarrier returns –1 and sets errno. The waitatbarrier does the following in a loop.

  1. Open name.request for writing.

  2. Write one byte to name.request.

  3. Close name.request.

  4. Open name.release for reading.

  5. Read one byte from name.release.

  6. Close name.release.

Be sure that waitatbarrier closes any pipe that it opens, even if an error occurs. If an error occurs on a read or write, save the value of errno, close the pipe, restore errno and return –1.

This function works because of the way blocking is done when a pipe is opened. An open operation for read will block until at least one process has called open for writing. Similarly, an open operation for write will block until at least one process called open for reading. The client will block on the open of the request pipe until the server has opened it. It will then block on the open of the release pipe until the server has read the bytes from all of the other processes and opened the release pipe for writing. A second attempt to use the barrier with the same name will block on the open of the request pipe until all of the processes have passed the first barrier since the server has closed the request pipe.

Test your clients and server by modifying the process chain of Program 3.1 on page 67 or the process fan of Program 3.2 on page 68. Have each one use the same named barrier several times. Each time they wait at the barrier, they should print a message. If the modification is working correctly, all the first messages should be printed before any of the second ones. Are there any circumstances under which reusing a barrier can fail?

Generalize your barrier server to handle many different barriers. You should still have one request pipe. The clients send the name and size of the barrier they are requesting in a single write to the request pipe. The server keeps a dynamic list of the barriers. If a request for a new barrier comes in, the server creates a new release pipe, adds this barrier to its list, and creates a child process to handle the barrier. If a request for an old barrier comes in, it is ignored.

Clients can create as many barriers as they need, but each client now has to know how many other clients there are. Alternatively, the server can be given the number of clients on the command line when it starts up. See if you can devise a mechanism for the server to find out from the clients how many they are. Be careful, this is not easy.

Exercise: The stty Command

Do the following to become more familiar with terminal control.

  1. Read the man page on struct termios.

  2. Execute stty -a and try to understand the different fields.

  3. Compare the facilities provided by the specific terminal calls to those provided by use of ioctl. Read the struct termios information in Section 7 of the man pages for additional information.

Read the man page for stty and write your own program modeled after it.

Exercise: Client-Server Revisited

Section 6.4 developed an implementation of request-reply using named pipes. The implementation was limited because multiple readers do not behave well with pipes. Write a new version of these programs in which the clients send their process IDs rather than single characters. To service each request, the server uses a FIFO whose name includes the process ID of the client. After servicing the request, the server closes the response FIFO and unlinks it. Be sure that no client can cause this version of the server to exit.

Although the clients are sending multibyte process IDs to the server, the server will not receive interleaved IDs because writes to the pipe are atomic. Since only one process is reading from each pipe, reads do not need to be atomic.

If the server is responsible for creating the pipe from the process ID that is sent to it, the client may try to open the pipe before it exists, generating an error. Have the client create the reply pipe before sending its ID to the server on the request pipe. After sending its ID, the client opens the reply pipe for reading and blocks until the server opens it for writing. After the client receives its reply, it can close and unlink the reply pipe.

Note that both the client and the server need to run in the same directory so that they can access the same pipes.

Additional Reading

The USENIX Conference Proceedings are a good source of current information on tools and approaches evolving under UNIX. Operating Systems Review is an informal publication of SIGOPS, the Association for Computing Machinery Special Interest Group on Operating Systems. Operating Systems Review sometimes has articles on recent developments in the area of file systems and device management.

Advanced Programming in the UNIX Environment by Stevens [112] contains some nice case studies on user-level device control, including a program to control a PostScript printer, a modem dialer, and a pseudo terminal management program. Understanding the LINUX Kernel: From I/O Ports to Process Management by Bovet and Cesati [16] discusses underlying I/O implementation issues in LINUX. Data Communications Networking Devices by Held [47] is a general reference on network device management. Finally, SunOS 5.3 Writing Device Drivers is a very technical guide to implementing drivers for block-oriented and character-oriented devices under Solaris [119].

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset