【Linux】进程间通信(无名/有名管道及System V共享内存)


目录

一、通信的相关概念

二、管道(半双工)

1、管道的概念

三、匿名管道(fork实现,用于父子及血缘关系进程间通信)

1、匿名管道的使用

2、匿名管道的读写情况

3、管道的特征

4、基于匿名管道的进程池

四、命名管道(open打开相同文件实现,可用于非血缘关系的进程间通信)

扫描二维码关注公众号,回复: 14603359 查看本文章

1、命名管道的创建及删除

1.1命令行创建命名管道

1.2程序内创建及删除命名管道

1.3基于命名管道的用户端发送,服务端接收

五、System V共享内存

1、共享内存(物理内存块+相关属性)的原理

2、共享内存相关命令

2.1查看共享内存(ipcs -m/-q/-s)

2.2删除共享内存(ipcrm -m shmnid)

3、创建/查看/删除/控制(删除)/关联共享内存

3.1形成key(ftok)

3.2创建共享内存(shmget)

3.3关联/卸载共享内存(shmat/shmdt)(关联类似malloc)

3.4控制(主要用移除)共享内存(shmctl)

4、利用共享内存进行进程间通信

5、共享内存的优缺点

5.1共享内存的优点

5.2共享内存的缺点

5.3共享内存的特点

5.4共享内存大小的建议

六、消息队列(了解)

1、获取消息队列(msgget)

2、控制消息队列(msgctl)

3、其他略

七、信号量(计数器)(了解)

1、信号量的本质

2、信号量的作用

八、IPC资源的管理方式


一、通信的相关概念

进程之间具有独立性,进程间如果要发生通信,就需要打破这种独立性。进程间通信必定需要一块公共的区域用来作为信息的存放点,操作系统需要直接的或间接给通信进程双方提供内存空间,例如这块内存空间是文件系统提供的,那么就是管道通信,通信的本质就是让不同的进程看到同一份内存空间。

进程间通信是为了完成:

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

2、资源共享:多个进程之间共享相同的资源;

3、事件通知:一个进程需要向另一个或另一组进程发送消息,通知他们发送了某种事件(例如子进程终止时要通知父进程)

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

二、管道(半双工)

1、管道的概念

管道是基于文件系统的通信方式,那么就从底层的角度看一下管道通信的原理:

管道文件是内存级文件,不用访问磁盘进行文件加载,操作系统直接创建结构体对象及内核缓冲区。如上图例子,管道文件不必使用open进行打开,操作系统会创建文件结构体对象及其内核缓冲区,并将其放入父进程的文件描述符表中,父进程创建子进程后,父子进程便能基于管道这个内存级文件进行通信。

管道只能单向通信

三、匿名管道(fork实现,用于父子及血缘关系进程间通信)

通过上方管道的概念可知,通过父进程fork创建子进程,让子进程拷贝父进程中管道文件的地址,两个进程便能看到同一个管道文件,这个管道文件是一个内存级文件,并没有名字,所以被称为匿名管道。

所以对待管道和对待文件一样,体现Linux一切皆文件的思想。

1、匿名管道的使用

#include <unistd.h>
int pipe(int pipefd[2]);//pipefd[2]是输出型参数
On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
成功时返回零,错误时返回 -1,并适当地设置 errno。

pipefd[2]是输出型参数,外边搞个pipefd[2]数组传进去,系统调用pipe结束后这个数组中存放的就是读/写的fd。

子进程写入数据到管道,父进程读取管道数据代码。

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
//父进程读取,子进程写入
int main()
{
    //第一步父进程创建管道
    int fds[2];
    int n=pipe(fds);
    assert(n==0);
    //第二步父进程fork()创建子进程
    pid_t id =fork();//fork之后,父进程返回值>0,子进程返回值==0
    assert(id>=0);
    const char* s="我是子进程,我的pid是:";
    int cnt=0;
    if(id==0)
    {
        close(fds[0]);//子进程关闭读取的fd
        //子进程的通信代码
        while(true)
        {
            char buffer[1000];//这个缓冲区只有子进程能看到
            snprintf(buffer,sizeof(buffer),"子进程第%d次向父进程发送:%s%d",++cnt,s,getpid());//向缓冲区buffer中打印
            write(fds[1],buffer,strlen(buffer));//子进程将缓冲区数据写入管道
            sleep(1);//每隔1秒写一次
            //break;
        }
        close(fds[1]);//如果break跳出循环,子进程将关闭写端
        exit(0);
    }
    close(fds[1]);//父进程关闭写入 
    //父进程的通信代码
    while(true)
    {
        char buffer[1000];//这个缓冲区只有父进程能看到
        //如果管道中没有数据,读取端再读,默认会阻塞当前读取的进程
        ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);
        if(s>0)//s是read读取成功字节数
        {
            buffer[s]='\0';
            cout << "父进程的pid是:"<<getpid()<<" "<<buffer<<endl;
        }  
        else if(s==0)//如果子进程关闭写端,父进程将会输出“读完了”
        {
            //读到文件结尾
            cout<<"读完了"<<endl;
            break;
        }  
    }
    n=waitpid(id,nullptr,0);
    assert(n==id);
    close(fds[0]);//父进程读取fd用完记得关一下
    return 0;
}

2、匿名管道的读写情况

1、如果管道中没有数据,读取端进程再进行读取,会阻塞当前正在读取的进程;

2、如果写入端写满了,再写就会对该进程进行阻塞,需要等待对方对管道内数据进行读取;

3、如果写入进程关闭了写入fd,读取端将管道内的数据读完后read的返回值为0,和谐退出;

4、如果读关闭,操作系统会给写端发送13号信号SIGPIPE,终止写端。

3、管道的特征

1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道;

2、管道提供流式服务 ;

3、进程退出,管道释放,所以管道的生命周期随进程

4、内核会对管道操作进行同步与互斥

5、管道是半双工。需要双方通信时,需要建立起两个管道

4、基于匿名管道的进程池

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
#define PROCESS_NUM 5
#define Make_Seed() srand((unsigned long)time(nullptr)^getpid()^0X55^rand()%1234)
typedef void(*func_t)();//函数指针类型
//子进程需要完成的任务
void downLodeTask()
{
    cout<<getpid()<<"下载任务"<<endl;
    sleep(1);
}
void ioTask()
{
    cout<<getpid()<<"IO任务"<<endl;
    sleep(1);
}
void flushTask()
{
    cout<<getpid()<<"刷新任务"<<endl;
    sleep(1);
}
//多进程代码
class sunEndPoint
{
public:
    sunEndPoint(pid_t subId,int writeFd)
    :_subId(subId)
    ,_writeFd(writeFd)
    {
        char namebuffer[1000];
        snprintf(namebuffer,sizeof(namebuffer),"process-%d[pid(%d)-fd(%d)]",num++,_subId,_writeFd);
        _name=namebuffer;
    }
public:
    string _name;
    pid_t _subId;//pid
    int _writeFd;//写fd
    static int num;
};
int sunEndPoint::num=0;
void loadTaskFunc(vector<func_t>* out)
{
    assert(out);
    out->push_back(downLodeTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}
int recvTask(int readFd)
{
    int code=0;
    ssize_t s=read(readFd,&code,sizeof(code));
    if(s==sizeof(code))//合法信息
    {
        return code;
    }
    else if(s<=0)
    {
        return -1;
    }
    else 
        return 0;
}
void createSubProcess(vector<sunEndPoint>* subs,vector<func_t>& funcMap)
{
    vector<int> deleteFd;//解决下一个子进程拷贝父进程读端的问题
    for(int i=0;i<PROCESS_NUM;++i)
    {
        int fds[2];
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)//子进程
        {
            //关闭上一个文件的写端文件描述符
            for(int i=0;i<deleteFd.size();++i)
            {
                close(deleteFd[i]);
            }
            //子进程,处理任务
            close(fds[1]);
            while(true)
            {
                //1、获取命令码,如果父进程没有发送,子进程被阻塞
                int commandCode=recvTask(fds[0]);
                //2、完成任务
                if(commandCode>=0&&commandCode<funcMap.size())
                {
                    funcMap[commandCode]();
                }
                else if(commandCode==-1)
                {
                    break;
                }
            }
           exit(0); 
        }
        close(fds[0]);
        sunEndPoint sub(id,fds[1]);
        subs->push_back(sub);
        
        deleteFd.push_back(fds[1]);
    }
}
void sendTask(const sunEndPoint& process,int taskNum)
{
    cout<<"send task num"<<taskNum<<"send to->"<<process._name<<endl;
    int n=write(process._writeFd,&taskNum,sizeof(taskNum));
    assert(n==sizeof(int));//判断是否成功写入4个字节
    (void)n;
}
void loadBlanceContrl(const vector<sunEndPoint>& subs,const vector<func_t>& funcMap,int count)
{
    int processnum =subs.size();//子进程的个数
    int tasknum=funcMap.size();
    bool forever=(count==0?true:false);
    while(true)
    {
        //选择一个子进程,从vector<sunEndPoint>选择一个index
        int subIdx=rand()%processnum;
        //选择一个任务,从vector<func_t>选择一个index
        int taskIdx=rand()%tasknum;
        //将任务发送给指定的子进程,将一个任务的下标发送给子进程
        sendTask(subs[subIdx],taskIdx);//taskIdx作为管道的大小4个字节
        sleep(1);
        if(!forever)//forever不为0
        {
            --count;
            if(count==0)
                break;
        }
    }
    //写端退出,读端将管道内数据读完后read返回0
    for(int i=0;i<processnum;++i)
    {
        close(subs[i]._writeFd);//最晚被创建的子进程拥有早期创建的子进程的父进程的读端,所以这里其实是后创建的进程先关闭
    }
}
//回收子进程
void waitProcess(vector<sunEndPoint> processes)
{
    int processnum=processes.size();
    for(int i=0;i<processnum;++i)
    {
        waitpid(processes[i]._subId,nullptr,0);
        cout<<"wait sub process success"<<processes[i]._subId<<endl;
    }
}
//父进程给子进程发布命令,父进程写,子进程读
int main()
{
    Make_Seed();//创建随机数
    //父进程创建子进程及和子进程通信的管道
    vector<func_t> funcMap;//vector<函数指针> funcMap
    loadTaskFunc(&funcMap);//加载任务
    vector<sunEndPoint> subs;//子进程集合
    createSubProcess(&subs,funcMap);//维护父子通信信道
    //这里的程序是父进程,用于控制子进程
    int taskCnt=9;//让子进程做9个任务
    loadBlanceContrl(subs,funcMap,taskCnt);
    //回收子进程信息
    waitProcess(subs);
    return 0;
}

四、命名管道(open打开相同文件实现,可用于非血缘关系的进程间通信)

1、命名管道的创建及删除

1.1命令行创建命名管道

mkfifo filename//filename是以p开头的管道文件

命名管道可用于无血缘关系的进程间通信,两个进程均打开同一路径下的同名文件(看到同一份资源),便能进行通信。命名管道大小为零的原因是数据并不会被刷新到磁盘中,它也是一个内存级文件。

1.2程序内创建及删除命名管道

创建命名管道:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);//路径,权限码
成功返回0,失败返回-1(失败错误码errno被设置)

删除命名管道: 

#include <unistd.h>
int unlink(const char *path);//路径
成功返回0,失败返回-1(失败错误码errno被设置)

1.3基于命名管道的用户端发送,服务端接收

1、comm.hpp

#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define NAMED_PIPE "/tmp/mypipe"
//创建管道
bool createFifo(const std::string& path)
{
    umask(0);
    int n=mkfifo(path.c_str(),0600);//创建管道文件
    if(n==0)
        return true;
    else
    {
        std::cout<<"error:"<<errno<<"error string"<<strerror(errno)<<std::endl;
        return false;
    }
}
//销毁管道
void destroyFifo(const std::string& path)
{
    int n=unlink(NAMED_PIPE);
    assert(n==0);
    (void)n;
}

2、server.cc(先运行服务端)

#include "comm.hpp"
int main()
{
    bool r=createFifo(NAMED_PIPE);
    assert(r);
    (void)r;
    std::cout << "server begin" << std::endl;
    int rfd =open(NAMED_PIPE,O_RDONLY);//会等用户端打开管道文件,服务端才进行open服务端以只读的方式打开
    if(rfd==-1)//打开失败
        exit(1);
    //读取
    char buffer[1000];
    while(true)
    {
        ssize_t s=read(rfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;//将最后一个字符设置为'\0'
            std::cout<<"client->server:"<<buffer;
        }
        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);
    destroyFifo(NAMED_PIPE);
    return 0;
}

服务端在以只读的方式open管道文件前,会被阻塞,等待用户端open写入管道文件。(读写端均需要被打开)

3、client.cc

#include "comm.hpp"
int main()
{
    int wfd =open(NAMED_PIPE,O_WRONLY);//用户端以写的方式打开
    if(wfd==-1)//打开失败
        exit(1);
    char buffer[1000];
    while(true)
    {
        std::cout<<"you can say:";
        fgets(buffer,sizeof(buffer),stdin);//fgets剩一个空间会被系统填充'\0',不用-1
        ssize_t s=write(wfd,buffer,strlen(buffer));
        assert(s==strlen(buffer));
        (void)s;
    }
    close(wfd);
    return 0;
}

五、System V共享内存

1、共享内存(物理内存块+相关属性)的原理

需要让不同进程看到同一块内存资源。用户使用操作系统提供的接口在物理内存中申请一块资源,通过进程的页表将这段物理空间映射至进程地址空间,进程将这段虚拟地址的起始地址返回给用户。通信结束后记得取消物理内存和虚拟内存的映射关系(去关联),并释放共享内存。

1、共享内存和malloc有点像,区别在于malloc出来的内存只能本进程知道这块空间的地址,共享内存是通过开辟一块物理空间,分别映射至通信进程的虚拟地址空间中。

2、共享内存是一种通信方式,所有想通信的进程都可以用,所以操作系统中可能会同时存在很多个共享内存。

2、共享内存相关命令

2.1查看共享内存(ipcs -m/-q/-s)

ipcs -m/-q/-s     //共享内存/消息队列/信号量数组

2.2删除共享内存(ipcrm -m shmnid)

ipcrm -m shmnid//使用shmid删除共享内存

3、创建/查看/删除/控制(删除)/关联共享内存

3.1形成key(ftok)

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);//路径/项目id
成功时,将返回key。失败返回-1(失败错误码errno被设置为系统调用出错)

通过传入相同的pathname和proj_id得到相同的key,从而找到同一块共享内存,实现进程间通信。

key通过shmget,设置进共享内存的属性中用来标识该共享内存在内核中的唯一性。key可以理解为一个个房间(内存块)的门牌号(编号)。

shmget的返回值:key类似fd:inode的关系。

3.2创建共享内存(shmget)

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);//标定唯一性(确认是哪块共享内存)/申请多大的内存空间/二进制标志位,见下图
成功时,将返回一个有效的共享内存标识符。(不同操作系统的数字下标不同,和文件的数字下标不兼容)
失败返回-1(失败错误码errno被设置)

3.3关联/卸载共享内存(shmat/shmdt)(关联类似malloc)

1、将共享内存与虚拟内存进行关联

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);//共享内存id,被映射的进程地址空间(给nullptr),给0默认可以读写
成功时,将返回共享内存的虚拟地址。失败返回-1(失败错误码errno被设置) 

2、将共享内存与虚拟内存去关联

#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);//参数:shmat的返回值
成功时,将返回0。失败返回-1(失败错误码errno被设置)

3.4控制(主要用移除)共享内存(shmctl)

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);//shmid(类似fd),传入系统设定的宏,shmid_ds数据结构
传入IPC_RMID移除共享内存成功时,将返回0。失败返回-1(失败错误码errno被设置)

4、利用共享内存进行进程间通信

1、comm.hpp

#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>


#define PATHNAME "."//当前路径(路径都行)
#define PROJ_ID 0X55//项目id也无要求
#define MAX_SIZE 4096
key_t getKey()
{
    key_t k=ftok(PATHNAME, PROJ_ID);
    if(k==-1)
    {
        std::cout<<"ftok"<<errno<<":"<<strerror(errno)<<std::endl;
        exit(1);
    }
    return k;
}
int getShmHelper(key_t key,int flags)
{
    int shmid=shmget(key,MAX_SIZE,flags);
    if(shmid==-1)//创建共享内存失败
    {
        std::cerr<<"shmget"<<errno<<":"<<strerror(errno)<<std::endl;
        exit(2);
    }
    return shmid;//返回共享内存标识符
}
int getShm(key_t key)//创建||获取共享内存
{
    return getShmHelper(key,IPC_CREAT);//传0也行
}
int createShm(key_t key)//必定创建共享内存
{
    return getShmHelper(key,IPC_CREAT|IPC_EXCL|0600);//生成一个全新的共享内存
}
void* attachShm(int shmid)//让共享内存与虚拟内存建立联系
{
    void* memstart=shmat(shmid,nullptr,0);
    if((long long)memstart==-1L)
    {
        std::cerr<<"shmat"<<errno<<":"<<strerror<<std::endl;
        exit(3);
    }
    return memstart;
} 
void detchShm(void* memStart)//去关联
{
    if(shmdt(memStart)==-1)
    {
        std::cerr<<"shmdt"<<errno<<":"<<strerror<<std::endl;
        exit(4);
    }
}   
void delShm(int shmid)//删除共享内存
{
    if(shmctl(shmid,IPC_RMID,nullptr)==-1)
    {
        std::cerr<<"shmctl"<<errno<<":"<<strerror<<std::endl;
    }
}

2、shm_server.cc

#include "comm.hpp"
int main()
{
    key_t k=getKey();
    printf("0X%x\n",k);
    int shmid=createShm(k);

    char* memStart=(char*)attachShm(shmid);//让共享内存与虚拟内存建立联系
    printf("memStart address:%p\n",memStart); 
 
    //通信接收代码
    while(true)
    {
        printf("client say:%s\n",memStart); 
        sleep(1);

        //调用用户级结构体
        struct shmid_ds ds;//创建结构体对象ds
        shmctl(shmid,IPC_STAT,&ds);//获取ds对象的状态
        printf("获取属性:%d,pid:%d,myself:%d,key:%d\n",ds.shm_segsz,getpid(),ds.shm_cpid,ds.shm_perm.__key);
    }


    detchShm(memStart);//去关联
    sleep(10);

    delShm(shmid);//删除共享内存,client和server都能删除共享内存,尽量谁创建谁删
    return 0;
}

2、shm_client.cc

#include "comm.hpp"
int main()
{
    key_t k=getKey();
    printf("0X%x\n",k);
    int shmid=getShm(k);//获取共享内存
    sleep(5);

    char* memStart=(char*)attachShm(shmid);//让共享内存与虚拟内存建立联系
    printf("memStart address:%p\n",memStart); 

    //通信传输代码
    const char* massage="I am client";
    pid_t id=getpid();
    int cnt=0;//发送计数
    while(true)
    {
        snprintf(memStart,MAX_SIZE,"%s[%d]:%d\n",massage,getpid,++cnt);
        sleep(1);
    }

    detchShm(memStart);//去关联
    return 0;
}

5、共享内存的优缺点

5.1共享内存的优点

共享内存是所有进程间通信中速度最快的。(无需缓冲区,能大大减少通信数据的拷贝次数)

5.2共享内存的缺点

如果服务端读取速度较快,用户端发送数据较慢,就会产生同一段消息被服务端读取多遍。共享内存是不进行同步和互斥的,没有对数据进行任何保护。

5.3共享内存的特点

上图表明共享内存的生命周期是随操作系统的,进程的退出不会销毁共享内存。这是System V资源的特征。

5.4共享内存大小的建议

因为系统分配共享内存是以4KB为基本单位,一般建议申请共享内存的大小为4KB的整数倍。

六、消息队列(了解)

1、获取消息队列(msgget)

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);//key:“门牌号”,msgflg:宏(IPC_CREAT and IPC_EXCL)
如果成功,返回值将是消息队列标识符(非负整数) ,否则为 -1。用 errno 指示错误。

2、控制消息队列(msgctl)

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//msqid(类似fd),传入系统设定的宏,shmid_ds数据结构

3、其他略

七、信号量(计数器)(了解)

1、信号量的本质

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

临界资源:未来将被保护的资源;

临界区:进程有对应的代码来访问对应的临界资源;

非临界区:进程中不访问临界资源的代码;

互斥和同步保护公共资源。(互斥:同一时间只有一个进程能进行资源访问)

原子性:要么不做,要么就做完。只有两态。

2、信号量的作用

多个资源块通过程序员的编码实现互斥,防止同一资源块同时被访问。

信号量就是通过计数器对链接资源进行保护的一种方式。(需要让进程间看到同一个信号量资源)

信号量通过计数器sem--(预定资源,P操作)/sem++(释放资源,V操作)

信号量本身也是一个临界资源,它能保护其他共享资源的同时,也需要保护自己的安全,信号量内部的加加减减具有原子性。

二元信号量:信号量为1,说明共享资源时一整个整体,提供互斥功能。

八、IPC资源的管理方式

三种ipc资源数据结构的首地址元素相同,用一个struct ipc_perm* perms[]指针数组进行管理。

访问时只需(struct shmid_ds*)perms[0]->共享内存的属性。

这是C语言模拟实现的一种多态行为。

猜你喜欢

转载自blog.csdn.net/gfdxx/article/details/129210780