【linux】进程间通信

一、进程间通信的本质

进程是相互独立的,要实现通信,必须让不同进程能看到同一份"资源"。"资源"是特定形式的内存空间,一般是由操作系统提供,进程访问这个空间进行通信,本质就是访问操作系统,通过系统调用接口,来访问和操作这份共享资源,以此完成数据交互。

二、进程间通信的常见需求

1、基本数据交互

2、发送命令

3、任务协同

4、事件通知

三、常见的IPC方式

System V IPC:

System V 消息队列

System V 共享内存

System V 信号量

四、匿名管道

1、管道通信的原理

只有具有血缘关系的进程之间可以进行通信,常用于父子之间,而且只能进行单向通信。

如果要进行双向通信,需要创建多个管道。

这个管道没有名字,所以叫做匿名管道。

我们让子进程进行写入,父进程进行读取。

目前,只是建立了通信信道,并没有通信,因为进程具有独立性,通信是有成本的。

2、接口

复制代码
#include<unistd.h>
int pipe(int pipefd[2]);//输出型参数
//pipefd[0]:读下标 // 3
//pipefd[1]:写下标 // 4
//上面的值为文件描述符

该接口用于创建匿名管道。

管道是有固定大小的,为64KB。不同的内核里,大小可能有差别。

3、编码实现

运行结果:

管道的特征:

1)具有血缘关系的进程进行进程间通信

2)管道只能单向通信

3)父子进程是会进程协同,同步与互斥的------保护管道文件的数据安全

4)管道是面向字节流的

5)管道是基于文件的,而文件的生命周期是跟进程同步的

管道中的四种情况:

1)读写端正常,管道如果为空,读端就要阻塞

2)读写端正常,管道如果被写满,写端就要阻塞

3)读端正常读,写端关闭,读端就会读到0,表明读到了文件结尾,不会被阻塞

4)写端正常写入,读端关闭,操作系统就要杀掉正在写入的进程,通过13号信号杀掉

**共识:**操作系统是不会做低效,浪费等工作的,如果做了,就是操作系统的bug。

4、管道的应用场景

使用管道实现一个简易版本的进程池。

1)ProcessPool.cc

2)Task.hpp

3)makefile

运行示例:

五、命名管道

1、理解

命名管道可以让毫不相关的进程进行进程间通信。

两个不同的进程打开同一个文件时,在内核中,操作系统会打开一个文件。

我们通过路径+文件名(唯一性)的方式,确定打开的是同一个文件。

管道的本质就是文件,管道是内存级文件,不需要刷新到磁盘。

进程间通信的前提:先让不同进程看到同一份资源。

2、编码

1)comm.hpp

复制代码
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <fcntl.h>
#include <cstring>

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        //创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if(n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
        int m = unlink(FIFO_FILE);
        if(m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

2)log.hpp

日志:日志时间,日志等级,日志内容,文件的名称和行号。

日志等级:

Info:常规消息

Warning:报警信息

Error:严重了,可能需要立即处理

Fatal:致命的

Debug:调试

实现一个简单的日志函数:

复制代码
#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch(level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }
    void printLog(int level, const std::string &logtxt)
    {
        switch(printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if(fd < 0) return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);
        printOneFile(filename, logtxt);
    }
    ~Log()
    {

    }
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);//初始化s
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        //格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        printLog(level, logtxt);
    }
private:
    int printMethod;
    std::string path;
};

3)client.cc

复制代码
#include <iostream>
#include "comm.hpp"

using namespace std;

int main()
{
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "client open file done" << endl;

    string line;
    while(true)
    {
        cout << "Please Enter@ ";
        getline(cin, line);

        write(fd, line.c_str(), line.size());
    }
    
    close(fd);
    return 0;
}

4)server.cc

复制代码
#include "comm.hpp"
#include "log.hpp"

using namespace std;

//管理管道文件
int main()
{
    Init init;
    Log log;
    log.Enable(Onefile);

    //打开管道
    int fd = open(FIFO_FILE, O_RDONLY);
    if(fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

    log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Warning, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Fatal, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Debug, "server open file done, error string: %s, error code: %d", strerror(errno), errno);

    //开始通信
    while(true)
    {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if(x > 0)
        {
            buffer[x] = 0;
            cout << "client say# " << buffer << endl;
        }
        else if(x == 0)
        {
            log(Debug, "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno);
            break;
        }
        else
            break;
    }

    close(fd);
    return 0;
}

5)makefile

复制代码
.PHONY:all
all:server client

server:server.cc
	mkdir -p ./log
	g++ -o $@ $^ -g -std=c++11
client:client.cc
	mkdir -p ./log
	g++ -o $@ $^ -g -std=c++11
	
.PHONY:clean
clean:
	rm -f server client

五、共享内存

1、原理

进程间通信的本质是:先让不同的进程,看到同一份资源。

操作系统要管理所有的共享内存 ----- 先描述,再组织 ----- 内核结构体描述共享内存

释放共享内存:去关联,释放共享内存。

上述操作是直接由操作系统来做的。

需求方 ------- 系统调用 ----- 执行方

2、接口和实现代码

size:创建共享内存的大小,单位是字节。

shmflg:

IPC_CREAT(单独使用):申请的共享内存不存在,就创建;存在,就获取并返回。

IPC_CREAT|IPC_EXCL:申请的共享内存不存在,就创建;存在,就出错返回。

确保成功申请一个新的共享内存。

IPC_EXCL:不单独使用。

key:

  1. key是一个数字,这个数字是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识。

2)第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了。

3)对于一个已经创建好的共享内存,key在共享内存的描述对象中。

4)第一次创建的时候,已经有一个key了。怎么有?

它是一套算法,将pathname和proj_id进行数值计算。

pathname和proj_id是由用户自由指定的。

5)key类似路径,具有唯一性。

返回值:shmid(共享内存标识符)

注意:共享内存的生命周期是随内核的。用户不主动关闭,共享内存会一直存在。除非内核重启(用户释放)。

key VS shmid:

key是操作系统内标定唯一性。

shmid只在我们的进程内,用来表示资源的唯一性。

ipcs -m:查看共享内存

ipcrm -m <shmid>:删除共享内存

代码(结合了管道以实现同步互斥机制):

1)comm.hpp

复制代码
#ifndef __COMM_HPP__
#define __COMM_HPP__

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "log.hpp"

using namespace std;

Log log;
// 共享内存的大小一般建议是4096的整数倍
// 4097,实际上操作系统给的是4096*2的大小,但我们只能访问4097,否则会越界访问
const int size = 4096; 
const string pathname="/home/syh";
const int proj_id = 0x6666;


key_t GetKey()
{
    key_t k = ftok(pathname.c_str(), proj_id);
    if(k < 0)
    {
        log(Fatal, "ftok error: %s", strerror(errno));
        exit(1);
    }
    log(Info, "ftok success, key is : 0x%x", k);
    return k;
}

int GetShareMemHelper(int flag)
{
    key_t k = GetKey();
    int shmid = shmget(k, size, flag);
    if(shmid < 0)
    {
        log(Fatal, "create share memory error: %s", strerror(errno));
        exit(2);
    }
    log(Info, "create share memory success, shmid: %d", shmid);

    return shmid;
}

int CreateShm()
{
    return GetShareMemHelper(IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm()
{
    return GetShareMemHelper(IPC_CREAT); 
}

#define FIFO_FILE "./myfifo"
#define MODE 0664

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        // 创建管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {

        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

#endif

2)processa.cc

复制代码
#include "comm.hpp"

extern Log log;

int main()
{
    Init init;
    int shmid = CreateShm();
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接

    // ipc code 在这里!!
    // 一旦有人把数据写入到共享内存,其实我们立马就能看到了!!
    // 不需要经过系统调用,直接就能看到数据了!

    int fd = open(FIFO_FILE, O_RDONLY); 
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }
    struct shmid_ds shmds;
    while(true)
    {
        char c;
        ssize_t s = read(fd, &c, 1);
        if(s == 0) break;
        else if(s < 0) break;

        cout << "client say@ " << shmaddr << endl; //直接访问共享内存
        sleep(1);

        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size: " << shmds.shm_segsz << endl;
        cout << "shm nattch: " << shmds.shm_nattch << endl;
        printf("shm key: 0x%x\n",  shmds.shm_perm.__key);
        cout << "shm mode: " << shmds.shm_perm.mode << endl;
    }

    shmdt(shmaddr);//去关联
    shmctl(shmid, IPC_RMID, nullptr);//删除共享内存

    close(fd);
    return 0;
}

3)processb.cc

复制代码
#include "comm.hpp"

int main()
{
    int shmid = GetShm();
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);//挂接

    int fd = open(FIFO_FILE, O_WRONLY); 
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }
    // 一旦有了共享内存,挂接到自己的地址空间中,直接把它当成你的内存空间来用即可!
    // ipc code
    while(true)
    {
        cout << "Please Enter@ ";
        fgets(shmaddr, 4096, stdin);

        write(fd, "c", 1); // 通知对方
    }

    shmdt(shmaddr);//去关联

    close(fd);
    return 0;
}

3、特性

1)共享内存没有同步互斥之类的保护机制。

2)共享内存是所有的进程间通信中速度最快的,因为它拷贝次数少。

3)共享内存内部的数据,由用户自己维护。

共享内存的属性:

六、信号量(浅谈)

1、理解一些概念

1)当A正在写入,写入了一部分就被B拿走了,导致双方发的和收的数据不完整---数据不一致问题

2)任何时刻,只允许一个执行流访问共享资源---互斥

3)共享的,任何时刻只允许一个执行流访问(就是执行访问代码)的资源---临界资源---一般是内存空间

4)一段代码中一般只有几行代码会访问临界资源,我们访问临界资源的代码---临界区

2、理解信号量(信号灯)

信号量的本质是一个计数器,用来描述临界资源中资源数量的多少。程序员把这个"计数器"叫做信号量。

引入计数器:

1)申请计数器成功,就表示具有访问资源的权限了

2)申请了计数器资源,但是当前还没有访问资源,申请了计数器资源是对资源的预定机制(如同我们看电影前,提前预定电影票)

3)计数器可以有效保证进入共享资源的执行流的数量

4)每一个执行流,想访问共享资源中的一部分时,不是直接访问,而是先申请计数器资源(看电影先买票)

我们把值只能为1, 0两态的计数器叫做二元信号量---本质就是一个锁(互斥功能)。

其实就是将临界资源不要分成很多块了,而是当作一个整体。整体申请,整体释放。

申请信号量,本质是对计数器--,P操作

释放资源,释放信号量,本质是对计数器++,V操作

申请和释放-----PV操作-----原子的-----要么不做,要做就做完,两态的,没有"正在做"的概念。

3、信号量凭什么是进程间通信的一种?

1)通信不仅仅是通信数据,互相协同也是通信

2)要协同,本质也是通信,信号量首先要被所有的通信进程看到

七、IPC在内核中的数据结构设计

在操作系统中,所有的IPC资源,都是整合进操作系统的IPC模块中的。

相关推荐
wdfk_prog1 小时前
EWMA、加权平均与一次低通滤波的对比与选型
linux·笔记·学习·游戏·ssh
longxibo2 小时前
【Ubuntu datasophon1.2.1 二开之六:解决CLICKHOUSE安装问题】
大数据·linux·clickhouse·ubuntu
何中应2 小时前
Jenkins如何注册为CentOS7的一个服务
linux·运维·jenkins·开发工具
yttandb2 小时前
linux的基础命令
linux·运维·服务器
之歆2 小时前
Linux 系统安装、故障排除、sudo、加密、DNS 与 Web 服务整理
linux·运维·前端
Project_Observer3 小时前
Zoho Projects自动化:状态变更时自动创建依赖任务
linux·数据库·windows
ruxshui3 小时前
# Linux diff命令使用
linux·运维·服务器
Sheffield3 小时前
为什么大家都用iptables,不愿碰原生firewalld?
linux·运维·安全
何中应3 小时前
Jenkins构建完,jar包启动不起来?
linux·运维·jenkins