【Linux的进程间通信】

进程通信

进程有独立性,但是进程间也可以有信息的交流,叫进程间通信,进程间通信可以通过匿名管道,命名管道,共享内存来进行通信。

进程间通信

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

匿名管道通信

  • 匿名管道只能对有血缘关系的父子进程间通信。因为是匿名,找不到进程的路径,所以要在父子关系的进程使用匿名管道。
  • 创建匿名管道使用系统调用pipe(),成功创建返回值为0,创建管道需要一个数组pipefd来接收写管道和读管道这两个文件描述符进行保存。
  • 匿名管道中,父进程如果是写管道,就要把读管道close关闭,然后子进程只能是读管道,把子进程的写管道进行关闭。反之一样。
  • pipefd[0]为读端,pipefd[1]为写端。

在这里插入图片描述

  • 读写管道正常,读进程要等待写进程写了才读,读管道要等待写管道。
  • 写端非常块,读端非常慢,写端写慢管道,读端就会等待,等待读端读完。
  • 如果写管道关闭了,读管道的read()系统调用接口读到返回值为0,用户可通过条件判断来退出或让做为读端的父进程执行其他的任务,或创建新的子进程执行其他的任务。
  • 如果读管道关闭,写管道无意义了,进程直接异常退出。信号码为13。
  • 匿名管道是内核的一段缓冲区,如果写端一直写,读端不取走数据,内核数据到达65536字节读端则阻塞。但是管道大小为521*8字节,有16个管道。
  • 所以一下代码我采用父进程作为读端。如果父进程作为写端,不管父进程有没有等待子进程,父进程退出的话,就会造成子进程成为孤儿进程,且进程是异常退出的。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <stdbool.h>
#include <string.h>
using namespace std;
int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);//管道创建,只能有一个读和一个写管道的文件
    if(n != 0)
    {
        cout << "创建管道失败" << endl;
        assert(n == 0);
    }
    else if(n == 0)
    {
        cout << "创建管道成功" << endl;
    }
    cout << "管道pipefd[0]: " << pipefd[0] << endl;
    cout << "管道pipefd[1]: " << pipefd[1] << endl;
    pid_t id = fork();
    if(id == 0)//子进程
    {
        close(pipefd[1]);
        char rbuff[1024];
        int cnt = 5;
        while(true)
        {
            ssize_t len = read(pipefd[0],rbuff,sizeof(rbuff) - 1);
            if(len > 0)
            {
                rbuff[len] = 0;
                cout << "I am child process my pid is: " << getpid() <<": " << rbuff << endl; 
            }
            if(len == 0)
            {
                close(pipefd[0]);
                cout << "写端停止写了,读端读到0就退出" << endl;
                break;
            }
            cnt--;
            if(cnt == 0)
            {
                close(pipefd[0]);
                cout << "关闭读端" << endl;
                break;
            }
            sleep(1);
        }
    };
    while(true)
    {
        close(pipefd[0]);
        char wbuff[1024];
        while(true)
        {
            snprintf(wbuff,sizeof(wbuff),"Hello,I am father process,my pid is %d",getpid());
            write(pipefd[1],wbuff,strlen(wbuff));
            sleep(1);
        }
    }
    cout << "#####################" << endl;
    int statu = 0;
    int ret = waitpid(id,&statu,0);
    if(ret == id)
    {
        cout << "等待成功........" << endl;
        printf("退出码: %d,退出信息:%s\n",(statu >> 8) & 0xFF,strerror((statu >> 8) & 0xFF));
        printf("退出信号:%d\n",statu & 0x7F);
    }
    return 0;
}
//父进程作为写端,读端以但关闭,进程异常退出,linux查看echo $?退出信号为13

命名管道

匿名管道只能是父子进程之间通信,命名管道可以是没有任何血缘关系的进程进行通信交流。命名管道会创建一个特殊的管道文件。由系统调用接口mififo(*pathname,mode)函数接口创建。*pathname为管道文件路径名称,mode表示文件打开权限。
在这里插入图片描述
创建管道的文件一般都是服务端的进程创建。客户端文件就不需要创建了。命名管道是基于先进先出的原理的。写端在队头数据一直写到队尾,读端在对头取数据一直到队尾。
在这里插入图片描述
当服务的程序执行的时候,会有一个fifo的文件,就是管道文件,如果服务端创建的管道还没有完成,打开客户端进程是无意义的。如果由了管道的话,先打开客户端进程是可以的,所以一般是先执行服务端进程再执行客户端。
在这里插入图片描述

进程可以通过共享内存来进行通信

匿名管道和命名管道是通过使用一个共同的文件进行通信,而共享内存使用的是同一个内存,不需要文件。不同的进程的虚拟地址空间指向同一个物理内存的区域。
在这里插入图片描述

如何指向同一块内存

  • 不同的进程有一个叫shmid_ds的结构体对象,描述着共享内存。有着共享内存的大小,进程挂接的时间和进程断掉挂接的时间等其他共享内存的属性。要创建共享内存,两个不同的进程要看到同一份的共享内存,就需要看到该共享内存的唯一的标识key_t。
  • 我们可以使用key_t ftok(const char *pathname, int proj_id)函数来创建一个唯一标识符,字符串pathname可以是任意字符串,但最好使用当前文件夹的路径作为字符串,proj_id也可以是任意数字,不过最好大一点。该函数的返回值其实是一个地址,我们可以使用该函数返回的值来继续创建共享内存。
  • 使用shmget来创建共享内存。int shmget(key_t key, size_t size, int shmflg);如果创建成功,返回创建共享内存的标识id,如果创建失败,返回-1,key值即ftok函数生成的地址变量,size即要使用多少字节的共享内存,shmflg是创建共享内存的模式,IPC_CREAT | IPC_EXCL | 0666,即没有就创建,然后以权限0666的方式使用共享内存,另一个进程的shmflg填0即可。这个时候共享内存就创建成功。IPC_CREAT共享内存不存在就创建,IPC_CREAT | IPC_EXCL 即不存在共享内存就创建,如果存在,就出错返回, IPC_EXCL 不单独使用,单独使用无意义。
  • 使用shmat把进程挂接到共享内存 有共享内存之后,两个进程要挂接到共享内存,使用系统调用shmat。void *shmat(int shmid, const void *shmaddr, int shmflg); shmid即创建的共享内存唯一识别id,shmaddr是指定地址挂接,但我们可以使用NULL自行找地址挂接,shmflg填0
  • 使用shmdt脱离挂接int shmdt(const void *shmaddr);shmaddr填NULL,脱离挂接不等于删除共享内存。
  • 使用shmctl删除共享内存共享内存需要用户自行删除,如果不删除,该共享内存的生命周期随内核的。int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmid即共享内存的id,cmd填IPC_RMID,buf填NULL即可删除共享内存。
  • Linux下可以使用指令ipcs -m查看共享内存信息,ipcrm -m id指令级别删除共享内存。ipcs -m查看指令。
  • 创建共享内存标识->创建共享的物理内存->通过标识将两个进程的虚拟地址映射到同一物理内存->脱离共享内存->删除共享内存。
    在这里插入图片描述
//共用的文件
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>

const char *pathname = "/home/USR/SHM";//是当前文件路径即可
const int proj_id = 0x11223344;
const int size = 4096;

using namespace std;

key_t Great_shm_key()
{
    key_t ret = ftok(pathname,proj_id);
    if(ret < 0)
    {
        cout << "创建key失败" << endl;
    }
    return ret;
}
//创建key值
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "commond.hpp"

using namespace std;

int main()
{
    key_t key = Great_shm_key();
    cout << "key: " << key << endl;
    int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL|0666);//服务端创建共享内存
    if(shmid < 0)
    {
        cout << "创建shmid失败" << endl;
        exit(0);
    }
    char* p = (char*)shmat(shmid,NULL,0);
    int cnt = 30;
    while(true)
    {
        cout << p << endl;
        sleep(1);
        cnt--;
        if(cnt == 0)
        {
            break;
        }
    }
    shmdt(NULL);
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
} 
//用户端
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "commond.hpp"

using namespace std;

int main()
{
    key_t key = Great_shm_key();
    cout << "key: " << key << endl;
    int shmid = shmget(key,size,0);
    if(shmid < 0)
    {
        cout << "创建shmid失败" << endl;
        exit(0);
    }
    char* p = (char*)shmat(shmid,NULL,0);
    char c = 'a';
    for(int i = 0;i < 26;i++)
    {
        cout << c << endl;
        p[i] = c++;
        sleep(1);
    }
    shmdt(NULL);
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

注意:上述代码一定先要服务端先启动,因为服务端创建共享内存。共享内存没有说哪个进程先要等待哪个进程输入信息才运行的,没有同步的机制,所以上述代码要有同步的功能的话,可以使用命名管道进行等待。

信号量

共享内存进行通信,如果多个进程对同一个共享内存进行修改,就会造成数据的冲突,进程A会覆盖进程B的数据,为了防止多个进程对同一共享资源的竞争,需要使用信号量这种保护机制,信号量就是一个整形的计数器,主要实现进程或线程之间的同步和互斥,而不是对通信的数据进行缓存的。
信号量表示的是资源的数量,有两种原子的操作,P和V。

  • P操作会对计数器的数量进行减一操作,如果减一操作的计数器信号<0,则表明资源已经使用完了,需要进行阻塞等待,如果>=0则表示还有资源可以使用,则进程还可以继续执行获取资源。
  • V操作会将该信号量的值加一,在V操作中,如果信号量<=0,说明P操作的进程被阻塞,V操作会进行加一并唤醒P操作的进程。如果信号量>0,则说明P操作的进程未被阻塞。V操作的进程可以不被阻塞。
  • P操作时使用共享资源之前的操作,V操作是离开共享资源之后的,需要PV一起使用。

具体过程在这里插入图片描述

  • 当初始化一个信号量为1的时候,进程A先执行,进行P操作进行减一操作,信号量>=0表示共享资源可用。
  • 此时进程B访问共享资源,进行P操作进行减一,信号量 <0表示资源不足不许访问,进程B阻塞。
  • 进程A结束任务进行V操作加一,发现为信号量为0,唤醒阻塞的B进程。
  • B进程被唤醒,任务也执行完毕,进行V操作,信号量重新为1了。

使用信号量进行生产消费

在这里插入图片描述

  • 此时A进程作为生产者,B进程作为消费者,AB进程相互合作与依赖,此时A为生产者,此时初始化信号量为0,B进程要进行P操作进行消费进行减一操作,信号量为-1了,B进程则阻塞。
  • 此时A进程才开始执行,进行V操作加一,信号量-1->0,会将阻塞进程B进行唤醒,B进程进行数据的获取消费。
  • 信号量初始化为0,可以保证V操作的进程会比P操作的进程先执行完毕。实现了进程的同步。

消息队列进行通信

消息队列是在消息的传输过程中保存消息的容器,可用于生产消费者模型,生产者将消息推送至消息队列,消费者可以在消息队列获取数据,也可以多生产者多消费者。这样的模型包含三个重要的元素:生产者,消息队列,作为进行数据的临时存储和发送的确认,消费者。
在这里插入图片描述

  • 使用消息队列通信,生产者和消费者可以进行异步的处理,生产者推送的数据到消息队列,但消费者不需要立刻进行处理,可以过了一段时间进行处理。即生产者可以一直生产数据,消费者不消费。或者消息队列有数据,生产者不生产,消费者一直消费,直到消息队列没有消息。

网络socket通信

网络socket套接字进行通信,可以让两个不同主机的不同进程进行通信,如TCP/IP协议的支持下通过底层物理的链接进行进程的通信,比如说两个微信用户使用微信进行文件的传输。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值