Inter Process Communications are...

·

10 min read

Inter Process Communications are...

First off, as a disclaimer, I need to inform you that we will be talking about POSIX systems. While all operating systems have these, the implementation can be different.

As we go along I will be talking about the level of persistence. There are three levels of persistence: Process level persistence which stays alive until all processes using it terminates, Kernel level persistence which stays alive until the OS reboots, and File level persistence which is present even after a reboot and needs to be deleted explicitly.

The easiest and lowest level construct we can use is pipes. Pipes are process level persistent. Creating a pipe gives two descriptors, the first one for reading from and the second for writing into the pipe. And they can be inherited by child processes.

One simple way to use them is to have the parent process make a pipe, fork itself to create a child process, and have one process close the reading link and the other close the writing link. Now one process can talk to the other. But pipes can only accomplish one-way communication. To have two processes talk to one other we need two pipes.

You might have already used pipes before without even realizing it. The «|» operator in shell is used for chaining up otherwise separate commands together. It takes the output of the command to its right and feeds it into the command to its left as an input. Pipes are used here.

First In First Out(FIFO) is a little more advanced version of pipes. They are created in a kernel controlled subfolder so they are mostly kernel level persistent. Some OS may destroy a FIFO that hasn’t been active for a long time.

Like pipes, FIFO is a one-way communication system. Unlike pipes, FIFO cannot be inherited by child processes. Each process needs to open the connection itself. But this also means after a parent process has created the FIFO, other concurrent non-child processes can also connect and open the FIFO.

Both FIFO and pipes don’t have a mechanism behind them to save the data, so the listener process needs to actively listen and wait for incoming messages. This can cause busy waiting situations. To save us, Signals come to the rescue.

Signals are little data structures used by pretty much everything in the OS. Mouse click sends a signal and keyboard key press also sends a signal. Signals are simple yet effective. A signal can only notify the process only once. Once the signal has been executed, it is destroyed. If a process wants to be notified every time a certain event occurs, it needs to create a new signal every time the old one executes.

There are pre-defined signals you can send to a process, and most of them usually terminate the process. To send a signal to a process from the shell you can use the «kill» command and send a terminate signal you can add the «-9» argument. Or simply adding the PID of the process after the kill command also sends a terminate signal.

There are multiple ways a process can react to a signal. The process can ignore the signal, stop executing, continue executing, or simply terminate itself.

Signals wait for a certain event to occur and then run a function. To use a signal you need to create a signal yourself. Bind it to a signal number and give it a function. When the process receives the signal it will run the function.

Signals are good at notifying but they don’t transfer any data between processes. To go up one more level we need to take a look at Semaphores.

Semaphores are a lock mechanism for resources to prevent concurrent access to critical resources. Instead of processes, semaphores are usually used by threads of the same process that are working on or with the same resource. To use semaphores you need to link the «-lrt» library to your program which also implies you have also linked the «-pthread» library to the program. That should give you an idea.

A thread that wants to use the semaphore will look at the lock. The lock is a simple integer number and it cannot go below 0. If the lock is 0 that means the resource is available and the thread can acquire it. Once a thread acquires the resource, the lock becomes 1. If another thread comes along and wants to access the resource they will see that the resource is in use and the semaphore is locked. They can wait until it becomes free again, they can wait a certain time, or just move along. All threads must also unlock the semaphore when they release the resource.

Now going back to the processes and more persistent communication systems. Message Queues are the next step of the ladder. Message Queues are priority queue data structures. They are created to hold a certain number of messages and messages can have a maximum of a certain length. Both of these values can be set at creation time but cannot be modified later on.

Message Queues are at least kernel level persistent but you can implement them with file level persistence as well. Message Queues does not require the reader and the writer process to be running at the same time. A writer process can dump five messages with various priorities in random order and then terminate. Sometime later a reader process can open the Message Queue and read the messages in priority order.

Messages in the system can be destroyed whenever they are read by a process or the MQ can be implemented in such a way that until all the processes in a certain group read the message, the message lives. OS defined MQ usually destroys whenever it is read but you can make your own with a simple priority queue and a memory space to write data.

Shared Memory is the last one we will be talking about. MQ are type dependent, but Shared Memory is literally a memory space that you can freely write to and read from which doesn’t need to be type dependent. You can write a custom data structure with one process and read it with another.

At the creation time, SM will be created with the size(in bytes) you requested. You can also ask the OS to give you a certain address but acquiring it or not depends on whatever the memory space is occupied or not. Since the address cannot be changed after creation, as far as I know, leaving the address to the OS is a good idea. SM size on the other hand can e changed. But it cannot shrink, it can only enlarge. The request for shrinking will be discarded.

In summary, Pipe is one-way communication between a parent and a child process, FIFO is an advanced pipe that cannot be inherited but opened by other processes, Signals eliminate busy waiting problems of the previous two but doesn’t transfer data, Semaphores are locking mechanisms to regulate the usage of resources among threads, Message Queues are type dependent persistent priority queues, Shared Memory is a memory space that multiple processes can read from and write to.

To learn about more stuff like this, follow me on Twitter!