【Linux】进程间通信

目录

一、进程间通信背景

1、进程间通信的理解

2、进程间通信的目的

3、进程间的必要性

二、管道

1、什么是管道

2、匿名管道

3、命名管道

4、管道通信的特点

三、System V IPC

1、共享内存

2、进程互斥

总结


一、进程间通信背景

1、进程间通信的理解

进程运行具有独立性,进程想要直接进行进行通信难度比较大,进程间通信的前提是让不同的资源能够看到同一块资源。

2、进程间通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。

3、进程间的必要性

单进程无法使用并发能力,无法实现多进程协同,传输数据,同步执行流,消息通知等。

二、管道

1、什么是管道

想象现实中的管道,它一定是有一个出口有一个入口,用来传输资源,资源只能从一端流向另一端

进程间通信的管道也同理,一个进程发送数据另一个进程接收数据。

管道是由操作系统所提供的最古老的进程间通信的形式。

2、匿名管道

匿名管道只适用于具有亲缘关系的进程间通信——通常用于父子进程间的通信

一个进程创建子进程,父进程打开的文件也会被子进程所继承,因为是同一个文件,所以父子进程打开的同一个文件具有相同的struct file,因此父子进程就同时看到了同一份内存资源,也就可以借助这个文件来进行父子进程间的通信。

匿名管道创建步骤

1、分别以读写方式打开同一个文件

2、fork()创建子进程

3、双方进程各自关闭自己不需要的文件描述符

管道文件是不需要刷新到磁盘的,它是专门用来进行进程间通信的文件,刷新到磁盘,相当于进行IO使进程间通信的效率降低

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

我们可以使用pipe来创建匿名管道,这里要传入一个数组,它是输出线参数

下标为0的代表读端,1代表写端

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

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

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

 这样前置工作就准备好了

接下来以一个比较复杂的例子来演示匿名管道

写一个简单的单机版负载均衡

创建一个进程池,父进程通过匿名管道随机向子进程派发任务

//task.hpp
#pragma once

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

typedef std::function func;
std::vector callbacks;
std::unordered_map desc;

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

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

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

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

void Load()
{
    desc.emplace(callbacks.size(), "readSQL: 读取数据库");
    callbacks.emplace_back(readMySQL);

    desc.emplace(callbacks.size(), "execuleURL: 进行url解析");
    callbacks.emplace_back(execuleUrl);

    desc.emplace(callbacks.size(), "cal: 进行加密计算");
    callbacks.emplace_back(cal);

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

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

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


//main.cpp
#include "task.hpp"

#define PROCESS_NUM 5

// 父进程给子进程派发任务

int waitCommind(int waitFd, bool &quit)
{
    uint32_t command = 0;
    ssize_t s = read(waitFd, &command, sizeof(command));
    if (s == 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));
    std::cout << "main process: call process " << who << "execute" << desc[command] << "through" << fd << std::endl;
}

int main()
{
    Load();

    // 简单的线程池
    // 进程pid 及 文件描述符
    std::vector> slots;

    // 创建子进程
    for (size_t i = 0; i < PROCESS_NUM; i++)
    {
        // 1、以读写方式打开文件
        int fd[2] = {0};
        int n = pipe(fd);
        assert(n == 0);

        pid_t id = fork();
        if (id < 0)
        {
            std::cerr << "fork" << std::endl;
        }
        else if (id == 0)
        {
            // child
            // 关闭写端
            close(fd[1]);
            while (true)
            {
                bool quit = false;
                int command = waitCommind(fd[0], quit);
                if (quit == true)
                {
                    break;
                }

                if (command >= 0 && command < handlerSize())
                {
                    callbacks[command];
                }
                else
                {
                    std::cout << "非法command" << std::endl;
                }
            }
            exit(1);
        }
        else
        {
            // father
            // 关闭读端
            close(fd[0]);
            slots.emplace_back(id, fd[1]);
        }
    }

    // 派发任务
    size_t count = 0;
    srand(time(0));
    while (true)
    {
        int command = rand() % handlerSize();

        int choice = rand() % slots.size();

        sendAndWakeUp(slots[choice].first, slots[choice].second, command);
        sleep(1);
        count++;

        if (count == 10)
        {
            break;
        }
    }

    // 关闭文件描述符
    for (const auto &slot : slots)
    {
        close(slot.second);
    }

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

    return 0;
}

3、命名管道

命名管道与匿名管道类似,不过命名管道可以让不具有亲缘关系的进程通信

首先也是要让不同的进程看到同一份资源,双方进程就可以通过管道文件的路径看到同一份资源

管道文件首先是一个文件,它是有名字的可以被打开,但是不会将内存数据进行刷新到磁盘

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

mkfifo命名可以创建一个管道文件

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

我们通过echo命令将hello通过管道文件发送过去,echo命令就进入了阻塞状态

这时通过管道文件就可以读取资源了

这样就完成了echo和cat的通信

删除管道文件可以使用unlink或者rm

 接下来看代码的方式创建管道文件

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

mkfifo可以创建管道文件

成功返回0,失败返回-1

 接下来还是举一个例子来演示命名管道进程间通信

server创建管道文件,从client读取资源,结束通信后,client关闭管道文件并且删除管道文件

client获取管道文件,进行正常通信,向server发送数据

//comm.hpp
#pragma once

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

#define MODE 0666
#define SIZE 128
#define PROCESS_NUM 3

std::string ipcPath = "./fifo.ipc";



//log.hpp
#include 
#include 
#include 

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

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


//server.cpp
#include "log.hpp"
#include "comm.hpp"

static void GetMessage(int fd)
{
    char buffer[SIZE];
    while (true)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer));
        if (s > 0)
        {
            buffer[s] = '\0';
            std::cout << "client call" << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cerr << "[ " << getpid() << " ]"
                      << "read end of file server quit" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read" << std::endl;
            break;
        }
    }
}

int main()
{
    // 1、创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        std::cerr << "mkfifo" << std::endl;
        exit(1);
    }

    Log("创建管道文件成功", Debug) << "step 1" << std::endl;

    // 2、正常文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open" << std::endl;
    }

    Log("打开文件成功", Debug) << "step 2" << std::endl;

    // 3、正常通信代码
    // 使用进程池

    Log("进程间开始通信", Debug) << "step 3" << std::endl;

    for (size_t i = 0; i < PROCESS_NUM; i++)
    {
        pid_t id = fork();
        if (id < 0)
        {
            std::cerr << "fork" << std::endl;
        }
        else if (id == 0)
        {
            GetMessage(fd);
            exit(1);
        }
    }

    Log("进程间通信完成", Debug) << "step 4" << std::endl;

    // 4、回收子进程

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

    Log("等待子进程成功", Debug) << "step 5" << std::endl;

    // 5、关闭文件
    close(fd);

    Log("关闭文件成功", Debug) << "step 6" << std::endl;

    // 5、删除文件
    unlink(ipcPath.c_str());
    Log("删除管道文件成功", Debug) << "step 7" << std::endl;

    return 0;
}


//clinet.cpp
#include "comm.hpp"

int main()
{
    //1、获取管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd < 0)
    {
        std::cerr << "open" << std::endl;
        exit(1);
    }

    //2、IPC过程
    std::string buffer;
    while(true)
    {
        std::cout << "Please Enter Mseeage Line:> ";
        std::getline(std::cin, buffer);
        if(buffer == "quit")
        {
            break;
        }
        write(fd, buffer.c_str(), buffer.size());
    }

    //3、关闭管道文件,server自动停止读取
    close(fd);
    return 0;
}

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

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

命名管道多个进程竞争读取,没有任何问题,发送数据大小是小于4096字节就是原子的

4、管道通信的特点

1、管道具有通过让进程间协同,提供了访问控制

2、管道提供的是面向流式的通信服务——面向字节流——协议

3、管道是基于文件的,文件的声明周期是随进程的,管道的声明周期是随进程的

4、管道是单向通信的,就是半双工通信的一种特殊情况

管道的访问控制

1、写快,读慢,写满就不能再写了

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

3、写关,读0,标识读到了文件结尾

4、读关,写继续,OS终止写进程

三、System V IPC

1、共享内存

还要说一下通信的前提:让不同的进程能够看到同一份资源共享内存也是同样道理

不过它是直接向OS申请一块空间,然后映射到进程的共享区,这样就使得进程间通信的效率提升,因为不用使用系统调用多次拷贝资源了,进程可以直接访问那块共享的内存资源

1)申请空间

2)建立映射(多个进程映射同一块共享内存)

3)通信

4)去掉映射

5)释放空间


共享内存的提供者是操作系统,操作系统也要管理共享内存,通过先描述在组织

共享内存 = 共享内存块 + 对应的共享内存的内核数据结构

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

我们可以使用shmget函数来申请共享内存

它的返回值shmid是共享内存的用户标识符,类似于文件描述符fd

它的最后一个参数有两个选项IPC_CREAT and IPC_EXCL 

一般两者是组合使用的,单独使用IPC_EXCL是没有意义的

单独使用IPC_CREAT:如果创建共享内存,底层已经存在,那么就获取它,不存在就创建它

两者组合使用:如果底层不存在就创建它,并且返回新创建的shmid,如果底层存在,出错返回

接下来就是shmget的第一个参数key

key可以保证与我们进程通信的进程就是要通信的进程,并且能够看到我创建的共享内存

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

我们可以使用ftok函数来创建key,ftok是一种加密算法,使用同样的算法规则,传入的参数相同就能够形成唯一值,ftok的第一个参数pathname最好设定我们有访问权限的路径

shmid vs key

只有创建的时候使用key,大部分情况用户访问共享内存,都是使用的shmid

当我们的程序运行结束,我们的共享内存,还存在

System V IPC资源的生命周期随内核

解决办法

1、手动删除

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

ipcs -m可以获取共享内存相关属性

使用命令 ipcrm -m + shmid  可以删除共享内存 

2、代码删除

shmctl + 选项IPC_RMID可以删除共享内存 

共享内存链接方法

使用shmat可以将共享内存与进程建立映射,这个函数类似于malloc,返回值是void*可以强制转换为char*当作数组来使用共享内存

共享内存去关联方法

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

去除关联的方式是使用shmdt

注意:创建共享内存的大小最好是页(4096bytes)的整数倍

如果你申请4097个字节,操作系统会给你申请4096 * 2bytes,剩下的4095字节就浪费了

总结:

shmget返回值是用户层标识符

key是内核层面标定共享内存的标识符

想要自己挂接共享内存等对共享内存的操作要使用shmid,同时共享内存的生命周期是随内核的

shmat:挂接共享内存

shmdt:去关联

shmctl :删除共享内存

共享内存是在堆栈之间的共享区,它是属于用户的

用户空间:不用经过系统调用就可以访问的空间

管道的通信方式是文件,它是在内核空间中,它是内核的一种特定的数据结构,是操作系统维护的在[3G, 4G]的内核空间中,用户访问需要使用系统调用

下面看一个简单的例子

//comm.hpp
#pragma once

#include 
#include 
#include 
#include 
#include "../named_pipe/log.hpp"
#include 
#include 
#include 
#include 
#include 
#include 

#define FIFO_NAME "./fifo"
#define PROJ_ID 0x66
#define SHM_SIZE 4096
#define PATH_NAME "/home/ww"

class Init
{
public:
    Init()
    {
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        Log("创建管道文件成功", Notice) << "\n";
    }

    ~Init()
    {
        unlink(FIFO_NAME);
        Log("删除管道文件成功", Notice) << "\n";
    }
};

#define READ O_RDONLY
#define WRITE O_WRONLY

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

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

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

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


//server.cpp
#include "comm.hpp"

Init init; 

int main()
{
    // 1、生成相同的key
    key_t key = ftok(PATH_NAME, PROJ_ID);

    Log("生成key成功", Notice) << "key: " << key << std::endl;

    // 2、申请共享内存
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        Log("申请共享内存失败", Error) << "shmid: " << shmid << std::endl;
        exit(2);
    }

    Log("申请共享内存成功", Notice) << "shmid: " << shmid << std::endl;
    // 3、共享内存与进程建立映射

    char *adder = (char *)shmat(shmid, nullptr, 0);

    Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;

    // 4、正常通信
    Log("开始通信", Notice) << std::endl;
    // 共享内存是没有访问控制的,使用管道进行管道控制

    int fd = OpenFIFO(FIFO_NAME, READ);
    while (true)
    {
        Wait(fd);
        printf("%s\n", adder);
        if (strcmp(adder, "quit") == 0)
            break;
    }

    // 5、取消映射

    int ret = shmdt(adder);
    if (ret < 0)
    {
        Log("共享内存去关联失败", Error) << std::endl;
    }

    // 6、删除共享内存

    ret = shmctl(shmid, IPC_RMID, nullptr);
    if (ret < 0)
    {
        Log("删除共享内存失败", Error) << std::endl;
        exit(3);
    }
    return 0;
}



//client.cpp
#include "comm.hpp"

int main()
{
    // 1、生成相同的key
    key_t key = ftok(PATH_NAME, PROJ_ID);
    Log("生成key成功", Notice) << "key: " << key << std::endl;

    // 2、获取共享内存
    int shmid = shmget(key, SHM_SIZE, 0);
    if (shmid < 0)
    {
        Log("获取共享内存失败", Error) << "shmid: " << shmid << std::endl;
        exit(2);
    }

    // 3、建立映射
    char *adder = (char *)shmat(shmid, nullptr, 0);

    Log("链接共享内存成功", Notice) << "shmAdder: " << adder << std::endl;

    // 4、通信
    Log("开始通信", Notice) << std::endl;

    int fd = OpenFIFO(FIFO_NAME, WRITE);
    while (true)
    {
        ssize_t s = read(0, adder, SHM_SIZE - 1);
        if (s > 0)
        {
            adder[s - 1] = 0;
            Signal(fd);
            if (strcmp(adder, "quit") == 0)
                break;
        }
    }

    // 5、取消映射
    int ret = shmdt(adder);
    if (ret < 0)
    {
        Log("取消共享内存失败", Error) << std::endl;
    }

    return 0;
}

2、进程互斥

为了让进程间通信,让不同的进程之间看到同一份资源,之前所说的所有通信方式,本质都是优先解决一个问题,让不同的进程看到同一份资源

这样就会带来一些时序问题,造成数据不一致问题

1、多个进程看到的公共的一份资源叫做临界资源

2、把自己的进程访问临界资源的代码叫做临界区

3、为了更好的保护临界区,可以让在任何时刻都只能有一个进程进入临界区——互斥

4、原子性:要么不做,要么做完,没有中间状态


总结


以上就是今天要讲的内容,本文仅仅简单介绍了进程间通信的管道和共享内存

你可能感兴趣的:(Linux,linux,centos)