Skip to main content

4. Inter-process-communication

4.1 Signals

  1. In the Linux system, signals are an asynchronous notification mechanism used for passing events and information between processes or between the operating system and processes. Processes can respond to external events, such as user input, hardware exceptions, or actions from other processes, through signals.

  2. User processes have three ways to respond to signals:

    • Ignore the signal: The process takes no action when the signal is received. However, there are two signals that cannot be ignored, namely SIGKILL and SIGSTOP. SIGKILL is used to immediately terminate the execution of a process, while SIGSTOP is used to pause a process's execution
    • Catch the signal: Define a signal handler function to execute a custom action when the signal is received. Applications can use system calls like signal() or sigaction() to register signal handler functions and respond to specific signals
    • Perform the default action: Linux defines default actions for each type of signal. For example, when a process receives the SIGTERM signal, Linux defaults to terminating the execution of that process. Applications can choose not to define a signal handler function and let the system perform the default action for specific signals
  3. The process from signal generation to handling includes the following steps:

    • Signal generation: Signals can be triggered by various events, such as hardware interrupts, software exceptions, or user key presses. When an event occurs, the Linux kernel automatically generates the corresponding signal and sends it to the target process
    • Signal delivery: The process generating the signal sends it to a specific target process, usually using system calls like kill() or sigqueue(). The sending process needs to know the target process's Process ID (PID) and specify the signal type and other parameters
    • Signal reception: When the target process receives the signal, the operating system checks how this process handles the signal. If the signal is blocked or ignored by the process, the operating system stores the signal in the process's signal queue, waiting for the target process to unblock or stop ignoring the signal before processing it
    • Signal handling: If the target process has not handled the signal specifically or the signal is not blocked or ignored, the operating system calls the target process's signal handler function to process the signal. Each process can set its own signal handler function, and when the process receives a signal, the operating system automatically calls the corresponding signal handler function
    • Signal handling options: Each process can set its own signal handling options, including the signal handler function, the way to block signals, and the way to ignore signals. Some signals cannot be blocked or ignored, such as SIGKILL and SIGSTOP
    • Signal priorities: Signals in Linux have priorities, where signals with lower numbers have higher priorities. For example, SIGKILL has a priority of 9, while SIGINT has a priority of 2. When a process receives multiple signals simultaneously, the operating system prioritizes signals based on their priorities to decide which signal to handle first
    • Default signal handling: For each signal type, the Linux kernel defines a default handling action. For example, for the SIGINT signal (usually generated by the user pressing Ctrl+C on the terminal), the default action is to terminate the target process. However, processes can set their own signal handling options, including defining signal handler functions and blocking or ignoring signals, by calling functions like sigaction
  4. If you want to view the signals already defined in Linux, enter the following in the terminal:

    linaro@linaro-alip:~$ kill -l
    1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
    6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
    11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
    16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
    21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
    26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
    31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
    38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
    43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
    48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
    53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
    58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
    63) SIGRTMAX-1 64) SIGRTMAX
  5. Commonly used signals in the system:

    SignalSignal ValueDefault ActionDescription
    SIGHUP1TerminateHangup signal, the controlling terminal is closed
    SIGINT2TerminateInterrupt signal (Ctrl-C)
    SIGQUIT3Terminate with core dumpQuit signal (Ctrl-)
    SIGILL4Terminate with core dumpIllegal instruction
    SIGTRAP5Terminate with core dumpTrace trap
    SIGABRT6Terminate with core dumpAbort signal from abort() function
    SIGBUS7Terminate with core dumpBus error
    SIGFPE8Terminate with core dumpFloating-point arithmetic error
    SIGKILL9TerminateKill signal, unblockable
    SIGUSR110TerminateUser-defined signal 1
    SIGSEGV11Terminate with core dumpInvalid memory reference
    SIGUSR212TerminateUser-defined signal 2
    SIGPIPE13TerminateWrite on a pipe with no reader
    SIGALRM14TerminateAlarm clock signal
    SIGTERM15TerminateSoftware termination signal, can be caught by the process
    SIGSTKFLT16Terminate (b)Stack fault
    SIGCHLD17IgnoreChild process has terminated
    SIGCONT18IgnoreContinue executing, if stopped
    SIGSTOP19StopStop the process
    SIGSTP20StopStop the process (Ctrl-Z)
    SIGTTIN21StopBackground process attempting read from terminal
    SIGTTOU22StopBackground process attempting write to terminal
    SIGURG23IgnoreUrgent condition on socket
    SIGXCPU24Terminate with core dumpCPU time limit exceeded
    SIGXFSZ25Terminate with core dumpFile size limit exceeded
    SIGVTALRM26TerminateVirtual timer expired
    SIGPROF27TerminateProfile timer signal
    SIGWINCH28IgnoreWindow size change
    SIGIO29Terminate (a)I/O is available for asynchronous system calls (Ctrl-C)
    SIGPWR30TerminatePower failure
    SIGSYS31Terminate with core dumpBad system call

4.1.1 Sending Signals

Functions for sending signals mainly include kill(), raise(), alarm(), and pause().

  1. kill() Function Declaration:

    #include <sys/types.h>
    #include <signal.h>
    int kill(pid_t pid, int sig);
    • The pid parameter specifies the process ID of the recipient of the signal
    • The sig parameter specifies the type of signal to be sent
    • Example of kill() Function:
    #include <signal.h>
    #include <stdlib.h>

    int main(int argc,char *argv[])
    {
    pid_t pid;
    int sig;

    if(argc < 3){
    printf("Usage:%s <pid_t> <signal>\n",argv[0]);
    return -1;
    }
    // Convert strings to integers
    sig = atoi(argv[2]);
    pid = atoi(argv[1]);

    kill(pid,sig);

    return 0;
    }
    • Compilation and Execution:
    gcc kill.c -o kill
    ./kill
  2. The raise() function allows a process to send a specified signal to itself. Function declaration:

    #include <signal.h>
    int raise(int sig);
    • The sig parameter specifies the type of signal to be sent.
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <signal.h>
    #include <stdlib.h>

    int main(void)
    {
    printf("raise before\n");

    raise(9);

    printf("raise after\n");


    return 0;
    }
    • Execution Result:
    pi@raspberrypi:-/Linux/signal $./raise
    raise before
    Killed
    • The program prints "raise before," uses the raise() function to send a stop signal, and then the process terminates.
  1. The alarm() function can send a SIGALRM signal to the current process after a specified time. Function declaration:

    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
    • The seconds parameter specifies the number of seconds for the timer. The alarm() function will send a SIGALRM signal to the current process after the specified seconds.
    • Example of alarm() Function:

      #include <stdio.h>
      #include <unistd.h>
      #include <signal.h>

      void handler(int sig) {
      printf("Received signal %d\n", sig);
      }

      int main() {
      signal(SIGALRM, handler); // Register the signal handler
      alarm(5); // Set a timer, trigger SIGALRM signal after 5 seconds
      printf("Waiting for alarm to go off...\n");
      pause(); // Block the process and wait for the signal to be triggered
      printf("Exiting...\n");
      return 0;
      }
    • Compilation and Execution:

      gcc alarm.c -o alarm
      ./alarm
    • Execution Result:

      pi@raspberrypi:~/Linux/signal s./alarm
      waiting for alarm to go off...
      Received signal 14
      Exiting...
    • In the above example program, we first registered the signal handling function handler(), and then called alarm(5) to set a 5-second timer. During the waiting period for the timer to trigger, we use the pause() function to block the process and wait for the signal to be triggered. When the timer is triggered, it automatically sends the SIGALRM signal to the process, triggering the signal handling function, and the program prints the received signal number before exiting.

4.1.2 Receiving Signals

  • To receive signals, the process that receives the signal must not stop. Typically, while loops, sleep, and pause are used.
  • In Linux, the system call signal() is used to set a new signal handler for a signal, which can be set as a user-specified function. The function declaration is as follows:

    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    • signum: The signal we want to handle. You can check the system's signals by typing kill -l in the terminal.
    • handler: The method of handling the signal (system default, ignore, or capture).
    *signal(SIGINT, SIG_IGN);  // SIG_IGN means ignore the SIGINT signal, which is usually generated by pressing Ctrl+C or Delete.
    *signal(SIGINT, SIG_DFL); // SIG_DFL means perform the system default action, which is to terminate the process for most signals.
    *signal(SIGINT, handler); // Capture the SIGINT signal and execute the code in the handler function, which we define ourselves.
  • Customizing the handling of the SIGINT signal:

    #include <stdio.h>
    #include <signal.h>

    void signal_handler_fun(int signum)
    {
    printf("catch signal %d\n", signum);
    }

    int main(int argc, char *argv[])
    {
    signal(SIGINT, signal_handler_fun);
    while (1);
    return 0;
    }
    • When Ctrl+C is pressed, the signal_handler_fun function is executed instead of exiting the program.

4.2 Pipes

In the Linux system, pipes are used to establish communication between reading and writing processes by sharing a file, hence they are also known as pipe files.there are two types of pipes: anonymous pipes and named pipes.

  • Anonymous Pipes: An anonymous pipe is a simple half-duplex pipe that can only be used for communication between processes with a parent-child relationship. Once created, the anonymous pipe becomes shared file descriptors between the two processes, where one process writes data to the pipe and the other process reads data from it. The lifetime of an anonymous pipe is associated with the processes that created it, and when the creating process terminates, the anonymous pipe is destroyed.

  • Named Pipes: Named pipes, also known as FIFO (First In, First Out), are a special type of file that allows any process to access it using its filename at any time. It provides a communication mechanism between processes that are not necessarily related and can allow multiple processes to access it simultaneously, making it suitable for interprocess communication over networks. Named pipes persist in the file system until they are deleted or the system is shut down. Any process with permission to access the named pipe can write to and read from it.

In summary, anonymous pipes are suitable for communication between related processes, while named pipes are suitable for communication between unrelated processes and have persistence.

4.2.1 Anonymous Pipes

  1. Characteristics of anonymous pipes:

    • Pipes use half-duplex communication, allowing data transmission in only one direction at a time
    • Pipes are actually fixed-size buffers, and if a process writes to a full pipe, the system will block that process until there is space in the pipe to receive the data
    • The pipe communication mechanism must provide synchronization between the reading and writing processes
  2. To create an anonymous pipe, the pipe() function is used with the following function declaration:

    #include <unistd.h>
    int pipe(int pipefd[2]);
    • The function parameter pipefd is an array containing two int elements, used to return two file descriptors; pipefd[0] represents the read end, and pipefd[1] represents the write end of the pipe. After calling the pipe() function, the two processes can communicate using these two file descriptors to transfer data.
  3. Example of parent-child processes communicating through a pipe:

    • Step 1: The parent process calls the pipe() function to create a pipe and obtains two file descriptors, fd[0] and fd[1], representing the read and write ends of the pipe

    • Step 2: The parent process uses the fork() function to create a child process, which also has two file descriptors pointing to the same pipe

    • Step 3: The parent process closes the read end of the pipe, and the child process closes the write end. The parent process can write data to the pipe, and the child process can read data from it

      #include <unistd.h>  
      #include <string.h>
      #include <stdlib.h>
      #include <stdio.h>
      #include <sys/wait.h>

      void sys_err(const char *str)
      {
      perror(str);
      exit(1);
      }

      int main(void)
      {
      pid_t pid;
      char buf[1024];
      int fd[2];
      char p[] = "test for pipe\n";

      if (pipe(fd) == -1)
      sys_err("pipe");

      pid = fork();
      if (pid < 0) {
      sys_err("fork err");
      }
      else if (pid == 0) {
      close(fd[1]);
      printf("child process wait to read:\n");
      int len = read(fd[0], buf, sizeof(buf));
      write(STDOUT_FILENO, buf, len);
      close(fd[0]);
      }
      else {
      close(fd[0]);
      write(fd[1], p, strlen(p));
      wait(NULL);
      close(fd[1]);
      }

      return 0;
      }

4.2.2 Named Pipes

  1. To create a named pipe, the mkfifo() function is used with the following function declaration:

    #include <sys/types.h>
    #include <sys/stat.h>

    int mkfifo(const char *pathname, mode_t mode);
    • The pathname parameter specifies the path of the named pipe, and the mode parameter specifies the permission mask. Calling mkfifo() function will create a named pipe at the specified path, and the return value of 0 indicates successful creation, while -1 indicates failure.
    • To read and write from a named pipe, you can use ordinary read() and write() functions, or you can use the open() function to open the pipe file and then use read() and write() functions for communication. After using the named pipe, it is necessary to use the unlink() function to delete the pipe file.
  2. In Linux, the access() function from C language can be used to check if a process has the permission to access a file or directory. The function declaration is as follows:

    int access(const char *pathname, int mode);
    • The pathname parameter specifies the path of the file or directory to be checked, and the mode parameter specifies the access permissions to be checked. Common access permissions include:
      • F_OK: Check if the file exists
      • R_OK: Check if the file is readable
      • W_OK: Check if the file is writable
      • X_OK: Check if the file is executable
    • The access() function returns 0 if the check is successful and -1 if it fails. If access() function returns -1, the errno variable can be used to obtain the error code, which usually includes:
  3. Example of communication using named pipes:

    • Pipe reading end:

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <fcntl.h>
      #include <string.h>
      int main(int argc, char *argv[])
      {
      char buf[32] = {0};
      int fd;
      if (argc < 2)
      {
      printf("Usage:%s <fifo name> \n", argv[0]);
      return -1;
      }
      fd = open(argv[1], O_RDONLY);
      while (1)
      {
      sleep(1);
      read(fd, buf, 32);
      printf("buf is %s\n", buf);
      memset(buf, 0, sizeof(buf));
      }
      close(fd);
      return 0;
      }
    • Pipe writing end:

      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <fcntl.h>

      int main(int argc, char *argv[])
      {
      int ret;
      char buf[32] = {0};
      int fd;
      if (argc < 2)
      {
      printf("Usage:%s <fifo name> \n", argv[0]);
      return -1;
      }

      if (access(argv[1], F_OK) == -1)
      {
      ret = mkfifo(argv[1], 0666);
      if (ret == -1)
      {
      printf("mkfifo is error \n");
      return -2;
      }
      printf("mkfifo is ok \n");
      }
      fd = open(argv[1], O_WRONLY);
      while (1)
      {
      sleep(1);
      write(fd, "hello", 5);
      }
      close(fd);
      return 0;
      }
    • Compile the programs and run them:

      gcc fifo_write.c -o fifow
      gcc fifo_read.c -o fifor
      #Open two terminals and run the programs in each terminal:
      ./fifor fifo
      ./fifow fifo

4.3 Message Queue

In the Linux system, a message queue is an inter-process communication (IPC) mechanism used for data transmission between different processes. It is a first-in-first-out (FIFO) data structure that allows one or multiple processes to communicate by sending and receiving messages in the message queue. The message queue is identified by a message queue identifier (mqid), similar to a file descriptor, which ensures the uniqueness of the message queue. When creating a message queue, a unique key needs to be specified. This key is used to identify the message queue within the system, ensuring that multiple processes can access the same message queue using the same key. Message queues enable data sharing between different processes, making them a powerful inter-process communication mechanism.

Using message queues for inter-process communication offers the following advantages:

  • It supports a many-to-many communication model, where multiple processes can simultaneously read and write to the same message queue.
  • Messages in the message queue can persist in the system, even if the sending process has exited. The receiving process can still read these messages.
  • Message queues have a certain fault tolerance. If the receiving process is temporarily unable to process a message, the message can be retained in the queue and processed later when the receiving process is ready.

In summary, message queues are a highly practical inter-process communication mechanism widely used in various applications, including network communication, multi-threaded programming, distributed systems, and more.In the Linux system, message queue operations primarily rely on the following functions: msgget(), msgsnd(), msgrcv(), and msgctl().

4.3.1 msgget() Function

The msgget() function is used to create or retrieve a message queue. Its function prototype is as follows:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
  • The key parameter is the key value of the message queue, used to uniquely identify a message queue

  • The msgflg parameter is the flag used to specify the attributes of the message queue

  • In the Linux system, several macros can be used to generate the key parameter value for the msgget() function. Commonly used macros include:

    • IPC_PRIVATE: Represents creating a new private message queue accessible only to the current process. This flag is generally used when sharing data between parent and child processes without sharing among different processes.

    • ftok(): Generates a unique key value based on a given file path and an integer. When using ftok(), you need to pass an accessible file path and a user-defined integer as parameters, for example:

      key_t key = ftok("/tmp", 'a');
    • This code will generate a key value for the msgget() function based on the path name /tmp and the character 'a'.

    • IPC_CREAT: Used to create a new message queue. If the specified message queue does not exist, a new one will be created; otherwise, the function returns the identifier of the existing message queue.

    • IPC_EXCL: Specifies that if both IPC_CREAT and IPC_EXCL flags are set, only create a new message queue if it does not already exist; otherwise, return an error.

  • The msgflg parameter is used to specify the attributes of the message queue and can be set using multiple flags, including:

    • IPC_CREAT: If the specified message queue does not exist, create a new one; otherwise, return the identifier of the existing message queue.
    • IPC_EXCL: If both IPC_CREAT and IPC_EXCL flags are set, only create a new message queue if it does not already exist; otherwise, return an error.
    • IPC_PRIVATE: Represents creating a new private message queue accessible only to the current process. This flag is generally used when sharing data between parent and child processes without sharing among different processes.
    • 0666: Represents the permission of the created message queue, using the same representation method as file permissions. Here, 6 represents read and write permissions, 4 represents read permissions, 2 represents write permissions, and 0 represents no permissions.

4.3.2 msgsnd() Function

The msgsnd() function is used to send a message to the message queue. Its function prototype is as follows:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • The msqid parameter is the identifier of the message queue (obtained from the msgget() function)

  • The msgp parameter is a pointer to the buffer where the message to be sent is temporarily stored. A generic structure can be used to represent the message:

    struct msgbuf {
    long mtype; /* Message type */
    char mtext[1024]; /* Message content */
    };
  • The msgsz parameter is the length (in bytes) of the message to be sent. The actual length of the message (excluding the message type field) can be calculated using the formula sizeof(struct mymsg) - sizeof(long):

    • The total size of the mymsg structure is sizeof(long) + 1024 = 1032 bytes
    • msgsz = sizeof(struct mymsg) - sizeof(long) = 1032 - 8 = 1024 bytes.
  • The msgflg parameter is used to specify the behavior of sending the message and can take the following values:

    • 0: Represents the blocking mode, where the thread will be blocked until the message can be written
    • IPC_NOWAIT: Represents the non-blocking mode, where the function immediately returns with an error if the message queue is full or other conditions prevent sending the message
  • If the function executes successfully, it returns 0; otherwise, it returns -1.

4.3.3 msgrcv() Function

The msgrcv() function is used to receive a message from the message queue. Its function prototype is as follows:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • The msqid parameter is the identifier of the message queue

  • The msgp parameter is a pointer to the buffer where the received message will be stored. A generic structure can be used to represent the message:

    struct msgbuf {
    long mtype; /* Message type */
    char mtext[1024]; /* Message content */
    };
  • The msgsz parameter is the length of the received message

  • The msgtyp parameter specifies the type of the message to receive

  • The msgflg parameter is used to specify the behavior of receiving the message and can take the following values:

    • 0: Represents the blocking mode, where the function waits until the message queue is not empty
    • IPC_NOWAIT: Represents the non-blocking mode, where the function immediately returns with an error if the message queue is empty
  • If the function executes successfully, it returns the number of bytes received into the mtext array.

4.3.3 msgctl() Function

The msgctl() function is used to control the state of the message queue. Its function prototype is as follows:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • The msqid parameter is the identifier of the message queue.
  • The cmd parameter specifies the operation to be executed, including deleting the message queue, querying the status of the message queue, etc. It can take the following values:
    • IPC_STAT: Read the attributes of the message queue and save them in the buffer pointed to by buf
    • IPC_SET: Set the attributes of the message queue based on the values from the buf parameter
    • IPC_EMID: Delete the queue from the system kernel
  • The buf parameter is a pointer to the msqid_ds structure used to save the status information of the message queue.

Below is a simple example of a message queue program, including sender and receiver processes:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_KEY 1234

/* Define the message structure */
struct mymsg {
long mtype; /* Message type */
char mtext[1024]; /* Message content */
};

/* Sender process */
void sender()
{
int msgid, ret;
struct mymsg msg;

/* Create or open the message queue */
msgid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msgid < 0) {
perror("msgget");
exit(1);
}

/* Construct the message */
msg.mtype = 1;
strncpy(msg.mtext, "Hello, waveshare!", 1024);

/* Send the message */
ret = msgsnd(msgid, &msg, sizeof(struct mymsg) - sizeof(long), 0);
if (ret < 0) {
perror("msgsnd");
exit(1);
}

printf("Sent message: %s\n", msg.mtext);
}

/* Receiver process */
void receiver()
{
int msgid, ret;
struct mymsg msg;

/* Open the message queue */
msgid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msgid < 0) {
perror("msgget");
exit(1);
}

/* Receive the message */
ret = msgrcv(msgid, &msg, sizeof(struct mymsg) - sizeof(long), 1, 0);
if (ret < 0) {
perror("msgrcv");
exit(1);
}

printf("Received message: %s\n", msg.mtext);
}

int main()
{
pid_t pid;

/* Create a child process */
pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
/* Child process acts as the sender */
sender();
} else {
/* Parent process acts as the receiver */
receiver();
}

return 0;
}

4.4 System-V IPC Semaphores

System V Semaphores are a mechanism for inter-process synchronization and mutual exclusion. They use a counter-based approach to control access to shared resources, allowing multiple processes to access the resource simultaneously while limiting the number of concurrent accesses. This mechanism ensures that each process can smoothly acquire control of the resource, achieving synchronization and mutual exclusion between multiple processes.

4.4.1 Creating or Getting a Semaphore

In System V IPC (Inter-Process Communication), use the semget() function to create or get a semaphore set. The function declaration is as follows:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • Parameter explanation:

    • key: The key value that identifies the semaphore set. You can use the ftok function to generate a unique key value
    • nsems: Specifies the number of semaphores in the semaphore set to be created or obtained
    • semflg: Specifies the operation flags, which can be one or a combination of several flags
  • Function return value: The function returns a non-negative integer if successful, representing the semaphore set identifier (also known as the semaphore set descriptor). If the function fails, it returns -1.

4.4.2 Operating on Semaphores

In System V IPC, use the semop() function to perform P/V operations (increment and decrement operations) on semaphores. The function declaration is as follows:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);
  • Parameter explanation:

    • semid: The semaphore set identifier (returned by the semget function)
    • sops: A pointer to an array of structures, with each structure describing a P/V operation
    • nsops: Specifies the number of structures in the sops array
  • Function return value: The function returns 0 if successful, and -1 if it fails

  • The sembuf structure is defined as follows:

    struct sembuf {
    unsigned short sem_num; // The number of the semaphore to operate on
    short sem_op; // Operation flag: 1 for V (release resource), -1 for P (allocate resource), 0 for wait until the semaphore value becomes 0
    short sem_flg; // 0 for blocking, IPC_NOWAIT for non-blocking
    };
  • There are two basic ways to operate on semaphores:

  • P operation, also known as the wait operation or decrement operation, is used to acquire control of the semaphore and decrement its value. Specifically, when a process needs to access a shared resource, it calls the P operation to wait for the semaphore. If the semaphore's value is greater than or equal to 1, the process will decrement the semaphore's value and continue execution. Otherwise, the process will be blocked and wait until the semaphore's value becomes greater than or equal to 1
  • V operation, also known as the signal operation or increment operation, is used to release control of the semaphore and increment its value. Specifically, when a process finishes accessing a shared resource, it calls the V operation to release the semaphore. This allows other processes waiting for the semaphore to regain control and continue execution
  • PV operations are the most basic and commonly used operations in System V Semaphores. They can be used to implement various synchronization and mutual exclusion mechanisms, such as mutexes and read-write locks. Due to the atomic nature of PV operations, they ensure that access to shared resources does not lead to race conditions and other issues, thus ensuring synchronization and mutual exclusion between multiple processes

4.4.3 Controlling Semaphore Set Attributes

In System V IPC, use the semctl() function to control semaphore set attributes. The function declaration is as follows:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
  • Parameter explanation:

    • semid: The semaphore set identifier (returned by the semget function)

    • semnum: The index of the semaphore in the semaphore set. For most commands, this parameter is ignored and can be set to 0

    • cmd: Specifies the operation command to be executed, which can be one of the following commands:

      • GETVAL: Get the current value of the semaphore
      • SETVAL: Set the value of the semaphore
      • SETALL: Set the values of all semaphores in the set
      • IPC_RMID: Remove the semaphore set
  • Function return value: The function returns a non-negative integer if successful and -1 if it fails.

  • When using the semctl function, depending on the command, additional parameters may be required. For example, for the SETVAL command, you need to provide a union semun structure as the fourth parameter, which contains the value to be set. The union semun structure is defined as follows:

    union semun {
    int val; // Value used for SETVAL command
    struct semid_ds *buf; // Buffer used for IPC_STAT and IPC_SET commands
    unsigned short *array; // Array used for GETALL and SETALL commands
    };

4.4.4 Semaphore Example

Using semaphores to control the execution order between two processes (parent and child):

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

union semun
{
int val;
};

int main(void)
{
int semid;
int key;
pid_t pid;
struct sembuf sem;
union semun semun_union;
key = ftok("./a.c", 0666);
semid = semget(key, 1, 0666 | IPC_CREAT);
semun_union.val = 0;
semctl(semid, 0, SETVAL, semun_union);
pid = fork();
if (pid > 0)
{
sem.sem_num = 0;
sem.sem_op = -1; // P operation
sem.sem_flg = 0;
semop(semid, &sem, 1);
printf("This is parents\n");
sem.sem_num = 0;
sem.sem_op = 1;
sem.sem_flg = 0;
semop(semid, &sem, 1);
}

if (pid == 0)
{
sleep(2);
sem.sem_num = 0;
sem.sem_op = 1; //V operation
sem.sem_flg = 0;
semop(semid, &sem, 1);
printf("This is son\n");
}
return 0;
}

Running the program:

linaro@linaro-alip:~/Linux/process$ ./semget
This is parents
This is son
linaro@linaro-alip:~/Linux/process$

4.5 Shared Memory

Shared memory refers to a mechanism where multiple processes share the same physical memory. This mechanism allows multiple processes to access the same memory area, enabling data sharing between processes.

Under the shared memory mechanism, multiple processes can map the same memory region to their address space to achieve data sharing. This allows multiple processes to read and write the same memory, achieving communication and synchronization between them without the need for any data copying or inter-process communication operations. Therefore, shared memory is an efficient inter-process communication mechanism.

4.5.1 Creating New Shared Memory

In Linux, use the shmget() function to create shared memory. The function declaration is as follows:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • In the function, key specifies the key value for identifying the shared memory, size specifies the size of the shared memory, and shmflg specifies the creation flags, which can be one or a combination of the following constants:

    • IPC_CREAT: If the shared memory does not exist, create it; otherwise, return the identifier of the existing shared memory
    • IPC_EXCL: Used together with IPC_CREAT, return an error if the shared memory already exists
    • IPC_PRIVATE: Use a random value as the key to create shared memory
  • Function return value: The function returns an integer indicating the identifier of the shared memory segment. If the creation fails, the function returns -1 and sets the errno variable to indicate the error type

Below is an example code that demonstrates how to use the shmget() function to create shared memory:

#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
int shmid;
shmid = shmget(IPC_PRIVATE,1024,0777);
if(shmid < 0)
{
printf("shmget is error n");
return -1;
}

printf("Shared memory segment ID = %d\n", shmid);

exit(0);
}

Running the program:

linaro@linaro-alip:~/Linux/process$ gcc -o shmget shmget.c
linaro@linaro-alip:~/Linux/process$ ./shmget
Shared memory segment ID = 983045
linaro@linaro-alip:~/Linux/process$ ipcs -m

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 65536 linaro 600 33554432 2 dest
0x00000000 163841 linaro 600 2097152 2 dest
0x00000000 196610 linaro 600 393216 2 dest
0x00000000 950275 linaro 600 524288 2 dest
0x00000000 491524 linaro 600 524288 2 dest
0x00000000 983045 linaro 777 1024 0

4.5.2 Mapping Shared Memory with shmat()

The shmat() function is used in Linux to map shared memory to the process's address space. The function declaration is as follows:

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • In the function, shmid is the identifier of the shared memory, which is returned by the shmget function
  • The shmaddr parameter is used to specify the starting address of the mapping. If the value of shmaddr is 0, the operating system will choose a suitable address for mapping, usually written as NULL
  • The shmflg parameter is used to specify the mapping flags, and 0 is typically used to indicate default options for operating on shared memory:
    • SHM_RDONLY: Map the shared memory region as read-only
    • SHM_REMAP: Remap the shared memory into the process's address space
    • SHM_EXEC: Allow the shared memory content to be executable. Generally, the content of shared memory is not allowed to be executed
    • SHM_DEST: When the shmdt() function is called to detach shared memory, it will be destroyed
  • Function return value: The shmat() function returns a pointer that points to the starting address of the shared memory mapping. If the mapping fails, the function returns -1 and sets the errno variable to indicate the error type.

4.5.3 Detaching Shared Memory with shmdt()

The shmdt() function is used in Linux to detach shared memory from the process's address space. The function declaration is as follows:

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
  • The shmaddr parameter is the starting address of the shared memory mapping, usually obtained from the pointer returned by the shmat() function
  • Function return value: The function returns 0 on success and -1 on failure
  • Note: The shmdt() function detaches the shared memory from the process's address space but does not delete the shared memory region. It just makes the current process unable to access the shared memory.

4.5.4 Controlling Shared Memory Attributes with shmctl()

The shmctl() function in Linux is used to control shared memory. It can be used to get or modify shared memory attributes. The function declaration is as follows:

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);
  • The shmid parameter is the identifier of the shared memory, which is returned by the shmget function. The cmd parameter specifies the operation command to be executed. It can be one of the following commonly used commands:

    • IPC_STAT: Get the status information of the shared memory and store the result in the shmid_ds structure pointed to by buf
    • IPC_SET: Set the status information of the shared memory using the data in the buf structure
    • IPC_RMID: Remove the shared memory and release its resources
  • The buf parameter is a pointer to a struct shmid_ds structure used to store or pass the status information of the shared memory

  • Function return value: The function returns 0 on success and -1 on failure.

4.5.5 Shared Memory Application Example

Creating shared memory between parent and child processes:

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{

int shmid;
key_t key;
pid_t pid;
char *s_addr, *p_addr;
key = ftok("./a.c", 'a');
shmid = shmget(key, 1024, 0777 | IPC_CREAT);
if (shmid < 0)
{
printf("shmget is error\n");
return -1;
}
printf("shmget is ok and shmid is %d\n", shmid);
pid = fork();
if (pid > 0)
{
p_addr = shmat(shmid, NULL, 0);
strncpy(p_addr, "hello", 5);
wait(NULL);
exit(0);
}
if (pid == 0)
{

sleep(2);
s_addr = shmat(shmid, NULL, 0);
printf("s_addr is %s\n", s_addr);
exit(0);
}
return 0;
}