Skip to content

Latest commit

 

History

History
209 lines (166 loc) · 7.82 KB

File metadata and controls

209 lines (166 loc) · 7.82 KB

HOWK Shell: Team Presentation & Demo Guide

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.


Person 1: Shell Core, Parsing & Execution

Topic: What is a shell, how does it parse input, and how does it execute built-in vs. external commands?

Speaker Notes

  • 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 cd or exit) must run in the parent process because they change the shell's own state (like its current directory).
    • External Commands (like ls or grep) are standalone programs. The shell uses the fork() system call to create a clone of itself, and then execvp() in the child to replace the clone with the new program.

Code Snippet (src/shell/shell.c & src/shell/process_executor.c)

// 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);
}

Demo Commands

Demonstrate the basic functionality of the shell.

# Show a built-in command changing shell state
cd Sample
help

# Show an external command executing
ls -la

Person 2: File Descriptors, Piping & Redirection

Topic: How the shell handles input/output streams, file descriptors, and pipes.

Speaker Notes

  • 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 calling execvp(), we open the target file and use the dup2() 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 like ls | 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!

Code Snippet (src/shell/process_executor.c)

// 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);
}

Demo Commands

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 -r

Person 3: Jobs & Scheduling

Topic: Managing background tasks, the job tracker, and priority scheduling.

Speaker Notes

  • What is a Job? When a user adds & (or BG in 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.

Code Snippet (src/jobs/queue.c & src/jobs/job_tracker.c)

// 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);
    }
}

Demo Commands

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 1

Person 4: Threads, Synchronization & Threadpool

Topic: Multi-threading, the producer-consumer problem, and preventing race conditions.

Speaker Notes

  • 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 a pthread_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.

Code Snippet (src/jobs/threadpool.c)

// 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(&current_job.cmd);
    }
}

Demo Commands

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!