System-Level Programming
Introduction
System-level programming in C involves interacting directly with the operating system to perform tasks such as processing command-line arguments, accessing environment variables, managing process execution, and handling system signals. These capabilities allow C programs to integrate seamlessly with the operating system environment and provide more sophisticated functionality than basic file I/O operations.
Understanding system-level programming concepts is essential for developing robust applications that can adapt to different environments, handle errors gracefully, and interact with system resources effectively.
Command-Line Arguments
Command-line arguments allow programs to receive input directly from the command line when they are executed. This is a fundamental way for programs to be configurable and flexible.
argc and argv
The main() function can accept two parameters for processing command-line arguments:
int main(int argc, char *argv[]);Parameters: - argc: Argument count (number of command-line arguments) - argv: Argument vector (array of string pointers to the arguments)
Examples
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
printf("Program name: %s\n", argv[0]);
printf("Number of arguments: %d\n", argc - 1);
// Print all arguments
for (int i = 1; i < argc; i++) {
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}Practical Command-Line Processor
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void print_usage(const char *program_name) {
printf("Usage: %s [options] <input_file> <output_file>\n", program_name);
printf("Options:\n");
printf(" -v, --verbose Verbose output\n");
printf(" -h, --help Show this help message\n");
printf(" -c, --copy Copy mode (default)\n");
printf(" -m, --move Move mode\n");
}
int main(int argc, char *argv[]) {
int verbose = 0;
int move_mode = 0;
const char *input_file = NULL;
const char *output_file = NULL;
// Process command-line arguments
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
verbose = 1;
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]);
exit(EXIT_SUCCESS);
} else if (strcmp(argv[i], "-m") == 0 || strcmp(argv[i], "--move") == 0) {
move_mode = 1;
} else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--copy") == 0) {
move_mode = 0;
} else if (input_file == NULL) {
input_file = argv[i];
} else if (output_file == NULL) {
output_file = argv[i];
} else {
fprintf(stderr, "Error: Too many arguments\n");
print_usage(argv[0]);
exit(EXIT_FAILURE);
}
}
// Validate required arguments
if (input_file == NULL || output_file == NULL) {
fprintf(stderr, "Error: Input and output files required\n");
print_usage(argv[0]);
exit(EXIT_FAILURE);
}
if (verbose) {
printf("Mode: %s\n", move_mode ? "Move" : "Copy");
printf("Input file: %s\n", input_file);
printf("Output file: %s\n", output_file);
}
// Perform the operation (simplified)
if (move_mode) {
if (verbose) printf("Moving file...\n");
// In a real implementation, you would use rename()
} else {
if (verbose) printf("Copying file...\n");
// In a real implementation, you would copy the file contents
}
printf("Operation completed successfully\n");
return 0;
}Environment Variables
Environment variables provide a way for programs to access system configuration information and user preferences. They are key-value pairs that are inherited from the parent process and can be accessed by C programs.
getenv()
Retrieves the value of an environment variable:
char *getenv(const char *name);Return Value: - Pointer to the value string on success - NULL if the variable is not found
Examples
#include <stdio.h>
#include <stdlib.h>
int main() {
char *home_dir, *path, *user, *shell;
// Get common environment variables
home_dir = getenv("HOME");
path = getenv("PATH");
user = getenv("USER");
shell = getenv("SHELL");
printf("Home directory: %s\n", home_dir ? home_dir : "Not set");
printf("User: %s\n", user ? user : "Not set");
printf("Shell: %s\n", shell ? shell : "Not set");
printf("Path: %s\n", path ? path : "Not set");
// Check for custom environment variable
char *debug = getenv("DEBUG");
if (debug != NULL && strcmp(debug, "1") == 0) {
printf("Debug mode enabled\n");
}
return 0;
}Setting Environment Variables
#include <stdio.h>
#include <stdlib.h>
int main() {
// Set an environment variable
if (setenv("MY_VARIABLE", "Hello, World!", 1) != 0) {
perror("Error setting environment variable");
exit(EXIT_FAILURE);
}
// Retrieve and display the variable
char *value = getenv("MY_VARIABLE");
printf("MY_VARIABLE = %s\n", value ? value : "Not set");
// Unset the environment variable
unsetenv("MY_VARIABLE");
// Try to retrieve it again
value = getenv("MY_VARIABLE");
printf("MY_VARIABLE after unset = %s\n", value ? value : "Not set");
return 0;
}Process Management
Process management functions allow programs to control their execution flow, terminate gracefully, and manage exit status codes.
exit()
Terminates the program with a specified exit status:
void exit(int status);atexit()
Registers functions to be called when the program exits:
int atexit(void (*func)(void));Examples
#include <stdio.h>
#include <stdlib.h>
void cleanup_handler1(void) {
printf("Cleanup handler 1 called\n");
}
void cleanup_handler2(void) {
printf("Cleanup handler 2 called\n");
}
void cleanup_handler3(void) {
printf("Cleanup handler 3 called\n");
}
int main() {
// Register cleanup handlers
if (atexit(cleanup_handler1) != 0) {
fprintf(stderr, "Failed to register cleanup handler 1\n");
exit(EXIT_FAILURE);
}
if (atexit(cleanup_handler2) != 0) {
fprintf(stderr, "Failed to register cleanup handler 2\n");
exit(EXIT_FAILURE);
}
if (atexit(cleanup_handler3) != 0) {
fprintf(stderr, "Failed to register cleanup handler 3\n");
exit(EXIT_FAILURE);
}
printf("Main function executing\n");
// Exit with success status
exit(EXIT_SUCCESS);
// This code will never be reached
printf("This won't be printed\n");
}Process Exit Status
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <number>\n", argv[0]);
exit(EXIT_FAILURE); // Exit with failure status
}
int number = atoi(argv[1]);
if (number < 0) {
fprintf(stderr, "Error: Negative numbers not allowed\n");
exit(EXIT_FAILURE); // Exit with failure status
}
if (number == 0) {
printf("Zero is neither positive nor negative\n");
exit(EXIT_SUCCESS); // Exit with success status
}
printf("Number %d is positive\n", number);
// Normal exit (equivalent to exit(EXIT_SUCCESS))
return 0;
}Signal Handling
Signals are software interrupts that allow the operating system to notify a process of various events. Signal handling enables programs to respond gracefully to events like user interrupts, termination requests, and other system events.
signal()
Sets up a signal handler:
#include <signal.h>
void (*signal(int sig, void (*handler)(int)))(int);Common Signals
| Signal | Description | Default Action |
|---|---|---|
SIGINT |
Interrupt (Ctrl+C) | Terminate process |
SIGTERM |
Termination request | Terminate process |
SIGKILL |
Kill signal | Terminate process (cannot be caught) |
SIGSEGV |
Segmentation fault | Terminate process |
SIGUSR1 |
User-defined signal 1 | Terminate process |
SIGUSR2 |
User-defined signal 2 | Terminate process |
Examples
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t signal_received = 0;
void signal_handler(int sig) {
printf("\nReceived signal %d\n", sig);
signal_received = 1;
}
int main() {
// Set up signal handlers
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("Error setting SIGINT handler");
exit(EXIT_FAILURE);
}
if (signal(SIGTERM, signal_handler) == SIG_ERR) {
perror("Error setting SIGTERM handler");
exit(EXIT_FAILURE);
}
printf("Program running. Press Ctrl+C to interrupt.\n");
// Main loop
while (!signal_received) {
printf("Working...\n");
sleep(2);
}
printf("Cleaning up and exiting gracefully...\n");
// Perform cleanup operations here
// Close files, free memory, etc.
exit(EXIT_SUCCESS);
}Advanced Signal Handling
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
volatile sig_atomic_t sigint_count = 0;
volatile sig_atomic_t sigterm_received = 0;
void sigint_handler(int sig) {
sigint_count++;
printf("\nSIGINT received (%d times)\n", sigint_count);
if (sigint_count >= 3) {
printf("Three interrupts received. Exiting...\n");
exit(EXIT_SUCCESS);
}
}
void sigterm_handler(int sig) {
printf("\nSIGTERM received. Cleaning up...\n");
sigterm_received = 1;
}
void sigsegv_handler(int sig) {
printf("\nSegmentation fault detected! Saving emergency data...\n");
// Save critical data before terminating
exit(EXIT_FAILURE);
}
int main() {
// Set up signal handlers
struct sigaction sa_int, sa_term, sa_segv;
// SIGINT handler
memset(&sa_int, 0, sizeof(sa_int));
sa_int.sa_handler = sigint_handler;
sigemptyset(&sa_int.sa_mask);
sa_int.sa_flags = 0;
sigaction(SIGINT, &sa_int, NULL);
// SIGTERM handler
memset(&sa_term, 0, sizeof(sa_term));
sa_term.sa_handler = sigterm_handler;
sigemptyset(&sa_term.sa_mask);
sa_term.sa_flags = 0;
sigaction(SIGTERM, &sa_term, NULL);
// SIGSEGV handler
memset(&sa_segv, 0, sizeof(sa_segv));
sa_segv.sa_handler = sigsegv_handler;
sigemptyset(&sa_segv.sa_mask);
sa_segv.sa_flags = SA_RESETHAND; // Only handle once
sigaction(SIGSEGV, &sa_segv, NULL);
printf("Advanced signal handling demo\n");
printf("Press Ctrl+C up to 3 times, or send SIGTERM to terminate\n");
// Main loop
while (!sigterm_received) {
printf("Working... (PID: %d)\n", getpid());
sleep(3);
}
printf("Program terminating normally\n");
return 0;
}System Calls and Process Creation
System calls provide direct access to operating system services. While C’s standard library provides higher-level interfaces, system calls offer more control and are sometimes necessary for specific tasks.
system()
Executes a command through the shell:
int system(const char *command);Examples
#include <stdio.h>
#include <stdlib.h>
int main() {
int result;
// Execute system commands
printf("Listing current directory:\n");
result = system("ls -la");
if (result == -1) {
perror("Error executing command");
}
printf("\nCurrent date and time:\n");
result = system("date");
if (result == -1) {
perror("Error executing command");
}
// Conditional execution based on system command result
printf("\nChecking if 'git' is available:\n");
result = system("which git > /dev/null 2>&1");
if (result == 0) {
printf("Git is available\n");
system("git --version");
} else {
printf("Git is not available\n");
}
return 0;
}Practical Examples
Configuration File Processor
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char database_host[100];
int database_port;
char log_level[20];
int max_connections;
} Config;
int load_config(const char *filename, Config *config) {
FILE *fp;
char line[256];
char key[100], value[100];
// Set default values
strcpy(config->database_host, "localhost");
config->database_port = 5432;
strcpy(config->log_level, "INFO");
config->max_connections = 100;
fp = fopen(filename, "r");
if (fp == NULL) {
// Try to load from environment variables
char *env_host = getenv("DB_HOST");
char *env_port = getenv("DB_PORT");
char *env_log = getenv("LOG_LEVEL");
char *env_max = getenv("MAX_CONN");
if (env_host) strcpy(config->database_host, env_host);
if (env_port) config->database_port = atoi(env_port);
if (env_log) strcpy(config->log_level, env_log);
if (env_max) config->max_connections = atoi(env_max);
return 0; // Success with environment variables
}
// Parse configuration file
while (fgets(line, sizeof(line), fp) != NULL) {
// Skip comments and empty lines
if (line[0] == '#' || line[0] == '\n') continue;
// Parse key=value pairs
if (sscanf(line, "%[^=]=%s", key, value) == 2) {
if (strcmp(key, "database_host") == 0) {
strcpy(config->database_host, value);
} else if (strcmp(key, "database_port") == 0) {
config->database_port = atoi(value);
} else if (strcmp(key, "log_level") == 0) {
strcpy(config->log_level, value);
} else if (strcmp(key, "max_connections") == 0) {
config->max_connections = atoi(value);
}
}
}
fclose(fp);
return 0;
}
void print_config(const Config *config) {
printf("Configuration:\n");
printf(" Database Host: %s\n", config->database_host);
printf(" Database Port: %d\n", config->database_port);
printf(" Log Level: %s\n", config->log_level);
printf(" Max Connections: %d\n", config->max_connections);
}
int main(int argc, char *argv[]) {
Config config;
const char *config_file = "app.conf";
// Check for command-line configuration file
if (argc > 1) {
config_file = argv[1];
}
// Load configuration
if (load_config(config_file, &config) != 0) {
fprintf(stderr, "Error loading configuration\n");
exit(EXIT_FAILURE);
}
// Print configuration
print_config(&config);
// Use configuration in application
printf("\nApplication starting with above configuration...\n");
return 0;
}Process Monitor
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
volatile sig_atomic_t terminate = 0;
void signal_handler(int sig) {
printf("\nTermination signal received\n");
terminate = 1;
}
int main(int argc, char *argv[]) {
pid_t pid;
int status;
if (argc != 2) {
fprintf(stderr, "Usage: %s <command>\n", argv[0]);
exit(EXIT_FAILURE);
}
// Set up signal handlers
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
printf("Process monitor started\n");
printf("Monitoring command: %s\n", argv[1]);
while (!terminate) {
// Fork a child process
pid = fork();
if (pid == -1) {
perror("Error forking process");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// Child process - execute the command
execl("/bin/sh", "sh", "-c", argv[1], (char *)NULL);
perror("Error executing command");
exit(EXIT_FAILURE);
} else {
// Parent process - wait for child
printf("Started process with PID %d\n", pid);
// Wait for child to complete
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Process exited with status %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Process terminated by signal %d\n", WTERMSIG(status));
}
// Wait before restarting (unless terminating)
if (!terminate) {
printf("Restarting in 5 seconds...\n");
sleep(5);
}
}
}
printf("Process monitor terminated\n");
return 0;
}Summary
System-level programming in C provides powerful capabilities for interacting with the operating system:
- Command-Line Arguments: Processing
argcandargvfor flexible program configuration - Environment Variables: Accessing system configuration through
getenv(),setenv(), andunsetenv() - Process Management: Controlling program termination with
exit()and cleanup withatexit() - Signal Handling: Responding to system events and interrupts gracefully
- System Calls: Executing shell commands and interacting with the operating system
These system-level programming concepts are essential for developing robust, production-ready C applications that can adapt to different environments and handle various runtime conditions effectively.