进程间通信 —— IPC

标签: 管道    消息队列  共享内存  信号量

进程间通信 —— IPC

目录:

为什么要有进程间通信?

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

进程间通信的目的:

  1. 数据传递:一个进程将数据传送到另一个进程
  2. 资源共享:多个进程共享同样的资源
  3. 事件通知:一个进程需要向另外一个进程发送消息,通知他们发生了某事件
1.管道

匿名管道 由pipe函数创建:

#include <unistd.h>
int pipe(int fileded[2]);

调用 pipe 函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过 filedes 参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像 0 是标准输入 1是标准输出一样)。
所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者 write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。

pipe 函数调用成功返回 0,调用失败返回-1。
这里写图片描述
管道的最典型的用途就是为两个不同的进程(一个是父进程,一个是子进程)提供进程间通信的手段。首先由一个进程创建一个管道后调用fork创建一个子进程。接着,父进程关闭这个管道的读端,子进程关闭这个管道的写端。这就在父子之间提供了一个单向的数据流。
这里写图片描述

例:创建管道,进行父子间通信
这里写图片描述

使用管道需要注意以下4种特殊情况:

1、如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被取走后,再次read时会返回0,就像读到文件末尾一样。
2、如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也不往管道
中写数据,这时有进程从管道中读数据时,管道中的数据被读取之后,就会再次read阻塞,直到管道中有数据可读才读取数据并返回。
3、如果所有指向管道读端的文件描述符都关闭了,这时有进程向管道的写端write,那么该进程会受到SIGPIPE信号。通常会导致进程异常终止。
4、如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读数据,这时有进程向管道的写端写数据,那么在管道被写满时会再次write阻塞,直到管道中有空位置了才写入数据并返回。

管道的限制:

  • 数据的流动方向是单向的
  • 只能在有共同祖先的进程之间通信

也有一种特殊的管道称为FIFO,也叫命名管道,它也能用于无关联进程间的通信。
这里写图片描述
FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道,各进程可以打开这个文件进行read/write,实际上是在读写内核通道。

例子:从键盘读取数据,写入管道,读取管道,写到屏幕
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

2.消息队列

消息队列是两个进程之间传递数据的一种简单有效的方式。每个数据块都有一个特定的类型,接收方可以根据类型来有选择的接收数据。而不一定像管道和命名管道那样必须以先进先出的方式接收数据。

创建消息队列:msgget()

int  msgget(key_t key, int msgflg); 
参数: 
    key:某个消息队列的名字
    msgflg: 0 / IPC_CREAT | 0644
返回值:
    成功返回该消息队列的标识码;失败返回-1
key_t ftok(const char* pathname, int proj id)
         id: 低8位不能是0
一般写为:msgget(ftok(".", 'a'), IPC_CREAT|0644)自动生成key

这里写图片描述
往消息队列里发送数据:msgsnd()

struct msgbuf
{
      long channel; 消息类型(通道号),必须>=1
      char mtext[1];//写上自己的消息内容
}

int msgsnd(int msqid, //msgget的返回值
            const void *msgp, //要发送的消息在哪里
            size_t msgsz, //消息的字节数,不包括channel的大小
            int magflg);//IPC_NOWAIT表示队列满,不等待,返回EAGAIN错误//0
返回值:
    成功返回0,失败返回-1

从消息队列中取数据:msgrcv()

ssize_t  msgrcv(int msqid, //id
                void *msgp, //取出来的消息放在这里
                size_t msgsz, //装消息的地方的大小,不包括类型
                long msgtyp, //取哪个类型的消息
                int msgflg);//0

返回值:
     成功返回实际放到接收缓冲区里去的字符个数,失败返回-1

删除消息队列:msgctl()

int magctl(int msqid,//由msgget函数返回
           int cmd,//IPC_RMID(删除消息队列)
           struct msqid_ds *buf); //0

返回值: 成功返回0,失败返回-1

1、系统中最多能创建多少个消息队列?
cat /proc/sys/kernel/msgmni

2、一条消息最多能够装多少字节?
cat /proc/sys/kernel/msgmax

/3、一个消息队列中所有的消息的总字节数是多少?
cat /proc/sys/kernel/msgmnb

ipcs :查看消息队列、共享内存、信号量全部的

查看当前已经存在的消息队列:pcs -q
删除消息队列:ipcrm -q + key

消息队列的缺点:

  1. 系统能创建的队列个数有限
  2. 每个消息能装的内容有限
  3. 一个消息队列中所有消息的总字节数也有限
  4. 每一次访问的时候需要调用系统的函数,意味着要从用户空间切换到内核空间,发送完毕后,再从内核空间切换到用户空间,开销比较大。

例子:用消息队列实现客户端和服务器之间的消息发送
每一个进程的进程id是唯一的,收消息时从id那收。最终将消息显示到客户端的显示器上
服务器:收1号通道的数据
发送到pid通道号
客户端:往1号通道发(带上自己的进程id)
这里写图片描述

3.共享内存—— 最快的进程间通信方式

将其映射到自己的虚拟地址空间,进程间的数据传递不用从用户空间到内核。

System V 共享内存方式

//创建或打开共享内存

int shmget(key_t  key, //共享内存的名字
           size_t  size,//要创建的共享内存段的大小,向上对齐到内存页的大小
           int shmflg) // 打开:0   创建:IPC_CREAT|0644

//将共享内存挂载到自己的地址空间

void* shmat(int shmid, //shmget得到的id
            const void* shmaddr, //NULL(让操作系统自己选择)想让操作系统挂载到这个地址空间
            int shmflg); //0
返回值:实际挂载到的虚拟地址的起始位置

//卸载掉共享内存,但不删除共享内存段

int shmdt(const  void* shmaddr);

//删除共享内存段

int shmctl(int shmid,  
           int cmd, //IPC_RMID 删除共享内存
           struct shmid_ds* buf); // NULL/0

查看共享内存:ipcs -m
删除共享内存 : ipcm -m + key

例:将id和name映射到自己的虚拟地址空间,然后读出来。
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
共享内存为进程间通信提供了一种效率很高的手段,但是这种手段并不提供进程间同步和互斥的功能,所以,共享内存作为广义的进程间通信手段还必须有其它机制来配合。所以我们一般将信号量和共享内存来搭配使用。

4.信号量—— 解决同步互斥问题

同步:多个进程为了完成一件事情需要相互协同工作。
互斥:多个进程访问共享资源,共享资源具有排它性,一次仅允许一个人访问,由于各个进程都要使用共享资源。而且这个资源必须同时只能有一个进程使用。

信号量的使用

互斥访问:临界区的互斥访问控制
条件同步:线程间的事件等待

用信号量实现临界区的互斥访问:
每类资源设置一个信号量,其初值为1
必须成对使用P()操作和V()操作

  • P()操作保证互斥访问临界资源
  • V()操作在使用后释放临界资源
  • PV操作不能次序错误。重复或遗漏

用信号量实现条件同步:
条件同步设置一个信号量,其初值为0

信号量值的含义

struct semphore{
     int value;
     struct PCB* queue;//等待队列,有哪些进程在等待该资源
}
s.value > 0 表示资源的个数
s.value = 0 表示没有资源可以使用,也没有进程等待该资源
s.value < 0 |s.value|个进程在等待该资源

P、V原语

//P原语                                                 //V原语
P(s){                                                   V(s){
    s.value--;                                            s.value++;
    if(s.value < 0){                                      if(s.value <= 0){
       将该进程置为等待状态                                     唤醒等待队列的进程
       加入等待队列queue                                       将进程的状态改为就绪态,放到就绪队列
    }                                                       }
}                                                       }

//创建或打开信号量

int semget(key_t key,
           int nsems,  //在信号量集中信号量的个数
           int semflg); //打开是0  创建是 IPC_CREAT|0644

//删除

int semctl(int semid, //segmet的返回值
           int semnum, //0
           int cmd, //IPC_RMID
           ...);

//设置信号量初值

union semun{
    int val;
};

int semctl(int semid,   //semget的返回值
           int semnum, // 对信号量集的第几个信号量操作
           int cmd,  //SETVAL
           su); //设置信号量初值

//查看信号量的值

int semctl(int semid,  //semget的返回值
           int semnum, // 对信号量集的第几个信号量操作
           int cmd,  //GETVAL
           0); 
返回值:当前信号量的值

//PV操作

struct sembuf{
    unsigned short sem_num;//信号量集中的第几个信号量,下标
    short  sem_op;  //P -1  ,  V +1
    short  sem_flg  //0
};

int semop(int semid,
          struct sembuf* buf,
          unsigned nsops);

查看信号量:ipcs -s
删除信号量:ipcrm -s + key

例子:哲学家进餐
这里写图片描述
这里写图片描述

原文链接:加载失败,请重新获取