《进程间通信》

1.IPC

IPC:interprocess communication-进程间通信

  Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程间不能互相访问,要交换数据必须通过内核,在内核中开辟一块缓存区,进程1把数据从用户空间拷贝到内存缓冲区,进程2再从内存缓冲区中把数据读走,内核提供的这种机制称为进程间通信(IPC)。

进程间通信的实质?

  不同的两个程序虽然0-4G内存地址空间不同,但两个程序的内核区在物理内存上是同一块。共享(物理内存中)同一内核区。——两个程序打开同一个文件,系统会在内核区维护打开文件的结构体,两个程序打开的是同一个结构体。

有哪些常用的进程间通信方法? 

    ①匿名管道(PIPE)和有名管道(FIFO):最简单

    ②信号(SIGNAL):系统的开销最小 

    ③共享映射区(MMAP):可以在无血缘关系的进程间通信

    ④本地套接字(SOCKET):最稳定(但是比较复杂)

IPC方法比较

  pipe-优点:实现简单;缺点:单向通信,只能用于有血缘关系进程间。   

  fifo-优点:可以在非血缘关系进程间。    信号-优点:开销小。   

  共享内存-优点:可以用于非血缘关系进程间;缺点:比较复杂。   

  本地套接字-优点:稳定性好;缺点:实现复杂。   

  共享内存和fifo的区别:在于数据是否可以反复读取;fifo是用队列实现的,不能反复读取;共享内存是Linux内核借助缓冲区,通过内存来实现,可以反复读取。

标识符和键

  每个内核中的IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符(identifier)加以引用。

  标识符是IPC对象的内部名。为此,每个IPC对象都与一个键(key)相关联,将这个键作为该对象的外部名。

  无论何时创建IPC结构(通过调用msgget、semget或shmget创建),都应指定一个键。这个键的数据类型是基本系统数据类型key_t,通常在头文件<sys/types.h>中被定义为长整型。这个键由内核变换成标识符。

  有多种方法使客户进程和服务器进程在同一IPC结构上汇聚。

  1.服务器进程可以指定键IPC_PRIVATE创建一个新的IPC结构,将返回的标识符存放在某处(如一个文件)以便客户进程取用。键IPC_PRIVATE保证服务器进程创建一个新IPC结构。这种技术的缺点:文件系统操作需要服务器进程将整型标识符写到文件中,此后客户进程又要读取这个文件取得此标识符。

  2.可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的IPC结构。这种方法的问题是该键可能已与一个IPC结构相结合,在次情况下,get函数(msgget、semget或shmget)出错返回。服务器进程必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。

  3.客户进程和服务器进程认同一个路径名和ID(0~255之间的字符值),调用函数ftok函数将这两个值变换成一个键。然后再方法(2)中使用此键。

  ftok提供的唯一服务就是由一个路径名和ID产生一个键

#include <sys/ipc.h>

key_t  ftok(const char *path, int id);

返回值:成功,返回键;失败返回-1

  3个get函数(msgget、semget和shmget)都有两个类似的参数:一个key和一个整形的flag。在创建新的IPC结构(通常由服务器进程创建)时,如果key是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要知名flag的IPC_CREAT标志位。为了引用一个现有队列(通常由客户进程创建),key必须等于队列创建时指明的key的值,并且IPC_CREAT必须不被指明。

  注意:绝不能指定该IPC_PRIVATE作为键来引用一个现有队列,因为这个特殊的键值总是用于创建一个新队列。为了引用一个用IPC_PRIVATE键创建的现有队列,一定要知道这个相关的标识符,然后在其他IPC调用中(如msgsnd、msgrcv)使用该标识符,这样可以绕过get函数。

  如果希望创建一个新的IPC结构,并且要确保没有引用具有同一标识符的一个现有IPC结构,那么必须在flag中同时指定IPC_CREAT和IPC_EXCL位。这样做了以后,如果IPC结构已经存在就会造成出错,返回EEXIST(这与指定了O_CREAT和O_EXCL标志的open相类似)。

2.管道

管道:就是把一个程序的输出直接连接到另一个程序的输入。(具有血缘关系的进程之间)

#include <unistd.h>

int pipe(int fd[2])

参数:

  • fd[2]:管道的两个文件描述符,fd[0]固定用于读管道,fd[1]固定用于写管道。

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

如何使用管道:

用pipe函数直接创建的管道两端是处于一个进程中,但是我们的管道是用于不同进程之间的通信。因此我们可以使用fork函数创建一个子进程,这样子进程就继承了父进程的管道,之后我们只需根据实际需求,我们只需关闭父子进程中各一个的管道的文件描述符就可以实现半双工的通信模式。(这也就是能解释为什么管道要用于具有血缘关系的进程之间的通信)。

管道读写注意点:

  • 只有在管道读端存在时向管道写入数据才有意义。否则,向管道中写入数据的进程会收到内核传来的SIFPIPE信号(通常Broken pipe错误)。
  • 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。
  • 父子进程在运行时,并不能保证前后次序,因此,为了保证父进程已经关闭读描述符,可在子进程中调用sleep函数。

 标准流管道(popen)

作用:用来创建一个连接到另一个进程的管道(这里的另一个进程也就是一个可以进行一定操作的可执行文件)。

popen主要完成以下几步:

  • 创建一个管道
  • fork一个子进程
  • 在父子进程中关闭不需要的文件描述符
  • 执行exec函数族调用
  • 执行函数中所指定的命令
#include <stdio.h>

FILE *popen(const char*command, const char* type)

参数:

  • command:一个字符串,包含一个shell命令,并被送到/bin/sh以-c参数执行,即由shell来执行。
  • type:“r”文件指针连接到command的标准输出,即改命令的结果产生输出。

                    “w”文件指针连接到command的标准输入,即该命令的结果产生输入。

返回值:成功:文件流指针  失败:NULL

#include <stdio.h>

int pclose(FILE *stream)

参数:

  • stream:要关闭的文件流

返回值: 成功:返回popen中执行命令的终止状态 出错:NULL

有名管道:

https://www.cnblogs.com/zhuangquan/p/10132432.html

 

3.信号量

信号:是在软件层次上对中断机制的一种模拟,是一种异步通信方式。可以直接进行用户空间进程和内核进程之间的交互。它可以再任何时候发给某一进程,而无需知道该进程的状态。如果该进程未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它。

semget

#include <sys/sem.h>

int semget(key_t key, int num_sems, int sem_flags);
第一个参数key:key是一个整数值,不相关的进程可以通过它访问同一个信号量(共享内存或消息队列)。程序对所有信号量的访问都是间接的,它先提供一个键,再有系统生成一个相应的信号量标识符,其他的函数使用这个标识符。
第二个参数num_sems:指定需要的信号量数目。它几乎总是取值为1.
第三个参数sem_flags:访问权限。
返回值: 成功:正数(非零)值; 失败:-1

  作用:创建一个新的信号量或取一个已有信号量的键。

semop

#include <sys/sem.h>

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
第一个参数sem_id:信号量标识符。
第二个参数sem_ops是指向一个结构数组的指针,每个数组元素至少包含以下几个成员:
struct sembuf{
   short sem_num; //信号量编号,除非使用一组信号量,否则它的取值为0
   short sem_op;  //信号量在一次操作中需要改变的数值。通过用到两个值,-1,也就是p操作,它等待信号量变为可用;+1,也就是V操作,它发送信号表示信号量现在已可用。
   short sem_flg; //通过被设置为SEM_UNDO
};

semctl

#include <sys/shm.h>

int semctl(int sem_id, int sem_num, int command,...);
第一个参数sem_id:信号量标识符
第二个参数sem_num:信号量编号,当用到成组的信号量,就要用到这个参数。它一般取值为0,表示这是第一个也是唯一的一个信号量
第三个参数command:要采取的动作
有很多不同的值,有两个常用的SETVAL:用来把信号量初始化成一个已知的值,这个值通过union semun中的val成员设置。
              IPC_RMID:用于删除一个已经无需使用的信号量标识符。
第四个参数:如果还有第四个参数,就是一个union semun结构
union semun{
  int val;
  struct semid_ds *buf;
  unsigned short *array;
};

4.共享内存

共享内存:共享内存是一种最为高效的进程间通信方式。因为数据可以直接读写内存,不需要任何数据的拷贝。内核专门留出一块内存区,这段内存区可以由需要访问的进程将其中映射到自己的私有地址空间。但同时由于多个进程共享一段内存,因此也需要依靠同步机制,如互斥锁和信号量。

shmget

#include <sys/shm.h>

int shmget(key_t key, int size, int shmflg)
第一个参数key:有效地为共享内存段命名
第二个参数size:以字节为单位制定需要共享的内存容量
第三个参数shmflg:权限标志
返回值: 成功:非负整数即共享内存标识符; 失败:-1

  作用:创建共享内存、

shmat

#include <sys/shm.h>

char *shmat(int shmid, const void*shmaddr,int shmflg)
第一个参数shmid:共享内存标识符
第二个参数shmaddr:将共享内存映射到指定位置(若为0则表示让系统来选择共享内存出现的地址)
第三个地址:SHM_RND(这个标志与shm_addr联合使用,用来控制共享内存连接的地址)
      SHM_RDONLY(使它连接的内存只读)
      默认0:共享内存可读可写
返回值:返回一个指向共享内存第一个字节的指针; 失败:-1

  作用:第一次创建共享内存时,它不能被任何进程访问。要想启用该共享内存的访问,必须将其连接到一个进程的地址空间中。

shmdt

#include <sys/shm.h>

int shmdt(const void *shmaddr)
第一个参数shmaddr:
  • shmaddr:被映射的共享内存段地址
  • 返回值: 成功:0; 失败:-1

  作用:将共享内存从当前进程中分离(并不是删除)。

#include <sys/shm.h>

int shmctl(int shm_id, int command, struct shmid_ds *buf);
第一个参数shm_id:shmget返回的共享内存标识符
第二个参数command:要采取的动作,可以取三个值
    IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值
    IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    IPC_RMID:删除共享内存段
第三个参数buf:是一个指针,它指向包含共享内存模式和访问权限的结构
返回值:成功:0;失败:-1
struct shmid_ds{
  uid_t shm_perm.uid,
  uid_t shm_perm.gid;
  mode_t shm_perm.mode;
};

5.消息队列

  消息队列:提供一种从一个进程向另一个进程发送一个数据块的方法。与FIFO相比,消息队列的优势在于,它独立于发送和接收进程而存在。

  1.链表式结构组织,存放于内核。

  2.通过队列标识来引用。

  3.通过一个数据类型来索引指定的数据。

msgget函数

#include <sys/msg.h>

int msgget(key_t key, int msgflg);
第一个参数key:每一个IPC对象与一个key对应
第二个参数msgflg:函数的行为(0666|IPC_CREAT表示用户具有读写权限)
返回值: 成功:非负队列ID; 失败:-1;

  作用 :创建和打开一个消息队列。

  可通过ipcs -q(只查看消息队列的状态):查看系统的IPC状态

msgsnd函数

#include<sys/msg.h>

int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);

第一个参数msqid是由msgget函数返回的消息队列标识符;
第二个参数msg_ptr是一个指向准备发送消息的指针,消息必须像刚才说的那样以一个长整型成员变量开始;
第三个参数msg_sz是msg_ptr指向的消息的长度。这个长度不能包括长整型消息类型成员变量的长度;
第四个参数msgfig控制在当前消息队列满或队列消息到达系统范围的限制时将要发生的事情,如果msgflg中设置了IPC_NOWAIT标志,函数将立刻返回,不发
送消息并且返回值为-1.如果msgflg中的IPC_NOWAIT标志被清除,则发送进程将挂起以等待队列中腾出可用空间。
返回值: 成功:0; 失败:-1;

  作用:把消息添加到消息队列中。

  消息的结构受两方面的约束。首先,它的长度必须小于系统规定的上限;其次,它必须以一个长整型成员变量开始。接收函数将用这个成员变量来确定消息的类型。当使用消息时,最好把消息结构定义为下面这样:

  struct my_message{

    long int message_type;

  }

  由于在消息的接收中要用到message_type,所以你不能忽略它。你必须在声明自己的数据结构时包含它,并且最好将它初始化为一个已知值。

msgrcv函数

#include<sys/msg.h>

int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
第一个参数msqid是由msgget函数返回的消息队列标识符;
第二个参数msg_ptr是一个指向准备接收消息的指针,消息必须像前面msgsnd函数中介绍的那样以一个长整型成员变量开始。
第三个参数msg_sz是msg_ptr指向的消息的长度,它不包括长整型消息类型成员变量的长度。它不包括长整型消息类型成员变量的长度。
第四个参数msgtype是一个长整型,它可以实现一种简单形式的接收优先级。如果msgtype的值为0,就获取队列中的第一个可用消息。
如果它的值大于零,将获取具有相同消息类型的第一个消息。如果它的值小于零,将获取消息类型等于或小于msgtype的绝对值的第一个消息。
这个函数看起来好像很复杂,但实际应用很简单。如果只想按照消息发送的顺序来接收它们,就把msgtype设置为0。如果只想获取某一特定类型的消息,就把msgtype设置
为相应的类型值。如果想接收类型等于或小于n的消息,就把msgtype设置为-n。
第五个参数msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。如果msgflg中的IPC_NOWAIT标志被设置,函数将会立刻返回,返回值是-1.如果msgflg
中的IPC_NOWAIT标志被清除,进程将会挂起以等待一条相应类型的消息到达。

  作用:从一个消息UI列中获取消息。

msgctl函数

#include<sys/msg.h>

int msgctl(int msqid, int command, struct msqid_ds *buf);
第一个参数msqid是由msgget返回的消息队列标识符;
第二个参数command是将要采取的动作,它可以取3个值。
IPC_STAT  把msqid_ds结构中的数据设置为消息队列的当前关联值    
IPC_SET    如果进程有足够的权限,就把消息队列的当前关联值设置为msqid_ds结构中给出的值
IPC_RMID  删除消息队列

返回值: 成功:0; 失败:-1
原文地址:https://www.cnblogs.com/zhuangquan/p/10900687.html