A Unix shell implementation in C featuring process management, job control, and signal handling. Demonstrates advanced systems programming concepts including process groups, signal masking, and race condition prevention.
This shell supports foreground and background job execution, built-in commands, and proper signal handling for job control. The implementation focuses on correctly handling race conditions and managing process lifecycle through careful use of signals and synchronization primitives.
- Job Control: Run processes in foreground or background
- Signal Handling: Proper handling of SIGINT (Ctrl-C), SIGTSTP (Ctrl-Z), and SIGCHLD
- Built-in Commands:
quit,jobs,fg,bg - Process Management: Correct zombie reaping and process group management
- Race Condition Prevention: Careful signal blocking to avoid race conditions
- Linux or WSL (Windows Subsystem for Linux)
- GCC compiler
- POSIX-compliant system
makeOr compile directly:
gcc -Wall -O2 -o tsh tsh.cRun the shell interactively:
./tshThe shell displays a prompt and waits for commands:
tsh> /bin/ls -l
tsh> /bin/ps &
tsh> jobs
tsh> fg %1
tsh> quitquit- Exit the shelljobs- List all background jobsfg <job>- Bring a background job to foreground (use%1for job ID or PID)bg <job>- Resume a stopped background job
<program> [args]- Run program in foreground<program> [args] &- Run program in background
Ctrl-C- Send SIGINT to foreground jobCtrl-Z- Send SIGTSTP to foreground job (suspend)
Race Condition Prevention
- Signal blocking before forking to prevent race between child termination and job list addition
- Proper use of
sigprocmask()to block/unblock SIGCHLD signals
Signal Handling
- Custom handlers for SIGINT, SIGTSTP, and SIGCHLD
- Forwarding signals to entire process groups using negative PIDs
- Non-blocking signal handlers using
waitpid()with WNOHANG
Process Groups
- Each job runs in its own process group
- Uses
setpgid(0, 0)to create new process group for child - Ensures shell remains in foreground process group
Zombie Reaping
- All reaping done in SIGCHLD handler
- Uses
waitpid(-1, &status, WNOHANG | WUNTRACED)to handle all terminated/stopped children
The shell uses a job list to track all running and stopped jobs. Each job contains:
- Job ID (JID)
- Process ID (PID)
- State (foreground, background, stopped)
- Command line
Main execution flow:
- Parse command line
- Check if built-in command
- If not built-in:
- Block SIGCHLD
- Fork child process
- Child: set process group, unblock signals, exec program
- Parent: add job to list, unblock signals, wait if foreground
The tests/ directory contains trace files that test various shell features:
# Test basic execution
./tsh < tests/trace01.txt
# Test process control
./tsh < tests/trace05.txt
# Test signal handling
./tsh < tests/trace08.txt
# Test background jobs
./tsh < tests/trace11.txt
# Comprehensive test
./tsh < tests/trace15.txtEach trace file tests specific functionality:
- trace01-04: Basic command execution
- trace05-07: Process termination and signals
- trace08-10: Signal handling (SIGINT, SIGTSTP)
- trace11-13: Background jobs and job control
- trace14-16: Comprehensive job control scenarios
unix-shell/
├── tsh.c # Shell implementation
├── Makefile # Build configuration
├── README.md # This file
└── tests/ # Test trace files
├── trace01.txt
├── trace02.txt
└── ...
- Uses
fork(),execve(),waitpid(),kill()for process management - Uses
sigprocmask(),sigemptyset(),sigaddset()for signal blocking - Uses
setpgid()to manage process groups - No use of
sleep()for synchronization (busy-wait loop withsleep()used inwaitfg) - All signal handlers are async-signal-safe
- Process creation and management
- Signal handling and masking
- Race condition prevention
- Process groups and job control
- Zombie process reaping
- Foreground/background job management
- Built-in command implementation