Lecture 03: Direct Communication & InDirect Communication
Communication between Processes
1. Direct Communication
Definition: A systematic communication method where the sender and receiver explicitly name each other to establish a direct, dedicated communication link.
How Communication is Done:
- Explicit Naming: The sending process must explicitly name the receiving process, and the receiving process must explicitly name the sending process.
- Example:
send(Process B, message)
,receive(Process A, message)
- Example:
- Direct Link: A link is established between exactly two communicating processes. This link is automatically created when processes try to communicate and typically exists only between that pair.
- Unidirectional or Bidirectional: The link can be either one-way or two-way, depending on the implementation.
Process Establishment (of the Communication Link): The communication link is established implicitly when one process attempts to send
to another, or when a process is prepared to receive
from a specific other process. Thereβs no separate βconnectβ step just for the link in many OS IPC contexts, as the naming itself implies the connection.
- Network Context (e.g., Sockets): If considering network communication (which can be seen as direct if IP addresses and ports are used to connect to a specific target), then establishing a process link involves:
- Server process creating a socket, binding it to a local IP address and Port, and listening.
- Client process creating a socket and explicitly connecting to the serverβs IP address and Port. This explicitly names the destination endpoint.
Ports:
- In the purest definition of direct IPC within an OS, ports arenβt always a primary conceptual element, as processes name each other directly.
- However, in network communication (like TCP/IP sockets), ports are fundamental for direct communication. A process on a host is identified by its IP address plus a port number, forming a complete endpoint address for direct connection.
Encryption:
- Optional Overlay: Encryption is not an inherent part of the direct communication mechanism itself.
- Itβs a security layer that can be applied on top of the communication channel to protect the data being exchanged. Whether direct or indirect, communication can be encrypted for confidentiality and integrity (e.g., using TLS/SSL over a socket connection).
2. Indirect Communication
Definition: A systematic communication method where processes communicate without explicitly naming each other, instead sending and receiving messages via an intermediary object, typically called a βmailboxβ or βport.β
How Communication is Done:
- Shared Mailbox/Port: Processes communicate by sending messages to a specific mailbox (or port) and receiving messages from a specific mailbox (or port). They donβt need to know the identity of the other communicating process.
- Decoupling: Sender and receiver are decoupled; multiple processes can send to the same mailbox, and multiple processes can receive from the same mailbox.
- Mailbox Identification: Each mailbox has a unique ID or name that processes use to address it.
Process Establishment (of the Communication Link):
- Mailbox Creation: A process (or the OS) must first create a mailbox (or port).
- Process Association: Processes wishing to communicate through that mailbox must then explicitly βopen,β βattach to,β or βregisterβ with that specific mailbox.
- Example:
send(Mailbox A, message)
,receive(Mailbox A, message)
- Example:
- The βlinkβ here is conceptual, between the process and the mailbox, rather than between two specific processes.
Ports:
- Central Concept: In indirect communication, the term βportβ is often synonymous with βmailboxβ (e.g., in microkernel architectures like Mach, which heavily use ports for IPC).
- Ports/mailboxes act as queues or buffers where messages are temporarily stored until a receiving process retrieves them.
Encryption:
- Optional Overlay: Similar to direct communication, encryption is not an inherent part of the indirect communication mechanism.
- It is a security measure that can be implemented to protect messages as they are sent to or retrieved from the mailbox/port, ensuring that even if an unauthorized process accesses the mailbox, the message content remains confidential.
- Excellent, letβs dive into the concepts of ownership, pipelines, and mailboxes, and then visualize it all with a diagram!
1. Concept of Ownership in Communication
Ownership in communication mechanisms refers to which entity (a process, the OS, or both) is responsible for the creation, management, and destruction of the communication resource, and who has access rights to it.
Direct Communication (e.g., Sockets, Pipes between parent/child):
- Link Ownership: The communication link itself (the conceptual connection) is often implicitly managed by the OS but βownedβ in a shared sense by the two participating processes. Neither process solely owns the other end of the connection.
- Resource Ownership: The underlying OS resources (e.g., file descriptors for pipes, socket descriptors for sockets) are owned by the individual processes that opened them. When a process closes its end, it releases its ownership of that specific resource.
- Establishment: Typically, one process βcreatesβ or βlistensβ for a connection (e.g., a server socket), and another βconnectsβ to it (e.g., a client socket). Both processes hold references to their respective ends of the communication channel.
Indirect Communication (e.g., Mailboxes/Message Queues):
- Mailbox Ownership: The mailbox or message queue itself is typically an OS-managed resource.
- Creator Process: Often, a specific process will create the mailbox. This creator process might be considered its initial βownerβ in the sense that it initiated its existence and might have special permissions to set its properties or destroy it.
- Access Ownership: Once created, other processes can gain βaccess ownershipβ or βusage rightsβ by opening it or registering with it. Permissions (read, write) are crucial here, defining who can send to and receive from the mailbox.
- Decoupling: A key feature is the decoupling of the mailboxβs existence from the lifetime of any single communicating process. The mailbox can persist even if the creating process terminates, as long as the OS maintains it.
2. Pipeline of Processes
A βpipeline of processesβ is a classic paradigm where the output of one process serves as the input for another, creating a sequential flow of data processing. This is a powerful concept for building modular and efficient systems.
- Concept: Imagine an assembly line for data. Each process in the pipeline performs a specific task on the data it receives and then passes its result to the next process.
- How Communication is Done:
- Pipes: Unnamed pipes (for related processes like parent-child) or named pipes (FIFOs, for unrelated processes) are the most common and intuitive systematic communication mechanism for pipelines in Unix-like systems.
- Message Queues (Mailboxes): Can also be used. Process A sends its output as a message to Mailbox M1. Process B receives from M1, processes it, and sends its output to Mailbox M2. Process C receives from M2, and so on.
- Shared Memory/Files: Less common for simple pipelines due to the overhead of explicit synchronization, but viable for complex scenarios.
- Benefits:
- Modularity: Each process does one thing well.
- Reusability: Individual processes can be combined in different pipelines.
- Concurrency: Processes can execute concurrently, improving throughput. The βupstreamβ processes can produce data while βdownstreamβ processes consume it.
3. Mailbox (Detailed)
A mailbox (often synonymous with a message queue or port in some contexts) is a fundamental systematic communication mechanism that enables indirect process communication.
- Definition: A kernel-managed data structure that acts as a buffer or queue for messages. It serves as an intermediary through which processes can send and receive messages without needing to know each otherβs identities.
- Core Functionality:
- Buffering: Stores messages sent by processes until they are retrieved by a receiving process. This decouples sender and receiver in time.
- Identification: Each mailbox has a unique identifier (name or ID) that processes use to access it.
- Synchronization: The OS handles synchronization implicitly.
- If a process tries to receive from an empty mailbox, it can either block (wait) or return immediately with an error, depending on the call.
- If a process tries to send to a full mailbox, it can either block or return an error.
- Capacity: Mailboxes typically have a finite capacity (a maximum number of messages or total bytes).
- Access Control: The OS usually provides mechanisms to control which processes have permission to send messages to a mailbox, and which can receive messages from it.
- Typical Operations:
create_mailbox(name, permissions)
: Creates a new mailbox.send_message(mailbox_id, message)
: Sends a message to the specified mailbox.receive_message(mailbox_id)
: Retrieves a message from the specified mailbox.destroy_mailbox(mailbox_id)
: Deallocates the mailbox.
- Analogy: Think of a post office box (PO Box). Multiple people can drop letters into it, and anyone with the key can retrieve letters from it. The sender doesnβt need to know who the receiver is, just the PO Box number.
Diagram: Communication in OS with Ownership, Mailboxes, and Pipelines
βββββββββββββββββββββββββββββββββββββββββββ
β OPERATING SYSTEM (OS) β
β (Manages all communication resources) β
βββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SYSTEMATIC COMMUNICATION TYPES β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββ ββββββββββββββββββββββββββββ
β **1. DIRECT COMMUNICATION** β β **2. INDIRECT COMMUNICATION** β
β (P1 & P2 know each other) β β (Via Mailbox / Port) β
βββββββββββββββββββββ ββββββββββββββββββββββββββββ
β β
β βΌ
β βββββββββββββββββββββββββββββ
β β **MAILBOX M1** β
β β (OS-Managed Resource) β
β β - Buffer for Messages β
β β - Unique ID/Name β
β β - Access Permissions β
β βββββββββββββββββββββββββββββ
β β²
β β
β β
βββββββββββΌββββββββββ ββββββββββββββββββββΌββββββββββββββββββ
β β β β β
β **PROCESS P1** β β **PROCESS A** β **PROCESS B** β
β - Owns its end of β β - Sends to M1 β - Receives from M1β
β Link L1 βββββLink L1βββΆβ β - May be creatorβ
β - Sends/Receives β β (User Process) β of M1 β
β (User Process) β ββββββββββββββββββββΌββββββββββββββββββ
βββββββββββββββββββββ β
^ (Optional Encryption applied over Link L1)β
β β
β ββββββββββββββββββββββββββββββββββββββββ
β β
β βΌ
β **Concept of Ownership:**
β - Link L1: Shared by P1 & P2. OS provides mechanisms.
β - Mailbox M1: OS-managed. Process B (or OS) might be creator/primary owner,
β but A has usage rights (permissions).
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β **3. PROCESS PIPELINE** β
β (Using Indirect Communication - e.g., Mailboxes) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββ βββββββββββββ βββββββββββββ
β β (Output) β β (Output) β β
β **P_SOURCE**βββββββββββββββββΆβ **P_FILTER**βββββββββββββββββΆβ **P_SINK** β
β (Writes β Mailbox_A β (Reads β Mailbox_B β (Reads β
β to Mailbox_A)ββββββββββββββββββΆβ from Mailbox_A,ββββββββββββββββββΆβ from Mailbox_B)β
β β (Input) β Writes to β (Input) β β
βββββββββββββ β Mailbox_B) β βββββββββββββ
βββββββββββββ
**Pipeline Flow:**
P_SOURCE generates data -> sends to Mailbox_A.
P_FILTER receives from Mailbox_A, processes data, -> sends to Mailbox_B.
P_SINK receives from Mailbox_B, processes final data.
**Ownership in Pipeline:**
- Mailbox_A & Mailbox_B: OS-managed. P_SOURCE owns write-access to Mailbox_A,
P_FILTER owns read-access to Mailbox_A and write-access to Mailbox_B, etc.
- Each process in the pipeline operates independently on its input/output mailboxes.
Drawback of Mailbox
Okay, here are the main drawbacks of using mailboxes for inter-process communication:
- Overhead of Message Copying: Messages must be copied from the senderβs address space to the kernelβs buffer (the mailbox), and then from the kernelβs buffer to the receiverβs address space. This incurs performance overhead, especially for large messages.
- Limited Capacity: Mailboxes typically have a finite size (either in the number of messages or total bytes). If a mailbox becomes full, sending processes may block or fail, leading to potential bottlenecks or message loss if not handled carefully.
- No Direct Data Sharing: Unlike shared memory, mailboxes donβt allow processes to directly share complex data structures in memory. Each message is a distinct copy, which can be inefficient for highly structured or frequently updated data.
- Performance Can Be Lower: Due to the copying overhead and kernel involvement for each message, mailboxes are generally slower than shared memory for high-throughput or low-latency communication.
- Complexity of Message Formats: Processes need to agree on message formats and parsing rules, adding a layer of complexity to application design.
- Potential for Deadlock/Livelock: If processes are not designed carefully (e.g., a sender waits indefinitely for space in a full mailbox while the receiver also waits indefinitely for another resource), deadlocks or livelocks can occur.
Alright, letβs break down buffering in process communication and those different capacity models!
Buffering in Process Communication
Definition: Buffering in process communication refers to the use of temporary storage (a buffer) to hold messages or data being exchanged between a sending process and a receiving process. This buffer is typically managed by the Operating System (often in kernel space).
Purpose: The primary goal of buffering is to decouple the sender and receiver in time, allowing them to operate at different speeds or to continue execution even if the other party isnβt immediately ready.
- Sender doesnβt block immediately: A sender can place data into the buffer and continue its work without waiting for the receiver to be ready to pick it up.
- Receiver doesnβt block immediately: A receiver can retrieve data from the buffer when itβs ready, without waiting for a sender to produce new data (if data is already buffered).
- Speed Mismatch Handling: It helps smooth out performance differences between processes, acting as a temporary reservoir.
Buffer Capacity Models
The behavior of inter-process communication (IPC) mechanisms often depends critically on the capacity of the buffer used.
1. Zero Capacity Buffering (No Buffering / Rendezvous)
- Definition: This model implies that there is no buffer at all, or effectively a buffer of size zero.
- How Communication is Done:
- Communication is strictly synchronous.
- The sending process must block until the receiving process is ready to receive the message.
- Conversely, the receiving process must block until the sending process is ready to send a message.
- Itβs a βrendezvousβ point: both processes must meet at the communication point simultaneously for the transfer to occur.
- Characteristics:
- Strong Synchronization: Provides the tightest possible synchronization between sender and receiver.
- No Message Loss: Since thereβs no buffer to overflow, messages are never dropped due to buffer capacity.
- High Latency Potential: Either process can be blocked for an unpredictable amount of time waiting for the other.
- Example: Often seen in specific types of synchronous message passing, or in the initial handshake of certain direct communication protocols.
2. Bounded Capacity Buffering
- Definition: The buffer has a fixed, finite size (e.g., N messages, or a specific number of bytes). This is the most common and practical buffering model for IPC mechanisms like pipes and message queues (mailboxes).
- How Communication is Done:
- Sender Behavior:
- If the buffer is not full, the sender places the message into the buffer and continues execution immediately (asynchronous send).
- If the buffer is full, the sender typically blocks until space becomes available (i.e., a receiver takes a message), or it might return an error depending on the API.
- Receiver Behavior:
- If the buffer is not empty, the receiver takes a message from the buffer and continues execution.
- If the buffer is empty, the receiver typically blocks until a message arrives, or it might return an error.
- Sender Behavior:
- Characteristics:
- Partial Decoupling: Senders and receivers are decoupled to the extent of the bufferβs size. They donβt always have to be ready at the same time.
- Resource Constraint: The buffer size is a system resource that needs to be managed.
- Potential for Blocking: Senders can still block if the buffer fills up.
- No Message Loss (under normal operation): Messages are not lost due to buffer overflow unless the sender chooses to non-blockingly discard messages when full.
- Examples: Linux pipes, message queues (mailboxes), TCP socket buffers.
3. Unbounded Capacity Buffering
- Definition: The buffer is theoretically of infinite size and can hold an unlimited number of messages.
- How Communication is Done:
- Sender Behavior: The sending process never blocks due to a full buffer. It always places its message into the buffer and continues execution immediately (fully asynchronous send).
- Receiver Behavior:
- If the buffer is not empty, the receiver takes a message and continues.
- If the buffer is empty, the receiver typically blocks until a message arrives, or it might return an error.
- Characteristics:
- Maximum Decoupling: Provides the highest degree of temporal decoupling between sender and receiver. The sender is never held back by the receiverβs speed.
- Theoretical Ideal (in practice): Truly infinite buffers are not possible in real computer systems due to finite memory.
- Resource Exhaustion Risk: In a practical implementation, an βunboundedβ buffer would grow dynamically, eventually consuming all available system memory if the sender consistently outpaces the receiver, leading to system instability or crashes.
- Example: Conceptually useful for designing highly decoupled systems, but practically implemented with large (bounded) dynamic buffers that are monitored, or with mechanisms that apply backpressure or discard old messages if they approach limits.
Bufer Working
graph TD
%% OS-managed buffers and rendezvous point
subgraph OS
BufferAB(Buffer_AB β Managed by OS)
BufferBC(Buffer_BC β Managed by OS)
Rendezvous((Rendezvous Point β No Buffer))
end
%% Processes
P1(Process P1 β Sender)
P2(Process P2 β Middleman)
P3(Process P3 β Receiver)
P1s(Process P1_Sync)
P2s(Process P2_Sync)
%% Zero-capacity (rendezvous) flow
P1s -- blocks until P2s ready --> Rendezvous
P2s -- blocks until P1s ready --> Rendezvous
Rendezvous --- P1s
Rendezvous --- P2s
%% Bounded/unbounded buffers between async processes
P1 -->|write| BufferAB
BufferAB -->|read| P2
P2 -->|write| BufferBC
BufferBC -->|read| P3
%% Legend for buffer types
subgraph Legend
Bounded[Bounded buffer β fixed size]
Unbounded[Unboundedβtheoretical]
ZeroCap[Zeroβrendezvous]
end
Bounded --- BufferAB
Unbounded --- BufferBC
ZeroCap --- Rendezvous
File I/O & Buffering: The OSβs Speed Trick
The OS uses RAM (buffers/cache) to manage the speed difference between fast CPU/memory and slower storage devices.
1. Buffering in File I/O
- Core Idea: Data temporarily held in RAM to optimize disk reads/writes.
- Read Buffer (Page Cache):
- Cache Hit: If data is already in RAM, itβs returned instantly. Speedy!
- Cache Miss: If not, the OS reads from disk, stores it in the cache, then delivers it. Next time, itβs a hit!
- Write Buffer (Write-Behind):
- Data is written to the page cache and marked βdirty.β
- The OS immediately acknowledges the write (super fast from the appβs perspective).
- βDirtyβ pages are then asynchronously written to disk in bulk, reducing many small I/O operations into fewer, larger ones.
- Benefits:
- Batches small I/O requests into efficient, larger transfers.
- Effectively βhidesβ slow disk latency from applications.
- Enables smart features like read-ahead (prefetching data the app might need soon).
- Drawbacks:
- Data at Risk: If the system crashes before dirty pages are flushed to disk, that data is lost. (Think of that unsaved document dread!).
- Memory Usage: Requires a portion of RAM for the cache.
2. File Creation Steps
When you ask the OS to create a file:
- System Call (
open(path, O_CREAT|O_WRONLY,...)
):- OS first verifies permissions on the parent directory.
- A new inode is allocated (this is where metadata like permissions, timestamps, and pointers to actual data blocks live).
- A directory entry is created, linking your human-readable filename to the new inode.
- The file size is initialized to zero; no actual data blocks are allocated yet.
- A file descriptor (fd) is returned to your application, acting as a handle to this new file.
- First Write(s):
- Data is initially copied into the write buffer (page cache).
- The inodeβs size and timestamps are updated (in memory).
- These βdirtyβ pages are then scheduled for an asynchronous write-back to the physical disk.
3. Diagram Flow Summary (Mermaid Explained)
open()
: Initiates the process, allocating an inode and directory entry, then hands back anfd
.write()
: Your app writes data, which lands in the page cache first, marking it dirty, and updating the inodeβs metadata in memory.flush
: The OS periodically moves these βdirtyβ pages from the cache to the actual disk.read()
: Checks the page cache first; if the data isnβt there (a miss), it reads the block from disk, populates the cache, then sends the data to your app.
graph TD
subgraph Kernel
Cache["Page Cache\n(Read/Write Buffer)"]
InodeTable["Inode Table"]
DirEntries["Directory Entries"]
Disk[(Disk)]
end
subgraph User
App["Application"]
end
%% File creation via open()
App -- open file O_CREAT --> Kernel
Kernel -->|1 Allocate inode| InodeTable
Kernel -->|2 Add directory entry| DirEntries
Kernel -->|3 Return file descriptor| App
%% Write flow
App -- write fd data --> Cache
Cache -- "mark dirty\nupdate inode size" --> InodeTable
Cache -- schedule flush --> Disk
%% Read flow
App -- read fd --> Cache
Cache -- "cache miss?\nread block" --> Disk
Disk --> Cache
Cache --> App
Unpacking Parent-Child Communication with UNIX Pipes
Imagine our Parent process wants to send a message to its future Child. Hereβs how they set up and use their private βpost officeβ via pipes:
graph TD
A["pipe(fd)\nCreates fd0=read, fd1=write"]
A --> B["check error\nif <0 handle"]
B --> C["fork()\nDuplicates fds"]
C --> P[Parent]
C --> Cc[Child]
P --> P1["close(fd0)\nClose read end"]
P1 --> P2["write(fd1, data)\nWrite to pipe"]
P2 --> P3["close(fd1)\nSignal EOF"]
P3 --> P4["wait(NULL)\nReap child"]
Cc --> C1["close(fd1)\nClose write end"]
C1 --> C2["read(fd0, buf)\nRead from pipe"]
C2 --> C3["close(fd0)\nClose read end"]
Hereβs the play-by-play, guided by your diagram:
A["pipe(fd)"]
- The Initial Setup:- Before
fork()
, theParent
process callspipe(fd)
. This is like creating a secret tunnel within the kernel. - The
fd
array now holds two special numbers (file descriptors):fd[0]
is the entrance to read from the tunnel, andfd[1]
is the exit to write into it. Crucially, this pipe is unidirectional β data only flows fromfd[1]
tofd[0]
.
- Before
C["fork()"]
- Duplicating the Channel:- Next, the
Parent
callsfork()
. This is where the magic really happens for inter-process communication! fork()
creates an exact clone of theParent
β ourChild
process. Importantly, both processes now inherit their own copies offd[0]
andfd[1]
, all pointing to the same underlying kernel pipe. Itβs like both parent and child suddenly have keys to the same secret tunnel.
- Next, the
Streamlining the Communication (Parentβs Role):
P1["close(fd0)"]
- Parent Closes its Read End: TheParent
decides it only wants to write to the child. So, to keep things clean and prevent potential deadlocks, it immediatelyclose(fd[0])
β its own read end of the pipe. If the parent keptfd[0]
open and tried to read from it while the child was reading, things could get sticky!P2["write(fd1, data)"]
- Parent Sends the Message: Now, theParent
confidentlywrite()
s its data usingfd[1]
. This data flows into the kernelβs pipe buffer.P3["close(fd1)"]
- Parent Signals End-of-Message: Once theParent
is done writing, itclose(fd[1])
. This is crucial! Closing the write end signals to theChild
(the reader) that there will be no more data coming. When theChild
tries toread()
from an empty pipe where all write ends are closed,read()
will return0
, indicating End-Of-File (EOF).P4["wait(NULL)"]
- Parent Waits for Child: Finally, theParent
callswait(NULL)
to pause its own execution until itsChild
finishes. This reaps the childβs resources and ensures proper process cleanup.
Receiving the Message (Childβs Role):
C1["close(fd1)"]
- Child Closes its Write End: Similarly, theChild
knows it only needs to read from theParent
. So, it immediatelyclose(fd[1])
β its own write end. This prevents it from accidentally writing to the pipe (which it shouldnβt) and, more importantly, ensures that when theParent
closes its write end, itβs the only writer closing the last writer, correctly signaling EOF.C2["read(fd0, buf)"]
- Child Listens: TheChild
thenread()
s fromfd[0]
. It will happily pull data byte-by-byte (or in chunks) from the kernel buffer until it hits that EOF signal.C3["close(fd0)"]
- Child Finishes Up: Once theChild
has read all the data and received EOF, it closes itsfd[0]
, releasing its end of the pipe.
In essence, pipe()
sets up the channel, fork()
gives both processes access, and then careful closing of unused file descriptors (fd[0]
for the writer, fd[1]
for the reader) defines the direction and ensures proper EOF signaling, allowing for smooth, one-way communication!
Itβs a neat ballet of system calls, right? The close()
calls are perhaps the most vital step after fork()
to prevent headaches! What do you think, does this clear up the diagramβs flow for two processes talking?
Alright, letβs dive into the fascinating world of pipes, both the ones that pop up and vanish, and the ones that stick around! Itβs all about how processes whisper secrets to each other.
π¬οΈ Unnamed Pipes (Anonymous Pipes)
Think of an unnamed pipe as a temporary, direct telephone line set up exclusively between two related individuals β usually a parent and a child. Once the call is over (or one hangs up), the line is gone.
Theory & How They Work:
- Nature: A unidirectional byte stream, existing solely within the kernelβs memory. Itβs truly βanonymousβ because it doesnβt have a name in the filesystem.
- Relation: Designed for communication between processes that share a common ancestor (typically parent-child, or siblings created by the same parent after the pipe). This is crucial because they inherit the pipeβs file descriptors.
- Creation: You conjure them into existence using the
pipe()
system call in a single process.int fd[2]; // An array to hold two file descriptors if (pipe(fd) == -1) { /* error handling */ } // fd[0] is the read end, fd[1] is the write end
- Mechanism:
- When
pipe(fd)
is called, the kernel allocates a small, fixed-size buffer (e.g., 64 KiB) in memory. It then assignsfd[0]
(read end) andfd[1]
(write end) to the current process, both pointing to this kernel buffer. - The
fork()
system call is then used. The child process inherits copies of all open file descriptors from the parent, includingfd[0]
andfd[1]
. Now both parent and child have access to the same underlying kernel pipe. - Crucial Step: Closing Unused Ends:
- If the parent wants to write and the child wants to read: The parent closes
fd[0]
(its read end), and the child closesfd[1]
(its write end). - Data written to
fd[1]
by the writer is buffered by the kernel and can be read fromfd[0]
by the reader.
- If the parent wants to write and the child wants to read: The parent closes
- FIFO Order: Data flows strictly First-In, First-Out.
- Blocking Behavior:
read()
blocks if the pipe is empty.write()
blocks if the pipe is full.
- EOF: When all write ends of a pipe are closed, a subsequent
read()
on the read end will return 0 bytes, signaling End-Of-File.
- When
- Lifetime: The pipeβs existence is tied to the processes. It evaporates once all processes that inherited its file descriptors have closed them or terminated.
Diagram:
graph TD
subgraph UserSpace
P_Before_Fork["Parent Process\nBefore fork"]
P_After_Fork["Parent Process\nAfter fork"]
C_After_Fork["Child Process\nAfter fork"]
end
subgraph KernelSpace
KernelPipe["Kernel Buffer\n64 KiB FIFO"]
end
P_Before_Fork -->|1 pipe fd creates| KernelPipe
P_Before_Fork -->|fd0 read end| KernelPipe
P_Before_Fork -->|fd1 write end| KernelPipe
P_Before_Fork -->|2 fork copies FDs| P_After_Fork
P_Before_Fork -->|2 fork creates child| C_After_Fork
P_After_Fork -->|fd0 read end| KernelPipe
P_After_Fork -->|fd1 write end| KernelPipe
C_After_Fork -->|fd0 read end| KernelPipe
C_After_Fork -->|fd1 write end| KernelPipe
P_After_Fork -->|3a close fd0| P_After_Fork
C_After_Fork -->|3b close fd1| C_After_Fork
P_After_Fork -->|4 write fd1 data| KernelPipe
KernelPipe -->|5 data buffered| KernelPipe
KernelPipe -->|6 read fd0 buf| C_After_Fork
P_After_Fork -->|7 close fd1| P_After_Fork
KernelPipe -->|8 EOF signal| C_After_Fork
C_After_Fork -->|9 close fd0| C_After_Fork
- Flow: The parent first creates the pipe, getting two file descriptors. When
fork()
happens, both parent and child get copies of these FDs, all pointing to the same kernel buffer. Each then closes the end they donβt need, establishing a one-way channel. Data flows from the writerβsfd[1]
into the buffer, and out through the readerβsfd[0]
.
π·οΈ Named Pipes (FIFOs)
Now, imagine you want to leave a message in a specific mailbox that anyone can access by its address, not just family. Thatβs a named pipe, often called a FIFO (First In, First Out). Itβs a special kind of file on the filesystem that acts like a pipe.
Theory & How They Work:
Nature: A unidirectional byte stream that has a name and exists as an entry in the filesystem. Itβs a βspecial file typeβ (like a directory or a device file), not a regular file that stores data.
Relation: The superpower of named pipes is enabling communication between unrelated processes. They donβt need to share a common ancestor or inherit file descriptors. Any process that knows the FIFOβs name can open it.
Creation:
- Using the
mkfifo()
system call in C:#include <sys/types.h> #include <sys/stat.h> if (mkfifo("/tmp/my_fifo", 0666) == -1) { /* error handling */ } // Creates a FIFO special file named /tmp/my_fifo
- Using the
mkfifo
shell command:mkfifo /tmp/my_fifo
- Using the
Mechanism:
Filesystem Entry:
mkfifo
creates a file-like entry in the directory structure. This entry itself doesnβt store data; itβs a pointer to where the kernel will set up the actual pipe buffer.Opening the FIFO: Processes communicate by opening this special file. One process opens it for writing (
O_WRONLY
), and another opens it for reading (O_RDONLY
).// Writer Process int write_fd = open("/tmp/my_fifo", O_WRONLY); // Reader Process int read_fd = open("/tmp/my_fifo", O_RDONLY);
Rendezvous Point: This is a key difference. An
open()
call on a FIFO typically blocks until another process opens the opposite end. For example, if a process opens a FIFO for writing, it will pause until another process opens that same FIFO for reading. This ensures both ends are ready for communication.Kernel Buffer: Once both ends are opened, the kernel effectively establishes an in-memory buffer, just like with an unnamed pipe. Data written by the writer flows into this buffer, and the reader pulls it out.
Behavior: Reads, writes, blocking, FIFO order, and EOF signaling (when the last writer closes its end) behave exactly like unnamed pipes.
Lifetime:
- The filesystem entry for the FIFO persists until itβs explicitly deleted with
rm
(or the filesystem containing it is removed/reformatted). - The actual pipe buffer in kernel memory is created when processes open both ends and disappears when the last processes close their ends.
- The filesystem entry for the FIFO persists until itβs explicitly deleted with
Diagram:
sequenceDiagram
autonumber
participant P as Parent
participant K as Kernel
participant C as Child
P->>K: pipe(fd) %% #1
K-->>P: returns fd[0], fd[1]
P->>P: fork() %% #2
alt In Parent
P->>P: close(fd[0]) %% #3a
P->>K: write(fd[1], data) %% #4
K->>K: buffer data %% #5
P->>P: close(fd[1]) %% #6a
else In Child
C->>C: close(fd[1]) %% #3b
C->>K: read(fd[0], buf) %% #5
alt Kernel has no more data
K-->>C: EOF %% #6b
end
C->>C: close(fd[0]) %% #7
end
- Flow: The FIFO file is first created on the filesystem. Then, the
WriterApp
opens it for writing andReaderApp
opens it for reading. Theseopen()
calls act as a rendezvous point, blocking until the opposite end is also opened. Once both are open, the kernel sets up the temporary in-memory buffer, and data flows from the writer, through the buffer, to the reader. The filesystem entry remains even after communication stops.
Key Differences & When to Use Which:
| Feature | Unnamed Pipes (Anonymous) | Named Pipes (FIFOs) | | :-------------- | :---------------------------------------------------------- | :------------------------------------------------------------------ | ----------------------------------------------------------------- | | Relation | Only between related processes (parent/child, siblings) | Between unrelated processes | | Creation | pipe()
system call | mkfifo()
system call or mkfifo
shell command | | Name | No name (exists only via inherited file descriptors) | Has a name in the filesystem (e.g., /tmp/my_fifo
) | | Access | Inherited file descriptors | open()
the filesystem path by name | | Persistence | Ephemeral; disappears when processes close FDs / terminate | Filesystem entry persists until rm
; buffer is ephemeral | | Rendezvous | No explicit rendezvous; fork()
does the sharing | open()
calls block until both ends are open (explicit rendezvous) | | Use Case | Simple parent-child communication (ls | grep
) | Client-server, shell scripts, communication between any processes |
Choosing between them is usually straightforward:
- If you need simple, one-way communication strictly within a family of processes that youβre managing with
fork()
, go for unnamed pipes. Theyβre lightweight and ideal for that scenario. - If you need any process to talk to any other process, especially if they werenβt spawned by the same parent, or if you want to use them in shell scripts, named pipes are your go-to. They provide that persistent βmailboxβ address.
Both are incredibly powerful and fundamental building blocks for inter-process communication in UNIX-like systems! This is super valuable for your OS class! Let me know if anything here sparked more curiosity!