进程通信
引言
进程之间为何需要通信?
- 概述中第六个问题提到,如果一个C/Java程序想要使用matlab来分析数据,那么程序之间怎么传输数据?—正是进程通信
- 其他应用:多进程排序,排序还需要多进程?如果1万条数据,那么直接一个函数排序就OK。如果10亿条呢?—使用多进程排序算法,需要使用进程间通信。
- 网络服务器大多都是多进程的,多个进程之间是需要传递数据的。
思考如何实现进程通信
很容易想到:使用文件,文件大多都保存在磁盘中,磁盘读取速度太慢了,有没有快一点的实现方案?
既然磁盘慢,那么内存总比磁盘块吧,所以能不能在内存中引入进程间通信机制?
1. 消息传递
简介
上图为内存的抽象,我们在内存中开辟一部分空间,该部分空间用于保存消息。
何为消息?可以理解成进程的交流语言。
该方案:在内核空间中开辟一段地址空间,用来保存进程之间通信的消息
实现
操作系统提供两个系统调用:
- send(P, msg), 发送msg到进程P
- receive(Q,msg), 从进程Q接收消息存到msg
提供了两种实现方式:
- 阻塞式(同步):发出系统调用的进程变为阻塞进程,直到消息被对方收到或接受到对方的消息。
- 非阻塞式(异步):消息发送或接受成功与否均立即返回。
消息定长与变长:
- 定长容易实现,不易使用
- 变长容易使用,不易实现
上文提到在内核中开辟一段空间用来保存消息,那么这里的数据结构是怎样的?
- 消息队列:其是一个队列。
- 对消息队列有写权限的进程可以向其中按照规则添加新消息。
- 对消息队列有读权限的进程可以从中按照规则读走消息。
消息队列编程
头文件:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
相关函数:
int msgget(key_t key, int msgflg); 返回消息队列ID
int msgrcv(int msqid, struct msgbuf* msgp, int msgsz, long msgtyp, int msgflg); 从队列接受消息
int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg); 向队列发送消息
int msgctl(int msgqid, int cmd, struct msqid_ds *buf); 向队列发送控制命令,cmd:IPC_STAT, IPC_SET, IPC_RMID。
对于每个消息队列,内核维护一个如下的结构:
struct msqid_ds {
struct ipc_perm msg_perm; /* operation permission struct */
struct msg *msg_first; /* ptr to first message on q */
struct msg *msg_last; /* ptr to last message on q */
unsigned short msg_cbytes; /* current # bytes on q */
msgqnum_t msg_qnum; /* # of messages on q */
msglen_t msg_qbytes; /* max # of bytes on q */
pid_t msg_lspid; /* pid of last msgsnd */
pid_t msg_lrpid; /* pid of last msgrcv */
time_t msg_stime; /* last msgsnd time */
time_t msg_rtime; /* last msgrcv time */
time_t msg_ctime; /* last change time */
};
函数讲解
这部分比较无聊,忽略,有兴趣的读者自行查资料。
2. 共享内存
上图为共享内存的示意图,从图中可以看出,其与消息传递类似,也是在内核空间开辟一段地址空间,称为共享内存。
其实现原理:
- 需要通信时直接读写共享存储区即可
- 但是同步问题需要程序员自行处理
Linux中的共享内存:
int shmget(key, size, flags)创建或获取共享内存
int shmat(shmid, addr, flag) 将shmid对应的共享内存绑定到本进程的地址空间,addr为指向共享内存的指针
int shmdt(shmid) 将共享内存解绑
3.管道
管道:
- 简单、高效、被大量操作系统采用
- 特殊的文件或缓冲区,大小一般固定
- 先入先出(FIFO)
- 被大量操作系统用于将一个程序的输出重定向为另一个程序的输入
如:ls -l /etc | less
该命令将ls程序的输出重定向为less程序的输入,从而实现一页一页的显示ls数据,这是一个管道的实例,也是一个很简单的进程通信实例。
管道的读写特征:
- 如管道已满,则试图写的进程被阻塞,直到有进程将部分数据取走
- 如管道为空,则试图读的进程被阻塞,直到有进程写入新的数据
- 如无人使用管道的另一侧,则阻塞的进程被唤醒
下面看一个实例:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
int pipdf[2];
int pid;
char recv[32];
pip(pipfd); //创建一个管道,管道的两端分别时pipfd[0],pipfd[1]
switch(pip=fork()){
case -1:
perror("fork");
exit(1);
case 0:
close(pipfd[0]); //将用不到的fd关掉
FILE *out=fdopen(pipfd[1], "w"); //打开pipfd[1]作为子进程的输出端
fprintf(out, "Hello World\n"); //向管道中输出数据
break;
case 1:
cloes(pipfd[1]);
FILE *in=fdopen(pipfd[0], "r"); //打开管道的另一侧
fscanf(in, "%s", recv); //从管道的另一侧将刚才的数据读进来
printf("%s", revv); //打印到标准输出
break;
}
}
小结
本节内容不多,除了管道以外其他的可以暂时当作了解内容。
主要记住:
- 实现进程通信的三种方式:消息传递、共享内存、管道
- 管道的使用方法以及原理(利用fd,即文件描述符)
如果觉得写的不错,对读者有帮助,可以给笔者点个赞,鼓励一下哦~