16-IPC之有名管道
1.有名管道
无名管道因为没有文件名,被称为了无名管道,同样的道理,有名管道之所以叫“有名管道”,是因为它有文件名
也就是说当我们调用相应的API创建好“有名管道”后,会在相应的路径下面看到一个叫某某名字的“有名管道文件”
不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。
进程间通过管道通信时,本质上就是通过共享操作这段缓存来实现,只不过操作这段缓存的方式,是以读写文件的形式来操作的
有名管道特点
能够用于非亲缘进程之间的通信
因为有文件名,所以进程可以直接调用open函数打开文件,从而得到文件描述符,不需要像无名管道一样,必须在通过继承的方式才能获取到文件描述符
所以任何两个进程之间,如果想要通过“有名管道”来通信的话,不管它们是亲缘的还是非亲缘的,只要
调用open函数打开同一个“有名管道”文件,然后对同一个“有名管道文件”进行读写操作,即可实现通信。
A进程 —————————> 有名管道 ————————> B进程
总之,不管是亲缘进程还是非亲缘进程,都可以使用有名管道来通信
读管道时,如果管道没有数据的话,读操作同样会阻塞(休眠)
当进程写一个所有读端都被关闭了的管道时,进程会被内核返回SIGPIPE信号
如果不想被该信号终止的话,我们需要忽略、捕获、屏蔽该信号,不过一般情况下,不需要对这个信号进行处理,除非你有必须要处理的理由
2.有名管道的使用步骤
(1)进程调用mkfifo创建有名管道
(2)open打开有名管道
(3)read/write读写管道进行通信
对于通信的两个进程来说,创建管道时,只需要一个人创建,另一个直接使用即可
为了保证管道一定被创建,最好是两个进程都包含创建管道的代码,谁先运行就谁先创建,后运行的发现管道已
经创建好了,那就直接open打开使用
2.1 有名管道API
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
(1)功能
创建有名管道文件,创建好后便可使用open打开。
如果是创建普通文件的话,我们可以使用open的O_CREAT选项来创建,比如:
open("./file", O_RDWR|O_CREAT, 0664);
但是对于“有名管道”这种特殊文件,这里只能使用mkfifo函数来创建
(2)参数
1)pathname:被创建管道文件的文件路径名
2)mode:指定被创建时原始权限,一般为0664(110110100),必须包含读写权限
使用open函数创建普通文件时,指定原始权限是一样的。
open("./file", O_RDWR|O_CREAT, 0664);
创建新文件时,文件被创建时的真实权限=mode & (~umask)
umask是文件权限掩码,一般默认为002或者022
mkfifo("./fifo", 0664);
(3)返回值:成功返回0,失败则返回-1,并且errno被设置。
比如有两个进程想要通信,而且还是非亲缘进程,此时我们就可以使用"有名管道"来通信。
(1)单向通信
mkfifo1.c写
mkfifo2.c,mkfifo3.c
读,两者会轮流(随机)读
mkfifo.h
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <strings.h> #include <signal.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FIFONAME "./fifo" void fun(int signum){ remove(FIFONAME); exit(-1); } void print_err(char *estr){ perror(estr); exit(-1); } int create_open_fifo(char *fifoname,int open_mode){ int ret = -1; int fd =-1; printf("before mkfifo\n"); ret = mkfifo(fifoname,0644); if( ret ==-1 && errno != EEXIST) print_err("mkfifo err"); printf("before open\n"); fd= open(fifoname,open_mode); printf("after open\n"); if(fd == -1) print_err("open fail "); return
mkfifo1.c
#include "mkfifo.h" int main(int argc, char const *argv[]) { signal(SIGINT,fun); int ret=0; int fd =0; fd = create_open_fifo(FIFONAME,O_WRONLY);//只写方式打开会阻塞,所以要在其之前安装信号 char buf[100]={0}; while(1){ bzero(buf, sizeof(buf)); scanf("%s",buf); write(fd,buf,sizeof(buf)); } return 0; }
mkfifo2.c
#include "mkfifo.h" int main(int argc, char const *argv[]) { signal(SIGINT,fun); int ret=0; int fd =0; fd = create_open_fifo(FIFONAME,O_RDONLY);//只写方式打开会阻塞,所以要在其之前安装信号 char buf[100]={0}; int r=0; while(1){ bzero(buf, sizeof(buf)); r=read(fd,buf,sizeof(buf)); if(r == 0){ break; } printf("r=%d,receive: %s\n",r,buf ); } return 0; }
mkfifo3.c
#include "mkfifo.h" int main(int argc, char const *argv[]) { signal(SIGINT,fun); int ret=0; int fd =0; fd = create_open_fifo(FIFONAME,O_RDONLY);//只写方式打开会阻塞,所以要在其之前安装信号 char buf[100]={0}; int r=0; while(1){ bzero(buf, sizeof(buf)); r=read(fd,buf,sizeof(buf)); if(r == 0){ break; } printf("r=%d,receive: %s\n",r,buf ); } return 0; }
当open一个FIFO时,非阻塞标志(O_NONBLOCK)会产生下列影响
一般情况下(没有指定O_NONBLOCK),只读open要阻塞到某个其他进程为写而打开这个FIFO为止
类似的,只写open要阻塞到某个其他进程为读而打开它为止
如果指定了O_NONBLOCK,则只读open立即返回,但是如果没有进程为读而打开一个FIFO,那么只写open将返回-1,并将errno设置成ENXIO
(2)双向通信
同样的,使用一个"有名管道"是无法实现双向通信的,因为也涉及到抢数据的问题
所以双向通信时需要两个管道
mkfifo_double.h
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <strings.h> #include <string.h> #include <signal.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FIFONAME1 "./fifo1" #define FIFONAME2 "./fifo2" void fun(int signum){ remove(FIFONAME1); remove(FIFONAME2); exit(-1); } void print_err(char *estr){ perror(estr); exit(-1); } int create_open_fifo(char *fifoname,int open_mode){ int ret = -1; int fd =-1; ret = mkfifo(fifoname,0644); if( ret ==-1 && errno != EEXIST) print_err("mkfifo err"); fd= open(fifoname,open_mode); if(fd == -1) print_err("open fail "); return fd; }
mkfifo_double1.c
#include "mkfifo_double.h" /** 借助2个有名管道实现双工通信 管道1:进程1读,进程2写 管道2:进程1写,进程2读 由于每个进程的读都是阻塞的,所以,进程需要创建子进程 负责管道每个管道的读写 父进程读,子进程写 **/ int main(int argc, char const *argv[]) { int ret= -1; int fd1 =-1; int fd2 =-1; fd1= create_open_fifo(FIFONAME1,O_RDONLY); fd2= create_open_fifo(FIFONAME2,O_WRONLY); char buf[100]={0}; int r=0; ret =fork(); if(ret >0 ){ signal(SIGINT,fun); //父进程读管道1 while(1){ bzero(buf, sizeof(buf)); r=read(fd1,buf,sizeof(buf)); if(r == 0){ break; } printf("pid=%d,r=%d,receive: [%s]\n",getpid(),r,buf ); } }else if(ret == 0){ char pid_str[100]={0}; sprintf(pid_str,"---from %d:----",getpid()); //子进程写管道2 while(1){ bzero(buf, sizeof(buf)); scanf("%s",buf); strcat(buf,pid_str); write(fd2,buf,sizeof(buf)); } }else{ perror("fork err:"); exit(-1); } return 0; }
mkfifo_double2.c
#include "mkfifo_double.h" /** 借助2个有名管道实现双工通信 管道1:进程1读,进程2写 管道2:进程1写,进程2读 由于每个进程的读都是阻塞的,所以,进程需要创建子进程 负责管道每个管道的读写 父进程读,子进程写 **/ int main(int argc, char const *argv[]) { int ret= -1; int fd1 =-1; int fd2 =-1; fd1= create_open_fifo(FIFONAME1,O_WRONLY); fd2= create_open_fifo(FIFONAME2,O_RDONLY); char buf[100]={0}; int r=0; ret =fork(); if(ret >0 ){ signal(SIGINT,fun); //父进程读管道2 while(1){ bzero(buf, sizeof(buf)); r=read(fd2,buf,sizeof(buf)); if(r == 0){ break; } printf("pid=%d,r=%d,receive:[ %s]\n",getpid(),r,buf ); } }else if(ret == 0){ char pid_str[100]={0}; sprintf(pid_str,"---from %d:----",getpid()); //子进程写管道1 while(1){ bzero(buf, sizeof(buf)); scanf("%s",buf); strcat(buf,pid_str); write(fd1,buf,sizeof(buf)); } }else{ perror("fork err:"); exit(-1); } return 0; }
gcc mkfifo_double1.c -o mm1 gcc mkfifo_double2.c -o mm2

什么时候使用有名管道
(1)实现网状通信
面对众多进程网状通信,有名管道依然实现起来很吃力,所以基本也只适合于两个进程之间的通信。
你自己可以尝试下,看看能不能使用有名管道来实现多进程的网状通信,在实现过程中,你自己就会
发现,实现起来很困难
(2)什么时候合适使用有名管道
当两个进程需要通信时,不管是亲缘的还是非亲缘的,我们都可以使用有名管道来通信。
至于亲缘进程,也可以选择前面讲的无名管道来通信
(1)回顾有名管道双向通信
在使用有名管道实现双向通信时,由于读管道是阻塞读的,为了不让"读操作"阻塞"写操作",使用了父子进程来多线操作,
1)父进程这条线:读管道1
2)子进程这条线:写管道2
实际上凡是涉及到多线操作的,基本都使用多线程来实现,比如
1)主线程:读管道1
2)次线程:写管道2
(2)对比多进程和多线程各自使用的场合
事实上线程和进程都是并发运行的,但是线程和进程各自的使用的场合有所不同
1)线程
凡是涉及多线时,我们使用线程来并发实现,比如我们讲的“有名管道”双向通信的例子,这个多线操作理论
上就应该使用多线程来实现,只不过我们还没讲多线程而已。
因为多线使用线程更省计算机cpu和内存的开销。
也就是说创建出并发运行次线程的目的,是为了多线操作。
2)进程
一般情况下,我们的程序并不会涉及到多进程,当涉及多线操作时,我们会直接使用线程来并发实现
(a)那什么时候我们的程序才会涉及到多进程呢?
一个简单的判断标准就是,如果你发现你的程序必须要去运行一个新程序时,此时必须涉及到多进程,
因为此时如果你不创建一个子进程,你是没有办法来执行新程序的
新创建的子进程和父进程肯定是并发运行的,只不过这里并发运行的主要目的并不是为了多线操作,
而是为了单独的去执行新程序,执行新程序时,我们只能使用多进程来操作,你是没有办法使用多线程
来操作的,因为线程是不可能去执行一个新程序的
(b)一般开发的应用程序不涉及执行新程序
除非你开发的是比较大型框架,或者拥有众多功能套件的大型应用软件,在你的程序中必须开辟新的
子进程去执行具有独立功能的新程序,否则们自己写的程序一般都是单进程,根本不涉及开辟一个
并发运行的子进程,然后在子进程里面去执行新程序
也就是说创建一个并发执行的子进程的目的,是为了执行一个全新的程序

浙公网安备 33010602011771号