进程间通讯介绍(IPC):
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
一、管道
1.无名管道(pipe)
管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。
1.1 概述:
特点:
(1)管道建立内核的内存中的,父进程与子进程退出后,这个管道就消失了,不会在磁盘中存在;
(2)它是半双工的,数据只能向一个方向流动;双方需要互相通信时,需要建立起两个管道;
(3)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间);
(4)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中;
(5)管道中的数据被读走就没了;
(6)进程被确认下来哪端是读还是写的话,在这个进程在这个程序的读写端基本就被定死了。
管道的实质是内核利用 环形队列
的数据结构在 内核缓冲区 中的一个实现,默认设置大小为4K
,可以通过ulimit -a
命令查看。由于利用 环形队列
进行实现,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读取一次,读取后数据就会从缓冲区中移除。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。
使用步骤:
(1)父进程调用pipe
函数创建管道,得到两个文件描述符fd[0]、fd[1]
指向管道的读端和写端。
(2)父进程调用fork
创建子进程,那么子进程也有两个文件描述符指向同一管道。
(3)父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
管道的读写行为
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK
标志):
(1)如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
(2)如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read
会阻塞,直到管道中有数据可读了才读取数据并返回。
(3)如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write
,那么该进程会收到信号SIGPIPE
,通常会导致进程异常终止。当然也可以对SIGPIPE
信号实施捕捉,不终止进程。
如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
1.2 原型:
1 #include <unistd.h>
2 int pipe(int fd[2]);
返回值:若成功返回0,失败返回-1;
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开;
要关闭管道只需将这两个文件描述符关闭即可。
**注意点:读端读取写端数据时,如果写端没有数据,那么读端的进程会被阻塞;
1.3 管道编程实战
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2];
int pid;
char buf[128];
// int pipe(int pipefd[2]);
if(pipe(fd) == -1){//创建管道
printf("creat pipe failed\n");
}
pid = fork();//创建进程
if(pid<0){
printf("creat child failed\n");
}
else if(pid > 0){
sleep(3);//这里父进程先睡3秒,让子进程先运行。
printf("this is father\n");
close(fd[0]);//父进程关闭读功能(无名管道就是要确认读写端然后关闭不要的功能(就是fd[i]某一个))
write(fd[1],"hello from father",strlen("hello from father"));
wait(NULL);
}else{
printf("this is child\n");
close(fd[1]);//子进程关闭写功能
read(fd[0],buf,128);
printf("read from father:%s\n",buf);
exit(1);
}
return 0;
}
2 命名管道(fifo)
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
命名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以命名管道的文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过命名管道相互通信,因此,通过命名管道不相关的进程也能交换数据。值的注意的是,命名管道严格遵循先进先出(first in first out)
,对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
命名管道的名字存在于文件系统中,内容存放在内存中。
2.1 特点
(1)与无名管道都是把数据存到管道流,一端读一端写;
(2)FIFO可以在无关的进程之间交换数据,与无名管道不同;
(3)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
匿名管道和命名管道总结
(1)管道是特殊类型的文件,在满足先入先出的原则条件下可以进行读写,但不能进行定位读写。
(2)无名管道是单向的,只能在有亲缘关系的进程间通信;有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
(3)无名管道阻塞问题:无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则将退出。如果当前进程向无名管道的一端写数据,必须确定另一端有某一进程。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将自动退出。
(4)命名管道阻塞问题:命名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)
模式打开命名管道,即当前进程读,当前进程写,不会阻塞。
2.2 原型:
(1) #include <sys/stat.h>
(2) 返回值:成功返回0,出错返回-1
(3) int mkfifo(const char *pathname, mode_t mode);
其中的 mode 参数与open函数中的 mode 相同,表示文件权限。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
当 open 一个FIFO时,是否设置非阻塞标志O_NONBLOCK的区别:
(1)若没有指定O_NONBLOCK(默认),命名管道在打开时需要确实对方的存在,否则将阻塞。只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。可以以读写(O_RDWR)模式打开命名管道,即当前进程读,当前进程写,不会阻塞。
(2)若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 ,如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。
2.3 命名管道的数据通信编程实现
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。
以下代码是通过FIFO来读写数据,分两个进程(open管道为读端与open管道为写段),首先先看看读端的代码(read.c)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#define MAX_BUF_SIZE 1024
int main(int argc, char *argv[])
{
if(argc < 2)
{
fprintf(stderr,"Usage: %s argv[1]\n",argv[0]);
return -1;
}
if(mkfifo(argv[1],0666) < 0 && errno != EEXIST)
{
fprintf(stderr,"Fail to mkfifo %s : %s",argv[1],strerror(errno));
return -1;
}
int fd;
if((fd = open(argv[1],O_RDONLY)) < 0)
{
fprintf(stderr,"Fail to open mkfifo %s : %s",argv[1],strerror(errno));
return -1;
}
int n;
char buf[MAX_BUF_SIZE];
while(1)
{
memset(buf, 0, sizeof(buf));
n = read(fd, buf, sizeof(buf));
printf("Read %d bytes\nRECV MSG:%s\n",n, buf);
}
return 0;
}
以下是open(管道)写端的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#define MAX_BUF_SIZE 1024
int main(int argc, char* argv[])
{
if(argc < 2)
{
fprintf(stderr,"usage: %s argv[1].\n",argv[0]);
return -1;
}
if(mkfifo(argv[1], 0666) < 0 && errno != EEXIST)
{
fprintf(stderr,"Fail to mkfifo %s : %s.",argv[1],strerror(errno));
return -1;
}
int fd;
if((fd = open(argv[1],O_WRONLY)) < 0)
{
fprintf(stderr,"Fail to open mkfifo %s : %s.",argv[1],strerror(errno));
return -1;
}
printf("open for write success\n");
int n;
char buf[MAX_BUF_SIZE];
while(1)
{
memset(buf, 0, sizeof(buf));
printf(">");
scanf("%s",buf);
n = write(fd, buf, strlen(buf) + 1);// 将\0也写入
printf("Write %d bytes.\nSend MSG:%s\n",n, buf);
}
return 0;
}