虽然看过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一起使用的。下面的程序是一个父子进程通过管道通信的方式,其数据流动方向是父进程->子进程,因此父进程关闭了而读端,子进程关闭了写端。程序如下:
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
int n;
int fd[2];
pid_t pid;
char line[1024];
if (pipe(fd) < 0)
{
fprintf(stderr, "cann't create the pipe!\n");
exit(EXIT_FAILURE);
}
if ((pid = fork()) < 0){
fprintf(stderr, "cann't fork process!\n");
exit(EXIT_FAILURE);
}
else if (pid>0){
close(fd[0]);
write(fd[1], "hello world\n", 12);
}
else{
close(fd[1]);
n = read(fd[0], line, 1024);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
这和图1中的数据流动方向是一致的。首先使用pipe(fd[2])创建管道,然后主进程(在本程序中是父进程)fork一个子进程,父子进程分别关闭读端和写端,这样就可以由父进程写数据到子进程。
1.匿名管道的读写规则说明:
1.1读进程
- 当read一个写端已经关闭的管道时,在所有的数据都被读取之后,read返回0标识管道读取完毕。从技术上来讲,如果管道的写端还有进程存在,就不会产生文件的结束。
- 如果读取一个写端还存在的进程,如果一次读取的数据字节数大于管道能够容纳的最大字节数(PIPE_BUF表示管道能够容纳的最大字节数,可以通过pathconf和fpathconf函数获取该值),则返回管道中现有的字节数;如果请求的数据量不大于PIPE_BUF,则返回请求读取的字节数(请求的字节数小于管道现有的字节数)或者管道中现有的字节数(管道中现有的字节数小于请求的字节数)。
- 如果写端存在,但是管道中没有数据,此时读端会被阻塞。
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函数打开该文件,根据是否设置阻塞标志可以分为两种情况:
- 没有指定O_NONBLOCK:只读的open调用要等到某个其他进程为写而打开这个FIFO为止,同样只写的open要等到某个进程为读而打开它为止;如果只读(写)的open在打开之前已经存在进程为只写(读)open而打开FIFO,则open立即返回打开成功的标志。也就是说如果没有指定O_NONBLOCK,那么无论是读端还是写端都会阻塞到另一个进程为写或者读而打开改FIFO。
- 指定了O_NONBLOCK:只读open立即返回成功的标志;但是如果没有进程为读而打开一个FIFO,那么只写的open调用失败,返回-1且将error置为ENXIO。
以上是打开这个FIFO的各种情况与O_NONBLOCK的对应关系。下面针对FIFO的写进程和读进程,做一个详细的解释。
命定管道的读写规则
1.读进程
如果一个进程为了从FIFO中读取数据以阻塞的方式打开FIFO(没有设置O_NONBLOCK),则称内核为改进程的读操作设置了阻塞标志。
- 如果存在进程已经为写而打开了FIFO且此时有名管道中不存在数据,则此时读进程将一直阻塞;如果设置了O_NONBLOCK,则读操作会立即返回-1,并将error置为EAGIN;
- 如果没有进程为写而打开FIFO,则没有设置O_NONBLOCK的读操作来说将一直阻塞到有进程为写而打开改FIFO;
- 如果写进程关闭,则管道中有数据时,读进程读取改数据;如果管道中没有数据,则此时读进程返回0;
2.写进程
如果一个进程为了向FIFO中写入数据而以阻塞的方式打开FIFO(没有设置O_NONBLOCK),则称内核为改进程的写操作设置了阻塞标志。
- 在调用open为写而打开的时候没有设置O_NONBLOCK,则以阻塞方式进行写入。如果写入的字节数不大于PIPE_BUF,则能保证写入的原子性。如果管道的空闲空间小于要写入的字节数,则写进程将被阻塞,直到缓冲区中的空间能够容纳要写入的字节数,才会一次性的写入所有的数据;如果写入的字节数大于PIPE_BUF,则不能保证写入的原子性,此时只要FIFO缓冲区已有空闲的空间,写进程就会将数据写入缓冲区,写操作在写完所有的数据之后返回;
- 如果在调用open为写而打开的时候设置了O_NONBLOCK,则以非阻塞的方式进行写入。如果写入的字节数大于PIPE_BUF,则不能保证写入的原子性,此时写满所有得FIFO缓冲区之后,写操作返回;如果写入的数据字节数小于PIPE_BUF,将保证写入的原子性。如果此时FIFO的空闲空间大于要写入的字节数,则一次性写入之后返回成功,否则返回EAGIN错误,提醒以后再写。
只有读端存在时,写端才有意义,如果读端不存在,则写端写数据到FIFO,则内核向写进程发送SIGPIPE信号(默认终止进程)。这也是这次遇到的问题的原因所在。
总结:
理论知识不能不看,但是遇到问题之后一定要通过搜索或者请教别人,即使温习对应的知识点。