C System Programming and Signals
50 minC system programming enables direct interaction with the operating system through system calls and library functions. System programming includes process management, signal handling, file system operations, and inter-process communication. Understanding system programming enables building system-level applications, daemons, and utilities. System programming requires understanding the operating system's interface and behavior.
Signals are software interrupts that notify processes of events. Common signals include SIGINT (interrupt, typically Ctrl+C), SIGTERM (termination request), SIGKILL (forceful termination), SIGUSR1/SIGUSR2 (user-defined), and SIGALRM (alarm). Signal handlers are functions that execute when signals are received. Understanding signals enables graceful program termination and event handling.
Process management includes creating processes (fork()), executing programs (exec family), waiting for processes (wait(), waitpid()), and terminating processes (exit(), kill()). fork() creates a copy of the current process, exec() replaces the process image, and wait() waits for child processes to complete. Understanding process management enables multi-process applications.
Inter-process communication (IPC) enables processes to share data and synchronize. IPC mechanisms include pipes (unidirectional data flow), named pipes/FIFOs (persistent pipes), shared memory (shared memory segments), message queues (message passing), and semaphores (synchronization). Understanding IPC enables building multi-process applications.
File system operations include low-level file I/O (open(), read(), write(), close()), directory operations (opendir(), readdir()), and file metadata (stat()). Low-level I/O provides more control than stream I/O but requires more manual management. Understanding file system operations enables system-level file manipulation.
System programming considerations include error handling (checking return values and errno), signal safety (avoiding non-reentrant functions in signal handlers), and portability (different systems have different interfaces). Understanding these considerations enables reliable system programming.
Key Concepts
- System calls provide low-level OS access.
- Signals handle asynchronous events and process communication.
- Process management includes creation, execution, and synchronization.
- Inter-process communication enables data sharing.
- System programming requires careful error handling.
Learning Objectives
Master
- Handling signals with signal handlers
- Creating and managing processes
- Understanding inter-process communication
- Using system calls for file operations
Develop
- Understanding operating system interfaces
- Building system-level applications
- Implementing multi-process programs
Tips
- Register signal handlers: signal(SIGINT, handler) or sigaction().
- Use fork() to create processes: pid_t pid = fork();
- Wait for children: waitpid(pid, &status, 0) to wait for child process.
- Check system call returns: if (result == -1) check errno.
Common Pitfalls
- Not handling signals, causing abrupt program termination.
- Creating zombie processes, not waiting for children.
- Using non-reentrant functions in signal handlers, causing undefined behavior.
- Not checking system call returns, missing errors.
Summary
- System programming enables direct OS interaction.
- Signals enable event handling and graceful termination.
- Process management enables multi-process applications.
- Understanding system programming enables system-level development.
Exercise
Create a program demonstrating system programming concepts and signal handling.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
// Global variables for signal handling
volatile sig_atomic_t running = 1;
volatile sig_atomic_t signal_count = 0;
// Signal handler function
void signal_handler(int sig) {
signal_count++;
switch (sig) {
case SIGINT:
printf("\nReceived SIGINT (Ctrl+C) - Signal count: %d\n", signal_count);
if (signal_count >= 3) {
printf("Exiting after 3 SIGINT signals\n");
running = 0;
}
break;
case SIGUSR1:
printf("Received SIGUSR1 signal\n");
break;
case SIGUSR2:
printf("Received SIGUSR2 signal\n");
break;
case SIGALRM:
printf("Received SIGALRM signal\n");
break;
default:
printf("Received signal %d\n", sig);
break;
}
}
// Process creation and management
int createChildProcess(const char *command, char *const args[]) {
pid_t pid = fork();
if (pid == -1) {
perror("Fork failed");
return -1;
} else if (pid == 0) {
// Child process
printf("Child process started (PID: %d)\n", getpid());
printf("Executing: %s\n", command);
// Execute command
if (execvp(command, args) == -1) {
perror("Exec failed");
exit(EXIT_FAILURE);
}
} else {
// Parent process
printf("Parent process (PID: %d) created child (PID: %d)\n",
getpid(), pid);
// Wait for child to complete
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child process exited with status: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child process killed by signal: %d\n", WTERMSIG(status));
}
}
return pid;
}
// Inter-process communication using pipes
int communicateWithPipe() {
int pipefd[2];
pid_t pid;
char buffer[256];
if (pipe(pipefd) == -1) {
perror("Pipe creation failed");
return 0;
}
pid = fork();
if (pid == -1) {
perror("Fork failed");
return 0;
}
if (pid == 0) {
// Child process - write to pipe
close(pipefd[0]); // Close read end
printf("Child writing to pipe...\n");
const char *message = "Hello from child process!";
write(pipefd[1], message, strlen(message) + 1);
close(pipefd[1]);
exit(EXIT_SUCCESS);
} else {
// Parent process - read from pipe
close(pipefd[1]); // Close write end
printf("Parent reading from pipe...\n");
ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer));
if (bytes_read > 0) {
printf("Received: %s\n", buffer);
}
close(pipefd[0]);
wait(NULL); // Wait for child
}
return 1;
}
// File descriptor manipulation
int demonstrateFileDescriptors() {
// Open a file
int fd = open("temp_file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Error opening file");
return 0;
}
// Write to file
const char *data = "This is test data written using file descriptors.\n";
ssize_t bytes_written = write(fd, data, strlen(data));
printf("Written %zd bytes to file\n", bytes_written);
// Close file
close(fd);
// Read from file
fd = open("temp_file.txt", O_RDONLY);
if (fd == -1) {
perror("Error opening file for reading");
return 0;
}
char buffer[256];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Read from file: %s", buffer);
}
close(fd);
// Clean up
unlink("temp_file.txt");
return 1;
}
// Signal sending between processes
int sendSignalsToProcess(pid_t target_pid) {
printf("Sending signals to process %d...\n", target_pid);
// Send SIGUSR1
if (kill(target_pid, SIGUSR1) == -1) {
perror("Error sending SIGUSR1");
return 0;
}
printf("Sent SIGUSR1\n");
sleep(1);
// Send SIGUSR2
if (kill(target_pid, SIGUSR2) == -1) {
perror("Error sending SIGUSR2");
return 0;
}
printf("Sent SIGUSR2\n");
return 1;
}
int main() {
printf("=== System Programming and Signals Demo ===\n\n");
// Set up signal handlers
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("Error setting SIGINT handler");
return 1;
}
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("Error setting SIGUSR1 handler");
return 1;
}
if (sigaction(SIGUSR2, &sa, NULL) == -1) {
perror("Error setting SIGUSR2 handler");
return 1;
}
if (sigaction(SIGALRM, &sa, NULL) == -1) {
perror("Error setting SIGALRM handler");
return 1;
}
printf("Signal handlers set up. PID: %d\n", getpid());
printf("Press Ctrl+C to test signal handling (3 times to exit)\n\n");
// Demonstrate file descriptors
printf("1. File Descriptor Operations:\n");
demonstrateFileDescriptors();
printf("\n");
// Demonstrate inter-process communication
printf("2. Inter-Process Communication with Pipes:\n");
communicateWithPipe();
printf("\n");
// Demonstrate process creation
printf("3. Process Creation and Management:\n");
char *args[] = {"echo", "Hello from child process!", NULL};
createChildProcess("echo", args);
printf("\n");
// Set up alarm
printf("4. Setting up alarm (5 seconds)...\n");
alarm(5);
// Main loop
printf("5. Main loop running...\n");
while (running) {
printf("Main process running... (Press Ctrl+C to test signals)\n");
sleep(2);
}
printf("\nProgram terminated normally.\n");
return 0;
}
Exercise Tips
- Use sigaction() instead of signal(): more reliable and portable.
- Wait for child processes: waitpid() to prevent zombie processes.
- Close unused pipe ends: close() unused file descriptors in pipes.
- Use volatile for signal handler variables: volatile sig_atomic_t for signal-safe variables.