进程间通信
(1)什么是进程间通信?
进程间通信(IPC)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。
(2)进程间通信的目的是什么?
进程间通信的目的一般有以下几点:
- 数据传输:一个进程需要将它的数据发送给别的进程
- 资源共享:多个进程间需要共享相同的资源
- 进程控制:一个进程需要控制别的进程的执行(如Debug进程),此时控制进程需要拦截另外一个进程的所有陷入和异常,并及时知道它的状态改变
- 通知事件:一个进程需要给另一个(组)进程发送消息,通知它(们)发生了某种事件(如子进程的终止通知父进程)
(3)进程间通信经历了哪几个阶段?
ps:想要快速学习Linux进程间通信接口接口使用的朋友,可以跳过这个问题
- 管道(pipe)是第一个广泛使用的IPC形式,既可以在程序中使用,也可以从shell中使用。管道的问题在于它们只能在具有共同祖先(父子进程关系)的进程间使用,最初的管道称为匿名管道,但是命名管道的出现解决了这个问题。
- System V进程间通信。System V消息队列是在20世纪80年代加入到System V内核中的。后面有陆陆续续的添加了这几种带有System V通信特色的进程间通信方式:共享内存,信号量。直到现在System V进程间通信系列的接口也是比较主流的。
- Posix进程间通信。Posix实时标准最初发布了Posix消息队列,它们可以用在同一主机上的不同进程而不受亲缘关系干扰(事实上只有匿名管道是受的),后来又加入了Posix信号量和Posix共享内存区。
- 互斥锁和条件变量事有Posix线程标准定义的两种同步形式。尽管往往用于线程间的同步,但是它们也能提供不同进程之间的同步。
- 读写锁是另外一种形式的同步。
(4)IPC对象的持续性分为哪些类型?
下图汇总了各种类型IPC对象的持续性:

(5)进程间通信的方法有哪些?分别怎么用?
进程间通信的方法有:
- 匿名管道
- 命名管道
- System V 消息队列
- System V 共享内存
- System V 信号量
- Posix系列:消息队列,共享内存,信号量,互斥量,条件变量,读写锁
PS:下面我只讲解管道和System V系列的进程间通信方法
1.管道
定义:从一个进程连接到另一个进程的数据流称为管道。(‘|’是命令行中管道的符号)
eg:
命令: who | wc -l

创建匿名管道:
函数原型:int pipe(int fd[2])
参数说明:fd是文件描述符数组,fd[0]表示读端,fd[1]表示写端。
返回值:成功则返回0,失败则返回错误码。

使用实例:从键盘读取数据写入管道,然后从管道读取并写到屏幕上。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define ERR_EXIT(n) \ //封装一个宏结构,下面的代码都可以使用
do\
{\
perror(n);\
EXIT(EXIT_FAILURE);\
}while(0)
int main(void)
{
int fd[2];
char buf[100];//缓冲区
int len;
if( pipe(fd) != 0)
ERR_EXIT("pipe");
//从键盘读
while(fgets(buf, 100, stdin)){
len = strlen(buf);
//写入管道
if( write(fd[1], buf, len) != len)
ERR_EXIT("write to pipe");
memset(buf, 0x00, sizeof(buf));//清空缓冲区
//从管道读
if((len = read(fd[0], buf, 100)) == -1)
ERR_EXIT("read to pipe");
//写到标准输出,即屏幕,1号文件描述符即标准输出
if(write(1, buf, len) != len)
ERR_EXIT("write to stdout");
}
}
管道读写规则
1.当没有数据可读时
- O_NONBLOCK disable:read调用阻塞,进程暂停执行,持续等待有数据到来
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN
2.当管道满时
- O_NONBLOCK disable:write调用阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1.errno值为EAGAIN
3.如果所有管道写端对应的文件描述符被关闭,则read返回0
4.如果所有管道读端对应的文件描述符被关闭,则write操作会产生吸纳后SIGPIPE,进而可能导致进程退出
5.当要写入的数据量不大于PIPE_BUF时,Linux保证写入的原子性,否则不再保证
管道特点
- 只能用于具有共同祖先的进程之间进行通信,通常一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以应用该管道
- 管道提供流式服务
- 一般,进程退出,管道释放,所以管道的生命周期跟随进程
- 一般,内核会对管道操作进行同步和互斥
- 管道是半双工的,数据只能往一个方向流动,需要双方通信时,需要建立两个管道(看图)

命名管道
- 如果我们想在不相关的进程之间交换数据, 可以使用FIFO文件来做这项工作,它经常被称作命名管道
- 命名管道是一种特殊类型的文件
在命令行上创建:
mkfifo filename
在程序里创建可以依靠函数:
函数原型:int mkfifo(const char *filename, mode_t mode);
使用实例:
int main(void)
{
mkfifo("p1",0644);//创建一个名字为p1的FIFO文件,权限为644
return 0;
}
匿名管道和命名管道的异同
- 匿名管道用pipe函数创建并打开
- 命名管道用mkfifo函数创建,用open打开
- FIFO和pipe只要创建工作完成后,它们具有相同的语义
2.消息队列
介绍
消息队列可以认为是一个消息链表。不同的拥有足够写权限进程(线程)可以往队列中放置消息,拥有读权限的进程(线程)也可以从消息队列中取消息。每一个消息都是一个记录记录,它由发送者赋予一个优先级。在某个进程往一个队列中写入消息之前,并不需要另外某个进程在该队列上等待消息的到达,这跟管道和FIFO是有区别的,对于后两者来说,除非读者已经存在,否则先有写者是没有意义的。
一个进程可以往某个队列中写入一些消息,然后终止,再让另外某个进程在以后某个时刻读出这些消息。消息队列具有随内核的持续性,这跟管道和FIFO不同,当一个管道或者FIFO最后一次关闭发生时,仍在该管道或FIFO上的数据将被丢弃。
消息队列接口函数
1>消息队列的创建
函数原型:
- int msgget(key_t key, int msgflg);
参数:
- key:某个消息队列的名字
- msgflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志相同
返回值:成功返回一个非负整数(消息队列的标识码)失败返回-1
2>消息队列的控制
函数原型:
- int msgctl(int msqid, int cmd, struct msqid_ds *buf)
参数:
- msgid:消息队列标识码
- cmd:将要采取的动作(见下表)
返回值:成功返回0,失败返回-1

3>消息的添加
函数原型:
- int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
参数:
- msgid:消息队列标识码
- msgp:指针,指向准备发送的数据的起始地址
- msgsz:所发送消息的长度(长度不包含保存消息类型的那个长整形)
- msgflg:控制着当前消息队列满,或到达系统上限时将要发生的事情
返回值:成功返回0,失败返回-1
函数说明:
- 消息队列结构在两方面受到制约:
1.它必须小于系统限定的上限值
2.它必须以一个long int开始,接收者函数利用这个长整形确定消息的类型
- 消息结构的形式如下:
struct msgbuf{
long mtype;
char mtext[you want size];
};
4>消息的接收
函数原型:
- ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
参数:
- msgid:消息队列标识码
- msgp:指向准备接收的消息的指针
- msgsz:要接收的消息长度(不包含消息结构中的long int)
- msgtype:控制接收优先级的简单形式
- msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事
返回值:成功则返回实际放到接收缓冲区中的字符个数,失败返回-1
msgtype和msgflg参数说明:
- msgtype = 0 返回消息队列第一条消息
- msgtype > 0 返回消息队列第一条类型等于msgtype的消息
- msgtype < 0 返回消息队列第一条类型<=msgtype绝对值的消息,并且是满足条件的消息类型最小的消息
- msgtype > 0 && msgflg == MSGEXCEPT,接收类型不等于msgtype的第一条消息
- msgflg = IPC_NOWAIT 队列没有可读消息不等待,返回ENOMSG错误
- msgflg = MSG_NOERROR 消息大小超过msgsz时被截断
命令行操作IPC资源
- ipcs:显式IPC资源
- ipcrm 手动删除IPC资源
使用实例:
ipcs -q 查看系统中的自定义消息队列
ipcrm -Q 消息队列标识号 :删除指定的消息队列
ipcs -m:查看系统中的共享内存段
ipcrm -M 共享内存的ID :删除用指定ID创建的共享内存
ipcs -s:查看系统中的信号量集
ipcrm -S 信号量集的ID:删除用指定ID创建的信号量集
3.共享内存
共享内存是最快的IPC形式。因为一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,也即进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存示意图:

操作共享内存的函数接口
1>共享内存的创建
函数原型
- int shmget(key_t key, size_t size, int shmflg);
参数:
- key:共享内存的名字
- size:共享内存大小
- shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即共享内存段的标识码;失败返回-1.
2>将共享内存段连接到进程地址空间
函数原型:
- void *shmat(int shmid, const void *shmaddr, int shmflg)
参数:
- shmid:共享内存标识
- shmaddr:指定挂载的地址
- shmflg:它的两个可能取值是SHM_RDN和SHM_RDONLY
返回值:成功返回一个指向共享内存的指针,失败返回-1.
参数说明:
- shmaddr为NULL,由系统自动选择一个地址
- shmaddr不为NULL,且shmflg无SHM_RND标记,则以shmaddr为挂载地址
- shmaddr不为NULL,且shmflg设置了SHM_RND标记,则挂载的地址会自动向下调整为SHMLBA的整数倍
- shmflg设置为SHM_RDONLY,表示挂载操作用来只读共享内存
3>将共享内存段和当前进程脱离
函数原型
- int shmdt(const void *shmaddr)
参数
- 由shmat函数所返回的指针
返回值:成功返回0,失败返回-1
注意:将共享内存段和当前进程脱离不等于删除共享内存段
4>控制共享内存
函数原型
- int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数
- shmid:由shmget返回的共享内存标识码
- cmd:将要采取的动作,见下表。
- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1

注意:共享内存没有同步和互斥
4.信号量
注:信号量主要用来同步和互斥的。
进程互斥
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此个进程间竞争使用这些资源,这种关系称为进程的互斥。
- 系统中某些资源一次只允许一个进程使用,这些资源被称作互斥资源或者临界资源。许多物理设备都是临界资源,例如打印机。
- 在进程中涉及到互斥资源的程序段称为临界区
进程同步
- 进程同步是指多个进程需要互相配合才能共同完成一项任务。
下面是一个进程同步的例子:

信号量通常要和P、V原语结合使用
信号量本质上是一个资源计数器:
struct semaphore{
int value;
pointer_PCB queue;
}
P原语伪代码
P(s)
{
s.value--;
if(s.value < 0)
{
将给进程状态置为等待状态
将该进程的PCB插入相应的等待队列s.queue队尾
}
}
V原语伪代码
V(s)
{
s.value++;
if(s.value <= 0)
{
唤醒相应等待队列s.queue中等待的一个进程
改变其状态为就绪态,并插入就绪队列
}
}
信号量集函数接口
1>创建和访问一个信号量集
函数原型
- int semget(key_t key, int nsems, int semflg)
参数
- key:信号量的名字
- nsems:信号量集中信号量的个数
- semflg:由九个权限标志构成
返回值:成功放回一个非负整数,即信号量集的标识码;失败返回-1
2>信号量集的控制
函数原型
- int semctl(int semid, int semnum, int cmd, …)
参数
- semid:信号量集标识码
- semnum:信号量集中信号量的序号
- cmd:将要采取的动作,见下表
注:最后一个参数根据命令不同而不同
返回值:成功返回0,失败返回-1

3>实现对信号量的PV操作
函数原型
- int semop(int semid, struct sembuf *sops, unisgned nsops)
参数
- semid:信号量的标识码,也就是semget函数的返回值
- sops:指向一个规定结构的指针
- nsops:信号量的个数
参数说明:
sembuf结构体:
struct sembuf{
short sem_num;
short sem_op;
short sem_flg;
}
sem_num:信号量的编号,从0开始,一般PV操作这个参数都写0
sem_op:信号量一次PV操作时加减的数值,一般P操作这个参数写-1;V操作这个参数写1
sem_flg:这个参数有三个可选值:0,IPC_NOWAIT,SEM_UNDO。一般都写0

本文详细介绍了Linux进程间通信(IPC)的概念、目的和历史发展,包括匿名管道、命名管道、System V和Posix系列的通信方式,如消息队列、共享内存、信号量等。通过具体函数接口的解析,阐述了各种通信方法的使用和特点,帮助读者深入理解Linux进程间通信的本质和应用场景。
570

被折叠的 条评论
为什么被折叠?



