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


需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。 


 目录

一、通信的相关概念

二、管道(半双工)

1、管道的概念

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

1、匿名管道的使用

2、匿名管道的读写情况

3、管道的特征

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

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

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进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

【Linux】进程间通信(无名/有名管道及System V共享内存)_第1张图片

二、管道(半双工)

1、管道的概念

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第2张图片

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

        管道只能单向通信

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

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第3张图片

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

1、匿名管道的使用

#include 
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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
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是:"<

2、匿名管道的读写情况

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

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

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

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

3、管道的特征

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

        2、管道提供流式服务 ;

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

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

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

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

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
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<* 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* subs,vector& funcMap)
{
    vector deleteFd;//解决下一个子进程拷贝父进程读端的问题
    for(int i=0;i=0&&commandCodepush_back(sub);
        
        deleteFd.push_back(fds[1]);
    }
}
void sendTask(const sunEndPoint& process,int taskNum)
{
    cout<<"send task num"<"<& subs,const vector& funcMap,int count)
{
    int processnum =subs.size();//子进程的个数
    int tasknum=funcMap.size();
    bool forever=(count==0?true:false);
    while(true)
    {
        //选择一个子进程,从vector选择一个index
        int subIdx=rand()%processnum;
        //选择一个任务,从vector选择一个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 processes)
{
    int processnum=processes.size();
    for(int i=0;i funcMap;//vector<函数指针> funcMap
    loadTaskFunc(&funcMap);//加载任务
    vector subs;//子进程集合
    createSubProcess(&subs,funcMap);//维护父子通信信道
    //这里的程序是父进程,用于控制子进程
    int taskCnt=9;//让子进程做9个任务
    loadBlanceContrl(subs,funcMap,taskCnt);
    //回收子进程信息
    waitProcess(subs);
    return 0;
}

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

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

1.1命令行创建命名管道

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第4张图片

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

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

        创建命名管道:

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

        删除命名管道: 

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

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

1、comm.hpp

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#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:"<

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:"<

        服务端在以只读的方式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、共享内存(物理内存块+相关属性)的原理

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第5张图片

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

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

2、共享内存相关命令

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

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

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

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第6张图片

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

3.1形成key(ftok)

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第7张图片

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

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

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

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

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

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

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第8张图片

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

1、comm.hpp

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#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"<

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共享内存的优点

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

【Linux】进程间通信(无名/有名管道及System V共享内存)_第9张图片

5.2共享内存的缺点

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

5.3共享内存的特点

【Linux】进程间通信(无名/有名管道及System V共享内存)_第10张图片

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

5.4共享内存大小的建议

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

六、消息队列(了解)

【Linux】进程间通信(无名/有名管道及System V共享内存)_第11张图片

1、获取消息队列(msgget)

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

2、控制消息队列(msgctl)

#include 
#include 
#include 
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//msqid(类似fd),传入系统设定的宏,shmid_ds数据结构

3、其他略

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

1、信号量的本质

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

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

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

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

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

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

2、信号量的作用

【Linux】进程间通信(无名/有名管道及System V共享内存)_第12张图片

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

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

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

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

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

八、IPC资源的管理方式

【Linux】进程间通信(无名/有名管道及System V共享内存)_第13张图片

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

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

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

你可能感兴趣的:(Linux,网络,linux,进程,通信,c++)