【Linux】进程间通讯

  1. 前提知识

进程间具有独立性 ,现在我们要打破独立性,去通讯,所以 通讯的成本一定很高。
又时候又需要多进程协同完成某种业务内容 ,例如以前: cat file | grep “hello",这就是两个进程之间的通讯。
通讯的本质就是:1,OS直接或者间接个通讯双方提供“内存空间”;2,要通讯的双方必须同时看到这一块“公共资源”(一般由OS提供)。
不同的通讯种类本质就是:上面所说的”公共资源“是OS哪一个模块提供的。例如:如果是由文件提供,就是管道通讯,如果是system V通讯模块提供就是system V通讯。
所以要通讯,首先要让不同的进程看到同一份资源(主要),然后进行通讯。

1.1.进程间通信目的

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止

时要通知父进程)。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另

一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1.2.进程间通信发展(分类)

管道

匿名管道pipe
命名管道

System V进程间通信

System V 消息队列(了解)
System V 共享内存
System V 信号量(了解)

POSIX进程间通信(目前主流)

消息队列
共享内存
信号量
互斥量
条件变量
读写锁

管道:是古老的通讯方式,是基于文件系统的方法。

System V :聚焦本主机的进程通讯,无法跨主机,现在几乎被主流淘汰了 。

POSIX:让通讯可以跨主机。

  1. 匿名管道

2.1.匿名管道定义

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

2.1.1.基本原理

上面用于管道通讯的文件叫做管道文件,他和普通文件不一样:

普通文件不仅有自己的内核缓冲区,还有对应的磁盘空间。会按照一定的规则,将缓冲区文件刷新到磁盘。

管道文件只有内核缓冲区,不需将数据刷新到磁盘,不需要在磁盘上存在对应的文件。

所以这里讲到的管道是内存级的文件,OS只需要创建自己的内核缓冲区,对应的读写方法,不关心在磁盘是否真的存在此文件,不需要IO的过程。

2.1.2.创建匿名管道(pipe)

匿名管道通常用于由血缘关系的进程之间的通讯。

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

创建之后:

fork之后

举例:

#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;
}

这种通讯方式就是管道通讯(这个管道是匿名管道)

这里也就证明了,我们以前讲的write和read是写入内核中的缓冲区,而不是直接写入文件的。

2.1.3.管道的特点

读快写慢

如果管道中没有数据,在读取端再次读取,默认会直接阻塞当前正在读取的进程。
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;
}

写快读慢

如果没有读取或者读取很慢的时候,写入端会一直写入,到写满的时候才阻塞。
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;
}

所以管道是一个,固定大小的内存空间。

也就是读取端,管道空阻塞,写入端写满阻塞

读取规则

管道的写入是按照一行一行去写入的,但是读取并不是按照行去读取的,是按照允许读取的最大字节数读取的。
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;
}

写退读到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;
}

写已经退出了,读取端会读到0.

读取关闭

读取关闭,但是写入端还没关闭,此情况OS会给写进程发送终止信号。

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

读写规则总结

当没有数据可读时

O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。

当管道满的时候

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原子操作

猜你喜欢

转载自blog.csdn.net/zxf123567/article/details/129428911