Chapter 17: Unix, Linux, and Shell Scripting
Introduction
While building kernels teaches you how operating systems work internally, most system programming involves working with existing systems like Linux and Unix. This chapter covers Linux system programming APIs, shell scripting for automation, process management, and interacting with the kernel from user space.
Why This Matters
Linux powers servers, embedded systems, Android phones, supercomputers, and cloud infrastructure. Understanding Linux system programming is essential for server development, DevOps, system administration, and application development. Shell scripting automates tasks and glues programs together, making you vastly more productive.
How to Study This Chapter
- Practice on Linux - Use a Linux VM or WSL if needed
- Read man pages - They're the authoritative documentation
- Write scripts - Automate your own workflows
- Study existing code - Read shell scripts and C programs
- Experiment safely - Test in VMs, not production systems
Linux System Call Interface
Common System Calls
Linux provides hundreds of system calls. Here are the most important:
File Operations:
open()- Open fileread()- Read from file descriptorwrite()- Write to file descriptorclose()- Close file descriptorlseek()- Move file offset
Process Management:
fork()- Create child processexec()- Replace process imagewait()- Wait for child processexit()- Terminate processgetpid()- Get process ID
Memory:
mmap()- Map memorymunmap()- Unmap memorybrk()/sbrk()- Change heap size
Signals:
kill()- Send signalsignal()/sigaction()- Set signal handler
Using System Calls
Example: File I/O
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
// Open file
int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}
// Write to file
const char *msg = "Hello, Linux!\n";
ssize_t written = write(fd, msg, 14);
if (written < 0) {
perror("write");
close(fd);
return 1;
}
// Move to beginning
lseek(fd, 0, SEEK_SET);
// Read from file
char buf[100];
ssize_t nread = read(fd, buf, sizeof(buf));
if (nread > 0) {
write(STDOUT_FILENO, buf, nread);
}
// Close file
close(fd);
return 0;
}
Process Management
Creating Processes with fork()
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
printf("Parent PID: %d\n", getpid());
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// Child process
printf("Child PID: %d\n", getpid());
printf("Child's parent PID: %d\n", getppid());
return 0;
} else {
// Parent process
printf("Parent created child with PID: %d\n", pid);
// Wait for child
int status;
wait(&status);
printf("Child exited with status: %d\n", WEXITSTATUS(status));
}
return 0;
}
Executing Programs with exec()
#include <unistd.h>
#include <stdio.h>
int main() {
printf("About to execute ls...\n");
char *args[] = {"ls", "-la", NULL};
execvp("ls", args);
// If exec returns, it failed
perror("execvp");
return 1;
}
Fork + Exec Pattern
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// Child: execute another program
char *args[] = {"echo", "Hello from child", NULL};
execvp("echo", args);
perror("execvp");
return 1;
} else {
// Parent: wait for child
wait(NULL);
printf("Child finished\n");
}
return 0;
}
Signals
Signal Handling
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int sig) {
printf("Received signal %d\n", sig);
}
int main() {
// Register signal handler
signal(SIGINT, signal_handler); // Ctrl+C
printf("Press Ctrl+C to trigger signal\n");
printf("PID: %d\n", getpid());
while (1) {
sleep(1);
}
return 0;
}
Using sigaction (preferred over signal)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void handler(int sig, siginfo_t *info, void *context) {
printf("Signal %d from PID %d\n", sig, info->si_pid);
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handler;
sa.sa_flags = SA_SIGINFO;
sigaction(SIGINT, &sa, NULL);
while (1) {
pause(); // Wait for signal
}
return 0;
}
Inter-Process Communication (IPC)
Pipes
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipefd[2];
pipe(pipefd); // Create pipe
pid_t pid = fork();
if (pid == 0) {
// Child: read from pipe
close(pipefd[1]); // Close write end
char buf[100];
read(pipefd[0], buf, sizeof(buf));
printf("Child received: %s\n", buf);
close(pipefd[0]);
} else {
// Parent: write to pipe
close(pipefd[0]); // Close read end
const char *msg = "Hello from parent";
write(pipefd[1], msg, strlen(msg) + 1);
close(pipefd[1]);
wait(NULL);
}
return 0;
}
Named Pipes (FIFOs)
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
// Writer process
int main() {
mkfifo("/tmp/myfifo", 0666);
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 11);
close(fd);
return 0;
}
// Reader process (separate program)
/*
int main() {
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[100];
read(fd, buf, sizeof(buf));
printf("Received: %s\n", buf);
close(fd);
return 0;
}
*/
Shared Memory
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
// Create shared memory object
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
// Map to process address space
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// Write to shared memory
strcpy(ptr, "Hello from shared memory");
// Another process can open "/myshm" and read the data
munmap(ptr, 4096);
close(fd);
shm_unlink("/myshm");
return 0;
}
File System Operations
Directory Operations
#include <stdio.h>
#include <dirent.h>
int main() {
DIR *dir = opendir(".");
if (!dir) {
perror("opendir");
return 1;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
printf("%s\n", entry->d_name);
}
closedir(dir);
return 0;
}
File Metadata (stat)
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
int main() {
struct stat st;
if (stat("test.txt", &st) < 0) {
perror("stat");
return 1;
}
printf("File size: %ld bytes\n", st.st_size);
printf("Permissions: %o\n", st.st_mode & 0777);
printf("Last modified: %s", ctime(&st.st_mtime));
if (S_ISREG(st.st_mode)) {
printf("Regular file\n");
} else if (S_ISDIR(st.st_mode)) {
printf("Directory\n");
}
return 0;
}
Shell Scripting (Bash)
Basic Shell Script
hello.sh:
#!/bin/bash
# This is a comment
echo "Hello, Shell!"
echo "Current directory: $(pwd)"
echo "Current user: $USER"
Run with:
chmod +x hello.sh
./hello.sh
Variables
#!/bin/bash
# Variable assignment (no spaces around =)
name="John"
age=25
echo "Name: $name"
echo "Age: $age"
# Command substitution
current_date=$(date)
echo "Date: $current_date"
# Arithmetic
result=$((5 + 3))
echo "5 + 3 = $result"
Conditionals
#!/bin/bash
num=10
if [ $num -gt 5 ]; then
echo "Greater than 5"
elif [ $num -eq 5 ]; then
echo "Equal to 5"
else
echo "Less than 5"
fi
# File tests
if [ -f "test.txt" ]; then
echo "test.txt exists"
fi
if [ -d "mydir" ]; then
echo "mydir is a directory"
fi
# String comparison
str="hello"
if [ "$str" = "hello" ]; then
echo "Strings match"
fi
Loops
#!/bin/bash
# For loop
for i in 1 2 3 4 5; do
echo "Number: $i"
done
# For loop with range
for i in {1..10}; do
echo $i
done
# For loop over files
for file in *.txt; do
echo "Processing: $file"
done
# While loop
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
Functions
#!/bin/bash
greet() {
echo "Hello, $1!"
}
add() {
local result=$(($1 + $2))
echo $result
}
greet "Alice"
sum=$(add 5 10)
echo "Sum: $sum"
Command-Line Arguments
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
# Check if argument provided
if [ $# -eq 0 ]; then
echo "Usage: $0 <name>"
exit 1
fi
echo "Hello, $1!"
Practical Shell Scripts
Backup script:
#!/bin/bash
SOURCE="/home/user/documents"
DEST="/backup"
DATE=$(date +%Y%m%d)
BACKUP_FILE="$DEST/backup-$DATE.tar.gz"
echo "Creating backup: $BACKUP_FILE"
tar -czf "$BACKUP_FILE" "$SOURCE"
if [ $? -eq 0 ]; then
echo "Backup successful"
else
echo "Backup failed"
exit 1
fi
# Remove backups older than 7 days
find "$DEST" -name "backup-*.tar.gz" -mtime +7 -delete
System monitoring:
#!/bin/bash
LOG_FILE="/var/log/system_monitor.log"
while true; do
DATE=$(date)
CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}')
MEM=$(free -m | grep Mem | awk '{print $3}')
echo "[$DATE] CPU: $CPU%, Memory: ${MEM}MB" >> "$LOG_FILE"
sleep 60
done
Process killer:
#!/bin/bash
if [ $# -eq 0 ]; then
echo "Usage: $0 <process_name>"
exit 1
fi
PROCESS=$1
PID=$(pgrep -f "$PROCESS")
if [ -z "$PID" ]; then
echo "Process '$PROCESS' not found"
exit 1
fi
echo "Killing process $PROCESS (PID: $PID)"
kill -9 $PID
if [ $? -eq 0 ]; then
echo "Process killed successfully"
else
echo "Failed to kill process"
exit 1
fi
Advanced Bash Features
Arrays
#!/bin/bash
# Declare array
fruits=("apple" "banana" "cherry")
# Access elements
echo "First fruit: ${fruits[0]}"
echo "All fruits: ${fruits[@]}"
echo "Number of fruits: ${#fruits[@]}"
# Add element
fruits+=("date")
# Loop through array
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
Error Handling
#!/bin/bash
set -e # Exit on error
set -u # Exit on undefined variable
set -o pipefail # Pipeline fails if any command fails
# Custom error handling
error_exit() {
echo "Error: $1" >&2
exit 1
}
cp file1.txt file2.txt || error_exit "Failed to copy file"
Input/Output Redirection
# Redirect stdout to file
echo "Hello" > output.txt
# Append to file
echo "World" >> output.txt
# Redirect stderr
command 2> errors.txt
# Redirect both stdout and stderr
command > output.txt 2>&1
# Redirect to null (discard)
command > /dev/null 2>&1
# Here document
cat << EOF
This is a
multi-line
string
EOF
System Programming with C
Process Information
#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
int main() {
printf("PID: %d\n", getpid());
printf("Parent PID: %d\n", getppid());
printf("User ID: %d\n", getuid());
printf("Group ID: %d\n", getgid());
// Get resource limits
struct rlimit limit;
getrlimit(RLIMIT_NOFILE, &limit);
printf("Max open files: %ld\n", limit.rlim_cur);
return 0;
}
Environment Variables
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main() {
// Get specific variable
char *path = getenv("PATH");
printf("PATH: %s\n", path);
// Set variable
setenv("MY_VAR", "Hello", 1);
printf("MY_VAR: %s\n", getenv("MY_VAR"));
// Print all environment variables
for (char **env = environ; *env != NULL; env++) {
printf("%s\n", *env);
}
return 0;
}
Working with /proc
#include <stdio.h>
#include <stdlib.h>
int main() {
// Read process status
char path[256];
snprintf(path, sizeof(path), "/proc/%d/status", getpid());
FILE *f = fopen(path, "r");
if (!f) {
perror("fopen");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), f)) {
printf("%s", line);
}
fclose(f);
return 0;
}
Key Concepts
- System calls are the kernel interface for user programs
- fork() creates child processes
- exec() replaces process image
- Signals are asynchronous notifications
- IPC mechanisms: pipes, FIFOs, shared memory, sockets
- Shell scripts automate tasks using bash
- File descriptors 0, 1, 2 are stdin, stdout, stderr
- /proc filesystem provides process information
Common Mistakes
- Not checking return values - System calls can fail
- Zombie processes - Forgetting to wait() for children
- Resource leaks - Not closing file descriptors
- Race conditions - Signals can interrupt execution
- Quoting in bash - Not quoting variables with spaces
- Assuming success - Commands can fail silently
- Hardcoded paths - Use variables for portability
Debugging Tips
- Use strace - Trace system calls:
strace ./program - Check errno -
perror()explains errors - Use bash -x - Debug shell scripts:
bash -x script.sh - Read man pages -
man 2 fork,man 3 printf - ShellCheck - Lint tool for bash scripts
- GDB for C - Debug system programs
- Check logs -
/var/logfor system issues
Mini Exercises
- Write program that forks and executes
ls - Create pipe between parent and child processes
- Implement signal handler for SIGINT and SIGTERM
- Write shell script to backup directory
- Create shared memory between two processes
- Write script to monitor disk usage
- Implement simple shell (read commands, execute)
- Create daemon process
- Write script to rotate log files
- Implement process pool with fork
Review Questions
- What's the difference between fork() and exec()?
- What are the standard file descriptors (0, 1, 2)?
- How do pipes work?
- What happens when you don't wait() for child processes?
- How do you redirect stderr in bash?
Reference Checklist
By the end of this chapter, you should be able to:
- Use Linux system calls (open, read, write, close)
- Create and manage processes with fork() and exec()
- Handle signals in C programs
- Use IPC mechanisms (pipes, shared memory)
- Work with file system APIs
- Write bash scripts with variables and loops
- Handle errors in shell scripts
- Use command-line arguments in scripts
- Understand process management
- Debug system programs with strace
Next Steps
With Linux system programming skills, the next chapter explores Linux kernel modules. You'll learn how to write kernel code that runs as loadable modules, interact with kernel APIs, and extend Linux kernel functionality.
Key Takeaway: Linux provides powerful system programming APIs through system calls. Combined with shell scripting, you can build robust system tools, automate tasks, and create complex applications that interact deeply with the operating system.