[Linux] Shared memory/message queue/semaphore for inter-process communication

1. The concept and principle of shared memory

Shared memory allows different processes to see the same memory block.

We know that each process will have a corresponding PCB-task_struct, an independent process address space, and then the address is mapped to physical memory through the page table. At this time, we can let the OS apply for a space in the memory, and then map the created memory space to the address space of the process. After mapping, the two processes that need to communicate will see the same memory space. In the next two After a process no longer wants to communicate, cancel the mapping relationship between the process and the memory—disassociate it, and then release the memory.

Insert image description here

What we need to note is that the process address space is specially designed for IPC. Shared memory is a communication method, so any process that wants to communicate can use it. Then there must be a lot of shared memory in the OS at the same time.

2. Shared memory related interface description

1.shmget function

功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
头文件
#include <sys/ipc.h>
#include <sys/shm.h>
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
其中最常用的两个标志位:
IPC_CREAT:如果文件不存在就创建,存在就获取
IPC_EXCL:无法单独使用,IPC_CREAT | IPC_EXCL,如果不存在就创建,如果存在就出错返回,这样就保证如果创建成功,一定是一个新的shm
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

2.ftok function

功能;获取key
原型
key_t ftok(const char *pathname, int proj_id);
头文件
#include <sys/types.h>
#include <sys/ipc.h>
参数
pathname:文件名
proj_id:非0整数,用于形成key
返回值:成功返回key,失败返回-1,错误码被设置

When we use the malloc function to apply for memory, we need to tell how much space to apply for. When releasing, we only need to tell the starting address of the memory. So how does the operating system know how much memory we have applied for? In fact, when we apply When using memory, the OS provides us with the requested space and the attributes of the space, such as size and so on. If a program applies for space multiple times, the operating system needs to manage these spaces. The management method is to first describe and then organize. The objects described are the attributes of these spaces. The same is true for shared memory. The OS describes it first and then organizes it, so shared memory = shared memory block + related attributes of shared memory. So when we create shared memory, how to ensure that the shared memory is unique in the system? The answer is to uniquely identify it through key. So where is the key? The key is in the attribute struct shm{key_t key;}; of the shared memory.

3.shmat function

功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
头文件
#include <sys/types.h>
#include <sys/shm.h>
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节(起始地址);失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - 
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

4.shmdt function

功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
头文件
#include <sys/types.h>
#include <sys/shm.h>
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

5.shmctl function

功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
IPC_STAT:将shmid的属性拷贝到buf中
IPC_SET:将buf中的属性写入到shmid中
IPC_RMID:是否shmid的内存
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

Insert image description here

3. Use shared memory to implement server&client communication

1.shm_server.cc

#include "comm.hpp"

int main()
{
    
    
    key_t key = getkey();
    printf("key:0x%x\n", key);
    int shmid = createShm(key);
    printf("shmid:%d\n", shmid);

    char *start = (char *)attachShm(shmid);
    printf("attach success, address start: %p\n", start);

    while (true)
    {
    
    
        printf("client say : %s\n", start);
        // struct shmid_ds ds;
        // shmctl(shmid, IPC_STAT, &ds);
        // printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x",
        //        ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);
        sleep(1);
    }

    // 去关联
    detachShm(start);

    // 删除共享内存
    delShm(shmid);
    return 0;
}

2.shm_client.cc

#include "comm.hpp"

int main()
{
    
    
    key_t key = getkey();
    printf("key:0x%x\n", key);
    int shmid = getShm(key);
    printf("shmid:%d\n", shmid);

    char *start = (char *)attachShm(shmid);
    printf("attach success, address start: %p\n", start);

    const char *message = "hello server, 我是另一个进程,正在和你通信";
    pid_t id = getpid();
    int cnt = 1;
    // char buffer[1024];
    while (true)
    {
    
    
        snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号%d]", message, id, cnt++);
        // snprintf(buffer, sizeof(buffer), "%s[pid:%d][消息编号:%d]", message, id, cnt++);
        // memcpy(start, buffer, strlen(buffer)+1);
        // pid, count, message
        sleep(1);
    }

    detachShm(start);

    return 0;
}

3.comm.hpp

#ifndef __COMM_HPP_
#define __COMM_HPP_

#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>

#define PATHNAME "."
#define PROJ_ID 0x666

// 共享内存的大小,一般建议是4KB的整数倍
// 系统分配共享内存是以4KB为单位的! --- 内存划分内存块的基本单位Page
#define MAX_SIZE 4096 // --- 内核给你的会向上取整, 内核给你的,和你能用的,是两码事

key_t getkey()
{
    
    
    key_t key = ftok(PATHNAME, PROJ_ID);
    if (key == -1)
    {
    
    
        std::cerr << "error" << strerror(errno) << std::endl;
        exit(1);
    }

    return key;
}

int getShmHelper(key_t key, int flags)
{
    
    
    int shmid = shmget(key, MAX_SIZE, flags);
    if (shmid < 0)
    {
    
    
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }

    return shmid;
}

int getShm(key_t key)
{
    
    
    return getShmHelper(key, IPC_CREAT);
}

int createShm(key_t key)
{
    
    
    return getShmHelper(key, IPC_CREAT | IPC_EXCL | 0600);
}

void *attachShm(int shmid)
{
    
    
    void *mem = shmat(shmid, nullptr, 0);
    if ((long long)mem == -1L)
    {
    
    
        std::cerr << "shmat:" << errno << ":" << strerror(errno) << std::endl;
        exit(3);
    }

    return mem;
}

void detachShm(void *start)
{
    
    
    if (shmdt(start) == -1)
    {
    
    
        std::cerr << "shmdt:" << errno << ":" << strerror(errno) << std::endl;
    }
}

void delShm(int shmid)
{
    
    
    if (shmctl(shmid, IPC_RMID, nullptr) == -1)
    {
    
    
        std::cerr << "shmctl:" << errno << ":" << strerror(errno) << std::endl;
    }
}
#endif

4. View ipc resources and their characteristics

Instructions for viewing IPC resources

ipcs -m/-q/-s
分别查看共享内存/消息队列/信号量
删除使用  ipcrm -m shmid

Insert image description here

Characteristics of IPC resources

The life cycle of shared memory depends on the OS, not the process.

After we use ctrl C to terminate the program, the file exists for the second time.

Insert image description here

5. Advantages and disadvantages of shared memory

Advantages: It is the fastest among all inter-process communications and can greatly reduce the number of data copies.

Shared memory areas are the fastest form of IPC. Once such memory is mapped into the address space of the process sharing it, these inter-process data transfers are no longer involved

The kernel, in other words processes no longer pass data to each other by executing system calls into the kernel

Insert image description here

If the same code is implemented using pipelines, consider the pipeline and shared memory, keyboard input and monitor output. There will be several data copies in the shared memory and pipeline.

Insert image description here

For pipes, we copy the input data line to our own defined buffer buffer, then write to the pipe through the system call write, then read it into our own defined buffer buffer through read, and then copy it to the display. Here we do not consider the copy from the keyboard to stdin and the copy from stdout to the display during input, so the pipeline needs to be copied 4 times in total.

For shared memory, we only need to copy the data to the shared memory, and then copy it from the shared memory to the display. Similarly, the copying from the keyboard to stdin and stdout to the display is not considered here, so a total of 2 copies are needed.

Disadvantages: Synchronization and mutual exclusion operations cannot be performed, and no data protection is done.

6. Data structure of 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 */
};

Insert image description here

The struct shmid_ds structure contains the struct ipc_perm structure, which contains the key

4. System V message queue

Message queue is a data structure provided by the kernel for inter-process communication. Two processes can communicate with each other. Each node of the queue contains a type identifier, indicating whether the data is read or written.

The interface of the message queue is as follows:

int msgget(key_t key, int msgflg);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

Message queues provide a way to send a block of data from one process to another.

Each data block is considered to have a type, and the data blocks received by the receiver process can have different type value characteristics.

IPC resources must be deleted, otherwise they will not be cleared automatically unless restarted, so the life cycle of system V IPC resources depends on the kernel.

I won’t go into too much introduction here, we just need to understand the message queue.

5. System V semaphore

The essence of a semaphore is a counter, which is usually used to indicate the number of resources in public resources. Public resources are resources that can be accessed by multiple processes at the same time. Accessing unprotected public resources can lead to data inconsistency.

Why do we let different processes see the same resource? Because we need to communicate and achieve collaboration between processes, but processes are independent. We let the processes see it and propose some methods, but it also introduces some new problems ( Data inconsistency problem)

Our commons will be protected in the future: Critical resources, mostly independent

Resources (memory, files, network, etc.) need to be used. How to use them by the process? The process must have corresponding code to access this part of the critical resource. This part of the code is called the critical section, and other codes are called is a non-critical region

How do we protect data: synchronization and mutual exclusion. Mutual exclusion means that when I access critical resources, others cannot access them.

Atomicity: either don’t do it, or do it if you want to, there are only two states

Why do we need a semaphore?

Let’s take an example. When we watch a movie in a cinema, we don’t just sit in the corresponding seat in the cinema. This seat CIA belongs to me, but when we buy the ticket, we reserve the seat in the screening hall. At this time, The seats are already ours. That is, when we want a certain resource, we can make a reservation. If there are 100 seats in the auditorium, will the cinema sell 101 tickets? The answer is no. How can we do it? We can define a semaphore with an initial value of 100. When a ticket is sold, it will be -1. When it is 0, it will stop selling tickets.

信号量sem=100
sem--;//预定资源   --- P操作
访问公共资源
sem++;//释放公共资源  --- V操作

All processes must first apply for the sem semaphore before accessing public resources -> The prerequisite for applying for the semaphore first is that all processes must first see the same semaphore -> The semaphore itself is a public resource -> The semaphore itself must also ensure its own safety. –, ++ -> The semaphore must ensure the safety of its own operations. –, ++ operations are atomic

Shared resources can be treated as a whole resource or divided into sub-parts of resources.

When the initial value of a semaphore is 1, then the semaphore is called a binary semaphore and has a mutually exclusive function.

Since each process requires shared resources, and some resources need to be used mutually exclusive, processes compete to use these resources. This relationship between processes is the mutual exclusion of processes.

Some resources in the system are only allowed to be used by one process at a time. Such resources are called critical resources or mutually exclusive resources.

The program section involving mutually exclusive resources in the process is called a critical section.

IPC resources must be deleted, otherwise they will not be cleared automatically unless restarted, so the life cycle of system V IPC resources depends on the kernel.

6. Organization of IPC resources

We can find that the data structures of the three modes of system V are as follows:

Shared memory:

Insert image description here

message queue:

Insert image description here

Signal amount:

Insert image description here

We found that their interfaces are very similar, which shows that they are all system V inter-process communication.

We can create an array of struct ipc_perm *perms[] to store struct ipc_perm

We create an object of shared memory data structure struct shmid_ds myshm, and then perms[0] = &myshm.shm_perm

Create a message queue data structure object struct msqid_ds mymsg, prems[1] = &mymsg.msg_perm

Then when we get the properties of the shared memory, we can get them in the following way

(struct shmid_ds*)perms[0] ->Other properties

This is because the address of the first member of a structure is numerically equal to the address of the structure object itself.

This method is the same as polymorphism in C++. The objects in the array are base classes, and the specific objects (shared memory, message queues, semaphores) are subclasses.

Guess you like

Origin blog.csdn.net/qq_67582098/article/details/134917061