A linked list (jobs_head) maintains all active jobs (both background and suspended). Each process entry stores PID, state (R for running, T for suspended), whether it is a background process, and the original prompt string. A linked list was chosen to maintain the job table because it allows for efficient insertion and removal of processes without the need to shift elements, as would be required in a fixed-size array. This structure enables dynamic management of active jobs.
Processes were terminated or suspended using kill() through functions that operate on the linked list. Since the linked list (job table) already stores each process’s PID, this approach allows signals to be sent to the target process while simultaneously removing its entry from the list, simplifying process management.
SIGINT (CTRL+C)is used to terminate the foreground process without affecting background jobs.SIGTSTP (CTRL+z)suspends the foreground process and updates its state in the jobs table.SIGCHLDis used to reap terminated background process and prevent zombies.SIGTERMis used to terminate running or suspended processes whenexitis called to gracelfully exit from the shell.kill()was used to send signal to child processessigaction()used to define signal handlers in parent process (shell).- I used
setpgid()to place child processes in their own process groups. This prevents background jobs from receiving terminal signals likeSIGINTandSIGTSTP, ensuring only the foreground job is affected.
Implemented as direct function calls except jobs which is implemented through linked list (cd -> chdir(), pwd -> getcwd(), exit -> _exit() jobs)
Spawned with fork() + execve(). _exit() used to exit out of child process if execve() fails. Shell stopped from execution when commands were run in foreground by waiting for child process to finish executing using waitpid(). Each type of command (I/O redirection, piped, regular) is handled by a dedicated function that formats a struct ExtCmd. This structure is then passed to run() in commands/external.c, which contains the core execution logic, improving modularity and code reusability.
Implemented using pipe(), dup2() and open(). For piped commands, a dedicated child process is spawned using fork() to orchestrate the communication between the two child processes (spawned by the original child) that perform the individual commands. This design is used to isolate the management of the pipe from the main shell process, ensuring that the parent shell remains responsive
Runs jobs asynchronously withot blocking the shell, no waitpid() call to avoid blocking, instead a SIGCHLD signal handler is implemented.
Signals could interrupt blocking I/O (like fgets). To handle this, sigaction with SA_RESTART is used.
malloc() and free() used to manage jobs data structure on heap so it can be accessible through the program lifetime.
To test Dragon Shell, I verified both built-in and external commands. Built-ins like cd, pwd, and jobs worked correctly and updated shell state. External commands such as ls and sleep executed as expected, including background execution with &. I tested input/output redirection (/usr/bin/ls > out.txt, /usr/bin/cat < in.txt) and piping (/usr/bin/find ./ | /usr/bin/sort) to confirm correct use of dup2() and pipe(). Signal handling was validated by running long-lived processes (/usr/bin/sleep 30) and interrupting them with Ctrl+C (terminates) or Ctrl+Z (suspends), with jobs updated accordingly. Background processes were reaped with SIGCHLD, ensuring no zombie processes remained.