Linux---进程间通信

进程间通信(IPC)介绍


进程间通信的本质就是让两个毫不相关的进程看到一份共同的资源,大概意思就是实现不同进程间的传播或交换信息

进程间通信的主要方式有管道,消息队列,共享内存,信号量,Socket,Streams等,这篇博客主要详细讲解前四种通信方式。(因为后面两个还没学,嘿嘿)

一、管道

管道是UNIX中最古老的进程间通信方式
我们把从一个进程连接到另一个进程的一个数据流称为“管道”
管道又分为匿名管道和命名管道两种

匿名管道

1、特点

  • 管道是半双工的,只支持单向通信,如果需要全双工通信,就需要建立起两个管道
  • 管道只支持有血缘关系的进程间通信,如父子进程和兄弟进程
  • 管道的生命周期随进程,进程终止管道就会被释放
  • 管道提供面向字节流的通信

2、原型

#include <unistd.h>
int pipe(int fd[2]);
管道创建成功后返回两个文件描述符,用fd[2]数组接收,fd[0]为读,fd[1]为写
返回值:成功返回0,失败返回错误代码

3、图解

这里写图片描述

父进程刚创建出子进程时,调用pipe函数返回的文件描述符都是打开的,父子进程通信时就要关闭对应的描述符,如父进程从管道读数据,用的是fd[0],就要关掉它的fd[1],子进程则相反

这里写图片描述

代码演示

    int main()
    {        
        pid_t pid;
        int file[2];//用于接收文件描述符
        pipe(file);//创建管道
        char buf[256] = {0};
        pid = fork();
        if(pid < 0)
        {    
            perror("fork");
        }else if(pid == 0)
        {    
            //子进程进行写操作,关闭文件描述符fd[0]
            close(file[0]);
            write(file[1],"hello father",strlen("hello father"));
        }else
        {    
            //父进程进行读操作,关闭文件描述符fd[1]                                                                                 
            close(file[1]);
            read(file[0],buf,sizeof(buf));
        }
        printf("%s\n",buf);
    }

因为在运行a.out文件时是敲了回车的,所以会自动换一次行
这里写图片描述

命名管道

也成为FIFO,是一种文件类型

1、特点

  • 支持任意两个进程间的通信
  • FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中

2、与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建并打开
  • 唯一的区别就是打开和创建的方式不同,一但这些工作完成之后,它们具有相同的语义

3、原型

#include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode);
成功返回0,出错返回-1

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

扫描二维码关注公众号,回复: 1053918 查看本文章
  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。

  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

4、例子:用命名管道实现文件拷贝

writer.c

  int main()
  {
      //创建命名管道,权限是644
      mkfifo("pipe",0644);
      //test 文件已在当前目录下创建,以只读方式打开
      int input = open("test",O_RDONLY);
      if(input == -1)
      {
          perror("open");
      }
      //以只写方式打开命名管道
      int output = open("pipe",O_WRONLY);
      if(output == -1)
      {
          perror("open");
      }
      char out[1024];
      int n;
      while((n = read(input,out,sizeof(out))) > 0)
      {
          //向管道写数据
          write(output,out,n);                                    
      }
      close(input);
      close(output);
      return 0;
  }

reader.c

  int main()
  {
      int output;
      //创建一个文件,并以只写方式打开它
      //目的是将test文件中的内容拷贝到test.bak中
      output = open("test.bak",O_WRONLY|O_CREAT|O_TRUNC,0644);
      if(output == -1)
      {
          perror("open");
      }
      int input;
      input = open("pipe",O_RDONLY);
      if(input == -1)
      {
          perror("open");
      }
      char buf[1024];
      int n;
      while((n = read(input,buf,sizeof(buf))) > 0)
      {
          write(output,buf,n);
      }
      close(input);
      close(output);
      unlink("pipe");
      return 0;
  }

test文件内容
这里写图片描述

test.bak 文件内容显示拷贝成功
这里写图片描述


二、消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

1、特点

  • 消息队列提供了一个从一个进程向另外一个进程发送一个数据块(有类型数据块)的方法
  • 消息队里的生命周期随内核,进程终止后消息队列也不会销毁

       ipcs -q                  查看当前的消息队列
       ipcrm -q + 消息队列ID    销毁消息队列,也可以通过在函数内调用msgctl函数清除消息队列
    
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
  • 消息队列也有管道一样的不足,即每个消息队列的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数是由上线的(MSGMNB),系统上消息队列的总数也有一个上限(MSGMNI)

2、原型

       #include <sys/types.h>
       #include <sys/ipc.h>
       #include <sys/msg.h>
       #include <sys/msg.h>
       // 创建或打开消息队列:成功返回队列ID,失败返回-1
       int msgget(key_t key, int flag);
       // key:消息队列的名字,下面的例子中将具体体现使用方法
       // 添加消息:成功返回0,失败返回-1
       int msgsnd(int msqid, const void *ptr, size_t size, int flag);
       // msqid:消息队列的标识符,即队列ID
       // 读取消息:成功返回消息数据的长度,失败返回-1
       int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
       // 控制消息队列:成功返回0,失败返回-1
       int msgctl(int msqid, int cmd, struct msqid_ds *buf);

3、例子:用两个终端实现两个前台进程的通信

common.h

    #pragma once

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <string.h>
    #include <unistd.h>

    #define PATHNAME "."
    #define PROJ_ID 0X6667

    #define SERVER_TYPE 1
    #define CLIENT_TYPE 2

    struct msgbuf{
        long mtype;
        char mtext[1024];
    };


    int CreateMsgQueue();

    int GetMsgQueue();

    int DestroyMsgQueue(int msgid);

    int SendMsg(int msgid, int who, char *msg);

    int RecvMsg(int msgid, int recvType, char out[]);

common.c

#include "common.h"                                                                                                                     
int CommonMsgQueue(int flags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);//ftok函数可以生成key
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int msgid = msgget(key,flags);
    if(msgid < 0)
    {
        perror("msgget");
        return -1;
    }
    return msgid;
}
int CreateMsgQueue()//创建一个消息队列
{
    //flag由9个权限标志构成
    //IPC_CREAT|IPC_EXCL一起用表示如果没有该消息队列,则创建一个新的,并返回队列ID
    return CommonMsgQueue(IPC_CREAT|IPC_EXCL|0666);
}
int GetMsgQueue()//获得一个消息队列
{
    //返回已创建的消息队列ID
    return CommonMsgQueue(IPC_CREAT);
}
int DestroyMsgQueue(int msgid)//销毁一个消息队列
{
    if (msgctl(msgid,IPC_RMID,NULL) < 0 )
    {
        perror("msgctl");
        return -1;
    }
    return 0;
}
int SendMsg(int msgid, int who, char *msg)//往消息队列中发送数据
{
    struct msgbuf buf;
    buf.mtype = who;
    strcpy(buf.mtext,msg);

    if(msgsnd(msgid,(void*)&buf,sizeof(buf.mtext),0) < 0)
    {
        perror("msgsnd");
        return -1;
    }
    return 0;
}
int RecvMsg(int msgid, int recvType, char out[])//从消息队列中读取数据
{
    struct msgbuf buf;
    if((msgrcv(msgid,(void *)&buf,sizeof(buf.mtext),recvType,0)) < 0)
    {
        perror("msgrcv");
        return -1;
    }
    strcpy(out,buf.mtext);
    return 0;
}      

server.c

#include "common.h"

int main()
{
    int msgid = CreateMsgQueue();

    char buf[1024];
    while(1)                                                                                                                            
    {   
        buf[0] = 0;
        RecvMsg(msgid,CLIENT_TYPE,buf);
        printf("Client say# %s \n",buf);

        printf("Please Enter# ");
        fflush(stdout);//刷新缓冲区
        ssize_t s = read(0,buf,sizeof(buf));//从标准输出读取数据到buf中
        if(s > 0)
        {   
            buf[s-1] = 0;
            SendMsg(msgid,SERVER_TYPE,buf);
            printf("send done,wait for client:..\n");
        }   
        if(strcmp(buf,"exit") == 0)
        {   
            break;
        }   
    }   
    DestroyMsgQueue(msgid);
    return 0;
}

client.c

#include "common.h"                                                                                                                     

int main()
{
    int msgid = GetMsgQueue();

    char buf[1024];
    while(1)
    {   
        buf[0] = 0;
        printf("Please Enter# ");
        fflush(stdout);//刷新缓冲区
        ssize_t s = read(0,buf,sizeof(buf));//从标准输出读取数据到buf中
        if(s > 0)
        {   
            buf[s-1] = 0;
            SendMsg(msgid,CLIENT_TYPE,buf);
            printf("send done, wait recv...\n");
        }   
        RecvMsg(msgid,SERVER_TYPE,buf);
        printf("Server say# %s \n",buf);
        if(strcmp(buf,"exit") == 0)
        {   
            return 0;
        }   
    }   
    return 0;
}

这里写图片描述


三、共享内存

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

1、特点

  • 共享内存是最快的IPC形式,因为进程是直接对内存进行读取的
  • 共享内存的生命周期随内核

    ipcs  -m                         查看共享内存
    ipcrm -m + 共享内存ID             手动销毁共享内存或者通过调用shmctl函数来销毁
    
  • 信号量和共享内存通常放在一起使用,信号量用来同步对共享内存的访问

2、图解

这里写图片描述

3、原型

 #include <sys/shm.h>
  // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
 int shmget(key_t key, size_t size, int flag);
 // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
 void *shmat(int shm_id, const void *addr, int flag);
 // 断开与共享内存的连接:成功返回0,失败返回-1
 int shmdt(void *addr); 
 // 控制共享内存的相关信息:成功返回0,失败返回-1
 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。

当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。

shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。

shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。

4、例子:一个进程往共享内存中写数据,一个进程往出读数据

commo.h

#pragma once                                                      

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>

#define PATHNAME "."
#define PROJ_ID 0X6666

int CreateShm(int size);

int DestroyShm(int shmid);

int GetShm(int size);

common.c

#include "common.h"
//与创建消息队列很类似,就不做介绍了                                                                                                    
int CommonShm(int size, int flags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int shmid = 0;
    if((shmid = shmget(key,size,flags)) < 0)
    {
        perror("shmget");
        return -2;
    }
    return shmid;
}
int CreateShm(int size)
{
    return CommonShm(size,IPC_CREAT|IPC_EXCL|0666);
}
int DestroyShm(int shmid)
{   
    if(shmctl(shmid,IPC_RMID,NULL) < 0)
    {   
        return -1;
    }
    return 0;
}
int GetShm(int size)
{   
    return CommonShm(size,IPC_CREAT);
}

server.c

  #include "common.h"

  int main()
  {
      int shmid = CreateShm(4096);
      char *addr = shmat(shmid, NULL, 0);
      sleep(2);
      int i = 0;                                                                                                                        
      while(i++<26)
      {
          printf("client# %s\n",addr);
          sleep(1);
      }
      //将共享内存段与当前进程脱离
      shmdt(addr);
      sleep(2);
      DestroyShm(shmid);
      return 0;
  }    

client.c

  #include "common.h"

  int main()
  {
      int shmid = GetShm(4096);
      sleep(1);
      char *addr =shmat(shmid, NULL, 0);
      sleep(2);
      int i = 0;
      while(i<26)
      {   
          //每隔1秒写一个字母进去                                                                                                       
          addr[i] = 'A'+i;
          i++;
          addr[i] = 0;
          sleep(1);
      }   

      shmdt(addr);
      sleep(2);
      DestroyShm(shmid);
      return 0;
  }

这里写图片描述
client只负责写数据,所以没有输出结果
这里写图片描述


信号量

信号量主要作用于同步和互斥,而不是存储进程间通信数据

1、特点

  • 信号量本质上是一个计数器,记录了临界资源的数目

    临界资源:系统中有些资源一次只允许一个进程访问,被称为临界资源,也可被当做互斥资源
    临界区: 在进程中涉及到互斥资源的程序段叫临界区
    
  • 信号量基于操作系统的P,V操作,且P,V操作是原子性的
  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

2、原型

简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

 #include <sys/sem.h>
 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
 int semget(key_t key, int num_sems, int sem_flags);
 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
 int semop(int semid, struct sembuf semoparray[], size_t numops);  
 // 控制信号量的相关信息
 int semctl(int semid, int sem_num, int cmd, ...);

当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。

在semop函数中,sembuf结构的定义如下:

 struct sembuf 
 {
     short sem_num; // 信号量组中对应的序号,0~sem_nums-1
     short sem_op;  // 信号量值在一次操作中的改变量
     short sem_flg; // IPC_NOWAIT, SEM_UNDO
 }

其中 sem_op 是一次操作中的信号量的改变量:

  • sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。
  • sem_op < 0,请求 sem_op 的绝对值的资源。

    • 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。

    • 当相应的资源数不能满足请求时,这个操作与sem_flg有关。

      • sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN。
      • sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        • 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
        • 此信号量被删除,函数smeop出错返回EIDRM;
        • 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
  • sem_op == 0,进程阻塞直到信号量的相应值为0:
    • 当信号量已经为0,函数立即返回。
    • 如果信号量的值不为0,则依据sem_flg决定函数动作:
      • sem_flg指定IPC_NOWAIT,则出错返回EAGAIN。
      • sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        • 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
        • 此信号量被删除,函数smeop出错返回EIDRM;
        • 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

semctl函数中的命令有多种,这里就说两个常用的:

  • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
  • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

    3、例子:父进程与子进程之间同步打印字母,使其成对出现

    commn.h

#pragma once                                                                                                                            

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

#define PATHNAME "."
#define PROJ_ID 0X6666

union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *_buf;
};

int CreateSem(int nums);
int InitSem(int semid, int nums, int initVal);
int GetSem(int nums);
int P(int semid, int who);
int V(int semid, int who);
int DestroySem(int semid);

common.c

#include "common.h"
static int CommonSemSet(int nums, int flags)
{
    key_t key = ftok(PATHNAME,PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int semid = semget(key,nums,flags);
    if(semid < 0)
    {
        perror("semget");
        return -2;
    }
    return semid;
}
 //创建信号量集                                                                                                                                       
int CreateSem(int nums)
{
    return CommonSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}   
//初始化信号量集
int InitSem(int semid, int nums, int initVal)
{
    union semun un;
    un.val = initVal;
    if(semctl(semid,nums,SETVAL,un) < 0)
    {
        perror("semctl");
        return -1;
    }   
    return 0;
} 
//获取已创建好的信号量集  
int GetSem(int nums)
{
    return CommonSemSet(nums,IPC_CREAT);
}
static int CommPV(int semid, int who, int op)
{
    struct sembuf sf;
    sf.sem_flg = 0;
    sf.sem_op = op;
    sf.sem_num = who;
    if(semop(semid,&sf,1)<0)
    {
        perror("semop");
        return -1;
    }
    return 0;
}
//申请资源,相当于对信号量进行减操作
int P(int semid, int who)
{
    return CommPV(semid,who,-1);
}
//释放资源,相当于对信号量进行加操作
int V(int semid, int who)
{
    return CommPV(semid,who,1);
}
//销毁刚创建的信号量集
int DestroySem(int semid)
{
    if(semctl(semid, 0 ,IPC_RMID) < 0)
    {
        perror("semctl");
        return -1;
    }
    return 0
}

test_sem.c

#include "common.h"
#include <unistd.h>
#include <sys/wait.h>
int main()                                                                                                                              
{
    int semid = CreateSem(1);
    InitSem(semid, 0 ,1);
    pid_t pid = fork();
    int i = 20; 
    if(pid < 0) 
    {
        perror("fork");
        return 0;
    }else if(pid == 0)
    {
        //子进程
        int _semid = GetSem(0);
        //通过P,V操作实现两个进程的互斥
        //在屏幕上打印成对出现的AB
        while(i--)
        {
            P(_semid, 0);
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");
            fflush(stdout);
            usleep(321456);
            V(_semid,0);
        }   
    }   
    else{
            //父进程
        while(i--)
        {        
            P(semid,0);
            printf("B");
            fflush(stdout);
            usleep(223456);
            printf("B ");
            fflush(stdout);
            usleep(121456);
            V(semid,0);
        }
        wait(NULL);
    }
    DestroySem(semid);
    return 0;
}   

运行结果(加上PV操作),AB成对出现,父子进程实现同步,那去掉PV之后呢
这里写图片描述

去掉PV之后现象如下,可以看出父子进程没有实现同步
这里写图片描述


五种进程间通信方式总结

  1. 管道:只支持有血缘关系的进程间通信,且生命周期随进程
  2. FIFO:支持任意进程间通信,创建和打开方式与管道不同,其他同管道一样
  3. 消息队列:存在系统限制,而且有时候需要手动删除,在读一条消息的时候还要注意上一条消息没有读完的情况
  4. 共享内存:速度最快的IPC方式,但要注意保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
  5. 信号量:不能用来传递复杂消息,只能用来互斥与同步

猜你喜欢

转载自blog.csdn.net/it_xiaoye/article/details/80075717