由命名管道的实现联想到 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 }
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 }
运行结果:
三.做一个新的尝试:
我们都知道,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 }
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 }
四.奇怪的现象出现了:
以上的运行结果:
??????????
??????????
可以很惊奇的发现:
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试试,看看会有什么新发现。
感谢审阅。