进程间通信的引入
1.由于进程与进程之间具有独立性,有时候我们想让不同的进程进行数据传输,进程的独立性就会使得数据传输变得很困难;
2.因此只要有一种机制,能够让不同的进程看到同一份资源,就可以通过该资源进行信息交互,因此就有了进程间通信。
进程间通信介绍
1.进程间通信:在不同的进程间传播或交换信息。
2.进程间通信的方式:管道、消息队列、共享内存、信号量、socket(套接字)等。
3.进程间通信的本质:让不同的进程看到同一份资源。
进程间通信的目的
1.数据传输
2.资源共享
3.通知事件
4.进程控制
管道
1.管道通常指匿名管道,是Unix中最古老的进程间通信方式。
2.我们把从一个进程连接到另一个进程的一个数据流称为一个管道。
3.管道被抽象成一个文件,管道的本体是一段内存。
管道的特点
1.对于匿名管道来说,它只能用于具有亲缘关系的进程(父子、兄弟、爷孙等)进行进程间通信;
- 因为子进程的PCB是以父进程的PCB为模板拷贝的,PCB里面含有一个file*的结构体指针,该指针指向一个结构体,里面包含文件描述符表,文件描述符表是继承自父进程的,所以他们的文件描述符表的内容是一样的,即就是他们可以看到同一个资源。
2.管道只能进行单向通信,如果需要双向通信,则需要两个管道;
3.管道是基于数据流(字节流)的;
- 因为管道的读或写没有确定的大小,而面向数据报是有大小的。
4.管道的生命周期随进程;
- 因为管道的本体是一段内存,当进程退出这段内存也就没有了。
5.一般而言,内核会对管道的操作进行同步与互斥,即管道自带同步与互斥。
- 例如:如果写端向管道里面写数据,如果没有写完,则不允许读,否则会读错数据。
匿名管道
1.创建方式(pipe函数)
#include <unistd.h>
int pipe(int pipefd[2]); //pipe用于创建一个匿名管道,pipefd为输出型参数,是一个文件描述符组,pipefd[0]表示读,pipefd[1]表示写
返回值:成功返回0,失败返回-1
- 管道创建成功后,就会有两个文件描述符,一个用来读,一个用来写,通过文件描述符就可以往管道里面读写数据。
- 通过read和write函数就可以进行相关读写操作。
2.示例1:创建一个匿名管道
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe()");
return 1;
}
return 0;
}
3.示例2:从键盘读取数据,写入管道,再从管道读取数据写到屏幕(或标准输出)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<string.h>
int main()
{
//创建一个管道
int fd[2];
int ret = pipe(fd);
if(ret <0)
{
perror("pipe");
return -1;
}
while(1)
{
//从标准输入读数据
char buf[1024]={0};
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s < 0)
perror("read");
return -2;
}
else if(s == 0)
{
printf("read done!!!\n");
break;
}
else
{
buf[s] = '\0';
//写入管道
if(write(fd[1],buf,strlen(buf)) < 0)
{
perror("write");
return -3;
}
//从管道中读取数据
char buf2[1024] = {0};
s = read(fd[0],buf2,sizeof(buf2)-1);
if(s < 0 )
{
perror("read from pipe");
return -4;
}
else if(s == 0)
{
printf("read from pipe done!!\n");
break;
}
else
{
//写到屏幕
buf2[s] = '\0'; //保证字符串以'\0'结束
write(1,buf2,strlen(buf2));
}
}
}
}
一般而言,只有一个进程操纵管道并没有什么作用,所以利用fork创建进程,让父子进程通过管道进行数据交互。
4.示例3:用fork来共享管道
(1)父进程创建管道;
(2)父进程fork出子进程
(3)父进程读,子进程写(则数据从子进程流向父进程,那么子进程需要关闭读端fd[0],父进程需要关闭写端fd[1])
示例3.1:
//让子进程每隔一秒写一次,父进程一直读(父进程并没有每隔一秒读一次)
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>
int main()
{
//父进程创建出管道
int fd[2]={0,0};
if(pipe(fd) == -1)
{
perror("pipe()");
return 1;
}
//父进程fork出子进程
pid_t id = fork();
//子进程写父进程读
if(id == -1)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//father
close(fd[1]);
char buf[1024]={0};
while(1)
{
ssize_t s = read(fd[0],buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("child->father#%s",buf);
}
}
wait(NULL);
}
else
{
//child
close(fd[0]);
const char* msg = "hello pipe! I am child\n";
while(1)
{
write(fd[1],msg,strlen(msg));
sleep(1);
}
exit(0);
}
return 0;
}
示例3.2:子进程一直写,父进程5秒之后不再读,并且将读端关闭
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>
int main()
{
//父进程创建管道
int fd[2]={0,0};
if(pipe(fd) == -1)
{
perror("pipe()");
return 1;
}
//父进程fork出子进程
pid_t id = fork();
if(id == -1)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child写,关闭读端
close(fd[0]);
const char* msg = "hello , I am child\n";
while(1) //让子进程一直写
{
write(fd[1],msg,strlen(msg));
sleep(1);
}
}
else
{
//parent读,关闭写端
close(fd[1]);
char buf[1024] = {0};
int i = 0;
while(1)
{
ssize_t s = read(fd[0],buf,sizeof(buf)-1);
printf("child->parent:%s",buf);
if(i++ > 5)
{
break; //父进程5秒之后退出
}
}
close(fd[0]);
int status;
int ret = wait(&status);
if(ret < 0)
{
perror("wait");
return 1;
}
if((status & 0X7F) == 0)
{//正常退出
printf("child exit code:%d\n",(status>>8)&0XFF);
}
else //异常退出,则低7位表示信号
{
printf("sig code:%d\n",status&0X7F);
}
}
return 0;
}
运行结果如下:(可以看到5秒之后,屏幕没有在打印,可以发现子进程被13号信号终止,通过kill -l命令,看到13号信号为SIGPIPE)
命名管道
1.命名管道的特点
(1)命名管道与匿名管道的特点除了命名管道可用于任意两个进程进行进程间通信之外其余特点与匿名管道一致。
(2)命名管道有路径与之关联,它是文件系统中一种特殊的文件,叫做管道文件
2.命名管道的创建方式有两种
(1)在命令行通过mkfifo命令(mkfifo 后面带上管道的名字)
(2)通过函数mkfifo
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//pathname指明创建命名管道的路径
//mode表示创建的管道的权限
//返回值:成功返回0,失败返回-1
3.创建命名管道
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
int main()
{
mkfifo("myfifo",0666);
return 0;
}
运行该程序,可以发现当前目录多了一个myfifo文件,通过ll命令查看该文件,可以看到该文件权限前面带一个p,表示这是一个管道文件。
3.用命名管道实现server/client通信(命名管道的读写与普通文件的读写方式一致,读用read,写用write)
(1)server.c
server创建一个命名管道,读client发送至管道的消息。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
//创建命名管道
umask(0);
int ret = mkfifo("myfifo",0666);
if(ret < 0)
{
perror("mkfifo");
return 1;
}
//读取client发送的消息
char buf[1024];
int fd = open("myfifo",O_RDONLY);
while(1)
{
int len = read(fd,buf,sizeof(buf)-1);
if(len > 0)
buf[len] = 0;
printf("client say:%s",buf);
}
else if(len == 0) //读完,表示客户端已经退出
{
printf("client Quit!\n");
break;
}
else
{
perror("read");
return 1;
}
}
close(fd);
return 0;
}
(2)client.c
client通过标准输入,向管道里面写入数据。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
//向管道里面写入数据
int fd = open("myfifo",O_WRONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char buf[1024];
while(1)
{
printf("please Enter# ");
fflush(stdout);
//从标准输入读取内容,送至buf里面
ssize_t len = read(0,buf,sizeof(buf)-1);
if(len > 0 )
{
buf[len]=0;
write(fd,buf,strlen(buf));
}
else
{
perror("read");
return 1;
}
}
close(fd);
return 0;
}
(3)运行结果如下:
(4)当结束掉两个进程,可以发现创建的管道文件myfifo还在,但是不是说管道的生命周期随进程么,该管道不是应该销毁,为什么管道文件还在?
其实是因为虽然创建了一个管道文件,但是管道的本体还是一段内存,当进程结束,这段内存也就被释放,管道也就不在了,但是这个文件还可以存在。