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)一般开发的应用程序不涉及执行新程序

  除非你开发的是比较大型框架,或者拥有众多功能套件的大型应用软件,在你的程序中必须开辟新的
  子进程去执行具有独立功能的新程序,否则们自己写的程序一般都是单进程,根本不涉及开辟一个
  并发运行的子进程,然后在子进程里面去执行新程序

  也就是说创建一个并发执行的子进程的目的,是为了执行一个全新的程序

posted @ 2018-09-15 21:39  H&K  阅读(233)  评论(0)    收藏  举报