{
    "items": [
        {
            "type": [
                "h-entry"
            ],
            "properties": {
                "name": [
                    "How are Unix pipes implemented?"
                ],
                "summary": [
                    "This article is about how pipes are implemented the Unix kernel. I was a little disappointed that a recent article titled \u201cHow do Unix pipes work?\u201d was not about the internals, and curious enough to go digging in some old sources to try to answer the question."
                ],
                "category": [
                    "unix"
                ],
                "uid": [
                    "https://toroid.org/unix-pipe-implementation"
                ],
                "url": [
                    "https://toroid.org/unix-pipe-implementation"
                ],
                "published": [
                    "2020-03-23T19:47:55+0000"
                ],
                "updated": [
                    "2020-03-27T12:46:27+0000"
                ],
                "content": [
                    {
                        "html": "<p class=\"p-summary\" property=\"abstract\">\nThis article is about how pipes are implemented the Unix kernel. I was a\nlittle disappointed that a recent article titled\n\u201c<a href=\"https://www.vegardstikbakke.com/how-do-pipes-work-sigpipe/\">How do Unix pipes work?</a>\u201d\nwas <em>not</em> about the internals, and curious enough to go\ndigging in some old sources to try to answer the question.\n\n</p><h2>What are we talking about?</h2>\n\n<p>\nPipes are \u201cperhaps the single most striking invention in\nUnix\u201d\u202f\u2014\u2009a defining characteristic of the Unix philosophy of\ncomposing small programs together, and a familiar sight in the Unix\nshell:\n\n</p><pre class=\"code\">\n$ echo hello | wc -c\n6\n</pre>\n\n<p>\nThis functionality depends on the kernel providing a system call named\npipe, which is described by the\n<a href=\"http://man7.org/linux/man-pages/man7/pipe.7.html\">pipe(7)</a>\nand\n<a href=\"http://man7.org/linux/man-pages/man2/pipe.2.html\">pipe(2)</a>\nmanual pages:\n\n</p><blockquote>\n<p>\nPipes provide a unidirectional interprocess communication channel. A\npipe has a read end and a write end. Data written to the write end of a\npipe can be read from the read end of the pipe.\n\n</p><p>\nA pipe is created using pipe(2), which returns two file descriptors, one\nreferring to the read end of the pipe, the other referring to the write\nend.\n</p></blockquote>\n\n<p>\nTracing the execution of the command above shows the creation of the\npipe and the flow of data through it from one process to another:\n\n</p><pre class=\"code\" style=\"margin: 0\">\n$ strace -qf -e execve,pipe,dup2,read,write \\\n    sh -c 'echo hello | wc -c'\n</pre>\n<div style=\"background: #eeeeee; margin: 0\">\n<pre class=\"code\" style=\"margin: 0\">\nexecve(\"/bin/sh\", [\"sh\", \"-c\", \"echo hello | wc -c\"], \u2026)\n<b>pipe</b>([3, 4])                            = 0\n</pre>\n</div>\n<div style=\"background: #cccccc; margin: 0\">\n<pre class=\"code\" style=\"margin: 0\">\n[pid <b>2604795</b>] dup2(4, 1)                = 1\n[pid 2604795] <b>write</b>(1, \"hello\\n\", 6)    = 6\n</pre>\n</div>\n<div style=\"background: #eeeeee; margin: 0\">\n<pre class=\"code\" style=\"margin: 0\">\n[pid <b>2604796</b>] dup2(3, 0)                = 0\n[pid 2604796] execve(\"/usr/bin/wc\", [\"wc\", \"-c\"], \u2026)\n[pid 2604796] <b>read</b>(0, \"hello\\n\", 16384) = 6\n[pid 2604796] write(1, \"6\\n\", 2)        = 2\n</pre>\n</div>\n\n<p>\nThe parent process calls pipe() to obtain connected fds, one child\nwrites to one fd and another reads the same data from the other fd. (The\nshell uses dup2 to \"rename\" fds 3 and 4 to match stdin and stdout.)\n\n</p><p>\nWithout pipes, the shell would have to write the output from one process\nto a file and arrange for the next process to read from it, at the\nexpense of some overhead and some disk space\u202f\u2014\u2009but pipes\nprovide more than just the convenience of avoiding temporary files:\n\n</p><blockquote>\nIf a process attempts to read from an empty pipe, then read(2) will\nblock until data is available. If a process attempts to write to a full\npipe (see below), then write(2) blocks until sufficient data has been\nread from the pipe to allow the write to complete.\n</blockquote>\n\n<p>\nThis important property, along with the POSIX requirement that writing\nup to PIPE_BUF (at least 512) bytes into a pipe must be atomic, allows\nprocesses to communicate with each other over a pipe in a way that\nregular files (which have no such guarantees) could not support.\n\n</p><p>\nWith a regular file, a process could write all its output and hand it\nover to the next process, or they could operate in lock-step by using an\nexternal signalling mechanism (like a semaphore) to let each other know\nwhen they were done reading or writing. Pipes eliminate all this\ncomplexity.\n\n</p><h2>What are we looking for?</h2>\n\n<p>\nA little hand-waving helps to imagine how pipes might work. You would\nneed to allocate a memory buffer and some state, have functions to add\nand remove data from the buffer and some glue to invoke them through\nread and write operations on file descriptors, and sprinkle some locking\non top to implement the special behaviour described above.\n\n</p><p>\nWe are now equipped to interrogate the kernel source under bright lights\nto confirm or deny this vague mental model, but always prepared to run\ninto any surprises along the way.\n\n</p><h2>Where do we look?</h2>\n\n<p>\nI'm not sure where my (reprinted) copy of the famous\n\u201c<a href=\"https://en.wikipedia.org/wiki/Lions%27_Commentary_on_UNIX_6th_Edition,_with_Source_Code\">Lions book</a>\u201d\nwith the source code of Unix 6ed is, but thanks to\n<a href=\"https://www.tuhs.org\">The Unix Heritage Society</a>,\nwe can start by looking at even older\n<a href=\"https://minnie.tuhs.org/UnixTree\">Unix sources</a>\nonline.\n\n</p><p>\n(Browsing the TUHS archives is like being in a museum\u202f\u2014\u2009not\nonly does it provide a rare glimpse at our shared history, it also makes\nme appreciate the years of effort that went into recovering all of these\nmaterials bit by bit from old tapes and printed documentation, and\nacutely conscious of the parts that are still lost.)\n\n</p><p>\nOnce we've satisfied our curiosity about the ancient history of pipes,\nwe can take a look at some modern kernels just for the contrast.\n\n</p><p>\n</p><ul class=\"pointed\">\n<li><a href=\"https://toroid.org/unix-pipe-implementation#ancient\">Unix (1970\u20131974)</a>\n</li><li><a href=\"https://toroid.org/unix-pipe-implementation#v6\">Sixth Edition Unix (1975)</a>\n</li><li><a href=\"https://toroid.org/unix-pipe-implementation#xv6\">Xv6, a reimplementation of 6E Unix (2006)</a>\n</li><li><a href=\"https://toroid.org/unix-pipe-implementation#linux\">Linux v0.01 (1991)</a>\n</li><li><a href=\"https://toroid.org/unix-pipe-implementation#modern\">Modern kernels (Linux, Free/Net/OpenBSD)</a>\n</li></ul>\n\n<p>\nIncidentally, pipe is system call number 42 in the sysent[] table in\nUnix. Coincidence?\n\n</p><h2><a id=\"ancient\">Traditional Unix kernels (1970\u20131974)</a></h2>\n\n<p>\nI can't find any trace of pipe(2) in\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=PDP7-Unix\">PDP-7 Unix</a>\nfrom January 1970, nor in\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V1\">First edition Unix</a>\nfrom November 1971, nor in the incomplete sources of\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V2\">Second edition Unix</a>\nfrom June 1972.\n\n</p><p>\nTUHS confirms that\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V3\">Third edition Unix</a>\nfrom February 1973 was the first version to include pipes:\n\n</p><blockquote>\nThe third edition of Unix was the last version with a kernel still\nwritten in assembly code, but is the first version to include pipes. For\nmuch of 1973, the existing Third Edition was maintained and improved,\nwhile the kernel was rewritten in C to become the Fourth Edition of\nUnix.\n</blockquote>\n\n<p>\n<b>Update</b>: A reader on HN was kind enough to dig up a scanned copy\nof the 1964 memo in which Doug McIlroy proposed the idea of \u201ccoupling\nprograms like garden hose\u201d.\n\n</p><p>\n<a href=\"https://www.oreilly.com/library/view/making-sense-of/9781492042563/assets/msos_0405.png\"><img class=\"post-img\" src=\"https://toroid.org/misc/msos_0405.png\" alt=\"McIlroy pipe memo\" height=\"450\" width=\"600\" property=\"image\"></a>\n\n</p><p>\nBrian Kernighan's book,\n\u201c<a href=\"https://www.cs.princeton.edu/~bwk/memoir.html\">Unix: A History and a Memoir</a>\u201d,\nalso talks about the history of pipes and mentions this memo \u201c\u2026that hung\non the wall in my office at Bell Labs for 30 years\u201d. Here's an\n<a href=\"https://www.princeton.edu/~hos/mike/transcripts/mcilroy.htm\">interview with McIlroy</a>,\nand some more history from a\n<a href=\"https://www.cs.dartmouth.edu/~doug/sieve/sieve.pdf\">2014 paper by McIlroy</a>:\n\n</p><blockquote>\n<p>\nWhen Unix came to be, my fascination with coroutines led me to badger its author, Ken Thompson, to allow writes in one process to go not only to devices but also to matching reads in another process. Ken saw that was possible. As a minimalist, though, he wanted every system feature to carry significant weight. Did direct writes between processes offer a really major advantage over writing to a temporary file in one process and then reading it in the other? Not until I made a specific proposal with a catchyname, \u2018\u2018pipe\u2019\u2019, and shell syntax to connect processes via pipes, did Ken finally exclaim, \u2018\u2018I\u2019ll do it!\u2019\u2019\n</p><p>\nAnd he did do it. In one feverish evening Ken modified both kernel and shell, and fixed several standard programs so they could take input from standard input (potentially piped) as well as named files. The next day brought a heady explosion of applications. By the end of the week, department secretaries were using pipes to send text-processor output to the printer spooler. Not long after, Ken replaced the original API and shell syntax for pipes with cleaner conventions that have been used ever since.\n</p></blockquote>\n\n<p>\nUnfortunately, the source code for the 3E kernel is no longer available,\nand although we do have the C source code for a version of the\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V4\">Fourth edition Unix</a>\nkernel from November 1973, it predates the official release by a few\nmonths, and is missing the pipe implementation. It is sad that the\norigin of such an iconic Unix feature is lost to us, perhaps forever.\n\n</p><p>\nWe do have the pipe(2) manpages from both releases, so we can start by\nlooking at the documentation from\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V3/man/man2/pipe.2\">3E</a>\n(with certain words underlined \u201cby hand\u201d with a string of literal ^H\nbackspaces followed by underscores!). This proto-pipe(2) written in\nassembly returned only one file descriptor, but provided the basic\nfunctionality that we expect:\n\n</p><blockquote>\nThe <u>pipe</u> system call creates an I/O mechanism called a pipe. The\nfile descriptor returned can be used in both read and write operations.\nWhen the pipe is written, the data is buffered up to 504 bytes at which\ntime the writing process is suspended. A read on the pipe will pick up\nthe buffered data.\n</blockquote>\n\n<p>\nBy next year, along with a rewrite of the kernel into C, the\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V4/man/man2/pipe.2\">4E pipe(2)</a>\nhad assumed its modern form, with a prototype of \"pipe(fildes)\":\n\n</p><blockquote>\n<p>\nThe <i>pipe</i> system call creates an I/O mechanism called a pipe. The\nfile descriptors returned can be used in read and write operations. When\nthe pipe is written using the descriptor returned in r1 (resp.\nfildes[1]), up to 4096 bytes of data are buffered before the writing\nprocess is suspended. A read using the descriptor returned in r0 (resp.\nfildes[0]) will pick up the data.\n</p><p>\nIt is assumed that after the pipe has been set up, two (or more)\ncooperating processes (created by subsequent <i>fork</i> calls) will\npass data through the pipe with <i>read</i> and <i>write</i> calls.\n</p><p>\nThe shell has a syntax to set up a linear array of processes connected\nby pipes.\n</p><p>\nRead calls on an empty pipe (no buffered data) with only one end (all\nwrite file descriptors closed) return an end-of-file. Write calls under\nsimilar conditions are ignored.\n</p></blockquote>\n\n<p>\nSo the earliest\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V5/usr/sys/ken/pipe.c\">surviving pipe implementation</a>\nis from\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V5\">Fifth Edition Unix</a>\nin June 1974, but it's nearly identical to what was in the next release,\nwhich also added some comments, so we might as well skip 5E too.\n\n</p><h2><a id=\"v6\">Sixth Edition Unix (1975)</a></h2>\n\n<p>\nAs so many others have done in the past,\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6\">Sixth Edition Unix</a>\nfrom May 1975 is where we start reading the Unix code. Sixth Edition\nsources are much more widely available than earlier versions, thanks\nlargely to the Lions book:\n\n</p><blockquote>\nFor many years, the Lions book was the only Unix kernel documentation\navailable outside Bell Labs. Although the license of 6th Edition allowed\nclassroom use of the source code, the license of 7th Edition\nspecifically excluded such use, so the book spread through illegal copy\nmachine reproductions.\n</blockquote>\n\n<p>\nYou can still buy a reprint of the book, featuring cover art with\nshifty-looking students at a photocopier, but thanks to Warren Toomey\n(who went on to start TUHS), you can also download\n<a href=\"http://v6.cuzuco.com/\">a PDF of the 6E source</a>.\nI can't resist a quick glimpse at the effort that went into making this\nPDF available:\n\n</p><blockquote>\n<p>\nOver 15 years ago I had produced a replica of the Lions' source code\nlisting since I was unhappy with the quality of my n-th generation copy.\nThere was no TUHS and I had no access to any old source code. But in\n1988 I discovered an old 9-track tape being discarded of a PDP11 backup.\nIt was hard to determine what it was running, but it did have an intact\n/usr/src/ tree of which most of the files were timesamped 1979, even at\nthat time it seemed ancient. So it was either 7th edition or a\nderivative like PWB, which I believe it was.\n</p><p>\nI used this as a basis and hand edited the source back into 6th edition\nform. Some code was completely the same, some required the easy edit of\nchanging the modern += token into the archaic =+. Others needed to just\nremove casts, while some had to be completely retyped, but not that\nmuch. \n</p></blockquote>\n\n<p>\nNow, decades later, we can just read the Sixth Edition source code\nonline at TUHS, from an archive\n<a href=\"http://minnie.tuhs.org/Archive/Distributions/Research/Dennis_v6/\">contributed by Dennis Ritchie</a>.\n\n</p><p>\nAside: at first glance, what is most striking about this pre-K&amp;R C code\nis how <em>narrow</em> it is. It's not often that I can include code\nlistings without extensive editing to fit the relatively narrow body\nof my web site.\n\n</p><p>\nThere's an instructive comment near the top of\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/pipe.c\">/usr/sys/ken/pipe.c</a>\n(and yes, there's also a\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/dmr\">/usr/sys/dmr</a>):\n\n</p><pre class=\"code\">\n/*\n * Max allowable buffering per pipe.\n * This is also the max size of the\n * file created to implement the pipe.\n * If this size is bigger than 4096,\n * pipes will be implemented in LARG\n * files, which is probably not good.\n */\n#define\tPIPSIZ\t4096\n</pre>\n\n<p>\nThe buffer size has not changed from 4E days, but here we can already\nsee beyond the documented public interface\u202f\u2014\u2009ancient pipes\nused a file to provide the backing storage!\n\n</p><p>\nAs for LARG files, they correspond to\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/inode.h\">the ILARG inode flag</a>\nused by the \"large addressing algorithm\" to handle\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/subr.c\">indirect blocks</a>\nto support larger filesystems. If Ken says it would probably not be good\nto use them, I'm happy to take his word for it.\n\n</p><p>\nHere's the actual pipe system call:\n\n</p><pre class=\"code\">\n/*\n * The sys-pipe entry.\n * Allocate an inode on the root device.\n * Allocate 2 file structures.\n * Put it all together with flags.\n */\npipe()\n{\n    register *ip, *rf, *wf;\n    int r;\n\n    ip = <b>ialloc</b>(rootdev);\n    if(ip == NULL)\n        return;\n    rf = <b>falloc</b>();\n    if(rf == NULL) {\n        iput(ip);\n        return;\n    }\n    r = u.u_ar0[R0];\n    wf = <b>falloc</b>();\n    if(wf == NULL) {\n        rf-&gt;f_count = 0;\n        u.u_ofile[r] = NULL;\n        iput(ip);\n        return;\n    }\n    <b>u.u_ar0[R1]</b> = u.u_ar0[R0]; /* wf's fd */\n    <b>u.u_ar0[R0]</b> = r;           /* rf's fd */\n    wf-&gt;f_flag = FWRITE|FPIPE;\n    wf-&gt;f_inode = ip;\n    rf-&gt;f_flag = FREAD|FPIPE;\n    rf-&gt;f_inode = ip;\n    ip-&gt;i_count = 2;\n    ip-&gt;i_flag = IACC|IUPD;\n    ip-&gt;i_mode = IALLOC;\n}\n</pre>\n\n<p>\nThe comment is quite clear about what's going on, but the code takes a\nbit of effort to follow, partly because of how the system call\nparameters and return values are passed in and out through the\nper-process\n\u201c<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/user.h\">struct user u</a>\u201d\nand the registers R0 and R1.\n\n</p><p>\nWe try to allocate\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/inode.h\">an inode</a>\non disk\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/alloc.c\">using ialloc()</a>\nand two\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/file.h\">files</a>\nin memory\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/fio.c\">using falloc()</a>.\nIf all goes well, we set flags to identify the files as two ends of a\npipe, point them at the same inode (whose reference count becomes 2),\nand mark the inode as modified and in-use. (Note the\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/iget.c\">calls to iput()</a>\nin the error paths, to decrement the reference count of the new inode.)\n\n</p><p>\npipe() must return the read and write file descriptor numbers via R0/R1.\nfalloc() returns a pointer to a file structure, but also \"returns\" the\nfd via u.u_ar0[R0]. So the code saves the read fd in r and assigns the\nwrite fd directly from u.u_ar0[R0] after the second call to falloc().\n\n</p><p>\nThe FPIPE flag that we set during pipe creation controls the behaviour\nof the\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/sys2.c\">rdwr() function in sys2.c</a>,\nwhich invokes the custom pipe I/O routines for pipes:\n\n</p><pre class=\"code\">\n/*\n * common code for read and write calls:\n * check permissions, set base, count, and offset,\n * and switch out to readi, writei, or pipe code.\n */\nrdwr(mode)\n{\n\tregister *fp, m;\n\n\tm = mode;\n\tfp = getf(u.u_ar0[R0]);\n        /* \u2026 */\n\n\tif(fp-&gt;f_flag&amp;FPIPE) {\n\t\tif(m==FREAD)\n\t\t\t<b>readp</b>(fp); else\n\t\t\t<b>writep</b>(fp);\n\t}\n        /* \u2026 */\n}\n</pre>\n\n<p>\nNext in pipe.c is the readp() function to read from a pipe, but it's\neasier to follow the implementation by looking at writep() first. Once\nagain, the oddities of the argument passing convention add some\ncomplexity, but we can gloss over some of the details.\n\n</p><pre class=\"code\">\nwritep(fp)\n{\n    register *rp, *ip, c;\n\n    rp = fp;\n    ip = rp-&gt;f_inode;\n    c = u.u_count;\n\nloop:\n    /* If all done, return. */\n\n    plock(ip);\n    if(c == 0) {\n        prele(ip);\n        u.u_count = 0;\n        return;\n    }\n\n    /*\n     * If there are not both read and write sides of the\n     * pipe active, return error and signal too.\n     */\n\n    if(ip-&gt;i_count &lt; 2) {\n        prele(ip);\n        u.u_error = EPIPE;\n        psignal(u.u_procp, SIGPIPE);\n        return;\n    }\n\n    /*\n     * If the pipe is full, wait for reads to deplete\n     * and truncate it.\n     */\n\n    if(ip-&gt;i_size1 == PIPSIZ) {\n        ip-&gt;i_mode =| IWRITE;\n        prele(ip);\n        <b>sleep(ip+1, PPIPE);</b>\n        goto loop;\n    }\n\n    /* Write what is possible and loop back. */\n\n    u.u_offset[0] = 0;\n    u.u_offset[1] = ip-&gt;i_size1;\n    u.u_count = min(c, PIPSIZ-u.u_offset[1]);\n    c =- u.u_count;\n    <b>writei</b>(ip);\n    prele(ip);\n    if(ip-&gt;i_mode&amp;IREAD) {\n        ip-&gt;i_mode =&amp; ~IREAD;\n        <b>wakeup(ip+2);</b>\n    }\n    goto loop;\n}\n</pre>\n\n<p>\nOn entry, we want to write u.u_count bytes into the pipe. First we\nacquire a lock on the inode (see below for more about plock/prele).\n\n</p><p>\nNext, we check the inode's reference count. So long as the two ends of\nthe pipe remain open, the reference count must remain 2. We hold one\nreference (from rp-&gt;f_inode), so if the count is less than 2, it must\nmean that the reader closed their end. In other words, we're trying to\nwrite into a closed pipe, which is an error. 6E Unix is when the EPIPE\nerror code and SIGPIPE signal were first introduced.\n\n</p><p>\nEven if the pipe is still open, it may be full. In that case, we release\nthe lock and go to sleep hoping that another process will create space\nin the pipe by reading from it. When we are woken up, we loop back to\nthe start and try to acquire the lock and start another write cycle.\n\n</p><p>\nIf the pipe does have free space, we use\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/rdwri.c\">writei()</a>\nto write out some data. The inode's i_size1 (which may be 0 for an empty\npipe) points to the end of whatever data it already contains. If we have\nenough data to write, we can fill the pipe from i_size1 up to PIPESIZ.\nThen we release the lock and try to wake up any process that is waiting\nto read from the pipe. Finally, we loop back to the start to see if we\nmanaged to write out as many bytes as we wanted. If not, we start\nanother write cycle.\n\n</p><p>\nThe inode's i_mode is normally used to hold r/w/x permissions, but for\npipes the IREAD and IWRITE bits are used to indicate that some process\nis waiting to read from or write to the pipe, respectively. The process\nsets the flag and calls sleep(), expecting a matching wakeup() to be\ncalled by some other process in future.\n\n</p><p>\nThe real magic happens in sleep() and wakeup(), which are implemented in\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/slp.c\">slp.c</a>,\nthe source of the infamous \u201cYou are not expected to understand this\u201d\ncomment. Fortunately, we don't have to understand the code, just take a\nquick look at some other comments:\n\n</p><pre class=\"code\">\n/*\n * Give up the processor till a wakeup occurs\n * on chan, at which time the process\n * enters the scheduling queue at priority pri.\n * The most important effect of pri is that when\n * pri&lt;0 a signal cannot disturb the sleep;\n * if pri&gt;=0 signals will be processed.\n * Callers of this routine must be prepared for\n * premature return, and check that the reason for\n * sleeping has gone away.\n */\nsleep(chan, pri) /* \u2026 */\n\n/*\n * Wake up all processes sleeping on chan.\n */\nwakeup(chan) /* \u2026 */\n</pre>\n\n<p>\nA process that calls sleep() for a particular channel can be woken up\nlater by another process calling wakeup() for the same channel. writep()\nand readp() coordinate their operations through matching pairs of these\ncalls. (Note that pipe.c always uses PPIPE as the priority when calling\nsleep, so the sleeps are interruptible by signals.)\n\n</p><p>\nNow we have everything we need to understand the readp() function:\n\n</p><pre class=\"code\">\nreadp(fp)\nint *fp;\n{\n    register *rp, *ip;\n\n    rp = fp;\n    ip = rp-&gt;f_inode;\n\nloop:\n    /* Very conservative locking. */\n\n    plock(ip);\n\n    /*\n     * If the head (read) has caught up with\n     * the tail (write), reset both to 0.\n     */\n\n    if(rp-&gt;f_offset[1] == ip-&gt;i_size1) {\n        if(rp-&gt;f_offset[1] != 0) {\n            rp-&gt;f_offset[1] = 0;\n            ip-&gt;i_size1 = 0;\n            if(ip-&gt;i_mode&amp;IWRITE) {\n                ip-&gt;i_mode =&amp; ~IWRITE;\n                <b>wakeup(ip+1);</b>\n            }\n        }\n\n        /*\n         * If there are not both reader and\n         * writer active, return without\n         * satisfying read.\n         */\n\n        prele(ip);\n        if(ip-&gt;i_count &lt; 2)\n            return;\n        ip-&gt;i_mode =| IREAD;\n        <b>sleep(ip+2, PPIPE);</b>\n        goto loop;\n    }\n\n    /* Read and return */\n\n    u.u_offset[0] = 0;\n    u.u_offset[1] = rp-&gt;f_offset[1];\n    <b>readi</b>(ip);\n    rp-&gt;f_offset[1] = u.u_offset[1];\n    prele(ip);\n}\n</pre>\n\n<p>\nIt may help to read this function from the bottom upwards. The \"read and\nreturn\" branch is the common case, when there is some data in the pipe.\nIf so, we use\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/rdwri.c\">readi()</a>\nto read as much data as available from the current read f_offset\nonwards, and update the offset accordingly.\n\n</p><p>\nOn a subsequent read, the pipe is empty if the read offset has reached\nthe inode's i_size1. We reset the positions to 0 and try to wake up any\nprocess that wants to write into the pipe. We know that writep() goes to\nsleep on on ip+1 when the pipe is full. Now that the pipe is empty, we\ncan wake it up and allow it to resume its write loop.\n\n</p><p>\nIf there's nothing to read, readp() can set the IREAD flag and go to\nsleep on ip+2. We know that writep() will wake it up when it manages to\nwrite some data into the pipe.\n\n</p><p>\nIt's worth a quick look at the comments for \n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/sys/ken/rdwri.c\">readi() and writei()</a>\nto realise that, other than having to pass in the parameters through\n\u201cu\u201d, we can treat them as ordinary IO functions that take a file, a\nposition, a memory buffer, and a count of bytes to read or write.\n\n</p><pre class=\"code\">\n/*\n * Read the file corresponding to\n * the inode pointed at by the argument.\n * The actual read arguments are found\n * in the variables:\n *\tu_base\t\tcore address for destination\n *\tu_offset\tbyte offset in file\n *\tu_count\t\tnumber of bytes to read\n *\tu_segflg\tread to kernel/user\n */\nreadi(aip)\nstruct inode *aip;\n/* \u2026 */\n\n/*\n * Write the file corresponding to\n * the inode pointed at by the argument.\n * The actual write arguments are found\n * in the variables:\n *\tu_base\t\tcore address for source\n *\tu_offset\tbyte offset in file\n *\tu_count\t\tnumber of bytes to write\n *\tu_segflg\twrite to kernel/user\n */\nwritei(aip)\nstruct inode *aip;\n/* \u2026 */\n</pre>\n\n<p>\nAs for the \"conservative\" locking, readp() and writep() hold a lock on\nthe inode until they finish or yield (i.e., call wakeup). plock() and\nprele() are straightforward, with another matched set of sleep and\nwakeup calls to enable us to wakeup any process that wants a lock that\nwe have just released:\n\n</p><pre class=\"code\">\n/*\n * Lock a pipe.\n * If its already locked, set the WANT bit and sleep.\n */\nplock(ip)\nint *ip;\n{\n    register *rp;\n\n    rp = ip;\n    while(rp-&gt;i_flag&amp;ILOCK) {\n        rp-&gt;i_flag =| IWANT;\n        <b>sleep(rp, PPIPE);</b>\n    }\n    rp-&gt;i_flag =| ILOCK;\n}\n\n/*\n * Unlock a pipe.\n * If WANT bit is on, wakeup.\n * This routine is also used to unlock inodes in general.\n */\nprele(ip)\nint *ip;\n{\n    register *rp;\n\n    rp = ip;\n    rp-&gt;i_flag =&amp; ~ILOCK;\n    if(rp-&gt;i_flag&amp;IWANT) {\n        rp-&gt;i_flag =&amp; ~IWANT;\n        <b>wakeup(rp);</b>\n    }\n}\n</pre>\n\n<p>\nI could not understand at first why readp() did not call prele(ip)\nbefore calling wakeup(ip+1). The first thing writep() does in its loop\nis plock(ip), which would deadlock if readp() still held its lock, so\nthe code must have the right effect somehow. A quick glance at wakeup()\nshowed that it only marks sleeping processes as runnable, and leaves it\nto a future sched() to actually start the process. So readp() calls\nwakeup(), releases its lock, sets IREAD, and calls sleep(ip+2) all\nbefore writep() resumes its loop.\n\n</p><p>\nThat's all there is to pipes in 6E. Simple code, far-reaching\nconsequences.\n\n</p><p>\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V7\">Seventh Edition Unix</a>\nfrom January 1979 was a major new release (after a gap of four years)\nwith many new applications and kernel features. It also featured\nwidespread changes due to the use of K&amp;R C with typecasts and unions and\ntyped pointers to structs, but the\n<a href=\"https://minnie.tuhs.org/cgi-bin/utree.pl?file=V7/usr/sys/sys/pipe.c\">pipe code</a>\nhardly changed at all. We can skip it without missing anything exciting.\n\n</p><h2><a id=\"xv6\">Xv6, a simple Unix-like kernel</a></h2>\n\n<p>\n<a href=\"https://pdos.csail.mit.edu/6.828/2019/xv6.html\">Xv6</a> is a\nkernel inspired by Sixth Edition Unix, but written in modern C to run on\nx86 processors. It's simple to read and understand but, unlike the Unix\nsources from TUHS, you can also compile and modify and run it on\nsomething other than a PDP 11/70. For this reason, it is widely used by\nuniversities in Operation Systems courses. The\n<a href=\"https://github.com/mit-pdos/xv6-public\">source code</a> is\navailable on Github.\n\n</p><p>\nIt has a clear and concise\n<a href=\"https://github.com/mit-pdos/xv6-public/blob/master/pipe.c\">pipe.c implementation</a>\nbacked by a buffer in memory instead of an inode on disk. I reproduce\nonly the definition of \u201cstruct pipe\u201d and the pipealloc() function here:\n\n</p><pre class=\"code\">\n#define PIPESIZE 512\n\nstruct pipe {\n  struct spinlock lock;\n  char data[PIPESIZE];\n  uint nread;     // number of bytes read\n  uint nwrite;    // number of bytes written\n  int readopen;   // read fd is still open\n  int writeopen;  // write fd is still open\n};\n\nint\npipealloc(struct file **f0, struct file **f1)\n{\n  struct pipe *p;\n\n  p = 0;\n  *f0 = *f1 = 0;\n  if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)\n    goto bad;\n  if((p = (struct pipe*)kalloc()) == 0)\n    goto bad;\n  p-&gt;readopen = 1;\n  p-&gt;writeopen = 1;\n  p-&gt;nwrite = 0;\n  p-&gt;nread = 0;\n  initlock(&amp;p-&gt;lock, \"pipe\");\n  (*f0)-&gt;type = FD_PIPE;\n  (*f0)-&gt;readable = 1;\n  (*f0)-&gt;writable = 0;\n  (*f0)-&gt;pipe = p;\n  (*f1)-&gt;type = FD_PIPE;\n  (*f1)-&gt;readable = 0;\n  (*f1)-&gt;writable = 1;\n  (*f1)-&gt;pipe = p;\n  return 0;\n\n bad:\n  if(p)\n    kfree((char*)p);\n  if(*f0)\n    fileclose(*f0);\n  if(*f1)\n    fileclose(*f1);\n  return -1;\n}\n</pre>\n\n<p>\npipealloc() sets the stage for the rest of the implementation,\ncomprising piperead(), pipewrite(), and pipeclose() functions. The\nactual system call, sys_pipe, is a wrapper implemented in\n<a href=\"https://github.com/mit-pdos/xv6-public/blob/master/sysfile.c\">sysfile.c</a>.\nI recommend reading the code in its entirety. It's about the same level\nof complexity as the 6E Unix source code, but much easier to go through,\ntherefore more satisfying.\n\n</p><h2><a id=\"linux\">Linux 0.01</a></h2>\n\n<p>\nIt's possible to find the\n<a href=\"https://kernel.org/pub/linux/kernel/Historic/\">Linux 0.01 source code</a>,\nand it's instructive\nto look at its pipe implementation in fs/pipe.c too. It uses an inode to\nrepresent the pipe, but it's written in modern C. Having worked our way\nthrough the 6E code, it's not difficult to follow. Here's the\nwrite_pipe() function:\n\n</p><pre class=\"code\">\nint write_pipe(struct m_inode * inode, char * buf, int count)\n{\n    char * b=buf;\n\n    wake_up(&amp;inode-&gt;i_wait);\n    if (inode-&gt;i_count != 2) { /* no readers */\n        current-&gt;signal |= (1&lt;&lt;(SIGPIPE-1));\n        return -1;\n    }\n    while (count--&gt;0) {\n        while (PIPE_FULL(*inode)) {\n            wake_up(&amp;inode-&gt;i_wait);\n            if (inode-&gt;i_count != 2) {\n                current-&gt;signal |= (1&lt;&lt;(SIGPIPE-1));\n                return b-buf;\n            }\n            sleep_on(&amp;inode-&gt;i_wait);\n        }\n        ((char *)inode-&gt;i_size)[PIPE_HEAD(*inode)] =\n            get_fs_byte(b++);\n        INC_PIPE( PIPE_HEAD(*inode) );\n        wake_up(&amp;inode-&gt;i_wait);\n    }\n    wake_up(&amp;inode-&gt;i_wait);\n    return b-buf;\n}\n</pre>\n\n<p>\nEven without looking up the structure definitions, we can understand the\nuse of the inode reference count to check if a write should raise\nSIGPIPE. Other than operating byte-by-byte, it's easy to map this\nfunction to the ideas we saw earlier. Even the sleep_on/wake_up logic\ndoesn't seem entirely foreign.\n\n</p><h2><a id=\"modern\">Modern kernels: Linux, FreeBSD, NetBSD, OpenBSD</a></h2>\n\n<p>\nI had a quick look at some modern kernels, and none of them uses a\ndisk-backed implementation any more (not surprisingly). Linux has its\nown implementation, of course, but although the three modern BSD kernels\nall have implementations derived from code written by John S. Dyson,\nthey too have diverged from each other over the years.\n\n</p><p>\nReading through fs/pipe.c (on Linux) or sys/kern/sys_pipe.c (on *BSD)\nnow takes real commitment. The code now needs to be concerned with\nperformance and support for features like vectored and asynchronous IO,\nand the details of memory allocation, locking, and kernel configuration\nall vary considerably. Not at all the sort of thing a university would\nwant for an introductory course.\n\n</p><p>\nNevertheless, I took some satisfaction in being able to recognise a few\nancestral patterns (raising SIGPIPE and returning EPIPE when writing to\na closed pipe, for example) in each of these very different modern\nkernels. I may never see a PDP-11 computer, but there are still useful\nlessons to learn from code that predates my own existence by a few\nyears.\n\n</p><p>\n\u201c<a href=\"https://www.slideshare.net/divyekapoor/linux-kernel-implementation-of-pipes-and-fifos\">The Linux Kernel Implementation of Pipes and FIFOs</a>\u201d\nby Divye Kapoor in 2011 is an overview of how pipefs (still) works in\nLinux. This\n<a href=\"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=0ddad21d3e99c743a3aa473121dc5561679e26bb\">recent Linux commit</a>\nillustrates how pipes support a model of interaction that goes beyond\nwhat temporary files allow, as well as how much further pipes have\nevolved from the \"very conservative locking\" in the 6E Unix kernel.\n</p>",
                        "value": "This article is about how pipes are implemented the Unix kernel. I was a little disappointed that a recent article titled \u201cHow do Unix pipes work?\u201d was not about the internals, and curious enough to go digging in some old sources to try to answer the question. What are we talking about?\nPipes are \u201cperhaps the single most striking invention in Unix\u201d\u202f\u2014\u2009a defining characteristic of the Unix philosophy of composing small programs together, and a familiar sight in the Unix shell: $ echo hello | wc -c 6\nThis functionality depends on the kernel providing a system call named pipe, which is described by the pipe(7) and pipe(2) manual pages:\nPipes provide a unidirectional interprocess communication channel. A pipe has a read end and a write end. Data written to the write end of a pipe can be read from the read end of the pipe.\nA pipe is created using pipe(2), which returns two file descriptors, one referring to the read end of the pipe, the other referring to the write end.\nTracing the execution of the command above shows the creation of the pipe and the flow of data through it from one process to another: $ strace -qf -e execve,pipe,dup2,read,write \\ sh -c 'echo hello | wc -c' execve(\"/bin/sh\", [\"sh\", \"-c\", \"echo hello | wc -c\"], \u2026) pipe([3, 4]) = 0 [pid 2604795] dup2(4, 1) = 1 [pid 2604795] write(1, \"hello\\n\", 6) = 6 [pid 2604796] dup2(3, 0) = 0 [pid 2604796] execve(\"/usr/bin/wc\", [\"wc\", \"-c\"], \u2026) [pid 2604796] read(0, \"hello\\n\", 16384) = 6 [pid 2604796] write(1, \"6\\n\", 2) = 2\nThe parent process calls pipe() to obtain connected fds, one child writes to one fd and another reads the same data from the other fd. (The shell uses dup2 to \"rename\" fds 3 and 4 to match stdin and stdout.)\nWithout pipes, the shell would have to write the output from one process to a file and arrange for the next process to read from it, at the expense of some overhead and some disk space\u202f\u2014\u2009but pipes provide more than just the convenience of avoiding temporary files: If a process attempts to read from an empty pipe, then read(2) will block until data is available. If a process attempts to write to a full pipe (see below), then write(2) blocks until sufficient data has been read from the pipe to allow the write to complete.\nThis important property, along with the POSIX requirement that writing up to PIPE_BUF (at least 512) bytes into a pipe must be atomic, allows processes to communicate with each other over a pipe in a way that regular files (which have no such guarantees) could not support.\nWith a regular file, a process could write all its output and hand it over to the next process, or they could operate in lock-step by using an external signalling mechanism (like a semaphore) to let each other know when they were done reading or writing. Pipes eliminate all this complexity. What are we looking for?\nA little hand-waving helps to imagine how pipes might work. You would need to allocate a memory buffer and some state, have functions to add and remove data from the buffer and some glue to invoke them through read and write operations on file descriptors, and sprinkle some locking on top to implement the special behaviour described above.\nWe are now equipped to interrogate the kernel source under bright lights to confirm or deny this vague mental model, but always prepared to run into any surprises along the way. Where do we look?\nI'm not sure where my (reprinted) copy of the famous \u201cLions book\u201d with the source code of Unix 6ed is, but thanks to The Unix Heritage Society, we can start by looking at even older Unix sources online.\n(Browsing the TUHS archives is like being in a museum\u202f\u2014\u2009not only does it provide a rare glimpse at our shared history, it also makes me appreciate the years of effort that went into recovering all of these materials bit by bit from old tapes and printed documentation, and acutely conscious of the parts that are still lost.)\nOnce we've satisfied our curiosity about the ancient history of pipes, we can take a look at some modern kernels just for the contrast.\nUnix (1970\u20131974) Sixth Edition Unix (1975) Xv6, a reimplementation of 6E Unix (2006) Linux v0.01 (1991) Modern kernels (Linux, Free/Net/OpenBSD)\nIncidentally, pipe is system call number 42 in the sysent[] table in Unix. Coincidence? Traditional Unix kernels (1970\u20131974)\nI can't find any trace of pipe(2) in PDP-7 Unix from January 1970, nor in First edition Unix from November 1971, nor in the incomplete sources of Second edition Unix from June 1972.\nTUHS confirms that Third edition Unix from February 1973 was the first version to include pipes: The third edition of Unix was the last version with a kernel still written in assembly code, but is the first version to include pipes. For much of 1973, the existing Third Edition was maintained and improved, while the kernel was rewritten in C to become the Fourth Edition of Unix.\nUpdate: A reader on HN was kind enough to dig up a scanned copy of the 1964 memo in which Doug McIlroy proposed the idea of \u201ccoupling programs like garden hose\u201d.\nMcIlroy pipe memo\nBrian Kernighan's book, \u201cUnix: A History and a Memoir\u201d, also talks about the history of pipes and mentions this memo \u201c\u2026that hung on the wall in my office at Bell Labs for 30 years\u201d. Here's an interview with McIlroy, and some more history from a 2014 paper by McIlroy:\nWhen Unix came to be, my fascination with coroutines led me to badger its author, Ken Thompson, to allow writes in one process to go not only to devices but also to matching reads in another process. Ken saw that was possible. As a minimalist, though, he wanted every system feature to carry significant weight. Did direct writes between processes offer a really major advantage over writing to a temporary file in one process and then reading it in the other? Not until I made a specific proposal with a catchyname, \u2018\u2018pipe\u2019\u2019, and shell syntax to connect processes via pipes, did Ken finally exclaim, \u2018\u2018I\u2019ll do it!\u2019\u2019\nAnd he did do it. In one feverish evening Ken modified both kernel and shell, and fixed several standard programs so they could take input from standard input (potentially piped) as well as named files. The next day brought a heady explosion of applications. By the end of the week, department secretaries were using pipes to send text-processor output to the printer spooler. Not long after, Ken replaced the original API and shell syntax for pipes with cleaner conventions that have been used ever since.\nUnfortunately, the source code for the 3E kernel is no longer available, and although we do have the C source code for a version of the Fourth edition Unix kernel from November 1973, it predates the official release by a few months, and is missing the pipe implementation. It is sad that the origin of such an iconic Unix feature is lost to us, perhaps forever.\nWe do have the pipe(2) manpages from both releases, so we can start by looking at the documentation from 3E (with certain words underlined \u201cby hand\u201d with a string of literal ^H backspaces followed by underscores!). This proto-pipe(2) written in assembly returned only one file descriptor, but provided the basic functionality that we expect: The pipe system call creates an I/O mechanism called a pipe. The file descriptor returned can be used in both read and write operations. When the pipe is written, the data is buffered up to 504 bytes at which time the writing process is suspended. A read on the pipe will pick up the buffered data.\nBy next year, along with a rewrite of the kernel into C, the 4E pipe(2) had assumed its modern form, with a prototype of \"pipe(fildes)\":\nThe pipe system call creates an I/O mechanism called a pipe. The file descriptors returned can be used in read and write operations. When the pipe is written using the descriptor returned in r1 (resp. fildes[1]), up to 4096 bytes of data are buffered before the writing process is suspended. A read using the descriptor returned in r0 (resp. fildes[0]) will pick up the data.\nIt is assumed that after the pipe has been set up, two (or more) cooperating processes (created by subsequent fork calls) will pass data through the pipe with read and write calls.\nThe shell has a syntax to set up a linear array of processes connected by pipes.\nRead calls on an empty pipe (no buffered data) with only one end (all write file descriptors closed) return an end-of-file. Write calls under similar conditions are ignored.\nSo the earliest surviving pipe implementation is from Fifth Edition Unix in June 1974, but it's nearly identical to what was in the next release, which also added some comments, so we might as well skip 5E too. Sixth Edition Unix (1975)\nAs so many others have done in the past, Sixth Edition Unix from May 1975 is where we start reading the Unix code. Sixth Edition sources are much more widely available than earlier versions, thanks largely to the Lions book: For many years, the Lions book was the only Unix kernel documentation available outside Bell Labs. Although the license of 6th Edition allowed classroom use of the source code, the license of 7th Edition specifically excluded such use, so the book spread through illegal copy machine reproductions.\nYou can still buy a reprint of the book, featuring cover art with shifty-looking students at a photocopier, but thanks to Warren Toomey (who went on to start TUHS), you can also download a PDF of the 6E source. I can't resist a quick glimpse at the effort that went into making this PDF available:\nOver 15 years ago I had produced a replica of the Lions' source code listing since I was unhappy with the quality of my n-th generation copy. There was no TUHS and I had no access to any old source code. But in 1988 I discovered an old 9-track tape being discarded of a PDP11 backup. It was hard to determine what it was running, but it did have an intact /usr/src/ tree of which most of the files were timesamped 1979, even at that time it seemed ancient. So it was either 7th edition or a derivative like PWB, which I believe it was.\nI used this as a basis and hand edited the source back into 6th edition form. Some code was completely the same, some required the easy edit of changing the modern += token into the archaic =+. Others needed to just remove casts, while some had to be completely retyped, but not that much.\nNow, decades later, we can just read the Sixth Edition source code online at TUHS, from an archive contributed by Dennis Ritchie.\nAside: at first glance, what is most striking about this pre-K&R C code is how narrow it is. It's not often that I can include code listings without extensive editing to fit the relatively narrow body of my web site.\nThere's an instructive comment near the top of /usr/sys/ken/pipe.c (and yes, there's also a /usr/sys/dmr): /* * Max allowable buffering per pipe. * This is also the max size of the * file created to implement the pipe. * If this size is bigger than 4096, * pipes will be implemented in LARG * files, which is probably not good. */ #define PIPSIZ 4096\nThe buffer size has not changed from 4E days, but here we can already see beyond the documented public interface\u202f\u2014\u2009ancient pipes used a file to provide the backing storage!\nAs for LARG files, they correspond to the ILARG inode flag used by the \"large addressing algorithm\" to handle indirect blocks to support larger filesystems. If Ken says it would probably not be good to use them, I'm happy to take his word for it.\nHere's the actual pipe system call: /* * The sys-pipe entry. * Allocate an inode on the root device. * Allocate 2 file structures. * Put it all together with flags. */ pipe() { register *ip, *rf, *wf; int r; ip = ialloc(rootdev); if(ip == NULL) return; rf = falloc(); if(rf == NULL) { iput(ip); return; } r = u.u_ar0[R0]; wf = falloc(); if(wf == NULL) { rf->f_count = 0; u.u_ofile[r] = NULL; iput(ip); return; } u.u_ar0[R1] = u.u_ar0[R0]; /* wf's fd */ u.u_ar0[R0] = r; /* rf's fd */ wf->f_flag = FWRITE|FPIPE; wf->f_inode = ip; rf->f_flag = FREAD|FPIPE; rf->f_inode = ip; ip->i_count = 2; ip->i_flag = IACC|IUPD; ip->i_mode = IALLOC; }\nThe comment is quite clear about what's going on, but the code takes a bit of effort to follow, partly because of how the system call parameters and return values are passed in and out through the per-process \u201cstruct user u\u201d and the registers R0 and R1.\nWe try to allocate an inode on disk using ialloc() and two files in memory using falloc(). If all goes well, we set flags to identify the files as two ends of a pipe, point them at the same inode (whose reference count becomes 2), and mark the inode as modified and in-use. (Note the calls to iput() in the error paths, to decrement the reference count of the new inode.)\npipe() must return the read and write file descriptor numbers via R0/R1. falloc() returns a pointer to a file structure, but also \"returns\" the fd via u.u_ar0[R0]. So the code saves the read fd in r and assigns the write fd directly from u.u_ar0[R0] after the second call to falloc().\nThe FPIPE flag that we set during pipe creation controls the behaviour of the rdwr() function in sys2.c, which invokes the custom pipe I/O routines for pipes: /* * common code for read and write calls: * check permissions, set base, count, and offset, * and switch out to readi, writei, or pipe code. */ rdwr(mode) { register *fp, m; m = mode; fp = getf(u.u_ar0[R0]); /* \u2026 */ if(fp->f_flag&FPIPE) { if(m==FREAD) readp(fp); else writep(fp); } /* \u2026 */ }\nNext in pipe.c is the readp() function to read from a pipe, but it's easier to follow the implementation by looking at writep() first. Once again, the oddities of the argument passing convention add some complexity, but we can gloss over some of the details. writep(fp) { register *rp, *ip, c; rp = fp; ip = rp->f_inode; c = u.u_count; loop: /* If all done, return. */ plock(ip); if(c == 0) { prele(ip); u.u_count = 0; return; } /* * If there are not both read and write sides of the * pipe active, return error and signal too. */ if(ip->i_count < 2) { prele(ip); u.u_error = EPIPE; psignal(u.u_procp, SIGPIPE); return; } /* * If the pipe is full, wait for reads to deplete * and truncate it. */ if(ip->i_size1 == PIPSIZ) { ip->i_mode =| IWRITE; prele(ip); sleep(ip+1, PPIPE); goto loop; } /* Write what is possible and loop back. */ u.u_offset[0] = 0; u.u_offset[1] = ip->i_size1; u.u_count = min(c, PIPSIZ-u.u_offset[1]); c =- u.u_count; writei(ip); prele(ip); if(ip->i_mode&IREAD) { ip->i_mode =& ~IREAD; wakeup(ip+2); } goto loop; }\nOn entry, we want to write u.u_count bytes into the pipe. First we acquire a lock on the inode (see below for more about plock/prele).\nNext, we check the inode's reference count. So long as the two ends of the pipe remain open, the reference count must remain 2. We hold one reference (from rp->f_inode), so if the count is less than 2, it must mean that the reader closed their end. In other words, we're trying to write into a closed pipe, which is an error. 6E Unix is when the EPIPE error code and SIGPIPE signal were first introduced.\nEven if the pipe is still open, it may be full. In that case, we release the lock and go to sleep hoping that another process will create space in the pipe by reading from it. When we are woken up, we loop back to the start and try to acquire the lock and start another write cycle.\nIf the pipe does have free space, we use writei() to write out some data. The inode's i_size1 (which may be 0 for an empty pipe) points to the end of whatever data it already contains. If we have enough data to write, we can fill the pipe from i_size1 up to PIPESIZ. Then we release the lock and try to wake up any process that is waiting to read from the pipe. Finally, we loop back to the start to see if we managed to write out as many bytes as we wanted. If not, we start another write cycle.\nThe inode's i_mode is normally used to hold r/w/x permissions, but for pipes the IREAD and IWRITE bits are used to indicate that some process is waiting to read from or write to the pipe, respectively. The process sets the flag and calls sleep(), expecting a matching wakeup() to be called by some other process in future.\nThe real magic happens in sleep() and wakeup(), which are implemented in slp.c, the source of the infamous \u201cYou are not expected to understand this\u201d comment. Fortunately, we don't have to understand the code, just take a quick look at some other comments: /* * Give up the processor till a wakeup occurs * on chan, at which time the process * enters the scheduling queue at priority pri. * The most important effect of pri is that when * pri<0 a signal cannot disturb the sleep; * if pri>=0 signals will be processed. * Callers of this routine must be prepared for * premature return, and check that the reason for * sleeping has gone away. */ sleep(chan, pri) /* \u2026 */ /* * Wake up all processes sleeping on chan. */ wakeup(chan) /* \u2026 */\nA process that calls sleep() for a particular channel can be woken up later by another process calling wakeup() for the same channel. writep() and readp() coordinate their operations through matching pairs of these calls. (Note that pipe.c always uses PPIPE as the priority when calling sleep, so the sleeps are interruptible by signals.)\nNow we have everything we need to understand the readp() function: readp(fp) int *fp; { register *rp, *ip; rp = fp; ip = rp->f_inode; loop: /* Very conservative locking. */ plock(ip); /* * If the head (read) has caught up with * the tail (write), reset both to 0. */ if(rp->f_offset[1] == ip->i_size1) { if(rp->f_offset[1] != 0) { rp->f_offset[1] = 0; ip->i_size1 = 0; if(ip->i_mode&IWRITE) { ip->i_mode =& ~IWRITE; wakeup(ip+1); } } /* * If there are not both reader and * writer active, return without * satisfying read. */ prele(ip); if(ip->i_count < 2) return; ip->i_mode =| IREAD; sleep(ip+2, PPIPE); goto loop; } /* Read and return */ u.u_offset[0] = 0; u.u_offset[1] = rp->f_offset[1]; readi(ip); rp->f_offset[1] = u.u_offset[1]; prele(ip); }\nIt may help to read this function from the bottom upwards. The \"read and return\" branch is the common case, when there is some data in the pipe. If so, we use readi() to read as much data as available from the current read f_offset onwards, and update the offset accordingly.\nOn a subsequent read, the pipe is empty if the read offset has reached the inode's i_size1. We reset the positions to 0 and try to wake up any process that wants to write into the pipe. We know that writep() goes to sleep on on ip+1 when the pipe is full. Now that the pipe is empty, we can wake it up and allow it to resume its write loop.\nIf there's nothing to read, readp() can set the IREAD flag and go to sleep on ip+2. We know that writep() will wake it up when it manages to write some data into the pipe.\nIt's worth a quick look at the comments for readi() and writei() to realise that, other than having to pass in the parameters through \u201cu\u201d, we can treat them as ordinary IO functions that take a file, a position, a memory buffer, and a count of bytes to read or write. /* * Read the file corresponding to * the inode pointed at by the argument. * The actual read arguments are found * in the variables: * u_base core address for destination * u_offset byte offset in file * u_count number of bytes to read * u_segflg read to kernel/user */ readi(aip) struct inode *aip; /* \u2026 */ /* * Write the file corresponding to * the inode pointed at by the argument. * The actual write arguments are found * in the variables: * u_base core address for source * u_offset byte offset in file * u_count number of bytes to write * u_segflg write to kernel/user */ writei(aip) struct inode *aip; /* \u2026 */\nAs for the \"conservative\" locking, readp() and writep() hold a lock on the inode until they finish or yield (i.e., call wakeup). plock() and prele() are straightforward, with another matched set of sleep and wakeup calls to enable us to wakeup any process that wants a lock that we have just released: /* * Lock a pipe. * If its already locked, set the WANT bit and sleep. */ plock(ip) int *ip; { register *rp; rp = ip; while(rp->i_flag&ILOCK) { rp->i_flag =| IWANT; sleep(rp, PPIPE); } rp->i_flag =| ILOCK; } /* * Unlock a pipe. * If WANT bit is on, wakeup. * This routine is also used to unlock inodes in general. */ prele(ip) int *ip; { register *rp; rp = ip; rp->i_flag =& ~ILOCK; if(rp->i_flag&IWANT) { rp->i_flag =& ~IWANT; wakeup(rp); } }\nI could not understand at first why readp() did not call prele(ip) before calling wakeup(ip+1). The first thing writep() does in its loop is plock(ip), which would deadlock if readp() still held its lock, so the code must have the right effect somehow. A quick glance at wakeup() showed that it only marks sleeping processes as runnable, and leaves it to a future sched() to actually start the process. So readp() calls wakeup(), releases its lock, sets IREAD, and calls sleep(ip+2) all before writep() resumes its loop.\nThat's all there is to pipes in 6E. Simple code, far-reaching consequences.\nSeventh Edition Unix from January 1979 was a major new release (after a gap of four years) with many new applications and kernel features. It also featured widespread changes due to the use of K&R C with typecasts and unions and typed pointers to structs, but the pipe code hardly changed at all. We can skip it without missing anything exciting. Xv6, a simple Unix-like kernel\nXv6 is a kernel inspired by Sixth Edition Unix, but written in modern C to run on x86 processors. It's simple to read and understand but, unlike the Unix sources from TUHS, you can also compile and modify and run it on something other than a PDP 11/70. For this reason, it is widely used by universities in Operation Systems courses. The source code is available on Github.\nIt has a clear and concise pipe.c implementation backed by a buffer in memory instead of an inode on disk. I reproduce only the definition of \u201cstruct pipe\u201d and the pipealloc() function here: #define PIPESIZE 512 struct pipe { struct spinlock lock; char data[PIPESIZE]; uint nread; // number of bytes read uint nwrite; // number of bytes written int readopen; // read fd is still open int writeopen; // write fd is still open }; int pipealloc(struct file **f0, struct file **f1) { struct pipe *p; p = 0; *f0 = *f1 = 0; if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0) goto bad; if((p = (struct pipe*)kalloc()) == 0) goto bad; p->readopen = 1; p->writeopen = 1; p->nwrite = 0; p->nread = 0; initlock(&p->lock, \"pipe\"); (*f0)->type = FD_PIPE; (*f0)->readable = 1; (*f0)->writable = 0; (*f0)->pipe = p; (*f1)->type = FD_PIPE; (*f1)->readable = 0; (*f1)->writable = 1; (*f1)->pipe = p; return 0; bad: if(p) kfree((char*)p); if(*f0) fileclose(*f0); if(*f1) fileclose(*f1); return -1; }\npipealloc() sets the stage for the rest of the implementation, comprising piperead(), pipewrite(), and pipeclose() functions. The actual system call, sys_pipe, is a wrapper implemented in sysfile.c. I recommend reading the code in its entirety. It's about the same level of complexity as the 6E Unix source code, but much easier to go through, therefore more satisfying. Linux 0.01\nIt's possible to find the Linux 0.01 source code, and it's instructive to look at its pipe implementation in fs/pipe.c too. It uses an inode to represent the pipe, but it's written in modern C. Having worked our way through the 6E code, it's not difficult to follow. Here's the write_pipe() function: int write_pipe(struct m_inode * inode, char * buf, int count) { char * b=buf; wake_up(&inode->i_wait); if (inode->i_count != 2) { /* no readers */ current->signal |= (1<<(SIGPIPE-1)); return -1; } while (count-->0) { while (PIPE_FULL(*inode)) { wake_up(&inode->i_wait); if (inode->i_count != 2) { current->signal |= (1<<(SIGPIPE-1)); return b-buf; } sleep_on(&inode->i_wait); } ((char *)inode->i_size)[PIPE_HEAD(*inode)] = get_fs_byte(b++); INC_PIPE( PIPE_HEAD(*inode) ); wake_up(&inode->i_wait); } wake_up(&inode->i_wait); return b-buf; }\nEven without looking up the structure definitions, we can understand the use of the inode reference count to check if a write should raise SIGPIPE. Other than operating byte-by-byte, it's easy to map this function to the ideas we saw earlier. Even the sleep_on/wake_up logic doesn't seem entirely foreign. Modern kernels: Linux, FreeBSD, NetBSD, OpenBSD\nI had a quick look at some modern kernels, and none of them uses a disk-backed implementation any more (not surprisingly). Linux has its own implementation, of course, but although the three modern BSD kernels all have implementations derived from code written by John S. Dyson, they too have diverged from each other over the years.\nReading through fs/pipe.c (on Linux) or sys/kern/sys_pipe.c (on *BSD) now takes real commitment. The code now needs to be concerned with performance and support for features like vectored and asynchronous IO, and the details of memory allocation, locking, and kernel configuration all vary considerably. Not at all the sort of thing a university would want for an introductory course.\nNevertheless, I took some satisfaction in being able to recognise a few ancestral patterns (raising SIGPIPE and returning EPIPE when writing to a closed pipe, for example) in each of these very different modern kernels. I may never see a PDP-11 computer, but there are still useful lessons to learn from code that predates my own existence by a few years.\n\u201cThe Linux Kernel Implementation of Pipes and FIFOs\u201d by Divye Kapoor in 2011 is an overview of how pipefs (still) works in Linux. This recent Linux commit illustrates how pipes support a model of interaction that goes beyond what temporary files allow, as well as how much further pipes have evolved from the \"very conservative locking\" in the 6E Unix kernel.",
                        "lang": "en"
                    }
                ],
                "author": [
                    {
                        "type": [
                            "h-card"
                        ],
                        "properties": {
                            "name": [
                                "Abhijit Menon-Sen"
                            ],
                            "url": [
                                "https://toroid.org"
                            ],
                            "email": [
                                "mailto:ams@toroid.org"
                            ]
                        },
                        "lang": "en",
                        "value": "Abhijit Menon-Sen"
                    }
                ]
            },
            "lang": "en"
        }
    ],
    "rels": {
        "stylesheet": [
            "https://toroid.org/toroid.css"
        ],
        "alternate": [
            "https://toroid.org/etc/index.atom"
        ],
        "canonical": [
            "https://toroid.org/unix-pipe-implementation"
        ]
    },
    "rel-urls": {
        "https://toroid.org/toroid.css": {
            "rels": [
                "stylesheet"
            ]
        },
        "https://toroid.org/etc/index.atom": {
            "title": "The Advisory Boar",
            "type": "application/atom+xml",
            "rels": [
                "alternate"
            ]
        },
        "https://toroid.org/unix-pipe-implementation": {
            "rels": [
                "canonical"
            ]
        }
    },
    "debug": {
        "package": "https://packagist.org/packages/mf2/mf2",
        "source": "https://github.com/indieweb/php-mf2",
        "version": "v0.5.0",
        "note": [
            "This output was generated from the php-mf2 library available at https://github.com/indieweb/php-mf2",
            "Please file any issues with the parser at https://github.com/indieweb/php-mf2/issues",
            "Using the Masterminds HTML5 parser"
        ]
    }
}