Introduction to interprocess communication
Concept of interprocess communication
Process communication refers to the transmission of data (exchange of information) between processes
Purpose of interprocess communication
- Data transfer: one process needs to send its data to another process
- Resource sharing: multiple processes share the same resources
- Notification event: a process needs to send a message to another process or group of processes to notify it (them) of an event (such as notifying the parent process when the process terminates).
- Process control: some processes want to fully control the execution of another process (such as Debug process). At this time, the control process wants to be able to intercept all traps and exceptions of another process and know its state changes in time.
Development of interprocess communication
- The Conduit
- System V interprocess communication
- POSIX interprocess communication
Interprocess communication classification
The Conduit
- Anonymous Pipe
- name pipes
System V IPC
- System V message queue
- System V shared memory
- System V semaphore
POSIX IPC
- Message queue
- Shared memory
- Semaphore
- mutex
- Conditional variable
- Read write lock
The Conduit
What is a pipe
Pipeline is the oldest form of interprocess communication in Unix. We call a data flow from one process to another a "pipeline"“
Example: count the number of users logged in on the ECS
After running, who and wc become two processes. Who processes type data into the "pipeline" through standard output, and wc processes read data from the "pipeline" through standard input.
The who command is used to view the login users of the current ECS (one user is displayed in one line), and wc -l is used to count the current number of lines.
Anonymous Pipe
pipe function
Role: create anonymous pipes
Function prototype:
int pipe(int pipefd[2]);
The parameter of the pipe function is an output parameter. The array pipefd is used to return two file descriptors pointing to the read and write ends of the pipe:
pipefd[0] represents the file descriptor at the reading end of the pipeline
pipefd[1] represents the file descriptor of the write end of the pipeline
The pipe function returns 0 when the call is successful and - 1 when the call fails.
To use anonymous pipes
- The parent process calls the pipe function to create a pipe.
2. The parent process creates a child process.
3. The parent process closes the write side and the child process closes the read side.
be careful:
- The pipeline can only conduct one-way communication. Therefore, after the parent process creates the child process, it is necessary to confirm who reads and writes the parent-child process, and then close the corresponding read-write end.
- Data written from the write side of the pipeline is buffered by the kernel until it is read from the read side of the pipeline.
From the perspective of file descriptor - deep understanding of pipeline
- The parent process calls the pipe function to create a pipe.
- The parent process creates a child process.
- The parent process closes the write side and the child process closes the read side.
Code demonstration
#include<stdio.h> #include<unistd.h> #include<sys/wait.h> #include<string.h> #include<stdlib.h> int main() { int pipe_fd[2]={0}; if(pipe(pipe_fd)<0){//Creating anonymous pipes using pipe perror("pipe"); return 1; } pid_t id=fork(); if(id<0){ perror("fork"); return 2; } else if(id==0){ // write24 // child close(pipe_fd[0]); // The child process closes the reader const char *msg="hello parent,I am child\n"; int count=5; while(count) { //The child process writes data to the pipeline write(pipe_fd[1],msg,strlen(msg)); sleep(1); count--; } close(pipe_fd[1]);//After the subprocess is written, close the file exit(0); } else{ // read41 // parent close(pipe_fd[1]); //The parent process closes the write side char buffer[64]; //The parent process reads data from the pipe while(1){ buffer[0]=0; ssize_t size=read(pipe_fd[0],buffer,sizeof(buffer)-1); if(size > 0){ buffer[size]=0; printf("parent get message from child# %s",buffer); } else if(size==0){ printf("pipe file close , child quit!\n"); break; } else{ break; } } int status=0; if(waitpid(id,&status,0)>0){ printf("child quit,wait success!\n"); } close(pipe_fd[0]); } return 0; }
Operation results:
Pipeline read / write rules
- When no data is readable
O_NONBLOCK disable: the read call is blocked, that is, the process suspends execution until data arrives.
O_NONBLOCK enable: the read call returns - 1, and the errno value is EAGAIN.
- When the pipe is full
O_NONBLOCK disable: the write call blocks until a process reads the data
O_NONBLOCK enable: the call returns - 1, and the errno value is EAGAIN
- If the file descriptors corresponding to all pipeline write ends are closed, read returns 0
- If the file descriptors corresponding to all pipeline readers are closed, the write operation will generate a signal SIGPIPE, which may cause the write process to exit
- When the amount of data to be written is not greater than pipe_ When buf, linux will ensure the atomicity of writing.
- When the amount of data to be written is greater than pipe_ When buf, linux will no longer guarantee the atomicity of writing.
Pipeline characteristics
- It can only be used for communication between processes with common ancestors (processes with kinship); Usually, a pipeline is created by a process, and then the process calls fork. After that, the pipeline can be applied between parent and child processes.
- Pipeline provides streaming services
For the data written by process A into the pipeline, the amount of data read by process B from the pipeline each time is arbitrary. This is called streaming service, which corresponds to datagram service: the data is clearly divided, and the data is taken according to the message segment.
- Generally speaking, the process exits and the pipeline is released, so the life cycle of the pipeline changes with the process
- Generally speaking, the kernel will synchronize and mutually exclusive pipeline operations
Synchronization: two or more processes are synchronized during operation and run in A predetermined order. For example, the operation of task A depends on the data generated by task B.
Mutual exclusion: a public resource can only be used by one process at the same time. Multiple processes cannot use public resources at the same time.
- The pipeline is half duplex, and the data can only flow in one direction; When both parties need to communicate, two pipelines need to be established
Simplex communication: data transmission in simplex mode is unidirectional. In both communication parties, one party is fixed as the sending end and the other party is fixed as the receiving end.
Half duplex: half duplex data transmission means that data can be transmitted in two directions of a signal carrier, but cannot be transmitted at the same time.
Full duplex communication: full duplex communication allows data to be transmitted simultaneously in two directions. Its ability is equivalent to the combination of two simplex communication modes. Full duplex can simultaneously (instantaneously) transmit signals in both directions.
Four special cases of pipeline
- If the writer process does not write and the reader process keeps reading, the corresponding reader process will be suspended because there is no data readable in the pipeline. The reader process will not be awakened until there is data in the pipeline.
- If the reader process does not read and the writer process keeps writing, then when the pipeline is full, the corresponding writer process will be suspended until the data in the pipeline is read by the reader process, and the writer process will not be awakened.
- The writer process closes the writer after writing the data. Then, after reading the data in the pipeline, the reader process will continue to execute the code logic after the process without being suspended.
- If the reading end process closes the reading end and the writing end process is still writing data to the pipeline, the operating system will kill the writing end process.
We can use the following code to see what kind of signal the writing segment receives in the fourth case
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { int fd[2] = { 0 }; if (pipe(fd) < 0){ //Create anonymous pipe perror("pipe"); return 1; } pid_t id = fork(); if (id == 0){ //child close(fd[0]); //The child process closes the reader //The child process writes data to the pipeline const char* msg = "hello father, I am child..."; int count = 10; while (count--){ write(fd[1], msg, strlen(msg)); sleep(1); } close(fd[1]); //Close write end exit(0); } //father close(fd[1]); //The parent process closes the write side close(fd[0]); //The parent process closes the reader directly (causing the child process to be killed by the operating system) int status = 0; waitpid(id, &status, 0); printf("child get signal:%d\n", status & 0x7F); //The signal received by the printing subprocess return 0; }
The writer receives a 13 signal
We can check the meaning of the signal through the kill -l command
Pipe size
The capacity of the pipeline is limited, so how much data can the pipeline store at the same time?
Method 1:
Use the ulimit -a command to view the current resource limit settings.
According to the display, the maximum capacity of the pipeline is 512 × 8 = 4096 bytes.
Method 2:
You can constantly write data to the pipeline to see how much data can be written at most
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> int main() { int fd[2] = { 0 }; if (pipe(fd) < 0){ //Create anonymous pipe perror("pipe"); return 1; } pid_t id = fork(); if (id == 0){ //child close(fd[0]); //The child process closes the reader char c = 'a'; int count = 0; //The subprocess keeps writing, one byte at a time while (1){ write(fd[1], &c, 1); count++; printf("%d\n", count); //Number of bytes currently printed and written } close(fd[1]); exit(0); } //father close(fd[1]); //The parent process closes the write side waitpid(id, NULL, 0); close(fd[0]); return 0; }
Operation results:
Different operating systems have different pipe sizes, and everything is subject to reality.
name pipes
- One limitation of pipeline application is that it can only communicate between processes with a common ancestor (kinship).
- If we want to exchange data between unrelated processes, we can do this by using FIFO files, which are often called named pipes. Named pipeline is a special type of file. Two processes open the same pipeline file through the file name of the named pipeline. At this time, the two processes see the same resource and can communicate.
be careful:
- Ordinary files are difficult to communicate. The named pipeline here is a special type of file.
- Named pipes, like anonymous pipes, are memory files, except that named pipes have a simple image on disk, but the size of this image is always 0, because both named pipes and anonymous pipes will not refresh communication data to disk.
Create named pipe
We can use the mkfifo command to create a named pipe.
Using this named pipeline file, the communication between two processes can be realized.
In one process (process A), we use shell script to write A string to the named pipe every second, and in another process (process B), we use cat command to read from the named pipe.
When the reading process of the pipeline exits, it is meaningless for the writing process to write data to the pipeline. At this time, the writing process will be killed by the operating system. After we terminate the reader process, because the circular script executed by the writer is executed by the command-line interpreter bash, bash will be killed by the operating system and our ECS will exit.
Create named pipes in process
The mkfifo function is required to create named pipes in the process
int mkfifo(const char *pathname, mode_t mode);
Parameters:
pathname
- If pathname is given as a path, the named pipe file is created under the pathname path.
- If pathname is given in the form of file name, the named pipe file will be created in the current path by default.
mode
Default permissions for creating named pipe files.
Return value:
If the creation is successful, return 0. If the creation fails, return - 1.
Using named pipe to realize the communication of different files
Let's write a server and client program first
Server code
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #define FIFO "./fifo" int main() { int ret=mkfifo(FIFO,0644); // Create a named pipe file if(ret<0){ perror("mkfilo"); return 1; } int fd=open(FIFO,O_RDONLY);//Open the named pipe file read-only if(fd<0){ perror("open"); return 2; } char buffer[128]; while(1){ buffer[0]=0; //Read information from the named pipe into the buffer ssize_t s=read(fd,buffer,sizeof(buffer)-1); if(s>0){ buffer[s]=0; printf("client# %s\n",buffer); / / output the information sent by the client } else if(s==0){ printf("client quit...\n"); break; } else{ break; } } close(fd);//Close the named pipe file return 0; }
Client code
#include<stdio.h> #include<string.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #define FIFO "./fifo"9 int main() { int fd=open(FIFO,O_WRONLY); if(fd<0){ perror("open"); return 2; } char buffer[128]; while(1){ printf("Please Enter# "); fflush(stdout); buffer[0]=0; ssize_t s=read(0,buffer,sizeof(buffer)-1); if(s>0){ buffer[s]=0; write(fd,buffer,strlen(buffer)); } else if(s==0){ printf("client quit...\n"); break; } else{ break; } } return 0; }
When the server and client code are running, we can input information from the client, and then the server can receive it.
system V shared memory
There are three communication modes provided by system V IPC:
- system V shared memory
- system V message queue
- system V semaphore
System V shared memory and system V message queue are designed to transmit data, while System V semaphores are designed to ensure synchronization and mutual exclusion between processes.
Basic principles of shared memory
Shared memory in order to let different processes see the same resource, apply for a piece of memory space in physical memory, and then map this memory space with the page tables of each process. Open up a space in the process virtual address space and fill the virtual address into the corresponding position of each page table, so as to establish a corresponding mapping relationship between the virtual address and the physical address, so that different processes can see the same physical memory, that is, shared memory.
Shared memory data structure
There may be a large number of processes communicating in the system, so there may be a large number of shared memory in the system, so the operating system must manage it. Therefore, in addition to opening up space in the memory, the system must maintain relevant kernel data structures for the shared memory.
struct shmid_ds { struct ipc_perm shm_perm; /* operation perms */ int shm_segsz; /* size of segment (bytes) */ __kernel_time_t shm_atime; /* last attach time */ __kernel_time_t shm_dtime; /* last detach time */ __kernel_time_t shm_ctime; /* last change time */ __kernel_ipc_pid_t shm_cpid; /* pid of creator */ __kernel_ipc_pid_t shm_lpid; /* pid of last operator */ unsigned short shm_nattch; /* no. of current attaches */ unsigned short shm_unused; /* compatibility */ void *shm_unused2; /* ditto - used by DIPC */ void *shm_unused3; /* unused */ };
Shared memory function
shmget function
Function: create shared memory
Function prototype:
int shmget(key_t key, size_t size, int shmflg);
Return value:
- The successful call returns the identification code of the shared memory segment
- Call failed, return - 1
Parameter Description:
- The first parameter, key, indicates the unique identifier of the shared memory to be created in the system (similar to hash table).
- The second parameter, size, indicates the size of the shared memory to be created. (4KB integer multiple is recommended)
- The third parameter, shmflg, indicates how shared memory is created.
Note: the shared memory allocated by the system is allocated by 4KB integer times. If the size of the created shared memory is not 4KB integer times, it will cause a waste of space.
Note: the first parameter key passed into shmget function needs to be obtained by ftok function
ftok function prototype
key_t ftok(const char *pathname, int proj_id);
The ftok function is used to combine an existing pathname with an integer identifier proj_id is converted into a key value, called IPC key value. When using shmget function to obtain shared memory, this key value will be filled into the data structure maintaining shared memory. Note that the file specified by pathname must exist and be accessible.
shmflg
The third parameter shmflg of shmget function is passed in. There are two common combinations:
- IPC_CREAT: if there is no shared memory with the same key value in the kernel, a new one will be created. If there is, the shared memory will be returned directly.
- IPC_CREAT | IPC_EXCL: if there is no shared memory with the same key value in the kernel, a new one will be created. If there is, an error will be reported directly.
Use of shmget function
We use the shmget function to create shared memory, and use the ipcs command to view relevant information.
The ipcs instruction can view information about interprocess communication
We can add options after ipcs to view the information of specified communication facilities
- -q: Lists information about message queuing.
- -m: Lists information about shared memory.
- -s: List semaphore related information.
example
We can use ipcs -m to view information about shared memory.
The meaning of each column of information output by ipcs command is as follows:
title | meaning |
---|---|
key | The system distinguishes the unique identification of each shared memory |
shmid | Shared memory d |
owner | Owner of shared memory |
perms | Permissions for shared memory |
bytes | Size of shared memory |
nattch | Number of processes associated with shared memory |
status | Status of shared memory |
The shmget function creates shared memory
comm.h file
#pragma once #include<stdio.h> #define PATH_NAME "/home/nzb/lesson20" #define PROJ_ID 0x6666 #define SIZE 4097
server.c Documents
#include"comm.h" #include<sys/ipc.h> #include<sys/shm.h> int main() { key_t k=ftok(PATH_NAME,PROJ_ID); if(k<0){ perror("ftok"); return 1; } printf("key:%x\n",k); int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL);// The shared memory does not exist, and an error is reported when > is created if(shmid<0){ perror("shmget"); return 2; } printf("shmid:%d\n",shmid); return 0; }
Operation results
From the figure, we can see that after the server process exits, the newly created shared memory is not deleted because
All ipc resources follow the kernel, not the process
So how to release ipc resources?
- When the process exits, call the function to release, or delete with an instruction.
- Restart the operating system.
So how to delete in the process, we need to introduce shmctl function here.
shmctl function
Function: delete shared memory
Function prototype:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
Parameters:
- The first parameter shmid represents the shared memory identification code returned by shmget.
- The second parameter cmd indicates the action to be taken (there are three values).
- The third parameter buf refers to a data structure that holds the mode state and access rights of shared memory.
Return value:
- shmctl is deleted successfully, and 0 is returned.
- shmctl deletion failed, return - 1.
The second parameter of shmctl function is passed in the following three common options:
command | explain |
---|---|
IPC_STAT | Set shmid_ Set in DS structure as the current association value of shared memory |
IPC_SET | On the premise that the process has sufficient permissions, set the current association value of shared memory to shmid_ The value given in the DS data structure |
IPC_RMID | Delete shared memory segment |
Example:
Create shared memory in the following code and release it after 2 seconds.
#include"comm.h" #include<sys/ipc.h> #include<sys/shm.h> #include<unistd.h> int main() { // Create key key_t k=ftok(PATH_NAME,PROJ_ID); if(k<0){ perror("ftok"); return 1; } printf("key:%x\n",k); // Request shared memory int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL);// The shared memory does not exist, and an error is reported if(shmid<0){ perror("shmget"); return 2; } printf("shmid:%d\n",shmid); sleep(2); // Free shared memory shmctl(shmid,IPC_RMID,NULL); printf("delete shm!\n"); return 0; }
When the program is running, we can use the following monitoring script to pay attention to the resource allocation of shared memory at all times:
while :; do ipcs -m;echo "###################################";sleep 1;done
Operation results
shmat and shmdt functions
shmat function
Function: connect shared memory to process address space
Function prototype:
void *shmat(int shmid, const void *shmaddr, int shmflg);
Parameters:
- The first parameter shmid is the shared memory ID
- The second parameter shmaddr specifies that the shared memory is mapped to an address in the process address space. It is usually set to NULL, which means that the kernel decides an appropriate address location by itself.
- The third parameter shmflg represents some properties set when associating shared memory.
Return value:
- The shmat call succeeds, and returns the starting address mapped from the shared memory to the process address space.
- shmat call failed and returned (void*)-1.
There are three common options that shmflg passes in
option | effect |
---|---|
SHM_RDONLY | Read only after associating shared memory |
SHM_RND | If shmaddr is not NULL, the associated address will be automatically adjusted downward to an integer multiple of SHMLBA. Formula: shmaddr-(shmaddr%SHMLBA) |
0 | The default is read-write permission |
shmdt function
Function: cancel the association between shared memory and process address space
Function prototype:
int shmdt(const void *shmaddr);
parameter
- The starting address of the shared memory to be decorrelated, that is, the starting address obtained when the shmat function is called.
Return value
- The shmdt call succeeds and returns 0.
- The shmdt call failed and returned - 1.
Use demo
#include"comm.h" #include<sys/ipc.h> #include<sys/shm.h> #include<unistd.h> int main() { // Create key key_t k=ftok(PATH_NAME,PROJ_ID); if(k<0){ perror("ftok"); return 1; } printf("key:%x\n",k); // Request shared memory int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);// Shared memory does not exist. Create > error if(shmid<0){ perror("shmget"); return 2; } printf("shmid:%d\n",shmid); sleep(1); // Associate the current process with shared memory char* start=(char*)shmat(shmid,NULL,0); printf("server already attach on shared memory!\n"); // You can use shared memory to communicate sleep(1); // Disassociate the current shared memory shmdt(start); printf("server already dattch off shared memory!\n"); sleep(1); // Free shared memory shmctl(shmid,IPC_RMID,NULL); printf("delete shm!\n"); return 0; }
Operation results
be careful:
The shmat function connects the shared memory to the process address space. The shared memory needs to have corresponding permissions. When we use the shmget function to create the shared memory, we need to set the permissions after the shared memory is created at its third parameter. The permission setting rules are the same as those for setting file permissions.
int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);
Communication with shared memory
After knowing the related functions of shared memory, we can try to realize the communication between shared memory
Header file comm.h
#pragma once #include<stdio.h> #define PATH_NAME "/home/nzb/lesson20" #define PROJ_ID 0x6666 #define SIZE 4097
server.c Documents
#include"comm.h" #include<sys/ipc.h> #include<sys/shm.h> #include<unistd.h> int main() { // Create key key_t k=ftok(PATH_NAME,PROJ_ID); if(k<0){ perror("ftok"); return 1; } printf("key:%x\n",k); // Request shared memory int shmid=shmget(k,SIZE,IPC_CREAT | IPC_EXCL | 0644);// Shared memory does not exist. Create > error if(shmid<0){ perror("shmget"); return 2; } printf("shmid:%d\n",shmid); // Associate the current process with shared memory char* start=(char*)shmat(shmid,NULL,0); printf("server already attach on shared memory!\n"); // You can use shared memory to communicate while (1){ printf("%s\n", start); sleep(1); } // Disassociate the current shared memory shmdt(start); printf("server already dattch off shared memory!\n"); // Free shared memory shmctl(shmid,IPC_RMID,NULL); printf("delete shm!\n"); return 0; }
client.c Documents
#include"comm.h" #include<sys/ipc.h> #include<sys/shm.h> #include<sys/types.h> #include<unistd.h> int main() { // Get the same key key_t k=ftok(PATH_NAME,PROJ_ID); if(k<0){ perror("ftok"); return 1; } printf("%x\n",k); //You don't need to create shm to get shared memory int shmid=shmget(k,SIZE,IPC_CREAT); if(shmid<0){ perror("shmget"); return 2; } // Associated shared memory char* start=(char*)shmat(shmid,NULL,0); //The client keeps writing data to the shared memory int i = 0; while (1){ start[i] = 'A' + i; i++; sleep(1); } // Decorrelation shmdt(start): return 0; }
Operation results