计算机基础OS 进程间通信(IPC)机制介绍

怀抱观古今,寝食展戏谑。这篇文章主要讲述计算机基础OS 进程间通信(IPC)机制介绍相关的知识,希望能为你提供帮助。
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
进程间通信(IPC)介绍
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

每个进程各有不同的用户地址空间,如何一个进程的全局变量是不能被别的进程看见的,所以进程间通信都要经过内核,在内核中开辟一个缓存区,这个缓存区是所有与缓存区相关的进程都能访问的。

进程1把数据从用户空间拷到内核缓存区,进程2再从缓存区把数据读到自己的用户空间。内核提供的这种机制就叫做进程间通信。

                                                  Linux内核结构
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
每个IPC都会维护一个结构体(ipc_perm):

一. 管道通信(pipe)
管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。
1、特点:
它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
 

#include < unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1

- 无名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

匿名管道是一种半双工(在同一信道上,同一时刻只能接受或者发送数据)的通信方式,数据只能单向流动。匿名管道通过文件描述符进行操作,子进程会继承父进程的文件描述符。所以匿名管道只能用于有血缘关系的进程的进程间通信。

创建匿名管道的过程:
  1. 父进程通过pipe函数创建一个管道,得到两个文件描述符指向管道的两端。
  2. 父进程调用fork函数创建一个子进程,子进程会继承父进程PCB中的一部分数据,所以子进程也有两个文件描述符指向管道的两端。
  3. 父进程关闭管道读端,子进程关闭管道写端(close函数关闭读/写端)。注:应该先写入数据再进行读操作(问题:难道不能父进程关闭写端,子进程关闭读端?)
注:管道是以环形队列实现的,是以流的形式传递数据的。
- 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
- 有名管道 (named pipe) :有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
命名管道也是一种半双工的通信方式,但它是通过路径名与缓存区关联。因为路径名在系统中是全局的,所以不同的进程即使没有血缘关系也能进行通信。
创建命名管道的方式:
  1. 在Shell下交互的建立一个命名管道。(manode或mkfifo)
  2. 在程序中使用系统调用函数建立命名管道。
二. 消息队列(message queue)
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
消息队列是比较高级的一种进程间通信方式,因为它真的是可以在进程间传送message,传送普通字符串也可以。
一个消息队列可以被多个进程所共享(IPC((Inter-Process Communication,进程间通信))就是在这个基础上进行的); 如果一个进程消息太多,一个消息队列放不下,也可以用多于一个的消息队列(不管管理可能会比较复杂)。共享消息队列的进程所发送的消息除了message本身外还有一个标志,这个标志可以指明该消息将由哪个进程或者哪类进程接受。每一个共享消息队列的进程针对这个队列也有自己的标志,可以用来申明自己的身份。
1、特点
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
消息队列的常用函数如下表:

进程间通过消息队列通信操作主要有:创建或打开消息队列,添加消息,读取消息和控制消息队列。
三. 信号量(semaphore)
信号和信号量是不同的,它们虽然都可以用来同步和互斥,但是信号是使用信号处理器来进行的,信号量是使用P,V操作来实现的。
信号量(semaphore),它是一个计数器。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
1、特点
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
支持信号量组。
最简单的信号量是只能取 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, ...);

四. 共享内存通信(Shared Memory)
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。

共享内存有两种实现方式:1、内存映射 2、共享内存机制
共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
1、特点
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。因为多个进程可以同时操作,所以需要进行同步。
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。

采用共享内存进行通信的一个主要好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队里等通信方式,则需要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另一次从共享内存到输出文件。
一般而言,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时在重新建立共享内存区域;而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件,因此,采用共享内存的通信方式效率非常高。
五. 套接字( socket )
套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
5.1命名socket
SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用 struct sockaddr_un 类型的变量。
5.2 绑定
SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用 struct sockaddr_un 类型的变量,将相应字段赋值,再将其绑定在创建的服务器套接字上,绑定要使用 bind 系统调用,其原型如下:
int bind(int socket, const struct sockaddr *address, size_t address_len);

其中 socket表示服务器端的套接字描述符,address 表示需要绑定的本地地址,是一个 struct sockaddr_un 类型的变量,address_len 表示该本地地址的字节长度。
5.3 监听
服务器端套接字创建完毕并赋予本地地址值(名称,本例中为Server Socket)后,需要进行监听,等待客户端连接并处理请求,监听使用 listen 系统调用,接受客户端连接使用accept系统调用,它们的原形如下:
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, size_t *address_len);

其中 socket 表示服务器端的套接字描述符;backlog 表示排队连接队列的长度(若有多个客户端同时连接,则需要进行排队);address 表示当前连接客户端的本地地址,该参数为输出参数,是客户端传递过来的关于自身的信息;address_len 表示当前连接客户端本地地址的字节长度,这个参数既是输入参数,又是输出参数。
5.4 连接服务器
客户端套接字创建完毕并赋予本地地址值后,需要连接到服务器端进行通信,让服务器端为其提供处理服务。
对于SOCK_STREAM类型的流式套接字,需要客户端与服务器之间进行连接方可使用。连接要使用 connect 系统调用,其原形为
int connect(int socket, const struct sockaddr *address, size_t address_len);

其中socket为客户端的套接字描述符,address表示当前客户端的本地地址,是一个 struct sockaddr_un 类型的变量,address_len 表示本地地址的字节长度。实现连接的代码如下:
connect(client_sockfd, (struct sockaddr*)& client_address, sizeof(client_address));

5.5 相互发送接收数据
无论客户端还是服务器,都要和对方进行数据上的交互,这种交互也正是我们进程通信的主题。一个进程扮演客户端的角色,另外一个进程扮演服务器的角色,两个进程之间相互发送接收数据,这就是基于本地套接字的进程通信。发送和接收数据要使用 write 和 read 系统调用,它们的函数原型为:
int read(int socket, char *buffer, size_t len);
int write(int socket, char *buffer, size_t len);

其中 socket 为套接字描述符;len 为需要发送或需要接收的数据长度;
对于 read 系统调用,buffer 是用来存放接收数据的缓冲区,即接收来的数据存入其中,是一个输出参数;
对于 write 系统调用,buffer 用来存放需要发送出去的数据,即 buffer 内的数据被发送出去,是一个输入参数;返回值为已经发送或接收的数据长度。
5.6 断开连接
交互完成后,需要将连接断开以节省资源,使用close系统调用,其原型为:
int close(int socket);

进程间通信方式总结:
1.管道:速度慢,容量有限,只有父子进程能通讯
2.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
3.信号量:不能传递复杂消息,只能用来同步
4.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
5.套接字:可用于不同机器间的进程通信。
参考链接:
??https://blog.51cto.com/10704527/1762349??
?


?
?




【计算机基础OS 进程间通信(IPC)机制介绍】


    推荐阅读