Two children, two pipes, syscalls only
◈ 12 cardsDesign a multi-process program before writing it: the pipe topology, the close table that keeps exactly one writer and one reader per pipe, and the system-call-only I/O the marking scheme insists on.
Design the plumbing first
The assignment shape is always the same: a parent, two children, two pipes, and a job that has to travel from one end of that chain to the other. The marks are not in the job. They are in the plumbing, and the only reliable way to get them is to draw the topology and the close table before writing a line of code.
The running example here is the one the assignment uses. The program takes a fully-qualified folder name as its argument and:
- child 1 decides whether that folder already exists, and reports its verdict;
- child 2 receives the verdict and creates the folder if it is missing, then confirms;
- the parent receives the confirmation, looks inside the folder, deletes anything it finds, and reports success.
That is a chain — child 1 to child 2 to parent — so it needs two pipes, because a pipe is unidirectional. Call them pipe A (child 1 → child 2) and pipe B (child 2 → parent).
Both pipes are created before either fork
A pipe can only be shared by inheritance, so the parent creates pipe A and pipe B before the first fork. Both children then inherit both pipes automatically. Creating a pipe after a fork gives the creating process a private pipe nobody else can see, which is the single design error that makes the whole program impossible rather than merely broken.
The close table
After two pipes and two forks there are three processes, each holding four pipe-end descriptors: A[0], A[1], B[0], B[1]. That is twelve descriptor copies for four actual ends, and the correctness condition is simple to state:
> When the closing is done, each pipe must have exactly one process holding its write end and exactly one holding its read end.
Work out who needs what:
- child 1 writes to pipe A. It keeps
A[1]and closesA[0],B[0],B[1]— three closes. - child 2 reads pipe A and writes pipe B. It keeps
A[0]andB[1], and closesA[1]andB[0]— two closes. - the parent reads pipe B. It keeps
B[0]and closesA[0],A[1],B[1]— three closes.
That is eight closes immediately after the forks, leaving four ends open — exactly one writer and one reader per pipe. The remaining four are closed when each process has finished with them: child 1 closes A[1] before _exit, child 2 closes both of its ends, and the parent closes B[0] after its last read. Twelve closes in the finished program, and if you cannot point to all twelve you have not finished the design.
Miss one and the symptom is always the same. Suppose the parent forgets close(A[1]). Child 2 reads pipe A, gets child 1’s message, calls read again, and blocks forever, because a write end of pipe A is still open — in the parent, which will never write to it. The program deadlocks, produces no error, and looks like a logic bug in child 2.
System calls only
A question of this shape usually pins you to the kernel interface: every read, every write and every message to the terminal has to go through a system call, with the C library’s conveniences ruled out. Read that constraint literally, because the marker will.
- No
printf, noputs, nofprintf, nofopen. Every one of those is stdio, which is a library on top ofwrite. Print withwrite(1, msg, len)and read withread(0, buf, n). writeneeds a length, andstrlenis a library function too. Either write the length as a constant —write(1, "folder created\n", 15)— or count it with a small loop of your own. Say in your answer which you did; it shows you noticed the constraint.- Test for the folder with
statorlstat, not withopendirplus stdio.statfills astruct stat;S_ISDIR(st.st_mode)then distinguishes "exists and is a directory" from "exists but is not one", and a −1 return witherrno == ENOENTis "does not exist". Those are three different outcomes and the marking scheme wants all three. - Create with
mkdir(path, 0755), remove entries withunlink, remove the directory itself withrmdir.
Say all of this explicitly in a written answer. It is where the marks are lost — not because candidates use printf by accident, but because they never mention the constraint and the marker cannot award a point for a decision that was not stated.
The messages
A pipe is a byte stream, so define the message before you write the code. One byte is enough here and is easier to defend than a string: child 1 writes 'Y' if the folder exists and 'N' if it does not; child 2 reads that one byte and writes back 'C' for created or 'K' for it was already there. Fixed-size messages mean the reader knows exactly how many bytes to expect and there is no parsing to get wrong. Whatever you choose, write the definition down — an answer that says "child 1 sends the result" without saying what the bytes are has left a mark on the table.
Wait for the children
The parent finishes by calling wait(NULL) twice, once per child. Skip it and both children become zombies — terminated processes still holding a process-table entry, because nobody has collected their exit status. They are cleared only when the parent itself exits and init adopts and reaps them. In a program this short nobody dies of it, but the marking scheme asks for it, and the habit is what stops a long-running server leaking a process-table entry per request.
And check every return. pipe, fork, stat, mkdir, read, write, unlink and rmdir all report failure the same way — −1 — and an answer that checks none of them is an answer that has not understood the convention the whole of Part II is built on.