POSIX (Wikipedia Lab Guide)

POSIX: A Deep Dive into Portable Operating System Interfaces
1) Introduction and Scope
POSIX (Portable Operating System Interface) is a family of standards maintained by the IEEE Computer Society, designed to ensure interoperability and portability of applications across different Unix-like and Unix-branded operating systems. It defines a common set of APIs, shell features, and command-line utilities. The primary goal is to allow software written for one POSIX-compliant system to compile and run on another with minimal or no modification. This guide will delve into the technical underpinnings of POSIX, its architectural implications, practical usage, common pitfalls, and defensive engineering considerations. We will focus on the core specifications that form the bedrock of modern Unix-like systems.
The scope of this study guide covers:
- Core System Services: Process management, file I/O, memory management, inter-process communication (IPC).
- Shell and Utilities: Standardized command-line interpreter and essential utilities.
- Threading: The POSIX threads (pthreads) standard.
- Real-time Extensions: Features for time-critical applications.
We will not cover graphical user interface (GUI) aspects or specific implementations beyond their relation to POSIX compliance.
2) Deep Technical Foundations
POSIX standards are built upon a layered approach, abstracting hardware and underlying OS specifics to provide a consistent programming interface.
2.1) The C Language Foundation
At its core, POSIX is tightly coupled with the C programming language. The POSIX.1 specification mandates adherence to the ANSI C standard (and later C standards). This means that functions, data types, and behaviors defined by the C standard are assumed to be available and behave as specified.
Example: The size_t type, defined in <stddef.h>, is used extensively in POSIX APIs for sizes and counts. Its precise size is implementation-defined but guaranteed to be large enough to represent the size of the largest object. ssize_t is a signed counterpart, often used for return values of read/write operations where -1 indicates an error.
// Example using size_t and ssize_t in POSIX file operations
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // For read() and write()
#include <errno.h> // For errno
#include <string.h> // For strerror
// STDIN_FILENO, STDOUT_FILENO are macros defined in <unistd.h>
// representing file descriptors 0 and 1 respectively.
int main() {
char buffer[1024];
ssize_t bytes_read; // ssize_t is a signed version of size_t, used for I/O counts.
// Read up to 1024 bytes from standard input (file descriptor 0).
bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
if (bytes_read == -1) {
// Check errno for specific error code. strerror(errno) provides a human-readable string.
if (errno == EINTR) {
// EINTR: read operation was interrupted by a signal.
fprintf(stderr, "read interrupted by a signal.\n");
} else {
// perror() prints a string describing the error associated with the current value of errno.
perror("read error");
}
exit(EXIT_FAILURE);
} else if (bytes_read == 0) {
// A return value of 0 from read() indicates end-of-file (EOF).
printf("End of file reached.\n");
} else {
// Process the data read into 'buffer'.
// %zd is the format specifier for ssize_t.
printf("Read %zd bytes.\n", bytes_read);
// Example: print the first 100 bytes if available.
size_t print_len = (bytes_read < 100) ? bytes_read : 100;
// Write the data to standard output (file descriptor 1).
if (write(STDOUT_FILENO, buffer, print_len) != print_len) {
perror("write error");
exit(EXIT_FAILURE);
}
printf("\n");
}
return 0;
}2.2) System Calls vs. Library Functions
POSIX defines a rich set of system calls and library functions. It's crucial to distinguish between them:
- System Calls: Direct interfaces to the operating system kernel. They typically involve a context switch from user space to kernel space, incurring overhead. Examples include
fork(),execve(),open(),read(),write(),sbrk(),mmap(). These are often identified by their direct mapping to kernel entry points, sometimes visible through assembly or tracing tools (e.g.,straceon Linux,dtraceon BSD/macOS). - Library Functions: Functions provided by standard C libraries (like
libc) or POSIX-specific libraries. These may wrap system calls, provide higher-level abstractions, perform operations entirely in user space, or offer portability layers. Examples includeprintf(),fopen(),malloc(),pthread_create().
Example: fopen() is a C library function that typically uses the open() system call internally to manage file access. fopen provides buffering, which open does not. This buffering significantly improves performance for many I/O patterns by reducing the number of expensive system calls.
// Conceptual illustration of fopen wrapping open
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // For open()
#include <fcntl.h> // For O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC
#include <string.h> // For strcmp
// Simplified representation of a FILE struct (actual is much more complex)
typedef struct {
int fd; // Underlying file descriptor
char *buffer; // I/O buffer
size_t buffer_size; // Size of the buffer
size_t read_ptr; // Pointer to the next byte to read from buffer
size_t write_ptr; // Pointer to the next byte to write into buffer
int flags; // File status flags (e.g., O_RDONLY)
// ... other fields for error status, mode, etc.
} MyFILE;
// Simplified my_fopen function
MyFILE *my_fopen(const char *path, const char *mode) {
int fd;
MyFILE *stream = NULL;
int open_flags = 0;
mode_t open_mode = 0666; // Default permissions if creating
// Determine open flags based on mode string
if (strcmp(mode, "r") == 0) {
open_flags = O_RDONLY;
} else if (strcmp(mode, "w") == 0) {
open_flags = O_WRONLY | O_CREAT | O_TRUNC;
} else if (strcmp(mode, "a") == 0) {
open_flags = O_WRONLY | O_CREAT | O_APPEND;
} // ... handle "rb", "wb", "ab", "r+", "w+", "a+" etc.
// Call the open system call
fd = open(path, open_flags, open_mode);
if (fd != -1) {
// Allocate memory for the MyFILE structure
stream = (MyFILE *)malloc(sizeof(MyFILE));
if (stream == NULL) {
close(fd); // Close the file descriptor if allocation fails
return NULL;
}
// Initialize stream structure
stream->fd = fd;
stream->flags = open_flags;
stream->buffer_size = 4096; // Example buffer size
stream->buffer = (char *)malloc(stream->buffer_size);
if (stream->buffer == NULL) {
free(stream);
close(fd);
return NULL;
}
stream->read_ptr = 0;
stream->write_ptr = 0;
// ... initialize other fields ...
// If in write mode, potentially fill buffer with existing content if "w+" or "r+"
// If in append mode, seek to end if not creating.
} else {
// Handle error from open()
// errno is set by the open() system call
return NULL; // Indicate failure
}
return stream;
}
// Note: A full implementation would also include my_fclose, my_fread, my_fwrite, etc.,
// which handle buffer management and flushing to the underlying file descriptor.2.3) The POSIX Shell and Utilities
POSIX.2 defines the standard shell (typically a Bourne-compatible shell, like sh or bash in POSIX mode) and a set of essential utilities. This provides a consistent command-line environment and scripting interface.
Key Shell Concepts:
- Environment Variables: Key-value pairs accessible by processes.
PATH,HOME,SHELL,USER,PWDare fundamental. They are inherited by child processes. The shell manages these in its own symbol table. - Redirection: Manipulating standard input (stdin, FD 0), standard output (stdout, FD 1), and standard error (stderr, FD 2) streams using operators like
<,>,>>,2>. This involves kernel system calls likedup2()to duplicate file descriptors. - Pipes: Connecting the standard output of one command to the standard input of another using the
|operator. This creates an anonymous pipe managed by the kernel, typically using thepipe()system call. - Job Control: Managing background and foreground processes using signals and shell built-ins like
fg,bg,jobs. This involves process management system calls likefork(),execve(),waitpid(), and signal handling.
Example Shell Script (Bash, POSIX compliant):
#!/bin/bash
# POSIX compliant script snippet
# Checks if a file exists and is readable, then counts its lines.
# Use a variable for the file path. Double quotes prevent word splitting and globbing.
FILE_TO_CHECK="/etc/passwd"
# Use POSIX test command [ ] or [[ ]] (bash extension, but common).
# -r checks for read permission.
if [ -r "$FILE_TO_CHECK" ]; then
echo "File '$FILE_TO_CHECK' exists and is readable."
# Example of using a POSIX utility: wc
# wc -l < "$FILE_TO_CHECK" redirects stdin of wc to the file.
# This is more efficient than cat file | wc -l as it avoids an extra process.
echo "Line count:"
wc -l < "$FILE_TO_CHECK"
else
# Use stderr for error messages. ">&2" redirects stdout to stderr.
echo "Error: File '$FILE_TO_CHECK' not found or not readable." >&2
exit 1 # Non-zero exit status indicates failure.
fi
exit 0 # Zero exit status indicates success.Key Utilities: ls, cp, mv, rm, grep, sed, awk, find, sort, uniq, tar, chmod, chown, ps, kill, echo, cat, more, less. These utilities are implemented as executable programs that conform to POSIX standards for their arguments and behavior.
3) Internal Mechanics / Architecture Details
POSIX dictates interfaces, not specific kernel architectures. However, understanding how these interfaces map to underlying OS mechanisms is crucial.
3.1) Process Model
POSIX defines processes as independent entities with their own address space, file descriptors, and execution context.
fork(): Creates a near-identical copy of the calling process. The child process inherits copies of the parent's memory (initially copy-on-write, meaning pages are shared until one process writes to them, at which point a private copy is made), file descriptors (sharing underlying file status flags and seek positions), signal handlers, environment variables, and current working directory. The return value offork()is0in the child and the child's PID in the parent.execve(): Replaces the current process image with a new program. It takes the path to the executable, an array of arguments (argv), and an array of environment variables (envp). Afterexecve, the old process context is gone; the new program starts execution at itsmainfunction. Crucially, file descriptors opened beforeexecveremain open (unless they have theFD_CLOEXECflag set).
Process Memory Layout (Conceptual):
+-------------------+ <-- Higher memory addresses
| Kernel | (Managed by the OS, not directly visible to user space)
+-------------------+
| User Space | (Addressable by the process)
+-------------------+
| Command Line Args | argv[] (passed to main)
+-------------------+
| Environment | envp[] (passed to main)
+-------------------+
| Stack | <-- Grows downwards. Contains local variables, function call frames.
| |
| Memory Mapped | (e.g., mmap'd files, shared memory, libraries loaded by dynamic linker)
| Region |
+-------------------+
| Heap | <-- Grows upwards (managed by malloc/brk/sbrk). Dynamic allocations.
| |
| .bss | (Uninitialized global/static variables. Zero-initialized by OS loader)
| .data | (Initialized global/static variables)
| .text | (Code segment. Read-only, executable)
+-------------------+ <-- Lower memory addressesExample: fork() and execve()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // For fork, execve, getpid, getppid
#include <sys/wait.h> // For waitpid
#include <errno.h> // For errno
#include <string.h> // For strerror
int main() {
pid_t pid;
printf("Parent (PID: %d): About to fork.\n", getpid());
pid = fork(); // Create a new process
if (pid < 0) { // Error handling: fork() returns -1 on failure
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid == 0) { // Child process: fork() returns 0 to the child
printf("Child (PID: %d): My Parent PID is %d\n", getpid(), getppid());
// Replace child process with 'ls -l' command
char *args[] = {"ls", "-l", NULL}; // argv for execve, last element must be NULL
// char *env[] = {NULL}; // Pass NULL to inherit parent's environment (often preferred)
// Or explicitly set environment:
char *env[] = {"MY_VAR=child_value", "PATH=/bin:/usr/bin", NULL}; // Example explicit env
printf("Child (PID: %d): Executing ls -l...\n", getpid());
// execve replaces the current process image. It does NOT return on success.
execve("/bin/ls", args, env);
// execve only returns if an error occurs
fprintf(stderr, "Child (PID: %d): execve failed: %s\n", getpid(), strerror(errno));
exit(EXIT_FAILURE); // Exit with error status if execve fails
} else { // Parent process: fork() returns the child's PID to the parent
printf("Parent (PID: %d): Created child with PID %d.\n", getpid(), pid);
int status;
// Wait for the specific child process to terminate.
// pid: PID of the child to wait for.
// &status: Pointer to an int to store the termination status.
// 0: flags (0 means wait for any child, or specific child if pid is set).
pid_t terminated_pid = waitpid(pid, &status, 0);
if (terminated_pid == -1) { // Error handling for waitpid
perror("waitpid failed");
exit(EXIT_FAILURE);
}
// Macros to interpret the status value
if (WIFEXITED(status)) { // Child terminated normally
printf("Parent (PID: %d): Child process %d terminated normally with exit status %d.\n",
getpid(), pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { // Child terminated by a signal
printf("Parent (PID: %d): Child process %d terminated by signal %d.\n",
getpid(), pid, WTERMSIG(status));
} else { // Other termination conditions (e.g., stopped, continued)
printf("Parent (PID: %d): Child process %d terminated with unknown status %d.\n",
getpid(), pid, status);
}
}
printf("Process (PID: %d): Exiting.\n", getpid());
return 0;
}3.2) File I/O and Descriptors
POSIX treats almost everything as a file: regular files, directories, devices (terminals, disks, network interfaces), and inter-process communication channels (pipes, sockets). This unified model simplifies programming.
- File Descriptors (FDs): Small, non-negative integers representing an open file or I/O resource. Each process has its own table of FDs, managed by the kernel. FDs are inherited across
fork()calls.0: Standard Input (stdin)1: Standard Output (stdout)2: Standard Error (stderr)
open(): System call to open a file or device. Returns a file descriptor.- Flags:
O_RDONLY,O_WRONLY,O_RDWR(read/write),O_CREAT(create if not exists),O_TRUNC(truncate to zero length),O_APPEND(append to end of file),O_EXCL(exclusive create, used withO_CREAT),O_NONBLOCK(non-blocking I/O). Combinations are bitwise ORed. - Mode: Permissions (e.g.,
0644forrw-r--r--) used only whenO_CREATis specified. This mode is masked by the system'sumask.
- Flags:
read()/write(): System calls for byte-stream I/O. They operate on file descriptors. The number of bytes transferred can be less than requested, especially when dealing with pipes, terminals, or signals.close(): System call to close a file descriptor, releasing associated resources and decrementing the kernel's open file count for that inode.
File Descriptor Table (Conceptual):
Each process has a file descriptor table managed by the kernel. This table maps the process's FDs to entries in the kernel's open file table, which in turn points to file table entries (containing current file offset, access mode) and then to inodes or device structures.
Process XYZ (PID: 1234):
FD Table (Kernel Array):
[0] --> Points to Kernel Open File Table Entry for stdin
[1] --> Points to Kernel Open File Table Entry for stdout
[2] --> Points to Kernel Open File Table Entry for stderr
[3] --> Points to Kernel Open File Table Entry for /path/to/data.txt (read mode, offset X)
[4] --> Points to Kernel Open File Table Entry for pipe read end (pipe buffer Y)
[5] --> Points to Kernel Open File Table Entry for socket Z
...Example: Low-level file copy using open, read, write, close
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h> // For open() flags (O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC)
#include <unistd.h> // For read(), write(), close(), STDIN_FILENO, STDOUT_FILENO
#include <errno.h> // For errno
#include <string.h> // For strerror
#define BUFFER_SIZE 4096 // A common buffer size for efficient I/O. Powers of 2 are often optimal.
int main(int argc, char *argv[]) {
// Ensure correct number of command-line arguments.
if (argc != 3) {
fprintf(stderr, "Usage: %s <source_file> <destination_file>\n", argv[0]);
exit(EXIT_FAILURE);
}
int fd_src, fd_dst;
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
ssize_t bytes_written;
// Open source file for reading.
// O_RDONLY: Read-only access.
fd_src = open(argv[1], O_RDONLY);
if (fd_src == -1) {
// strerror(errno) provides a descriptive error message.
fprintf(stderr, "Error opening source file '%s': %s\n", argv[1], strerror(errno));
exit(EXIT_FAILURE);
}
// Open destination file for writing.
// O_WRONLY: Write-only access.
// O_CREAT: Create the file if it does not exist.
// O_TRUNC: If the file exists, truncate it to zero length.
// Permissions: 0644 (owner read/write, group read, others read). This is masked by umask.
fd_dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd_dst == -1) {
fprintf(stderr, "Error opening destination file '%s': %s\n", argv[2], strerror(errno));
close(fd_src); // Clean up already opened source file descriptor.
exit(EXIT_FAILURE);
}
// Copy content block by block.
// The loop continues as long as read() returns a positive number of bytes.
while ((bytes_read = read(fd_src, buffer, sizeof(buffer))) > 0) {
// Write the exact number of bytes read.
// It's crucial to write exactly 'bytes_read' bytes.
bytes_written = write(fd_dst, buffer, bytes_read);
if (bytes_written == -1) {
fprintf(stderr, "Error writing to destination file '%s': %s\n", argv[2], strerror(errno));
close(fd_src);
close(fd_dst);
exit(EXIT_FAILURE);
}
// Check for partial writes. While less common for regular files, it can happen.
if (bytes_written != bytes_read) {
fprintf(stderr, "Partial write to destination file '%s'. Expected %zd, wrote %zd.\n",
argv[2], bytes_read, bytes_written);
// Depending on requirements, this might be an error or require retry logic.
close(fd_src);
close(fd_dst);
exit(EXIT_FAILURE);
}
}
// After the loop, check if read() returned an error.
if (bytes_read == -1) {
fprintf(stderr, "Error reading from source file '%s': %s\n", argv[1], strerror(errno));
close(fd_src);
close(fd_dst);
exit(EXIT_FAILURE);
}
printf("File copied successfully from %s to %s\n", argv[1], argv[2]);
// Close file descriptors. It's good practice to check return values.
if (close(fd_src) == -1) {
perror("Error closing source file descriptor");
}
if (close(fd_dst) == -1) {
perror("Error closing destination file descriptor");
}
return 0;
}3.3) Memory Management
POSIX systems typically employ virtual memory, providing each process with a large, contiguous address space independent of physical RAM.
- Virtual Memory: The kernel manages the mapping between virtual addresses used by processes and physical RAM addresses. This provides memory protection (one process cannot access another's memory) and allows processes to use more memory than physically available (via swapping to disk).
mmap(): A powerful system call that maps files or devices into memory. It can be used for:- Efficient file I/O (reading/writing directly to memory regions).
- Creating anonymous memory regions (not backed by a file, similar to
malloc's heap). - Implementing shared memory between processes (
MAP_SHARED). - Flags:
MAP_SHARED(changes are visible to other processes mapping the same region and are written back to the backing store),MAP_PRIVATE(changes are copy-on-write and not shared with other processes or the backing store),MAP_ANONYMOUS(region not backed by a file, often used for allocating memory).
brk()/sbrk(): Older, simpler mechanism for adjusting the program break (the end of the data segment).malloc()often usessbrk()for smaller allocations, growing the heap.malloc()/free(): C library functions for dynamic memory allocation. They abstractsbrk()ormmap()to provide a convenient API for allocating and deallocating memory blocks in the heap.mallocimplementations are complex and aim for efficiency and fragmentation reduction.
Shared Memory Example (POSIX Shared Memory Objects):
Two processes can map the same POSIX shared memory object into their respective address spaces, allowing them to communicate by reading and writing to that memory. This requires careful synchronization to avoid race conditions.
// Process A (Writer) - POSIX Shared Memory Object
#include <sys/mman.h> // For shm_open, mmap, munmap, shm_unlink
#include <fcntl.h> // For O_CREAT, O_RDWR, etc.
#include <unistd.h> // For ftruncate, close
#include <string.h> // For strcpy
#include <stdio.h>
#include <stdlib.h>
#include <errno.h> // For errno
#define SHM_SIZE 1024
// POSIX shared memory object name (must start with '/').
// It appears in the filesystem, typically under /dev/shm on Linux.
#define SHM_NAME "/my_posix_shm_segment"
int main() {
int shm_fd;
char *ptr; // Pointer to the mapped shared memory region
// Create or open a POSIX shared memory object.
// O_CREAT: create if it doesn't exist.
// O_RDWR: open for reading and writing.
// 0666: permissions (owner, group, others all read/write). This is masked by umask.
shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open failed");
exit(EXIT_FAILURE);
}
// Set the size of the shared memory object.
// This must be done by one process before mapping if creating.
if (ftruncate(shm_fd, SHM_SIZE) == -1) {
perror("ftruncate failed");
close(shm_fd);
shm_unlink(SHM_NAME); // Clean up the object if ftruncate fails.
exit(EXIT_FAILURE);
}
// Map the shared memory object into the process's address space.
// 0: address hint (kernel chooses an appropriate address).
// SHM_SIZE: length of the mapping.
// PROT_READ | PROT_WRITE: memory protection flags (readable and writable).
// MAP_SHARED: changes are visible to other processes mapping the same object and are written back to the backing store.
// shm_fd: file descriptor of the shared memory object.
// 0: offset within the object (usually 0 for the whole object).
ptr = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (ptr == MAP_FAILED) { // MAP_FAILED is typically ((void *) -1)
perror("mmap failed");
close(shm_fd);
shm_unlink(SHM_NAME); // Clean up.
exit(EXIT_FAILURE);
}
// Write to shared memory.
strcpy(ptr, "Hello from Process A (Writer)!");
printf("Process A wrote: '%s'\n", ptr);
printf("Process A: Shared memory segment '%s' created and written to.\n", SHM_NAME);
printf("Process A: Waiting for user to press Enter before cleaning up...\n");
getchar(); // Keep the shared memory alive until user interaction.
// Cleanup: Unmap the memory, close the file descriptor, and remove the shared memory object.
if (munmap(ptr, SHM_SIZE) == -1) {
perror("munmap failed");
}
if (close(shm_fd) == -1) {
perror("close failed");
}
// shm_unlink removes the object name from the filesystem. The actual memory is freed when all processes unmap it and the last descriptor is closed.
if (shm_unlink(SHM_NAME) == -1) {
perror("shm_unlink failed");
}
printf("Process A: Cleaned up shared memory.\n");
return 0;
}(A corresponding "Process B (Reader)" would shm_open the same name, mmap it, and then read from ptr. It should also call shm_unlink if it's the last one to finish to ensure cleanup.)
3.4) Inter-Process Communication (IPC)
POSIX provides a rich set of IPC mechanisms for processes to exchange data and synchronize.
- Pipes:
- Anonymous Pipes (
pipe()): Unidirectional byte streams. Typically used between related processes (e.g., parent-child afterfork()). A pipe has two ends: a read end and a write end. Reading from an empty pipe blocks; writing to a full pipe blocks. Closing the write end signals EOF to readers. Closing the read end signals an error to writers. - Named Pipes (FIFOs): Created as special files in the filesystem (
mkfifo). Allow unrelated processes to communicate via a filesystem path. They also provide blocking read/write semantics and behave similarly to anonymous pipes regarding EOF and errors.
- Anonymous Pipes (
- Message Queues (
mq_open,mq_send,mq_receive): POSIX message queues allow processes to exchange structured messages. Each message has a priority, and messages are delivered in priority order. They are more robust than pipes for structured data and can provide non-blocking operations. Messages are persistent until read. - Semaphores (
sem_open,sem_wait,sem_post): Synchronization primitives used to control access to shared resources. POSIX semaphores can be named (for IPC, usingsem_open) or unnamed (for threads, usingsem_init).sem_wait(orsem_trywaitfor non-blocking) decrements the semaphore value (blocking if zero), andsem_postincrements it. - Shared Memory (
shm_open,mmap): As shown above, allows processes to share a region of memory. This is the fastest IPC mechanism but requires explicit synchronization using other primitives like mutexes or semaphores. - Sockets: A general-purpose IPC mechanism, also used for network communication. POSIX sockets (BSD sockets API) are fundamental and can be used for local (Unix domain sockets, using
AF_UNIXaddress family) or remote (TCP/IP, UDP, usingAF_INETorAF_INET6) communication.
Pipe Example (Anonymous Pipe for Parent-Child Communication):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // For pipe, fork, read, write, close
#include <sys/wait.h> // For wait
#include <string.h> // For strlen
#include <errno.h> // For errno
int main() {
int pipefd[2]; // pipefd[0] is for reading, pipefd[1] is for writing
pid_t pid;
char buffer[100];
const char *msg_to_child = "Hello from parent!";
const char *msg_to_parent = "ACK from child!";
// Create an anonymous pipe.
// pipefd[0] will be the read end, pipefd[1] will be the write end.
if (pipe(pipefd) == -1) {
perror("pipe creation failed");
exit(EXIT_FAILURE);
}
pid = fork(); // Create a child process
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid == 0) { // Child process
// Child does not need the write end of the pipe. Close it to avoid deadlocks.
close(pipefd[1]);
printf("Child (PID: %d): Waiting to read from pipe...\n", getpid());
// Read from the pipe. Leave space for null terminator.
ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
perror("Child: read from pipe failed");
close(pipefd[0]); // Clean up
exit(EXIT_FAILURE);
} else if (bytes_read == 0) {
printf("Child (PID: %d): Pipe closed by parent before receiving data.\n", getpid());
} else {
buffer[bytes_read] = '\0'; // Null-terminate the received string
printf("Child (PID: %d): Received: '%s'\n", getpid(), buffer);
// Child sends a reply back.
printf("Child (PID: %d): Sending ACK to parent...\n", getpid());
if (write(pipefd[1], msg_to_parent, strlen(msg_to_parent)) == -1) {
perror("Child: write to pipe failed");
}
}
close(pipefd[0]); // Close the read end of the pipe.
printf("Child (PID: %d): Exiting.\n", getpid());
exit(EXIT_SUCCESS);
} else { // Parent process
// Parent does not need the read end of the pipe. Close it.
close(pipefd[0]);
printf("Parent (PID: %d): Writing to pipe...\n", getpid());
---
## Source
- Wikipedia page: https://en.wikipedia.org/wiki/POSIX
- Wikipedia API endpoint: https://en.wikipedia.org/w/api.php
- AI enriched at: 2026-03-30T23:00:21.431Z