进程间通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。也就是说两个进程之间是相对独立的,而且是互不干扰没有交集的,进程间通信就是让两个进程之间产生联系。
下面是实现进程间通信的图示
实现方法一:匿名管道
管道是一种最基本的IPC机制,由pipe函数创建
#include <unistd.h>
int pipe (int fileds[2]);
调用pipe函数在内核中开辟一块缓冲区(称为管道)用于通信,他有一个读端和一个写端,然后通过fileds参数传出给用户程序两个文件描述符,filedes0, filedes1,通过read(filedes[0])和filedes[1],那么开辟了管道之后如何实现通信呢,接下来我们来看图示。
1.父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
2.父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一条管道。
3.父进程关闭管道读端,子进程关闭管道写写端,父进程可以往管道里写,子进程可以从管道里读,管道使用环形队列实现,数据从读端流出,从写端流入这样就实现了进程间通信。
下边我们用代码演示一下:
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
int main()
{
int _pipe[2];//定义参数
int ret = pipe(_pipe);
if(ret==-1)//创建管道失败
{
printf("creat pipe error!errno code is :%d\n",errno);//错误码
return 1;//返回值,这样你就会知道到底是哪里出现了错误
}
pid_t id = fork();
if(id<0)//创建子进程失败
{
printf("fork error!");
return 2;
}
else if(id==0)
{
//child
close(_pipe[0]);//关闭读端
int i = 0;
char *_mesg = NULL;
while(i<100)
{
_mesg = "I am child!";
write(_pipe[1],_mesg,strlen(_mesg)+1);//xie
sleep(1);
i++;
}
}
else
{
//father
close(_pipe[1]);//关闭写端
char _mesg_c[100];
int j = 0;
while(j<100)
{
memset(_mesg_c,'\0',sizeof(_mesg_c));
read(_pipe[0],_mesg_c,sizeof(_mesg_c));
printf("%s\n",_mesg_c);
j++;
}
}
}
运行结果
这样就实现了进程间通信。(单向通信)父进程读,子进程写。
使用管道需要注意以下四种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志)
1. 如果所有指向管道写端的文件描述符都关闭了,(管道写端的引用计数为0),而仍然有进程从管道的读端读取数据,那么管道中剩余的数据都被读取之后,再次read将会返回0,就像读到文件结尾一样。也就是说,写端不会写,读端读完之后就会再等着写端去写,但是写端关闭了啊,不会写了,所以就出现上面说的情况。这就体现出了管道的同步机制。
2. 如果有指向管道写端的文件描述符没有关闭,(管道写端的引用计数大于0)而持有管道写端的进程也没有向管道中写数据,这时有进程管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。通俗讲就是,读端读数据,一直读,但是写端不写了,而且写端并没有关闭,所以这时读端就会一直等着写端去写。这就造成了阻塞式等待。
3.如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数为0),这时有进程向管道的写端写数据,那么该进程会收到SIGPIPE,通常会导致进程异常终止。所以进程就会异常退出了。
4.如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0)而持有管道读端的进程也没有从管道中读取数据,这时有进程向管道写端写数据,那么在管道写满时再写将会阻塞,直到管道中有了空位置才写入并返回,也就是管道的同步机制。
实现方法二:命名管道
匿名管道存在一个很大的缺陷,只能用于具有亲缘关系的进程间通信,所以提出了FIFO提出后,该限制得到了克服,FIFO不同于匿名管道之处在于它提供了一个与路径与之关联,以FIFO的文件形式存储于文件系统中,命名管道是一个设备文件,因此,即使进程之间不存在血缘关系,只要可以访问该路径,就能通过FIFO相互通信,值得注意的是命名管道是先进先出。
在linux下FIFO的创建有两种方式:
1.在shell下交互地建立一个命名管道。(使用mknod或mkfifo命令【建议使用】)
2.在程序中使用系统函数建立命名管道。
#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char* path, mode_t mode, dev_t dev);
int mkfifo(const char* path, mode_t mode);
函数参数中的path为创建的命名管道的路径名,mod为创建命名管道的模式,指明其存取权限,dev为设备值,该值文件创建的种类,它只在创建设备文件时才会用到。这两个函数带哦用成功返回0,失败都返回-1.
下面用mknod函数创建一个命名管道
umask(0);//重置管道的存取权限
if(mknod("/tmp/fifo",S_IFIFO|0666)==-1)
{
perror("mknod error");
exit(1);
}
//函数mkfifo的使用代码
umask(0);
if(mkfifo("/tmp/fifo",S_IFIFO|0666)==-1 )
{
perror("mkfifo error");
exit(1);
}
//"S_IFIFO|0666"致命创建一个管道的存取权限为0666
命名管道的使用和匿名管道基本相同,只是在使用命名管道之前首先要使用open函数打开,因为命名管道是存在于硬盘上的文件,而管道是存在于内存中的特殊文件。
需要注意,使用open的几点:
1. 调用open()打开命名管道可能会被阻塞,但是如果同时用读写方式(O_RDWR)打开,则一定不会造成阻塞。
2. 如果以制度方式(O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写才能打开管道。
3. 同样,以写方式(O_WRONLY)打开也会阻塞直到有读方式打开管道。
下面用代码实现一下FIFO管道通信。
servser.c //写端
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define _PATH_ "/tmp/file.tmp"
#define _SIZE_ 100
//写端
int main()
{
umask(0);
int ret = mkfifo(_PATH_, 0666 | S_IFIFO);
if(ret == -1)
{
printf("mkfifo error\n");
return 1;
}
int fd = open(_PATH_, O_WRONLY);
if(fd < 0)
{
printf("open error!\n");
}
char buf[_SIZE_];
memset(buf, '\0', sizeof(buf));
while(1)
{
scanf("%s", buf);
int ret = write(fd, buf, strlen(buf)+1);
if(ret < 0)
{
printf("write error\n");
break;
}
if(strncmp(buf, "quit", 4) == 0)
{
break;
}
}
close(fd);
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define _PATH_ "/tmp/file.tmp"
#define _SIZE_ 100
//读端
int main()
{
int fd = open(_PATH_, O_RDONLY);
if(fd < 0)
{
printf("open file error\n");
return 1;
}
char buf[_SIZE_];
memset(buf, '\0', sizeof(buf));
while(1)
{
int ret = read(fd, buf, sizeof(buf));
if(ret <= 0)
{
printf("read end or error\n");
break;
}
printf("%s\n", buf);
if(strncmp(buf, "quit", 4) == 0)
{
break;
}
}
close(fd);
return 0;
}
下边是运行结果:
为了能清晰的看到不同的进程之间进行通信,我们启动了两个终端,通过写端发送消息读端可以收到。
总结:
我们实现了两种方法来进行不同的进程之间的通信,总结来说匿名管道和命名管道最大的区别就是匿名管道只能进行有血缘关系的进程之间才能通信,而命名管道可以是两个不相干的进程之间通信,避免了匿名管道的局限性,而他们之所具有这样的特点是因为他们的实现方法,匿名管道通过血亲拥有相同的文件描述符来读取同一段缓冲区而命名管道是设定一个全局路径,两个进程都可以看到,用这个路径名来标识一个IPC通道。然而他们也有一些其他的缺点,比如管道是有容量的,进程之间的通信是单向的,所以后边我们还会学习XSI IPC来实现进程间通信。