进程间通信(IPC通信)
IPC通信就是进程之间的沟通交流。进程具有独立性,但实际工作中往往会出现在一个系统中好几个
进程协同工作,那么就需要这些进程进行通信。
通信目的:
- 数据传输(一个进程将它的数据发送给另外一个进程)
- 事件通知(一个进程向另一个进程发送消息,通知它发生了某一件事情)
- 资源共享(多个进程共享痛的资源)
- 进程控制(有些进程希望控制另外一个进程截住另一个进程的异常等)
进程间通信的三种方式:
1.管道—最古老的通信方式
匿名管道
命名管道
2.System V标准接口
消息队列
共享内存
信号量
3.Posix标准接口
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
=======================================
接下来介绍前两种通信方式,管道和Ssystem v
管道(本质上是内核的一块缓冲区):传输数据资源
Linux下一切皆文件,Linux为管道提供的操作方法就是文件IO操作,即管道的使用和文件一致。
调用pipe函数的进程,系统给进程分配了两个文件描述符,即pipe函数返回的两个描述符
特性:半双工(),单向通信,所以在通信之前设置数据的流向。 (单工:数据只能在一个方向上进行,只能读或只能写;半双工:允许在数据在两个方向进行但某一时刻,只允许在一个方向进行;全双工:允许数据在两个方向上同时进程,数据可边读边写)
**匿名管道/命名管道 **
1.匿名管道
创建:
int pipe(int fd[2]) //仅用于具有亲缘关系进程之间通信,
功能:没有名字的文件,在创建管道时,返回两个文件描述符,创建子进程时,父子进程各关闭一个fd,实现管道单向流通,如上图所示
参数:整型数组
fd[0] 用于读取数据
fd[1] 用于写入数据
返回值:成功返回0,失败返回-1
对于管道,如果管道中没有数据,则阻塞等待
匿名管道特性:
1.只能用于具有亲缘关系间进程的通信
2.半双工单向通
3.管道的生命周期随进程,一般进程退出,管道释放,这也是它和普通文件最大的差异
4.管道是面向字节流传输数据的
面向字节流:数据无规则,没有明显边界,收发数据比较灵活。
5.管道自带同步与互斥属性
临界资源:大家都能访问到的公共资源就叫临界资源。
临界区:对临界资源进行操作的代码
同步:访问的可控时序性
互斥:对临界资源同一时间的唯一访问性 保护临界资源的安全。
### 2.命名管道
文件系统可见,也是一个特殊(管道类型)文件
命名管道可以应用于同一主机上的任意进程间通信
创建:
1.命令创建:mkfifo + pipe_filename
2.代码创建:int mkfifo(const char *pathname, mode_t mode);
参数:第一个是创建的管道名称; 第二个是赋予管道的权限
管道文件存在则创建失败返回EEXITST,创建成功返回0
命名管道的打开规则:
- 如果以只读打开命名管道,那么open函数将会阻塞等待*,直到有其他进程以写的方式打开这个命名管道
- 如果命名管道以读写方式打开,则不会阻塞
一个命名管道打开之后则所有特性和匿名管道完全相同。
管道的读写规则:
-
当管道中没有数据可读时:
如果描述符是默认的阻塞属性,那么读取将会阻塞挂起等待,直到有数据。 如果描述符被设置为非阻塞属性,那么读取操作将不具备条件,直接报错返回 错误码:EAGAIN
-
当管道数据写满时:
如果描述符默认的阻塞属性,那么写入操作将阻塞挂起等待,直到数据被读走 如果描述符被设置为非阻塞等待,那么写入操作将不具备条件,直接报错返回 错误码:EAGAIN
-
如果写端全部被关闭,这时候如果读取数据,读取完管道数据,然后返回0
-
当读端都被关闭,则进程哪个进程写入就会触发异常,操作系统会给进程发送13号SIGPIEPE信号
收到这个信号的进程就会退出 -
当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性
-
当要写入的数据量大于PIPE_BUF(最小是512B)时,Linux将不能保证写入原子性(所谓原子性就是指某个操作在执行期间,不被其他事件所打断)
#匿名管道和命名管道的区别
- 匿名管道由pipe创建并打开,而命名管道由mkfifo创建,用open函数打开
- 通信的两个进程结束后,匿名管道会自行消失,释放,但命名管道 的文件路径本身还存在。
system v
消息队列
消息队列实际上是操作系统在内核为我们创建的一个队列,多个进程可以向队列中添加节点获取节点来进行数据传输。
如何传输数据:
用户组织一个带有类型的数据块,其他的进程从队列中获取数据块,也就是说消息队列传输的是一个个带有类型的数据块
消息队列是一个全双工通信,可读可写。消息队列的生命周期随内核
消息队列的操作接口:
1.创建消息队列:
int msgget(key_t key, int msgflg);
key:内核中消息队列的标识(由ftok创建的key值)
msgflg:
IPC_CREAT ---------不存在则创建,存在则打开
IPC_EXCL ---------与上一个同用时,存在则报错
mode ---------- 权限
返回值:返回一个非负整数,即消息队列的标识符;失败返回:-1
2.消息队列属性控制
消息队列创建以后,可对消息队列的基本属性 进行修改(控制)
int msgctl(int msqid, int cmd,struct msqid_ds*_buf)
功能:修改消息队列属性
msqid: 消息队列标识符,即msgget的返回值
cmd(要执行的动作,有以下三个可取值):
IPC_STAT------------读取消息队列。将其存放在buf所指的结构体中
IPC_SET------------设置消息队列属性,把消息队列的当前关联值设置为msqid_ds结构中给出的值
IPC_RMID----------删除消息队列
第三个参数临时的一个结构体类型的变量,存储读取消息队列属性
3.发送数据/接收数据:
msgsnd:发送消息到消息队列中
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:将新的消息队列添加到消息队列为端msqid:消息队列标识符
maqp:指向要发送的消息队列
msgsz:接受信息 的大小,数据类型为size_t,即unsigned int
msgflg:控制当前消息队列满时或者达到系统上限时将要发生的事情,msgflag为IPC_NOWWAIT时,队列不等待,返回 EAGAIN错误
其他说明 :
消息结构在两方面必须受到 限制:
第一,它的大小必须小于系统规定的上限值
第二,它必须以long int长整数开头,接受函数将利用这个长整数确定消息的类型
消息结构参考如下
struct msgbuf
{
long mtype; // 消息类型
char mtext[1]; //存储消息的位置
}
msgrcv:从消息队列中接受数据
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
msqid : msgget返回的操作句柄
msgp: 指向准备接受的数据
msgsz:指定数据的大小,即mtext的大小,以字节为单位
msgtyp:指定接收数据类型
msgtyp等于0,接收队列中第一个节点,不分类型
msgtyp大于0,接收指定类型数据块的第一个节点
msgtyp小于0,接收小于msgtype绝对值类型的第一个节点
msgfalg:操作选项
设置为IPC_NOWAIT,若消息队列没有消息,返回ENOMSG报错
设置为MSG_NOERROR,消息大小超过msgsz时被截断。
释放消息队列:
指令:
ipcs :显示IPC资源
-q 查看消息队列
-m 查看共享内存
-s 查看信号量
ipcrm 手动删除ipc
共享内存
共享内存是常用的进程间通信,两个进程可以直接共享访问同一块内存区域
进程间通信最快的方式,少了从用户态向内核态拷贝数据的过程
共享内存是在内存中单独开辟的一段内存空间,两个进程再使用共享内存时,需要在进程地址空间与共享内存之间建立联系,如上图所示。共享内存主要用于进程间大量数据的传输。
共享内存的使用:
1.创建共享内存
shmget:
int shmget(key_t key, size_t size, int shmflg);
key:操作系统ipc标识符
size:要创建的共享内存的大小
shmflg:IPC_CREAT|IPC_EXCL|0664
返回值:成功返回操作句柄, 失败 返回-1
2.将共享内存映射到虚拟地址空间
shmat:
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid:操作句柄
shmaddr:映射起始地址,为NULL时,操作系统分配地址
shmflg: SHM_RDONLY--只读 否则为 读写
返回值:映射的虚拟地址空间的首地址
3.内存数据操作
4.完成
1> 解除映射关系
shmdt:
int shmdt(const void *shmaddr);
shmaddr : 首地址
返回值:成功0 失败-1
2> 删除共享内存
如果有进程依然与共享内存保持映射练习,共享内存将不会被立即删除,而是等最后一个映射断开后再删除
在这期间,将拒绝其他进程映射。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:句柄
cmd:IPC_RMID 删除
buf: 用于接收共享内存描述性息,不关心可以置空
信号量
进程间通信方式之一,主要用于实现进程间的同步与互斥(进程/线程安全概念),防止并发访问共享资源*
信号量本质:具有一个等待队列的计数器(代表现在还有没有资源可以使用)
1.同步:
保证对临界资源访问的时序可控性
2.互斥:
对临界资源同一时间的唯一访问性
多个进程同时操作一个临界资源的时候,就需要通过同步与互斥机制来实现进程之间对
临界资源的安全访问
当信号量没有资源可用(资源计数器为0)时,需要阻塞等待
如何实现同步?
只有信号量资源计数从0转变为1的时候,会通知别人打断阻塞等待,竞争后再去操作临界资源
也就是说前者释放资源(计数器+1),之后才能被后者获取资源(计数器-1)进行操作临界资源。
如何实现互斥?
信号量想要实现互斥,指计数器只能是0或者1(一元信号量)。
进程在操作临界资源之前,先获取信号量资源,判断是否能对临界资源进行操作,如果没有信号量(计数器为0),则需要等待,当其他进程释放信号量计数器会加一,等待的所有进程会被唤醒去获取信号量
注意:信号量作为进程间通讯方式,所以信号量实际上也是一个临界资源
信号量的操作:
- semget:创建信号量
int semget(key_t key, int nsems, int semflg);
参数:
key:信号量的名字
nsems:创建的信号的个数,用数组存放
semflag:权限标识,与mode权限表示是一样的
返回值:成功返回信号量标识符,非负整数,失败返回-1; - semctl:控制信号量集
int semctl(int semid, int semnum, int cmd, …);
参数:
semid:信号量标识符,即semget的返回值
semnum:信号量集合中信号量的序号,即信号量的下标,若操作整个信号,则此参数无意义
cmd(要执行的动作,有以下可取值):
IPC_STAT-----------把semid_ds结构中的的数据设置为信号量集当前的关联值
IPC_SET------------设置信号量 属性,把消息队列的当前关联值设置为semid_ds结构中给出的值
IPC_RMID----------删除信号量
SETVAL SETALL 设置信号量的计数器初值
semop:操作信号量集合
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数:
semid:要操作的信号量集合的ID
第二个参数:
{
unsigned short sem_num; //信号量下标
short sem_op ; //信号量操作,pv操作,p操作-1,v操作+1;
short sem_flg; //操作标识符,IPC_NOWAIT(信号量集合操作不能执行时,调用立即返回)/SET_UNDO(进程退出后,撤销进程对信号量集合的操作)
}