Pipes

This third form of IPC implemented in GNO/ME is one of the most powerful features ever put into an operating system. A pipe is a conduit for information from one process to another. Pipes are accessed just like regular files; the same GS/OS and ToolBox calls currently used to manipulate files are also used to manipulate pipes. When combined with GNO/ME standard I/O features, pipes become very powerful indeed. For examples on how to use gsh to connect applications with pipes, see the GNO Shell Reference Manual.

Pipes are unidirectional channels between processes. Pipes are created with the pipe(2) system call, which returns two GS/OS refNums; one for the write end, and one for the read end. An attempt to read from the write end or vice-versa results in an error.

Pipes under GNO/ME are implemented as a circular buffer of 4096 bytes. Semaphores are employed to prevent the buffer from overflowing, and to maintain synchronization between the processes accessing the pipe. This is done by creating two semaphores; their counts indicate how many bytes are available to be read and how many bytes may be written to the buffer (0 and 4096 initially). If an I/O operation on the pipe would result in the buffer being emptied or filled, the calling process is blocked until the data (or space) becomes available.

The usual method of setting up a pipeline between processes, used by gsh and utilities such as script, is to make the pipe call and then fork(2) off the processes to be connected by the pipe.

    /* No error checking is done in this fragment.  This is
     * left as an exercise for the reader.
     */

    int fd[2];
    int 
    testPipe(void)
    {
        pipe(fd);      /* create the pipe */
        fork(writer);  /* create the writer process */
        fork(reader);  /* create the reader process */
        close(fd[0]);  /* we don't need the pipe anymore, because */
        close(fd[1]);  /*    the children inherited them          */
    
        { wait for children to terminate ... }
    }

    void 
    writer(void) {
        /* reset the standard output to the write pipe */
        dup2(STDOUT_FILENO, fd[1]);
    
        /* we don't need the read end */
        close(fd[0]);
        { exec writer process ...}
    }

    void 
    reader(void) {
        /* reset the standard input to the write pipe */
        dup2(STDIN_FILENO, fd[0]);
    
        /* we don't need the write end */
        close(fd[1]);
        { exec reader process ...}
    }

Recall that when a new process is forked, it inherits all of the open files of it's parent; thus, the two children here inherit not only standard I/O but also the pipe. After the forks, the parent process closes the pipe and each of the child processes closes the end of the pipe it doesn't use. This is actually a necessary step because the kernel must know when the reader has terminated in order to also stop the writer (by sending SIGPIPE. Since each open refNum to the read end of the pipe is counted as a reader, any unnecessary copies must be closed.

For further examples of implementing and programming pipes, see the sample source code for pipe.c.

Feedback