linux system programming - communication mechanism between processes or threads

Posted by darkwolf on Tue, 18 Jan 2022 20:59:57 +0100

1. Classification of communication mechanism

Communication mechanisms between linux processes or threads are mainly divided into three categories:
Communication: these tools focus on data exchange between processes.
Synchronization: these processes focus on synchronization between processes and thread operations.
Signal: in a specific scenario, the signal can be used as a synchronization technology, and the signal can also be used as a communication technology.


According to the above figure, it is summarized as follows:
The main used for communication are: pipeline and FIFO, message queue (POSIX and SYSTEM V), shared memory (POSIX and SYSTEM V), memory mapping, socket (datagram and stream), and pseudo terminal.
Used for synchronization mainly include semaphores, mutexes, conditional variables, etc. In fact, mutex, read-write lock and spin lock, which are used to protect the critical area, can be classified into one category, and the other category is semaphore and condition variable, which is used to control the number of concurrent threads.
Signal: it is divided into standard signal and real-time signal.

The synchronization mechanism of thread process is summarized in detail in the article of linux system programming - process and thread correlation and locking mechanism. Here we mainly explain the communication mechanism of linux.

2. Pipeline and FIFO

2.1 characteristics of pipeline
1. A pipe is a byte stream
A pipeline has no concept of a message or message boundary. The process reading data from the pipeline can read data blocks of any size, regardless of the size of the data blocks written to the pipeline by the writing process.
2. Read blocking and write blocking
Attempts to read data from a currently empty pipe will be blocked until at least one byte is written to the pipe. If the write end of the pipeline is closed, the process reading data from the pipeline will see the end of the file after reading all the remaining data in the pipeline (that is, read() returns 0).
Similarly, attempts to write data to a currently full pipeline will be blocked until there is enough space for the data to be read. If the reader is turned off, write will return an EPIPE error and receive a SIGPIPE signal.
3. The pipe is unidirectional
The direction of data transmission is one-way. One section of the pipeline is used for writing and the other end is used for reading. If you want to achieve the effect of two-way communication, you can create two pipelines.
4. Write no more than pipe_ The operation of buf bytes is atomic
If multiple processes write to the same pipe, if the amount of data they write at one time does not exceed PIPE_BUF bytes, it can ensure that the written data will not be mixed with each other. On Linux, pipe_ The value of buf is 4096.
5. The capacity of the pipeline is limited.
2.2 example program: pipeline realizes parent-child process communication
After the fork() system call, the child process will inherit the copy of the file descriptor of the parent process, as shown in the following figure:

The pipeline data flow is one-way, so usually the two communication processes need to close the ports they do not need to use. As shown in the left and right figures above, after the fork call, the parent process closes the reading end and the child process closes the writing end. The data flow direction is that the parent process writes data to the pipeline, and the child process reads data.

The function of the following example program is that the parent process writes data to the pipeline, and the child process reads data from the pipeline piece by piece. After the pipe system calls to create a pipe, the parent process needs to close the reading end and the child process needs to close the writing end (as shown in the figure above); The parent process writes the data obtained from the command line parameters into the pipeline, and the child process obtains the data from the pipeline through cyclic reading, and prints the obtained data to the screen; When the parent process writes data, it will close the write end of the pipeline. Then, after the child process reads the remaining data in the pipeline, it will read to the end of the file. At this time, the child process jumps out of the read cycle, closes the write end, and the program ends.

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


#define BUFF_SIZE 1024


int main(int argc,char *argv[])
{
	int pfd[2];   //After using pipe to create a pipe, pfd[0] represents the read end and pfd[1] represents the write end
	char buf[BUFF_SIZE];
	ssize_t numread;
	
	
	if(pipe(pfd)==-1) //Pipe create pipe
		return -1;
	switch(fork()){
	case -1:
		return -1;
	case 0:
		if(close(pfd[1])==-1) //The child process closes the write side
			return -1;
		while(1){
			numread=read(pfd[0],buf,BUFF_SIZE); //The subprocess circulates reading data from the reader to the buf array
			if(numread==-1){
				printf("read err\n");
				return -1;}
			if(numread==0){   //read returns 0, indicating that the write end has been closed by the parent process. At this time, the loop jumps out
				break;}
			if(write(STDOUT_FILENO,buf,numread)!=numread) //The read buf is output to stdout on the screen_ FILENO
				return -1;
		}
		write(STDOUT_FILENO,"\n",1);
		if(close(pfd[0])==-1) //The subprocess closes the reader
			return -1;
		return 0;
		
	default:
		if(close(pfd[0])==-1) //The parent process closes the reader
			return -1;
		if(write(pfd[1],argv[1],strlen(argv[1]))!=strlen(argv[1])) //The parent process writes end-to-end data to the pipeline. The data is a command-line parameter
			return -1;
		
		if(close(pfd[0])==-1) //The parent process closes the writer and the program ends
			return -1;
		wait(NULL);   //Wait for the child process to return
		return 0;
	}
	
}

Program effect:


2.2 example program: pipeline is used for process synchronization
The pipeline is mainly used for inter process communication, but it can also be used for inter process synchronization. This is mainly based on the above. After closing the write end of the pipeline, the process at the read end will read the end of the file, that is, read returns 0.
The function of the following program is: the parent process creates multiple child processes. Each child process sleeps for different times. The parent process needs to wait for all child processes to sleep before executing its own operations.
The following program data flow becomes read by the parent process and write by the child process. Therefore, at the beginning of the for loop, three sub processes are created. All three sub processes close the read end of the pipeline and sleep for 2 seconds, 6 seconds and 10 seconds respectively. After the sleep is completed, the sub process closes the write end; When all child processes complete hibernation and close the write side, the parent process will read the end of the file. At this time, the synchronization operation is completed, and the parent process will know that it can do its own thing.

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

int main(int argc,char *argv[])
{
	int pfd[2]; 
	int sleep_time[3]={2,6,10};
	int i,dummy;
	
	
	if(pipe(pfd)==-1)
		return -1;
		
	for(i=0;i<3;i++)          //Create three sub processes
	{
	switch(fork()){
	case -1:
		return -1;
	case 0:
		if(close(pfd[0])==-1) //Three sub processes close the reader
			return -1;
		sleep(sleep_time[i]); //The three sub processes sleep for different times: 2 seconds, 6 seconds and 10 seconds.
		printf("child pid=%d,sleep time=%ds\n",getpid(),sleep_time[i]);
		if(close(pfd[1])==-1) //After hibernation, the three sub processes close the write side
			return -1;
		return 0;
		
	default:
		break;			
	}
	}
	
	if(close(pfd[1])==-1)     //The parent process closes the write side
		return -1;
	if(read(pfd[0],&dummy,1)!=0)   //The parent process reads data from the reader to see if it can read the end of the file.
		return -1;
	printf("parent get EOF\n");    //This means that the parent process already knows the synchronization information and the child process completes hibernation.
	printf("start parent work now\n");
	return 0;
}

Program effect:

2.3 bind the standard output and standard input of the process to both ends of the pipeline

Execute the command under the shell command

ls | wc -l

It is equivalent to creating two processes ls and wc. The standard output stdout of LS is linked to the write end of the pipeline, and the standard input stdin of wc is connected to the read end of the pipeline.

Take the above figure as an example. If we want to bind the standard output of the parent process and the standard input of the child process to both ends of the pipeline, we can
Do:

...
char *str_get;
switch(fork()){
...
case 0:
	close(pfd[1]);   //The child process closes the write side
	dup2(pfd[0],STDIN_FILENO);  //Copy the file descriptor and bind the standard input to the pipeline reader
	close(pfd[0]);  //Close redundant file descriptors
	gets(str_get);  //Read from pipe  
default:
	close(pfd[0]);   //The parent process closes the reader
	dup2(pfd[1],STDOUT_FILENO);  //Copy the file descriptor and bind the standard output to the write end of the pipeline
	close(pfd[1]);  //Close redundant file descriptors
	puts("test message\n");  //Write to pipe 
}

2.4 communicating with shell commands
The popen system call can execute a shell command and read its output or send some input to it. The principle of the popen() function is to create a pipe, then create a child process to execute the shell, and the shell creates a child process to execute the command string.

FILE *popen(const char *command,const char *mode);

Note that the return value of popen is a file stream pointer.
Example: execute ls command under shell through popen and read its output:

char *buff;
FILE *fp;
fp=popen("ls",r);
fgets(buff,size,fp);  //Read ls output to buff array
close(fp);

2.5 FIFO
Difference between FIFO and pipeline: FIFO has a name in the file system, and its opening method is the same as opening an ordinary file. In this way, FIFO can be used for communication between unrelated processes. Pipelines need to be related to processes to communicate.
Create a FIFO using mkfifo:

int mkfifo(const char *pathname,mode_t mode);

Example:

int fd1;
int fd2;
#define FIFO_PATH "tmp/fifotest"
mkfifo(FIFO_PATH ,S_IRUSR|S_IWUSR|S_IWGRP);
//Process 1 needs to open FIFO in read-only mode for reading data
fd1=open(FIFO_PATH,O_RDONLY);
//Process 2 opens FIFO in write only mode for writing data
fd2=open(FIFO_PATH,O_WRONLY);
//... Next, fd1 and fd2 can use write and read calls
...
...

When a process opens one end of FIFO and the other end is not opened, read or write operations will be blocked; When the write end is not opened and you do not want to read blocking, you can specify o when the read process opens FIFO_ Nonblock implementation. At this time, read will not block and can return correct data; However, the reader is not opened, and O is specified when the FIFO is opened by the write process_ Nonblock is not allowed. The open call will fail.

3. Message queuing (SYSTEM V)

3.1 system v message queue features
1. The handle that references the message queue is an identifier returned by the msgget() call. These identifiers are different from the file descriptors used by other forms of I/O.
2. Message oriented, that is, the reader receives the whole message written by the writer. It is impossible to read part of a message and leave the rest in the queue or read multiple messages at once.
3. Messages can be read from the message queue either in first in first out order or according to the type of message.
3.2 create a message queue
The msgget() system call creates a new message queue or obtains the identifier of an existing queue:

int msgget(key_t key,int msgflag);

The key parameter can be obtained using ftok(), for example:

key_t key;  //key value
int id;   //Identifier of the message queue
key=ftok("/program/test",'x');  // ftok() get key value 
id=msgget(key,IPC_CREAT | S_IRUSR | S_IWUSR); //The msgget() system call creates a new message queue

IPC_ If creat does not have a message queue corresponding to the specified key, it creates a new queue.
IPC_EXCL if IPC is also specified_ Create and the queue corresponding to the specified key already exists, then the call will fail and return EEXIST error.
You can also use IPC to create a message queue_ Private mode:

id=msgget(IPC_PRIVATE,S_IRUSR | S_IWUSR);

3.3 message queue sending and receiving messages
The msgsnd() system call writes a message to the message queue.

int msgsnd(int id,const void *msg,size_t msgsz,int flag);

The parameters are: id is the queue identifier, msg is the sent message (structure pointer), msgsz is the length of the message content, and flag can be IPC_NOWAIT non blocking transmission.
Sender:

int len;
//Send message custom structure msg
struct msg{
	long tpye;   //Type of message
	char *text;  //Content of the message
	};
struct msg msg_send;
msg_send.type=20;  
msg_send.text="thi is a message\n";
len=strlen(msg_send.text);  //Length of message content
msgsnd(id,&msg_send,len,IPC_NOWAIT);  //Start sending a message. id is the identifier of the message queue

When the message queue is full, msgsnd() blocks until there is enough space in the queue to hold the message. But if IPC is specified_ Nowait, msgsnd() will immediately return the EAGAIN error.

The msgrcv() system call reads (and deletes) a message from the message queue and copies its contents into the buffer pointed to by msgp:

ssize_t msgrcv(int id,void *msg,size_t maxmsgsz,long rec_type,int flag);

The parameters are: id is the queue identifier, msg is the read buffer, maxmsgsz is the maximum number of bytes available in the buffer, rec_type indicates the type of accepted message, and flag can be set to IPC_NOWAIT non blocking read.
Receiving procedure:

#define maxlen 1024;  // Specifies the maximum number of bytes available for the accept buffer
struct msg_rec{
	long tpye_rec;   //Type of message
	char *text_rec;  //Content of the message
	};
struct msg_rec mymsg_rec;
msglen=msgrcv(id,&mymsg_rec,maxlen,20,IPC_NOWAIT); //The specified acceptance type is 20

Usually, if there is no matching rec in the queue_ Type, msgrcv () will block until there is a matching message in the queue. Specify IPC_ The nowait flag causes msgrcv() to immediately return an ENOMSG error.
The above program specifies the accepted data type rec in msgrcv_ Type is 20, this rec_tpye means:
a. If rec_ If type equals 0, the first message in the queue is deleted and returned to the calling process.
b. If rec_ If the type is greater than 0, the type of the first message sent in the queue will be equal to REC_ The message of type is deleted and returned to the calling process.
c. If rec_type is less than 0, type is the smallest in the queue, and its value is less than or equal to REC_ The first message of the absolute value of type is deleted and returned to the calling process.
For example, if there are messages and corresponding types in the message queue, see the following figure

If you let rec_type=0, which is equivalent to using the message queue as a first in first out queue. If I specify rec in msgrcv_ If the type is - 300, these msgrcv calls will read messages in the order of 2 (type 100), 5 (type 100), 3 (type 200), and 1 (Type 300). 4 will not be read.
So I specify rec in the program_ If the type is 20, you can match the message of type 20 in the sender.
3.4 control operation of message queue
msgctl() system call performs control operation on the message queue with identifier id, msqid_ds is the structure associated with the message queue.

int msgctl(int id,int cmd,struct msqid_ds *buf);
struct msqid_ds 
  { 
    struct msqid_ds { 
    struct ipc_perm msg_perm; 
    struct msg *msg_first;      /* first message on queue,unused  */ 
    struct msg *msg_last;       /* last message in queue,unused */ 
    __kernel_time_t msg_stime;  /* last msgsnd time */ 
    __kernel_time_t msg_rtime;  /* last msgrcv time */ 
    __kernel_time_t msg_ctime;  /* last change time */ 
    unsigned long  msg_lcbytes; /* Reuse junk fields for 32 bit */ 
    unsigned long  msg_lqbytes; /* ditto */ 
    unsigned short msg_cbytes;  /* current number of bytes on queue */ 
    unsigned short msg_qnum;    /* number of messages in queue */ 
    unsigned short msg_qbytes;  /* max number of bytes on queue */ 
    __kernel_ipc_pid_t msg_lspid;   /* pid of last msgsnd */ 
    __kernel_ipc_pid_t msg_lrpid;   /* last receive pid */ 
}; 

cmd parameters can be as follows:
IPC_RMID
Immediately delete the message queuing object and its associated msqid_ds data structure. All remaining messages in the queue are lost.
IPC_STAT
The msqid that will be associated with this message queue_ A copy of the DS data structure is placed in the buffer pointed to by the buf.
IPC_SET
Update the msqid associated with this message queue with the value provided by the buffer pointed to by buf_ The selected field in the DS data structure.
3.5 message queue in display system
One method: ipcs of shell command can display the information of IPC objects on the system:

The ipcrm command deletes an IPC object:

ipcrm -X key
ipcrm -x id

The second method:
/proc/sysvipc/msg lists all message queues and their characteristics.
/proc/sysvipc/sem lists all semaphore sets and their characteristics.
/proc/sysvipc/shm lists all shared memory segments and their characteristics.
For example, the contents of a / proc/sysvipc/sem file:

The third method:
It can also be obtained by msgctl operation control.

4. Message queuing (POSIX)

4.1 differences between POSIX and system v message queues
Advantages of posix message queuing:
1. The interface is simpler. At the same time, POSIX IPC objects are reference counted. The queue will be marked for deletion only after all processes currently using the queue have closed the queue, which simplifies the task of determining when to delete an object.
2. A process can asynchronously receive notifications through signal or thread instantiation when a message enters a queue that was empty before.
3. You can use poll(), select(), and epoll to monitor POSIX message queues. System V message queuing does not have this feature.
System V message queuing benefits:
1. Compared with POSIX message queue, which is strictly prioritized, System V message queue has more flexibility in selecting messages according to types.

4.2 creation and closing of POSIX message queue
The creation, closing and deletion of POSIX message queue use the following three function interfaces:

#include <mqueue.h>
mqd_t mq_open(const char *name, int oflag, /* mode_t mode, struct mq_attr *attr */);  //The message queue descriptor is returned successfully, and - 1 is returned for failure
mqd_t mq_close(mqd_t mqdes);
mqd_t mq_unlink(const char *name);  //0 is returned for success and - 1 is returned for failure

Name: indicates the name of the message queue, which conforms to the name rules of POSIX IPC
oflag: indicates the opening method, which is similar to that of the open function. There are required options: O_RDONLY,O_WRONLY,O_RDWR, there are optional options: O_NONBLOCK,O_CREAT,O_EXCL.
mode: is an optional parameter and contains o in oflag_ This parameter is required only when the creat flag and the message queue does not exist. Indicates the default access rights. You can refer to open.
attr: it is also an optional parameter and contains o in oflag_ Only required when the creat flag and the message queue does not exist. This parameter is used to set some properties for the new queue. If it is a null pointer, the default property is adopted.
mq_close is used to close a message queue and the close type of the file. After closing, the message queue is not deleted from the system. When a process ends, it will automatically call to close the open message queue.
mq_ The unlink() function deletes the message queue identified by name and marks the queue as destroyed after all processes use the queue (deleted after all processes close the message queue descriptor).

4.3 properties of POSIX message queue
Front in MQ_ The last parameter MQ in open_ The definition of attr structure is as follows:

#include <bits/mqueue.h>
struct mq_attr
{
	long int mq_flags //Flag of message queue: 0 or O_NONBLOCK, used to indicate whether it is blocked 
	long int mq_maxmsg  //Maximum number of messages in the message queue
	long int mq_msgsize  //The maximum number of bytes per message in the message queue
	long int mq_curmsgs  //The current number of messages in the message queue
  	long int __pad[4];
};

The property setting and acquisition of POSIX message queue can be realized through the following two functions:

#include <mqueue.h>
mqd_t mq_getattr(mqd_t mqdes, struct mq_attr *attr);
mqd_t mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr); //0 is returned for success and - 1 is returned for failure

mq_getattr is used to get the properties of the current message queue, mq_setattr is used to set the properties of the current message queue. Where MQ_ oldattr in setattr is used to save the properties of the message queue before modification. It can be empty.

mq_ The only attribute setattr can set is mq_flags, used to set or clear the non blocking flag of the message queue. Other attributes of the newattr structure are ignored. mq_maxmsg and MQ_ The msgsize property can only be passed through MQ when creating a message queue_ Open to set. mq_open will only set these two properties, ignoring the other two properties. mq_ The curmsgs property can only be obtained and cannot be set.
4.4 posix message queue sending and receiving
POSIX message queue can send and receive messages through the following two functions:

#include <mqueue.h>
mqd_t mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);                
mqd_t mq_receive(mqd_t mqdes, char *msg_ptr,size_t msg_len, unsigned *msg_prio);                 

mq_send writes a message to the message queue, mq_receive reads a message from the message queue.
mqdes: message queue descriptor;
msg_ptr: pointer to the message body buffer;
msg_len: the length of the message body, where MQ_ The parameter of receive cannot be less than the maximum size of messages that can be written to the queue, that is, it must be greater than or equal to the MQ of the queue_ MQ in attr structure_ The size of msgsize. If MQ_ MSG in receive_ If len is less than this value, an EMSGSIZE error will be returned. The message length sent by POXIS message queue must be less than MQ_ MQ in attr structure_ The size of msgsize.
msg_prio: message priority; It is less than MQ_ PRIO_ The higher the number of Max, the higher the priority. POSIX message queue calling mq_receive always returns the earliest message with the highest priority in the queue. If the message does not need to be prioritized, it can be set in mq_send is set to msg_prio is 0, MQ_ MSG of receive_ Set prio to NULL.

As a simple example, the following program obtains information from the command line and writes a message to the posix signal queue.
. / a.out / / execute file + signal queue name + sent message + priority

#include <stdio.h>  
#include <mqueue.h>  
#include <errno.h>  
#include <string.h>
#include <stdlib.h>


int main(int argc, char *argv[])  
{  

        unsigned long prio;  
        mqd_t mqd;
		struct mq_attr attr;
		attr.mq_maxmsg=32;
		attr.mq_msgsize=512;  
		
        if (argc != 4)  
        {  
                printf("usage: mqsend <name> <msg> <priority>\n");  
                return -1; 
        }  
        mqd = mq_open(argv[1], O_WRONLY | O_CREAT,0666,&attr);
		if(mqd == (mqd_t)-1)
        {  
                printf("mq_open() error %d: %s\n", errno, strerror(errno));  
                return -1;  
        } 
		
        prio = atoi(argv[3]);  

        if (mq_send(mqd, argv[2], strlen(argv[2]), prio) == -1)  
        {  
                printf("mq_send() error %d: %s\n", errno, strerror(errno));  
                return -1;  
        }  
        return 0;  
}  

The following program reads a message from the posix message queue
Usage:/ a.out / / executable + message queue name

#include <stdio.h>  
#include <mqueue.h>  
#include <errno.h>  
#include <string.h>
#include <stdlib.h>


int main(int argc, char *argv[])  
{  

        int prio;  
        mqd_t mqd;
		struct mq_attr attr;
		char *buff;
		
        if (argc != 2)  
        {  
                printf("usage: mq_receive <name>\n");  
                return -1; 
        }  
        mqd = mq_open(argv[1], O_RDONLY);
		if(mqd == (mqd_t)-1)
        {  
                printf("mq_open() error %d: %s\n", errno, strerror(errno));  
                return -1;  
        } 
		
        if(mq_getattr(mqd,&attr) == -1)
		{
			 printf("mq_getattr() error %d: %s\n", errno, strerror(errno));  
             return -1; 
		}
		
		buff=malloc(attr.mq_msgsize);
		
		
        if (mq_receive(mqd, buff, attr.mq_msgsize, &prio) == -1)  
        {  
            printf("mq_receive() error %d: %s\n", errno, strerror(errno));  
            return -1;  
        }  
		
		printf("read data is %s\n",buff);
		
        return 0;  
}  

Test results: send 3 pieces of information with priority of 1, 5 and 10 respectively by sending program. Using the receiver to receive and print messages, it can be observed that they are read in order of priority from high to low:

4.5 posix message queue asynchronous message notification

One characteristic that distinguishes POSIX message queue from System V message queue is that POSIX message queue can receive asynchronous notification that there are available messages on the previously empty queue (that is, the queue has changed from empty to non empty). This feature means that there is no need to perform a blocked call or mark the message queue descriptor as non blocking and execute MQ on the queue periodically_ Receive () calls, the process can choose to receive notifications through the form of a signal or by calling a function in a single thread.
Using MQ_ The calling process registered with the notify() function will receive a notification signal when a message enters the empty queue referenced by the descriptor mqdes:

int mq_notify(mqd_t mqdes, const struct sigevent* notification);

The sigevent structure is as follows:

union sigval {          /* Data passed with notification */
 	int  sival_int;         /* Integer value */
    void   *sival_ptr;      /* Pointer value */
};

struct sigevent {
	int sigev_notify; /* Notification method */
    int sigev_signo;  /* Notification signal */
    union sigval sigev_value;  /* Data passed with notification */
    void (*sigev_notify_function) (union sigval);/* Function used for thread notification (SIGEV_THREAD) */
    void *sigev_notify_attributes;/* Attributes for notification thread (SIGEV_THREAD) */
    pid_t sigev_notify_thread_id; /* ID of thread to signal (SIGEV_THREAD_ID) */
};

sigev_notify field: SIGEV_SIGNAL, by generating one in sigev_ The signal specified in the Signo field to notify the process. SIGEV_THREAD, by calling sigev_notify_function to notify the process, just like starting the function in a new thread.

Using MQ_ Notice:
1. At any one time, only one process ("registration process") can register with a specific message queue to receive notifications. If a registration process already exists on a message queue, subsequent registration requests on the queue will fail (mq_notify() returns EBUSY error).
2. The registration process will only be notified when a new message enters the previously empty queue. If the queue already contains messages at the time of registration, the notification will be issued only when a new message arrives after the queue is emptied.
3. If a process wants to receive notifications continuously, it must call MQ again after each notification_ Notify() to register yourself.
4. If other processes are in MQ_ If the receive () call is blocked, the process will read a new message in the message queue, and the registration process will remain registered.

Example of program for receiving notification by signal
The following program opens the queue through the non blocking mode, blocks the notification signal (SIGUSR1) and establishes a processor for it, and then calls mq_. Notify() to register the process, receive message notifications, and execute an infinite loop in which the following tasks are performed:
1. Call sigsuspend(), which will unblock the notification signal and wait until the signal is captured (why not pause, because sigsuspend will unblock and Hibernate into an atomic operation, avoiding the arrival of the signal between unblocking and hibernating pause, resulting in the process hanging all the time)
2. The process is called mq_ after the signal is waken up. Notify() re registers the process to receive notification messages.
3. Execute a while loop to read as many messages as possible from the queue in order to empty the queue.

#include <stdio.h>  
#include <mqueue.h>  
#include <errno.h>  
#include <string.h>
#include <stdlib.h>
#include <signal.h>

#define NOTIFY_SIG SIGUSR1

static void handler(int sig){
	 if(sig == NOTIFY_SIG)
     printf("NOTIFY_SIG sig wake up the process!!!\n");
	
}

int main(int argc, char *argv[])  
{  

        int prio;  
        mqd_t mqd;
		sigset_t blockmask,emptymask;
		struct mq_attr attr;
		struct sigaction act;
		struct sigevent sev;
		char *buff;
		
        if (argc != 2)  
        {  
                printf("usage: mq_receive <name>\n");  
                return -1; 
        }  
		//Open it in a non blocking manner and use the while loop mq_receive cleans the data in the message queue
        mqd = mq_open(argv[1], O_RDONLY | O_NONBLOCK); 
		if(mqd == (mqd_t)-1)
        {  
                printf("mq_open() error %d: %s\n", errno, strerror(errno));  
                return -1;  
        } 
        if(mq_getattr(mqd,&attr) == -1)
		{
			 printf("mq_getattr() error %d: %s\n", errno, strerror(errno));  
             return -1; 
		}
		//Receive buffer allocation memory
		buff=malloc(attr.mq_msgsize);
		
		//Block NOTIFY_SIG signal
		sigemptyset(&blockmask);
		sigaddset(&blockmask, NOTIFY_SIG);
		sigprocmask(SIG_BLOCK, &blockmask, NULL);  
		//Start capturing signal NOTIFY_SIG
		act.sa_handler = handler;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sigaction(NOTIFY_SIG, &act, NULL);    
		//The registration signal notifies that after registration, when a new message comes in the empty message queue, a notify will be sent_ SIG signal to the process
		sev.sigev_notify=SIGEV_SIGNAL;
		sev.sigev_signo=NOTIFY_SIG;
		if(mq_notify(mqd,&sev) == -1){
			printf("mq_notify error %d: %s\n", errno, strerror(errno));
		}
		
		sigemptyset(&emptymask);
		
		while(1){
			//Hang here and wait for NOTIFY_SIG signal wakes up. In fact, there is no blocking signal here, and any signal can wake up
			sigsuspend(&emptymask);  
			//Execution here indicates that the process has been notified_ When the sig signal wakes up, the next operation is to re register and read data from the message queue
			if(mq_notify(mqd,&sev) == -1){
				printf("mq_notify error %d: %s\n", errno, strerror(errno));
			}
			//Read data
			while((mq_receive(mqd, buff, attr.mq_msgsize, &prio)) >= 0)
			{
				printf("read data is %s\n",buff);
			}	
			
			
		}
        return 0;  
}  

Use the previous send to send data to the message queue, use the above program to replace the rec receiving program for testing, and run it first/ Rec / MQ & is executed in the background:


I don't want to write the method and code for the notification thread to receive the notification. I feel that the deduction is too fine. I will have time and opportunity to learn about it in the future.

5. Shared memory and memory mapping

Well, there is still a shared memory and memory mapping. socket is sorted out in a separate chapter. After all, there are a lot of contents.
5.1 characteristics of shared memory
Shared memory allows two or more processes to share the same area of physical memory (often referred to as segments). Because a shared memory segment will become a part of a process's user space memory, this IPC mechanism does not need kernel intervention. Compared with the practice that the pipeline or message queue requires the sending process to copy data from the buffer in user space into the kernel memory and the receiving process to copy data from the kernel memory into the buffer in user space, this IPC technology is faster. The IPC mechanism of shared memory is not controlled by the kernel, which means that some synchronization methods are usually needed to prevent processes from accessing shared memory at the same time.

5.2 system v shared memory operation process
1. Create / open shared memory
2. Mapping shared memory, that is, mapping the specified shared memory to the address space of the process for access
3. Unmap shared memory
4. Delete shared memory object

Create / open shared memory:

int shmget(key_t key, size_t size, int shmflg)

The key parameter is not explained much, just like the previous system v message queue.
The size parameter is the length of shared memory to be created, in bytes.
The first shmfg parameter specifies the flag to create or open and the permission to read and write (mode member in ipc_perm). Second, IPC can be specified_ Creat and IPC_EXCL,IPC_CREAT if the shared memory does not exist, create a shared memory; otherwise, open the existing memory directly. IPC_EXCL a new memory is created only when the shared memory does not exist, otherwise an error will be generated.

Map shared memory:

void * shmat(int shmid, const void *shmaddr, int shmflg);

shmid: shared memory area identifier to map
shmaddr: map the shared memory to the specified address (if NULL, it means that the mapping is completed automatically by the system)
shmflg: SHM_RDONLY shared memory is read-only, default 0: shared memory is read-write
Return value: the mapped address is returned after the call is successful, and the error is returned (void *)-1.

Undo shared memory:

int shmdt(const void * shmadr);

Note: when a process no longer needs a shared memory segment, it will call the shmdt() system call to cancel the segment, but this does not really delete the segment from the kernel, but the relevant shmid_ SHM of DS structure_ The value of nattch domain is minus 1. When the value is 0, the kernel will physically delete the shared segment.

Control shared memory:

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid: shared memory identifier ID
cmd : IPC_STAT obtains the state of shared memory; IPC_SET changes the state of shared memory; IPC_RMID delete shared memory
buf: is a structure pointer. IPC_ During stat, the obtained state is placed in this structure. If you want to change the state of shared memory, use this structure struct shmid_ds specify:

struct shmid_ds{
      struct ipc_perm shm_perm;/* Operation authority*/
      int shm_segsz;                    /*The size of the segment in bytes*/
      time_t shm_atime;          /*The time when the last process is attached to the segment*/
      time_t shm_dtime;          /*The time the last process left the segment*/
      time_t shm_ctime;          /*The last process modifies the period of time*/
      unsigned short shm_cpid;   /*Create the pid of the segment process*/
      unsigned short shm_lpid;   /*pid of the last process operating on this segment*/
      short shm_nattch;          /*The number of processes currently attached to the segment*/
/*The following is private*/
      unsigned short shm_npages;  /*The size of the segment in pages*/
      unsigned long *shm_pages;   /*Pointer array to frames - > SHMMAX*/
      struct vm_area_struct *attaches; /*Description of the shared segment*/
};

In the previous example program of system v shared memory, the following program process creates a shared memory area, maps the area to the specified address, and then modifies the data in the shared area:

#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <time.h>
#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAME "."
int main(int argc, char **argv)
{

    int      id;
    int flag;
    char    *ptr;
    size_t  length=1024;
    key_t key;
    struct shmid_ds buff;
    key = ftok(PATHNAME,1);

    if(key<0)
    {
        printf("ftok error\r\n");
        return -1;
    }

    id = shmget(key, length,IPC_CREAT | IPC_EXCL| S_IRUSR | S_IWUSR  );
    if(id<0)
    {
        printf("errno: %s\r\n",strerror(errno));
        printf("shmget error\r\n");
        return -1;
    }
    ptr = shmat(id, NULL, 0);
    if(ptr==NULL)
    {
        printf("shmat error\r\n");
        return -1;
    }

    shmctl(id,IPC_STAT,&buff);
    int i;
    for(i=0;i<buff.shm_segsz;i++)
    {
        *ptr++ = i%256;
    }
    return 0;
}

The following program opens the same shared memory area, maps it to the virtual address of the process, reads the shared memory data and prints it:

#include <sys/mman.h>
#include <sys/stat.h>        /* For mode constants */
#include <fcntl.h>           /* For O_* constants */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <time.h>
#include <assert.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>
#include <sys/ipc.h>
#include <sys/shm.h>




#define PATHNAME "."
int main(int argc, char **argv)
{

    int      id;
    int flag;
    char    *ptr;
    size_t  length=1024;
    key_t key;
    struct shmid_ds buff;
    key = ftok(PATHNAME,1);

    if(key<0)
    {
        printf("ftok error\r\n");
        return -1;
    }

    id = shmget(key, length,IPC_CREAT | IPC_EXCL| S_IRUSR | S_IWUSR  );
    if(id<0)
    {
        printf("errno: %s\r\n",strerror(errno));
        printf("shmget error\r\n");
        return -1;
    }
    ptr = shmat(id, NULL, 0);
    if(ptr==NULL)
    {
        printf("shmat error\r\n");
        return -1;
    }

    shmctl(id,IPC_STAT,&buff);
    int i;
    for(i=0;i<buff.shm_segsz;i++)
    {
        *ptr++ = i%256;
    }
    return 0;
}

5.3 memory mapping
Before learning about memory mapping, first understand several concepts.
a. File mapping and anonymous mapping.
File mapping maps a portion of a file directly to the virtual memory of the calling process. Once a file is mapped, the contents of the file can be accessed by manipulating bytes in the corresponding memory area. Mapped pages are loaded (automatically) from the file when needed.
An anonymous mapping has no corresponding file. This mapped page is initialized to 0.
b. Private mapping and shared mapping
Changes in the mapping content of private mapping are not visible to other processes. For file mapping, changes will not be made on the underlying file. This means that whenever a process tries to modify the contents of a page, the kernel will first create a new page for the process and copy the contents of the page to be modified to the new page.
Changes in the mapping content of shared mapping are visible to all other processes sharing the same mapping. For file mapping, changes will occur on the underlying file.
Combine the previous concepts in pairs, so what we often say about memory mapping actually includes private file mapping, shared file mapping, private anonymous mapping and shared anonymous mapping.

Let's start with private file mapping: map part of the contents of a file to the virtual space address. The changes in the mapped contents are not visible to other processes, and the content changes will not be made on the underlying file. The main purpose of this mapping is to initialize a memory area using the contents of a file. Some common examples include initializing the text and data segments of a process based on the corresponding parts of a binary executable or shared library file.
Shared file mapping: it also maps part of the contents of a file to the virtual space address, but the changes in the mapped contents are visible to other processes, and the content changes will be synchronized on the underlying file. It is mainly used for two purposes. First, it allows memory mapped I/O, because normal read() or write() requires two transfers: for example, to initiate read() once, the file contents are copied to the kernel cache, and then the kernel cache and the user's buffer exchange data, while using mmap() does not need a second transfer, The user process only needs to modify the content mapped to the virtual space address, and then can rely on the kernel memory manager to automatically update the underlying file. In addition to saving one transfer between kernel space and user space, MMAP () can also improve performance by reducing the memory required. When read () or write () is used, the data will be saved in two buffers: one in user space and the other in kernel space. When using MMAP (), kernel space and user space will share the same buffer, that is, the area mapped to virtual space, which saves memory consumption.
The second function of shared file mapping is somewhat similar to that of shared memory, because it is visible to other processes for modifying the space content mapped to the virtual address, but the difference from shared memory is that changes in the content in the region will be reflected on the underlying mapping file.
Private anonymous mapping: every time you call mmap() to create a private anonymous mapping, a new mapping will be generated. This mapping is different from other anonymous mappings created by the same (or different) process (that is, physical paging will not be shared). The main purpose of private anonymous mapping is to allocate new (zero filled) memory for a process (for example, malloc() uses mmap() for this purpose when allocating large memory).
Shared anonymous mapping: equivalent to System V shared memory, but this can only be done between related processes.

Create a memory map:

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

The addr parameter specifies the virtual address where the map is placed. If addr is specified as NULL, the kernel selects an appropriate address for the mapping. This is the preferred practice for creating mappings.
The length parameter specifies the number of bytes mapped.
The prot parameter is a bitmask that specifies the protection information applied to the map:

The flag parameter can be MAP_PRIVATE means to create a private MAP_SHARED means to create a shared map.
The parameters fd and offset are used for file mapping (anonymous mapping ignores them). The fd parameter is a file descriptor that identifies the mapped file. The offset parameter specifies the starting point of the mapping in the file.
For example, create a shared file map:

#define pathname "./testfile"
#define SIZE 1024
int main(int argc,char argv[]){
	int fd;
	char *addr;
	fd=open(pathname,O_RDWR);
	if(fd<0){
		printf("open file err\n");
		return -1;
	}
	addr=mmap(NULL,SIZE,PROT_READ | PROTT_WRITE,MAP_SHARED);
	if(addr==MAP_FAILED)
	{
		printf("mmap err\n");
		return -1;
	}
	return 0;
}

Unmap area:
The munmap() system call does the opposite of mmap(), which removes a mapping from the virtual address space of the calling process.

int munmap(void *start, size_t length);

Synchronous mapping area:
The kernel will automatically take place in map_ The changes in the shared mapping content are written to the underlying file, but by default, the kernel does not guarantee when this synchronization operation will occur. Msync() system call allows the application to explicitly control when to complete the synchronization between the shared mapping and the mapping file, Calling msync() also allows an application to ensure that updates that occur on the writable map are visible to other processes executing read() on the file:

int  msync(void *addr,size_t len,int flags)

The addr and length parameters passed to msync() specify the starting address and size of the memory area to be synchronized. The flag parameters are as follows:
MS_SYNC: the memory area will be synchronized with the disk.
MSASYNC: the memory area is only synchronized with the kernel cache.
posix shared memory
After learning the previous memory mapping, posix shared memory is actually very simple. In fact, posix shared memory belongs to a special shared file mapping. Its fd file descriptor does not refer to a specific file, but a file created by shm_open() creates an open shared memory object, so it does not need to use keys and identifiers like System v shared memory, nor does it need to create an actual disk file like shared file mapping.
Create a posix shared memory using the following example code:

int fd;
char *addr;
fd=shm_open("./shm",flags,mode);
ftruncate(fd,size);
addr=mmap(NULL,size,PROT_READ | PROTT_WRITE,MAP_SHARED,fd,0);

Well, so far, I've almost gone through the linux communication mechanism and synchronization mechanism. I shouldn't ask such a detailed question during the interview. Ha ha, I hope I can find a good job in February next year. It's not worth reciting so many eight part essay QAQ

Topics: C Linux Embedded system