This document is structured for a 4-person team presentation. It includes the theoretical concepts, code snippets from the HOWK codebase, and live terminal commands for each presenter to demonstrate their specific topic.
Topic: What is a shell, how does it parse input, and how does it execute built-in vs. external commands?
- What is a Shell? A shell is a command-line interpreter that provides a traditional user interface for the Unix/Linux operating system. At its core, it runs a REPL loop: Read, Evaluate, Print, Loop.
- The Parser: When the user types a command, it's just a raw string of text. The parser (
parser.c) breaks this string down into tokens (like words), handles quotes so"hello world"stays as one token, and looks for special shell syntax like pipes|or background markers&. - Built-in vs External:
- Built-ins (like
cdorexit) must run in the parent process because they change the shell's own state (like its current directory). - External Commands (like
lsorgrep) are standalone programs. The shell uses thefork()system call to create a clone of itself, and thenexecvp()in the child to replace the clone with the new program.
- Built-ins (like
// The core REPL loop in shell.c
while (1) {
reap_background_processes(); // Clean up zombies
read_command_line(prompt, line, sizeof(line));
parse_input(line, &cmd);
if (cmd.is_builtin) {
execute_builtin(&cmd);
} else {
execute_process(&cmd);
}
}
// The core of executing an external command
pid_t pid = fork();
if (pid == 0) {
// Child process: replace itself with the new program
execvp(cmd->args[0], cmd->args);
} else {
// Parent process: wait for the child to finish
waitpid(pid, &status, 0);
}Demonstrate the basic functionality of the shell.
# Show a built-in command changing shell state
cd Sample
help
# Show an external command executing
ls -laTopic: How the shell handles input/output streams, file descriptors, and pipes.
- File Descriptors (FDs): In Linux, everything is a file. Every process starts with 3 standard open files: STDIN (0), STDOUT (1), and STDERR (2).
- Redirection (
>,<): We can change where a program reads from or writes to without the program knowing. Before callingexecvp(), we open the target file and use thedup2()system call to forcefully map STDOUT (1) to that new file. - Pipes (
|): A pipe is an in-memory buffer provided by the OS. It has a read end and a write end. To chain commands likels | grep, we fork two children. Child A maps its STDOUT to the pipe's write end. Child B maps its STDIN to the pipe's read end. Data flows directly from one program to another!
// Handling Redirection (apply_redirections)
if (cmd->output_file) {
int fd = open(cmd->output_file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // Force STDOUT to point to the file
close(fd);
}
// Handling Pipes (execute_pipeline)
int pipes[MAX_PIPES][2];
pipe(pipes[0]); // Create a pipe buffer in the OS
if (fork() == 0) {
// First child: connect STDOUT to pipe's write end
dup2(pipes[0][1], STDOUT_FILENO);
execvp(cmd1);
}
if (fork() == 0) {
// Second child: connect STDIN to pipe's read end
dup2(pipes[0][0], STDIN_FILENO);
execvp(cmd2);
}Demonstrate I/O manipulation.
# Output redirection
ls -la > my_files.txt
# Input redirection
wc -l < my_files.txt
# Piping multiple commands together
cat my_files.txt | grep ".c" | sort -rTopic: Managing background tasks, the job tracker, and priority scheduling.
- What is a Job? When a user adds
&(orBGin HOWK) to a command, the shell shouldn't block waiting for it. It runs the process in the background. We track these in a "Job Tracker" registry. - Zombies & Reaping: When a background process finishes, it becomes a "zombie" until the parent acknowledges it. HOWK calls
waitpid(-1, &status, WNOHANG)at every prompt to clean these up silently. - The Priority Queue: We upgraded standard background execution by adding a custom scheduling layer (
queue.c). Instead of just running jobs immediately, we can queue them based on user-defined priority levels (High, Normal, Low). The queue is implemented as a priority-sorted linked list.
// Adding a job to the tracker array
int add_job(pid_t pid, const char *cmd, JobState state) {
for (int i = 0; i < MAX_JOBS; i++) {
if (jobs[i].job_id == 0) { // Find empty slot
jobs[i].job_id = next_job_id++;
jobs[i].pid = pid;
strncpy(jobs[i].command, cmd, 256);
return jobs[i].job_id;
}
}
}
// Reaping zombies without blocking the shell
void reap_background_processes(void) {
pid_t pid;
// WNOHANG means "return immediately if no child has exited"
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
remove_job_by_pid(pid);
}
}Demonstrate job tracking.
# Run a long task in the background
sleep 20 BG
# Show that the shell is tracking it
j*b
# Bring it back to the foreground to wait for it
onTop 1Topic: Multi-threading, the producer-consumer problem, and preventing race conditions.
- Why Threads? Instead of just letting the OS manage background processes chaotically, HOWK implements a strict Threadpool (
threadpool.c). We spawn a fixed number of worker threads at startup. - Producer-Consumer Model: The main shell thread is the Producer—it parses commands and drops them into the Priority Queue. The worker threads are the Consumers—they constantly check the queue, grab a job, and execute it.
- Synchronization & Race Conditions: Because multiple threads are accessing the exact same queue at the exact same time, we could have a Race Condition where two threads try to grab the same job.
- Mutexes & Condition Variables: We solve this using a
pthread_mutex. A thread must "lock" the queue before touching it. We also use apthread_cond_t(Condition Variable) so idle threads can go to sleep efficiently, and the main thread can "wake them up" when a new job arrives.
// The Worker Thread Loop
static void* worker_loop(void *arg) {
while (1) {
// 1. Lock the shared resource (mutex)
pthread_mutex_lock(&pool_mutex);
// 2. Sleep if the queue is empty (Condition Variable)
while (is_queue_empty()) {
pthread_cond_wait(&pool_cond, &pool_mutex);
}
// 3. Grab a job and unlock so other threads can access the queue
Job current_job = dequeue_job();
pthread_mutex_unlock(&pool_mutex);
// 4. Do the actual heavy lifting (fork/exec)
execute_process(¤t_job.cmd);
}
}Demonstrate the threadpool and priority logic working together.
# Initialize a threadpool with 2 workers
tpool 2
# Fill up the workers with long jobs
sleep 15 BG
sleep 15 BG
# The pool is now full. Add a LOW priority job to the queue
sleep 5 priority=2 BG
# Add a HIGH priority job to the queue
sleep 5 priority=0 BG
# View the state
j*b
# Observation: Even though the Low Priority job was added first,
# as soon as a worker finishes its 15s sleep, it will grab the
# High Priority job from the queue first!