Skip to content

Teaching Notes: Exam Rank 04

Rank 04 tests Unix system programming fundamentals. Students must understand:

  • How processes work (fork, exec, wait)
  • How data flows between processes (pipes, fd redirection)
  • How to handle errors gracefully
  • Basic parsing techniques

The exam has two levels. Level 1 focuses on process management, Level 2 on parsing. Guide students through concepts, don’t give direct answers.



  • Pipeline execution with pipe() and fork()
  • File descriptor management with dup2()
  • Process synchronization with wait()
  • Error handling and cleanup
int picoshell(char **cmds[]);
// Execute pipeline: cmd1 | cmd2 | cmd3 | ...
// Return 0 on success, 1 on any error
// Must close all fds before returning
  1. Not closing pipe ends in child processes

    // WRONG
    if (fork() == 0) {
    dup2(pipefd[1], 1);
    execvp(cmd[0], cmd);
    }
    // CORRECT
    if (fork() == 0) {
    close(pipefd[0]); // Close read end!
    dup2(pipefd[1], 1);
    close(pipefd[1]); // Close after dup2!
    execvp(cmd[0], cmd);
    exit(1); // Exit if exec fails!
    }
  2. Parent not closing pipe ends

    • After forking all children, parent must close all pipe fds
    • Otherwise children reading from pipe will hang
  3. Not waiting for all children

    • Creates zombie processes
    • Must wait() or waitpid() for every forked child
  4. Forgetting to exit after failed execvp

    • If execvp fails, child continues running parent’s code
    • Always exit(1) after failed exec
  5. Wrong pipe direction

    • pipefd[0] = read, pipefd[1] = write
    • Previous command writes to pipefd[1]
    • Next command reads from pipefd[0]
  1. “What does pipe() create? Which end is for reading?”
  2. “After fork(), who needs to close which pipe ends?”
  3. “What happens if you don’t close the write end of a pipe?”
  4. “How do you redirect stdout to a pipe?”
  5. “What happens if execvp fails? Does it return?”
  • Process hangs (forgot to close pipe ends)
  • Zombie processes (forgot to wait)
  • File descriptor leaks (check with lsof -c picoshell)
For cmd1 | cmd2:
1. Create pipe
2. Fork for cmd1:
- Close pipe read end (won't read)
- dup2 pipe write end to stdout
- Close original pipe write end
- exec cmd1
3. Fork for cmd2:
- Close pipe write end (won't write)
- dup2 pipe read end to stdin
- Close original pipe read end
- exec cmd2
4. Parent:
- Close both pipe ends
- Wait for both children

  • Understanding of popen() system call behavior
  • Pipe creation and direction based on mode
  • Fork/exec pattern
  • File descriptor management
int ft_popen(const char *file, char *const argv[], char type);
// type 'r': return fd connected to command's stdout (read from it)
// type 'w': return fd connected to command's stdin (write to it)
// Return -1 on error
  1. Wrong pipe direction for read/write mode

    // For 'r' mode (read command's output):
    // Parent reads from pipefd[0]
    // Child writes to pipefd[1] (redirect stdout)
    // For 'w' mode (write to command's input):
    // Parent writes to pipefd[1]
    // Child reads from pipefd[0] (redirect stdin)
  2. Returning wrong fd to caller

    • ‘r’ mode: return read end, close write end
    • ‘w’ mode: return write end, close read end
  3. Not validating type parameter

    • Must check type == 'r' || type == 'w'
    • Return -1 for invalid type
  4. Forgetting child process handling

    • Must close unused pipe end in child
    • Must exit(1) if exec fails
  1. “If you want to read a command’s output, which pipe end do you return?”
  2. “In ‘r’ mode, what should the child redirect? stdin or stdout?”
  3. “What fd should the parent keep open? What should it close?”
'r' mode: I want to READ what the command outputs
- Child: redirect stdout -> pipe write
- Parent: return pipe read, close pipe write
'w' mode: I want to WRITE to the command's input
- Child: redirect stdin <- pipe read
- Parent: return pipe write, close pipe read

  • Signal handling with sigaction()
  • Timeouts with alarm()
  • Process status analysis (WIFEXITED, WIFSIGNALED)
  • Error detection and reporting
int sandbox(void (*f)(void), unsigned int timeout, bool verbose);
// Return 1 if f is "nice" (exits 0, no signal, no timeout)
// Return 0 if f is "bad" (signal, non-zero exit, timeout)
// Return -1 on error in sandbox itself
  1. Not handling all “bad” cases

    • Killed by signal (WIFSIGNALED)
    • Exited with non-zero code (WEXITSTATUS != 0)
    • Timed out (SIGALRM)
  2. Signal handler issues

    // WRONG - handler modifies global state unsafely
    void handler(int sig) {
    timed_out = true; // Race condition!
    }
    // BETTER - use volatile sig_atomic_t or check in parent
    volatile sig_atomic_t timed_out = 0;
  3. Zombie processes

    • If timeout kills child, still need to wait() for it
    • Must reap ALL children
  4. Wrong alarm() usage

    // Set alarm BEFORE fork, or in parent after fork
    // Child inherits parent's alarms - be careful!
    pid_t pid = fork();
    if (pid == 0) {
    // Child
    f(); // Run the function
    exit(0);
    }
    // Parent
    alarm(timeout); // Set timeout
    waitpid(pid, &status, 0);
    alarm(0); // Cancel alarm
  5. Wrong verbose messages

    • Must match exact format from subject
    • Use strsignal() for signal description
  1. “How do you detect if a process was killed by a signal?”
  2. “What macro tells you the exit code?”
  3. “How does alarm() work? What signal does it send?”
  4. “What happens to the alarm when waitpid returns?”
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
if (code == 0)
// Nice!
else
// Bad: exited with code
}
else if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
// Bad: killed by signal
// Use strsignal(sig) for description
}


  • Recursive descent parsing
  • Character-by-character input processing
  • Escape sequence handling
  • Error reporting with specific messages
int argo(json *dst, FILE *stream);
// Parse JSON into AST structure
// Handle: numbers, strings, objects (maps)
// Return 1 on success, -1 on failure
// Print "Unexpected token '%c'\n" or "Unexpected end of input\n"
  1. Not handling escape sequences correctly

    // Only handle \\ and \"
    // Other escapes (\n, \t, etc.) are NOT required
    if (c == '\\') {
    c = getc(f);
    if (c == '\\' || c == '"') {
    // Valid escape
    } else {
    // Invalid - but subject may not test this
    }
    }
  2. Treating spaces as valid

    • Subject says: “Don’t handle spaces -> consider them as invalid tokens”
    • Space should trigger “Unexpected token ’ ’”
  3. Wrong error messages

    • Must be exactly: "Unexpected token '%c'\n"
    • For EOF: "Unexpected end of input\n"
  4. Not handling recursive objects

    {"a":{"b":{"c":1}}}
    • Must recursively parse nested objects
  5. Memory leaks

    • If parsing fails mid-way, must free allocated memory
  1. “What are the three types of values you need to parse?”
  2. “How do you detect a string vs a number vs an object?”
  3. “What function lets you ‘peek’ at the next character without consuming it?”
  4. “How do you handle escaped quotes inside strings?”
int parse_value(FILE *f, json *dst) {
int c = getc(f);
if (c == '"')
return parse_string(f, dst);
if (isdigit(c)) {
ungetc(c, f);
return parse_number(f, dst);
}
if (c == '{')
return parse_object(f, dst);
if (c == EOF) {
printf("Unexpected end of input\n");
return -1;
}
printf("Unexpected token '%c'\n", c);
return -1;
}

  • Recursive descent parsing with operator precedence
  • Parenthesis handling
  • Error detection and reporting
  • Basic arithmetic evaluation
vbc '3+4*5' -> 23 (not 35!)
vbc '(3+4)*5' -> 35
Handle: +, *, (), digits 0-9
Error: "Unexpected token '%c'\n" or "Unexpected end of input\n"
  1. Wrong operator precedence

    // WRONG - left to right
    3+4*5 = 35 // (3+4)*5
    // CORRECT - * before +
    3+4*5 = 23 // 3+(4*5)

    The trick: parse * at a deeper level than +.

  2. Not handling nested parentheses

    (((3))) should work
    ((1+2)*3) should work
  3. Wrong error for unmatched parentheses

    Input: "1+2)"
    Error: "Unexpected token ')'\n"
    Input: "(1+2"
    Error: "Unexpected end of input\n"
  4. Not handling multi-digit… wait, subject says 0-9 only!

    • Each digit is a single number
    • 12 would be 1 then 2 (unexpected token ‘2’ after expression)
  1. “If * has higher precedence than +, which should you parse first?”
  2. “In the grammar, what’s the difference between expr, term, and factor?”
  3. “How do parentheses affect the grammar?”
  4. “What happens when you see an unexpected character?”
expr = term (('+') term)* // Addition (lowest precedence)
term = factor (('*') factor)* // Multiplication (higher precedence)
factor = '(' expr ')' | DIGIT // Parentheses or number (highest)
int parse_expr(const char **s); // Handles +
int parse_term(const char **s); // Handles *
int parse_factor(const char **s); // Handles () and digits
// factor is called first for each operand
// This ensures * binds tighter than +

  • Level 1: ~45 min (process exercise)
  • Level 2: ~45 min (parsing exercise)
  • Leave 20 min for testing and debugging
  1. Test happy path first
  2. Test edge cases (empty input, single element)
  3. Test error cases
  4. Check for fd leaks: lsof -c program_name
  5. Check for zombies: ps aux | grep defunct
  • Forgetting to exit(1) after failed execvp
  • Closing fds in wrong order
  • Wrong error message format
  • Not handling EOF
  1. “Draw the pipe diagram on paper first”
  2. “Which process needs to read? Which needs to write?”
  3. “What happens to data after close()?”
  4. “Trace through your code with a simple example”