管道的内核实现

2023-05-14,,

一.概述:

     管道是进程间通信的手段之一,它实际上是一个存在于存的特殊文件,而这个文件要通过两个已经打开的文件才能进行操作,这两个文件分别指向管道的两端。

            管道是通过在内存中开辟一个缓存区来实现进程间的通信的,这个缓存区的大小是固定的。在linux中,这个缓存区的大小为一页,即4k。但固定的大小会带来问题,当缓存区已经被write操作写满时,之后的write操作会阻塞,等待缓存区内的某些数据被读取,以便腾出空间给写操作调用。

            在linux中,管道并没有专门的数据结构,而是通过file结构和inode共同实现。两个file结构指向一个inode,而这个inode对应了内存的某个区域。

注:一个file中的f_op只对应写或者读操作中的一个。

          而管道创建的缓存区是一个环形缓存区,即当用到缓存区的尾部时,下一个可用区间为缓存区的首部。缓存区一般采用的是数组形式,即申请的是一个线性的地址空间。而形成环状用的是    模    缓存区长度。而访问这个缓存区的方式是一种典型的“生产者—消费者模型”,即当生产者有大量的数据要写时,而缓存区的大小只有1k,所以当缓存区被写满时,生产者必须等待,等待消费者读取数据,以便腾出空间让消费者写。而当消费者发现缓存区内并没有数据时,消费者必须等待,等待生产者往缓存区内写数据来提供消费者读数据。

          那么又如何判断是“空”还是“满”呢。当read和write指向环形缓存区的同一位置时为空或满。为了区别空和满,规定read和write重叠时为空,而当write比read快,追到距离read还有一个元素间隔时,就认为是满。

         摘:(不明觉厉)        

 并发访问

考虑到在不同环境下,任务可能对环形缓冲区的访问情况不同,需要对并发访问的情况进行分析。

在单任务环境下,只存在一个读任务和一个写任务,只要保证写任务可以顺利的完成将数据写入,而读任务可以及时的将数据读出即可。如果有竞争发生,可能会出现如下情况:

Case1:假如写任务在“写指针加1,指向下一个可写空位置”执行完成时被打断,此时写指针write指向非法位置。当系统调度读任
务执行时,如果读任务需要读多个数据,那么不但应该读出的数据被读出,而且当读指针被调整为0是,会将以前已经读出的数据重复读出。

Case2:假设读任务进行读操作,在“读指针加1”执行完时被打断,此时read所处的位置是非法的。当系统调度写任务执行时,如果
写任务要写多个数据,那么当写指针指到尾部时,本来缓冲区应该为满状态,不能再写,但是由于读指针处于非法位置,在读任务执行前,写任务会任务缓冲区为
空,继续进行写操作,将覆盖还没有来的及读出的数据。

二.用管道实现进程间的通信:

代码:

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

int main()
{
    //.....创建管道
    int pipefd[2] ={-1,-1};
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        return -1;
    }

    //....创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return -1;
    }

    else if(id == 0)
    {
         //....子进程关闭读端
         close(pipefd[0]);

         char* buf = "i'm  child";
         int count = 5;
         while(count--)
         {
             write(pipefd[1],buf,strlen(buf));
             sleep(1);
         }
    }
    else
    {
        //....父进程关闭写端
        close(pipefd[1]);

        char buf[1024];
        int count = 5;
        while(count--)
        {
              memset(buf, '\0', sizeof(buf));
              ssize_t size = read(pipefd[0],buf,sizeof(buf) - 1);
              buf[size] = '\0';
              printf("%s\n",buf);
        }
     }

}

结果截图:

三.使用管道会出现的四种情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

1:当所有写端都关闭时,但还有进程要从管道中读取数据,那么当管道中的数据读完后,再次read时,就会返回0,就像读到文件末尾一样。

代码如下:

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

int main()
{
    //.....创建管道
    int pipefd[2] ={-1,-1};
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        return -1;
    }

    //....创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return -1;
    }

    else if(id == 0)
    {
        //....子进程关闭写端
        close(pipefd[1]);

        char buf[1024];
        int count = 5;
        while(count--)
        {
              memset(buf, '\0', sizeof(buf));
              ssize_t size = read(pipefd[0],buf,sizeof(buf) - 1);
              if(size > 0)
              {
                    buf[size] = '\0';
                    printf("%s\n",buf);
              }
              else
              {
                  printf("read error");
              }
        }
     }
    else
    {
         //....父进程关闭读端
         close(pipefd[0]);

         char* buf = "i'm  father";
         int count = 5;
         int i = 0;
         while(count--)
         {
             if(i == 3)
             {
                 printf("i just want to sleep");
                 break;
             }
             i++;
             write(pipefd[1],buf,strlen(buf));
             sleep(1);
         }
    }
}

执行结果:

2.如果指向管道写端的文件标识符没关闭,但写端也不向管道中写数据时,如果此时有进程从管道中读数据,那么当缓存区内的数据被读完之后,再次read,读进程会进入阻塞状态,直到缓存区内有数据才进行读数据。

代码如下:

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

int main()
{
    //.....创建管道
    int pipefd[2] ={-1,-1};
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        return -1;
    }

    //....创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return -1;
    }

    else if(id == 0)
    {
        //....子进程关闭写端
        close(pipefd[1]);

        char buf[1024];
        int count = 5;
        while(count--)
        {
              memset(buf, '\0', sizeof(buf));
              ssize_t size = read(pipefd[0],buf,sizeof(buf) - 1);
              if(size > 0)
              {
                    buf[size] = '\0';
                    printf("%s\n",buf);
              }
        }
     }
    else
    {
         //....父进程关闭读端
         close(pipefd[0]);

         char* buf = "i'm  father";
         int count = 5;
         int i = 0;
         while(count--)
         {
             if(i == 3)
             {
                 printf("i just want to sleep\n");
                 sleep(5);
             }
             i++;
             write(pipefd[1],buf,strlen(buf));
             sleep(1);
         }
    }
}

执行结果:

3.如果所有指向管道的读端都关闭了,但还有进程要往管道中写数据,那么进程就会收到信号SIGPIPE,通常会导致进程异常终止。(因为此时再写也没用,又没进程要读)

代码如下:

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

int main()
{
    //.....创建管道
    int pipefd[2] ={-1,-1};
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        return -1;
    }

    //....创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return -1;
    }
    else if(id == 0)
    {
        //....子进程关闭读端
        close(pipefd[0]);

        char buf[] = "i'm  child";
        int count = 5;
        while(count--)
        {
              write(pipefd[1],buf,strlen(buf));
              sleep(1);
        }
    }
    else
    {
         //....父进程关闭写端
         close(pipefd[1]);
         char buf[1024];
         int count = 5;
         int i = 0;
         while(count--)
         {
             if(i == 3)
             {
                   close(pipefd[0]);
                   break;
             }
             i++;
 
            int ret = 0;
            memset(buf, '\0', sizeof(buf));
            ret =  read(pipefd[0],buf,sizeof(buf) - 1);
            buf[ret] = '\0';
            if(ret > 0)           

            {
                 printf("%s\n",buf);
                 fflush(stdout);
            }
         }
         int status = 0;
         pid_t pid = waitpid(id, &status, 0);
         printf("pid: [%d]  signal:[%d]\n",id,status|0xff);
         fflush(stdout);
    }
}

执行结果:

4.如果指向管道读端的文件标识符没有关闭,但又不读缓存区内的内容,那么如果此时还有进程要往缓存区内写,而缓存区已被写满时,写进程会阻塞,直到缓存区内有空位置之后才能写。

代码如下:

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

int main()
{
    //.....创建管道
    int pipefd[2] ={-1,-1};
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        perror("pipe");
        return -1;
    }

    //....创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return -1;
    }
    else if(id == 0)
    {
        //....子进程关闭读端
        close(pipefd[0]);
 
        char buf[] = "i'm  child";
        int count = 5;
        while(count--)
        {
              write(pipefd[1],buf,strlen(buf));
              sleep(1);
        }
    }
    else
    {
         //....父进程关闭写端
         close(pipefd[1]);
         char buf[1024];
         int count = 5;
         int i = 0;
         while(count--)
         {
             if(i == 3)
             {
                   printf("i just want to sleep\n");
                   fflush(stdout);
                   sleep(5);
             }
             i++;
 
            int ret = 0;
            memset(buf, '\0', sizeof(buf));
             ret =  read(pipefd[0],buf,sizeof(buf) - 1);
            buf[ret] = '\0';
            if(ret > 0)
            {
                 printf("%s\n",buf);
                 fflush(stdout);
            }
         }
         int status = 0;
         pid_t pid = waitpid(id, &status, 0);
         printf("pid: [%d]  signal:[%d]\n",id,status|0xff);
         fflush(stdout);
    }
}

执行结果:

《管道的内核实现.doc》

下载本文的Word格式文档,以方便收藏与打印。