首页 > 技术文章 > 第八章 进程间通信 [notice pipe at RIL.pdf] [popen] [mkfifo]

jimwind 2013-12-05 16:11 原文

前言:

管道只能用于父子进程或兄弟进程。pipe创建管道,fork创建子进程,完全继承管道,可以理解为对同一个通道拥有读写权(见图8.4),父子进程分别关闭其中一个不同的权限,形成父读子写或父写子读的一个通道。

popen

mkfifo 用于命名管道,但权限问题目前还没有搞清楚,难道管道必须开放读写权限。任何进程都可以访问。

========================================================================

8.1 Linux下进程间通信概述

Linux下的进程间通信基本上是从UNIX平台继承下来的,而AT&T和BSD对UNIX都做出了重大贡献,但侧重点不同。

1.AT&T对UNIX早期的进程间通信手段进行了系统的改进和扩充,形成了"system V IPC",其通信进程主要局限在单个计算机内。

2.BSD跳过了单机局限,形成了套接口(socket)的进程间通信机制。

(1+2)Linux则把两者的优势都继承了下来。

 

1.UNIX进程间通信(IPC)方式包括管道、FIFO、信号。

2.System V 进程间通信(IPC)包括System V 消息队列、System V 信号灯、System V 共享内存区。

3.Posix 进程间通信(IPC)包括Posix消息队列、Posix信号灯、Posix共享内存区。

现在Linux中使用较多的进程间通信方式主要有以下几种:

(1)管道及命名管道

(2)信号:信号是在软件层次上对中断机制的一种模拟。

(3)消息队列:

(4)共享内存:可以说这是最有用的进程间通信方式。这种通信方式需要依靠同步机制,如互斥锁和信号量等。

(5)信号量:主要作为进程间以及同一进程不同线程之间的同步手段。<--第10章中单独介绍

(6)套接字:这是一种更为一般的进程间通信机制,它可用于不同机器之间的进程间通信

管道可用于具有亲缘关系进程间的通信

命名管道,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

 

 

8.2 管道通信

8.2.1 管道概述

它是把一个程序的输出直接连接到另一个程序的输入。ps -ef | grep ntp

*它只能用于父子进程或者兄弟进程

*它是一个半双工的通信模式,具有固定的读端和写端

*管理也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但它不是普通文件,并不属于其他任何文件系统,并且只存在内存中。

8.2.2 管道的创建与关闭

1.管道创建与关闭说明

管理是基于文件描述符的通信方式,当一个管道建立时,它会创建两个文件描述符fds[0]和fds[1],其中fds[0]固定用于读管道,而fds[1]固定用于写管道。

就构成了一个半双工的通道。

管道关闭时,只需要将这两个文件描述符关闭即可,可使用普通的close函数关闭各个文件描述符

2.管道创建函数

pipe.c
#include<unistd.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
 
int main(){
    int pipe_fd[2];
    if(pipe(pipe_fd)<0){
        printf("pipe create errorn");
        return -1;
    } else
        printf("pipe create successn");
 
    close(pipe_fd[0]);
    close(pipe_fd[1]);
 
}

 

8.2.3管道读写

1.管道读写说明
用pipe函数创建的管道两端处于一个进程中,由于管道主要用于在不同进程间通信的,因此这在实际应用中没有太大意义。
实际上,通常先创建一个管理,再通过fork()函数创建一子进程,该子进程会继承父进程所创建的管道(fork是完全复制,且有两个返回值,分别是父子进程)
父进程fd[0]读管道,fd[1]写管道
子进程fd[0]读管道,fd[1]写管道
如果关闭父进程fd[1]和子进程fd[0],就在父子进程间建立一条“子进程写入父进程读”的通道
如果关闭父进程fd[0]和子进程fd[1],就在父子进程间建立一条“父进程写,子进程读”的通道

#include<unistd.h>
#include<sys/types.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>

int main(){
    int pipe_fd[2];
    pid_t pid;
    char buf_r[100];
    char* p_wbuf;
    int r_num;

    memset(buf_r, 0, sizeof(buf_r));
    if(pipe(pipe_fd)<0){
        printf("pipe create error\n");
        return -1; 
    }   
    printf("1   getpid() = %d\n", getpid());
    printf("1   getppid() = %d\n", getppid());
    if((pid = fork()) == 0){ // call fork will return twice
        printf("\n");
        close(pipe_fd[1]);//run at child
        printf("2   getpid() = %d\n", getpid());
        printf("2   getppid() = %d\n", getppid());
        sleep(2);
        if((r_num=read(pipe_fd[0], buf_r, 100))>0){
            printf("%d numbers read from the pipe is %s\n", r_num, buf_r);
        }   
        close(pipe_fd[0]);
        exit(0);
    } else if(pid > 0){ 
        close(pipe_fd[0]);//run at parent
        printf("3   getpid() = %d\n", getpid());
        printf("3   getppid() = %d\n", getppid());
        if(write(pipe_fd[1], "Hello", 5)!=-1)
            printf("parent write1 success!\n");
        if(write(pipe_fd[1], " Pipe", 5)!=-1)
            printf("parent write2 success!\n");
        close(pipe_fd[1]);
        sleep(3);
        waitpid(pid,NULL, 0); 
        exit(0);
    }
}
pipe_rw

waitpid,即等到子进程pid退出后才退出。

3.管道读写注意点

只有在管道的读端存在时向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号。(通常是Broken pipe错误,以后遇到Broken pipe时,可以知道,管道读端已经不存在了)。

向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。

父子进程在运行时,它们的先后次序并不能保证,因此,在这里为了保证父进程已经关闭了读描述符,可在子进程中调用sleep函数。

 

8.2.4 标准流管道

1.标准流管道函数说明

popen
include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#define BUFSIZE 1000
int main(){
    FILE *fp;
    char *cmd = "ps -ef";
    char buf[BUFSIZE];
    
    if((fp = popen(cmd,"r")) == NULL)
        perror("popen");
    while((fgets(buf, BUFSIZE, fp)) != NULL)
        printf("%s", buf);
    pclose(fp);
    exit(0);
}

 

8.2.5 FIFO

1.有名管道说明

有名管道可以使互不相关的两个进程实现彼此通信

该管道可以通过路径名来指出,并且在文件系统中是可见的。

在建立了管道之后,两个进程就可以把它当作普通文件一样进程读写操作,使用非常方便。

不过值得注意的是,FIFO是严格遵循先进先出规则的,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如lseek()等文件定位操作。

有名管道的创建可以使用函数mkfifo(),该函数类似文件中的open()操作,可以指定管道的路径和打开的模式。

[用户还可以在命令行使用"mknod 管道名 p"来创建有名管道]

在创建管道成功之后,就可以使用open read write这些函数了。

对于为读而打开的管道可在open中设置O_RDONLY,对为写而打开的管道可在open中设置O_WRONLY

在这里与普通文件不同的是阻塞问题。在管道中读写中却有阻塞的可能,这里的非阻塞标志可以在open函数中设定为O_NONBLOCK

对于读进程
• 若该管道是阻塞打开,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞直到有数据写入。
• 若该管道是非阻塞打开,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。
对于写进程
• 若该管道是阻塞打开,则写进程而言将一直阻塞直到有读进程读出数据。
• 若该管道是非阻塞打开,则当前 FIFO 内没有读操作,写进程都会立即执行读操作。

2. mkfifo函数格式

3.使用实例

#include<sys/types.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
#define MYFIFO  "/tmp/myfifo"
#define MAX_BUFFER_SIZE     PIPE_BUF
int main()
{
  char buff[MAX_BUFFER_SIZE];
  int fd;
  int nread;
  //判断管道是否存在,如果不存在则创建
  if(access(MYFIFO, F_OK) == -1)
  {
    if((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
    {
      printf("cannot creat fifo file!\n");
      exit(1);
    }
  }
  fd = open(MYFIFO, O_RDONLY);//打开管道,只读阻塞方式
  if(fd == -1)
  {
    printf("open fifo file error!\n");
    exit(1);
  }
  while(1)
  {
    memset(buff, 0, sizeof(buff));
    if((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)//读管道
    {
      printf("read '%s' from FIFO\n", buff);
    }
  }
  close(fd);//关闭
  exit(0);
}
read
#include<sys/types.h>
#include<sys/stat.h>
#include<errno.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
#define MYFIFO  "/tmp/myfifo"
#define MAX_BUFFER_SIZE     PIPE_BUF
int main(int argc, char* argv[])
{
  char buff[MAX_BUFFER_SIZE];
  int fd;
  int nwrite;
  if(argc <= 1)
  {
    printf("usage: ./write string!\n");
    exit(1);
  }
  sscanf(argv[1], "%s", buff);
  fd = open(MYFIFO, O_WRONLY);//打开管道,写阻塞方式
  if(fd == -1)
  {
    printf("open fifo file error!\n");
    exit(1);
  }
  if((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0)//写管道
  {
    printf("write '%s' to FIFO!\n ", buff);
  }
  close(fd);//关闭
  exit(0);
}
write

 

8.3 信号通信

8.3.1信号概述

信号是UINX中所使用的进程通信的一种最古老的方法,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式。

信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。

它可以在任何时候发给某一进程,而无需知道该进程的状态。

 

 

 

 

推荐阅读