Linux 下进程通信

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

为什么需要进程间通信(IPC)?因为有些功能需要多进程间共同完成。比如分享功能,多进程服务器等。每个进程各自有不同的地址空间,任何一个进程的全局变量在另一个进程中都看不到。所以进程间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区中,进程2再从内核缓冲区中把数据读走。


一、IPC原理图

二、通信方式

1.匿名管道

调用pipe函数时再内核中开辟一块缓冲区用于通信,有一个读端,一个写端。pipefd[0]指向管道的读端,pipefd[1]指向管道的写端。管道在用户程序看来就是一个打开的文件。通过read() write()函数就可以往管道里读写数据。   

管道通信的过程为: 

  1. 父进程调用pipe开辟管道,得到两个文件描述符,分别指向管道的两端
  2. 父进程调用fork创建子进程,那么子进程也会拷贝父进程的两个文件描述符    
  3. 父进程关闭管道读端,子进程关闭管道写端。父进程往管道里写,子进程可以从管道中读。管道是用环形队列实现的。

 既然是缓冲区,就会有写满或没有数据的情况。

  1.  写端关闭,读端不关闭。管道中剩余的数据都被读取后,再次read会返回0。就像读到文件末尾一样。
  2. 读端关闭,写端不关闭。该进程会受到SIGPIPE,通常导致进程异常终止。
  3. 写端不关闭,但是也不写数据,读端不关闭。管道中剩余的数据都被读取之后再次read会被阻塞,直到管道中有数据了才会重新读取数据并返回。
  4. 读端不关闭,但是也不读数据,写端不关闭。当写段写满了之后再次write会阻塞,知道管道中有空位置了才会写入数据并重新返回。

匿名管道有以下缺陷:

  1. 两个进程通过一个管道时,为了防止数据混乱,只能实现单向通信,如果想双向通信必须再创建一个管道。
  2. 只能用于具有亲缘关系的进程间通信。例如父子,兄弟进程。       

2.命名管道

FIFO的出现解决了匿名管道只能用于有亲缘关系的IPC问题。不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储文件系统中。命名管道是一个设备文件,因此即使进程与创建FIFO的进程不存在亲缘关系,只要访问该路径,也可以同个FIFO相互通信。

#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
/**
 * @brief 创建FIFO
 * 
 * @param pathname 路径名
 * @param mode 权限
 * @param dev 设备值 取决于文件创建的种类
 * @return int success : 0 ; error : -1
 */
int mknod(const char* pathname, mode_t mode, dev_t dev);
/**
 * @brief 创建FIFO
 * 
 * @param pathname 文件路径
 * @param mode 权限
 * @return int int success : 0 ; error : -1
 */
int mkfifo(const char* pathname, mode_t mode);

特点:

  1. 命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。所以使用命名管道的时候必须先open将其打开。
  2. 命名管道可以用于任何两个进程间的通信,不管者两个进程有无关系。

3、消息队列

对于消息队列的操作,可以类比为这么一个过程:A有个东西要给B,因为某些原因A不能当面直接给B,这时候他们需要借助第三方托管(如银行),A找到某个具体地址的建设银行,然后把东西放到某个保险柜中(1号保险柜),对于B而言,要想成功取出A的东西,必须保证去同一地址的同一间银行取东西,并且只有1号保险柜的东西才是给自己的。

所以,A和B要想交换东西,涉及到几个重要的信息:地址、银行、保险柜密码。

而在消息队列中,键(key)值相当于地址,消息队列标识符相当于具体的某个银行,消息类型相当于保险柜密码。同一个键(key)值可以保证是同一个消息队列,同一个消息队列标识符才能保证不同的进程可以相互通信。同一个消息类型才能保证某个进程取出的是对方的消息。

键(key) : 值(value)System V提供的进程间通信机制需要一个key值,通过key值可以在系统内
获得一个唯一的消息队列标识符。key值可以人为指定,也可以通过ftok()函数获得。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
/**
 * @brief 获得消息队列的键值
 * 
 * @param pathname 路径名
 * @param proj_id 项目ID,非0整数
 * @return key_t success : key; error : -1
 */
key_t ftok(cong char* pathname, int proj_id);
/**
 * @brief 创建或打开一个消息队列。不同的进程调用此函数,只要
 * 用相同的key就能得到一个相同的消息队列标识符
 * 
 * @param key ftok()返回的key值
 * @param msgflg 标识函数的行为及消息队列的权限
 * @return int success : 标识符; error : -1
 */
int msgget(key_t key, int msgflg);

将新消息添加到消息队列:

/**
 * @brief 消息个数
 * 消息类型必须是长整型的,而且必须是结构体类型的第一个成员
 * 类型下面是消息正文,正文可以有多个成员,任意数据类型。
 * 至于这个结构体类型叫什么名字,自行定义,没有明文规定
 */
typedef struct _msg
{
    long mtype;
    char mtext[100];
}MSG;
/**
 * @brief 将新消息添加到消息队列中
 * 
 * @param msqid 消息队列标识符
 * @param msgp 待发送消息结构体的地址
 * @param msgsz 消息正文的字节数
 * @param msgflg 函数的控制属性
 *               0 : 调用阻塞知道条件满足位置
 *       IPC_NOWAIT: 若消息没有立即发送则调用该函数的进程会立即返回
 * @return int success : 0; error : -1
 */
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);

从消息队列中接收一个消息:

/**
 * @brief 从消息队列中接收一个消息,一旦接收成功,则消息会从队列中删除
 * 
 * @param msqid 消息队列标识符
 * @param msgp 存放消息结构体的地址
 * @param msgsz 消息正文的字节数
 * @param msgtyp 消息的类型,有以下几种类型
 *             = 0 : 返回队列中的第一个消息
 *             > 0 : 返回消息类型为msgtyp的消息
 *             < 0 : 返回队列中消息类型值小于或等于msgtyp绝对值的消息,若有
 *                   若干个,则返回最小的
 * 如果有多条消息,则遵循先进先出原则  
 * @param msgflg 控制属性
 * @return ssize_t success : 读取消息的长度; error : -1
 */
ssize_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);

消息队列特点:

  1. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次数读取,编程时可以按消息类型读取。
  2. 消息队列允许一个或多个进程向它写入或读取消息。
  3. 从队列中读出消息,队列中都会删除对应的消息。
  4. 每个消息队列以消息队列标识符为区分,系统中唯一。
  5. 消息队列时消息的链表,存放在内存中,由内核维护。只有重启或人为删除消息队列时,该消息队列才会被删除。 

4、共享内存

共享内存是进程间通信中最简单的方式之一。共享内存允许多个进程访问同一块内存,就如同malloc()函数向不同的进程返回了指向同一块物理内存区域的指针。当一个进程改变了这块地址内容的时候,其他进程都会觉察到这个更改。

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

/**
 * @brief 创建或打开一块共享内存区
 * 
 * @param key 键值 ftok()的返回值
 * @param size 共享存储段的大小(字节)
 * @param shmflg 权限
 * @return int 成功 : 标识符 失败 : -1
 */
int shmget(key_t key, size_t size, int shmflg);
/**
 * @brief 将一个共享内存段映射到调用进程的数据段中。让
 *         进程和共享内存建立一种联系。
 * 
 * @param shmid 共享内存标识符
 * @param shmaddr 共享内存映射地址 为NULL则由系统自动制定
 * @param shmflg 访问权限
 * @return void* 成功 : 共享内存段映射地址 失败 -1
 */
void* shmat(int shmid, const void* shmaddr, int shmflg);
/**
 * @brief 将共享内存和当前进程分离,不删除共享内存。
 * 
 * @param shmaddr 共享内存映射地址
 * @return int 成功 0; 失败 -1
 */
int shmdt(const void* shmaddr);

共享内存特点:

  1. 是进程间共享数据的一种最快、效率最高的方法。
  2. 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥,一般和信号量配合使用。若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。

5、信号量 

信号量的主要作用是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。信号量的值为正的时候,说明它空闲,所测试的线程可以锁定而使用它,若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,需要一种方法,可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。

临界区域是指指令数据更新的代码需要独占地执行,而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个进程或线程访问它。

信号量是一个特殊的变量,程序对齐访问都是原子操作,且只允许对它进行等到(即P)和发送即(V)信息操作。

当对低开销、短期、中断上下文加锁,优先考虑自旋锁。当对长期、持有所需要休眠的任务,优先考虑信号量。最简单的信号量就是只能取 0 和 1的信号量,这也是心好累最常见的一种方式,叫做二进制信号量。而可以取多个正整数的信号量则被成为通用信号量。

P(sv) 如果sv的值大于零,就给它减一;如果它的值为0,就挂起该进程的执行

V(sv) 如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有其他进程因等待sv而挂起,就给sv加1。

用户态进程使用的信号量

  • POSIX信号量
    无名信号量。其值保存在内存中。
    有名信号量,其值保存在文件中,可以用于线程也可以用于进程间的同步。
  • SYSTEM V信号量

两者的差别在于:

  1. 对于POSIX来说,信号量是个非负整数,常用于线程间同步。而SYSTEM V信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体。信号量不过是它的一部分 。常用于进程间同步。
  2. POSIX信号量是简单的,SYSTEM V信号量则是比较复杂的。


总结

参考洋洋的IPC通信思维导图,仅供梳理学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值