Lesson 21-C System Programming

System programming is one of the core application areas of the C language, involving interaction with the operating system, management of hardware resources, and implementation of process/thread collaboration. This article covers key topics such as file I/O, process management, signal handling, memory management, inter-process communication (IPC), thread management, system calls, error handling, multiplexing, asynchronous I/O, and file locking, with code examples and principle analysis to help developers master the core skills of system programming.

File I/O: The Bridge Between the Operating System and Storage

File I/O (input/output) is the foundation for programs to interact with external storage (such as hard drives, SSDs). The C language implements file read and write operations through system calls.

Core System Calls for File I/O

System CallFunction Description
open()Opens/creates a file, returns a file descriptor (int fd).
close()Closes the file descriptor, releasing resources.
read()Reads data from the file descriptor into a buffer.
write()Writes data from a buffer to the file descriptor.
lseek()Adjusts the file pointer position (random access).
fstat()Retrieves file metadata (size, permissions, etc.).
unlink()Deletes a file (effective only when no process has it open).

File Opening and Closing (open/close)

open Function Prototype:

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int open(const char* pathname, int flags, mode_t mode);

• Parameter Explanation:

pathname: File path (absolute/relative path).
flags: Open mode (required), common combinations:
O_RDONLY: Read-only.
O_WRONLY: Write-only.
O_RDWR: Read-write.
O_CREAT: Create if the file does not exist (requires mode).
O_TRUNC: Truncate the file to 0 length if it exists.
O_APPEND: Append mode (write to the end of the file).

mode: File permissions (effective only with O_CREAT), such as 0644 (user read/write, group read, others read).

Example: Open a File and Write Data

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    // Open file (create if not exists, truncate if exists, write-only mode)
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open failed");
        return 1;
    }

    // Write data
    const char* data = "Hello, File I/O!\n";
    ssize_t written = write(fd, data, strlen(data));
    if (written == -1) {
        perror("write failed");
        close(fd);
        return 1;
    }
    printf("Written %zd bytes\n", written);

    // Close file
    close(fd);
    return 0;
}

File Reading/Writing and Random Access (read/write/lseek)

read Function Prototype:

ssize_t read(int fd, void* buf, size_t count);

write Function Prototype:

ssize_t write(int fd, const void* buf, size_t count);

• Return Value: Returns the number of bytes read/written on success; returns -1 on failure (error code stored in errno).

Example: Read File Content and Random Jump

int main() {
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) { perror("open failed"); return 1; }

    // Read first 10 bytes
    char buf[10];
    ssize_t read_bytes = read(fd, buf, sizeof(buf)-1);  // Leave 1 byte for '\0'
    if (read_bytes == -1) { perror("read failed"); close(fd); return 1; }
    buf[read_bytes] = '\0';
    printf("First 10 bytes: %s\n", buf);

    // Jump to 5 bytes before the end of the file
    off_t offset = lseek(fd, -5, SEEK_END);
    if (offset == (off_t)-1) { perror("lseek failed"); close(fd); return 1; }

    // Read last 5 bytes
    read_bytes = read(fd, buf, sizeof(buf)-1);
    buf[read_bytes] = '\0';
    printf("Last 5 bytes: %s\n", buf);

    close(fd);
    return 0;
}

File Descriptor

• Essence: A non-negative integer identifier assigned by the kernel to each open file (e.g., 0 standard input, 1 standard output, 2 standard error).
• Management:
dup()/dup2(): Duplicate file descriptor (e.g., redirect standard output to a file).
close(): Close the descriptor (kernel releases resources).
ulimit -n: View/modify the maximum number of file descriptors per process (default 1024).

Example: Redirect Standard Output to a File

int main() {
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open failed"); return 1; }

    // Duplicate standard output (fd=1) to file descriptor fd
    if (dup2(fd, 1) == -1) { perror("dup2 failed"); close(fd); return 1; }
    close(fd);  // Original descriptor can be closed, standard output is redirected

    printf("This content will be written to output.txt\n");  // Output to file instead of terminal
    return 0;
}

Process Management: The Operating System’s “Task Scheduler”

A process is the basic unit of resource allocation in the operating system. System programming requires mastering process creation, termination, waiting, and state management.

Process Creation (fork)

fork() is the system call to create a new process. The child process is a copy of the parent process (memory image), returning 0 (child process) or the child process PID (parent process).

Function Prototype:

#include <unistd.h>

pid_t fork(void);

Example: Parent and Child Processes Print Information

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    }

    if (pid == 0) {  // Child process
        printf("Child process PID: %d, Parent process PID: %d\n", getpid(), getppid());
    } else {  // Parent process
        printf("Parent process PID: %d, Child process PID: %d\n", getpid(), pid);
        int status;
        waitpid(pid, &status, 0);  // Wait for child process to terminate
        printf("Child process exit status: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

Process Termination (exit/_exit)

exit(int status): Standard library function, terminates the process and performs cleanup (e.g., flushes I/O buffers).
_exit(int status): System call, terminates the process directly (no cleanup).

Example: Child Process Abnormal Termination

int main() {
    pid_t pid = fork();
    if (pid == 0) {  // Child process
        printf("Child process about to terminate abnormally\n");
        _exit(1);  // Terminate directly, no buffer flush
    } else {
        int status;
        wait(&status);
        printf("Child process exit status: %d (%s)\n", 
               WEXITSTATUS(status), 
               WIFEXITED(status) ? "normal exit" : "abnormal termination");
    }
    return 0;
}

Process Waiting (wait/waitpid)

The parent process uses wait or waitpid to wait for the child process to terminate, avoiding zombie processes.

waitpid Function Prototype:

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int* status, int options);

• Parameter Explanation:
pid: PID of the child process to wait for (-1 means wait for any child process).
status: Stores the child process exit status (parsed with macros).
options: Control options (e.g., WNOHANG for non-blocking wait).

Example: Non-blocking Wait for Child Process

int main() {
    pid_t pid = fork();
    if (pid == 0) {  // Child process
        sleep(2);  // Simulate time-consuming operation
        return 42;
    } else {
        int status;
        while (1) {
            pid_t ret = waitpid(pid, &status, WNOHANG);
            if (ret == -1) {  // Error (e.g., child process already terminated)
                perror("waitpid failed");
                break;
            } else if (ret == 0) {  // Child process not terminated (non-blocking)
                printf("Child process still running...\n");
                sleep(1);
            } else {  // Child process terminated
                printf("Child process exited, status: %d\n", WEXITSTATUS(status));
                break;
            }
        }
    }
    return 0;
}

Signal Handling: The “Interrupt Mechanism” of Processes

Signals are asynchronous notifications sent by the operating system to processes (e.g., pressing Ctrl+C sends SIGINT), used to control process behavior.

Common Signal Types

Signal NameValueDescription
SIGINT2User presses Ctrl+C (interrupt)
SIGKILL9Force terminate process (uncatchable)
SIGSEGV11Segmentation fault (illegal memory access)
SIGTERM15Normal termination signal (catchable)
SIGALRM14Alarm signal (timer triggered)

Signal Handler Registration (signal/sigaction)

signal(): Simple signal handler registration (traditional method, poor portability).
sigaction(): More flexible, supports signal masks and flags (recommended).

sigaction Function Prototype:

#include <signal.h>

int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);

Example: Catch SIGINT Signal

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int sig) {
    printf("\nReceived SIGINT signal (Ctrl+C), preparing to exit...\n");
    _exit(0);  // Terminate process
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);  // Do not block other signals
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction failed");
        return 1;
    }

    printf("Running... Press Ctrl+C to exit\n");
    while (1) {
        sleep(1);  // Simulate main loop
    }
    return 0;
}

Signal Handling Precautions

• Async-Safety: Signal handlers should only call async-safe functions (e.g., write, _exit), avoid calling non-async-safe functions like printf (may cause deadlock).
• Signal Masking: Use sigprocmask to temporarily block signals, preventing interruption during handling.
• Race Conditions: Signals can arrive at any time, use atomic operations or mutexes to protect shared data.

Memory Management: The “Dialogue” Between Program and Memory

Memory management involves allocation, protection, and reclamation of memory during program execution. System programming focuses on virtual memory, physical memory mapping, and kernel-level memory operations.

Virtual Memory and Physical Memory

• Virtual Memory: The memory space from the process’s perspective (e.g., 0x00000000 to 0xFFFFFFFF), mapped to physical memory via the MMU (Memory Management Unit).
• Physical Memory: Actual storage chips, managed uniformly by the operating system.

Memory Mapping (mmap)

mmap maps a file or device into the process’s virtual memory, enabling efficient file I/O or shared memory.

Function Prototype:

#include <sys/mman.h>

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);

• Parameter Explanation:
addr: Suggested starting address for mapping (usually NULL, chosen by the kernel).
length: Mapping length.
prot: Memory protection mode (e.g., PROT_READ, PROT_WRITE).
flags: Mapping type (e.g., MAP_SHARED shared mapping, MAP_PRIVATE private mapping).
fd: File descriptor (-1 for anonymous mapping).
offset: File offset (usually 0).

Example: Shared Memory Communication

#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char* name = "/shared_mem";
    const int SIZE = 4096;

    // Create shared memory object (POSIX shared memory)
    int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    if (fd == -1) { perror("shm_open failed"); return 1; }

    // Adjust shared memory size
    if (ftruncate(fd, SIZE) == -1) { perror("ftruncate failed"); close(fd); return 1; }

    // Map to current process memory
    char* ptr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) { perror("mmap failed"); close(fd); return 1; }
    close(fd);  // Can close file descriptor after mapping

    // Write data
    strcpy(ptr, "Hello, Shared Memory!");

    // Wait for other processes to read (omitted in example)
    sleep(2);

    // Unmap
    munmap(ptr, SIZE);
    shm_unlink(name);  // Delete shared memory object
    return 0;
}

Memory Allocation Hierarchy

• User Space: malloc/free (C library functions) → brk/sbrk (system calls to adjust heap top).
• Kernel Space: kmalloc (kernel dynamic memory allocation) → vmalloc (virtual memory allocation) → Physical page allocation (alloc_pages).

Inter-Process Communication (IPC): The “Collaboration Bridge” Between Processes

IPC is used for data exchange between different processes. Common methods include pipes, message queues, shared memory, semaphores, and sockets.

Pipe

A pipe is a half-duplex communication channel, used for related processes (e.g., parent-child processes).

Function Prototype:

#include <unistd.h>

int pipe(int pipefd[2]);  // pipefd[0] read end, pipefd[1] write end

Example: Parent-Child Process Communication via Pipe

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) { perror("pipe failed"); return 1; }

    pid_t pid = fork();
    if (pid == 0) {  // Child process (read)
        close(pipefd[1]);  // Close write end
        char buf[100];
        ssize_t read_bytes = read(pipefd[0], buf, sizeof(buf));
        printf("Child process received: %s\n", buf);
        close(pipefd[0]);
    } else {  // Parent process (write)
        close(pipefd[0]);  // Close read end
        const char* msg = "Hello, Pipe!";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);
        wait(NULL);  // Wait for child process
    }
    return 0;
}

Message Queue

A message queue is a kernel-managed linked list of messages, supporting reading/writing by type (POSIX or System V implementation).

POSIX Message Queue Example:

#include <mqueue.h>
#include <stdio.h>
#include <string.h>

int main() {
    mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, NULL);
    if (mq == (mqd_t)-1) { perror("mq_open failed"); return 1; }

    // Send message
    const char* msg = "Hello, Message Queue!";
    mq_send(mq, msg, strlen(msg), 0);  // Priority 0

    // Receive message
    char buf[100];
    unsigned int prio;
    ssize_t recv_bytes = mq_receive(mq, buf, sizeof(buf), &prio);
    printf("Received message: %s (priority: %u)\n", buf, prio);

    mq_close(mq);
    mq_unlink("/my_queue");  // Delete queue
    return 0;
}

Shared Memory

Shared memory allows multiple processes to directly access the same physical memory, the fastest IPC method (requires synchronization).

Example: Multi-Process Shared Counter

#include <stdio.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

#define SHM_KEY 1234  // Shared memory key

int main() {
    // Create shared memory segment
    int shmid = shmget(SHM_KEY, sizeof(int), IPC_CREAT | 0666);
    if (shmid == -1) { perror("shmget failed"); return 1; }

    // Attach to current process memory
    int* counter = shmat(shmid, NULL, 0);
    if (counter == (int*)-1) { perror("shmat failed"); shmctl(shmid, IPC_RMID, NULL); return 1; }

    // Initialize counter
    *counter = 0;

    // Create child process to increment counter
    pid_t pid = fork();
    if (pid == 0) {
        for (int i = 0; i < 1000; i++) {
            (*counter)++;
        }
        _exit(0);
    } else {
        wait(NULL);
        printf("Final counter value: %d\n", *counter);  // Output 1000 (if no race condition)
        shmdt(counter);
        shmctl(shmid, IPC_RMID, NULL);  // Delete shared memory segment
    }
    return 0;
}

Thread Management: Lightweight Process Collaboration

Threads are execution units within a process, sharing process memory, suitable for high-concurrency scenarios (e.g., servers handling multiple requests).

POSIX Threads (pthread)

The C language implements multithreading via the POSIX thread library (pthread.h).

Function Prototypes:

#include <pthread.h>

// Create thread
int pthread_create(pthread_t* thread, const pthread_attr_t* attr,
                   void* (*start_routine)(void*), void* arg);

// Wait for thread termination
int pthread_join(pthread_t thread, void** retval);

// Terminate current thread
void pthread_exit(void* retval);

Example: Multithreaded Sum Calculation

#include <stdio.h>
#include <pthread.h>

#define NUM_THREADS 4
#define ARRAY_SIZE 1000

int array[ARRAY_SIZE];
int sum = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // Mutex lock

void* compute_sum(void* arg) {
    int thread_id = *(int*)arg;
    int start = thread_id * (ARRAY_SIZE / NUM_THREADS);
    int end = (thread_id == NUM_THREADS - 1) ? ARRAY_SIZE : start + (ARRAY_SIZE / NUM_THREADS);

    for (int i = start; i < end; i++) {
        pthread_mutex_lock(&mutex);  // Lock to protect shared variable sum
        sum += array[i];
        pthread_mutex_unlock(&mutex);  // Unlock
    }
    return NULL;
}

int main() {
    // Initialize array
    for (int i = 0; i < ARRAY_SIZE; i++) {
        array[i] = i + 1;
    }

    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];

    // Create threads
    for (int i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, compute_sum, &thread_ids[i]);
    }

    // Wait for all threads to terminate
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("Array sum: %d\n", sum);
    return 0;
}

Thread Synchronization Mechanisms

• Mutex: Protects shared resources, only one thread can access at a time.
• Condition Variable: Threads wait for a specific condition to be met (e.g., producer-consumer model).
• Read-Write Lock: Allows multiple read threads or one write thread (suitable for read-heavy scenarios).

Example: Producer-Consumer Model (Condition Variable)

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];
int in = 0, out = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;  // Buffer empty condition
pthread_cond_t full = PTHREAD_COND_INITIALIZER;   // Buffer full condition

void* producer(void* arg) {
    int item;
    for (int i = 0; i < 10; i++) {
        item = rand() % 100;
        pthread_mutex_lock(&mutex);
        while (in == BUFFER_SIZE) {  // Buffer full, wait
            pthread_cond_wait(&empty, &mutex);
        }
        buffer[in++] = item;
        printf("Producer %ld produced: %d\n", (long)pthread_self(), item);
        pthread_cond_signal(&full);  // Notify consumer
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* consumer(void* arg) {
    int item;
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
        while (out == in) {  // Buffer empty, wait
            pthread_cond_wait(&full, &mutex);
        }
        item = buffer[out++];
        printf("Consumer %ld consumed: %d\n", (long)pthread_self(), item);
        pthread_cond_signal(&empty);  // Notify producer
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t prod_tid, cons_tid;
    pthread_create(&prod_tid, NULL, producer, NULL);
    pthread_create(&cons_tid, NULL, consumer, NULL);
    pthread_join(prod_tid, NULL);
    pthread_join(cons_tid, NULL);
    return 0;
}

System Calls: The “Interface” Between User and Kernel

System calls are the interface for user-space programs to request kernel services. All I/O, process management, etc., are ultimately completed through system calls.

Common System Call Categories

CategoryExample System Calls
File I/Oopen, read, write, close
Process Managementfork, exit, waitpid, execve
Memory Managementbrk, sbrk, mmap, munmap
IPCpipe, shmget, mq_open, socket
Thread Managementpthread_create, pthread_join (POSIX threads)
Network Programmingsocket, bind, listen, accept, connect

System Call Error Handling

System calls return -1 on failure, with the error code stored in the global variable errno (include <errno.h>). Use perror or strerror to output error messages.

Example: Error Handling

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main() {
    int fd = open("nonexistent.txt", O_RDONLY);
    if (fd == -1) {
        perror("open failed");  // Output: "open failed: No such file or directory"
        printf("Error code: %d, Error message: %s\n", errno, strerror(errno));
    }
    return 0;
}

Error Handling: The “Shield” of Robust Programs

In system programming, error handling is key to ensuring program robustness. Follow these principles:

  1. Check return values of all system calls: e.g., open, read, fork.
  2. Release resources promptly: Avoid leaks of file descriptors, memory, locks, etc.
  3. Use errno to locate errors: Output detailed error messages with perror or strerror.
  4. Handle signals: Catch fatal signals like SIGSEGV, SIGBUS to prevent crashes.

Example: Robust File Read Function

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

ssize_t safe_read(int fd, void* buf, size_t count) {
    ssize_t bytes_read;
    while ((bytes_read = read(fd, buf, count)) == -1) {
        if (errno == EINTR) {  // Interrupted by signal, retry
            continue;
        } else {
            perror("read failed");
            return -1;
        }
    }
    return bytes_read;
}

Multiplexing and Asynchronous I/O: The “Ultimate Weapon” for Efficient I/O

Multiplexing (select/poll/epoll)

Multiplexing allows a single thread to monitor multiple file descriptors, notifying the application when a descriptor is ready (readable/writable/exception).

epoll Example (Linux-specific):

#include <sys/epoll.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

#define MAX_EVENTS 10

int main() {
    int epfd = epoll_create1(0);
    if (epfd == -1) { perror("epoll_create1 failed"); return 1; }

    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) { perror("open failed"); close(epfd); return 1; }

    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;  // Monitor readable events
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {
        perror("epoll_ctl failed"); close(epfd); close(fd); return 1;
    }

    struct epoll_event events[MAX_EVENTS];
    while (1) {
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);  // Block waiting
        if (nfds == -1) {
            perror("epoll_wait failed"); break;
        }

        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == fd) {
                char buf[1024];
                ssize_t bytes_read = read(fd, buf, sizeof(buf));
                if (bytes_read == -1) {
                    perror("read failed"); break;
                }
                printf("Read %zd bytes\n", bytes_read);
            }
        }
    }

    close(epfd);
    close(fd);
    return 0;
}

Asynchronous I/O (aio)

Asynchronous I/O allows the application to initiate an I/O operation and return immediately, with the kernel notifying via signal or callback upon completion.

Example: Asynchronous File Read

#include <aio.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) { perror("open failed"); return 1; }

    struct aiocb aiocb;
    char buf[1024];
    memset(&aiocb, 0, sizeof(aiocb));
    aiocb.aio_fildes = fd;
    aiocb.aio_buf = buf;
    aiocb.aio_nbytes = sizeof(buf);
    aiocb.aio_offset = 0;

    // Initiate asynchronous read
    if (aio_read(&aiocb) == -1) {
        perror("aio_read failed"); close(fd); return 1;
    }

    // Wait for asynchronous operation to complete (blocking)
    while (aio_error(&aiocb) == EINPROGRESS) {
        usleep(1000);  // Brief sleep
    }

    if (aio_error(&aiocb) != 0) {
        perror("Asynchronous read failed"); close(fd); return 1;
    }

    ssize_t bytes_read = aio_return(&aiocb);
    printf("Asynchronously read %zd bytes\n", bytes_read);
    printf("Content: %s\n", buf);

    close(fd);
    return 0;
}

File Locking: Coordinating Multi-Process File Access

File locking controls concurrent access to the same file by multiple processes, preventing data inconsistency.

File Lock Types

• Shared Lock (Read Lock): Multiple processes can hold simultaneously (read operations).
• Exclusive Lock (Write Lock): Only one process can hold (write operations).

flock and fcntl Locks

flock(): Simple file lock (entire file), supports shared/exclusive modes.
fcntl(): More flexible, supports region locks (specify file offset and length).

flock Example:

#include <stdio.h>
#include <sys/file.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("data.txt", O_RDWR);
    if (fd == -1) { perror("open failed"); return 1; }

    // Acquire exclusive lock (blocking)
    if (flock(fd, LOCK_EX) == -1) {
        perror("flock failed"); close(fd); return 1;
    }
    printf("Acquired exclusive lock successfully, starting to write file...\n");

    // Write file
    const char* data = "Hello, File Lock!\n";
    write(fd, data, strlen(data));

    // Release lock
    flock(fd, LOCK_UN);
    close(fd);
    return 0;
}

Summary and Practice

Key Summary

• File I/O: Operate files via open/read/write/close, pay attention to modes and error handling.
• Process Management: fork creates processes, waitpid waits for termination, avoid zombie processes.
• Signal Handling: Use sigaction to register handlers, ensure async-safety.
• Memory Management: mmap for shared memory, malloc/free for user-space memory.
• IPC: Pipes, message queues, shared memory, semaphores each suit different scenarios.
• Thread Management: pthread for multithreading, use synchronization to avoid race conditions.
• System Calls: Interface between user and kernel, check return values and error codes.
• Multiplexing/Asynchronous I/O: Efficiently handle large numbers of I/O operations, improve performance.
• File Locking: Coordinate multi-process file access, ensure data consistency.

Practice

• Principle of Least Privilege: Files and processes retain only necessary permissions, reducing security risks.
• Timely Resource Release: Close file descriptors, free memory, destroy locks, avoid leaks.
• Standardized Error Handling: Check return values of all system calls, use perror for error messages.
• Avoid Busy Waiting: Use epoll, aio, or condition variables instead of polling loops.
• Code Portability: Prefer POSIX standard interfaces (e.g., pthread, mmap), reduce platform dependency.

By mastering this knowledge, developers can efficiently implement C system programming, building high-performance, robust applications (e.g., servers, databases, embedded systems).

Share your love