进程间通信之-管道--linux内核剖析(八)

管道


管道是一种两个进程间进行单向通信的机制。

因为管道传递数据的单向性,管道又称为半双工管道。

管道的这一特点决定了器使用的局限性。管道是Linux支持的最初Unix IPC形式之一,具有以下特点:

  • 数据只能由一个进程流向另一个进程(其中一个读管道,一个写管道);如果要进行双工通信,需要建 立两个管道。

  • 管道只能用于父子进程或者兄弟进程间通信。,也就是说管道只能用于具有亲缘关系的进程间通信。

    除了以上局限性,管道还有其他一些不足,如管道没有名字(匿名管道),管道的缓冲区大小是受限制的。管道所传输的是无格式的字节流。这就需要管道输入方和输出方事先约定好数据格式。虽然有那么多不足,但对于一些简单的进程间通信,管道还是完全可以胜任的。

信号和消息的区别


我们知道,进程间的信号通信机制在传递信息时是以信号为载体的,但管道通信机制的信息载体是消息。那么信号和消息之间的区别在哪里呢? 

首先,在数据内容方面,信号只是一些预定义的代码,用于表示系统发生的某一状况;消息则为一组连续语句或符号,不过量也不会太大。在作用方面,信号担任进程间少量信息的传送,一般为内核程序用来通知用户进程一些异常情况的发生;消息则用于进程间交换彼此的数据。

在发送时机方面,信号可以在任何时候发送;信息则不可以在任何时刻发送。在发送者方面,信号不能确定发送者是谁;信息则知道发送者是谁。在发送对象方面,信号是发给某个进程;消息则是发给消息队列。在处理方式上,信号可以不予理会;消息则是必须处理的。在数据传输效率方面,信号不适合进大量的信息传输,因为它的效率不高;消息虽然不适合大量的数据传送,但它的效率比信号强,因此适于中等数量的数据传送。

管道-流管道-命名管道的区别


我们知道,命名管道和管道都可以在进程间传送消息,但它们也是有区别的。

管道这种通讯方式有两种限制,

  • 一是半双工的通信,数据只能单向流动

  • 二是只能在具有亲缘关系的进程间使用。

进程的亲缘关系通常是指父子进程关系。

流管道s_pipe去除了第一种限制,可以双向传输。

管道可用于具有亲缘关系进程间的通信,命名管道name_pipe克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

管道技术只能用于连接具有共同祖先的进程,例如父子进程间的通信,它无法实现不同用户的进程间的信息共享。再者,管道不能常设,当访问管道的进程终止时,管道也就撤销。这些限制给它的使用带来不少限制,但是命名管道却克服了这些限制。

命名管道也称为FIFO,是一种永久性的机构。FIFO文件也具有文件名、文件长度、访问许可权等属性,它也能像其它Linux文件那样被打开、关闭和删除,所以任何进程都能找到它。换句话说,即使是不同祖先的进程,也可以利用命名管道进行通信。

如果想要全双工通信,那最好使用Sockets API(linux并不支持s_pipe流管道)。下面我们分别介绍通过管道命令解析管道的技术模型,然后详细说明用来进行管道编程的编程接口和系统级命令。

管道技术模型


管道技术是Linux操作系统中历来已久的一种进程间通信机制。

所有的管道技术,无论是半双工的匿名管道,还是命名管道,它们都是利用FIFO排队模型来指挥进程间的通信。

对于管道,我们可以形象地把它们当作是连接两个实体的一个单向连接器。

使用管道进行通信时,两端的进程向管道读写数据是通过创建管道时,系统设置的文件描述符进行的。从本质上说,管道也是一种文件,但它又和一般的文件有所不同,可以克服使用文件进行通信的两个问题,这个文件只存在内存中。

通过管道通信的两个进程,一个进程向管道写数据,另外一个从中读数据。写入的数据每次都添加到管道缓冲区的末尾,读数据的时候都是从缓冲区的头部读出数据的。

管道命令详解


参见

linux shell 管道命令(pipe)使用及与shell重定向区别

例如,请看下面的命令

管道符号,是unix功能强大的一个地方,符号是一条竖线:”|”,

用法: command 1 | command 2

他的功能是把第一个命令command 1执行的结果作为command 2的输入传给command 2

注意:

  1. 管道命令只处理前一个命令正确输出,不处理错误输出

  2. 管道命令右边命令,必须能够接收标准输入流命令才行。

例如:
ls -l | more

该命令列出当前目录中的任何文档,并把输出送给more命令作为输入,more命令分页显示文件列表。

管道命令与重定向区别


区别是:

  • 左边的命令应该有标准输出 | 右边的命令应该接受标准输入

  • 左边的命令应该有标准输出 > 右边只能是文件

  • 左边的命令应该需要标准输入 < 右边只能是文件

  • 管道触发两个子进程执行”|”两边的程序;而重定向是在一个进程内执行

重定向与管道在使用时候很多时候可以通用

其实,在shell里面,经常是条条大路通罗马的。

一般如果是命令间传递参数,还是管道的好,如果处理输出结果需要重定向到文件,还是用重定向输出比较好。

前面的例子实际上就是在两个命令之间建立了一根管道(有时我们也将之称为命令的流水线操作)。

第一个命令ls执行后产生的输出作为了第二个命令more的输入。

这是一个半双工通信,因为通信是单向的。两个命令之间的连接的具体工作,是由内核来完成的。

当然内核也为我们提供了一套接口(系统调用),除了命令之外,应用程序也可以使用管道进行连接。

管道编程技术


参考 http://www.cppblog.com/jackdongy/archive/2013/01/07/197055.html

http://blog.chinaunix.net/uid-26495963-id-3066282.html

管道的接口


无名管道pipe


创建管道pipe

  1. 函数原型`int pipe(int filedes[2]);

    • pipe()会建立管道,并将文件描述词由参数 filedes 数组返回。

    • filedes[0]为管道里的读取端,所以pipe用read调用的。

    • filedes[1]则为管道的写入端。使用write进行写入操作。

  2. 返回值

    • 若成功则返回零,否则返回-1,错误原因存于 errno 中。
  3. 错误代码

    • EMFILE 进程已用完文件描述词最大量

    • ENFILE 系统已无文件描述词可用。

    • EFAULT 参数 filedes 数组地址不合法。

当调用成功时,函数pipe返回值为0,否则返回值为-1。成功返回时,数组fds被填入两个有效的文件描述符。数组的第一个元素中的文件描述符供应用程序读取之用,数组的第二个元素中的文件描述符可以用来供应用程序写入。

关闭管道close

  • 关闭管道只是将两个文件描述符关闭即可,可以使用普通的close函数逐个关闭。

如果管道的写入端关闭,但是还有进程尝试从管道读取的话,将被返回0,用来指出管道已不可用,并且应当关闭它。如果管道的读出端关闭,但是还有进程尝试向管道写入的话,试图写入的进程将收到一个SIGPIPE信号,至于信号的具体处理则要视其信号处理程序而定了。

dup函数和dup2函数


dup和dup2也是两个非常有用的调用,它们的作用都是用来复制一个文件的描述符。

它们经常用来重定向进程的stdin、stdout和stderr。

这两个函数的原型如下所示:

#include <unistd.h>

int dup( int oldfd );

int dup2( int oldfd, int targetfd )

dup函数

利用函数dup,我们可以复制一个描述符。传给该函数一个既有的描述符,它就会返回一个新的描述符,这个新的描述符是传给它的描述符的拷贝。这意味着,这两个描述符共享同一个数据结构。

例如,如果我们对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。下面是用来说明dup函数使用方法的代码片段:

int fd1, fd2;
fd2 = dup( fd1 );

需要注意的是,我们可以在调用fork之前建立一个描述符,这与调用dup建立描述符的效果是一样的,子进程也同样会收到一个复制出来的描述符。

dup2函数

dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的id。

dup2函数成功返回时,目标描述符(dup2函数的第二个参数)将变成源描述符(dup2函数的第一个参数)的复制品,换句话说,两个文件描述符现在都指向同一个文件,并且是函数第一个参数指向的文件。

下面我们用一段代码加以说明:


int oldfd;
oldfd = open("app_log", (O_RDWR | O_CREATE), 0644 );
dup2( oldfd, 1 );
close( oldfd );

我们打开了一个新文件,称为“app_log”,并收到一个文件描述符,该描述符叫做fd1。我们调用dup2函数,参数为oldfd和1,这会导致用我们新打开的文件描述符替换掉由1代表的文件描述符(即stdout,因为标准输出文件的id为1)。任何写到stdout的东西,现在都将改为写入名为“app_log”的文件中。需要注意的是,dup2函数在复制了oldfd之后,会立即将其关闭,但不会关掉新近打开的文件描述符,因为文件描述符1现在也指向它。

命名管道mkfifo


mkfifo函数的作用是在文件系统中创建一个文件,该文件用于提供FIFO功能,即命名管道。前边讲的那些管道都没有名字,因此它们被称为匿名管道,或简称管道。对文件系统来说,匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。而命名管道是一个可见的文件,因此,它可以用于任何两个进程之间的通信,不管这两个进程是不是父子进程,也不管这两个进程之间有没有关系。Mkfifo函数的原型如下所示:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode );

mkfifo函数需要两个参数,第一个参数(pathname)是将要在文件系统中创建的一个专用文件。第二个参数(mode)用来规定FIFO的读写权限。Mkfifo函数如果调用成功的话,返回值为0;如果调用失败返回值为-1。下面我们以一个实例来说明如何使用mkfifo函数建一个fifo,具体代码如下所示:

    int ret;

    ret = mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );
    if (ret == 0)
    {
      // 成功建立命名管道
    }
    else
    {
      // 创建命名管道失败
    }

在这个例子中,利用/tmp目录中的cmd_pipe文件建立了一个命名管道(即fifo)。之后,就可以打开这个文件进行读写操作,并以此进行通信了。命名管道一旦打开,就可以利用典型的输入输出函数从中读取内容。举例来说,下面的代码段向我们展示了如何通过fgets函数来从管道中读取内容:

    pfp = fopen( "/tmp/cmd_pipe", "r" );
    ret = fgets( buffer, MAX_LINE, pfp );

我们还能向管道中写入内容,下面的代码段向我们展示了利用fprintf函数向管道写入的具体方法:

    pfp = fopen( "/tmp/cmd_pipe", "w+ );
    ret = fprintf( pfp, "Here’s a test string!\n" );

对命名管道来说,除非写入方主动打开管道的读取端,否则读取方是无法打开命名管道的。Open调用执行后,读取方将被锁住,直到写入方出现为止。尽管命名管道有这样的局限性,但它仍不失为一种有效的进程间通信工具。

无名管道


无名管道为建立管道的进程及其子孙提供一条以比特流方式传送消息的通信管道。

该管道再逻辑上被看作管道文件,在物理上则由文件系统的高速缓冲区构成,而很少启动外设。

发送进程利用文件系统的系统调用write(fd[1],buf,size),把buf 中的长度为size字符的消息送入管道入口fd[1]

接收进程则使用系统调用read(fd[0],buf,size)从管道出口fd[0]出口读出size字符的消息置入buf中。

这里,管道按FIFO(先进先出)方式传送消息,且只能单向传送消息(如图)。

这里写图片描述

无名管道pipe读写


管道用于不同进程间通信。通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程创建的管道。注意事项:必须在系统调用fork()前调用pipe(),否则子进程将不会继承文件描述符。否则,会创建两个管道,因为父子进程共享同一段代码段,都会各自调用pipe(),即建立两个管道,出现异常错误。

无名管道读写过程如图所示

这里写图片描述

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

#define MAX_DATA_LEN 256
#define DELAY_TIME 1

int main(void)
{
    pid_t       pid;
    char        buf[MAX_DATA_LEN];
    const char  *data="Pipe Test program";
    int         real_read, real_write;
    int         pipe_fd[2];

    memset((void*)buf, 0, sizeof(buf));

    if(pipe(pipe_fd) < 0)
    {
        perror("Pipe create error...\n");
        exit(1);
    }
    else
    {
        printf("Pipe create success...\n");
    }

    if ((pid = fork()) < 0)
    {
        perror("Fork error!\n");

        exit(1);
    }
    else if (pid == 0)
    {
        printf("I am the child process, PID = %d, PPID = %d", getpid(), getppid());

        close(pipe_fd[1]);
        sleep(DELAY_TIME * 3);

        if ((real_read=read(pipe_fd[0],buf, MAX_DATA_LEN)) > 0)
        {
            printf("Child receive %d bytes from pipe: '%s'.\n", real_read, buf);
        }

        close(pipe_fd[0]);

        exit(0);
    }
    else
    {
        printf("I am the parent process, PID = %d, PPID = %d", getpid(), getppid());

        close(pipe_fd[0]);
        sleep(DELAY_TIME);

        if ((real_write = write(pipe_fd[1], data, strlen(data))) > 0)
        {
            printf("Parent write %d bytes into pipe: '%s'.\n", real_write, data);
        }

        close(pipe_fd[1]);
        waitpid(pid,NULL,0);

        exit(0);
    }

    return EXIT_SUCCESS;
}

多进程管道读写


建立一个管道。同时,父进程生成子进程P1,P2,这两个进程分别向管道中写入各自的字符串,父进程读出它们(如图)。

#include < stdio.h>  
main( )  
{  
  int I,r,p1,p2,fd[2];  
  char buf[50],s[50];  
  pipe(fd); /*父进程建立管道*/  
  while((p1=fork()) = = -1);  
  if(p1 = = 0 )  
  {  
     lockf(fd[1],1,0); /*加锁锁定写入端*/  
     sprinrf(buf, ”child process P1 is sending messages! \n”);  
     printf(“child process P1! \n”);  
     write(fd[1],buf, 50); /*把buf中的50个字符写入管道*/  
     sleep(5);  
     lockf(fd[1],0,0); /*释放管道写入端*/  
     exit(0); /*关闭P1*/  
  }  
  else /*从父进程返回,执行父进程*/  
{  
    while((p2=fork()) = = -1); /*创建子进程P2,失败时循环*/  
    if(p2 = = 0) /*从子进程P2返回,执行P2*/  
    {  
       lockf(fd[1],1,0); / *锁定写入端*/  
       sprintf(buf, ”child process P2 is sending messages \n”);  
       printf(“child process P2 ! \n”);  
       write(fd[1],buf,50); /*把buf中字符写入管道*/  
       sleep(5); /* 睡眠等待*/  
       lockf (fd[1],0,0); /*释放管道写入端*/  
       exit(0); /*关闭P2*/  
     }  
    wait(0);  
    if (r = read(fd[0],s 50) = = -1)  
      printf(“can’t read pipe \n”);  
    else printf(“%s\n”,s);  
    wait(0);  
    if(r = read(fd[0],s,50)= = -1)  
      printf(“can’t read pipe \n”);  
    else printf((“%s\n”,s);  
    exit(0);  
}  
}  

使用dup函数实现指令流水


我们的子进程把它的输出重定向的管道的输入,然后,父进程将它的输入重定向到管道的输出。这在实际的应用程序开发中是非常有用的一种技术。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    int pfds[2];
    if ( pipe(pfds) == 0 )
    {

        if ( fork() == 0 )
        {
            close(1);
            dup2( pfds[1], 1 );
            close( pfds[0] );
            execlp( "ls", "ls", "-1", NULL );

        }
        else
        {
            close(0);
            dup2( pfds[0], 0 );
            close( pfds[1] );
            execlp( "wc", "wc", "-l", NULL );
        }
    }

    return 0;
}

命名管道


write端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

#define FIFO        "myfifo"
#define BUFF_SIZE   1024

int main(int argc,char* argv[])
{
    char    buff[BUFF_SIZE];
    int     real_write;
    int     fd;

    if(argc <= 1)
    {
        printf("Usage: %s string\n", argv[0]);

        exit(1);
    }
    else
    {
        printf("%s at PID = %d\n", argv[0], getpid());
    }

    sscanf(argv[1], "%s", buff);

    // 测试FIFO是否存在,若不存在,mkfifo一个FIFO
    if(access(FIFO, F_OK) == -1)
    {
        if((mkfifo(FIFO, 0666) < 0) && (errno != EEXIST))
        {
            printf("Can NOT create fifo file!\n");

            exit(1);
        }
    }

    //  调用open以只写方式打开FIFO,返回文件描述符fd
    if((fd = open(FIFO, O_WRONLY)) == -1)
    {
        printf("Open fifo error!\n");

        exit(1);
    }

    //  调用write将buff写到文件描述符fd指向的FIFO中
    if ((real_write = write(fd, buff, BUFF_SIZE)) > 0)
    {
        printf("Write into pipe: '%s'.\n", buff);
        exit(1);
    }

    close(fd);
    exit(0);

}

read端

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

#define FIFO "myfifo"
#define BUFF_SIZE 1024

int main(int argc, char *argv[])
{
    char    buff[BUFF_SIZE];
    int     real_read;
    int     fd;

    printf("%s at PID = %d  ", argv[0], getpid());

    //  access确定文件或文件夹的访问权限。即,检查某个文件的存取方式
    //  如果指定的存取方式有效,则函数返回0,否则函数返回-1
    //  若不存在FIFO,则创建一个
    if(access(FIFO, F_OK) == -1)
    {
        if((mkfifo(FIFO, 0666) < 0) && (errno != EEXIST))
        {
            printf("Can NOT create fifo file!\n");
            exit(1);
        }
    }

    //  以只读方式打开FIFO,返回文件描述符fd
    if((fd = open(FIFO, O_RDONLY)) == -1)
    {
        printf("Open fifo error!\n");
        exit(1);
    }

    //  调用read将fd指向的FIFO的内容,读到buff中,并打印
    while(1)
    {
        memset(buff, 0, BUFF_SIZE);

        if ((real_read = read(fd, buff, BUFF_SIZE)) > 0)
        {
            printf("Read from pipe: '%s'.\n",buff);
        }
    }

    close(fd);
    exit(0);
}
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本PDF电子书包含上下两册,共1576页,带目录,高清非扫描版本。 作者: 毛德操 胡希明 丛书名: Linux内核源代码情景分析 出版社:浙江大学出版社 目录 第1章 预备知识 1.1 Linux内核简介. 1.2 Intel X86 CPU系列的寻址方式 1.3 i386的页式内存管理机制 1.4 Linux内核源代码中的C语言代码 1.5 Linux内核源代码中的汇编语言代码 第2章 存储管理 2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理页面的使用和周转 2.7 物理页面的分配 2.8 页面的定期换出 2.9 页面的换入 2.10 内核缓冲区的管理 2.11 外部设备存储空间的地址映射 2.12 系统调用brk() 2.13 系统调用mmap() 第3章 中断、异常和系统调用 3.1 X86 CPU对中断的硬件支持 3.2 中断向量表IDT的初始化 3.3 中断请求队列的初始化 3.4 中断的响应和服务 3.5 软中断与Bottom Half 3.6 页面异常的进入和返回 3.7 时钟中断 3.8 系统调用 3.9 系统调用号与跳转表 第4章 进程与进程调度 4.1 进程四要素 4.2 进程三部曲:创建、执行与消亡 4.3 系统调用fork()、vfork()与clone() 4.4 系统调用execve() 4.5 系统调用exit()与wait4() 4.6 进程的调度与切换 4.7 强制性调度 4.8 系统调用nanosleep()和pause() 4.9 内核中的互斥操作 第5章 文件系统 5.1 概述 5.2 从路径名到目标节点 5.3 访问权限与文件安全性 5.4 文件系统的安装和拆卸 5.5 文件的打开与关闭 5.6 文件的写与读 5.7 其他文件操作 5.8 特殊文件系统/proc 第6章 传统的Unix进程间通信 6.1 概述 6.2 管道和系统调用pipe() 6.3 命名管道 6.4 信号 6.5 系统调用ptrace()和进程跟踪 6.6 报文传递 6.7 共享内存 6.8 信号量 第7章基于socket的进程间通信 7.1系统调用socket() 7.2函数sys—socket()——创建插口 7.3函数sys—bind()——指定插口地址 7.4函数sys—listen()——设定server插口 7.5函数sys—accept()——接受连接请求 7.6函数sys—connect()——请求连接 7.7报文的接收与发送 7.8插口的关闭 7.9其他 第8章设备驱动 8.1概述 8.2系统调用mknod() 8.3可安装模块 8.4PCI总线 8.5块设备的驱动 8.6字符设备驱动概述 8.7终端设备与汉字信息处理 8.8控制台的驱动 8.9通用串行外部总线USB 8.10系统调用select()以及异步输入/输出 8.11设备文件系统devfs 第9章多处理器SMP系统结构 9.1概述 9.2SMP结构中的互斥问题 9.3高速缓存与内存的一致性 9.4SMP结构中的中断机制 9.5SMP结构中的进程调度 9.6SMP系统的引导 第10章系统引导和初始化 10.1系统引导过程概述 10.2系统初始化(第一阶段) 10.3系统初始化(第二阶段) 10.4系统初始化(第三阶段) 10.5系统的关闭和重引导

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值