Skip to content

Exam Rank 04: Process & Parsing

Download Subject Files:

Exam Structure:

  • Level 1: picoshell, ft_popen, or sandbox (process-focused)
  • Level 2: argo or vbc (parsing-focused)

Key Concepts:

  • Process creation with fork()
  • Inter-process communication with pipe()
  • File descriptor manipulation with dup2()
  • Signal handling with sigaction()
  • Recursive descent parsing
  • Expression evaluation with operator precedence

fork() creates an exact copy of the current process.

#include <unistd.h>
#include <sys/wait.h>
pid_t pid = fork();
if (pid == -1) {
// Error - fork failed
perror("fork");
exit(1);
}
else if (pid == 0) {
// Child process
// pid == 0 means "I am the child"
printf("I am the child (PID: %d)\n", getpid());
exit(0);
}
else {
// Parent process
// pid == child's PID
printf("I am the parent, child PID: %d\n", pid);
wait(NULL); // Wait for child to finish
}
ParentChild
Returns child’s PIDReturns 0
Continues executionStarts at same point
Has original file descriptorsGets copies of file descriptors
Must wait() for childrenShould exit() when done

A pipe creates a unidirectional data channel:

  • pipefd[0] = read end
  • pipefd[1] = write end
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(1);
}
// pipefd[0] - read from pipe
// pipefd[1] - write to pipe

To connect cmd1 | cmd2:

  1. Create pipe
  2. Fork for cmd1: redirect stdout to pipe write end
  3. Fork for cmd2: redirect stdin from pipe read end
  4. Close unused pipe ends in all processes
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
// Child 1 (cmd1): writes to pipe
close(pipefd[0]); // Close read end
dup2(pipefd[1], STDOUT_FILENO); // Redirect stdout to pipe
close(pipefd[1]); // Close original write end
execvp(cmd1[0], cmd1);
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Child 2 (cmd2): reads from pipe
close(pipefd[1]); // Close write end
dup2(pipefd[0], STDIN_FILENO); // Redirect stdin from pipe
close(pipefd[0]); // Close original read end
execvp(cmd2[0], cmd2);
exit(1);
}
// Parent: close both ends and wait
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);

Why? A pipe’s read end returns EOF only when ALL write ends are closed. If you forget to close the write end in the reader process, it will hang forever waiting for input.

// WRONG - cmd2 will hang!
pid_t pid2 = fork();
if (pid2 == 0) {
dup2(pipefd[0], STDIN_FILENO);
// Forgot to close pipefd[1]!
execvp(cmd2[0], cmd2);
}
// CORRECT
pid_t pid2 = fork();
if (pid2 == 0) {
close(pipefd[1]); // MUST close write end!
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
execvp(cmd2[0], cmd2);
}

dup2(oldfd, newfd) makes newfd point to the same file as oldfd.

// Redirect stdout to a file
int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
dup2(fd, STDOUT_FILENO); // Now stdout writes to output.txt
close(fd); // Close original fd (stdout still works)
printf("This goes to output.txt\n");
// Redirect stdout to pipe write end
dup2(pipefd[1], STDOUT_FILENO);
// Redirect stdin from pipe read end
dup2(pipefd[0], STDIN_FILENO);
// Redirect stderr to stdout
dup2(STDOUT_FILENO, STDERR_FILENO);

FunctionPath ResolutionEnvironment
execve(path, argv, envp)Must be full pathYou provide envp
execvp(file, argv)Searches PATHUses current environ
// execvp - easier, searches PATH
char *argv[] = {"ls", "-la", NULL};
execvp("ls", argv); // Will find /bin/ls automatically
// execve - more control
char *envp[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, envp);

Important: execvp Never Returns on Success!

Section titled “Important: execvp Never Returns on Success!”
execvp(cmd[0], cmd);
// If we reach here, execvp failed!
perror("execvp");
exit(1); // Child must exit on exec failure

#include <signal.h>
void handler(int sig) {
// Handle signal
write(1, "Caught signal\n", 14);
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, NULL);
alarm(5); // Send SIGALRM in 5 seconds
pause(); // Wait for signal
return 0;
}
alarm(timeout); // Schedule SIGALRM in 'timeout' seconds
// ... do work ...
alarm(0); // Cancel the alarm
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
// Process exited normally
int exit_code = WEXITSTATUS(status);
printf("Exited with code %d\n", exit_code);
}
else if (WIFSIGNALED(status)) {
// Process killed by signal
int sig = WTERMSIG(status);
printf("Killed by signal: %s\n", strsignal(sig));
}

For expression parsing, define a grammar:

expr = term (('+') term)*
term = factor (('*') factor)*
factor = '(' expr ')' | NUMBER
int parse_expr(const char **s);
int parse_term(const char **s);
int parse_factor(const char **s);
// Factor: number or (expr)
int parse_factor(const char **s) {
if (**s == '(') {
(*s)++; // Skip '('
int result = parse_expr(s);
if (**s != ')') error("Expected ')'");
(*s)++; // Skip ')'
return result;
}
if (isdigit(**s)) {
int num = **s - '0';
(*s)++;
return num;
}
error("Unexpected token");
return 0;
}
// Term: factor (* factor)*
int parse_term(const char **s) {
int result = parse_factor(s);
while (**s == '*') {
(*s)++;
result *= parse_factor(s);
}
return result;
}
// Expr: term (+ term)*
int parse_expr(const char **s) {
int result = parse_term(s);
while (**s == '+') {
(*s)++;
result += parse_term(s);
}
return result;
}
  • * has higher precedence than +
  • By parsing * in term (called first), it binds tighter
  • 3+4*5 parses as 3+(4*5) = 23, not (3+4)*5 = 35

value = string | number | object
object = '{' (pair (',' pair)*)? '}'
pair = string ':' value
string = '"' characters '"'
number = digits
int parse_value(FILE *f, json *dst) {
int c = getc(f);
if (c == '"') {
return parse_string(f, dst);
}
else if (isdigit(c)) {
ungetc(c, f);
return parse_number(f, dst);
}
else if (c == '{') {
return parse_object(f, dst);
}
else if (c == EOF) {
printf("Unexpected end of input\n");
return -1;
}
else {
printf("Unexpected token '%c'\n", c);
return -1;
}
}
int parse_string(FILE *f, json *dst) {
// Already consumed opening "
char buffer[1024];
int i = 0;
int c;
while ((c = getc(f)) != EOF && c != '"') {
if (c == '\\') {
c = getc(f);
if (c == '"' || c == '\\') {
buffer[i++] = c;
} else {
// Invalid escape
printf("Unexpected token '%c'\n", c);
return -1;
}
} else {
buffer[i++] = c;
}
}
if (c == EOF) {
printf("Unexpected end of input\n");
return -1;
}
buffer[i] = '\0';
// Store string in dst...
return 1;
}

// WRONG - leaking fds
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
dup2(pipefd[1], 1);
execvp(cmd[0], cmd);
}
// Parent forgot to close pipefd[0] and pipefd[1]!
// CORRECT
if (pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], 1);
close(pipefd[1]);
execvp(cmd[0], cmd);
exit(1);
}
close(pipefd[0]);
close(pipefd[1]);
// WRONG - creates zombies
for (int i = 0; i < n; i++) {
if (fork() == 0) {
execvp(cmds[i][0], cmds[i]);
exit(1);
}
}
// Parent exits without waiting!
// CORRECT
for (int i = 0; i < n; i++) {
pids[i] = fork();
if (pids[i] == 0) {
execvp(cmds[i][0], cmds[i]);
exit(1);
}
}
for (int i = 0; i < n; i++) {
waitpid(pids[i], NULL, 0);
}
// WRONG - child continues after failed exec
if (fork() == 0) {
execvp(cmd[0], cmd);
// If execvp fails, child continues running parent's code!
}
// CORRECT
if (fork() == 0) {
execvp(cmd[0], cmd);
perror("execvp");
exit(1); // MUST exit!
}

FunctionPurpose
fork()Create child process
wait(NULL)Wait for any child
waitpid(pid, &status, 0)Wait for specific child
exit(code)Terminate process
FunctionPurpose
pipe(int fd[2])Create pipe (fd[0]=read, fd[1]=write)
dup2(old, new)Redirect new to old
close(fd)Close file descriptor
FunctionPurpose
execvp(file, argv)Execute with PATH search
execve(path, argv, envp)Execute with full path
FunctionPurpose
sigaction(sig, &sa, NULL)Set signal handler
alarm(seconds)Schedule SIGALRM
kill(pid, sig)Send signal to process
MacroPurpose
WIFEXITED(status)True if exited normally
WEXITSTATUS(status)Get exit code
WIFSIGNALED(status)True if killed by signal
WTERMSIG(status)Get signal number