NANDHOO.

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"Currentuser:(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:name" echo "Age: age"


Command substitution

current_date=(date)echo"Date:(date) echo "Date: current_date"


Arithmetic

result=((5+3))echo"5+3=((5 + 3)) echo "5 + 3 = result"


Conditionals


#!/bin/bash

num=10


if [ numgt5];thenecho"Greaterthan5"elif[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 [ countle5];doecho"Count: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))echo2)) echo result }


greet "Alice" sum=(add510)echo"Sum:(add 5 10) echo "Sum: sum"


Command-Line Arguments


#!/bin/bash

echo "Script name: 0"echo"Firstargument:0" echo "First argument: 1" echo "Second argument: 2"echo"Allarguments:2" echo "All arguments: @" echo "Number of arguments: $#"


Check if argument provided

if [ # -eq 0 ]; then echo "Usage: 0 " exit 1 fi


echo "Hello, $1!"


Practical Shell Scripts


Backup script:

#!/bin/bash

SOURCE="/home/user/documents" DEST="/backup" DATE=(date+BACKUPFILE="(date +%Y%m%d) BACKUP_FILE="DEST/backup-$DATE.tar.gz"


echo "Creating backup: BACKUPFILE"tarczf"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=(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=1PID=1 PID=(pgrep -f "$PROCESS")


if [ -z "PID"];thenecho"ProcessPID" ]; then echo "Process 'PROCESS' not found" exit 1 fi


echo "Killing process PROCESS(PID: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"Allfruits:{fruits[0]}" echo "All fruits: {fruits[@]}" echo "Number of fruits: ${#fruits[@]}"


Add element

fruits+=("date")


Loop through array

for fruit in "fruits[@]";doecho"Fruit:{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.