In the event that the OP cares about internals, a pipe is often implemented as circular buffer. That is the OS puts aside a chunk of memory and keeps track of what segment of it is currently in use (wrapping around from the end to the beginning as needed):
+-------+
| front |
+-------+
| +-------+
| | back |-------\
V +-------+ V
+---+---+---+---+---+ +---+---+----+---+---+---+
| | c | o | n | t | ... | e | r | \n | | | | // the buffer itself
+---+---+---+---+---+ +---+---+----+---+---+---+
And we control the behavior with these rules:
- The wrap-around behavior is accomplished by doing all the position arithmetic modulo the buffer length (watch out if our language uses 1 indexed arrays!).
- If
front
and back
are ever the same (as in the starting condition), then the pipe is empty and attempts to read from it will block until there is something to read.
- If
back
is (front-1)
(modulo the length, remember!), the buffer is full and attempts to write will block until there is room.
- Reading a value from the pipe returns the contents at
front
, and advances front by one.
- Writing a value to the pipe advance
back
by one and inserts the new input at this location.
More complicated schemes involving allowing the buffer to grow when there is a need and memory is available are possible (and indeed are common), and the above can be simplified by make the buffer itself only one character long (meaning that we don't even need front and back anymore) but you pay a cost in terms of a lot of locking and many unnecessary context switches.