标签:
虽然看过APUE这本书,但是还是实践出真知。虽然看过相关的内容,但是只是停留在理论的层面,今天遇到的问题还是在大牛的提示下了解了原因所在---进程之间的管道通信导致进程阻塞。
问题是这样的,使用资源管理器rigger -ng启动nginx的过程中,卡在nginx的语法校验一步而无法再继续向下进行,导致nginx无法启动。一开始我和同事折腾半天以为是nginx的配置问题,后来才发现是进程之间的通信出了问题,nginx启动的过程中需要通过管道和另一个负责写日志的进程通信,而另一个负责写日志的进程,也就是负责读取管道的进程没有启动。趁着周日有时间,将原来看过的关于进程间管道通信的内容重新温习一遍,做个记录。
进程之间的通信方式一般有:匿名管道(pipe)、有名管道(fifo)、消息队列、信号量、共享存储区以及套接字。其中管道作是一种最古老而应用又很普遍的通信方式,一般只支持半双工的通信方式(一个管道只能朝一个方向发送数据,而不能发向发送)。虽然现在有的系统支持全双工的管道通信方式,但是使用的时候一般不应该作此假设。下面针对匿名管道(pipe)和有名管道(fifio)之间的通信方式做一个回顾。
一、匿名管道(pipe)
匿名管道,顾名思义其实就是没有名字的管道。正是由于这种管道没有名字,因此其应用受到了限制。这种管道只能用于存在亲缘关系的进程之间的通信,也就是父子进程或者是兄弟进程之间的通信。从本质上而言,匿名管道可以理解成一种特殊的文件系统,只是这种文件系统与unix下所说的文件系统不同,它不存在于磁盘上,只是存在于计算机的内存之中(和文件系统中的/proc有点类似)。其创建函数如下:
#include<unistd.h>
int pipe(int fd[2]);
管道创建成功之后通过fd返回两个文件描述符fd[0]和fd[1]。fd[0]为读而打开,fd[1]为写而打开,fd[1]的输出恰好是fd[0]的输入。
由于是半双工通信,因此通过管道进行通信的进程之间数据的流动如图1所示,当然通过关闭不同的文件描述符,也可以使数据反向流动。
图1 匿名管道的数据流动图
如上所述,匿名管道的通信一般应用于有亲缘关系的进程之间,因此匿名管道的的创建函数pipe一般是和进程的创建函数fork一起使用的。下面的程序是一个父子进程通过管道通信的方式,其数据流动方向是父进程->子进程,因此父进程关闭了而读端,子进程关闭了写端。程序如下:
1 #include<stdlib.h> 2 #include<stdio.h> 3 #include<unistd.h> 4 5 int main() 6 { 7 int n; 8 int fd[2]; 9 pid_t pid; 10 char line[1024]; 11 12 if (pipe(fd) < 0) 13 { 14 fprintf(stderr, "cann‘t create the pipe!\n"); 15 exit(EXIT_FAILURE); 16 } 17 if ((pid = fork()) < 0){ 18 fprintf(stderr, "cann‘t fork process!\n"); 19 exit(EXIT_FAILURE); 20 } 21 else if (pid>0){ 22 close(fd[0]); 23 write(fd[1], "hello world\n", 12); 24 } 25 else{ 26 close(fd[1]); 27 n = read(fd[0], line, 1024); 28 write(STDOUT_FILENO, line, n); 29 } 30 exit(0); 31 }
这和图1中的数据流动方向是一致的。首先使用pipe(fd[2])创建管道,然后主进程(在本程序中是父进程)fork一个子进程,父子进程分别关闭读端和写端,这样就可以由父进程写数据到子进程。
1.匿名管道的读写规则说明:
1.1读进程
1.2写进程
向管道中写数据的时候,PIPE_BUF规定了内核中管道缓冲区的大小。如果对管道调用write且所写的字节数小于等于PIPE_BUF,则此操作不会与其他进程对同一管道的的写操作交叉进行。但是如果有多个进程对管道同时写一个管道,且所写的字节数大于PIPE_BUF,那么缩写的数据就会与其他的进程交叉。如果管道已经满了,且没有读进程读取管道中的数据,此时写操作将会被阻塞。
另一个需要注意的问题就是,只有在读端存在的情况下,向管道中写数据才有意义。如果读端不存在了(压根就没有或者已经被关闭),那么会产生信号SIGPIPE,应用程序可以处理该信号或者忽略(默认动作是使得应用程序终止)。
二、命名管道(FIFO)
为了克服匿名管道只能在具有亲缘关系的进程之间通信的缺点,提出了命名管道。命名管道不同于匿名管道的之处在于,创建命名管道的时候需要提供一个路径名参数,该路径名已FIFO的形式存在于文件系统之中。这样即使与创建FIFO的进程没有亲缘关系的进程只要可以访问该路径,就可以通过FIFO彼此通信。创建FIFO的函数如下:
#include<sys/stat.h>
int mkfifo(const char *path,mode_t mode);
如果path只是一个普通的路径名,那么该path就是创建FIFO之后的路径名,路径名存在于文件系统中,但是管道中的内容仍然存在于内存中;第二个参数mode_t与普通文件打开函数open(const char* path,int oflag,.../*mode_t mode*/);的mode_t一致。这里我们会重点说明一下mode_t参数中的o_NONBLOCK选项。如果路径名path已经存在,则函数返回一个EEXIST的错误,因此在使用FIFO创建函数创建FIFO的之后首先检查是否返回EEXIST错误,如果返回该错误,那么直接调用FIFO的open函数就可以了;如果返回其他的错误,则证明创建FIFO不成功。
open调用的规则
我们成功创建FIFO之后要使用open函数打开该文件,根据是否设置阻塞标志可以分为两种情况:
以上是打开这个FIFO的各种情况与O_NONBLOCK的对应关系。下面针对FIFO的写进程和读进程,做一个详细的解释。
命定管道的读写规则
1.读进程
如果一个进程为了从FIFO中读取数据以阻塞的方式打开FIFO(没有设置O_NONBLOCK),则称内核为改进程的读操作设置了阻塞标志。
2.写进程
如果一个进程为了向FIFO中写入数据而以阻塞的方式打开FIFO(没有设置O_NONBLOCK),则称内核为改进程的写操作设置了阻塞标志。
只有读端存在时,写端才有意义,如果读端不存在,则写端写数据到FIFO,则内核向写进程发送SIGPIPE信号(默认终止进程)。这也是这次遇到的问题的原因所在。
总结:
理论知识不能不看,但是遇到问题之后一定要通过搜索或者请教别人,即使温习对应的知识点。
标签:
原文地址:http://www.cnblogs.com/yue-blog/p/5861158.html