进程通信(IPC)是进程知识点中重要的一环,本篇意在介绍几种常用的通信技术及其基本用法。常用的IPC通信方式有:
- 管道
- FIFO
- 信号
- 消息队列
- 共享内存
- Socke
下面我们就从pipe 开始说起吧。
1. 管道(pipe)
管道是最古老也是最常用的IPC方式,具有实现简单,使用也简单的优点。但这种古老而经典的通信方式应用于现在的系统,就有了一定的局限性。pipe的基本思想可以如下:
父进程创建了一个文件,然后fork一个子进程,子进程会继承这个文件描述符,。这样父子进程就可以通过这个文件去通信,同样两个兄弟进程之间也可以利用这个文件去通信。如果这个文件没有名字而且只能单方向流通数据,那就是我们所说的管道了。下图就是一个常见的管道通信模型。
1.1 管道的创建
#include<unistd.h>
int pipe(int fd[2]);
pipe()函数通过fd数组返回两个文件描述符,fd[0]为读打开,相当于输入;fd[1]为写打开,相当于输出,fd[1]的输出是fd[0]的输入。这里的读写都是相对于pipe文件来说。
如果两个进程分别关闭读端和写端,则这两个进程就可以通过这个管道建立起一个共享信息的通道。
举个例子,父进程关闭 fd[0],子进程关闭fd[1],这样就建立了一个从父进程流向子进程的pipe通道。
1.2 管道读写
读写管道类似与读写文件,可以直接用write和 read 来读写。但是读数据时,每读一段数据,管道会自动清除已读走的数据。管道可以用于多个读写进程的通信,但是通常只有一个读进程和一个写进程,否则没有同步机制,不是很容易发生可重入问题吗?
- 读管道时,如果管道为空,则读进程阻塞,直到写端往里面写入数据。如果写端已经关闭的话,则读端在读完数据后,下一个read会返回0,表示文件已经结束。
- 写管道时,如果管道已满,写进程阻塞,直到读端读走数据。如果读端已经关闭,则会产生SIGPIPE信号,如果我们对该信号的处理方式是忽略,或者从其信号处理程序返回,则write 返回-1,errno设置为EPIPE。
问题是我们怎么知道读端或者写端已经关闭呢?
管道的读写两端都有一个计数器,当计数器为0时不就说明读端或者写端已经 关闭了吗?管道自己会判断的!
来吧,用事实说话,写个程序验证下上述说的第二点:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#define MAXLINE 2047
static void sig_pipe(int signo)
{
printf("signal sigpipe\n");
}
int main(int argc, char *argv[])
{
int cout = 10;
int fd[2];
int n = 0;
pid_t pid;
char line[MAXLINE];
ssize_t m;
if(pipe(fd) < 0)
{
printf("creat pipe fail\n");
}
pid = fork();
if(pid < 0)
{
printf("fork fail\n");
}
else if(pid == 0)
{
close(fd[0]);
if(signal(SIGPIPE,sig_pipe) == SIG_ERR)
{
printf("signal error\n");
}
while(cout--)
{
printf("write data to pipe\n");
if(write(fd[1],"hello world\n",12) == -1)
{
printf("write fail\n");
}
sleep(1);
}
}
else
{
close(fd[1]);
while(1)
{
n = read(fd[0],line,MAXLINE);
write(STDOUT_FILENO,line,n);
close(fd[0]);
}
}
return 0;
}
以上程序,在子进程每隔10s向管道写入数据,父进程则一直从管道读入数据。当父进程突然关闭读端时,子进程的写端会产生SIGPIPE 信号,然后从SIGPIPE处理程序返回后,write 返回-1,执行结果为:
写到这里,不妨先对pipe进行一个简单的总结:
- 半双工通信,管道只允许单方向流通数据;原因很简单:使用管道也没有其他的同步机制,全双工的话就会存在数据覆盖的情况;
- pipe只允许在具有血缘关系的进程之间通信;这一点也是显而易见的,通信的进程间需要共享文件描述符呢;
- 因pipe是内核直接建立在内存中的,所以它的容量大小是由限制的。而且pipe中传输的是无格式字节流,所以需要通信双方自己约定格式协议。
1.3 popen和pclose 函数
上节中的例程是最用的pipe 用法,父进程先创建一个pipe,fork一个子进程,然后父进程关闭写端,子进程关闭读端。本节中将要介绍的是一组多功能pipe相关函数,popen函数。先来看下这组函数的定义:
#include<stdio.h>
FILE *popen(const char *cmdstring, const char *type);
//成功返回文件指针,失败返回NULL
int pclose*(FILE *fp);
//成功返回shell 的终止状态,出错返回-1
popen函数的执行过程如下:
- 先创建一个管道
- 随后fork一个子进程,子进程通过exec 函数调用shell执行 cmdstring命令,即execl(“/bin/sh”,”-c”,”cmdstring”,NULL)
- popen函数返回一个指向输入输出流的文件指针fp。管道是单双工通信,这个的文件指针只能对应输入或者输出流。数据 流动的方向则取决于type参数:
1). 若type = ‘r’, 于输入流文件指针;则fp相当此时fp与子进程执行cmd 的stdout直接相连,即stdout的数据会流向fp。换个角 度fd这里相当于管道的fd[0],子进程的stdout相当于fd[1]
2). 若type = ‘w’, 于输出流文件指针;则fp相当此时fp与子进程执行cmd 的stdin直接相连,即fp的数据会流向shell命令。换个 角度fd这里相当于管道的fd[1],子进程的stdout相当于fd[0]
关于popen函数还有以下几点需要说明下:
- popen函数返回的文件指针fp同open函数获取的文件指针一样,可以用文件IO相关函数去操作,但不能用fclose去关闭
- popen函数的输出流默认是全缓冲的
pclose 函数等待相关的进程结束并返回shell执行cmdstring命令的退出状态,随后会关闭popen创建的pipe以及文件指针。
下面这个不成熟的例子中,使用popen去执行“ps –ef”命令,并将结果显示出来:执行结果:
1. #include<stdio.h>
2. #include<stdlib.h>
3. #include<unistd.h>
4. #define MAXLINE 1024
5.
6. int main(int argc, char *argv[])
7. {
8. int n = 0;
9. FILE *fp = NULL;
10. char buf[MAXLINE];
11. fp = popen("ps -ef","r");
12. n = fread(buf,1,MAXLINE,fp);
13. if(fputs(buf,stdout) == EOF)
14. {
15. printf("error\n");
16. }
17. pclose(fp);
18. return 0;
19. }
执行结果:
2. fifo
fifo类似于pipe,也是一种特殊管道文件,但不同于管道只能用于具有共同祖先的两个进程,fifo是有名字的,可以让不相关的进程进行通信,所以通常又被成为有名管道。创建fifo 的函数如下:
#include<sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode )
创建成功后,会在指定的path下生成一个fifo文件,同时函数返回0;失败的话返回-1。
关于fifo,我们不妨大致的总结下吧:
- 创建的fifo文件类似于creat函数,使用的话需要先open,文件IO函数也可以用于fifo文件(open、write、read、unlink等),也可以用S_ISFIFO对创建的fifo文件进行测试
- Fifo遵循先入先出的原则,且其中的数据读取后就消失了
- 没有指定O_NONBLCK标志时,只读open要阻塞到其它进程为写而打开这个fifo为止;只写write也会阻塞知道该fifo被其它进程以读的方式打开
- 如果指定了O_NONBLCK标志,只读open或者只写open会立即返回
- 读一个写端突然关闭的fifo,会返回文件结束标志;写一个读端突然关闭的fifo会产生一个SIGPIPE信号,这与pipe类似。
下面是一个简单的例子来看看fifo 是怎么用的,例程中,在子进程中创建了一个fifo,然后以只读的方式打开,并写入一些数据;在父进程中读取这个fifo中的数据并显示出来:
1. #include<stdio.h>
2. #include<stdlib.h>
3. #include<sys/stat.h>
4. #include<sys/types.h>
5. #include<unistd.h>
6. #include<fcntl.h>
7. #include<string.h>
8.
9. # define FIFO_NAME "/tmp/fifo2"
10. # define MAXLINE 1048
11.
12. int main(int argc, const char *argv[])
13. {
14. int fd;
15. pid_t pid;
16.
17. pid = fork();
18.
19. if(pid < 0)
20. {
21. printf("fork error\n");
22. }
23. else if(pid == 0)
24. {
25. if(mkfifo(FIFO_NAME,0777) == -1)
26. {
27. printf("fail\n");
28. }
29.
30. fd = open(FIFO_NAME,O_WRONLY);
31. write(fd,"TEST\n",6);
32. close(fd);
33. }
34. else
35. {
36. char ptr[MAXLINE];
37. int n = 0;
38. sleep(2);
39. fd = open(FIFO_NAME,O_RDONLY);
40. memset(ptr,'\0',sizeof(ptr));
41. n = read(fd,ptr,MAXLINE);
42. write(STDOUT_FILENO,ptr,n);
43. close(fd);
44. }
45. return 0;
46. }
fifo的两个经典应用场景:
- Shell命令使用FIFO 将数据从一条管道传送到另一条时,而无需创建临时文件用来保存文件;
- 客户进程-服务进程应用程序中,fifo用作汇聚点,在客户进程服务进程之间通信。
关于pipe和 fifo 的使用还有一个需要注意的点:
对fifo或者pipe进行写的时候,如果要求写入的字节数小于PIPE_BUF,则写操作不会与其他进程对同一管道的write操作交叉,但是如果多个进程同时去写,且要求写的字节数大于PIPE_BUF就会产生在竞争问题。
这里的PIPE_BUF是内核规定的管道缓冲区大小,我们可以通过pathconf函数或者fpathconfg函数获取。