15.7 Exercise: Implementing Pipes with Shared MemoryThis section develops a specification for a software pipe consisting of a semaphore set to protect access to the pipe and a shared memory segment to hold the pipe data and state information. The pipe state information includes the number of bytes of data in the pipe, the position of next byte to be read and status information. The pipe can hold at most one message of maximum size _POSIX_PIPE_BUF. Represent the pipe by the following pipe_t structure allocated in shared memory. typedef struct pipe { int semid; /* ID of protecting semaphore set */ int shmid; /* ID of the shared memory segment */ char data[_POSIX_PIPE_BUF]; /* buffer for the pipe data */ int data_size; /* bytes currently in the pipe */ void *current_start; /* pointer to current start of data */ int end_of_file; /* true after pipe closed for writing */ } pipe_t; A program creates and references the pipe by using a pointer to pipe_t as a handle. For simplicity, assume that only one process can read from the pipe and one process can write to the pipe. The reader must clean up the pipe when it closes the pipe. When the writer closes the pipe, it sets the end_of_file member of pipe_t so that the reader can detect end-of-file. The semaphore set protects the pipe_t data structure during shared access by the reader and the writer. Element zero of the semaphore set controls exclusive access to data. It is initially 1. Readers and writers acquire access to the pipe by decrementing this semaphore element, and they release access by incrementing it. Element one of the semaphore set controls synchronization of writes so that data contains only one message, that is, the output of a single write operation. When this semaphore element is 1, the pipe is empty. When it is 0, the pipe has data or an end-of-file has been encountered. Initially, element one is 1. The writer decrements element one before writing any data. The reader waits until element one is 0 before reading. When it has read all the data from the pipe, the reader increments element one to indicate that the pipe is now available for writing. Write the following functions. pipe_t *pipe_open(void);
int pipe_read(pipe_t *p, char *buf, int bytes);
int pipe_write(pipe_t *p, char *buf, int bytes); behaves like an ordinary blocking write function. The algorithm for pipe_write is as follows.
Perform a semop on p->semid to atomically decrement both semaphore elements zero and one. Copy at most _POSIX_PIPE_BUF bytes from buf into the pipe buffer. Set p->data_size to the number of bytes actually copied, and set p->current_start to 0. Perform another semop call to atomically increment semaphore element zero of the semaphore set. If successful, return the number of bytes copied. If an error occurs, return 1 with errno set. int pipe_close(pipe_t *p, int how);
Test the software pipe by writing a main program that is similar to Program 6.4. The program creates a software pipe and then forks a child. The child reads from standard input and writes to the pipe. The parent reads what the child has written to the pipe and outputs it to standard output. When the child detects end-of-file on standard input, it closes the pipe for writing. The parent then detects end-of-file on the pipe, closes the pipe for reading (which destroys the pipe), and exits. Execute the ipcs command to check that everything was properly destroyed. The above specification describes blocking versions of the functions pipe_read and pipe_write. Modify and test a nonblocking version also. |