【Linux】八、进程通信

进程通信的介绍

目的

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

资源共享:多个进程间共享资源;

通知事件:一个进程向另一个或一组进程发送消息,同时事件如,进程终止时要通知父进程;

进程控制:有的进程需要完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并及时知道它的状态;

进程间通信的必要性

在单进程下不通信是可以的,但是多进程的并发能力就无法使用,更加无法实现多进程协同,有的是为了传输数据,有的为了同步执行流,有的为了消息通知; 

进程间通信的技术背景

1、进程具有独立性,使用虚拟地址空间+页表的方式,保证进程运行的独立性(进程内核数据结构+进程的代码和数据);

2、通信成本会比较高,进程间通信的前提是首先让不同的进程看到同一块空间;

3、那么进程看到的同一块空间,不能属于任何一个进程,应该更强调共享;

理解进程间通信

进程的运行具有独立性,所以进程想要通信难度是比较大的,通信的本质是进程之间数据的交换,而交换就要拷贝,拷贝就要有一份提供给两个进程拷贝的空间,也就是在进程通信之时需要让不同的进程看到同一份资源(内存空间);

为什么要进行进程间通信,交换数据,控制,通知等目标,需要进程协同;

进程通信的发展

在早期参与操作系统开发的人员,再使用操作系统工作时,发现只是用单进程完成一些任务比较困难;比如要将文件的内容打印出来,然后要逆置,还要统计行数,做这些事情的时候就耦合在一起了,所以就可以做成三个功能,让三个进程分别执行不同的功能;

管道

System V进程间通信(了解)是一种标准,接口复杂,不好整合到文件场景,主要做单机通信;

POSIX进程间通信(使用较多),网络通信;

进程通信分类

管道

匿名管道,pipe;

命名管道;

System V IPC

System V 消息队列

System V 共享内存

System V 信号量

POSIX IPC

消息队列

共享内存

信号量

互斥量

条件变量

读写锁

管道

 生活中的管道一般只能单方向通信;传送的都是资源;

而计算机中的管道也是传输资源的,这种资源是数据;

把一个进程连接到另一个进程的数据流成为一个管道;

who | wc -l

将who进程的数据通过管道传送给wc -l进程;

管道的提供者是操作系统;

【Linux】八、进程通信_第1张图片

父进程和子进程通过集成文件描述符表方案,让父进程先把文件打开,然后让子进程也指向同一份文件,这个文件就是管道;

首先父进程(下图中上边的进程),让这个进程分别以读写打开文件,给父进程返回的时候会有两个操作符,分别是以读打开的和以写打开的操作符;然后创建子进程,子进程也会有一份文件描述符表;在子进程最初创建的时候,会拷贝父进程的文件描述付表,所以子进程也会指向,同一个文件;这样就让不同的进程看到了同一份资源;然后双进程关闭自己不需要的文件描述符,如果让父进程进行写入,子进程进行读取,则关闭父进程的读端,关闭子进程的写端;通信数据不会写到磁盘中;

【Linux】八、进程通信_第2张图片

这种方式能够让具有血缘关系的进程进行进程间通信,常用于父子进程;

Makefile

mypipe:mypipe.cc

        g++ -o $@ $^ -std=c++11

.PHONY:clean

clean:

        rm -f mypipe

mypipe

#include 
#include 
#include 

using namespace std;

int main()
{
    //1、创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);//完成管道的创建
    assert(n != -1);//判断pipe调用是否失败,如果等于-1则失败;
    (void)n;//assert在debug下式有效的,但是在release断言就没了,如果没有这一行,那么在release下就会出现
    //n被定义了但是没有被使用;

    cout << "pipefd[0]:" << pipefd[0] << endl;
    cout << "pipefd[1]:" << pipefd[1] << endl;

    return 0;
}

此时的运行结果为:

将代码改为

#include 
#include 
#include 

using namespace std;

int main()
{
    //1、创建管道
    int pipefd[2] = { 0 };
    int n = pipe(pipefd);//完成管道的创建
    assert(n != -1);//判断pipe调用是否失败,如果等于-1则失败;
    (void)n;//assert在debug下式有效的,但是在release断言就没了,如果没有这一行,那么在release下就会出现
    //n被定义了但是没有被使用;
#ifdef DEBUG
    cout << "pipefd[0]:" << pipefd[0] << endl;
    cout << "pipefd[1]:" << pipefd[1] << endl;
#endif
    return 0;
}

条件编译并且将makefile中改为 g++ -o $@ $^ -std=c++11 -DDEBUG ,此时编译后打印结果与上面相同,如果不想进入调试的话,就将Makefile改为g++ -o $@ $^ -std=c++11 #-DDEBUG,将 -DDEBUG 注释即可;

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    //1、创建管道
    int pipefd[2] = { 0 };//pipefd[0(嘴巴)]:读端;  pipefd[1(钢笔)]:写端;
    int n = pipe(pipefd);//完成管道的创建
    assert(n != -1);//判断pipe调用是否失败,如果等于-1则失败;
    (void)n;//assert在debug下式有效的,但是在release断言就没了,如果没有这一行,那么在release下就会出现
    //n被定义了但是没有被使用;
#ifdef DEBUG
    cout << "pipefd[0]:" << pipefd[0] << endl;
    cout << "pipefd[1]:" << pipefd[1] << endl;
#endif

    //2、创建子进程
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        //子进程
        //3、构建单向通信的信道,父进程写入,子进程读取;
        //关闭子进程不需要的fd
        close(pipefd[1]);

        //要想让子进程读取需要有缓冲区
        char buffer[1024];
        while (true)//一直读取,从pipefd[0]读出,读到buffer中,读buffer-1个;
        {
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (s > 0)//此时读取成功
            {
                buffer[s] = 0;//相当于给字符串添加'/0';
                cout << "child get message[" << getpid() << "]Father#" << buffer << endl;
            }
        }

        exit(0);
    }
    //父进程
    close(pipefd[0]);
    string message = "i am father ,loading!!";
    int count = 0;//计数器,看给子进程发了多少条;
    char send_buffer[1024];
    while (true)
    {
        //构建变化的字符串
        snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d", message.c_str(), getpid(),count++);
        //写入
        write(pipefd[1], send_buffer, strlen(send_buffer));
        sleep(1);
    }

    pid_t ret = waitpid(id, nullptr, 0);
    assert(ret < 0);

    return 0;
}

运行结果为

【Linux】八、进程通信_第3张图片

那为什么不定义一个全局的buffer来进行通信呢?

因为存在写时拷贝的存在,所以无法通过此方法通信;

管道特点总结

1、管道是用来进行具有血缘关系的进程进行进程间通信,常用与父子进程通信;

2、管道具有通过让进程间协同,提供了访问控制;上面的代码父进程是1s写一条,但是子进程在读取的时候没有任何时间限制,当时在程序运行时,是按照父进程写入的时间进行读取的;管道是一个文件,显示器也是一个文件,父子进程同时往显示器写入的时候,默认没有任何等待的打印,缺乏访问控制;但是对管道文件进行读取的时候,具有访问控制;

如果父进程一直,写入,子进程读取较慢,就会出现,父进程写满之后就不会在写入了,读端读取后,才会继续写入,而且读端会一次读取一批数据;

3、管道提供的是面向字节流式的通信服务;

4、管道是基于文件的;如果通信双方将文件描述符关闭,文件的生命周期是随进程的,管道的生命周期是随进程的;

5、管道是单向的,就是半双工的通信的一种特殊情况,正常的一个读一个写叫做半双工,两个同时又读又写,就是全双工的;

四种情况:

a、写得快,读的慢,写满后就不能继续写了,需要等读端读出一批后再写入;

b、写的慢,读的快,管道没有数据的时候,读端必须等待;

c、将写端关闭,读0,标识读到了文件结尾;

d、读关,写继续写,OS将会终止写的进程;

前两种是由管道的访问控制决定的;

扩展(管道的作用)

父进程创建若干子进程,父进程和每一个子进程建立对应的管道,每一个子进程内部预先防止很多处理任务的方法,让每一个子进程通过读端读取,父进程将四个写端进行管理,父进程有任务时,就可以指派给某一个子进程来完成该任务,于是就实现了进程池的概念;

【Linux】八、进程通信_第4张图片

代码如下

Task.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 

typedef std::function func;
//等效  using func = std::function;

std::vector callbacks;
std::unordered_map desc;

void readMySQL()//四个任务
{
    std::cout << "sub process[" << getpid() << "]执行访问数据库的任务" << std::endl;
}

void execuleUrl()
{
    std::cout << "sub process[" << getpid() << "]执行Url解析" << std::endl;
}

void cal()
{
    std::cout << "sub process[" << getpid() << "]执行加密任务" << std::endl;
}

void save()
{
    std::cout << "sub process[" << getpid() << "]执行数据持久化任务" << std::endl;
}

void load()//接口
{
    desc.insert({ callbacks.size(),"readMySQL:读取数据库" });
    callbacks.push_back(readMySQL);

    desc.insert({ callbacks.size(),"execuleUrl:进行Url解析" });
    callbacks.push_back(execuleUrl);

    desc.insert({ callbacks.size(),"cal:进行加密计算" });
    callbacks.push_back(cal);

    desc.insert({ callbacks.size(),"save:进行数据的文件保存" });
    callbacks.push_back(save);
}

void showHandler()
{
    for (const auto &iter : desc)
    {
        std::cout << iter.first << "\t" << iter.second << std::endl;
    }
}

int handlerSize()
{
    return callbacks.size();
}

processPool.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Task.hpp"

#define PROCESS_NUM 5 //进程池的进程个数

using namespace std;

int waitCommand(int waitFd, bool &quit)
{
    uint32_t command = 0;
    ssize_t s = read(waitFd, &command, sizeof(command));
    if (s == 0)//读到0,就证明文件描述符关了
    {
        quit = true;
        return -1;
    }
    assert(s == sizeof(uint32_t));
    return command;
}

void sendAndWakeUp(pid_t who, int fd, uint32_t command)
{
    write(fd, &command, sizeof(command));
    cout << "main process call process" << who << " execute" << desc[command] << "through" << fd << endl;
}

int main()
{
    load();
    vector>slots;

    //先创建多个进程
    for (int i = 0; i < PROCESS_NUM; i++)
    {
        //创建管道
        int pipefd[2] = { 0 };
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        pid_t id = fork();
        assert(id != -1);

        //子进程进行读取
        if (id == 0)
        {
            //关闭读端
            close(pipefd[1]);
            //子进程
            while (true)
            {
                //等命令
                bool quit = false;
                int command = waitCommand(pipefd[0],quit);//如果对方不发,就阻塞;
                if (quit) break;
                //执行对应的命令
                if (command >= 0 && command < handlerSize())
                {
                    callbacks[command]();
                }
                else
                {
                    cout << "非法command命令" << command << endl;
                }

            }
            exit(1);
        }
        //father,进行写入,关闭读端
        close(pipefd[0]);
        slots.push_back(pair(id,pipefd[1]));
    }

    //父进程派发任务
    //单机版的负载均衡,随机的,rr(轮询的);不能只让一个子进程去工作
    srand((unsigned long)time(nullptr) ^ getpid() ^ 123543556);//让数据源更随机
    while (true)
    {
        int select;
        int command;
        cout << "#############################" << endl;
        cout << "##   1. show functions   ####" << endl;
        cout << "##   2. send command     ####" << endl;
        cout << "#############################" << endl;
        cout << "Please Select";
        cin >> select;
        if (select == 1) showHandler();
        else if (select == 2)
        {
            cout << "Enter Your Command> ";
            cin >> command;
            //选择进程
            int choice = rand() % slots.size();
            //布置任务,把任务给指定的进程
            sendAndWakeUp(slots[choice].first,slots[choice].second,command);
        }
        else
        {

        }
    }

    //关闭fd,结束所有进程
    for (const auto slot : slots)
    {
        close(slot.second);
    }

    //回收所有的子进程信息
    for (const auto &slot : slots)
    {
        waitpid(slot.first, nullptr, 0);
    }

    return 0;
}

命名管道

管道应用的一个限制就是只能在具有共同祖先的的进程通信;

如果想要在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道;

命名管道是一种特殊类型的文件;

进程间通信首要是先让不同的进程看到同一份资源;

【Linux】八、进程通信_第5张图片

A进程打开了文件,在内核中会出现一个描述该被打开文件的结构体,如果此时B进程也要打开此文件,只要B进程将文件的地址填入,直接指向struct_file即可,但是一般文件,如果数据写进去会刷盘,将数据就写到了磁盘中,要是进行刷盘的话就要执行IO,效率会很低;操作系统设计了新的方案,会在磁盘上创建管道文件,可以被多个进程打开,但是不会将内存数据进行刷盘操作,这种管道文件没有内容,因为是在磁盘上被创建的,所以就会有路径,也就有了名字,该文件一定会在系统路径中,路径具有唯一性,双方进程就可以通过管道,看到同一份资源;

命名管道与匿名管道的本质是一样的,一种是通过继承的方式,一种是通过唯一的路径的方式;

运行指令  mkfifo name_pipe ,可以看到出现了文件name_pipe,并且第一个字母是p,表示管道文件;

代码

函数mkfifo;

【Linux】八、进程通信_第6张图片

Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include 
#include 

#define Notice  1
#define Warning 2
#define Error   3
#define Debug   4

const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};

std::ostream &Log(std::string message, int level)
{
    
    std::cout << "|" << (unsigned)time(nullptr) << "|" << msg[level] << "|" << message;
    return std::cout;
}



#endif

comm.hpp

#pragma once

#ifdef _COMM_H_
#define _COMM_H_

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

using namespace std;

#define MODE 0666
#define SIZE 128

string ipcPath = "./fifo.ipc";


#endif

serve.cc

#include "comm.hpp"

int main()
{
    //1\创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }
    Log("创建管道文件成功", Debug) << "step 1" << endl;
    //2、正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功", Debug) << "step 2" << endl;
    //编写正常的通信代码
    char buffer[SIZE];//定义好缓冲区;
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer));//清空缓冲区
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);//读取,预留空间添加\0
        if (s > 0)
        {
            cout << "client say" << buffer << endl;
        }
        else if (s == 0)
        {
            //end of file
            cerr << "read end of file, client quit,server quit too!" << endl;
            break;
        }
        else
        {
            //读取失败
            perror("read");
            break;
        }
    }

    close(fd);
    Log("关闭管道文件成功", Debug) << "step 3" << endl;
    unlink(ipcPath.c_str());
    Log("删除管道文件成功", Debug) << "step 4" << endl;
    return 0;
}

client.cc

#include "comm.hpp"

int main()
{
    //1.获取管道文件
    int fd = open(ipcPath.c_str, O_WRONLY);
    if (fd < 0)
    {
        perror("open");
        exit(1);
    }
    //2、通信
    string buffer;
    while (true)
    {
        cout << "please Enter Message Line :>";
        std::getline(std::cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }

    //3.关闭
    close(fd);
    unlink(ipcPath.c_str());//通信完毕删除文件

    return 0;

}

运行结果,左边为client,右边为server

修改代码 server.cc

#include "comm.hpp"
#include 

static void getMessage(int fd)
{
    char buffer[SIZE];//定义好缓冲区;
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer));//清空缓冲区
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);//读取,预留空间添加\0
        if (s > 0)
        {
            cout << "[" << getpid() << "]" << "client say" << buffer << endl;
        }
        else if (s == 0)
        {
            //end of file
            cerr << "[" << getpid() << "]" << "read end of file, client quit,server quit too!" << endl;
            break;
        }
        else
        {
            //读取失败
            perror("read");
            break;
        }
    }
}

int main()
{
    //1\创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }
    Log("创建管道文件成功", Debug) << "step 1" << endl;
    //2、正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功", Debug) << "step 2" << endl;
    //编写正常的通信代码

    int nums = 3;
    for (int i = 0; i < nums; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            getMessage(fd);
        }
    }

    for (int i = 0; i < nums; i++)
    {
        waitpid(-1, nullptr, 0);
    }


    close(fd);
    Log("关闭管道文件成功", Debug) << "step 3" << endl;
    unlink(ipcPath.c_str());
    Log("删除管道文件成功", Debug) << "step 4" << endl;
    return 0;
}

就可以实现一个管道多进程进行读取;

System V共享内存

原理

现在内存中申请空间,然后建立映射,将申请的空间与调用映射接口的进程建立映射;

释放就是将映射去掉,然后将内存释放;

一般共享内存、内存映射和共享库位于共享区中;

【Linux】八、进程通信_第7张图片

共享内存的建立

共享内存不属于任何一个进程,属于操作系统,是操作系统单独设立的内核模块;

如果有很多进程在通信,那么内存终究会存在很多共享内存,所以操作系统需要对共享内存需要管理,于是就需要先描述在组织,所以重新理解共享内存:共享内存块+对应的共享内存的内核数据结构;

创建共享内存的代码

【Linux】八、进程通信_第8张图片

shmflg: IPC_CREAT 单独使用时,如果创建共享内存,如果底层已经存在,获取它并且返回,如果不存在就创建并返回;

IPC_EXCL,单独使用时,是没有意义的;

IPC_CREAT 和 IPC_EXCL 一起使用,如果底层不存在,创建并返回,如果底层存在,出错返回;返回成功一定是一个全新的共享内存;

返回值是共享内存的用户层标识符,类似曾经的fd;

key:操作系统要先描述在组织,创建好共享内存和对应的数据结构;当一个进程通过操作系统创建好了共享内存,要通信的对方进程,需要保证对方能看到,并且看到的就是我创建的共享内存,也就是如何让要通信的进程能够找到自己创建的内存空间;通过key,数据是几,不重要,只要能够在系统唯一即可;server和client只要他们使用同一个key,就可以看到同一个共享内存了,只要key值相同就可以看到同一个共享内存;

进程client和server,server进程和client通过算法形成一个唯一值,然后server调用接口在底层创建了一个共享内存,创建好后server将标识符写到共享内存里(管理共享内存生成的数据结构中),然后client,也形成的是1234,就可以匹配起来,看到同一个共享内存了;下层是操作系统;

【Linux】八、进程通信_第9张图片

如何形成唯一的key值;

调用函数,ftok;

【Linux】八、进程通信_第10张图片

#include 

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;
    assert(k != -1);
    Log("create key done", Debug) << "server key:" << k << std::endl;

    //创建共享内存  建议要创建一个全新的共享内存 通信的发起者;
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL);//成功返回共享内存的标识符,失败返回-1;
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }

    return 0;
}

此时运行完程序后,创建的内存还会存在;

查看ipc的命令, ipcs -m 查看共享内存;

perms:权限,权限为0表示没有任何的进程有权利读取;

attach是映射的意思;

nattch:有多少个进程与此共享内存关联;

删除共享内存,ipcrm -m 22,此处使用的是shmid;

ipc资源生命周期随内核;所以需要

1、手动删除,就是用使用上边的命令删除;

2、代码删除,将server的代码改为;

【Linux】八、进程通信_第11张图片

#include 

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;
    assert(k != -1);
    Log("create key done", Debug) << "server key:" << k << std::endl;

    //创建共享内存  建议要创建一个全新的共享内存 通信的发起者;
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL);//成功返回共享内存的标识符,失败返回-1;
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << "shmid :" << shmid << std::endl;


    //删除共享内存
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("Delete shm done", Debug) << "shmid :" << shmid << std::endl;

    return 0;
}

挂接进程与共享内存;

shmat

【Linux】八、进程通信_第12张图片

成功后返回挂接成功的address,失败返回-1;

将指定的共享内存从自己的地址空间中去关联;

shmdt

【Linux】八、进程通信_第13张图片

共享区域是在内核空间还是用户空间?是属于对应的用户空间;

在用户空间就是不用经过系统调用,直接访问;

双方进程如果要通信,直接进行内存级的读和写即可;

Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include 
#include 

#define Notice  1
#define Warning 2
#define Error   3
#define Debug   4

const std::string msg[] = {
"Debug",
"Notice",
"Warning",
"Error"
};

std::ostream &Log(std::string message, int level)
{

    std::cout << "|" << (unsigned)time(nullptr) << "|" << msg[level] << "|" << message;
    return std::cout;
}



#endif

comm.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"

#define PATH_NAME "/home/zyl"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小最好是页(PAGE:4096)的整数倍;

shmServer.cc

#include 

string TransToHex(key_t k)//转换为16进制
{
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;
    assert(k != -1);
    Log("create key done", Debug) << "server key:" << TransToHex(k) << std::endl;

    //创建共享内存  建议要创建一个全新的共享内存 通信的发起者;
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);//成功返回共享内存的标识符,失败返回-1;
    //0666表示运行权限;此时运行后共享内存的perms就会变为666;
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << "shmid :" << shmid << std::endl;
    //将指定的共享内存挂接到自己的共享内存空间;
    char shmaddr = (char*)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << "shmid :" << shmid << std::endl;


    //此处是通信的逻辑
    //将共享内存当做是一个大字符串
    for (;;)
    {
        printf("%s\n", shmaddr);
        sleep(1);
    }


    //将指定的共享内存,从自己的地址空间去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << "shmid :" << shmid << std::endl;

    //删除共享内存,就算有内存和当前的内存挂接依旧会强制删除共享内存;
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("Delete shm done", Debug) << "shmid :" << shmid << std::endl;

    return 0;
}

shmClient.cc

#include 

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值
    if (k < 0)
    {
        Log("create key failed", Error) << "client key:" << k << std::endl;
        exit(1);
    }
    Log("create key done", Debug) << "client key:" << k << std::endl;

    //获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0);
    if (shmid < 0)
    {
        Log("create shm failed", Error) << "client key:" << k << std::endl;
        exit(2);
    }

    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
    if (shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << "client key:" << k << std::endl;
        exit(3);
    }
    Log("attach shm success", Error) << "client key:" << k << std::endl;

    //使用
    //将共享内存看做一个对应的char 类型的buffer
    char a = 'a';
    for (; a <= 'z'; a++)
    {
        //向shmaddr进行写入;
        snprintf(shmaddr, SHM_SIZE - 1, "hello server,我是其他进程,pid:%d,%c\n",getpid(), a);
        sleep(2);
    }

    //去关联
    int n = shmdt(shmaddr);
    Log("detach shm success", Error) << "client key:" << k << std::endl;

    //不需要删除;

    return 0;
}

此时代码存在一些问题

在退出时,不会删除共享内存

server中加入

    //此处是通信的逻辑
    //将共享内存当做是一个大字符串
    for (;;)
    {
        printf("%s\n", shmaddr);
        if (strcmp(shmaddr, "quit") == 0) break;
        sleep(1);
    }

client中加入

    //使用
    //将共享内存看做一个对应的char 类型的buffer
    char a = 'a';
    for (; a <= 'z'; a++)
    {
        //向shmaddr进行写入;
        snprintf(shmaddr, SHM_SIZE - 1, "hello server,我是其他进程,pid:%d,%c\n",getpid(), a);
        sleep(2);
    }

    strcpy(shmaddr, "quit");

结论1、只要是通信双方使用共享内存,一方直接向共享内存中写入数据,另一方会立马看到;所以共享内存是所有进程间通信速度最快的;因为不需要过多的拷贝,不需要将数据交给操作系统;例如管道通信,首先操作系统创建一个管道文件,首先发送方获取数据,通过调用接口将数据写到管道文件中,调用接口的本质就是,将数据先拷贝到操作系统中,然后将数据从管道文件中拷贝到另一个文件的缓冲区中;

【Linux】八、进程通信_第14张图片

键盘到进程是一次拷贝,从进程到管道文件也是一次拷贝,从管道进程到另一个文件也是一次拷贝,进程将内容打印出来也是一次拷贝;共四次拷贝;

【Linux】八、进程通信_第15张图片

共享内存在同种情况下相当于只有两次拷贝;

可以修改代码为从键盘获取字符

client修改为

//使用
    //将共享内存看做一个对应的char 类型的buffer
    while (true)
    {
        ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
        if (s > 0)
        {
            shmaddr[s - 1] = 0;//此处需要-1,是因为在输入的时候会按回车键就会输入一个\n,
            //s-1就相当于,将 \n 修改为0;

            if (strcmp(shmaddr, "quit") == 0) break;
        }
    }

结论2、共享内存没有访问控制;会带来并发问题;如果想要一定程度的访问控制怎么办?

用管道实现共享内存同步的过程;

Log.hpp不变;

comm.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "Log.hpp"

#define PATH_NAME "/home/zyl"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小最好是页(PAGE:4096)的整数倍;

#define FIFO_NAME "./fifo"

class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        (void)n;
        Log("create fifo success", Notice) << "\n";
    }
    ~Init()
    {
        unlink(FIFO_NAME);
        Log("remove fifo success", Notice) << "\n";
    }
};



#define READ O_RDONLY
#define WRITE O_WRONLY

int OpenFIFO(std::string pathname, int flags)
{
    int fd = open(pathname.c_str(), flags);
    assert(fd >= 0);
    return fd;
}

void wait(int fd)
{
    Log("等待中...", Notice) << "\n";
    uint32_t temp = 0;
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
}

void Signal(int fd)
{
    uint32_t temp = 1;
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中...", Notice) << "\n";

}

void CloseFifo(int fd)
{
    close(fd);
}

server变化

#include 

//对应的程序在加载的时候会自动构建全局变量,就要调用该类的构造函数----创建管道文件
//程序退出的时候,全局变量会被析构,自动调用析构函数,会自动删除管道文件;
Init init;

string TransToHex(key_t k)//转换为16进制
{
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}

int main()
{
    key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值,失败后返回-1;
    assert(k != -1);
    Log("create key done", Debug) << "server key:" << TransToHex(k) << std::endl;

    //创建共享内存  建议要创建一个全新的共享内存 通信的发起者;
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);//成功返回共享内存的标识符,失败返回-1;
    //0666表示运行权限;此时运行后共享内存的perms就会变为666;
    if (shmid == -1)
    {
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << "shmid :" << shmid << std::endl;
    //将指定的共享内存挂接到自己的共享内存空间;
    char shmaddr = (char*)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << "shmid :" << shmid << std::endl;


    //此处是通信的逻辑
    //将共享内存当做是一个大字符串

    int fd = OpenFIFO(FIFO_NAME, READ);

    for (;;)
    {
        Wait(fd);

        //临界区
        printf("%s\n", shmaddr);
        if (strcmp(shmaddr, "quit") == 0) break;
        sleep(1);
    }
    

    //将指定的共享内存,从自己的地址空间去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << "shmid :" << shmid << std::endl;

    //删除共享内存,就算有内存和当前的内存挂接依旧会强制删除共享内存;
    int n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("Delete shm done", Debug) << "shmid :" << shmid << std::endl;

    CloseFifo(fd);

    return 0;
}

client.cc改变

#include 

int main()
{
    Log("child pid is", Debug) << getpid() << std::endl;
    key_t k = ftok(PATH_NAME, PROJ_ID);//创建key值
    if (k < 0)
    {
        Log("create key failed", Error) << "client key:" << k << std::endl;
        exit(1);
    }
    Log("create key done", Debug) << "client key:" << k << std::endl;

    //获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0);
    if (shmid < 0)
    {
        Log("create shm failed", Error) << "client key:" << k << std::endl;
        exit(2);
    }

    char *shmaddr = (char*)shmat(shmid, nullptr, 0);
    if (shmaddr == nullptr)
    {
        Log("attach shm failed", Error) << "client key:" << k << std::endl;
        exit(3);
    }
    Log("attach shm success", Error) << "client key:" << k << std::endl;

    //使用
    //将共享内存看做一个对应的char 类型的buffer

    int fd = OpenFIFO(FIFO_NAME, WRITE);

    while (true)
    {
        ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
        if (s > 0)
        {
            shmaddr[s - 1] = 0;//此处需要-1,是因为在输入的时候会按回车键就会输入一个\n,
            //s-1就相当于,将 \n 修改为0;
            Signal(fd);
            if (strcmp(shmaddr, "quit") == 0) break;
        }
    }

    CloseFifo(fd);

    //去关联
    int n = shmdt(shmaddr);
    Log("detach shm success", Error) << "client key:" << k << std::endl;

    //不需要删除;

    return 0;
}

信号量

基于共享内存的理解

为了让进程间通信,首先要做的是让不同的进程先看到同一份资源,所以管道,共享内存的本质都是优先解决一个问题,让不同的进程看到同一份资源;

让不同的进程看到同一份资源带来了一些时序的问题,造成了数据不一致;

把多个进程(执行流)看到的一份资源成为临界资源;

将自己的进程访问临界资源的代码称为临界区;在非临界区多个执行流是互相不影响的;

为了更好的进行临界区的保护,可以让多执行流在任何时刻都只能有一个进程进入临界区,这就叫做互斥;

 原子性,要么不做,要么做完,没有中间状态;

每一个进程想要进入临界资源,访问临界资源中的额一部分,不能让进程直接去使用临界资源,需要先申请信号量;信号量本质是一个计数器;

先申请信号量:

首先申请信号量的本质是让信号量计数器 -- ;

只要申请信号量成功,临界资源内部一定预留了你想要的资源,申请信号量本质其实是对临界资源的一种预定机制;

工作流程:申请信号量,访问临界资源(进程执行自己的临界区代码),释放信号量 ++ ;

可以用一个整数标识信号量吗?

多进程不能对全局变量同时做修改;父子进程会有写时拷贝,不相关的进程也不能看到同一个全局变量;

假设让多个进程能看到同一个全局变量(整数n在共享内存中),大家都进行申请信号量n -- ,也不可以,假设对同一个变量进行n--操作,也会出现问题;

常规情况下,client将内存中的5加载到cpu中,cpu运算--后将值再返回到内存中;

但是执行流在执行的时候,在任何时候都有可能被切换; 

如果一个n--操作只有一行汇编,那么该操作是原子的;

信号量是对临界资源的预订机制 

 【Linux】八、进程通信_第16张图片

【Linux】八、进程通信_第17张图片

你可能感兴趣的:(linux,c语言,c++)