由命名管道的实现联想到 read 和 fread 的区别。

Posted shy_BIU

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了由命名管道的实现联想到 read 和 fread 的区别。相关的知识,希望对你有一定的参考价值。

  

  学过进程通信的一定知道管道:

  它可以当做是两个不同进程的共有资源,也可以说是进程通信的媒介之一。

  管道可分为匿名管道 以及 命名管道。

  管道的本质就是内核缓存,用于运输一个进程到另一个进程的数据流。

  今天我们主要实现的是命名管道——其不像匿名管道只能用于有亲缘关系的进程。所以,命名管道的实现是通过函数创造一个管道文件,然后进程通过打开该管道文件来进行通信。

 

 

一.须知mkfifo函数:

  mkfifo函数:用于创建一个管道文件。

  shell命令中的用法:mkfifo file名   ---------- 生成 file 文件  

  函数原型:int mkfifo ( const char *pathname , mode_t mode);

       参数:管道文件名和绝对(相对)地址 ,文件生成时的权限。

 

 

二.创建两个不同的进程使用命名管道

  开始创建两个C程序——server.c 与 client.c 前者负责读管道,后者负责写管道,通过管道进行通信:

  那么思路很清楚,server用只读的方式打开管道文件,每当管道被client写入数据后变读走,交给server以完成后续操作(我们这里后续操作为打印)。而 client 以只写的方式打开管道文件,我们在标准输入上输入文字输入到一个字符串中,然后运用函数将字符串内容写入管道中。

  思路完毕,代码实现如下:

  

  server.c:

 

 1 #include<stdio.h>
 2 #include<errno.h>
 3 #include<stdlib.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<sys/stat.h>
 7 #include<fcntl.h>
 8 #include<string.h>
 9 
10 #define ERR_EXIT(x) do \\
11                     { \\
12                         perror(x);\\
13                         exit(1);\\1
14                     }while(0)
15 
16 int main(void)
17 {
18     int ret = 0;
19     umask(0);
20     //创建一个管道文件
21     if((ret = mkfifo("mypipe" , 0644)) == -1)
22         ERR_EXIT("myfifo");
23 
24     //服务器用于读
25     int fd = 0;
26     if((fd = open("mypipe",O_RDONLY)) < 0)
27         ERR_EXIT("open");
28 
29     char recvbuf[1024] = {0};
30     while(1)
31     {
32         printf("Server#:");//C库文件,行缓冲,故开始不打印。
33 
34         memset(recvbuf,0,sizeof(recvbuf));
35         ret = read(fd ,recvbuf ,sizeof(recvbuf) - 1);
36         if(ret == -1)
37             ERR_EXIT("read");
38         else if(ret == 0)//对方关闭,系统定义
39         {
40             printf("peer exit!\\n");
41             break;
42         }
43 
44         recvbuf[ret] = 0;
45         fputs(recvbuf,stdout);
46     }
47     close(fd);
48 
49     return 0;
50 }
Server

 

  client.c:

 

 1 #include<stdio.h>
 2 #include<errno.h>
 3 #include<stdlib.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<sys/stat.h>
 7 #include<fcntl.h> 
 8 #include<string.h>
 9 
10 #define ERR_EXIT(x) do \\
11                     { \\
12                          perror(x);\\
13                          exit(1);\\
14                     }while(0)
15  
16 int main()
17 {
18     int ret = 0;
19     int fd = 0;
20     if((fd = open("mypipe",O_WRONLY)) == -1)
21         ERR_EXIT("open");
22 
23  
24     char sendbuf[1024] = {0};
25     printf("I am Clienr:");
26     while((fgets(sendbuf,sizeof(sendbuf),stdin)) != NULL)
27     {
28         if((strncmp(sendbuf,"quit",4)) == 0)//输入quit自觉退出
29         {
30             close(fd);
31             exit(0);
32         }
33         ret = write(fd,sendbuf,strlen(sendbuf));
34         printf("ret = %d\\n",ret);
35         
36         memset(sendbuf,0,sizeof(sendbuf));
37         printf("I am Clienr:");
38     }
39  
40     return 0;
41 }
Client

 

 

  运行结果:

        

        

        

 

 三.做一个新的尝试:

  我们都知道,read和write是系统调用,也就是说每次我们用read和write时,都会从用户态转换到系统态,然后再从系统态回到用户态。

  

  所以我决定,用C库中的fopen、fclose、fread和fwrite来实现这两个函数:

 

  server_2.c:

  

 1 #include<stdio.h>
 2 #include<errno.h>
 3 #include<stdlib.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<sys/stat.h>
 7 #include<fcntl.h>
 8 #include<string.h>
 9 
10 #define ERR_EXIT(x) do \\
11                     { \\
12                         perror(x);\\
13                         exit(1);\\
14                     }while(0)
15 
16 int main(void)
17 {
18     int ret = 0;
19     umask(0);
20     //创建一个管道文件
21     if((ret = mkfifo("mypipe" , 0644)) == -1)
22         ERR_EXIT("myfifo");
23 
24     //服务器用于读
25     FILE* fd = NULL;
26     fd = fopen("mypipe","r");
27     if(fd == NULL);
28         ERR_EXIT("open");
29 
30     char recvbuf[1024] = {0};
31     while(1)
32     {
33         printf("Server#:");//C库文件,行缓冲。
34 
35         memset(recvbuf,0,sizeof(recvbuf));
36         ret = fread(recvbuf ,sizeof(char),sizeof(recvbuf) - 1,fd);
37         if(ret == -1)
38             ERR_EXIT("read");
39         else if(ret == 0)
40         {
41             printf("peer exit!\\n");
42             break;
43         }
44 
45         recvbuf[ret] = 0;
46         fputs(recvbuf,stdout);
47     }
48     fclose(fd);
49 
50     return 0;
51 }
Server_2

 

  client_2.c:

 

 1 #include<stdio.h>
 2 #include<errno.h>
 3 #include<stdlib.h>
 4 #include<unistd.h>
 5 #include<sys/types.h>
 6 #include<sys/stat.h>
 7 #include<fcntl.h>
 8 #include<string.h>
 9 
10 #define ERR_EXIT(x) do \\
11                     { \\
12                         perror(x);\\
13                         exit(1);\\
14                     }while(0)
15 
16 int main()
17 {
18     int ret = 0;
19     FILE* fd = NULL;
20     fd = fopen("mypipe","w");
21     if(fd == NULL)
22         ERR_EXIT("fopen");
23 
24 
25     char sendbuf[1024] = {0};
26 
27     printf("I am Clienr:");
28     while((fgets(sendbuf,sizeof(sendbuf),stdin)) != NULL)
29     {
30         if((strncmp(sendbuf,"quit",4)) == 0)
31         {
32             fclose(fd);
33             exit(0);
34         }
35         ret = fwrite(sendbuf ,sizeof(char),strlen(sendbuf),fd);
36         printf("ret = %d\\n",ret);
37         
38         memset(sendbuf,0,sizeof(sendbuf));
39         printf("I am Clienr:");
40     }
41 
42     return 0;
43 }
Client_2

 

 四.奇怪的现象出现了:

   以上的运行结果:

        

 ??????????

        

 ??????????

        

 

  可以很惊奇的发现:

    1.当我在客户程序写入字符串是,服务程序完全没有反应,貌似什么都没做,没读入数字。

    2.当我结束掉客户程序时,服务程序突然有了反应,而且可以看出他执行了两次循环,第二次循环的时候读到ret == 0,所以输出“peer exit!” 并且退出了循环之后便结束了程序。

    3.可以明显的看到,我开始在客户程序中输入的字符串在结束时,在服务程序的一次循环中全部输出了(因为Server# 仅仅出现一次,所以确定只是一次循环)。

 

五.发现问题并解决问题:

  我们猜测问题发生的可能性:

  A.客户程序的问题:

  客户程序的内容根本就没有读入到管道中?

  这是不对的,我们可以看到,每次输入我都特地输出了一次 fwrite 的有效写入个数,而且每次都能打印正确,并且都能输出每次循环最后的“I am Client”,说明每次循环都在正常执行,那么说明,客户程序这部分,就没有问题。

   B.管道的问题:

  这也是没有依据的,因为我之前用read和write的版本就用的是这个管道,不可能两个程序同一个管道文件,会出现不同结果。

  C.服务程序的问题:
  排除法来看,的确是这里的问题,那么到底是哪里出了问题?fopen和open的区别吗?还是fread和read的区别?

   

  我们都知道,fopen返回的是一个指向FIFE结构体的指针,而open返回的是一个整型,这个整型时这个文件的文件描述符,而FIFE*结构体里的内容,就包括文件描述符,除此之外还包括文件当前缓冲区的相对位置,文件读写指针的所在位置。

  程序根据得到的FIFE结构体的指针指向的结构体里文件描述符,通过文件描述符表,找到文件表,再从文件表中的V结点表来找到文件的想过信息,再通过V结点中的inode来找到磁盘对应位置的文件。

  看下列图片:

  注:箭头中还有很多步骤,结构体中还有一些成员,没有画,但是写出来了。

 

  可以看出,fread在读取文件中的内容时,是需要通过缓冲的,而read和open就不需要。由此可得,上述问题的产生,是因为每次fread都是在等系统把磁盘文件中的内容送到缓冲区,缓冲区把东西交给fread。

  但是我们需要知道,各个文件有各自的缓冲规则。我们查阅资料,看《UNIX高级环境编程》第五章:

  

  可以知道,系统一直再往缓冲区送东西,但是要等缓冲区满,才能让fread执行其操作。

  经过调试也可以发现,我将fread执行为每次读一个的操作,但是在调试时发现,fread并没有执行并且在client可执行程序退出前一直阻塞。

  以下是fread 每次读一个 的伪代码:

  

 1 char recvbuf[10] = {0};
 2 while(1)
 3 {
 4      printf("Server#:");//C库文件,行缓冲。
 5 
 6      memset(recvbuf,0,sizeof(recvbuf));
 7      int i = 0;
 8      while(1) 
 9      {
10       ret = fread(recvbuf+i,sizeof(char),1,fd);
11       if(ret == -1)
12           ERR_EXIT("fread");
13       else if(ret == 0)
14       {
15           printf("peer exit!\\n");
16           fclose(fd);
17           exit(0);
18       }
19       if(recvbuf[i] == \'\\n\')
20           break;
21 
22       i++;
23 }

 

 

六.意外发现:

  

  总结发现有以下两点:

  看客户程序和服务程序,一个是“Server#”,另一个是“I am Client:”,两者都没有加 \'\\n\' ,但是在程序开始时,服务程序没有打印,客户程序却把“I am Client:” 打印出来了。这是为什么?

  我们从《UNIX高级系统编程》知道,stdout,也就是标准输出文件,他的缓冲为行缓冲,所以在遇到 ‘\\n’ 之前,它是不会将缓冲区内容打印到显示器上的,但是,为什么客户程序却打印了?

  那是因为我们的fgets函数,你想一下,你printf一串没有\'\\n\'的字符串,后面跟一个scanf,你输入之前是不是肯定能看到printf打印的东西,这是有系统规定的机制蕴含在其中,至于是什么,得问内核了。

 

  再看后面的server_2的结果,为什么当我程序退出时,可以一次性打印呢?

  因为exit在退出时,他还会做一些结束的收尾工作,就比如,清空缓存区。不信?不信你换个_exit试试,看看会有什么新发现。

 

 

  感谢审阅。

 

以上是关于由命名管道的实现联想到 read 和 fread 的区别。的主要内容,如果未能解决你的问题,请参考以下文章

追加多个大data.table;使用 colClasses 和 fread 的自定义数据强制;命名管道

read/fread write/fwrite 的区别

read/fread write/fwrite 的区别

如何使用fgets / fread读取PIPE

Boost.Asio - 轮询命名管道

data.table fread 可以接受连接吗?