Linux Internals - 2: How Inter-Process Communication (IPC) works in Linux

Aman KumarAman Kumar
9 min read

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 -

  1. Pipes - named & unnamed

  2. Message Queues

  3. 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.

  1. pipe(fd) - This call initializes fd, an array of two integers that represent the file descriptors for the pipe.

  2. The fork() function is called to create a new process. The current process (the parent) creates a child process.

  3. fork() returns 0 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).

  4. Inside the child process (where fork() returns 0):

    • The reading end of the pipe (fd[0]) is closed using close(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 using write(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 using close(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;
      }
    
    1. 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.

    2. 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.

    3. 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;
      }
    
    1. Open the Named Pipe: The open function is used to open the named pipe for reading.

    2. Read from the Pipe: The read function retrieves the message from the pipe into the buffer.

    3. Display the Message: The message is printed to the console.

    4. 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;
}
  1. 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 like strcpy.

  2. 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.

  3. 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.
  4. Create the Message Queue:

    • msgid = msgget(key, 0666 | IPC_CREAT); creates a message queue with the given key. The permissions 0666 allow read and write access for everyone. The flag IPC_CREAT creates the queue if it doesn’t already exist.
  5. 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 the msg_text field of the message structure.

  6. 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 and sizeof(message) specifies the size of the message being sent.
  7. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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!

0
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.