[Linux] Inter-process communication

  1. prerequisite knowledge

Processes are independent , and now we need to break the independence and communicate, so the cost of communication must be high.
Sometimes it is necessary for multiple processes to cooperate to complete certain business content. For example, before: cat file | grep "hello", this is the communication between two processes.
The essence of communication is: 1. The OS directly or indirectly provides "memory space" for both parties to communicate; 2. Both parties who want to communicate must see this "common resource" (generally provided by the OS) at the same time.
The essence of different communication types is: which module of the OS provides the "public resources" mentioned above. For example: if it is provided by a file, it is pipeline communication, and if it is provided by a system V communication module, it is system V communication.
So to communicate, first let different processes see the same resource (mainly), and then communicate.

1.1. Inter-process communication purpose

Data transfer : one process needs to send its data to another process

Resource sharing : The same resource is shared between multiple processes.

Notification event : A process needs to send a message to another process or group of processes, informing it (they) that some event (such as process termination

to notify the parent process).

Process control : Some processes want to completely control the execution of another process (such as Debug process), at this time the control process hopes to be able to intercept the execution of another process

All traps and exceptions of a process, and be able to know its state changes in time.

1.2. Inter-process communication development (classification)

pipeline

anonymous pipe
named pipe

System V interprocess communication

System V Message Queues (Learn)
System V Shared Memory
System V Semaphores (Learn)

POSIX interprocess communication (current mainstream)

message queue
shared memory semaphore
mutex
condition
variable
read-write lock

Pipeline: It is an ancient communication method and a method based on the file system.

System V: Focus on the process communication of the host, cannot cross the host, and now it is almost eliminated by the mainstream.

POSIX: Allows communication across hosts.

  1. anonymous pipe

2.1. Anonymous pipe definition

Pipes are the oldest form of interprocess communication in Unix.

We call a flow of data from one process to another a "pipe"

2.1.1. Basic principles

The above file used for pipeline communication is called a pipeline file, which is different from ordinary files:

Ordinary files not only have their own kernel buffers, but also corresponding disk space. The buffer file will be flushed to disk according to certain rules.

The pipeline file only has the kernel buffer, and there is no need to flush the data to the disk, and the corresponding file does not need to exist on the disk.

So the pipeline mentioned here is a memory-level file. The OS only needs to create its own kernel buffer and the corresponding read and write methods. It does not care whether the file actually exists on the disk, and does not need the IO process.

2.1.2. Create an anonymous pipe (pipe)

Anonymous pipes are usually used for communication between processes that are related by blood.

#include <unistd.h>
//功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
fd:输出型参数。
返回值:
成功返回0,失败返回-1,并且填写错误代码

After creation:

after fork

Example:

#include <iostream>
#include<unistd.h>
#include<cassert>
 #include <cstdlib>
 #include<cstdio>
 #include<cstring>
//一般在c++程序中包含C语言头文件的时候可以使用#include<assert.h>
//但是#include<cassert>兼容性更好一些。
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
    int fd[2];
    int ret = pipe(fd);
    //创建了一个匿名管道文件,并且被把读取端和写入端的文件描述符存入fd数组中。
    //fd[0]:读取端
    //fd[1]:写入端
    std::cout<<"fd[0]:"<<fd[0]<<std::endl;//3,这个是读取端
    std::cout<<"fd[1]:"<<fd[1]<<std::endl;//4,这个是写入端
    int id = fork();
    assert(id>= 0);
    if(id == 0)
    {//子进程
        //我们让子进程给父进程发送信息
        //子进程写入,父进程读取。
        //子进程 --->  父进程
        //我们关系子进程的读取端
        close(fd[0]);
        //开始发送信息
        const char* str = "我是子进程,我正在给你发消息!";
        int ct =1;
        while(true)
        {
            char buf[1024];
            snprintf(buf, sizeof buf,"child---->parent say: %s[%d],我的PID是:%d",str,ct++,getpid());
            write(fd[1], buf, strlen(buf));//这里strlen不需要加1
            //主要不能直接使用 ,文件描述符常数,不能使用 3,4等等。
            sleep(1);
        }
        //严谨一点,用完之后把写入端也关闭。
        close(fd[1]);
        exit(0);
    }
    //父进程
    //子进程 --->  父进程
    close(fd[1]);//关闭父进程的写入端,保留读取
    //开始读取信息。
    while(true)
    {
        char buf[1024];
        ssize_t s = read(fd[0], buf, sizeof(buf) - 1);//我们希望当成字符串处理,所以sizeof需要 -1 最后一位放置 '\0'
        if(s > 0 )//读取成功了
        {
            buf[s] = 0;
        }
        std::cout<<"我是父进程我收到一条消息:"<<"#############"<<buf<<std::endl;
    }
    ret = waitpid(id,0, 0);//阻塞等待
    assert(ret == id);
    //严谨一点,用完之后把写读取也关闭。
    close(fd[0]);
    return 0;
}

This communication method is channel communication (this channel is an anonymous channel)

This also proves that the write and read we talked about before are written to the buffer in the kernel, not directly to the file.

2.1.3. Characteristics of pipelines

read fast write slow

If there is no data in the pipeline, read again at the reading end, by default it will directly block the currently reading process.
int main()
{
    int fd[2];
    int ret = pipe(fd);
    std::cout<<"fd[0]:"<<fd[0]<<std::endl;
    std::cout<<"fd[1]:"<<fd[1]<<std::endl;
    int id = fork();
    assert(id>= 0);
    if(id == 0)
    {//子进程
        close(fd[0]);
        const char* str = "我是子进程,我正在给你发消息!";
        int ct =1;
        while(true)
        {
            char buf[1024];
            snprintf(buf, sizeof buf,"child---->parent say: %s[%d],我的PID是:%d",str,ct++,getpid());
            write(fd[1], buf, strlen(buf));
            sleep(1000);//我们让子进程写入一条不写了,
        }
        close(fd[1]);
        exit(0);
    }

    //父进程
    close(fd[1]);
    while(true)
    {
        char buf[1024];
        std::cout<<"正在读取!!"<<std::endl;
        ssize_t s = read(fd[0], buf, sizeof(buf) - 1);
        std::cout<<"读取完成!!"<<std::endl;
        if(s > 0 )//读取成功了
        {
            buf[s] = 0;
        }
        std::cout<<"我是父进程我收到一条消息:"<<"#############"<<buf<<std::endl;
    }
    ret = waitpid(id,0, 0);
    assert(ret == id);
    return 0;
}

write fast read slow

If there is no reading or the reading is very slow, the writing end will keep writing until it is full.
int main()
{
    int fd[2];
    int ret = pipe(fd);
    std::cout<<"fd[0]:"<<fd[0]<<std::endl;
    std::cout<<"fd[1]:"<<fd[1]<<std::endl;
    int id = fork();
    assert(id>= 0);
    if(id == 0)
    {//子进程
        close(fd[0]);
        const char* str = "我是子进程,我正在给你发消息!";
        int ct =1;
        while(true)
        {
            char buf[1024];
            ct++;
            snprintf(buf, sizeof buf,"child---->parent say: %s[%d],我的PID是:%d",str,ct,getpid());
            write(fd[1], buf, strlen(buf));
            //sleep(1000);//我们让子进程快速写入
            std::cout<<ct<<std::endl;
        }
        close(fd[1]);
        exit(0);
    }

    //父进程
    close(fd[1]);
    while(true)
    {
        sleep(1000);//让父进程读取慢一点
        char buf[1024];
        std::cout<<"正在读取!!"<<std::endl;
        ssize_t s = read(fd[0], buf, sizeof(buf) - 1);
        std::cout<<"读取完成!!"<<std::endl;
        if(s > 0 )//读取成功了
        {
            buf[s] = 0;
        }
        std::cout<<"我是父进程我收到一条消息:"<<"#############"<<buf<<std::endl;
    }
    ret = waitpid(id,0, 0);
    assert(ret == id);
    return 0;
}

So a pipe is a single, fixed-sized memory space.

That is, the read end, the pipeline is empty and blocked, and the write end is full and blocked

read rules

The writing of the pipeline is written line by line, but the reading is not done line by line, but according to the maximum number of bytes allowed to be read.
int main()
{
    int fd[2];
    int ret = pipe(fd);
    std::cout<<"fd[0]:"<<fd[0]<<std::endl;
    std::cout<<"fd[1]:"<<fd[1]<<std::endl;
    int id = fork();
    assert(id>= 0);
    if(id == 0)
    {//子进程
        close(fd[0]);
        const char* str = "我是子进程,我正在给你发消息!";
        int ct =1;
        while(true)
        {
            char buf[1024];
            ct++;
            snprintf(buf, sizeof buf,"child---->parent say: %s[%d],我的PID是:%d",str,ct,getpid());
            write(fd[1], buf, strlen(buf));
            //sleep(1000);//我们让子进程快速写入
            std::cout<<ct<<std::endl;
        }
        close(fd[1]);
        exit(0);
    }

    //父进程
    close(fd[1]);
    while(true)
    {
        sleep(3);//让管道快速被写满,然后父进程去读取管道的内容。
        char buf[1024];
        std::cout<<"正在读取!!"<<std::endl;
        ssize_t s = read(fd[0], buf, sizeof(buf) - 1);
        std::cout<<"读取完成!!"<<std::endl;
        if(s > 0 )//读取成功了
        {
            buf[s] = 0;
        }
        std::cout<<"我是父进程我收到一条消息:"<<"#############"<<buf<<std::endl;
    }
    ret = waitpid(id,0, 0);
    assert(ret == id);
    return 0;
}

write back read to 0

int main()
{
    int fd[2];
    int ret = pipe(fd);
    std::cout<<"fd[0]:"<<fd[0]<<std::endl;
    std::cout<<"fd[1]:"<<fd[1]<<std::endl;
    int id = fork();
    assert(id>= 0);
    if(id == 0)
    {//子进程
        close(fd[0]);
        const char* str = "我是子进程,我正在给你发消息!";
        int ct =1;
        while(true)
        {
            char buf[1024]; 
            snprintf(buf, sizeof buf,"child---->parent say: %s[%d],我的PID是:%d",str,ct++,getpid());
            write(fd[1], buf, strlen(buf));
            //sleep(1000);
            break;//让子进程直接退出(写端退出)
        }
        close(fd[1]);//子进程关闭写端文件描述符号。
        std::cout<<"子进程关闭写端文件描述符号"<<std::endl;
        exit(0);
    }

    //父进程
    close(fd[1]);
    while(true)
    {
        sleep(1);
        char buf[1024];
        ssize_t s = read(fd[0], buf, sizeof(buf) - 1);
        //如果写入端没有关闭,但是管道数据被读取完了,读取端进程会被阻塞
        //如果写入端文件描述符被关闭了,管带数据被读取完了,read会读取到文件结尾,read就会读取到0个字符
        //read返回值就是0;
        if(s > 0 )//读取成功了
        {
            buf[s] = 0;
            std::cout<<"我是父进程我收到一条消息:"<<"#############"<<buf<<std::endl;
        }
        else if ( s == 0)
        {
            //子进程读到文件结尾。
            std::cout<< "read:" << s << std::endl;
            break;//父进程不读了
        }
        
        
    }
    ret = waitpid(id,0, 0);
    assert(ret == id);
    return 0;
}

The write has exited, and the reader will read 0.

read off

The reading is closed, but the writing end is not yet closed. In this case, the OS will send a termination signal to the writing process.

int main()
{
    int fd[2];
    int ret = pipe(fd);
    std::cout<<"fd[0]:"<<fd[0]<<std::endl;
    std::cout<<"fd[1]:"<<fd[1]<<std::endl;
    int id = fork();
    assert(id>= 0);
    if(id == 0)
    {//子进程
        close(fd[0]);
        const char* str = "我是子进程,我正在给你发消息!";
        int ct =1;
        while(true)
        {
            char buf[1024]; 
            snprintf(buf, sizeof buf,"child---->parent say: %s[%d],我的PID是:%d",str,ct++,getpid());
            write(fd[1], buf, strlen(buf));
            sleep(1);
            std::cout<<ct<<std::endl;
        }

        close(fd[1]);//子进程关闭写端文件描述符号。
        std::cout<<"子进程关闭写端文件描述符号"<<std::endl;
        exit(0);
    }

    //父进程
    close(fd[1]);
    while(true)
    {
        char buf[1024];
        ssize_t s = read(fd[0], buf, sizeof(buf) - 1);
        if(s > 0 )//读取成功了
        {
            buf[s] = 0;
            std::cout<<"我是父进程我收到一条消息:"<<"#############"<<buf<<std::endl;
        }
        else if ( s == 0)//子进程读到文件结尾。
        {
            std::cout<< "read:" << s << std::endl;
            break;//父进程不读了
        }
        break;
    }
    close(fd[0]);//关闭读取端文件描述符。
    std::cout<<"关闭读取端文件描述符号"<<std::endl;

    int status = 0;
    ret = waitpid(id,&status, 0);
    assert(ret == id);
    std::cout<<"子进程等待成功"<<std::endl;
    if((status&0x7F) == 0)
    {
        std::cout<<"子进程正常退出"<<std::endl;
        std::cout<<"子进程的退出码是:"<<((status>>8)&0xFF)<< std::endl;
    }
    else
    {
        std::cout<<"子进程异常退出"<<std::endl;
        std::cout<<"子进程退出信号是:"<< (status&0x7F) << std::endl;
    }

    return 0;
}

Summary of Reading and Writing Rules

when there is no data to read

O_NONBLOCK disable: The read call is blocked, that is, the process suspends execution and waits 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: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

如果所有管道写端对应的文件描述符被关闭,则read返回0

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出,(又时候不会退出)

当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特征

只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。兄弟进程之间也可以管道通讯。

管道提供流式服务,管道是面向字节流的。读取端不考虑你写的是什么,直接按字节读取
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥,对共享资源可以进行保护
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

2.1.3.深度理解管道

文件描述符角度

内核角度

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。

2.1.4.管道控制实例

代码:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <cassert>
#include<cstdio>
#include <ctime>
#include <unistd.h>
#include<sys/types.h>
#include<sys/wait.h>


#define PROCESS_NUM 5

class SubEP // 把子进程和管道描述起来
{
public:
    SubEP(int writefd, pid_t pid)
        : _writefd(writefd), _pid(pid)
    {
        char tmp[1024];
        sprintf(tmp, "process_number[%d]; pid: %d; writefd:%d",_cnt++, pid, writefd);
        _name = std::string(tmp);
    }

public:
    static int _cnt;
    std::string _name;
    int _writefd;
    pid_t _pid;
};
int SubEP::_cnt = 1;


///模拟任务/
typedef void(* func_t)();//对函数指针类型重命名为func

void func1()
{    
    sleep(1);
    std::cout<<"任务1:打印任务"<<std::endl;
}

void func2()
{
    sleep(1);
    std::cout<<"任务2:输出任务"<<std::endl;
}

void func3()
{
    sleep(1);
    std::cout<<"任务3:写入任务"<<std::endl;
}

void func4()
{
    sleep(1);
    std::cout<<"任务4:下载任务"<<std::endl;
}

void func5()
{
    sleep(1);
    std::cout<<"任务5:其他任务"<<std::endl;
}
void LoadTask( std::vector<func_t>* funcmap)//加载任务
{
    funcmap->push_back(func1);
    funcmap->push_back(func2);
    funcmap->push_back(func3);
    funcmap->push_back(func4);
    funcmap->push_back(func5);
}

void CreateSubProcess(std::vector<SubEP> *subs, std::vector<func_t>& funcmap)
{
    int i =0;
    std::vector<int> v;
    for (i = 0; i < PROCESS_NUM; i++)
    {
        // 创建管道
        int fds[2];
        int ret = pipe(fds); // 成功返回0;
        assert(ret == 0);
        (void)ret; // 防止对未使用的ret报警告。
        v.push_back(fds[1]);

        // 创建子进程,
        int id = fork();
        if (id == 0)
        {
            // 子进程执行代码
            //close(fds[1]); // 子进程读取指令//关闭写入端
            //我们这样写会出现一个问题,在开始创建第二个进程的时候,此时父进程的文件描述符表,
            //不只有第二个管道的读取写入的fd,还有第一个进程的写入端的文件描述符(读取端口被关闭)
            //所以此时第二个进程会继承到第一个管道的写入文件描述符,这不符合我们的意思。
            //所以要用到下面的方式。
            //所以把所有文件写入端口存入vector中。
            for(int i = 0; i< v.size(); i++)
            {
                close(v[i]);
            }
            
            while(true)
            {
                int code =0 ;
                int n = read(fds[0], &code, 4);
                if(n == 4)//读到的必须是四个字节
                {
                    funcmap[code]();
                }
                else if (n == 0 )//父进程关闭写端文件描述符,子进程读到0 ,退出。
                { break; }
                else
                {std::cout<<"读取错误!!"<<std::endl;}
            }
            exit(0);
        }
        // 父进程执行代码
        close(fds[0]); // 父进程写入指令,//关闭读取端口
        SubEP sub(fds[1], id);
        subs->push_back(sub);
    }
}



//获取一个随机树
int RandomNumber(int n)
{
    return rand() % n;
}

int main()
{
    srand((unsigned long)time(NULL)^12345454);//产生随机数种子。 
    //后面的数字是为了增加随机性

    std::vector<func_t> funcmap;//把任务组织起来
    LoadTask(&funcmap);
    //任务全部加载到funcmap中了。

    std::vector<SubEP> subs; // 把子进程和管道组织起来
    // 建立多个子进程和对应的管道,并且传入对应的任务列表
    CreateSubProcess(&subs, funcmap);
    // 子进程和对应的管道创建完成,并且将信息保存到subs里面去了

    // 父进程开始控制子进程
    int processcnt =subs.size();
    int taskcnt =funcmap.size();
    int count = 5;//循环次数
    //如果大于等于0 ,如若为-1,无限执行。
    while(count)
    {
        //选择一个子进程  //std::vector<SubEP> ->> index
        int processindex = RandomNumber(processcnt);

        //选择一个任务   // std::vector<func_t> ->> index
        int taskindex = RandomNumber(taskcnt);

        //把任务通过管道发送给子进程
        //给选的任务发送四个字节的任务码
        std::cout<<"send task num:"<<taskindex+1<<' '<<"task to ->"<<subs[processindex]._name<<std::endl;
        int ret = write(subs[processindex]._writefd, &taskindex, 4);
        assert(ret == 4);
        (void)ret;
        sleep(2);//每隔两秒发送一个任务码
        if(count != -1)
        {
            count--;
        }
    }
    
    //回收子进程
    //关闭写端文件描述符
    for(int i  =0 ; i< subs.size(); i++){
        close(subs[i]._writefd); 
        waitpid(subs[i]._pid, nullptr,0); 
        sleep(1);
        std::cout<<subs[i]._name<<"--->"<<"等待完成"<<std::endl;
    }
    // for(int i  =subs.size()-1; i>=0; i--){
    //     close(subs[i]._writefd); 
    //     waitpid(subs[i]._pid, nullptr,0); 
    //     sleep(1);
    //     std::cout<<subs[i]._name<<"--->"<<"等待完成"<<std::endl;
    // }

    // for(int i  =0 ; i <subs.size() ; i++)
    // {    
    //     waitpid(subs[i]._pid, nullptr,0); 
    //     sleep(1);
    //     std::cout<<subs[i]._name<<"--->"<<"等待完成"<<std::endl;
    // }

    // for(int i  =subs.size()-1; i >=0 ; i--)
    // {    
    //     waitpid(subs[i]._pid, nullptr,0); 
    //     sleep(1);
    //     std::cout<<subs[i]._name<<"--->"<<"等待完成"<<std::endl;
    // }
    return 0;
}

  1. 命名管道

上面我们写的是匿名管道,一般是两个具有血缘关系的进程进行通讯。如果想让两个完全不相干的进程进行通讯,就需要命名管道了。

3.1.创建命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

命名管道就是通过让不同的进程打开指定名称(路径+文件名)的同一个文件

路径+名称具有唯一性。

命名管道也可以从程序里创建,相关函数有

int mkfifo(const char *filename,mode_t mode);
//comm.hpp
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<cassert>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include <fcntl.h>


#define PIPE_PATH "/tmp/name_pipe"
//我们确定把管道建立在系统tmp路径下。

//创建管道
bool CreateFifo(const std::string & path)
{
    umask(0000);//自己修改文件掩码
    int n = mkfifo(path.c_str(), 0666);//如果想自己用,mode写0600;
    //mkfifo成功之后返回值为0
    //失败就是 -1 ,并且填写对应的错误码
     if( n ==0 ){ return true;}
     else{std::cout<<"error:"<<strerror(errno)<<std::endl; return false;}
}

//删除管道
bool RemoveFifo(const std::string& path)
{
    //删除此文件,
    int n = unlink(path.c_str());
    //unlink成功之后返回值为0
    //失败就是 -1 ,并且填写对应的错误码
    if(n ==0 ){ return true;}
    else{std::cout<<"error:"<<strerror(errno)<<std::endl; return false;}
}


//clinet.cc
#include<iostream>
#include"comm.hPP"

int main()
{
    std::cout<<"hello clinet!!"<<std::endl;
    std::cout<< "clinet begin"<<std::endl;
    int fd = open(PIPE_PATH, O_WRONLY);//clinet端口是写入数据
    std::cout<< "clinet end"<<std::endl;
    if(fd < 0)
    {
        std::cout<<"error"<<strerror(errno)<<std::endl;
        exit(1);
    }
    
    char buf[1024];
    while(true)
    {
        std::cout<<"please say$";
        fgets(buf, sizeof(buf), stdin);//C语言接口函数,会自己添加 ‘\0’ 
        //buf[strlen(buf) - 1] = 0;//消除末尾的 \n
        if(strlen(buf)>0){buf[strlen(buf) - 1] = 0;}//做一下防御性编程
        
        int s = write(fd, buf,strlen(buf));//写入成功返回写入的字节数,失败返回-1
        assert(s == strlen(buf));
    }
    close(fd);
    return 0;
}

//server.cc
#include <iostream>
#include "comm.hPP"

int main()
{
    std::cout << "hello server!!" << std::endl;
    int c = CreateFifo(PIPE_PATH);
    assert(c);
    (void)c;
    std::cout<< "server begin"<<std::endl;
    int fd = open(PIPE_PATH, O_RDONLY); // server端口是读取数据
    //当写入端口还没打开的时候,server进程会阻塞在这里
    std::cout<< "server end"<<std::endl;
    if (fd < 0)
    {
        std::cout << "error" << strerror(errno) << std::endl;
        exit(1);
    }
    char buf[1024];
    while (true)
    {
        ssize_t s = read(fd, buf, sizeof(buf) - 1);
        //写端口写的慢或者不写,会阻塞在这里
        if (s > 0)
        { // 读取成功了
            buf[s]=0;//结尾加'\0'
            std::cout <<"clinet->>server# "<< buf << std::endl;
        }
        else if(s == 0){//写入端文件描述符关闭了
            std::cout << "写端关闭" << std::endl;
            break;
        }
        else{
            std::cout << "error:" << strerror(errno) << std::endl;
            break;
        }
    }
    close(fd);
    int r = RemoveFifo(PIPE_PATH);
    assert(r);
    (void)r;

    return 0;
}

3.2.和匿名管道的区别

匿名管道由pipe函数创建并打开。

命名管道由mkfififo函数创建,打开用open

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

3.3.命名管道的打开规则

如果当前打开操作是为读而打开FIFO时,阻塞直到有相应进程为写而打开该FIFO

如果当前打开操作是为写而打开FIFO时,阻塞直到有相应进程为读而打开该FIFO

和匿名管道相同。其他参考匿名管道。

  1. system V共享内存(shm)

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

4.1.原理

让两个进程看到同一个内存块的通讯方式叫做共享内存

理解:

1.这里的共享内存空间是OS专门设计的为了实现进程间通讯的.
2.共享内存是一种通讯方式,所有想通讯的进程都可以申请自己的共享内存,实现通讯
3.OS中会同时存在着很多的共享内存,且被描述和组织起来。

4.2.调用接口

shmget函数

申请共享内存段(申请共享内存)
功能:用来创建共享内存
原型
 int shmget(key_t key, size_t size, int shmflg);
参数
 key:这个共享内存段名字
 size:共享内存大小
 shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:
  成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

ftok传入相同的参数,得到相同的Key值

实例:

comm.hpp
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>


#ifndef _COMM_HPP_
#define _COMM_HPP_

#define PATHNAME "." //当前路径
#define PROJ_ID 0x111//项目id(随便写的)
#define SHM_SIZE 4096 //共享的内存大小
//注意这里尽量给4KB整数倍大小。

key_t GetKey()
{
    key_t tmp =  ftok(PATHNAME, PROJ_ID);
    if(tmp == -1)
    {
        //perror("fyok");
        //cin cout cerr -> stdin stdout stderr ->SHM_SIZE 0  1  2 
        std::cerr<< errno <<":"<<strerror(errno)<<std::endl;
        exit(1);
    }
    return tmp;
}

int getshmfun(key_t key,int flags)
{
    int shmid = shmget(key , SHM_SIZE , flags);
    if(shmid < 0)
    {
        std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
        exit(1);
    }
    return shmid;
}

int GetShm(key_t key)
{
    return getshmfun(key,IPC_CREAT);
}
 int CreateShm(key_t key)
{
    return  getshmfun(key, IPC_CREAT | IPC_EXCL);
}
#endif
shm_server.cc
#include "comm.hpp"

int main()
{
    std::cout<<"test server"<<std::endl;
    key_t key = GetKey();
    printf("key: 0x%x\n",key);
    int shmid = CreateShm(key);
    std::cout<<"shmid: "<<shmid<<std::endl;


    return 0;
}
shm_clinet.cc
#include "comm.hpp"

int main()
{
    std::cout<<"test clinet"<<std::endl;
    key_t key = GetKey();
    printf("key: 0x%x\n",key);
    int shmid = GetShm(key);
    std::cout<<"shmid: "<<shmid<<std::endl;

    return 0;
}

可以看到,我们两个进程看到的shmid和key都是相同的,说明shmget传入相同的key就可以得到相同的shmid.

shmctl函数

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

shmat函数

功能:将共享内存段连接到进程地址空间
原型
 void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
 shmid: 共享内存标识
 shmaddr:指定连接的地址(一般不指定)
 shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY (一般设置为空,默认读写)
返回值:
成功返回一个指针,指向共享内存第一个节点地址;失败返回-1
类似 malloc的返回值.


shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - 
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

shmdt函数

功能:将共享内存段与当前进程脱离
原型
 int shmdt(const void *shmaddr);
参数
 shmaddr: 由shmat所返回的指针
返回值:
     成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

4.3.共享内存数据结构

首先我们知道共享内存是OS给我们申请的一个内存空间。OS为了很好的管理他,就会给他创建对应的数据结构保存对应的属性信息,也就是先描述,再组织。

所以共享内存是: 物理内存块+共享内存的相关属性

我们创建共享内存的时候,通过Key保证此共享内存再操作系统中的唯一性,另一个进程可以通过同一个Key找到此共享内存。

这个key就是共享内存的其中一个属性。通过shmget函数设定进去属性结构体中。用来标定共享内存的唯一性。

但是我们发现我们调用shmget函数的时候返回值并不是key而是返回一个新的共享内存id

这里的就很像:
文件fd】 和 【文件 inode】
shmid】 和 【key】

inode和Key是内核用来标定文件和共享内存唯一性的标记

fd 和 shmid 用户(进程)用来标定文件和共享内存唯一性的标记

这是为了内核层面 和 用户层面强解耦,不用一套标记。

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 */
};

struct ipc_perm { 
   key_t          __key;    /* Key supplied to shmget(2) */
   uid_t          uid;      /* Effective UID of owner */
   gid_t          gid;      /* Effective GID of owner */
   uid_t          cuid;     /* Effective UID of creator */
   gid_t          cgid;     /* Effective GID of creator */
   unsigned short mode;     /* Permissions + SHM_DEST and
                               SHM_LOCKED flags */
   unsigned short __seq;    /* Sequence number */
};

这里的结构内容可以通过shmctl获取.

4.4.共享内存的特征

生命周期随OS,不随进程死亡而死亡

这是所有system V 通讯的共性

ipcs 查看systmp V 通讯申请的内存块

ipcs -m :惨查看共享内存申请的内存块
ipcs -q :惨查看消息队列申请的内存块
ipcs -s :惨查看信号量数组申请的内存块

删除其中一个共享内存开辟的空间

ipcrm -m [shmid值] //注意使用的是共享内存的id.
可以看到我们刚刚的进程内申请的共享内存没有消失,进程运行结束了他还在.

共享内存是所有进程间通讯最快的

综合考虑管道和共享内存,共享内存可以大大减少数据拷贝次数.

共享内存是4次拷贝,管道是6次拷贝

缺点:没有对数据做任何保护

不和管道相同,管道是当我们写入端口没有的数据写入的时候,读取端口会阻塞在哪里
而共享内存不会阻塞,他会重复的读取以前读取的数据
共享内存没有对数据做保护,可以和信号量或者管道配合使用,实现对数据做保护
  1. system V消息队列(msg)(了解)

调用接口

msgget函数:申请一个消息队列

和共享内存一样,都是ftok 获取key ,返回一个 msgid

msgctl函数:控制消息队列

用法也是和 shmctl相同,可以删除,可以获取属性,可以修改属性

msgsnd函数和msgrcv函数:发送数据和读取数据

注意自定义数据块

  1. system V 信号量数组(sem)(了解)

储备知识

信号量:本质是一个计数器,通常用来表示公共资源中,资源数量的多少。

1.公共资源:被多个进程可以同时看到的资源。例如我们刚刚学到的共享内存。

访问没有保护的公共资源的时候,就会出现数据不一致的问题。

为了实现进程间通讯,打破进程之间的独立性,就建立各种通讯机制,让不同的进程可以看到同一个公共资源,就会会出现数据不一致的问题,所以需要引入一些保护策略。

2.临界资源:被我们保护起来的公共资源叫做:临界资源,我们的进程中大部分资源是独立的,只有一小部分的资源是临界资源。

资源一般是:内存,文件,网络----

3.临界区:资源是要被使用的进程中一定有一部分的代码 来访问这部分资源,访问这部分临界资源的代码叫做:临界区。非临界区占大部分。

如何保护临界资源: 同步 &&& 互斥

4.原子性:要么不做,要么做完。只有两态。

信号量就是未来保证进程同步和互斥的计数器,表示资源的多少

信号量的本质就是:对资源进行预定。

我在后面多线程的时候会重点讲解 信号量!!!

调用函数

semget函数:创建信号量数组

semctl函数:控制信号量数组(获取属性,删除---)

semop函数:PV原子操作

Guess you like

Origin blog.csdn.net/zxf123567/article/details/129428911