 lecture in the course design and engineering of computer systems. So in the previous lecture, we have seen how different threads in a single process can coordinate with each other in order to do some work. Now in this lecture, we are going to study inter-process communication that is we are going to see how different processes in a single system can communicate with each other and coordinate with each other. So the question comes up, if you know you have application logic that has to be done in one machine, why do you need multiple processes, you know why cannot you just do all the work in a single process and use multiple threads. So there are many reasons why we use multiple processes instead of multiple threads, you know for example, it could be the case that these different processes were developed independently by different teams or by different companies or so on. You might also want to use different programming languages, different frameworks for different tasks. So therefore, in a single system, in a single computer, you might have multiple processes that are doing the same work of the same application and have to somehow coordinate with each other, you know. One process might have to do some work, then the other process may have to do something else, you know some mechanism for coordination may be needed. So those mechanisms for coordination and communication between processes are called inter-process communication or IPC mechanisms and these are available via the operating system or other libraries. So just like you have mechanisms for different threads in a process to synchronize with each other like for example, condition variables, similarly IPC mechanisms are used for different processes to synchronize with each other. So let us see an example of where this is needed, where you have multiple processes to do different tasks of an application. So if you look at a web application, a web server or something that is receiving requests from users and has to handle those requests, then such web applications usually in a single system they are composed of multiple processes. For example, you can have a web server process that is actually, you know, getting HTTP requests from clients and sending out HTTP responses, okay. So the main thing in a web application is you will have a web server process and this process can have multiple threads, it can be using, you know, TCP or if it is a secure server TLS sockets, sending and receiving HTTP requests, responses and so on. And you know, you can have multiple threads, it can be written in a language like CRC++ for high performance. And of course if the request is just for a file, the web server can simply read the file and return the response. But if you have dynamic requests where, you know, the user has provided some parameters and you have to do some work, you know, run some functions and return a response, dynamic response, maybe this web server process may not be able to, you know, do all of that work and it might use another process on the same system that is called an application server. So, you know, for some specific task there is some application server, okay. So this web server will send these dynamic requests to the application server. The application server will send the responses back and the web server will then pass them on to the client. You can have such a situation, okay. And this application servers, you can write them in like scripting languages, you know, like Python or something where these scripting languages are not very fast but they are very good for text parsing and text manipulation, you know, parse an HTTP request, extract various parameters, you know, get the information corresponding to the request, generate, construct a HTTP response, all of that may be easy to do in scripting languages. So you might use a write your application server process in some other language, that is also possible. Now this application server may want to, you know, get or store data in some database, right. Because database could be a separate process that you get from some vendor or some open source software, something you get and you install it and this application server has to send request to this database and get responses back. Again you need some IPC mechanism here or this, you know, you might have a remote database somewhere else and this could just be a database client, you talk to this client, this client will talk to some other database over the network, get back responses, okay. All of this also can be possible. So overall there are multiple components here, you cannot build all of this in a single process, you know, you have different databases written by somebody else, this application logic is written by somebody else, web server you get from somewhere else, right. All of these things have to, different processes have to communicate with each other. So normally when you do, you know, use any application frameworks to build web application, there are many web application frameworks available out there, you know, for example, Python, Django, React, Node.js, you might have heard some of these names. These are all frameworks that make it easy for you to build web applications. So what these frameworks do is, they do all of the setting up multiple processes, setting up the IPC mechanisms between these processes, they do it for you. So you may not realize that these things are happening under the hood, but any system, any computer system component that is running on a single machine will most likely have multiple processes for different purposes and you will need some mechanism to, you know, send the request, get responses back across these different components, across these different processes in one system, okay. So what are these IPC mechanisms that are available? There are many, we will briefly see a few popular ones in this lecture. So one is Unix domain sockets, right, we have already studied this. We have seen how processes can open sockets and you read and you send message into a socket, it can be read from the other socket, you send a message here, it can be received here. You know, you can connect two different sockets and exchange messages with each other. We have already seen this in the context of, you know, networking and the same concept applies within a system also. So you can open TCP, UDP sockets to communicate across machines, but on the same machine you can use Unix domain sockets, local sockets to communicate between processes on the same machine. So of course, for Unix domain sockets if you write something into a socket, it will not be put in a packet and send over the network, it will be stored in some, you know, buffer inside the OS and it can be read from the other process from this buffer, okay. So transmitted messages are stored in socket buffers and read by the receiving process. There are no making packets, adding headers, all of that does not apply here. So this is something we have seen before, the system calls and all of that. So there are also other mechanisms for IPC besides Unix domain sockets. One is what is called message queues, okay. So this is almost like a mailbox, you know, one process, P1 will post some message into some mailbox and another process P2 can, you know, contact this mailbox and get this message. In this way P1 puts messages, P2 will read messages from the mailbox, that is the concept of message queues. Then you have pipes, pipes is a unidirectional channel of communication that is one process will write into a pipe, you know, write some, push some bytes into a pipe, another process will read from this pipe. That is the concept of pipes. The other concept is of signals, you know, you can send some specific messages, standard set of messages to processes. For example, when you press Ctrl C on the keyboard, an interrupt signal called SIGINT is sent to the process. Similarly, there are many different type of signals defined, of course, you cannot send any arbitrary signal, you know, there the OS will give you, okay, these are the 20, 30, whatever number of signals, you can send one of these signal to one of the processes in your system. Then you have shared memory, that is normally a process has some, you know, virtual address space and all of these pages are mapping to some physical memory. What you can do is for two different processes, you can map the same physical memory frame into the address spaces of two different processes, so that when this process writes to these addresses or this process writes to these addresses, it is written into the same physical memory. So they can address access the same memory and you know, put some data here and read and write and so on, that is the concept of shared memory, okay. So these mechanisms are all different, so sockets, message queues, pipes, they are somewhat similar, but these two are very different signals and shared memory and they are useful in different scenarios, okay. So what we will do now is we will try to go over these in a little bit more detail, sockets I will not cover because we have studied them enough, but the other mechanisms I will go into a little more detail, so that when you are designing your application that has multiple different processes, if you understand each of these mechanisms in some detail, you can decide, oh, in this situation this mechanism is suitable, in this situation some other mechanism is suitable and so on, okay. So let us study this in a little bit more detail. So first message queues, okay. So message queues are used to exchange messages, you can think of it like a mailbox, okay. So what one process can do is it can open a connection to a message queue, you will get back some identifier to talk to a message queue. So this is again like a file descriptor, you know, you will get back some handle to a message queue, there can be multiple message queues in your system that are all identified by different keys. So the sender will open a message queue with a certain key and it will send a message on that message queue. The receiver can also open the same mailbox and receive the message from that message queue, okay. When you send a message you are giving some set of bytes, they will be stored inside the message queue and whoever some other process does receive, those bytes will be passed on to this receiving process. And in the interim when the sender sends, you know, the sender sends a message into the message queue and the receiver will receive from the message queue. In the interim, before this receiver process does this receive, the message will be stored in the message queue and some, you know, memory inside the OS, whichever, whoever is implementing these send receive system calls, there the message will be stored temporarily, okay. And these message queues can be used to do all the IPC in the web application that, you know, we have seen before. For example, your web server is getting request from clients and it will put these HTTP requests into some message queue, okay. And the application server will fetch request from this message queue and handle them and whenever there is a response, it will put that response into probably another message queue and the web server will read these responses and send them back to the client. Similarly, the application server may post, you know, hey, I want this data, it can post that to a message queue to the database and this database can, you know, run this SQL query or whatever and return back the responses in the app server can use these responses. So, you can have one or more message queues in the system in this way to communicate to pass messages across all of these different processes in your application. Of course, this is one design, you can have many different such types of designs. So the next IPC mechanism is what is called a pipe, okay. A pipe is a unidirectional P4 channel. What does it mean? You can put bytes into the pipe and in P4 order, the bytes will be read from the other side, okay. So how do you create these pipes? These pipes are created with this pipe system call. What this pipe system call will do is it will return two file descriptors. So if you remember, every process has a file descriptor array which points to, you know, the inode or which points to the open file table, which will point to the inode or socket buffer or whatever, okay. Many things can be created and managed using these file descriptors. Now when you open a pipe, when you create a pipe, what is happening is two file descriptors are created, one file descriptor is used to write into this pipe buffer and another file descriptor is used to read from this pipe buffer, okay. So when you create a pipe, the two file descriptor values are filled in and using one file descriptor, you can read messages from this pipe buffer using the other file descriptor, you can write messages into the pipe buffer. So you write here, you can read from a file descriptor. So normally when you read from a file descriptor, you are reading from a file or socket or something, but here you are actually reading from the pipe buffer. Similarly, when you write also, you are storing it in this pipe buffer and this pipe buffer is of course the pointer to this pipe buffer is stored in the open file table. So given a file descriptor, you go to open file table, you can locate the pipe buffer and you can copy the message there, okay. And this is one pipe is a unidirectional communication, you can only write from one file descriptor and read from the other file descriptor. If you want bi-directional, you have to open two pipes, okay. So now you might be thinking, okay fine, I have opened a pipe, I am sending messages on one file descriptor reading from the other file descriptor in the same process. What sense does it make? Why will a process want to send messages to itself? This is not useful. So these are called anonymous pipes that are created like this within the same process. They are only useful to communicate if a process has other child processes which are all sharing the same file descriptor. So for example, you can have a situation like this, parent process has opened a pipe, okay. So there is a pipe buffer and there is a write file descriptor, there is a read file descriptor. Now this parent has forked a child and remember that the child gets a copy of everything of the parent including the file descriptor array. So the child processes file descriptors also will have, you know, one file descriptor to read from this pipe, one file descriptor to write into this pipe, okay. So the parent and child both have hooks into this pipe buffer. So now what you can do is the parent can close, say, you know, this read end and the child can close this write end. So then what do you have? The parent is writing into the pipe here and the child is reading from the pipe here, okay. Using this file descriptor, the parent writes into the pipe using this file descriptor the child reads from the pipe. So in this way, you create a pipe, then you fork some child processes and the parent and the child can communicate over these pipes. Note that these anonymous pipes are only available within a process and its descendants, okay. Because there is this pipe, you cannot refer to it by any name, there is no identifier, some other process cannot use this pipe. But if you want to communicate between unrelated processes, then you can use named pipes, okay. So here is an example of a named pipe. You create a named pipe with a certain name, you know, this can be a path name, some string. Then different processes can open, if you have a named pipe, different processes can open a write file descriptor and a read file descriptor into the same pipe. So one process can say into this pipe, give me a read file descriptor. Using this file descriptor, this process can read from this pipe. Another process can say give me a write end of this pipe, it can, you know, get a file descriptor to write into this pipe. So when one process, so these two pieces of code can be in different processes, when one process writes into a pipe, the other process can read from the pipe. So if you want to communicate between unrelated processes, that is not parent, child, such kind of relationships don't exist, some random two different processes in a system, then you can use named pipes, where one process, where both these processes open the same pipe with the same name and one of them writes into the pipe, the other will read from the pipe, okay. And if you want bi-directional communication, you will need two pipes because a pipe by itself is only unidirectional channel. So now across all of these, you know, whether it is sockets, pipes, message queues, across all of these, the concept is the same, you know, your one process is sending messages, this message is temporarily stored inside some buffer inside the OS, you know, it could be the pipe buffer, the socket buffer, the message queue, whatever, there is some memory location inside the OS where, you know, whenever a process writes a message, it is stored here and another process can read from this buffer later on using some system calls, okay. So this is the basic concept, sockets, pipes, message queues, well the syntax might be different, but how they work is at a high level the same. With of course, minor differences like, you know, sockets are bi-directional whereas pipes are unidirectional, message queues are somewhat asynchronous, you can post a message and some other process can read the message later on. So they are slight differences, but high level concept is the same. Now for all of these system calls that read or write into these sockets or pipes or message queues, you can have two variants of those system calls. The system calls can be blocking or non-blocking. For example, if you are writing into a pipe, but this maximum buffer size is filled, you know, this buffer is full, the reading end has not yet read all the data in the pipe. In that case, you know, if your buffer is full and you write into a pipe or a socket, the write or the sending can block. Similarly, if your buffer does not have any content and you read, this read can also block. That is one option or you can configure all of these system calls to be non-blocking, where you say, if I write into one of these pipes or sockets, but the buffer is full, then you can return an error instead of blocking. Similarly, when reading also, you can say, 0 bytes are available and return some value without actually reading any data. So it is possible to do this IPC either in a blocking fashion or a non-blocking fashion. It depends on the preferences of the programmer in his or her code. So the next IPC mechanism we are going to study is signals, which is significantly different from the sockets, message queues, pipes we have seen so far. Signal is a way to send some specific notifications or messages to processes. So you cannot send arbitrary data like in a pipe, you can write whatever bytes you want, but in a signal you cannot do that. You cannot send arbitrary data, but there are some specified set of standard signals that are available that are defined in an operating system and you can only send these signals between processes. And these signals can be sent between two processes or the OS and a process also. So what are these signals? So whenever you want to interrupt a process, you can send a sigint. For example, when you hit control C, the user is sending sigint to some process or processes can also send sigint to each other. Then there are other signals, you know signal to kill, signal to stop a process, there can be user can define some signals. So there are many different signals in an operating system that, but with signals, you can only send these specific messages. You cannot send a random array of bytes, you can only send some standard predefined signals. And how do you send a signal? You can use the kill system call. The kill system call can be used to send a signal, you can say you know I want to send to this process, to some process ID, I want to send this particular signal, you can specify that and you can send that using the kill system call. So one process P1 can send some signal to another process P2 using the kill system call. The system call is kind of unfortunately named. It is not just to send this kill signal, but any signal, you know not dangerous signals, but regular signals also can be sent. And of course, there are certain restrictions on you know any random process cannot kill any other random process, there are certain restrictions in place. But most signals can be sent from one process to the other. Signals can also be generated by operating systems. You know when you hit control C, whichever process is on the screen, the OS whenever it handles this keyboard event, this interrupt handler, it will send the sigint, the control C signal to whichever processes on your screen at that point of time. So signals can be sent between processes, they can also be sent by OS to processes. And what happens when a process receives a signal? So every process has a set of functions defined which are called signal handlers that say when I get this signal, I will do this work. When I get this signal for each signal, there will be a handler, some functions defined. So you may be wondering, I have never written these signal handlers, what is happening here? So it turns out that there are many default signal handlers that already exist. For every process when you create a process in the template of the process, there are certain default signal handlers defined for each process that when this signal happens do this, when this signal happens do this, when control C comes terminate, you know such signal handlers are already defined. So you as a programmer you do not have to define every single signal handler, most of them are already defined for you. And what happens when a signal arrives, the process, the program counter will jump from whatever instruction it has to run next to this signal handler. The process is executing some code, from here it will jump to this signal handler in order to handle a signal and then it will come back here if possible. So that is how signal handlers work and default handlers exist. For example, there is code to terminate a process when you get control C. But you can also override these signal handlers. A process can say when I get control C, I will do something else. I will not terminate, maybe I will print a message and terminate or I will do something else. So you can override these signal handlers. In this way when a process P1 sends a message to process P2, P2 can do some extra work, you know because it has defined what the work is in its signal handler. Of course, you cannot override all signals. There are some signals which you cannot override, which you have to execute like you know somebody the OS wants to kill you, you cannot say oh no I am not going to do that. So some signals you cannot override but for some other signals you can override the default behavior and specify your own signal handler so that the process can do some different work from the default work that is done when it receives a signal. So the next IPC mechanism is shared memory. So we have seen the basic idea before. So what is shared memory is every process it has some pages and the page table maps these pages into some physical frames, right. We have seen this before many weeks back. And two different processes in general have a different set of frames, you know that map to the same virtual addresses. So virtual address 0 can map to some physical address to this frame, this page maps to this frame for this process then virtual address 0, you know the first page maps to this frame for some other process that is possible. And in general they share different frames except for things like OS where they share the different pages point to the same physical frames. But what you can do is you can explicitly say that two different processes the virtual addresses will point to the same physical memory frame. You can do that using shared memory. So there are certain the OS can create this is a shared memory segment a special physical frame whose physical address is present in the page tables of two different processes. So you can add page table entries where this page number points to the same frame number for two different processes. Of course the virtual addresses can be different but the physical address will be a same. So how do you identify it you can create a shared memory segment using a key. So there can be many such shared memory segments each of which has a name a key and when a process says you know attach this shared memory segment into my virtual address space the OS will map this shared memory segment into a certain virtual address and return that virtual address pointer some car start pointer it will return to you. So now two different processes can attach the same shared memory segment using the same key and map it into their virtual memories and now you know two different processes can just read or write you know using this address you can put some data here this process can read from here and vice versa. Once the same memory is shared you can exchange data easily using this shared memory segment. Of course note that the shared memory by itself may not be always useful in all scenarios for example if you want to do a producer consumer with this shared memory the producer will put some data into the shared memory. How will the consumer know normally you know the consumer has to read this memory and see has it been updated has it been updated. Similarly if both processes are writing to the same data structure you can have inconsistent updates. So with shared memory there are all of these concurrency race conditions you have to take care of but shared memory along with other mechanisms can be used. For example if you want to do producer consumer with shared memory producer process puts the request here and maybe sends a signal to the consumer process then the consumer process knows the data has arrived it will read that shared memory data. So in this way so you can use some combination of all of these IPC mechanisms to do the coordination between processes. So in this lecture we have studied many different IPC mechanisms and of course there are more also that are available. So as a programming exercise try to you know write some code using any of these IPC mechanisms probably you can do like a producer consumer that we did with threads you can do that with processes where one process is producing some messages another processes you know reading that data you know you can use message queues or shared memory any of these mechanisms and understand the differences between these mechanisms which mechanism is useful in which situation. And another programming exercise you can do is you can write signal handlers in your program by default when you get control see the program terminates but you can write a new signal handler that will print a message instead of terminating right. So such things also you can understand how signals work by writing simple programs like this. So that is all I have for this lecture. Let us continue our discussion in the next lecture. Thank you.