进程间通信(命名管道,消息队列,PV操作)

【摘要】进程间的通信有三种常用的实现形式,在这篇博客中,我总结了管道,消息队列以及共享内存的常用操作和知识,希望会对你们有所帮助。

进程间通信目的

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

进程间通信发展

  • 管道
  • System V 进程间通信
  • POSIX进程间通信

进程间通信

管道

-匿名管道 pipe
-命名管道

System V IPC

-System V 消息队列
-System V 共享内存
-System V 信号量

POSIX IPC

-消息队列
共享内存
信号量
互斥量
条件变量
读写锁

管道

管道具有以下局限性
(1)它们是半双工的(数据只能在一个方向上流动)虽然现在有的系统支持全双工管道,但是为了最佳的可移植性,我们暂不采用这种方式。

(2)管道只能在具有公共祖先的两个进程之间使用。通常一个管道由一个父进程创建,在进程调用fork之后,这个管道就可以在父进程和子进程之间使用啦。

管道的工作机制

每当用户在管道中输入一个命令序列,让shell执行时,shell都会为每个命令独立创建一个进程,然后用管道将前一条命令进程的标准输出和后一个命令的标准输入相连接。

管道的创建
#include <unistd.h>

int pipe(int fd[2]);

//返回值:成功返回0,出错返回-1

经由参数fd返回两个文件描述符,fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。

管道的几种工作方式

 int main()
  7 {
  8    int n;
  9    int fd[2];
 10    pid_t pid;
 11    char line[MAXLINE];
 12 
 13    if(pipe(fd) < 0)
 14     printf("pipe error");
 15    if( (pid = fork()) < 0) {
 16     printf("fork error");
 17    }
 18    else if(pid > 0){
 19       close(fd[0]);
 20       write(fd[1],"hello world\n",12);
 21 
 22    }
 23    else {
      close(fd[1]);
 25         n = read(fd[0],line,MAXLINE);
 26         write(STDOUT_FILENO,line,n);
 27    }
 28    exit(0);
 29 } 

管道读写规则

当没有数据可读时
- O_NONBLOCK disable :read 调用阻塞,即进程暂停执行,一直等到有数据来为止
- O_NONBLOCK enable : read调用返回 -1 ,errno 值为 EAGAIN
- 当管道满的时候
- O_NONBLOCK disable : write调用阻塞,直到有进程读走数据
- O_NONBLOCK enable : 调用返回 -1 ,errno 值为 EAGIN

-如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性
- 当要写入的数据量大于PIPE_BUF 时,Linux将不再保证写入的原子性

扫描二维码关注公众号,回复: 1565332 查看本文章
为了解决管道只有在具有亲缘关系才能通信的缺点,如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道

创建一个命名管道

- 命名管道可以直接使用命令行创建

mkfifo filename

也可以从程序里创建

int mkfifo ( const char *filenam,mode_t mode);
创建命名管道
int main (int argc,char *argv[])
{
    mkfifo("p2",0644)
    return 0;
}

匿名管道和命名管道的区别

  • 匿名管道由pipe 函数创建并打开
  • 命名管道由mkfifo 函数创建,打开用 open
  • FIFO(命名管道)与pipe(匿名管道)之间的唯一区别就是他们创建和打开的方式不同,一旦这些工作完成之后,它们具有相同的意义

共享内存

一旦共享内存和物理内存建立起联系,就不需要内核

编写服务器性能的四大杀手之一 环境切换 / 内存拷贝
消息队列的发数据和收数据都需要切换到内核
服务器通过 read

服务器只要read到共享内存,客户端只要write 到磁盘上,四次操作就变成了两次
如何申请共享内存,挂载共享内存到自己地址空间,删除共享内存,卸载共享内存空间

1.创建共享内存

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

 int shmget(key_t key,
            size_t size,)//申请空间的大小按照块的大小进行分配,找到最小的4k的倍数那么大     0
            int shmlg //IPC_CREAT|0644   0
消息队列存储量特别小,不适合真实使用
ipcs -m 查看共享内存是否已经创建

删除共享内存 
   iprm -M 0x4d2 key
   程序中删除    shmcntl
int shmctl(int shmid,
           int  cmd,   //IPC_RMID

           struct shmid_ds *buf)
#include<stdio.h>
  2 #include<sys/ipc.h>
  3 #include<sys/shm.h>
  4 
  5 int main()
  6 {
  7    int id =shemget(1234,0,0); //实际工作中不要把第一个参数写死  ftok
  生成key key_t ftok (const char * pathname) //文件名
                      int proj_id; // 低8位不能全部为0
    当前文件下的inode 会一直保持一个值,如果cp 到另一个目录下面,那么  iNode会变




  9    shmctl(id,IPC_RMID,0);
 10 

进程间通信,消息队列(通过链子链起来,里面有一个数据,通道号,管道

int shmget(key_t key,int size,int flag);
int shmctl(int id.int cmd,...);

将共享内存段挂载在自己的虚拟内存空间

void * shmat (int shmid,
             const void * shmaddr,//NULL
             int shmflg) // 0
返回值:挂载的虚拟地址空间起始地址,失败返回NULL

卸载不会共享内存删除

int shmdt (const void *shmaddr);
mmap 也是一种共享内存
申请一个共享内存段,让它不断给里面写数据
pv操作(哲学家必须同时管理,只能一次系统调用,传递两个信号量)

一个semop操作可以管理一个信号集的多个操作
int semop(int semid,
struct sembuf sops[] , //传数组,就要传数组元素大小
unsigned nsops);

struct sembuf{
    unsigned short sem_num; //信号量集中信号量下标
    short          sem_op;   // -1 p操作 1 v操作
    short          sem_flg;  // 0 主要写0
}
这是系统定义的结构体,我们直接使用
pv操作
int main()
   {
     int id = semget(1234,0,0);
     if( id == -1) exit(1);

    struct sembuf sb[1];
    sb[0].sem_num = 0;
    sb[0].sem_op = -1;
    sb[0].sem_flg = 0;
    semop(id,sb,1);
  }

   #include<stdio.h>
   #include<stdlib.h>
   #include<sys/ipc.h>
   #include<sys/sem.h>
   #include<unistd.h>

   void printxo(char c) //父子进程交替执行,然后打出来的结果是乱的
   {
       int i;

      for(i=0;i<10;i++) {
          printf("%c",c);
          fflush(stdout);
          sleep(rand()%3);
          printf("%c",c);
          fflush(stdout);
          sleep(rand()%2);
          }
  }

  int main()
  {
    //临界区,要么都操作,要么都不操作
    pid_t pid = fork(); //没加pv操作之前,父进程和子进程之间的切换
    if( pid == 0){    //有可能是子进程完成了一半,父进程醒了,就会打印出来不一致的结果
     printxo('o');
    } else {
     printxo('x');
    }
  }

加上pv,就可以同步打印
int id;
  8 
  9 void p()  //使用p操作
 10 {
 11   struct sembuf sb[1] = {
 12    {0, -1, 0}};
 13    semop (id,sb,1);
 14 }
 15 
 16 void v()  //使用v操作
 17 {
 18   struct sembuf sb[1] = {
 19    {0, 1, 0}};
 20    semop(id,sb,1);
 21 }
 22 
 23 void printxo(char c)
 24 {
 25     int i;
 26 
 27     for(i=0;i<10;i++) {
 28      p();
 29     printf("%c",c);
 30         fflush(stdout);
 31         sleep(rand()%3);
 32         printf("%c",c);
 33         fflush(stdout);
 34         sleep(rand()%2);
 35         v();
 36 }
 37 }

信号量集

int semget(key_t key,
           int nsems,  //信号量集中有几个信号  0 打开的话,第二个参数也填0
           int semflg  //IPC_CREAT| 0644 0
    既有创建也有打开的功能
    ipcs -s  查看信号量集一般一个信号量集有一个信号

设置初值

int  semctl(int semid,
            int semnum , 给第几个信号量设置初值
            int cmd)   //SETVAL  
             senum.val = 2 //初值放在val 中

获得信号量的值

int semctl(int semid,
           int semnum, //想获得的第几个信号量的值
           int cmd) //GETVAL
        返回值:信号量的当前值

给共享内存中装东西(pv操作)如果有人在里面装东西时,我们可以一直取,多个人装东西时,不能装在同一个位置,多个人拿也不能从同一个位置拿。

消息队列

  • 缺陷:
    1. 实现了跨进程的数据传递
    2. 大小内核规定死了
    3. 很复杂,消息有类型,按类型去取
    4. 每一次都要从用户空间切换到内存空间

共享内存块到底能够装多少数据应该在初始化的时候就知道了

如果进程a写入了两个消息,然后又来了一个进程c,共享内存的当前读写位置应该保存在当前共享内存中。保存在头部。
有一个写的索引位置,

有一个读的索引位置

struct shm_head{
    int wr_idx; //写的索引位置
    int rd_idx; //读的索引位置
    int blocks; //数据大小
    int blksz;  //
}
struct shmfifo{
    shm_head *p_head;
    char     *p_payload;
    int      shmid; //共享内存ID
    int      sem_mutex; //互斥量
    int      sem_full;  //还有多少个地方可以生产
    int      sem_empty; // 还有多少个地方可以消费
}

猜你喜欢

转载自blog.csdn.net/zb1593496558/article/details/80409798