Linux Internals - 2: How Inter-Process Communication (IPC) works in Linux
Inter-process communication (IPC) in Linux is essential for processes to exchange data and synchronize their actions, enabling more complex and modular applications. Several IPC methods exist in Linux, including pipes, message queues, and shared memory.
Here in this blog, I will be talking about -
Pipes - named & unnamed
Message Queues
Shared Memory
Pipes
Unnamed Pipes
Unnamed pipes are a simple way for processes to communicate. Imagine a parent process that starts a child process and needs to send it a message or data—like instructing a helper. Unnamed pipes work well here because they only connect two related processes (like a parent and child).
Think of unnamed pipes as a one-way tunnel between two processes. Data can only go in one direction at a time, from one end of the pipe to the other. You create a pipe using the pipe()
function, which gives you two file descriptors(a small integer value used to represent open files or resources in a process): one for writing and one for reading. The child writes into one end of the pipe, and the parent reads from the other.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd[2]; // The fd array will contain the two file descriptors
pipe(fd); // This function will create a pipe
if (fork() == 0) { // Code for the child process
close(fd[0]); // Close the reading end
char msg[] = "Hello from child";
write(fd[1], msg, strlen(msg) + 1); // Write to the pipe
close(fd[1]);
} else { // Parent process
close(fd[1]); // Close the writing end
char buffer[100];
read(fd[0], buffer, 100); // Read from the pipe
printf("Parent received: %s\n", buffer);
close(fd[0]);
}
return 0;
}
So let’s go through the code line by line so that you can understand what exactly happens here.
pipe(fd)
- This call initializesfd
, an array of two integers that represent the file descriptors for the pipe.The
fork()
function is called to create a new process. The current process (the parent) creates a child process.fork()
returns0
to the child process and the child's PID (process ID) to the parent process. This is how we can differentiate between the two processes in the code. (I talked elaborately about process creation in my previous Lunix Internal Blog).Inside the child process (where
fork()
returns0
):The reading end of the pipe (
fd[0]
) is closed usingclose(fd[0])
. This is because the child will only write to the pipe.A message
"Hello from child"
is defined and written to the pipe usingwrite(fd[1], msg, strlen(msg) + 1)
. The+1
is added to include the null terminator (\0
) at the end of the string.Finally, the writing end (
fd[1]
) is closed.
In the parent process (where fork()
returns a non-zero value):
The writing end of the pipe (
fd[1]
) is closed usingclose(fd[1])
since the parent will only read from the pipe.A buffer is created to hold the incoming message. The parent reads from the pipe using
read(fd[0], buffer, 100)
, which attempts to read up to 100 bytes of data from the reading end.The received message is then printed to the console with
printf("Parent received: %s\n", buffer)
.Finally, the reading ends (
fd[0]
) is closed.
Hence the Ouput received will be this.
Parent received: Hello from child
Although this is ideal for sharing data between processes that share a relationship, it only works in one direction. Also, two unrelated processes cannot communicate using this. Hence, the named pipes.
Named Pipes
Named pipes (also called FIFOs) allow communication between unrelated processes. Imagine two programs that don’t know about each other in advance but still need to exchange information. Named pipes help here by creating a special file that both programs can access to communicate.
You can create a named pipe in Linux using the mkfifo
command. After it’s created, any process can read from or write to this “file” (which behaves like a pipe) to exchange data.
Create a Named Pipe
This command creates a named pipe called
my_fifo
in the/tmp
directory. You can name your pipe anything you want, but it's a common convention to place it in a temporary directory.mkfifo /tmp/my_fifo
Use the named pipe in C to write
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("/tmp/my_fifo", O_WRONLY); // Open pipe for writing write(fd, "Hello through named pipe", 25); // Write to pipe close(fd); // Close pipe after writing return 0; }
Open the Named Pipe: The
open
function is used to open the named pipe for writing. If the pipe doesn't exist, this call will block until another process opens the pipe for reading.Write to the Pipe: The
write
function sends a message ("Hello through named pipe") to the pipe. The second parameter is the string, and the third parameter is the number of bytes to write. Here,25
is the length of the string including the null terminator.Close the Pipe: After writing, the pipe is closed with
close(fd)
to release the file descriptor.
Use the named pipe in C to read
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { char buffer[100]; int fd = open("/tmp/my_fifo", O_RDONLY); // Open pipe for reading read(fd, buffer, sizeof(buffer)); // Read from pipe printf("Received: %s\n", buffer); // Print the received message close(fd); // Close pipe after reading return 0; }
Open the Named Pipe: The
open
function is used to open the named pipe for reading.Read from the Pipe: The
read
function retrieves the message from the pipe into thebuffer
.Display the Message: The message is printed to the console.
Close the Pipe: Finally, the pipe is closed.
Output
Received: Hello through named pipe
Here it allows unrelated processes to communicate with each other, as the pipe exists as a persistent file in the filesystem.
Message Queues
Message queues allow for more organized communication where multiple processes may send or receive messages. Imagine having a mailbox that different people can leave letters in; each letter has an identifier for the recipient. The receiver can check the messages, look at their ID, and decide which ones to read first. This flexibility makes message queues ideal for scenarios where processes need asynchronous, or out-of-order, communication.
You can create and use message queues in Linux with a unique key to identify the queue, and messages are sent with a message type, which lets you filter or prioritize them.
Here is an example of sending a message:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msg_buffer {
long msg_type;
char msg_text[100];
} message;
int main() {
key_t key;
int msgid;
key = ftok("progfile", 65); // Generate unique key
msgid = msgget(key, 0666 | IPC_CREAT); // Create message queue
message.msg_type = 1;
strcpy(message.msg_text, "Hello from message queue");
msgsnd(msgid, &message, sizeof(message), 0); // Send message
printf("Message sent: %s\n", message.msg_text);
return 0;
}
Include Necessary Headers:
#include <sys/ipc.h>
: This header provides definitions for key management.#include <sys/msg.h>
: This header includes the definitions needed for message queue operations.#include <string.h>
: This header allows the use of string manipulation functions likestrcpy
.
Define the Message Structure:
A structure
msg_buffer
is defined to hold the message. It includes:long msg_type
: A type identifier for the message (used to categorize messages).char msg_text[100]
: An array to hold the actual message text.
Generate a Unique Key:
key = ftok("progfile", 65);
generates a unique key based on a specified file and an integer identifier. The file (progfile
) should exist; if not, the program will fail. This key is used to identify the message queue.
Create the Message Queue:
msgid = msgget(key, 0666 | IPC_CREAT);
creates a message queue with the given key. The permissions0666
allow read and write access for everyone. The flagIPC_CREAT
creates the queue if it doesn’t already exist.
Prepare the Message:
message.msg_type = 1;
sets the message type. You can send multiple types of messages, and they can be received based on their type.strcpy(message.msg_text, "Hello from message queue");
copies the actual message into themsg_text
field of themessage
structure.
Send the Message:
msgsnd(msgid, &message, sizeof(message), 0);
sends the message to the queue. The&message
argument passes the address of the message structure andsizeof(message)
specifies the size of the message being sent.
Print Confirmation:
- The program prints the message that has been sent to confirm that the operation was successful.
Message sent: Hello from message queue
How Message Queues Work
Message Queue Creation: When the program runs, it creates a message queue using a unique key. This queue can be accessed by other processes using the same key, enabling them to send or receive messages.
Sending Messages: The
msgsnd
function adds the message to the end of the queue. If the queue is full, the sending process will block until space is available (unless it is configured to not block).Message Types: The use of message types allows for the categorization of messages, which can be useful if multiple processes are sending different types of messages to the same queue.
Shared Memory
Shared memory is one of the fastest methods of inter-process communication (IPC) in Linux, allowing multiple processes to access the same region of memory. This enables them to exchange data directly, avoiding the need for explicit message-passing mechanisms. This technique is particularly beneficial for applications that demand high performance and low latency.
How Shared Memory Works
Creation of Shared Memory: In Linux, shared memory is created using system calls that generate a unique key, which identifies the memory segment. This key is essential for processes to access the same shared memory.
Attaching to Memory: Once the shared memory segment is created, processes can attach to it, mapping it into their address space. This means that the processes can access the memory as if it were part of their data, allowing for efficient communication.
Data Exchange: With shared memory, processes can read from and write to the same memory location directly. This direct access makes shared memory one of the fastest IPC methods available, as there’s no overhead involved in copying data between processes.
Detaching and Cleanup: After completing the communication, processes can detach from the shared memory. Additionally, the shared memory segment must be explicitly removed when it is no longer needed, which helps in managing system resources and preventing memory leaks.
In practice, developers use shared memory to achieve fast data exchanges without the overhead of copying data between user and kernel space, making it an essential tool for optimizing performance in concurrent programming environments. By leveraging shared memory effectively, developers can create responsive applications that handle data-intensive tasks with ease.
Conclusion
Each of these IPC methods in Linux has strengths that suit different scenarios. Whether you need straightforward data transfer between related processes or fast communication for handling large datasets, getting comfortable with these IPC techniques can truly elevate your applications. They empower you to create software that’s not just efficient but also responsive to user needs. So, dive into these tools and discover how they can enhance the way your processes interact in your Linux projects!
Subscribe to my newsletter
Read articles from Aman Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Aman Kumar
Aman Kumar
Konnichiwa! I'm Aman Kumar, a passionate developer, competitive programmer, and AI/ML enthusiast currently studying at the Indian Institute of Technology, Indore. Delving into Next.js, I'm broadening my web development horizons. Through regular participation in competitive programming contests, I continuously refine my problem-solving skills. As a web developer, I leverage my expertise to craft innovative solutions and deliver impactful digital experiences.