标签:
在本系列序中作者概述了 linux 进程间通信的几种主要手段。其中管道和有名管道是最早的进程间通信机制之一,管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。 认清管道和有名管道的读写规则是在程序中应用它们的关键,本文在详细讨论了管道和有名管道的通信机制的基础上,用实例对其读写规则进行了程序验证,这样做有利于增强读者对读写规则的感性认识,同时也提供了应用范例。
管道是Linux支持的最初Unix IPC形式之一,具有以下特点:
#include <unistd.h> int pipe(int fd[2]) |
该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。
管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。
从管道中读取数据:
关于管道的读规则验证:
/************** * readtest.c * **************/ #include <unistd.h> #include <sys/types.h> #include <errno.h> main() { int pipe_fd[2]; pid_t pid; char r_buf[100]; char w_buf[4]; char* p_wbuf; int r_num; int cmd; memset(r_buf,0,sizeof(r_buf)); memset(w_buf,0,sizeof(r_buf)); p_wbuf=w_buf; if(pipe(pipe_fd)<0) { printf("pipe create error\n"); return -1; } if((pid=fork())==0) { printf("\n"); close(pipe_fd[1]); sleep(3);//确保父进程关闭写端 r_num=read(pipe_fd[0],r_buf,100); printf( "read num is %d the data read from the pipe is %d\n",r_num,atoi(r_buf)); close(pipe_fd[0]); exit(); } else if(pid>0) { close(pipe_fd[0]);//read strcpy(w_buf,"111"); if(write(pipe_fd[1],w_buf,4)!=-1) printf("parent write over\n"); close(pipe_fd[1]);//write printf("parent close fd[1] over\n"); sleep(10); } } /************************************************** * 程序输出结果: * parent write over * parent close fd[1] over * read num is 4 the data read from the pipe is 111 * 附加结论: * 管道写端关闭后,写入的数据将一直存在,直到读出为止. ****************************************************/ |
向管道中写入数据:
对管道的写规则的验证1:写端对读端存在的依赖性
#include <unistd.h> #include <sys/types.h> main() { int pipe_fd[2]; pid_t pid; char r_buf[4]; char* w_buf; int writenum; int cmd; memset(r_buf,0,sizeof(r_buf)); if(pipe(pipe_fd)<0) { printf("pipe create error\n"); return -1; } if((pid=fork())==0) { close(pipe_fd[0]); close(pipe_fd[1]); sleep(10); exit(); } else if(pid>0) { sleep(1); //等待子进程完成关闭读端的操作 close(pipe_fd[0]);//write w_buf="111"; if((writenum=write(pipe_fd[1],w_buf,4))==-1) printf("write to pipe error\n"); else printf("the bytes write to pipe is %d \n", writenum); close(pipe_fd[1]); } } |
则输出结果为: Broken pipe,原因就是该管道以及它的所有fork()产物的读端都已经被关闭。如果在父进程中保留读端,即在写完pipe后,再关闭父进程的读端,也会正常写入pipe,读者可自己验证一下该结论。因此,在向管道写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭,否则就会出现上述错误(管道断裂,进程收到了SIGPIPE信号,默认动作是进程终止)
对管道的写规则的验证2:linux不保证写管道的原子性验证
#include <unistd.h> #include <sys/types.h> #include <errno.h> main(int argc,char**argv) { int pipe_fd[2]; pid_t pid; char r_buf[4096]; char w_buf[4096*2]; int writenum; int rnum; memset(r_buf,0,sizeof(r_buf)); if(pipe(pipe_fd)<0) { printf("pipe create error\n"); return -1; } if((pid=fork())==0) { close(pipe_fd[1]); while(1) { sleep(1); rnum=read(pipe_fd[0],r_buf,1000); printf("child: readnum is %d\n",rnum); } close(pipe_fd[0]); exit(); } else if(pid>0) { close(pipe_fd[0]);//write memset(r_buf,0,sizeof(r_buf)); if((writenum=write(pipe_fd[1],w_buf,1024))==-1) printf("write to pipe error\n"); else printf("the bytes write to pipe is %d \n", writenum); writenum=write(pipe_fd[1],w_buf,4096); close(pipe_fd[1]); } } 输出结果: the bytes write to pipe 1000 the bytes write to pipe 1000 //注意,此行输出说明了写入的非原子性 the bytes write to pipe 1000 the bytes write to pipe 1000 the bytes write to pipe 1000 the bytes write to pipe 120 //注意,此行输出说明了写入的非原子性 the bytes write to pipe 0 the bytes write to pipe 0 ...... |
结论:
写入数目小于4096时写入是非原子的!
如果把父进程中的两次写入字节数都改为5000,则很容易得出下面结论:
写入管道的数据量大于4096字节时,缓冲区的空闲空间将被写入数据(补齐),直到写完所有数据为止,如果没有进程读数据,则一直阻塞。
实例一:用于shell
管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。比如,当在某个shell程序(Bourne shell或C shell等)键入who│wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道。考虑下面的命令行:
$kill -l 运行结果见 附一。
$kill -l | grep SIGRTMIN 运行结果如下:
30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1 34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5 38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9 42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13 46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14 |
实例二:用于具有亲缘关系的进程间通信
下面例子给出了管道的具体应用,父进程通过管道发送一些命令给子进程,子进程解析命令,并根据命令作相应处理。
#include <unistd.h> #include <sys/types.h> main() { int pipe_fd[2]; pid_t pid; char r_buf[4]; char** w_buf[256]; int childexit=0; int i; int cmd; memset(r_buf,0,sizeof(r_buf)); if(pipe(pipe_fd)<0) { printf("pipe create error\n"); return -1; } if((pid=fork())==0) //子进程:解析从管道中获取的命令,并作相应的处理 { printf("\n"); close(pipe_fd[1]); sleep(2); while(!childexit) { read(pipe_fd[0],r_buf,4); cmd=atoi(r_buf); if(cmd==0) { printf("child: receive command from parent over\n now child process exit\n"); childexit=1; } else if(handle_cmd(cmd)!=0) return; sleep(1); } close(pipe_fd[0]); exit(); } else if(pid>0) //parent: send commands to child { close(pipe_fd[0]); w_buf[0]="003"; w_buf[1]="005"; w_buf[2]="777"; w_buf[3]="000"; for(i=0;i<4;i++) write(pipe_fd[1],w_buf[i],4); close(pipe_fd[1]); } } //下面是子进程的命令处理函数(特定于应用): int handle_cmd(int cmd) { if((cmd<0)||(cmd>256)) //suppose child only support 256 commands { printf("child: invalid command \n"); return -1; } printf("child: the cmd from parent is %d\n", cmd); return 0; } |
管道的主要局限性正体现在它的特点上:
|
管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char * pathname, mode_t mode) |
该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。
有名管道比管道多了一个打开操作:open。
FIFO的打开规则:
如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
对打开规则的验证参见 附2。
从FIFO中读取数据:
约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。
注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
向FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
对于设置了阻塞标志的写操作:
对于没有设置阻塞标志的写操作:
对FIFO读写规则的验证:
下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。
#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define FIFO_SERVER "/tmp/fifoserver" main(int argc,char** argv) //参数为即将写入的字节数 { int fd; char w_buf[4096*2]; int real_wnum; memset(w_buf,0,4096*2); if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST)) printf("cannot create fifoserver\n"); if(fd==-1) if(errno==ENXIO) printf("open error; no reading process\n"); fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0); //设置非阻塞标志 //fd=open(FIFO_SERVER,O_WRONLY,0); //设置阻塞标志 real_wnum=write(fd,w_buf,2048); if(real_wnum==-1) { if(errno==EAGAIN) printf("write to fifo error; try later\n"); } else printf("real write num is %d\n",real_wnum); real_wnum=write(fd,w_buf,5000); //5000用于测试写入字节大于4096时的非原子性 //real_wnum=write(fd,w_buf,4096); //4096用于测试写入字节不大于4096时的原子性 if(real_wnum==-1) if(errno==EAGAIN) printf("try later\n"); } |
#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define FIFO_SERVER "/tmp/fifoserver" main(int argc,char** argv) { char r_buf[4096*2]; int fd; int r_size; int ret_size; r_size=atoi(argv[1]); printf("requred real read bytes %d\n",r_size); memset(r_buf,0,sizeof(r_buf)); fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0); //fd=open(FIFO_SERVER,O_RDONLY,0); //在此处可以把读程序编译成两个不同版本:阻塞版本及非阻塞版本 if(fd==-1) { printf("open %s for read error\n"); exit(); } while(1) { memset(r_buf,0,sizeof(r_buf)); ret_size=read(fd,r_buf,r_size); if(ret_size==-1) if(errno==EAGAIN) printf("no data avlaible\n"); printf("real read bytes %d\n",ret_size); sleep(1); } pause(); unlink(FIFO_SERVER); } |
程序应用说明:
把读程序编译成两个不同版本:
把写程序编译成两个四个版本:
下面将使用br、nbr、w代替相应程序中的阻塞读、非阻塞读
验证阻塞写操作:
验证非阻塞写操作:
不管写打开的阻塞标志是否设置,在请求写入的字节数大于4096时,都不保证写入的原子性。但二者有本质区别:
对于阻塞写来说,写操作在写满FIFO的空闲区域后,会一直等待,直到写完所有数据为止,请求写入的数据最终都会写入FIFO;
而非阻塞写则在写满FIFO的空闲区域后,就返回(实际写入的字节数),所以有些数据最终不能够写入。
对于读操作的验证则比较简单,不再讨论。
在验证了相应的读写规则后,应用实例似乎就没有必要了。
|
管道常用于两个方面:(1)在shell中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;(2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。
FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。
管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输"协议",采用传播具有特定意义的消息。
要灵活应用管道及FIFO,理解它们的读写规则是关键。
附1:kill -l 的运行结果,显示了当前系统支持的所有信号:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1 34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5 38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9 42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13 46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14 50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10 54) SIGRTMAX-9 55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-6 58) SIGRTMAX-5 59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-2 62) SIGRTMAX-1 63) SIGRTMAX |
除了在此处用来说明管道应用外,接下来的专题还要对这些信号分类讨论。
附2:对FIFO打开规则的验证(主要验证写打开对读打开的依赖性)
#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #define FIFO_SERVER "/tmp/fifoserver" int handle_client(char*); main(int argc,char** argv) { int r_rd; int w_fd; pid_t pid; if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST)) printf("cannot create fifoserver\n"); handle_client(FIFO_SERVER); } int handle_client(char* arg) { int ret; ret=w_open(arg); switch(ret) { case 0: { printf("open %s error\n",arg); printf("no process has the fifo open for reading\n"); return -1; } case -1: { printf("something wrong with open the fifo except for ENXIO"); return -1; } case 1: { printf("open server ok\n"); return 1; } default: { printf("w_no_r return ----\n"); return 0; } } unlink(FIFO_SERVER); } int w_open(char*arg) //0 open error for no reading //-1 open error for other reasons //1 open ok { if(open(arg,O_WRONLY|O_NONBLOCK,0)==-1) { if(errno==ENXIO) { return 0; } else return -1; } return 1; } |