进程间通信的概念
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
进程间通信的目的
- 数据传输: 一个进程需要将它的数据发送给另一个进程。
- 资源共享: 多个进程之间共享同样的资源。
- 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
- 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的本质
进程间通信的本质就是,让不同的进程看到同一份资源。
管道
管道特点
- 管道内部自带同步与互斥机制。管道在同一时刻只允许一个进程对其进行写入或是读取操作。
- 管道的生命周期随进程。那么当所有打开该文件的进程都退出后,该文件也就会被释放掉。
- 管道提供的是流式服务。数据没有明确的分割,不分一定的报文段。
- 管道是半双工通信的。
匿名管道
匿名管道通信的原理
让父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
匿名管道通信的代码实现
- 父进程调用pipe函数创建管道
- 父进程创建子进程
- 父进程关闭写端,子进程关闭读端
//child->write, father->read
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
/* 1.父进程调用pipe函数创建管道 */
int fd[2] = { 0 };
if (pipe(fd) < 0){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
/* 2.父进程创建子进程 */
pid_t id = fork(); //使用fork创建子进程
if (id == 0){
/* 3.子进程关闭读端 */
close(fd[0]);
/* 4.子进程向管道写入数据 */
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
/* 5.父进程关闭写端 */
close(fd[1]);
/* 6.父进程从管道读取数据 */
char buff[64];
while (1){
ssize_t s = read(fd[0], buff, sizeof(buff));
if (s > 0){
buff[s] = '\0';
printf("child send to father:%s\n", buff);
}
else if (s == 0){
printf("read file end\n");
break;
}
else{
printf("read error\n");
break;
}
}
close(fd[0]); //父进程读取完毕,关闭文件
waitpid(id, NULL, 0);
return 0;
}
匿名管道总结
- 匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。
- 父子进程看到的同一份文件资源是由操作系统来维护的,当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
- 操作系统不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。
- 只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
- 匿名管道由pipe函数创建并打开。
命名管道
命名管道的原理
命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。命名管道可实现两个毫不相关进程之间的通信。
命名管道通信的代码实现
以下代码可以实现服务端(server)和客户端(client)之间的通信。
运行服务端,创建一个命名管道文件,以读的方式打开该命名管道文件,等待读取客户端发来的通信信息。
运行客户端,以写的方式打开该命名管道文件,将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
//server.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
int main()
{
/* 1.使用mkfifo创建命名管道文件 */
umask(0); //将文件默认掩码设置为0
if (mkfifo(FILE_NAME, 0666) < 0){
perror("mkfifo");
return 1;
}
/* 2.以读的方式打开命名管道文件 */
int fd = open(FILE_NAME, O_RDONLY);
if (fd < 0){
perror("open");
return 2;
}
/* 3.从命名管道当中读取信息 */
char msg[128];
while (1){
msg[0] = '\0'; //读之前将msg清空
//从命名管道当中读取信息
ssize_t s = read(fd, msg, sizeof(msg)-1);
if (s > 0){
msg[s] = '\0'; //手动设置'\0',便于输出
printf("client# %s\n", msg); //输出客户端发来的信息
}
else if (s == 0){
printf("client quit!\n");
break;
}
else{
printf("read error!\n");
break;
}
}
close(fd); //通信完毕,关闭命名管道文件
return 0;
}
//client.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#define FILE_NAME "myfifo" //让客户端和服务端使用同一个命名管道
int main()
{
/* 4.以写的方式打开命名管道文件 */
int fd = open(FILE_NAME, O_WRONLY); //以写的方式打开命名管道文件
if (fd < 0){
perror("open");
return 1;
}
/* 5.将信息写入命名管道 */
char msg[128];
while (1){
msg[0] = '\0'; //读之前将msg清空
printf("Please Enter# "); //提示客户端输入
fflush(stdout);
//从客户端的标准输入流读取信息
ssize_t s = read(0, msg, sizeof(msg)-1);
if (s > 0){
msg[s - 1] = '\0';
//将信息写入命名管道
write(fd, msg, strlen(msg));
}
}
close(fd); //通信完毕,关闭命名管道文件
return 0;
}
命名管道总结
- 命名管道在磁盘有一个简单的映像,但这个映像的大小为0,因为命名管道不会将通信数据刷新到磁盘当中。
- 命名管道可以实现派发计算任务,进程遥控和文件拷贝等。
- 命名管道由mkfifo函数创建,由open函数打开。
system V进程间通信
system V IPC是操作系统特地设计的一种通信方式,让不同的进程看到同一份由操作系统提供的资源。
共享内存的基本原理
在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
共享内存的建立与释放
共享内存的建立大致包括以下两个过程:
- 在物理内存当中申请共享内存空间。
- 将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放大致包括以下两个过程:
- 将共享内存与地址空间去关联,即取消映射关系。
- 释放共享内存空间,即将物理内存归还给系统。
共享内存通信的代码实现
以下代码可以通过共享内存实现服务端(server)和客户端(client)两个进程的通信。
运行服务端,创建共享内存,创建好后将共享内存和服务端进行关联,等待读取共享内存中的通信信息。
运行客户端,和服务端创建的共享内存进行关联,向共享内存写入数据 。
//server.c
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/study/IPC_test/server.c" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
/* 1.使用ftok和shmget函数创建共享内存 */
key_t key = ftok(PATHNAME, PROJ_ID); //获取key值
if (key < 0){
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建新的共享内存
if (shm < 0){
perror("shmget");
return 2;
}
printf("key: %x\n", key); //打印key值
printf("shm: %d\n", shm); //打印共享内存用户层id
/* 2. 关联共享内存 */
char* mem = shmat(shm, NULL, 0); //关联共享内存
/* 3.读取共享内存当中的数据并输出 */
while (1){
printf("client# %s\n", mem);
sleep(1);
}
/* 4.共享内存去关联 */
shmdt(mem); //共享内存去关联
/* 5.释放共享内存 */
shmctl(shm, IPC_RMID, NULL); //释放共享内存
return 0;
}
//client.c
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/study/IPC_test/server.c" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
int main()
{
/* 1.获取与server进程相同的key值 */
key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值
if (key < 0){
perror("ftok");
return 1;
}
/* 2.获取server进程创建的共享内存的用户层id */
int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
if (shm < 0){
perror("shmget");
return 2;
}
printf("key: %x\n", key); //打印key值
printf("shm: %d\n", shm); //打印共享内存用户层id
/* 3. 关联共享内存 */
char* mem = shmat(shm, NULL, 0); //关联共享内存
/* 4. 向共享内存写入数据 */
int i = 0;
while (1){
mem[i] = 'A' + i;
i++;
mem[i] = '\0';
sleep(1);
}
/* 5.共享内存去关联 */
shmdt(mem); //共享内存去关联
return 0;
}
共享内存总结
- 共享内存的生命周期是随内核的。
- 进行通信的各个进程,在使用ftok函数获取key值时,采用同样的路径名和和整数标识符,生成同一种key值,才能找到同一个共享资源。
- 不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启。
- 共享内存创建好后就不再需要调用系统接口进行通信了,因此共享内存是所有进程间通信方式中最快的一种通信方式。
- 共享内存的缺点是没有提供任何的保护机制。
以上为Linux进程间通信的学习总结。
原文链接:https://blog.csdn.net/chenlong_cxy/article/details/121184624