The way to indicate this specialness is up to you, but the easiest thing is to put a flag in the file descriptor data structure that indicates whether the descriptor refers to the console or not. Initially, the flag is set (indicating the console) for file descriptors 0, 1, and 2. If the code closes a file descriptor that refers to the console, then this flag should be cleared. If the file descriptor is "duped" then the flag must be duped as well. Don't worry about getting open() to work for the console. If all file descriptors for the console are closed, then it is okay if there is no way to reopen it.
Further, in a regular file system, a file descriptor points to an open file table entry. Since we won't be implementing files in this class, it is okay to have your file descriptors point to your pipe data structure. That is, there will be two kinds of file descriptors: 0, 1, and 2 which work the same way as they do in the previous lab, and file descriptors that are created by the pipe() system call that point to a pipe data structure you design.
Read the man page on pipe() carefully. In particular, note that it returns two separate file descriptors -- one for reading and another for writing -- that can be each closed separately. However both file descriptors refer to the same pipe. Thus your file descriptor should contain a way to distinguish a read file descriptor from a write file descriptor since closing a reader will mean something different than closing a writer.
Think about how the bounded buffer problem works (recall that we studied the bounded buffer problem in the Client/Trader Lectures). You'll need to be able to determine when there is data in the pipe (because it has been written), where that data is, and when it has been delivered to a reader. Each byte goes into the pipe when it is written, and comes out of the pipe when it is read. Further, the data must be read out of the pipe in the order that it is written (i.e. in FIFO order).
Pipes add an additional wrinkle that has to do with what happens when there are multiple writers and multiple readers. Let's imagine that there are two processes writing a pipe and two processes reading it (don't worry yet how this situation came to be -- I'll discuss it below). Technically, if the slots are bytes, you are entitled to schedule the writers round-robin so that the bytes go into the pipe in an interleaved way. Similarly, you are entitled to interleave the readers on the read side.
Pipes are different from the bounded buffer example, however, because they try and preserve buffer boundaries. For example, imagine that
Notice that this can get tricky when the sizes do not match. For example, what happens if the writers in this example each write three bytes but the readers try and read 10? That's tricky because if the two writers run before any reader, the first reader has enough space in her read buffer for both writes. You get some leeway in this case but in my view, the correct thing to do would be to return 6 bytes (3 bytes from the first write and 3 bytes from the second) to the first reader and have the second reader block.
When in doubt, ask Linux. That is, write a test program that tests these scenarios on Linux and try (as best you can) to emulate the Linux functionality. However don't go overboard. We won't be trying to trip you up by testing wicked corner cases so you should spend a ton of time trying to make sure you exactly match Linux.
However one thing you should not do is to make it so that a reader or a writer blocks indefinitely if it is possible to make progress. For example, imagine that
One additional wrinkle concerns what happens when a reader or a writer calls close() on an end of a pipe. You will need to implement close() for file descriptors. Additionally, your implementation will need to recognize when the last writer has closed and deliver an EOF to any readers after the last byte from the pipe has been read. For example, imagine
In the reverse case, if there are no readers for a pipe, a writer attempting to write the pipe should get an EBADF error (or some other error, but EBADF is a good choice) indicating that file descriptor is invalid. You need to be careful with test codes here.
You will also need to allocate two free file descriptors from the PCB whenever a pipe system call is executed by a user program. It is okay for the file descriptor table to be of fixed size (make it large enough to handle several pipes simultaneously along with stdin, stdout, and stderr). If you are out of file descriptors, then the pipe system call should return an error.
Similarly, you'll need to free file descriptors when a file descriptor is closed.
Now let's imagine that the process forks. You will need to make copies of the file descriptors in the child process so that the parent and child file descriptor table look the same. Notice also, though, that you should bump the reference counts both to 2 since there are now two open writer file descriptors (one in the parent and one in the child) and also two open read descriptors.
Now let's say that the parent closed pd[0] (the read side of the pipe) and the child closes pd[1] (the write side of the pipe). That is, the parent intends to write into the pipe but never to read and, similarly the child intends to read from the pipe but never to write. What should the reference counts be after the two close operations? The writer count should be 1, and the reader count should be 1.
Notice that you'll need to make sure the pipe reference counts are correct in your process exit processing as well. For example, imagine that the writer in this case exits. The call to exit() in your OS should decrement the writer reference count and treat the reader as if the writer has closed the pipe. That is, the file descriptor gets closed either when the process calls close() on it, or when it dies.