进程间通信——管道

原文地址:点击打开链接

一.管道容量:管道容量分为pipi capacity 和 pipe_buf .这两者的区别在于pipe_buf定义的是内核管道缓冲区的大小,这个值的大小是由内核设定的,这个值仅需一条命令就可以查到;而pipe capacity指的是管道的最大值,即容量,是内核内存中的一个缓冲区。pipe_buf: 命令:ulimit -a在终端输入该命令就会出现如下一表:


管道容量 sizeof(pipe_buf)= 512 bytes* 8 = 4kb

pipi capacity:

 当管道满的时候    O_NONBLOCK disable: write调用阻塞,直到有进程读走数据

                   O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

 pipi capacity的大小需要写一段程序去检测,代码如下所示:

#include<sys/stat.h>  
#include<unistd.h>  
#include<fcntl.h>  
#include<stdio.h>  
#include<stdlib.h>  
#include<errno.h>  
#include<string.h>  
#include<signal.h>  
#define ERR_EXIT(m) \  
    do { \  
        perror(m); \  
        exit(EXIT_FAILURE); \  
    } while(0)  
  
int main(int argc, char *argv[])  
{  
    int pipefd[2];  
    if (pipe(pipefd) == -1)  
        ERR_EXIT("pipe error");  
  
    int ret;  
    int count = 0;  
    int flags = fcntl(pipefd[1], F_GETFL);  
    fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK); // 设置为非阻塞  
    while (1)  
    {  
        ret = write(pipefd[1], "A", 1);  
        if (ret == -1)  
        {  
            printf("err=%s\n", strerror(errno));  
            break;  
        }  
  
        count++;  
    }  
    printf("count=%d\n", count); //管道容量  
  
    return 0;  
}
运行结果如下所示:


由此图可知 pipe capacity的大小为 65536

二.管道缓冲区

1、管道(pipe)

管道是进程间通信的主要手段之一。一个管道实际上就是个只存在于内存中的文件,对这个文件的操作要通过两个已经打开文件进行,它们分别代表管道的两端。管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构。根据管道的适用范围将其分为:无名管道和命名管道。

●     无名管道

主要用于父进程与子进程之间,或者两个兄弟进程之间。在Linux系统中可以通过系统调用建立起一个单向的通信管道,且这种关系只能由父进程来建立。因此,每个管道都是单向的,当需要双向通信时就需要建立起两个管道。管道两端的进程均将该管道看做一个文件,一个进程负责往管道中写内容,而另一个从管道中读取。这种传输遵循“先入先出”(FIFO)的规则。

●     命名管道

命名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。

管道通讯如下图:


管道通讯带来的问题:管道容量不像其他文件一样不加检索的增长


 1.使用单个固定缓冲区也会带来问题,比如在写管道时可能变满,当这种情况发生时,随后对管道的write()调用将默认地被阻塞,等待某些数据被读取,以便腾出足够的空间供write()调用写。

2.读取进程也可能工作得比写进程快。当所有当前进程数据已被读取时,管道变空。当这种情况发生时,一个随后的read()调用将默认地被阻塞,等待某些数据被写入,这解决了read()调用返回文件结束的问题。

 注意,从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。

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


两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。一个普通的管道仅可供具有共同祖先的两个进程之间共享,并且这个祖先必须已经建立了供它们使用的管道。

注意,在管道中的数据始终以和写数据相同的次序来进行读,这表示lseek()系统调用对管道不起作用。

转载出处:http://blog.csdn.NET/csdnldsg/article/details/51813366




进程通信( IPC) : (InterProcess Communication)
让不同的 进程能看到一个公共资源(这份资源由操作系统提供)


管道分为命名管道和匿名管道。

一,关于匿名管道

管道是一种最基本的 IPC 机制,  由pipe函数创建。
#include <unistd.h>
int pipe(int filedes[2]);
调用pipe函数时在内核中开辟出一块缓冲区(称为管道)用于 通信,参数为输出型参数,返回值为传出的两个文件描述符,其中filed[0]指向管道的读端,filed[1]指向管道的写端(其中0相当于标准输入,1相当于标准输出),所以管道在用户程序看起来就像一个打开的文件。调用成功返回0,调用失败返回-1
常见的匿名管道就是我们在Terminal上面的‘|’,之所以称为匿名管道是因为我们不能在系统的文件管理中找到相应管道所在的文件

匿名管道的通信步骤

(1)父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端;

(2)父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道;
(3)父子进程根据相应的需求关闭掉对应的读端或者写端;

管道的5个特性:

1,一个管道只能进行单向 通信。( 只能一段写入,一端读出,就像水管一样只能单向流动,要实现双向就要用两条管道)
2,具有血缘关系,常用于父子间进行通信。(为什么?因为只有血缘关系才能赋值/继承其文件信息)
3,管道是字节流进行通信的。
4,管道是依赖于文件系统的,他的生命周期随进程,(即进程退出,则管道消失)。
5,自带同步效果机制。实现了数据一致性。
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1.  如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有 进程 从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像 读到文件末尾一样。
即:写段关闭,读段一直读,读端读到文件尾信号退出。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/wait.h>
int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if (-1 == ret)
    {
        printf("creat pipe error! errno code is:%d\n", errno);
        return 1;
    }
    pid_t id = fork();
    if (id < 0)
    {
        printf("fork error!\n");
        return 2;
    }
    else if (0 == id)//child
    {
        close(_pipe[0]);//close read fd
        int i = 0;
        char *_mesg_c = NULL;
        while(i < 10)
        {
            _mesg_c = "i am a child!";
            write(_pipe[1], _mesg_c, strlen(_mesg_c)+1);
            sleep(1);
            i++;
        }
        close(_pipe[1]);//close write fd
        exit(1);
    }
    else//father
    {
        close(_pipe[1]);
        char _mesg[100];
        int j = 0;
        while (j < 100)
        {
            memset(_mesg, '\0', sizeof(_mesg));
            ssize_t ret = read(_pipe[0], _mesg, sizeof(_mesg));
            printf("%s:code is:%d\n", _mesg,ret);
            j++;
        }
        if (waitpid(id, NULL, 0)<0)
        {
            return 3;
        }
    }
    return 0;
}
2, 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写 端的 进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数 据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
即: 如果读者读完,则读者阻塞等待(写端是没有关闭,但是没有写东西)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/wait.h>
int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if (-1 == ret)
    {
        printf("creat pipe error! errno code is:%d\n", errno);
        return 1;
    }
 
    pid_t id = fork();
    if (id < 0)
    {
        printf("fork error!");
        return 2;
    }
    else if (0 == id)//child
    {
        close(_pipe[0]);//close read fd
        int i = 0;
        char *_mesg_c = NULL;
        while(i < 20)
        {
            if (i < 10)
            {
                _mesg_c = "i am a child!";
                write(_pipe[1], _mesg_c, strlen(_mesg_c)+1);
            }
            sleep(1);
            i++;     
        }
        close(_pipe[1]);//close write fd
    }
    else//father
    {
        close(_pipe[1]);
        char _mesg[100];
        int j = 0;
        while (j < 10)
        {
            memset(_mesg, '\0', sizeof(_mesg));
            int ret = read(_pipe[0], _mesg, sizeof(_mesg));
            printf("%s:code is:%d\n", _mesg,ret);
            j++;
        }
        if (waitpid(id, NULL, 0)<0)
        {
           return 3;
        }
    }
    return 0;
}
3.  如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进 程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
即:所有读端关闭,写段一直写,写段被操作系统终止(SIGPIPE);
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/wait.h>
int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if (-1 == ret)
    {
        printf("creat pipe error! errno code is:%d\n", errno);
        return 1;
    }
    pid_t id = fork();
    if (id < 0)
    {
        printf("fork error!");
        return 2;
    }
    else if (0 == id)//child
    {
        close(_pipe[0]);//close read fd
        int i = 0;
        char *_mesg_c = NULL;
        while(i < 20)
        {
            if (i < 10)
            {
                _mesg_c = "i am a child!";
                write(_pipe[1], _mesg_c, strlen(_mesg_c)+1);
            }
                sleep(1);
                i++;     
        }
    }
    else//father
    {
        close(_pipe[1]);
        char _mesg[100];
        int j = 0;
        while (j < 3)
        {
            memset(_mesg, '\0', sizeof(_mesg));
            int ret = read(_pipe[0], _mesg, sizeof(_mesg));
            printf("%s:code is:%d\n", _mesg,ret);
            j++;
        }
        close(_pipe[0]);
        sleep(10);
        if (waitpid(id, NULL, 0)<0)
        {
           return 3;
        }
    }
    return 0;
}

4.  如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读 端的 进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时 再 次write会阻塞,直到管道中有空位置了才写入数据并返回。
即: 如果写者写满,则写者阻塞等待(读端是没有关闭的, 但是没有读东西)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/wait.h>
int main()
{
    int _pipe[2];
    int ret = pipe(_pipe);
    if (-1 == ret)
    {
        printf("creat pipe error! errno code is:%d\n", errno);
        return 1;
    }
    pid_t id = fork();
    if (id < 0)
    {
        printf("fork error!");
        return 2;
    }
    else if (0 == id)//child
    {
        close(_pipe[0]);//close read fd
        int i = 0;
        char *_mesg_c = NULL;
        while(1)
        {
            _mesg_c = "i am a child!";
            write(_pipe[1], _mesg_c, strlen(_mesg_c)+1);
           // sleep(1);
            i++;
            printf("%d\n",i);
        }
    }
    else//father
    {
        close(_pipe[1]);
        sleep(1);
 
        if (waitpid(id, NULL, 0)<0)
        {
            return 3;
        }
    }
    return 0;
}

命名管道:

一 .概念

管道的一个不足之处是没有名字,因此,只能用于具有亲缘关系的进程间通信,在命名管
道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一
个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因
此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO
相互通信。值得注意的是,FIFO(first input first output)总是按照先进先出的原则工作,第一
个被写入的数据将首先从管道中读出。

二:命名管道实现函数:mkfifo

函数原型:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char* pathname,mode_t mode)

函数调用成功都返回0,失败都返回-1

示例:

umask(0);

if (mkfifo("/tmp/fifo",S_IFIFO|0666) == -1)

{

perror("mkfifo error!");

exit(1);

}

三:注意

“S_IFIFO|0666”指明创建一个命名管道且存取权限为0666,即创建者、与创建者同组的 用户、其他用户对该命名管道的访问权限都是可读可写( 这里要注意umask对生成的

管道文件权限的影响 )。

命名管道创建后就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命 名管道时,必须先调用open()将其打开。因为命名管道是一个存在于硬盘上的文件,而管道

是存在于内存中的特殊文件。

需要注意的是,调用open()打开命名管道的进程可能会塞。但如果同时用读写方式 (O_RDWR)打开,则一定不会导致阻塞;如果以只读方式(O_RDONLY)打开,则调 用open()函数的进程将会被阻塞直到有写方打开管道;同样以写方式(O_WRONLY)打开 也会阻塞直到有读方式打开管道。

write:



Read:



然后运行两个文件,就可以实现进程间的通信(这里不贴图了)。


匿名实现机制:

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个 进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大一般为4K大小,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

实现细节:

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

有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
在匿名管道的下,这两个结构体一般是一个父进程和一个子进程,父子进程通过指向同一个管道来实现彼此的数据 通信,对于同名管道,两个结构体则没有任何的关系,彼此之间也是相互独立的,一个进程用于向管道中写数据,而另外一个进程用于从管道中读数据。

关于管道的读写

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

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

 *  内存中有足够的空间可容纳所有要写入的数据;

*  内存没有被读程序锁定。

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

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

pipe_read()
1.获取索引节点的i_sem信号量 //我要读了
2.判断缓冲区 是否 空 ,或阻塞 //空不空啊?
阻塞: a .调用prepare_wait() 把当前进程(cur)加到等待队列
b.释放索引节信号量
c.调用 schedule()
d.cur一旦被唤醒,从等待队列中删除 。拷贝所有请求字节 //读
3.释放索引节点的i_sem信号量 //我读完了
4.唤醒管道中所有的写者进程 //你来写吧
5.返回 已经拷贝的字节数目 //读了这么多

pipe_write()
1.获取索引节点的i_sem信号量 //我要写了
2.检查是否至少有一个读进程, //有没有人正在读啊
如果不是,向当前进程发送 SIGPIPE信号,释放i_sem信号量 ,并返回-EPIPE
3.判断是否有足够的写空间, //满不满啊?
是,则向缓冲区 拷贝数据。如果不是非阻塞,释放索引节点并返回—EAGAIN; 如果不是且阻塞, 将当前写操作放入等待队列,释放信号量 ,调用schedule(),一旦被唤醒,返回 3 操作。
4.写入 所有请求的字节 //不满就写
5.释放索引节点信号量 //我写完了
6.唤醒所有等待队列的读进程 //快来读吧

7.返回写入的字节数目(如果没有写入 则返回错误码) //写了这么多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值