进程间的通信—管道


1、管道的类型

管道包括三种:

  • 普通管道PIPE: 通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用.
  • 流管道s_pipe: 去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输.
  • 命名管道name_pipe: 去除了第二种限制,可以在许多并不相关的进程之间进行通讯.

2、无名管道(也称管道)

管道是如何通信的:

  • 管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。
  • 一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。
  • 数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。
  • 管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞,另一进程放入数据。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞,直到另一进程读出数据。
  • 管道包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者可用于运行于同一系统中的任意两个进程间的通信。

管道是如何创建的:

从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如上图的PIPE。

  • 管道通信的实现细节
    在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面从而实现的。如下图

有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。

管道的读写:

管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()和管道写函数pipe_wrtie()。管道写函数通过将字节复制到 VFS索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。

当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:

  • 内存中有足够的空间可容纳所有要写入的数据;
  • 内存没有被读程序锁定。

如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。

管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。

管道的linux函数原型及例程:

#include <unistd.h>

int pipe(int pipefd[2]);

pipefd[0]用于读出数据,读取时必须关闭写入端,即close(pipefd[1]);
pipefd[1]用于写入数据,写入时必须关闭读取端,即close(pipefd[0])。

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

void read_data(int *);  //进程读函数
void write_data(int *); //进程写函数 

int main(int argc,char *argv[])
{
    int pipes[2],rc;
    pid_t pid;
        
    rc = pipe(pipes);   //创建管道                 
    if(rc == -1){
        perror("\npipes\n");
        exit(1);
    }
        
    pid = fork();   //创建进程 
    switch(pid){
        case -1:
            perror("\nfork\n");
            exit(1);
        case 0:
            read_data(pipes);   //相同的pipes
        default:
            write_data(pipes);  //相同的pipes
    }   
    return 0;
}

//进程读函数
void read_data(int pipes[])
{
    int c,rc;
    
    //由于此函数只负责读,因此将写描述关闭(资源宝贵)
    close(pipes[1]);
    
    //阻塞,等待从管道读取数据, int 转为 unsiged char 输出到终端
    while( (rc = read(pipes[0],&c,1)) > 0 ){        
        putchar(c);                                    
    }

    exit(0);
}

//进程写函数
void write_data(int pipes[])
{
    int c,rc;

    //关闭读描述字
    close(pipes[0]);                          

    while( (c=getchar()) > 0 ){
        rc = write( pipes[1], &c, 1);   //写入管道
        if( rc == -1 ){
            perror("Parent: write");
            close(pipes[1]);
            exit(1);
        }
    }

    close(pipes[1]);
    exit(0);
}

3、有名管道:

由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做有名管道(named PIPE)。

实现原理
FIFO (First in, First out)为一种特殊的文件类型, 它在文件系统中有对应的路径。 当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,有名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。

有名管道的linux函数原型及例程:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
int mknod(const char *pathname, mode_t mode, dev_t dev);

int mkfifo(const char *pathname, mode_t mode);
参数: 
    pathname :要创建文件的名称
    mode :权限
    
int mknod(const char *pathname, mode_t mode, dev_t dev);
参数:
    pathname :要创建文件的名称
    mode :文件类型与权限
    dev :该文件对应设备的设备号,只有当文件类型为 S_IFCHR 或 S_IFBLK 的时候该文件才有设备号,创建普通文件时传入0即可。
例:mknod(FIFO_FILE, S_IFIFO|0666, 0); 
    FIFO_FILE是一个字符指针,指向文件名,
    S_IFIFO表示要创建一个FIFO文件,
    0666表示该文件的权限是所有人可读可写,
    0表示该文件不是一个设备文件。
创建
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void filecopy(FILE *,char *);

int main(void)
{
    FILE *fp1;
    long int i = 100000;
    char buf[] = "I want to study Linux!\n";
    char *file1 = "data.txt";
    
    printf("begin!\n");
    
    if((fp1 = fopen(file1,"a+")) == NULL )
            printf("can't open %s\n",file1);
            
    while(i--)
        filecopy(fp1,buf);

    fclose(fp1);
    
    printf("over!\n");
    
    return 0;
}

void filecopy(FILE *ifp,char *buf)
{
    char c;
    int i,j;
    j = 0;
    i = strlen(buf)-1;  
    while(i--){
        putc(buf[j],ifp);
        j++;
    }
    putc('\n',ifp);
}
写入
#include <unistd.h>  
#include <stdlib.h>  
#include <fcntl.h>  
#include <limits.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <stdio.h>  
#include <string.h>  
  
int main()  
{  
    const char *fifo_name = "my_fifo";
    char *file1 = "data.txt";   
    int pipe_fd = -1;  
    int data_fd = -1;  
    int res = 0;  
    const int open_mode = O_WRONLY;  
    int bytes_sent = 0;  
    char buffer[PIPE_BUF + 1];  
  
    if(access(fifo_name, F_OK) == -1)  
    {  
        //管道文件不存在, 创建命名管道
        res = mkfifo(fifo_name, 0777);  
        if(res != 0)  
        {  
            fprintf(stderr, "Could not create fifo %s\n", fifo_name);  
            exit(EXIT_FAILURE);  
        }  
    }  
  
    printf("Process %d opening FIFO O_WRONLY\n", getpid());  
    //以只写阻塞方式打开FIFO文件,以只读方式打开数据文件  
    pipe_fd = open(fifo_name, open_mode);  
    data_fd = open(file1, O_RDONLY);  
    printf("Process %d result %d\n", getpid(), pipe_fd);  
  
    if(pipe_fd != -1)  
    {  
        int bytes_read = 0;  
        //向数据文件读取数据  
        bytes_read = read(data_fd, buffer, PIPE_BUF);  
        buffer[bytes_read] = '\0';  
        while(bytes_read > 0)  
        {  
            //向FIFO文件写数据  
            res = write(pipe_fd, buffer, bytes_read);  
            if(res == -1)  
            {  
                fprintf(stderr, "Write error on pipe\n");  
                exit(EXIT_FAILURE);  
            }  
            //累加写的字节数,并继续读取数据  
            bytes_sent += res;  
            bytes_read = read(data_fd, buffer, PIPE_BUF);  
            buffer[bytes_read] = '\0';  
        }  
        close(pipe_fd);  
        close(data_fd);  
    }  
    else  
        exit(EXIT_FAILURE);  
  
    printf("Process %d finished\n", getpid());  
    exit(EXIT_SUCCESS);  
} 
读取
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <fcntl.h>  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <limits.h>  
#include <string.h>  
  
int main()  
{  
    const char *fifo_name = "my_fifo";  
    int pipe_fd = -1;  
    int data_fd = -1;  
    int res = 0;  
    int open_mode = O_RDONLY;  
    char buffer[PIPE_BUF + 1];  
    int bytes_read = 0;  
    int bytes_write = 0;  
    //清空缓冲数组  
    memset(buffer, '\0', sizeof(buffer));  
  
    printf("Process %d opening FIFO O_RDONLY\n", getpid());  
    //以只读阻塞方式打开管道文件,注意与fifowrite.c文件中的FIFO同名  
    pipe_fd = open(fifo_name, open_mode);  
    //以只写方式创建保存数据的文件  
    data_fd = open("DataFormFIFO.txt", O_WRONLY|O_CREAT, 0644);  
    printf("Process %d result %d\n",getpid(), pipe_fd);  
  
    if(pipe_fd != -1)  
    {  
        do  
        {  
            //读取FIFO中的数据,并把它保存在文件DataFormFIFO.txt文件中  
            res = read(pipe_fd, buffer, PIPE_BUF);  
            bytes_write = write(data_fd, buffer, res);  
            bytes_read += res;  
        }while(res > 0);  
        close(pipe_fd);  
        close(data_fd);  
    }  
    else  
        exit(EXIT_FAILURE);
  
    printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
    exit(EXIT_SUCCESS);
} 
优质内容筛选与推荐>>
1、数独游戏的难度等级分析及求解算法研究3——数独解法研究及附录
2、【串线篇】sql映射文件-分布查询(下)cellection的1-n
3、VS2012的3个离线补丁:WinLibJS_VSE,Update1,Update2,集合包2.14G_7z
4、SoftLayer®凭借Flex Images™消融物理与虚拟服务器之间的界线
5、wcf问题之找不到终结点或不匹配


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号