Linux–进程间通信

前言

        上一篇相关Linux文章已经时隔2月,Linux的学习也相对于来说是更加苦涩;无妨,漫漫其修远兮,吾将上下而求索。

下面该片文章主要是对进程间通信进行介绍,还对管道,消息队列,共享内存,信号量都一 一进行讲解,虽然是改知识都是属于进程间通信的范畴,但更多是为了更好的理解进程。如果对该文章的进行深入了解,想必对进程的理解也能得到巨大的提升。

进程间通信

在讲进程时,非常重要的一个知识就是:进程具有独立性!既然它具有独立性,那为什么又需要通信?进程间为什么要需要联系?进程的独立性是独立的分配资源,不同进程之间的资源是独立的。但是进程不是孤立的,进程与进程之间是需要进行信息的交互和状态的传递等。比如父子进程,他们是需要通信,随时了解状态等信息。因此需要进程间通信。


Ⅰ什么是通信

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

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

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

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

Ⅱ 为什么需要通信

在很多时候是需要多进程协同的,例如:

查看一个文件中的 "hello",这个时候就需要一个进程专门负责打印文件,另一进程负责进行过滤。两个进程通过'|'(书划线)

cat file | grep 'hello'

总结:需要多进程协同去完成某种业务内容

通信的本质问题:操作系统需要直接或者间接给通信双方的进程提供"内存空间",通信的进程,必须看见同一份公共资源。

Ⅲ如何通信

1.管道--基于文件系统下

(1)匿名管道pipe

(2)命名管道

 

2.System V IPC --聚焦在本地通信

(1)System V 消息队列

(2)System V 共享内存

(3)System V 信号量

3.POSIX IPC--让通信过程可以跨主机

(1)消息队列

(2)共享内存

(3)信号量

(4)互斥量

(5)条件变量

(6)读写锁

不同的通讯种类本质就是:通过“公共资源”实现双方通讯,公共资源是操作系统的特地一模个块所提供的,例如:“公共资源”是文件,那么就是管道通信。

 管道

管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道是通过(不同的)PCB控制fd链接共同管道文件(内存级文件),故内存不会通过磁盘I/O的方式进行通信。


Ⅰ匿名管道

实现两个进程看到同一个管道文件,实质就是通过父进程通过管道特定的系统调用,以读方式和写方式打开一个内存级文件,并通过fork创建子进程时写时拷贝被子,进程继承后,各自关闭自己的读写端进而形成一条通信信道,这条通信信道是基于文件的,故此叫做管道。这个管道文件是没有名字的,所以就叫做匿名管道。

pipe(管道)的用法

#include<unistd.h>

功能:创建一无名管道

原型 

int pipe(int fd[2]);

参数

fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端

返回值:成功返回0,失败返回错误代码

用fork来共享管道原理 

 实 例 代 码

代码的实现也是按照深度理解管道的步骤进行的,第一步创建管道-pipe,第二步父进程fork出子进程,第三步父进程关闭写-fds[1]子进程关闭读-fds[0]。

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
#include <sys/types.h>
#include <cstring>
#include <cstdio>


using namespace std;

//父进程进行读取,子进程进行写入
int main()
{
    //第一步:创建管道文件,打开读写端
    int fds[2];
    int n = pipe(fds);
    assert(n==0);

    //第二步:fork
    pid_t id = fork();
    assert(id >= 0);
    if(id == 0)
    {
        //子进程的通讯代码
        close(fds[0]);
        //string msg = "hello , i am child";
        const char *s ="我是子进程,正在发消息->";
        int cnt = 0;
        while(true)
        {
            //int sprintf(char *str, const char *format, ...);
            //int fprintf(FILE *stream, const char *format, ...);
            //int snprintf(char *str, size_t size, const char *format, ...);

            cnt++;
            char buf[1024];
            snprintf(buf,sizeof buf, "child->parent say: %s[%d][%d]n",s, cnt,getpid());
            //写端写满的时候,在写会阻塞,等对方进行读取!
            write(fds[1],buf,strlen(buf));
            cout<<"count: " << cnt <<endl;

            //sleep(20);//每隔一秒写入
            //break;
        }

        //子进程
        close(fds[1]); 
        cout << "子进程关闭自己的写端" << endl;
        exit(0);
    }
    //父进程进行读取
    close(fds[1]);

    //父进程的通信代码
    while(true)
    {
        char buf[1024];
        ssize_t s = read(fds[0],buf,sizeof(buf)-1);

        if(s>0)
        {
            buf[s]=0;
            cout << "Get Message# " << buf <<"| my pid:" << getpid() << endl;
        }
        else if(s == 0)
        {
            cout << "read: " << s << endl;
            break;
        } 
        //sleep(5);
        //细节:父进程可没有进行sleep
        break;
    }

    close(fds[0]);
    cout <<"父进程关闭读端"<< endl;

    int status = 0;

    n = waitpid(id,&status,0);
    assert(n == id);
    
    cout << "pid->"<<(status & 0x7F)<<endl;



    // 0,1,2被占用->3 ,4
    //[0]:读取 
    //[1]:写入  
    // cout<<"fds[0]:"<< fds[0]<<endl;
    // cout<<"fds[1]:"<< fds[1]<<endl;


    return 0;
}

结果演示 

[hongxin@VM-8-2-centos pipe]$ make
g++ -o mypipe mypipe.cc -std=c++11
[hongxin@VM-8-2-centos pipe]$ ./mypipe 
Get Message# child->parent say: 我是子进程,正在发消息->[1][12302]
| my pid:12301
Get Message# child->parent say: 我是子进程,正在发消息->[2][12302]
| my pid:12301
Get Message# child->parent say: 我是子进程,正在发消息->[3][12302]
| my pid:12301
Get Message# child->parent say: 我是子进程,正在发消息->[4][12302]
| my pid:12301
Get Message# child->parent say: 我是子进程,正在发消息->[5][12302]
| my pid:12301
Get Message# child->parent say: 我是子进程,正在发消息->[6][12302]
| my pid:12301
Get Message# child->parent say: 我是子进程,正在发消息->[7][12302]
| my pid:12301
Get Message# child->parent say: 我是子进程,正在发消息->[8][12302]
| my pid:12301

站在文件描述符角度-深度理解管道

读写特征

1.读慢,写快--缓存写

[hongxin@VM-8-2-centos pipe]$ ./mypipe 
Get Message# child->parent say: 我是子进程,正在发消息->[1][16115]
| my pid:16114
Get Message# child->parent say: 我是子进程,正在发消息->[2][16115]
child->parent say: 我是子进程,正在发消息->[3][16115]
child->parent say: 我是子进程,正在发消息->[4][16115]
child->parent say: 我是子进程,正在发消息->[5][16115]

2.读快,写慢--需要读等写

[hongxin@VM-8-2-centos pipe]$ ./mypipe 
Get Message# child->parent say: 我是子进程,正在发消息->[1][16754]
| my pid:16753
Get Message# child->parent say: 我是子进程,正在发消息->[2][16754]
| my pid:16753
Get Message# child->parent say: 我是子进程,正在发消息->[3][16754]
| my pid:16753

3.写关闭,读到0

[hongxin@VM-8-2-centos pipe]$ ./mypipe 
count: 1
Get Message# child->parent say: 我是子进程,正在发消息->[1][18227]
| my pid:18226
read: 0

4.读关闭,写的话OS会终止写端 会给写进程发送信号,终止写端。 
[hongxin@VM-8-2-centos pipe]$ ./mypipe 
count: 1
count: 2
count: 3
Get Message# child->parent say: 我是子进程,正在发消息->[1][21895]
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
count: 10
count: 11
| my pid:21894
count: 12
count: 13
父进程关闭读端
pid->13

操作系统会给写进程发送13号-sigpipe终止之前的写进程

管道的特征

1.管道的生命周期随进程

2.管道可以用来具有血缘关系的进程之间进行通信,常用于父子通信

3.管道是面向字节流(网络)

4.半双工通信--单向通信(特殊概念)

5.互斥与同步机制--对共享资源进行保护的方案

Ⅱ深度理解控制管道--一父控4子

实现功能:父进程通过管道随机给不同子进程发随机的任务,子进程接受后执行任务。

思路:

1.建立子进程并建立和子进程通信的信道

        1.1加载方法表

                1.1.1声明函数指针类型

                1.1.2创建函数任务

                1.1.3加载任务          

        1.2创建子进程,并且维护好父子通信信道

                1.2.1创建子进程和管道

                1.2.2通信

                        获取命令码   --recvTask(接受任务):read

                        完成任务      --调用函数

2.父进程控制子进程,负载均衡的向子进程发生命令码

        2.1设置随机数种子MakeSeed()

        2.2随机选择进程--通过随机数%进程数量

        2.3随机选择任务--通过随机数%任务数量

        2.4随机选择进程后发送随机的任务

        2.5次数限制/永远进行

3.回收子进程信息--避免僵尸

测 试 代 码

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

//获取随机数
#define MakeSeed()  srand((unsigned long)time(nullptr)^getpid()^0x123456 ^ rand()%1234);

#define PROCSS_NUM 5

///子进程完成某一种任务--模拟

//函数指针类型
typedef void (*func_t)();

void downloadTask()
{
    std::cout<< getpid() <<"下载任务n"<<std::endl;
    sleep(1);
}

void ioTask()
{
    std::cout<<getpid() <<"IO任务n"<<std::endl;
    sleep(1);
}

void flushTask()
{
    std::cout<<getpid() <<"刷新任务n"<<std::endl;
    sleep(1);
}

//加载任务到函数指针类型的数组中
void loadTaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(downloadTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}


///下面代码是多进程程序//

//结构体类型
class subEp //Endpoint
{
public:
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)//初始化列表
    {
        //将不同的进程的pid-文件描述信息 通过 snprintf函数以字符串的形式传入缓冲区nameBuffer 在拷贝在name_
        char nameBuffer[1024];  
        snprintf(nameBuffer,sizeof nameBuffer,"process-%d[pid(%d)-fd(%d)]",num++,subId_,writeFd_);
        name_=nameBuffer;
    }

public:
    static int num;
    std::string name_;  //进程信息
    pid_t subId_;       //进程的pid
    int writeFd_;       //子进程的文件描述符
};
int subEp::num =0;

//接受任务
int recvTask (int readFd)
{
    int code = 0;
    ssize_t s = read(readFd,&code,sizeof code);//读取4byte(文件描述符(int类型))
    if(s == 4) return code;
    else if(s <= 0) return -1;
    else return 0;
}

//发送任务
void sendTask(const subEp &process,int taskNum)
{
    std::cout << "send task num :"<< taskNum <<" send to " << process.name_ << std::endl;
    int n = write(process.writeFd_,&taskNum,sizeof(taskNum));//通过文件描述符(process.writeFd_就代表是哪个进程)发送(int类型-taskNum就代表几号任务)
    assert(n == sizeof(int));
    (void)n;
}

//生成子进程/管道 处理任务
void createSubProcess(std::vector<subEp> *subs,std::vector<func_t> &funcMap)
{
    std::vector<int> deleteFd;
    for (int i = 0; i < PROCSS_NUM; i++)
    {
         for(int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]);
        //生成管道
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;

        //生成子进程
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程,进行处理任务
            close(fds[1]);//关闭子进程(管道)写
            while (true)
            {
                //1.获取命令码,如果没有发送,子进程应该阻塞
                int commandCode = recvTask(fds[0]);
                //2.完成任务 /* code */
                if(commandCode >= 0 && commandCode < funcMap.size()) funcMap[commandCode]();//通过数组下标选择并执行该任务
                else if(commandCode == -1) break;

            }
            exit(0);
        }

        //父进程建立信道
        close(fds[0]);//关闭父进程(管道)读
        subEp sub(id, fds[1]);//初始化
        subs->push_back(sub); //后插
        deleteFd.push_back(fds[1]);
    }
}
 
void loadBlanContrl(const std::vector<subEp>& subs,const std::vector<func_t> &funcMap,int count)
{
    int processnum = subs.size();//进程数量
    int tasknum = funcMap.size();//任务数量
    bool forever = (count == 0 ? true :false);
    while(true)
    {
        //1.先选择一个进程 -->std::vector<subEp> ->index -随机选择进程
        int subIdx = rand()%processnum;
        //2.选择一个任务  -->std::vector<func_t> ->index -随机选择任务
        int taskIdx = rand() % tasknum;
        //3.任务发送给选择的进程
        sendTask(subs[subIdx], taskIdx);//发送随机选择的进程/随机任务

        sleep(1);//每隔1秒发送
        
        //限制发送次数和0退出
        if(!forever)
        {
            count--;
            if(count == 0)break;
        }
    }
    //write quit -> read 0
    //退出后 全部关闭通道
    for(int i = 0;i < processnum; i++)
    {
        close(subs[i].writeFd_);
    }
}

//回收子进程信息
void waitProcess(std::vector<subEp> processes)
{
    int processnum = processes.size();//进程数量
    for(int i = 0;i < processnum;i++)
    {
        waitpid(processes[i].subId_,nullptr,0);//回收子进程信息,避免僵尸
        std::cout << "wait sub process success ...:" <<processes[i].subId_ << std::endl;
    }
}

int main()
{
    MakeSeed()//随机数种子

    // 1.建立子进程并建立和子进程通信的信道
    //1.1加载方法表
    std::vector<func_t> funcMap;//数组<函数指针> 
    loadTaskFunc(&funcMap);//加载任务
    //1.2创建子进程,并且维护好父子通信信道
    std::vector<subEp> subs;//数组<结构体/类> 
    createSubProcess(&subs,funcMap);//创建进程/管道

    // 2.走到这里是父进程,控制子进程,负载均衡的向子进程发生命令码
    int taskCnt =10;//0:永远进行
    loadBlanContrl(subs,funcMap,taskCnt);


    // 3.回收子进程信息
    waitProcess(subs);

    return 0;
}

站在内核角度-管道本质 

 

Ⅲ命名管道

创建一个命名管道

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

mkfifo filename

mkfifo clean_pipe

prw-rw-r-- 1 hongxin hongxin   0 Jan 30 17:47 clean_pipe

♧两个文件的通信--先建立两个普通文件和一个管道文件

prw-rw-r-- 1 hongxin hongxin   0 Jan 30 17:47 clean_pipe
-rw-rw-r-- 1 hongxin hongxin  96 Jan 30 17:40 client.cc
-rw-rw-r-- 1 hongxin hongxin 162 Jan 30 17:41 makefile
-rw-rw-r-- 1 hongxin hongxin  96 Jan 30 17:40 server.cc

脚本语言

cnt=0; while :; do echo "hello world-> $cnt"; let cnt++; sleep 1; done > named_pipe

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

        #include <sys/types.h>
        #include <sys/stat.h>

        int mkfifo(const char *filename,mode_t mode);

♧ 命名管道也可以从程序里删除,相关函数:

       #include <unistd.h>

       int unlink(const char *path);

例子-用命名管道实现server&client通信

server.cc

#include "comm.hpp"

int main()
{
    bool r = createFifo(NAMED_PIPE);
    assert(r);
    (void)r;

    int rfd = open(NAMED_PIPE,O_RDONLY);
    if(rfd < 0) exit(1);

    //read
    char buf[1024];
    while(true)
    {
        ssize_t s = read(rfd, buf, sizeof(buf)-1);
        if(s > 0)
        {
            buf[s] = 0;
            std::cout << "client->server# " << buf << std::endl;
        }
        else if(s == 0)
        {
            std::cout << "client quit, me too!" << std::endl;
            break;
        }
        else{
                std::cout << "err string" << strerror(errno) << std::endl;
                break;
        }
    }

    close(rfd);

    removeFifo(NAMED_PIPE);

    return 0;
}

client.cc 

#include "comm.hpp"

int main()
{
    std::cout << "client begin" << std::endl;
    int wfd = open(NAMED_PIPE,O_WRONLY);
    std::cout << "client end" << std::endl;
    if(wfd < 0) exit(1);

    //write
    char buf[1024];
    while(true)
    {
        std::cout << "Please Say:";
        fgets(buf,sizeof buf,stdin);
        if(strlen(buf) > 0) buf[strlen(buf)-1] = 0;
        ssize_t n = write(wfd,buf,strlen(buf));
        assert(n == strlen(buf));
        (void)n;
        
    }

    close(wfd);

    return 0;
}

 comm.hpp

#pragma once

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

#define NAMED_PIPE "/tmp/mypipe.130"

bool createFifo(const std::string &path)
{
    umask(0);
    int n = mkfifo(path.c_str(), 0600);
    if (n == 0)
        return true;
    else
    {
        std::cout << "errno: " << errno << " err string: " << strerror(errno) << std::endl;
        return false;
    }
}

void removeFifo(const std::string &path)
{
    int n = unlink(path.c_str());
    assert(n == 0); // debug , release 里面就没有了
    (void)n;
}

演示:

system V共享内存

Ⅰ共享内存的介绍

共享内存的原理

原理图如下

对共享内存的理解:

        1.共享内存是专门设计用来IPC(进程间通信):以前c语言中学的malloc是开辟一块空间,pcb从进程地址空间通过页表映射到内存开辟一块空间,但是这块空间是不被其他进程所共享的。

        2.共享内存是一种通信方式,所有的进程都可以用:开辟一块共享内存,不光是你运行的进程可以用,其他进程也也可以访问这块内存,只是看如何获取该内存的shmid 。

        3.OS(操作系统)中一定会同时存在很多的共享内存。

共享内存的概念

通过让不同的进程,看到同一个内存块的方式:共享内存

Ⅱ共享内存函数

shmget函数 

头文件:

       #include <sys/ipc.h>
       #include <sys/shm.h>

功能:用来创建共享内存

原型:int shmget(key_t key, size_t size, int shmflg);

参数

        key:这个共享内存段名字

        size:共享内存大小

        shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

参数 shmflg

        有两个默认选项IPC_CREAT,IPC_EXCL

        IPC_CREAT:如果创建共享内存时,不存在,创建即可;存在,获取该共享内存。

        IPC_EXCL:

                1.无法单独使用 ;

                2.IPC_CREAT | IPC_EXCL同时使用时,如果不存在就创建,如果存在就返回错误。换句话说,如果创建了一块共享内存,那么这个共享内存一定是新的shm。

返回值 int

         On success, a valid shared memory identifier is returned.  On errir, -1 is returned, and errno is set to indicate the error.

        --创建成功,返回一个合法的共享内存的标识符。创建失败,返回-1,错误码被设置。

参数 key 

        作用是:能进行唯一性标识

        通过ftok函数形成一个key,ftok的介绍     

                头文件: #include <sys/types.h>       #include <sys/ipc.h>

                原型: key_t ftok(const char *pathname, int proj_id);

                作用:将路径名和项目标识符转换为唯一标识符key

                参数:*pathname 路径名 ,proj_id 项目标识符

shmctl函数

功能:用于控制共享内存

头文件:

        #include <sys/ipc.h>
        #include <sys/shm.h>

原型

        int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数

        shmid:由shmget返回的共享内存标识码

        cmd:将要采取的动作(有三个可取值) IPC_SET / IPC_RMID / IPC_INFO

        buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

shmat函数

功能:创建共享内存与进程的关联

头文件

       #include <sys/types.h>
       #include <sys/shm.h>

原型

       void *shmat(int shmid, const void *shmaddr, int shmflg);

参数

        shmid:由shmget返回的共享内存标识码

       *shmaddr:一般设置为nullptr,系统会选择一个合适的(未使用的)地址来附加段。(因为我们一般是不知道内存地址的)

        shmflg::默认0,共享内存只读

返回值:成功返回shmat()附加的共享内存段的地址;失败返回-1

shmdt函数

功能:取消共享内存与进程的关联

头文件:

       #include <sys/types.h>
       #include <sys/shm.h>

原型

        int shmdt(const void *shmaddr);

参数

        *shmaddr:shmat()的返回值

返回值:成功返回0;失败返回-1

Ⅲ实例代码

makefile

.PHONY:all
all:shm_client shm_server

shm_client:shm_client.cc
	g++ -o $@ $^ -std=c++11
shm_server:shm_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shm_client shm_server

comm.hpp

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAME "."
#define PROJ_ID  0x66
// 共享内存的大小,一般建议是4KB的整数倍
// 系统分配共享内存是以4KB为单位的! --- 内存划分内存块的基本单位Page
#define MAX_SIZE 4097  // --- 内核给你的会向上取整, 内核给你的,和你能用的,是两码事

key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID); //可以获取同样的一个Key!
    if(k < 0)
    {
        // cin, cout, cerr -> stdin, stdout, stderr -> 0, 1, 2
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}
int getShmHelper(key_t k, int flags)
{
    // k是要shmget,设置进入共享内存属性中的!用来表示
    // 该共享内存,在内核中的唯一性!!
    // shmid vs key:
    // fd    vs inode
    int shmid = shmget(k, MAX_SIZE, flags);
    if(shmid < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int getShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT);
}

int createShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}

void *attachShm(int shmid)
{
    // int a = 10;
    // 100u; // 字面值
    // 纯数字是没有意义的,必须得有类型才可以
    // 10u;
    // 10L;
    // 10;
    // 3.14f;
    // 100元,200$
    void *mem = shmat(shmid, nullptr, 0); //64系统,8
    if((long long)mem == -1L)
    {
        std::cerr <<"shmat: "<< errno << ":" << strerror(errno) << std::endl;
        exit(3);
    }
    return mem;
}

void detachShm(void *start)
{
    if(shmdt(start) == -1)
    {
        std::cerr <<"shmdt: "<< errno << ":" << strerror(errno) << std::endl;
    }
}

void delShm(int shmid)
{
    if(shmctl(shmid, IPC_RMID, nullptr) == -1)
    {
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }
}

#endif

shm_server.cc

#include "comm.hpp"
#include <unistd.h>

int main()
{
    key_t k = getKey(); 
    printf("key: 0x%xn", k); // key
    int shmid = createShm(k);
    printf("shmid: %dn", shmid); //shmid 

    // sleep(5);
    
    char *start = (char*)attachShm(shmid);
    printf("attach success, address start: %pn", start);

    // 使用
    while(true)
    {
        // char buffer[]; read(pipefd, buffer, ...)
        printf("client say : %sn", start);
        struct shmid_ds ds;
        shmctl(shmid, IPC_STAT, &ds);
        printf("获取属性: size: %d, pid: %d, myself: %d, key: 0x%x",
                ds.shm_segsz, ds.shm_cpid, getpid(), ds.shm_perm.__key);
        sleep(1);
    }

    // 去关联
    detachShm(start);

    sleep(10);

    // 删除共享内存
    delShm(shmid);
    return 0;
}

shm_client.cc

#include "comm.hpp"
#include <unistd.h>

int main()
{
    key_t k = getKey(); 
    printf("key: 0x%xn", k);
    int shmid = getShm(k);
    printf("shmid: %dn", shmid);

    char *start = (char*)attachShm(shmid);
    printf("attach success, address start: %pn", start);

    const char* message = "hello server, 我是另一个进程,正在和你通信";
    pid_t id = getpid();
    int cnt = 1;
    // char buffer[1024];
    while(true)
    {
        sleep(5);
        snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);
        // snprintf(buffer, sizeof(buffer), "%s[pid:%d][消息编号:%d]", message, id, cnt++);
        // memcpy(start, buffer, strlen(buffer)+1);
        // pid, count, message
    }

    detachShm(start);

    //done

    return 0;
}

Ⅳ代码实现的细节把控

再谈key

共享内存和malloc开辟的内存是一样的,都是先需要先描述,后组织。当我们调用接口,操作系统开辟一块普通的内存空间时,操作系统是需要维护的它的,所以他们就不仅仅只开辟一块物理空间。共享内存空间亦是如此:物理内存空间+共享内存的相关属性

操作系统对共享内存相关属性进行增删改查,实质就是对它对应的物理内存空间进行管理。在创建共享内存时,因为共享内存相关属性必须有一个数据体现出改共享内存的唯一性--key。

为什么共享内存要在系统中保持唯一性呢?因为在OS中,可能会存在多个共享内存。

key是为了保证该共享内存的唯一性,也是为了让其他进程能够看到key,当其他进程识别到key时,就可以进入该共享内存,然后对这共享内存进行读取操作。这好比就是,小王和小明两个去酒店吃饭,他们都知道同一个房间号,同时在酒店的有一个房间也挂着这个房间号,那么他们就能够通过他们的信息找到该酒店的房间,然后在里面就餐。

所以说可以被进程知道,同时在共享内存的相关数据中,这个结构体肯定也存在这个key值。

struct  shm{

        .............

                key_t key;

        ................
}

shmid与key的关系

在编写代码的时候,我们看见两个进行不管有相同的key,还有相同的shmid。那么key与shmid有什么关系呢?

他们的关系就好像fd与inode一样,fd与key是给用使用的,而shmid与inode是被封装在内核数据结构中的,是不被用户使用的,但是能被用户看见。这个好比我们在学校用学号,在社会上用身份证号,当毕业后学校对我们的学号进行删除,不会影响,但身份证号被删除就会影响一生。这就好比key与shimd的关系。

当我们只运行下部分代码时:

    key_t k = getKey(); 
    printf("key: 0x%xn", k);
    int shmid = getShm(k);
    printf("shmid: %dn", shmid);

第一次结果:

[hongxin@VM-8-2-centos 2023-3-29]$ ./shm_server 
key: 0x660110c2
shmid: 1
[hongxin@VM-8-2-centos 2023-3-29]$ ./shm_client 
key: 0x660110c2
shmid: 1

但是第二次再运行时就会发生错误,这是什么原因呢?

[hongxin@VM-8-2-centos 2023-3-29]$ ./shm_server 
key: 0x660110c2
17:File exists 

那我们先观察一下,共享内存端ipcs -m(共享内存)/-q(消息队列)/-s(信号量)

[hongxin@VM-8-2-centos 2023-3-29]$ ipcs -m

------ Shared Memory Segments --------
key                shmid      owner      perms      bytes      nattch     status      
0x660106d4       0        hongxin        0              4096       0                       
0x660110c2      1          hongxin    600            4097       0    

通过观察我们发现,共享内存的生命周期是随操作系统的,而不是随进程。换句说,退出进程时共享内存还在存在,只有关机重启时才能结束共享内存。但是我们可以通过指令来关闭共享内存ipcsm -m shmid,或者通过调用函数接口。

[hongxin@VM-8-2-centos 2023-3-29]$ ipcrm -m 0
[hongxin@VM-8-2-centos 2023-3-29]$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status  

通过调用shmctl 函数接口删除共享内存 

void delShm(int shmid)
{
    if(shmctl(shmid,IPC_RMID,nullptr))
    {
        std::cerr <<"shmctl: "<< errno << ":" << strerror(errno) << std::endl;
    }
}

共享内存的特点

优点:所有进程间通信,速度最快的,因为减少了拷贝次数。在同样的代码下,考虑键盘输入和显示器输出共享内存与管道数据拷贝次数。

管道数据拷贝需要4+(2次)

 共享内存数据拷贝需要2+(2次)

缺点:共享内存不进行同步和互斥操作,没有对数据进行保护。

共享内存的属性信息

首先大家肯定知道创建共享内存不单单只是开辟一块内存存储数据,也开辟一块内存,存储它的属性信息。这个过程就是先描述,后组织。如下结构体就是储存共享内存的属性信息(一部分),这是内核级的数据,只能查看不能被修改。

struct shmid_ds {
               struct ipc_perm shm_perm;    /* Ownership and permissions */
               size_t          shm_segsz;   /* Size of segment (bytes) */
               time_t          shm_atime;   /* Last attach time */
               time_t          shm_dtime;   /* Last detach time */
               time_t          shm_ctime;   /* Last change time */
               pid_t           shm_cpid;    /* PID of creator */
               pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
               shmatt_t        shm_nattch;  /* No. of current attaches */
               ...
           };

仔细观察发现为什么没有key?key被封装在结构体struct ipc_perm shm_perm中。

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

system V消息队列 (了解) 

在消息队列中, 进程A可以读写,进程B也可以读写。他们通过type类型进行区分,例如:当type==0时,可以说明是A进程写入,此时A进程就不读取type==0的数据,相反就读取type==1的数据。B进程的type为1时,就只读取为0的数据即可。这样就可以实现进程间通信。

创建消息队列  msgget

       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgget(key_t key, int msgflg);

去掉消息队列 msgctl

       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgctl(int msqid, int cmd, struct msqid_ds *buf);

向消息队列发送消息   msgsnd

       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>

       int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

接收消息队列的消息 msgrcv

        ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值特性方面IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

system V信号量(了解)

Ⅰ信号量的理解

什么是信号量?

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

如何理解这句话?理解这句话的前提是理解:公共资源是什么?被保护的公共资源和为被保护的公共资源又是什么?如果没有保护公共资源会出现问题吗?保护起来的公共资源又叫什么?如何来保护公共资源呢?

公共资源:可以被多个进程同时访问的资源。(相信大家都能理解)

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

        因为在进程间通信时,进程都是独立,为了能让不同的进程访问同一资源,这时候就提出了共享资源这一方法,但是提出这一方法后,又出现了新的问题,就是数据不一致的,何为数据不一致:当进程A在读取时,进程B还在写入,当A进程读取时x=10,但进程B在写入时,将x=10修改成x=20;这个时候内存中的数据是x=20,但打印出的数据却是x10,此时就出现数据不一致。

将被保护公共资源叫作:临界资源,有大部分资源是独立的。

资源:内存,文件网络等。

该进程对对应代码访问这部分临界资源:临界区;相反,非临界区。

如何保护共享文件:互斥与同步

        互斥:两个或者多个进程不能同时访问一组共享变量的临界区域,否则可能发生与时间有关的错误,这种现象被称为进程互斥。也就是说,一个进程正在访问临界资源,那么另一个访问该资源的进程就必须等待。

原子性:在两态情况下,要么不做,做必须做完。

        举例: 在生活中,微信转账;比如给朋友转账100块,不可能说先转5块,然后再依次转到100块。这样是不现实,我们还害怕钱的丢失。鹅厂的处理就是,要么成功转账100块,要么出问题就不转账。   

Ⅱ为什么要有信号量?

在特定的场景应用下,比如买票。信号量就好比是票的数量。

实例:

        比如我们去电影院,肯定是需要买票的。买票的本质就是对放映厅中的座位进行预订机制。这个时候就可以把这个座位看做是共享资源(每个人都可以买票)。当我们想要获得这种资源时,我们就需要去预订它。预订它时,就需要控制好它。当票有100份时,我们用计数器count=100,当买一份票时,--count。如果count==0时,票卖完了。

如果当count==1时,代表是共享资源作为一个整体。

共享资源:

        1.作为一个整体使用(一台电脑,需要整体使用)

        2.划分成为一个一个的资源子部分(一箱书,需要一本一本去看)

这里其实就知道为什么需要有信号量了,因为在这里信号量对公共资源能起到保护作用。所有的进程在访问公共资源之前,都必须先申请访问信号量,所有进程必须先看到同一个信号,故此信号量本身就是公共资源。

信号量本身都是公共资源,所有信号量也应该保证自己的安全,如何保证,就可以在++或者--操作的时候保证其具有原子性!

获取信号量 semget

       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semget(key_t key, int nsems, int semflg);

删除信号量  semctl

       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/sem.h>

       int semctl(int semid, int semnum, int cmd, ...);

对信号量进行操作 semop

       int semop(int semid, struct sembuf *sops, unsigned nsops);

system v标准的进程间通信:共享内存,消息队列,信号量他们的接口的设计,结构的设置都遵守的同一标准,他们都是同一范畴下的。在内核数据结构中都是以下面形式构建的:

struct shmid_ds / smgid_ds / semid_ds

{

         struct ipc_perm shm_perm; / struct ipc_perm smg_perm;

        /  struct ipc_perm sem_perm;

        .........................

        ........................

}

         struct ipc_perm {
               key_t          __key;    /* Key supplied to shmget(2) */

                ....................

                ....................

        }

  ☺  [ 作者 ]   includeevey

 📃  [ 日期 ]   2023 / 4/ 1 
 📜  [ 声明 ]   到这里就该说再见了,若本文有错误和不准确之处,恳望读者批评指正!
                    有则改之无则加勉!若认为文章写的不错,一键三连加关注!

————————————————

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>