DEV Community

Dhananjay Jadhav
Dhananjay Jadhav

Posted on

Why Your Pipe Code Deadlocks - A Lesson from MIT xv6

Introduction:

I was working on MIT's xv6 OS lab - the pingpong program. The idea was simple: parent sends a byte to child through a pipe, child reads it and prints "received ping", sends a byte back, parent reads it and prints "received pong".

I wrote the code. Ran it manually inside QEMU. Worked perfectly.
Then I ran the automated test. It just hung. No output. No error.Just silence until timeout.
 
Two hours later I found the problem. It was two lines.

What is a Pipe:

A pipe is a one way communication channel between two processes. In Unix every pipe has two ends:

int p[2];
pipe(p);
// p[0] → read end (you read from here)
// p[1] → write end (you write here)
Enter fullscreen mode Exit fullscreen mode

Parent writes into p[1], Child reads from p[0]. Simple.

For two way communication like pingpong you need two pipes, one for each direction.

 int p[2]; // parent → child
 int p1[2]; // child → parent
 pipe(p);
 pipe(p1);
Enter fullscreen mode Exit fullscreen mode

What is a Deadlock:

A deadlock is when two processes are both waiting for each other and neither can move forward.

Automated test timing out

Think of it like two people standing in a narrow corridor. Each one is waiting for the other to move first. Nobody moves. Forever.

In pipes it happens when:

  • Process A is waiting for Process B to finish
  • Process B is waiting for Process A to send data
  • Neither moves

My Mistake - wait() in the Wrong Place:

Wrong code in Neovim

This was parent code:

} else {
 wait(0); // ← waiting for child to finish FIRST
 read(p[1], &c, 1);
 write(p1[0], &d, 1);
}
Enter fullscreen mode Exit fullscreen mode

And my child code:

if(id == 0){
 read(p[0], &c, 1); // ← waiting for parent to send data
 write(p1[1], &c, 1);
}
Enter fullscreen mode Exit fullscreen mode

See the problem?

  • Parent calls wait(0) - it stops and waits for child to exit
  • Child calls read(p[0]) - it stops and waits for parent to send data
  • Parent is waiting for child. Child is waiting for parent.
  • Nobody moves. Forever.

This is a deadlock. The process just hangs silently until the test runner kills it with a timeout.

My Second Mistake - Not Closing Pipe Ends:

Every process gets both ends of the pipe after fork(). If you don't close the ends you don't use, read() never sees EOF and hangs waiting for more data even after the writer is done.

// Wrong - child keeps both ends open
 if(id == 0){
 read(p[0], &c, 1);
 }
Enter fullscreen mode Exit fullscreen mode
// Correct - child closes the end it doesn't use
 if(id == 0){
 close(p[1]); // child doesn't write to p
 read(p[0], &c, 1);
 }
Enter fullscreen mode Exit fullscreen mode

Rule: every process must close every pipe end it doesn't use. Always.

The Fix:

int main(){
 int p[2], p1[2];
 pipe(p); // parent writes, child reads
 pipe(p1); // child writes, parent readsint id = fork();
 if(id == 0){
   // child
   close(p[1]); // child doesn't write to p
   close(p1[0]); // child doesn't read from p1char c;
   read(p[0], &c, 1);           // receive from parent
   printf("%d: received ping\n", getpid());
   write(p1[1], &c, 1);         // send back to parentclose(p[0]);
   close(p1[1]);
   exit(0);
} else {
   // parent
   close(p[0]); // parent doesn't read from p
   close(p1[1]); // parent doesn't write to p1char c = 'x';
   write(p[1], &c, 1);          // send to child
   read(p1[0], &c, 1);          // receive from child
   printf("%d: received pong\n", getpid());close(p[1]);
   close(p1[0]);
   wait(0);        // wait AFTER all pipe I/O is done
   exit(0);
  }
}
Enter fullscreen mode Exit fullscreen mode

All tests passing
Two things changed:

  • wait(0) moved to after all pipe communication is done
  • Every unused pipe end is closed immediately after fork

The Rules That Stuck With Me:

  1. Always close pipe ends you don't use → otherwise read() hangs waiting for EOF forever
  2. wait() always comes after pipe I/O, never before → otherwise parent and child wait for each other forever
  3. pipe[0] is always read end, pipe[1] is always write end → never mix these up

What I'm Building Next:
This was Lab 1 of MIT 6.1810 - the xv6 RISC-V operating systems course. I'm working through all the labs on Arch Linux with a RISC-V cross compiler and QEMU.

Next up is Lab 2 - system calls. I'll be adding new syscalls directly into the xv6 kernel.

All my code is on GitHub: github.com/Dj-pages/xv6-labs

If you're learning systems programming, MIT xv6 is the best hands-on resource out there. Everything breaks, and that's exactly the point.

Top comments (0)