2006-06-30

关于fstream.open()中使用in|out|app模式打开文件总是失败的问题


  其实是以前找到的一个问题了,不过今天提到了编译器的版本,突然想起了它。在国内的网站上似乎很难搜索到关于这个问题的报告,因此写出来供分享。

  事情的起因是服务器版本的升级。以前我们的程序都是在一台RH 7.3的内部服务器上编译的。编译之后的程序拿到正式服务器(RHEL AS 3.0)上使用非常正常,因此一直以来都是采取的这样的方式。
  后来新增了一台服务器,是x86_64的CPU,我们就打算安装最新的RHEL AS 4.0 Update 3 for x86_64版本,以获取最新的技术带来的优势。虽然最终因为短期内Oracle中的数据和设计无法升级到预计的10g Release 2 for x86_64版本,而导致升级计划暂时搁置,但在升级过程中,就发现以前的代码出现了许多问题。
  当然,2.96的编译器和3.4.x的编译器之间,肯定是有着不小的差别,因此代码会有问题这早就在我们的预料之中。大部分的错误都是容易发现和容易排除的(另文介绍),但其中有一个棘手的问题,就是本文要说到的,fstream.open()的打开模式问题。

  在以前的代码中,我们一直使用in|out|app的模式来进行日志文件的打开。采用这种模式,如果文件不存在,那么系统会创建它,如果文件存在,那么打开它并从最后面开始写信息,同时也可以从中读信息。可以说,针对那种以当前日期命名的日志文件,这种打开模式组合是再合适不过了。
  然而,编译器升级之后,我们发现这样的打开模式再也打不开任何的文件了。文件既不会被创建,也不会被打开,即使是这个文件已经存在也不行。去掉app或去掉in,都可以正常打开文件,但是这样就牺牲了追加或是读取文件的能力。相比而言,out|app的打开模式对于我们的日志类来说应该是可以接受的,但是为了了解这个问题的根源,我们还是花了不少力气。

  还是google好用。当我们发现这个问题确实是编译器的升级带来的,与操作系统内核、文件系统读写权限、路径/文件名等因素无关之后。我们很快就在google上搜索到了这篇文章:
  http://gcc.gnu.org/ml/gcc-bugs/2002-04/msg01055.html
  这位老兄发现了和我们类似的问题,并上报给了gcc那边。当然,他的工作比我们要做得细致得多。他仔细测试了多种打开模式的组合,并在多种平台、多种编译器版本上做了测试,并得到了结果。没有耐性点开帖子看或无法访问国外站点的用户可以看看下面这个测试结果:

GCC 2.95.2 i686-pc-linux-gnu
open("ios::in", O_RDONLY|0x8000)
open("ios::out", O_WRONLY|O_CREAT|O_TRUNC|0x8000, 0664)
open("ios::in|ios::out", O_RDWR|O_CREAT|0x8000, 0664)
open("ios::in|ios::ate", O_RDONLY|O_CREAT|0x8000, 0664)
open("ios::out|ios::app", O_WRONLY|O_APPEND|O_CREAT|0x8000, 0664)
open("ios::out|ios::ate", O_WRONLY|O_CREAT|0x8000, 0664)
open("ios::out|ios::trunc", O_WRONLY|O_CREAT|O_TRUNC|0x8000, 0664)
open("ios::in|ios::out|ios::app", O_RDWR|O_APPEND|O_CREAT|0x8000, 0664)
open("ios::in|ios::out|ios::ate", O_RDWR|O_CREAT|0x8000, 0664)
open("ios::in|ios::out|ios::trunc", O_RDWR|O_CREAT|O_TRUNC|0x8000, 0664)
GCC 2.95.2 sparc-sun-solaris2.8
open("ios::in", O_RDONLY)
open("ios::out", O_WRONLY|O_CREAT|O_TRUNC, 0664)
open("ios::in|ios::out", O_RDWR|O_CREAT, 0664)
open("ios::in|ios::ate", O_RDONLY|O_CREAT, 0664)
open("ios::out|ios::app", O_WRONLY|O_APPEND|O_CREAT, 0664)
open("ios::out|ios::ate", O_WRONLY|O_CREAT, 0664)
open("ios::out|ios::trunc", O_WRONLY|O_CREAT|O_TRUNC, 0664)
open("ios::in|ios::out|ios::app", O_RDWR|O_APPEND|O_CREAT, 0664)
open("ios::in|ios::out|ios::ate", O_RDWR|O_CREAT, 0664)
open("ios::in|ios::out|ios::trunc", O_RDWR|O_CREAT|O_TRUNC, 0664)

GCC 3.0.4 i686-pc-linux-gnu or sparc-sun-solaris2.8
open("ios::in", O_RDONLY)
open("ios::out", O_WRONLY|O_CREAT|O_TRUNC, 0666)
open("ios::in|ios::out", O_RDWR)
open("ios::in|ios::ate", O_RDONLY)
open("ios::out|ios::app", O_WRONLY|O_APPEND|O_CREAT, 0666)
open("ios::out|ios::ate", O_WRONLY|O_CREAT|O_TRUNC, 0666)
open("ios::out|ios::trunc", O_WRONLY|O_CREAT|O_TRUNC, 0666)
open("ios::in|ios::out|ios::ate", O_RDWR)
open("ios::in|ios::out|ios::trunc", O_RDWR|O_CREAT|O_TRUNC, 0666)

Sun CC 5.0 sparc-sun-solaris2.8
open("ios::in", O_RDONLY)
open("ios::out", O_WRONLY|O_CREAT|O_TRUNC, 0666)
open("ios::in|ios::out", O_RDWR)
open("ios::in|ios::ate", O_RDONLY)
open("ios::out|ios::app", O_WRONLY|O_APPEND|O_CREAT, 0666)
open("ios::out|ios::ate", O_WRONLY|O_CREAT|O_TRUNC, 0666)
open("ios::out|ios::trunc", O_WRONLY|O_CREAT|O_TRUNC, 0666)
open("ios::in|ios::out|ios::app", O_RDWR|O_APPEND|O_CREAT, 0666)
open("ios::in|ios::out|ios::ate", O_RDWR)
open("ios::in|ios::out|ios::trunc", O_RDWR|O_CREAT|O_TRUNC, 0666)

  他得到了几乎所有有用的fstream.open()打开模式组合所对应的实际操作。可以看到,编译器版本的影响确实很大。
  可以注意到,gcc 3.0.4与2.95.2的测试结果相差不小。而且,最重要的是,有效的测试结果中,没有in|out|app这种组合。作者在最后发问,这种打开模式组合没有出现,是bug还是别的原因?可想而知,所谓“没有出现”,就是指这种打开模式组合总是会失败,相当于无法使用。就和我们遇到的情况一样。

  再次的搜索,发现一年之后gcc那边出现了这样的帖子:
  http://gcc.gnu.org/ml/gcc-prs/2003-04/msg00771.html
  这是gcc开发组的人(应该是吧)发的,具体地说,是paolo@gcc.gnu.org。帖子大概是针对一个bug报告的回应,内容就是说in|out|app这种打开模式组合根据ISO标准是非法的,无效的,因此这个bug报告可以close了。ISO标准,应该就是gcc 3.x所遵循的C++标准了。

  至此,真相水落石出。我们也就安心改用out|app模式来写日志了。

不同版本G++编译器的差异真是让人吃惊

  做了一个程序,测试用的,在一台RHEL AS 4.0的机器上做编译。编译完之后瞄了一眼文件大小,1837684。吓了一跳,因为我记得以前类似程序都在2M以上的,怎么这次小了这么多?难道我漏了几个模块?
  去正式服务器上找程序一对,果然,大小差了不少。正式服务器上的程序是在一台RH 7.3的机器上编译的。为了验证到底是源代码有不同还是编译器的差别,我把刚才编译的代码上传了一份到RH 7.3的机器上,用同一个Makefile编译,编译完的大小是2617012。
  对比了一下两边编译器的版本,RH 7.3这一台是2.96,RHEL AS 4.0那台是3.4.3。版本差别的确比较大,但编译的结果差别也真的不小。除开链接的库,单单看这个程序的.o文件,大小竟然差了一倍多。以前没有在这方面比较过,确实有点惊人。

  我又去找来了upx,2.00 for linux i386版本。用默认选项压缩的结果,更令我吃了一惊。RHEL AS 4.0这台上,压缩之后的大小是535802,而RH 7.3那台上的较大的那个执行程序,压缩之后只有384610,反而拥有较大的压缩比和较小的压缩后尺寸。
  不好解释了,也许是2.96的编译器生成的代码中,垃圾比较多,而有用的部分反而比较少吧。和内核的系统调用也许也有关系吧?两台机器的内核差别也挺大的,一个是2.4一个是2.6。

  记录下这个情况,以供参考。