Unix, Linux, and Shell Scripting

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

  1. Practice on Linux - Use a Linux VM or WSL if needed
  2. Read man pages - They're the authoritative documentation
  3. Write scripts - Automate your own workflows
  4. Study existing code - Read shell scripts and C programs
  5. 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 file
  • read() - Read from file descriptor
  • write() - Write to file descriptor
  • close() - Close file descriptor
  • lseek() - Move file offset

Process Management:

  • fork() - Create child process
  • exec() - Replace process image
  • wait() - Wait for child process
  • exit() - Terminate process
  • getpid() - Get process ID

Memory:

  • mmap() - Map memory
  • munmap() - Unmap memory
  • brk()/sbrk() - Change heap size

Signals:

  • kill() - Send signal
  • signal()/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

  1. Not checking return values - System calls can fail
  2. Zombie processes - Forgetting to wait() for children
  3. Resource leaks - Not closing file descriptors
  4. Race conditions - Signals can interrupt execution
  5. Quoting in bash - Not quoting variables with spaces
  6. Assuming success - Commands can fail silently
  7. 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/log for system issues

Mini Exercises

  1. Write program that forks and executes ls
  2. Create pipe between parent and child processes
  3. Implement signal handler for SIGINT and SIGTERM
  4. Write shell script to backup directory
  5. Create shared memory between two processes
  6. Write script to monitor disk usage
  7. Implement simple shell (read commands, execute)
  8. Create daemon process
  9. Write script to rotate log files
  10. Implement process pool with fork

Review Questions

  1. What's the difference between fork() and exec()?
  2. What are the standard file descriptors (0, 1, 2)?
  3. How do pipes work?
  4. What happens when you don't wait() for child processes?
  5. 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.