IPC 进程间通信 interprocess communicate
IPC(Inter-Process Communication),即进程间通信,其产生的原因主要可以归纳为以下几点:
进程空间的独立性
-
资源隔离:在现代操作系统中,每个进程都拥有自己独立的代码和数据空间。这种隔离机制确保了进程之间的安全性和稳定性,但同时也使得进程间无法直接访问对方的资源。
-
资源无法共享:由于进程空间的独立性,一个进程无法直接读取或修改另一个进程的内存空间中的数据。这种情况下,如果需要进行数据交换或共享资源,就需要通过特定的机制来实现。
进程间协作的需求
-
数据交换:在多进程操作系统中,不同的进程可能需要交换数据以完成共同的任务。例如,一个进程可能负责生成数据,而另一个进程则负责处理这些数据。为了实现这种数据交换,就需要使用IPC机制。
-
资源共享:进程间通信还允许进程共享资源,如文件、数据库、内存等。通过IPC,进程可以协调对共享资源的访问,避免冲突和竞争条件。
-
任务协调:在分布式系统或并发编程中,多个进程可能需要协同工作以完成复杂的任务。IPC提供了进程间同步和互斥的机制,确保任务能够按照预定的顺序和条件执行。
IPC的必要性
-
提高系统并发性:通过IPC,多个进程可以并行处理不同的任务,从而提高系统的并发处理能力。
-
优化资源利用:IPC允许进程共享资源,减少了资源的重复分配和浪费,提高了资源利用率。
-
实现复杂功能:在构建复杂的软件系统时,往往需要多个进程协同工作。IPC为这些进程之间的通信和协作提供了必要的支持。
IPC的实现方式
IPC的实现方式多种多样,包括但不限于以下几种:
-
管道(Pipe):一种单向通信方式,常用于具有亲缘关系的进程间通信。
-
消息队列(Message Queue):允许多个进程从同一个队列中读取数据,具有独立性和异步性。
-
共享内存(Shared Memory):一段可以被多个进程同时访问的物理内存区域,提高了进程间的数据交换效率。
-
信号(Signal):一个进程向另一个进程发送信号来传递某种信息或通知某个事件的发生。
-
套接字(Socket):用于不同主机之间的进程通信,提供了网络通信的能
管道==》无名管道、有名管道
无名管道 ===》pipe ==》只能给有亲缘关系进程通信
有名管道 ===》fifo ==》可以给任意单机进程通信
管道的特性:
1、管道是 半双工的工作模式(收或者发)
2、所有的管道都是特殊的文件不支持定位操作。不支持lseek->> fd fseek ->>FILE*
3、管道是特殊文件,读写使用文件IO。fgets,fread,fgetc,
一般情况下使用open,read,write,close;
主要看是否是二进制文件,如果是文本文件使用fges之类的标准io。
注意事项:
1,读端存在,一直向管道中去写,超过64k,写会阻塞。(写的快,读的慢)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int pipefd[2]={0};
int ret = pipe(pipefd);
if(-1 == ret)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid>0)
{
char buf[]="hello,world";
close(pipefd[0]);
sleep(3);
write(pipefd[1],buf,strlen(buf));
}
else if(0 == pid)
{
close(pipefd[1]);
char buf[100]={0};
read(pipefd[0],buf,sizeof(buf));
printf("pipe %s\n",buf);
}
else
{
perror("fork");
exit(1);
}
return 0;
}
2,写端是存在的,读管道,如果管道为空的话,读会阻塞(读的快,写的慢)
读阻塞和写阻塞都是正常情况。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int pipefd[2]={0};
int ret = pipe(pipefd);
if(-1 == ret)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid>0)
{
char buf[1024]={0};
close(pipefd[0]);
memset(buf,'a',1024);
int i = 0 ;
for(i=0;i<65;i++)
{
write(pipefd[1],buf,sizeof(buf));
printf("i is %d\n",i);
}
}
else if(0 == pid)
{
close(pipefd[1]);
char buf[100]={0};
// read(pipefd[0],buf,sizeof(buf));
// printf("pipe %s\n",buf);
while(1)sleep(1);
}
else
{
perror("fork");
exit(1);
}
return 0;
}
3.管道破裂,读端关闭,写管道。
(会导致写关闭,接收端关闭,导致发送端关闭)类似于段错误
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int pipefd[2]={0};
int ret = pipe(pipefd);
if(-1 == ret)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid>0)
{
char buf[]="hello,world";
close(pipefd[0]);
sleep(3);
// 管道破裂 gdb 跟踪
write(pipefd[1],buf,strlen(buf));
printf("aaaa\n");
}
else if(0 == pid)
{
close(pipefd[1]);
close(pipefd[0]);
}
else
{
perror("fork");
exit(1);
}
return 0;
}
4. read 0 ,写端关闭,如果管道没有内容,read 0 ;
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int pipefd[2]={0};
int ret = pipe(pipefd);
if(-1 == ret)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid>0)
{
char buf[]="hello,world";
close(pipefd[0]);
write(pipefd[1],buf,strlen(buf));
write(pipefd[1],buf,strlen(buf));
close(pipefd[1]);
}
else if(0 == pid)
{
close(pipefd[1]);
char buf[5]={0};
while(1)
{
//memset(buf,0,5);
bzero(buf,sizeof(buf));
int rd_ret = read(pipefd[0],buf,sizeof(buf)-1);
if(rd_ret<=0)
{
break;
}
printf("pipe %s\n",buf);
}
}
else
{
perror("fork");
exit(1);
}
return 0;
}
if(rd_ret<=0)
{
break;
}
如果读端进程调用read()
函数从管道中读取数据,并且管道已经为空(即没有任何待读取的数据),同时写端已经被关闭,那么read()
函数将会立即返回,并且返回值为0。这个返回值0是一个特殊的信号,它告诉读端进程,管道的写端已经被关闭,并且管道中没有更多的数据可以读取了。
使用框架:
创建管道 ==》读写管道 ==》关闭管道
1、无名管道 ===》管道的特例 ===>pipe函数
特性:
1.1 亲缘关系进程使用
1.2 有固定的读写端
流程:
创建并打开管道: pipe函数
先创建管道再fork();
关闭管道1或者0用,close(xx);
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建并打开一个无名管道
参数:pipefd[0] ==>无名管道的固定读端
pipefd[1] ==>无名管道的固定写端
返回值:成功 0
失败 -1;
注意事项:
1、无名管道的架设应该在fork之前进行。
无名管道的读写:===》文件IO的读写方式。
读: read()
写: write()
关闭管道: close();
通过管道的方式实现文件的复制:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int pipefd[2]={0};
int ret = pipe(pipefd);
if(-1 == ret)
{
perror("pipe");
exit(1);
}
pid_t pid = fork();
if(pid>0)
{
close(pipefd[0]);
int fd = open("/home/linux/1.png",O_RDONLY);
if(-1 == fd)
{
perror("open");
exit(1);
}
while(1)
{
char buf[4096]={0};
int rd_ret = read(fd,buf,sizeof(buf));
if(rd_ret<=0)
{
break;
}
write(pipefd[1],buf,rd_ret);
}
close(fd);
close(pipefd[1]);
}
else if(0 == pid)
{
close(pipefd[1]);
int fd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(-1==fd)
{
perror("open");
exit(1);
}
while(1)
{
char buf[1024]={0};
int rd_ret = read(pipefd[0],buf,sizeof(buf));
if(rd_ret<=0)
{
break;
}
write(fd,buf,rd_ret);
}
close(fd);
close(pipefd[0]);
}
else
{
perror("fork");
exit(1);
}
return 0;
}
验证如下问题:
1、父子进程是否都有fd[0] fd[1],
如果在单一进程中写fd[1]能否直接从fd[0]中读到。
可以,写fd[1]可以从fd[0]读
2、管道的数据存储方式是什么样的
数据是否一直保留?
栈, 先进后出
队列形式存储 读数据会剪切取走数据不会保留
先进先出
3、管道的数据容量是多少,有没有上限值。
操作系统的建议值: 512* 8 = 4k
代码测试实际值: 65536byte= 64k
4、管道的同步效果如何验证?读写同步验证。
读端关闭能不能写? 不可以 ===>SIGPIPE 异常终止 (触发管道破裂)
写端关闭能不能读? 可以,取决于pipe有没有内容,===>read返回值为0 不阻塞
结论:读写端必须同时存在,才能进行
管道的读写。
5、固定的读写端是否就不能互换?
能否写fd[0] 能否读fd[1]? 不可以,是固定读写端。
练习:
如何用管道实现一个双向通信功能
将从父进程发送的消息在发回给父
进程。
pipe,
fork()
if(pid>0)
{
read(file,,)
wirte(fd[1]);
}
if(0 == pid)
{
read(fd[0]);
write(newfile);
}
person
{
char name[];
int age;
char phone;
}
pipe[];
fork();
> 0
fgets 获得输入
填结构体,
写入管道,
==0
read,
填结构体,,
显示到。。。
hello
pipe:hello
wordld
pipe2 ::world;
有名管道===》fifo ==》有文件名称的管道。
文件系统中可见
框架:
创建有名管道 ==》打开有名管道 ==》读写管道
==》关闭管道 ==》卸载有名管道
1、创建:mkfifo
#include <sys/types.h>
#include <sys/stat.h>
remove();
int mkfifo(const char *pathname, mode_t mode);
功能:在指定的pathname路径+名称下创建一个权限为
mode的有名管道文件。
参数:pathname要创建的有名管道路径+名称
mode 8进制文件权限。
返回值:成功 0
失败 -1;
2、打开有名管道 open
注意:该函数使用的时候要注意打开方式,
因为管道是半双工模式,所有打开方式直接决定
当前进程的读写方式。
一般只有如下方式:
int fd-read = open("./fifo",O_RDONLY); ==>fd 是固定读端
int fd-write = open("./fifo",O_WRONLY); ==>fd 是固定写端
不能是 O_RDWR 方式打开文件。
不能有 O_CREAT 选项,因为创建管道有指定的mkfifo函数
3、管道的读写: 文件IO
读: read(fd-read,buff,sizeof(buff));
写: write(fd-write,buff,sizeof(buff));
4、关闭管道:
close(fd);
5、卸载管道:remove();
int unlink(const char *pathname);
功能:将指定的pathname管道文件卸载,同时
从文件系统中删除。
参数: ptahtname 要卸载的有名管道
返回值:成功 0
失败 -1;
open会阻塞,等到另一端读端打开,解除阻塞。
函数是否会阻塞,具体根据操作的对象有关。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo",0666);
if(-1 == ret)
{
//如果是管道文件已存在错误,让程序继续运行
if(EEXIST== errno)
{
}else
{
perror("mkfifo");
exit(1);
}
}
//open 会阻塞,等到另一端读段打开,解除阻塞
int fd = open("myfifo",O_WRONLY);
if(-1 == fd)
{
perror("open");
exit(1);
}
char buf[256]="hello,fifo,test";
write(fd,buf,strlen(buf));
close(fd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo",0666);
if(-1 == ret)
{
//如果是管道文件已存在错误,让程序继续运行
if(EEXIST== errno)
{
}else
{
perror("mkfifo");
exit(1);
}
}
int fd = open("myfifo",O_RDONLY);
if(-1 == fd)
{
perror("open");
exit(1);
}
char buf[256]={0};
read(fd,buf,sizeof(buf));
printf("fifo %s\n",buf);
close(fd);
//remove("myfifo");
return 0;
}
remove可以在命令行调用。
运行方法:
练习:
编写一个非亲缘关系进程间通信程序,可以从
A程序向B程序连续发送不同的数据,并从B中
将发送的信息打印输出,当双方收到quit的时候
程序全部退出。
思考题:
是否每次启动程序必须进行有名管道的创建工作
能否有一个独立的维护工具,可以任意创建并删除
有名管道?
比如:
./fifo_tool -A fifo ==>创建一个有名管道fifo
./fifo_tool -D fifo ==>删除一个有名管道fifo
作业:
有名管道的操作函数封装:
int fifo_read(char *fifoname,void *s,int size);
int fifo_write(char *fifoname,void *s,int size);
编写测试程序验证以上两个函数效果。
gstream
有名管道 ===》
1、是否需要同步,以及同步的位置。
读端关闭 是否可以写,不能写什么原因。
写端关闭 是否可以读。
结论:有名管道执行过程过必须有读写端同时存在。
如果有一端没有打开,则默认在open函数部分阻塞。
2、有名管道是否能在fork之后的亲缘关系进程中使用。
结论: 可以在有亲缘关系的进程间使用。
注意: 启动的次序可能会导致其中一个稍有阻塞。
3、能否手工操作有名管道实现数据的传送。
读: cat fifoname
写: echo "asdfasdf" > fifoname