Interprocess Communication

code can be found https://github.com/lets-learn-it/c-learning/tree/master/17-ipc

TechniqueModelPurposeGranularityNetwork
pipe/FIFOmessage passingdata exchangebyte streamlocal
socketmessage passingdata exchangeeithereither
message queuemessage passingdata exchangestructuredlocal
shm()shared memorydata exchangenonelocal
memory mapped fileshared memorydata exchangenonelocal
signalmessage passingsynchronizationnonelocal
semaphoremessage passingsynchronizationnonelocal

Pipes

  • pipes are unidirectional
  • pipes are order preserving
  • pipes have limited capacity and they use blocking I/O. If a pipe is full, any additional writes to the pipe will block the process until some of the data has been read.
  • pipes send data as unstructured byte streams.
  • Messages that are smaller than the size specified by PIPE_BUF are guaranteed to be sent atomically. As such, if two processes write to a pipe at the same time, both messages will be written correctly and they will not interfere with each other.

It is very important to close the unused end of the pipe immediately after the fork(). Failure to do so can cause programs to freeze unexpectedly.
Example: parent process will try to read from the pipe. Instead of immediately returning, the process will block until an EOF (end-of-file) is written into the pipe. Since the child is the only other process that could write to the pipe and the child exits without writing anything, the parent will block indefinitely.

Named Pipes / FIFO

Above pipes (anonymous pipes) can't be used for unrelated processes. Specifically, the call to pipe() must happen within the same program that later calls fork().

FIFOs work by attaching a filename to the pipe thats why named pipes.

Also similar to anonymous pipes, FIFOs use blocking I/O until both ends are opened by at least one process.. For using non blocking fifo, pass the O_NONBLOCKoption during the call to open() to make the FIFO access non-blocking.

Creating FIFO

  • use mkfifo() / mkfifoat() / mknod / mknodat to create fifo
  • other process can access fifo by calling open() on associated filename. (should have permissions)

When all readers for a FIFO close and the writer is still open, the writer will receiver the signal SIGPIPE the next time it tries to write(). The default signal handler for this signal prints “Broken Pipe” and exits.


Message Queues

POSIX message queues

  • for processes to communicate by exchanging structured messages.
  • When a message is retrieved from the queue, the process receives exactly one message in its entirety; there is no way to retrieve part of a message and leave the rest in the queue.
  • message queues are special identifiers and not file descriptors.
  • message queues do not require or guarantee a first-in, first-out ordering. depends on priority.
  • Message queues have kernel-level persistence and use special functions or utilities for removing them. Killing the process will not remove the message queue.
  • POSIX message queue is only removed once it is closed by all processes currently using it.
  • message queues has asynchronous notification feature means reader do not wait for message
  • can add attributes to queue like message size or capacity of queue.

Steps

// Open (and possibly create) a POSIX message queue.
mqd_t mq_open (const char *name, int oflag, ... /* mode_t mode, struct mq_attr *attr */);

// Get the attributes associated with a given message queue.
int mq_getattr(mqd_t mqdes, struct mq_attr *attr);

// Close a message queue.
int mq_close (mqd_t mqdes);

// Initiate deletion of a message queue.
int mq_unlink (const char *name);

// Send the message pointed to by msg_ptr with priority msg_prio.
int mq_send (mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);

// Receive a message into a buffer pointed to by msg_ptr and get its priority msg_prio.
ssize_t mq_receive (mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);

Asynchronous MQ

  • by default, if queue is full, writer will block and if no messages available, reader will block
  • We can use O_NONBLOCK while opening queue in oflag bit mask. which will return error instead of blocking.
  • Or we can use mq_timedsend() and mq_timedreceive() which takes parameter abs_timeout. It will block read/write till abs_timeout
  • Most useful way is using mq_notify(). it can be used for requesting asynchronous notification when message is sent.

SystemV message queues

  • use ipcs command to list message queues and ipcrm to delete them.
#include <sys/msg.h>

// create message queue
key = ftok("./queue.txt", 'b');
msgq = msgget(key, 0666 | IPC_CREAT);

// message
// message can be any struct as long as first element is long
struct _person {
  int age;
  char name[20];
}_person;
struct my_msg {
  long mtype;
  person p;
};

// create message
struct Message message;

// while receiving, receiver will specify this id
message.msgid = 2;

strncpy(message.p.name, "Parikshit", sizeof(msg)+1);
message.p.age = 26;

// send msg
bytes = msgsnd(msgq, &message, sizeof(person), 0)

// read msg
// get message of type 2
msgrcv(msgq, &message, sizeof(message), 2, 0);

person p = (person) message.p;

// delete message queue
msgctl(msgq, IPC_RMID, NULL);

Shared memory


Sockets


Memory Mapped files

  • memory of region corresponds to a traditional file on disk.
  • no need of read(), write() or fseek() because file is already in memory.
  • Memory-mapped files allow for multiple processes to share read-only access to a common file. As a straightforward example, the C standard library (glibc.so) is mapped into all processes running C programs.
  • when we use read() with file, kernel copies data from disk to kernel's buffer cache and then copies to process's user mode memory. But memory mapped files bypass buffer cache & copies directly into user mode portion of memory.
  • If shared region is writable, memory mapped files provide extremely fast IPC data exchange.

    Setting up regions in both processes is expensive operation in terms of execution time.

  • memory mapped files create persistent IPC. in case of pipes, message wont be available when any process reads it.

Disadvantages of bypassing kernel's buffer cache

  • If 2 processes access same file using read() function, kernel's buffer will read file from disk once and copy it to user space of those respective processes.
  • So second read will be slower in case of memory mapped files because there is no kernel's buffer.

C library functions

// map a file by fd into memory at address addr
// prot is protection. discussed below
void *mmap (void *addr, size_t length, int prot, int flags, int fd, off_t offset);

// Unmap a mapped region.
int munmap (void *addr, size_t length);

// Synchronize mapped region with its underlying file.
int msync (void *addr, size_t length, int flags);

Region protections and privacy

These protections only apply to the current process. If another process maps the same file into its virtual memory space, that second process may set different protections. As such, it is possible that a region marked as read-only in one process may actually change while the process is running.

ProtectionActions Permitted
PROT_NONEThe region may not be accessed
PROT_READThe contents of region can be read
PROT_WRITEThe contents of region can be modified
PROT_EXECThe contents of region can be executed

Privacy

In flags parameter, region can be designated as private (MAP_PRIVATE) or shared (MAP_SHARED). exactly one flag is required while calling mmap(). In addition to these flags, there are multiple falgs available https://man7.org/linux/man-pages/man2/mmap.2.html

Mapping

A file is mapped in multiples of the page size. For a file that is not a multiple of the page size, the remaining bytes in the partial page at the end of the mapping are zeroed when mapped, and modifications to that region are not written out to the file.


Signals

  • Signals are software interrupts. They provide way of handling asynchronous events.

  • kill() function allows process to send any signal to another process/process group. (need to be owner of the process that we are sending signal to, or superuser).

  • process can define what to do when XYZ signal occurs using signal(int sigcode, void (*func) (int)). func can be 1. constant SIG_IGN (ignore signal) 2. constant SIG_DFL (default action) or 3. address of function.

    We can tell kernel to do one of below action

    1. Ignore the signal: This works with all signals except SIGKILL and SIGSTOP. These two signals provide kernel & superuser a way to kill or stop process.
    2. Catch the signal: We can register function with kernel. We can write function which perform clean up when SIGTERM signal occurs. Note: SIGKILL & SIGSTOP can't be caught.
    3. Let default action apply: Every signal has their own default action. Note: most signals has terminate process as default action.
  • When new process created, it inherits parent's signal disposition (action for signal). This is because child starts off with copy of parent's memory image, address of signal catching function.


Semaphores

POSIX semaphores

System V semaphores


File Locking

files can be used for IPC by locking mechanism.

  • Mandatory locking: It will prevent read() and write() to file.
  • Advisory locking: processes can still read and write from file while it's locked. Process has to check if file is locked or not.

Mandatory Locking

https://stackoverflow.com/questions/77931997/linux-mandatory-locking-for-file-locking

Advisory Locking

  • process can lock file for reading or writing. Multiple processes can lock for reading at same time.
  • When process lock file for writing, no other process can lock for reading or writing.
  • for locking, we can use either flock() or fcntl().

Lock/Unlock file

#include <fcntl.h>
struct flock fl = {
  .l_type = F_WRLCK,    /* F_RDLCK, F_WRLCK, F_UNLCK */
  .l_whence = SEEK_SET, /* SEEK_SET, SEEK_CUR, SEEK_END */
  .l_start = 0,         /* Offset from l_whence */
  .l_len = 0,           /* length, 0 mean whole file */
  // .l_pid             /* PID holding lock, F_RDLCK only. set bu kernel */
};

int fd = open("./file.txt", O_WRONLY);

// lock file
fcntl(fd, F_SETLKW, &fl);   /* F_GETLK, F_SETLK, F_SETLKW */

// unlock same region of file
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &fl);

when you open the file, you need to open it in the same mode as you have specified in the lock. If you open the file in the wrong mode for a given lock type, fcntl() will return -1 and errno will be set to EBADF.

Check if file locked

struct flock fl = { 0 };

int fd = open("../file.txt", O_WRONLY);
fcntl(fd, F_GETLK, &fl);   /* F_GETLK, F_SETLK, F_SETLKW */

if (fl.l_type == F_WRLCK) {
  printf("file is locked by process %d\n", fl.l_pid);
} else {
  printf("file not locked.\n");
}