博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

一个诡异的C语言问题

Posted on 2013-05-10 08:54  凤凰火舞  阅读(537)  评论(0编辑  收藏  举报

昨天在博客给出了一个C语言问题,详细问题见帖:谁能解决这个C语言问题?(转)今天给出我的一种解决方案(这是我以前在其他站发过的,现在想转到博客园里面跟大家分享)。

首先来回顾一下昨天的问题,是这样一段代码:

 1 #include <stdio.h>
 2 
 3 int main()
 4 {
 5     char buff[50];
 6     while (true)
 7     {
 8         scanf("%s", buff);
 9         printf("you inputed: %s\n", buff);
10     }
11     return 0;
12 }

在控制台下执行“bug < break.txt”操作的时候,这个程序就会陷入一个死循环,不断地打印出break.txt里面最后的一串字符,无法停止。这样就遇到一个问题,既然缓冲区里面已经没有数据了,但是为什么scanf没有处于阻塞状态呢,其实我们再使用 < 重定向的时候,就已经把标准输入stdin重定向到了break.txt文件中,scanf是通过stdin来读取数据的,也就是说stdin重定向以后,scanf其实就是通过break.txt来读取数据了,重定向stdin以后,当缓冲区没有数据的时候,scanf并不会阻塞,但是会返回-1(EOF),我们可以通过判断返回值来判断缓冲区是否已经为空。

既然在stdin被重定向以后,scanf处于非阻塞状态,那么我们把stdin恢复会本来的状态不就可以了么,就像我们平时写的程序一样,scanf一直在等待通过键盘输入数据,然后去处理。那么该怎么恢复stdin呢,给stdin赋值么,我尝试了一下,stdin被认为是常量无法赋值。通过搜索,知道了一个函数:freopen(_In_z_ const char * _Filename, _In_z_ const char * _Mode, _Inout_ FILE * _File);。该函数功能是:替换一个流,或者说重新分配文件指针,实现重定向。

举一个例子,freopen(“output.txt”, “w”, stdout);这样就会把stdout重定向到output.txt文件中,也就是说执行printf函数,会把结果输出到那个文件中而不是屏幕上。

那么我们就可以用这个函数来恢复stdin,但是该怎么恢复呢,stdin初始值是什么呢,答案是“CON”,CON就是控制台,把stdin重定向回到控制台,这样程序就正常了。只是这个设备文件的名字是与操作系统相关的。DOS/Windows: freopen(“CON”, “r”, stdin);                 Linux: freopen(“/dev/console”, “r”, stdin);

根据上面的解决方案,那么程序应该改成下面这样:

 1 #include <stdio.h>
 2 
 3 int main()
 4 {
 5     char buff[50];
 6     int i;
 7     while (true)
 8     {
 9         i = scanf("%s", buff);
10          if (i == -1)
11          {
12              freopen("CON", "r", stdin);
13              continue;
14          }
15         printf("you inputed: %s\n", buff);
16     }
17     return 0;
18 }

通过上述代码问题可以解决了。

我们现在回到原始代码,打开break.txt,在文件尾部(注意:是在尾部)随便添加几个单词,然后保存,发现了什么,是不是控制台中输出了那几个单词?

如果在文件头部或者中部增加字符,屏幕上并不会输出刚才增加的几个字符,而是该文件的最后几个,刚才增加几个,屏幕上就输出几个。也就是说增加的几个字符,又把文件最后的几个字符挤到了缓冲区里面,如果最后几个字符里面有1个空格的话,我们可以看到屏幕上并没有把空格输出来,而是把空格前后的字符串作为两个输入输出了,由此也可以验证,确实是文件的最后几个字符会被挤到缓冲区里面,然后scanf就像从来没见过它们一样的重新把他们读入buff,然后输出。

看来scanf在stdin被重定向以后确实会有不能阻塞的问题。其实我们查看这个bug不用命令行也可以,在while循环上面加一句:freopen(“\\debug\break.txt”, “r”, stdin);就可以了,这其实就重定向了stdin。也会出现那个不能阻塞的bug,不过这个bug通过freopem(“CON”,”r”,stdin)可以解决。所以现在的问题就是搞清楚为什么重定向stdin以后不能阻塞,我想这个应该就是这么设计的,可是为什么要这样设计呢?如果有高人知道的话,希望来指点一下。


附:

当时的一个评论:“关于那个为什么重定向以后不阻塞我觉得是因为早起的时候为了实现多进程间共享数据的时候用的,如果是阻塞的必定有某种机制触发这个函数继续读取字符,而这样是否会加大难度,比如说信号触发或者上锁,信号量等,猜测而已。”

我的回复:“有可能是因为上锁问题,在scanf源码里调用了函数_lock_str2(0, stdin);而_lock_str2(0, stdin);又调用了EnterCriticalSection( _locktable[locknum].lock );,这个函数式等待互斥资源的(msdn上是这么写的:Waits for ownership of the specified critical section object. The function returns when the calling thread is granted ownership.),不知道和阻塞有没有关系。根据我单步调试scanf函数,发现scanf的阻塞是在read.c 文件中 int __cdecl _read_nolock (int fh, void *inputbuf, unsigned cnt)这个函数调用API:if(!ReadFile( (HANDLE)_osfhnd(fh), buffer, cnt, (LPDWORD)&os_read, NULL ) || os_read cnt)的时候,会在这里阻塞,而重定向以后就不会。”