全栈开发知识库

进程间通信

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常是指用户进程)之间的资源是独立的,没有关联。不能在一个进程中直接访问另一个进程的资源(例如打开的文件描述符)。

进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。

Linux操作系统支持的主要进程通信的通信机制

image

进程间通信的实质:

系统只要创建一个进程,就会给当前进程分配4G的虚拟内存(32位操作系统)。

虚拟内存不是常说的内存条的空间,内存条的空间称为物理内存。 虚拟内存和物理内存之间存在映射关系。

4G的虚拟内存分为两部分: - 3G(0 ~ 3G)的用户空间 - 1G(3G ~ 4G)的内核空间

用户空间是进程所私有的,每一个进程的用户空间只能自己访问和使用,例如前面所说的栈区、堆区、数据区、代码区等都是用户空间的区域。

内核空间是所有进程所共有的,也就意味着绝大多数进程间通信的方式,本质就是对内核空间的操作。

特殊的进程间通信方式

  • socket通信可以实现不同主机的进程间通信,其他的都能在一台主机的多个进程间通信。
  • 信号通信是唯一的一种异步通信机制。
  • 共享内存是所有进程间通信方式中效率最高的,它是直接对物理内存进行操作。

信号

信号是软件中断,它是软件层次上对中断机制的一种模拟。

信号是由当前系统已经定义好的一些标识,每一个标识都会在特定场合使用,并且都会对进程有一定的影响。 当信号产生时,会让当前进程做出相应的操作。

例如:当用户按某些终端键时,将会产生信号。就像我们命令行执行时按下Ctrl+C组合键,这时就会产生中断信号SIGINT、终端上按Ctrl+\键通常会产生中断信号SIGQUIT、终端上按Ctrl+Z键通常会产生中断信号SIGSTOP。

kill函数

给指定的进程发送信号。

int kill(pid_t, int signum); 

raise函数

给调用进程本身发送一个信号

#include<signal.h>

int raise(int signum);
例如:  raise(SIGINI); 

abort函数

向进程发送一个SIGABRT信号,默认情况下进程会退出。

#include <stdlib.h>
void abort();

pause函数

include <unistd.h>

int pause();

将调用进程挂起直至捕捉到信号为止,这个函数通常用于判断信号是否已到。

signal函数

进程接收到信号后的处理方式: - 执行系统默认的操作 - 忽略此信号 - 执行自定义信号处理函数

程序中可用函数signal()改变信号的处理方式

     typedef void (*sig_t) (int);

     sig_t
     signal(int sig, sig_t func);

管道

管道(Pipe)又称无名管道。

无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。

任何一个进程在创建的时候,系统都会分配给它4G(32位系统)的虚拟内存,分为3G的用户空间和1G的内核空间,内核空间是所有进程公有的,无名管道就是创建在内核空间,多个进程知道同一个无名管道的空间,就可以利用它来进行通信。

无名管道虽然是创建在内核空间的,但是会给当前用户进程两个文件描述符,一个负责执行读操作,一个负责执行写操作。

管道是最古老的Unix IPC方式,其特点是:
- 半双工,数据在同一时刻只能在一个方向上流动。 - 数据只能从管道的一端写入,从另一端读出。 - 写入管道中的数据遵循先入先出的规则。 - 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式。 - 管道不是普通的文件,不属于某个文件系统,其只存在于内存中。 - 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。 - 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。 - 管道没有名字,只能在具有公共祖先的进程之间使用。

无名管道创建函数

经由参数filedes返回两个文件描述符

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

filedes为int型数组的首地址,其存放了管道的文件描述符fd[0]、fd[1]。 - filedes[0]:负责对管道执行读操作。 - filedes[1]:负责对管道执行写操作。

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

image

注意:
利用无名管道实现进程间的通信,都是父进程创建无名管道,然后再创建子进程,子进程继承父进程的无名管道的文件描述符,然后父子进程通过读写无名管道实现通信。

命名管道(FIFO)

命名(有名)管道(FIFO)和管道(Pipe)基本相同,但也有一些显著的不同。

其特点是:
- FIFO在文件系统中作为一个特殊的文件而存在并且在文件系统中可见,所以有名管道可以实现不相关进程间的通信,但FIFO中的内容却存放在内存中。
- 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。 - FIFO有名字,不相关的进程可以通过打开命名管道进行通信。

系统调用的I/O函数都可以作用域FIFO,如open、close、read、write等。

有名管道的创建

#include <sys/types.h>
#Include <sys/stat.h>


int mkfifo(const char *pathname, mode_t mode);

功能:创建一个有名管道,产生一个本地文件系统可见的文件pathname。

参数:
- pathname: 有名管道创建后生成的文件,可以带路径。 - mode: 管道文件的权限,一般通过八进制设置即可,例如0664。

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

消息队列

消息队列是消息的链表,存放在内存中,由内核维护。

除了最原始的进程间通信方式:信号、无名管道、有名管道外, 还有三种进程间通信的方式,这三种方式称之为IPC对象。

IPC对象分类:消息队列、共享内存、信号灯集。

IPC对象也是在内核空间开辟区域,每一种IPC对象创建好之后都会将其设置为全局,并且会给其分配一个编号。

只要找到唯一的这个编号就可以进行通信,所以不相关的进程可以通过IPC对象通信。

IPC对象创建好之后,会在当前系统中可见,只要不删除或者不关闭系统,就会一直存在。

查看已经创建的IPC对象

ipcs 查看当前系统中所有创建的IPC对象
ipcs -q 查看创建的消息队列
ipcs -m 查看创建的共享内存
ipcs -s 查看创建的信号量
ipcrm 删除IPC对象

消息队列的特点

  • 消息队列中的消息都是有类型的。
  • 消息队列中的消息是有格式的。
  • 消息队列可以实现消息的随机查询,消息队列不一定以先进先出的次序读取,编程时可以按消息的类型读取。
  • 消息队列允许一个或多个进程向它写入或者读取消息。
  • 每个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的。
  • IPC通信机制需要一个key值,通过key值可以在系统内获得一个唯一的消息队列标识符。key值可以是人为指定的,也可以通过ftok()函数获得
  • 如果多个进程想通过IPC对象通信,则必须找到唯一的标识,而唯一的标识是由key决定的,所以只要key知道,则就可以实现多个进程通信

ftok()函数

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

key_t ftok(const char *pathname, int proj_id);

功能: 获取项目相关的唯一的IPC键值。
pathname: 路径名
proj_id: 项目ID,非0整数(只有低8位有效)
成功则返回key值,失败返回-1。

如果使用ftok()函数获取键值,得到的键值是由ftok的第一个参数对应文件的信息和第二个参数一起决定的。
只要保证ftok()函数的第一个参数对应的文件和第二个参数值相同,则不管程序运行多少遍,得到的key值都是一样的。

创建消息队列

#include <sys/msg.h> 
int msgget(key_t key, int msgflg);

创建一个新的活打开一个已经存在的消息队列,不同的进程调用此函数,只要用相同的key值就能得到同一个消息队列的标识符。

key: IPC键值
msgflg: 标识函数的行为及消息队列的权限。可以为IPC_CREATE表示创建消息队列,或者为IPC_EXCL检测消息队列是否存在。

发送消息

#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

将消息添加到消息队列。

  • msqid: 消息队列的标识符
  • msgp: 待发送消息结构体的地址
  • msggsz: 消息正文的字节数
  • msgflg: 函数的控制属性

共享内存

共享内存允许两个或者多个进程共享给定的存储区域。

共享内存的特点:
- 共享内存是进程间共享数据的一种最快的方法。 一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。

  • 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥 若一个进程正在向共享内存去写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。

image

共享内存是进程间通信方式中效率最高的。
原因在于进程是直接在物理内存上进行操作,将物理地址映射到用户进程这,所以只要对其地址进行操作,就是直接对物理地址操作。

在ubuntu 12.04中共享内存限制值如下:
- 共享存储区的最大字节数: 32M - 每个进程最多映射的共享存储区的个数: 4096

获得一个共享存储标识符

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shflg);

功能: 创建或打开一块共享内存区

参数: - key: IPC键值 - size: 该共享存储区的长度(字节) - shmflg: 标识函数的行为及共享内存的权限。

返回值:
- 成功: 返回共享内存标识符。 - 失败: 返回-1。

共享内存映射(attch)

#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

作用: 将一个共享内存区映射到调用进程的数据段中。
参数:

  • shmid: 共享内存标识符。
  • shmaddr: 共享内存映射地址(若为NULL,则由系统自动指定),推荐使用NULL。
  • shmflg: 共享内存区的访问权限和映射条件。

返回值:
- 成功: 返回共享内存区映射地址 - 失败: 返回-1

解除共享内存映射(detach)

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

功能: 将共享内存和当前进程分离(仅仅是断开联系,并不是删除共享内存)

参数: - shmaddr: 共享内存映射地址。

成功返回0,失败返回-1.

本页由 Markdown 自动渲染 · 共 628 篇笔记