gzip1.2.4 源码分析之一 这是分析的第一部分,主要看了程序主体结构、特别关注的是原始文件时间戳的保存、恢复这个细节,具体的压缩算法实现敬请期待下一篇分析文章。 ------------------------------------------------------ gzip.c我之前加过一些注释,可能精确的行号和官网上下载的稍微有一点差别,总体来说问题肯定不大。 文档algorithm.doc 的大体算法描述,详见我的博客 blog.163.com/probe@126 下面以main函数为主线,开始分析gzip的源码 main函数位于gzip.c中,打开此文件,开头是 static char *license_msg[] 这是一个字符指针数组,里面存了14行字符,最后以0结尾,用来保存版权信息。 接下来的注释说明gzip压缩后,文件的属主、时间、权限等都不改变,我做了个测试,的确是不改变的: [root@zw110-11 bin]# chown -R liufeng:liufeng * [root@zw110-11 bin]# ll total 72 -rw-r--r-- 1 liufeng liufeng 2027 Dec 20 11:32 78.tgz -rwxr-xr-x 1 liufeng liufeng 2407 Dec 20 11:32 httpd.sh -rw-r--r-- 1 liufeng liufeng 3635 Dec 20 11:32 jdbc.properties -rw-r--r-- 1 liufeng liufeng 859 Dec 20 11:32 jms.properties -rw-r--r-- 1 liufeng liufeng 6828 Dec 20 11:32 proxool.xml -rwxr-xr-x 1 liufeng liufeng 3503 Dec 20 11:32 urs.sh drwxr-xr-x 2 liufeng liufeng 4096 Dec 20 11:32 util -rw-r--r-- 1 liufeng liufeng 19435 Dec 20 11:32 wrapper.pl -rw-r--r-- 1 liufeng liufeng 19390 Dec 20 11:32 wrapper.pl.in [root@zw110-11 bin]# stat wrapper.pl File: `wrapper.pl' Size: 19435 Blocks: 40 IO Block: 4096 regular file Device: fc01h/64513d Inode: 3842560 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 502/ liufeng) Gid: ( 502/ liufeng) Access: 2012-12-20 11:32:32.000000000 0800 Modify: 2012-12-20 11:32:32.000000000 0800 Change: 2013-01-05 14:58:37.000000000 0800 [root@zw110-11 bin]# gzip wrapper.pl [root@zw110-11 bin]# stat wrapper.pl. wrapper.pl.gz wrapper.pl.in [root@zw110-11 bin]# stat wrapper.pl.gz File: `wrapper.pl.gz' Size: 5268 Blocks: 16 IO Block: 4096 regular file Device: fc01h/64513d Inode: 3844337 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 502/ liufeng) Gid: ( 502/ liufeng) Access: 2012-12-20 11:32:32.000000000 0800 Modify: 2012-12-20 11:32:32.000000000 0800 Change: 2013-01-05 14:58:53.000000000 0800 [root@zw110-11 bin]# ll wrapper.pl.gz -rw-r--r-- 1 liufeng liufeng 5268 Dec 20 11:32 wrapper.pl.gz [root@zw110-11 bin]# id uid=0(root) gid=0(root) groups=0(root) 唯一改变了的是ctime,即文件的改变时间这一个属性被设置为当前时间。 第47行定义了RCSID,我百度了一下,这东西是很早之前,还没有acs、svn的年代,用来记录版本信息的。很明显这个字符串里显示gzip的版本还是0.24 第51行开始include一些必要的系统头文件和项目文件夹内自己写的一些头文件了,这里面的tailor.h和gzip.h我曾经快速浏览过,主要是定义了一些数据结构和函数原型声明等。 为了兼容各种不同系统,在include类似time.h、fcntl.h、unistd.h、stdlib.h、sys/dir.h等库头时,使用了ifdef判定 第85行开始定义一些DIR_OPT、TIME_OPT、S_ISDIR、O_BINARY等宏定义,其中也考虑到了跨平台的支持,而其S_ISDIR的定义和apue书上写的很相符,对于我所在的linux系统来说 第188行开始为inbuf outbuf d_buf window 声明了数组类型,这几个数组实际malloc申请内存,是在本文件的main函数中进行的,其实这也可以看出一个优秀的开源软件的编码风格,声明就是声明,实际使用的时候再去malloc 第201行开始声明很多的全局变量,这些全局变量控制了大部分的程序运行方式,我看了多次,确保有比较深刻的印象,将来用到的时候才能比较熟悉 第243行定义了命令行参数结构longopts,这东东应该是每个c开源程序里都会有的一部分吧 第276行开始声明函数 第305行定义的#define strequ(s1, s2) (strcmp((s1),(s2)) == 0) 这个宏是有点意思哈,我们一般都用stccmp,很少定义这样的宏的 第308行开始实际进入函数的具体实现,第一个函数是usage(),这个函数非常的简单,就是向stderr上输出一行字符串。 第326行的help()函数也是相当简单的,开始就定义了一个字符指针数组help_msg[] ,然后调用308行的usage函数,然后一行一行的输出刚才定义的局部变量help_msg[] ,这些输出都是向stderr输出的。 第370行的license()、379行的version()都和上面两个函数类似,向stderr输出点东西,很简单,但是的确有必要,有这样友好的输出信息,才能让用户满意。 第424行就开始了激动人心的main函数啦 main函数说明: 在看main函数的时候,先不去仔细查看各个调用函数的具体实现,粗略地看一下整体功能,等main函数的主架看完了,再逐层看具体实现。和剥洋葱很像。 刚开始就取到progname,取程序名称的时候用路径分隔符,取到最后一个,这个progname很有用,可以用来判定是否是windows系统上的.exe程序,最大的用处在于第470的妙用,根据progname的前几位判断是压缩还是解压,是解压到标准输出还是普通的解压,这个地方的处理的确很精悍,特别是判断是否是标准输出,令我感觉到了代码之美。 接下来,第443行,做了标准的unix程序做的事情,设置env里的GZIP环境变量,我gdb调试的时候,没有这东东 446--459这几行是用来设置信号处理函数的,大体的意思是如果程序放在前台台,就忽略sigint信号,不论前台后台,都忽略sighup和sigterm这两个信号,当然,如果设置信号处理函数失败的话,将信号处理程序设置为abort_gzip,这个abort_gzip做一些清理工作,然后退出程序。这一块要注意的是,sigterm和sighup两个信号处理函数的设置,都用ifdef进行了判断,以便使程序更健壮,这个细节是值得深思的,如果是我来写这个程序,肯定直接就上去signal完事了,人家作者精益求精,竟然考虑到了没有这些信号的系统,真心不错。 480行,将z_suffix这个数组置为'.gz' 481行,将z_suffix字符数组的长度赋给z_len 483~553行,利用函数getopt_long对程序收到的命令行参数进行判断并初始化各个全局变量,这些全局变量在这里得到初始值之后,在接下来用于控制程序执行的具体行为。 558行开始的no_time和no_name,应该是没有用处 562行,将file_count设置为整个命令行参数的总个数减去各个-参数后的个数 567行,判断命令行参数--ascii 是否有效,在有的系统上,需要忽略这个参数 572行,如果z_len是0并且是压缩,或者z_len的长度大于MAX_SUFFIX的宏定义值,就报错并退出程序 577行,如果是压缩,并且do_lzw非0(这个全局变量由命令行参数 Z获得) 579~589这10行,进行内存空间的malloc,为inbuf outbuf d_buf window tab_prefix tab_prefix0 tab_prefix1等数组申请了内存 592行,如果file_count(命令行里指定的文件个数)是0,则执行函数treat_stdin(),这个函数我还没有看,但一看就知道肯定是处理stdin的内容。否则的话就逐个处理命令行传过来的文件,处理这些文件之前,先判断一下,如果向stdout输出,并且不测试、并且不是list列表、并且(压缩或者不是ascii,这两个条件满足一个即可),这样的话,把stdout设置为二进制模式,最后要说明的是,在这个分支里处理各个文件的时候,使用的函数是treat_file,这个函数接受的形参是各个文件名,也就是一次处理一个文件。 602行,做完上面的事情后,稍事休息,继续判断命令行参数里如果要求list(列表)并且不是quiet,并且待处理的文件个数至少有一个,则执行函数do_list 605行,全部搞定,调用do_exit退出程序 至于606行的return,乍看起来是画蛇添足,其实人家注释里写的很明白,不来这么一笔的话,在有些编译器上会报警告 好了,main函数的主干就算是看完了,最大的感受当然就是这几个函数需要仔细去研读啦: 1、treat_stdin 2、treat_file 3、do_list 我了个去啊,难道作者会算卦?接下来609行就开始了函数treat_stdin的具体实现。 treat_stdin函数说明: 614行,根据注释也可以看明白,默认情况下,如果命令行参数里文件名那里是空的话,会直接报错退出,但是为了遵循unix标准,程序提供了一个开关force(-f指定它)来决定是否向stdin或者stdout读写数据,很明显,如果-f指定了,则全局变量的值为1,if判断到这里直接退出,执行接下来的语句: 636行,如果是解压,或者不是文本模式的话,将stdin设置为二进制模式 639行,如果不是测试,不是列表,并且是(压缩,或者不是文本模式两个条件之一满足),将stdout设置为二进制模式 642、643行,将全局变量ifname(输入文件的文件名)设置为字符串'stdin',将全局变量ofname(输出文件的文件名)设置为'stdout' 646行,将time_stamp设置为0,因为现在是在处理标准输入,木文件的时间戳这回事。 648~657行,有点意思啊,如果没有定义 NO_STDIN_FSTAT 宏的话,调用fstat函数将stdin的stat信息保存到全局变量istat中,从中取出mtime,保存到全局变量time_stamp中。 659行,设ifile_size为 -1 ,因为输入是从stdin读到的,所以无从得到输入文件的大小 661行,调函数clear_bufs,将一些全局变量(outcnt、insize、inptr、bytes_in、bytes_out等)置0 662、663行,将全局变量to_stdout、part_nb置0 ? 665行,这个if判断里是有个大大的疑问的,有疑问的时候在行首打个问号吧,将来回过头看的时候好解决。如果是解压,将method设置为调函数 get_method(ifd)得到的值,但是ifd之前一直没有定义啊,这个地方能取到什么值呢? 671行,如果程序执行列表功能,则调函数do_list(ifd, method),同样,这里的ifd也是和上面一样的疑问啊。调完do_list后,程序会直接return退出。 678~687执行一个for循环,用work函数处理stdin和stdout,在满足一定条件时break出来, ? 684行,又调了一次get_method(ifd),这个真不知道有什么用处 每次循环执行的最后一个语句,都将bytes_out置0 689到函数结束,是根据verbose开关进行了一系列的判断,只有verbose为1的情况才会走到这一大块语句中: 如果程序指定了测试,只是向stderr输出个'OK',这意思是执行到这里的话,也就证明压缩解压都成功了,可以打印ok啦 如果不指定测试,而其如果是压缩的话,调用函数display_ratio,输出压缩比率,如果是解压的话,调display_ratio,输出解压比率 这个函数看完了,有以下问题: 1、ifd由何而来?--23:49又从头开始看了一遍,突然明白了,这个全局变量在初始化时被初始为0了,这里调get_method函数时就将0带过去了,因为stdin的fd就是0嘛,擦啊,这肯定是个小坑。。。 2、get_method函数的妙用 3、do_list函数的细节 4、更核心的是work函数 treat_file函数说明: 711行,如果此函数接受到的形参是 '-',就调用上面已经看过的treat_stdin,然后返回 720行,调用函数get_istat,将形参iname(输入文件的文件名)的stat信息保存在变量istat中 723行,如果此文件的类型是个目录,则: 如果系统没有定义NO_DIR(说明系统支持目录这样的文件格式),如果递归目录,则调用treat_dir,处理这个iname目录,treat完这个目录后,调用reset_times将刚才保存好(用了个临时变量st保存)的stat信息恢复(具体细节到时候再看,这个函数的功能就是这样了)这是恢复目录的stat 如果系统不支持目录这样的文件格式,则在735行报错并返回。 738行,判断文件是否为普通文件,如果不是,报错并返回 744行,如果文件的连接数(这个在apue里有详细讲解,哥理解)大于1,说明有其他进程打开了这个文件,此时如果同时满足以下两个条件:一是不向stdout输出,二是不强制执行程序(-f参数),则报错并返回,这个小小的if语句里有个细节很有意思啊,如果打开文件个数大于2,人家报错 links,如果只有一个,就没有那个 's'字符,这个小小的细节足见作者也是个追求完美的人儿呐。 751行,将文件stat结构中的size保存到全局变量ifile_size中,代表输入文件的大小 752行,将文件的mtime保存到time_stamp中,?操作符的执行顺序要稍微留意一下,time_stamp不是取值0,就是mtime 757~762,弄出来输出文件的文件名,保存在ofname中,其中的函数make_ofname() 以后再看吧 768~775行,打开文件ifname,将文件描述符保存到ifd中,其实看到这里,我对调用函数时进行错误判断、进行系统调用时进行错误判断,有错误都报错退出的做法已经习以为常了,如果不进行这种判断,我反倒不习惯啦 776行,调用函数clear_bufs,和treat_stdin一样,将几个全局变量置0 777行,将part_nb置0 779行的if,如果程序执行解压的操作,就调用get_method(ifd),将结果保存到method中,get_method还没有看,知道它要求的参数是个文件描述符,返回一种方法,在这里暂时够了 786行的if,如果程序执行列表功能,就调用do_list然后关闭文件并返回 796~806的if,是取到ofd、ofname这些输出文件描述符和文件名 808行,一般还是保留原始文件名的,所以save_orig_name的值应该为1,这个到时候gdb的时候验证一下 810行,程序继续执行,如果是verbose模式,就在屏幕上打印ifname和几个tab 817~828的循环,调用work(ifd, ofd) ,进行实际工作,循环结束的条件在822行给出,随着work函数的执行,这几个变量肯定会同时满足的 830行,关闭ifd 831行,如果不向stdout输出并且关闭ofd失败的话,报错退出 接下来判断method是否-1,是的话,删除ofname文件 839~851行的verbose输出,和treat_stdin里的一样的 853行的if,如果不向stdout输出,就调用copy_stat,该函数的作用是将输入文件的time、权限等信息赋给输出文件,这个地方是当时森问我那个问题的关键所在,明天仔细看看细节实现。 这个函数里需要注意的几个点: 1、get_istat 2、treat_dir 3、make_ofname 4、get_method 5、copy_stat copy_stat函数说明: 为了弄明白到底是怎么重新设置的输出文件ofname的stat,我刚开始一直看代码,用sourceinsight的追踪功能,查到设置时间的最终函数竟然是SetFileDate,不理解,strace了一把,发现真实的系统调用是utimensat,请教了伟哥,他给我解答了疑问,具体的流程记录在下面: 1621~1629行,如果是解压,并且time_stamp和ifstat->st_mtime的值不等,则把ifstat->st_mtime的值设为time_stamp,这是因为在解压的时候,ifstat取到的是压缩文件的stat信息,而原始文件的各个stat信息是保存在压缩文件中,通过全局变量time_stamp保存的,需要恢复的也是这个时间。 在恢复时间时,调用函数reset_times进行,它的实现细节困扰了我一会,待会继续看它,现在接着看copy_stat函数恢复完时间后做的事情 1631~1634行,调用chmod恢复输出文件的权限 1636行,调用chown恢复输出文件的所有者信息 以上3个系统调用,在我的linux系统上strace的时候都看到了 1638~1645行,将输入文件的权限设置为000,然后调unlink删除掉它 这个函数的主架还是比较清晰的,就做了几件大事:恢复时间、权限、所有者、删除原始文件,下面来具体看看是怎么恢复的时间 reset_times(ofname, ifstat);函数说明 此函数在程序的1595行,接受一个文件名和stat结构,共2个形参 1598~1610行,因为utime系统调用需要使用一个struct utimbuf结构,构建了这么个临时变量,将stat结构中的时间信息填充进去,然后调用utime系统调用设置好文件的时间,至此,恢复文件stat信息的流程我是完全弄明白了。但是怎么将时间戳保存到压缩文件里去的细节还需要继续看代码。 下面开始看get_method和work函数,估计看完这两个函数,就能理解时间戳是怎么写入压缩之后的文件的了,很显然,如果是解压的话,time_stamp的值应该是从压缩文件中取到的。 get_method函数说明 1155~1316行,共161行的一个函数,接受的形参是int类型的,注释里说明了,形参是输入文件的文件描述符 1158行,声明3个局部变量:flags压缩标记,magic[2]幻数,stamp时间戳 1165行,如果强制执行,并且向stdout输出的话,magic数组的两位字符都调用函数try_byte得到,否则调用get_byte得到 1173行,程序继续执行,先将method设置为-1,表示未知,然后设置part_nb、header_bytes、last_member的值 1179行,检查magic数组的两个字符,如果是'\037\213',或者'\037\236',执行下面这一大段程序,一直到1274行,这个if判断共95行,够一屏多了: 1182行,method通过调用函数get_byte得到值,如果值不是8的话,就报错并返回-1,如果是8的话,继续执行,将work设置为unzip 1191行,接下来,flags的值也是通过get_byte函数调用获得 1193行,开始对flags进行一系列的判断,首先,如果和0x20做与操作,不为0的话,报错并返回-1,这个意思是,如果flags的第5个byte设置了非0的话,证明文件是加密的,gzip不支持这加密的东东,所以报错了 1200行,如果flags和0x02做与,结果不为0的话,说明这个文件是压缩文件的一个部分,如果满足force <= 1,报错并返回-1 1207行,如果flags的第6、7位不是0的话(第6、7位保留未使用,应该是0的)也报错,返回-1 1214行,通过get_byte获得stamp的值,并将此值保存到全局的time_stamp中 1220行,再调两次get_byte函数,这个函数到底干啥的,稍后得好好看看,好像就是从inbuf中一个一个的取字符 1223行,对应上面的1200行的判断,这里可以处理,说明force > 1了,在这个if里,调用函数get_byte,得到part的值,如果是verbose模式的话,把这个“部分”的数目打印到stderr上 1231行,接下来判断flags的第2位,置1的话,说明文件是扩展部分,接下来的len个字节都要忽略,len通过get_byte获得,最后调用len次get_byte,估计这样就能把len个字节给忽略了。 1242行,flags的第3位如果是1的话,说明原始的文件名存在,这时候:如果no_name是1,或者(向stdout输出并且不是列表),或者part_nb > 1,调用get_byte忽略一些数据,直到get_byte返回的值是0为止 否则的话,就把ofname设置为get_char取到的值(这是通过指针强制修改的,这句话是关键:*p = (char)get_char(),当然接下来的判断中执行p 了,这个细节很重要,意思是一个字符一个字符地给p赋值,直到p为0) 1267行,如果flags的第4位为1,执行get_char忽略注释信息直到\0字符 1270行,执行完上面那么多对flags的判断,如果此时part_nb == 1,则将header_bytes(gzip文件的头信息)设置为 inptr 2, inptr具体是怎么变化的,应该是在函数get_char中了 1274~1304行,接上面第1179行的if判断,检查magic的幻数获得不同的method等变量值 1305行,程序一般在这里返回method ,如果method 小于0的话,说明有异常了,接下来几行返回错误或者警告 下面看看get_byte函数,然后就可以开始看work函数了 get_byte函数说明: 在gzip.h中有个宏定义: #define get_byte() (inptr < insize ? inbuf[inptr ] : fill_inbuf(0)) 当inptr<insize的时候,返回的是inbuf中指向inptr的那个字符,否则的话inptr=insize了,说明该填充inbuf了,fill_inbuf函数我简单看了,很简单,就是把从ifd中read数据,保存在inbuf中而已。然后把inptr置为1,撒了泡尿回来突然明白了为什么要把inptr置为1,因为fill_inbuf每次返回的是inbuf[0],下次自然是从第1(也就是第2个元素)开始读了 work函数之一 zip 函数zip在文件zip.c中实现,函数一开始便调用函数put_byte将压缩算法(8)、幻数、是否保存文件名、时间戳写入outbuf,在函数put_byte中,每当outbuf满了之后,会调用write系统调用,将outbuf数组的字符写入到文件描述符ofd中 58行,取到crc的值 64行,继续写入deflate_flags、OS_CODE、文件名(如果需要) 75行,调用(void)deflate();,这个函数是zip的精华所在,日后好好专研 最后把crc、文件大小写入,结束函数。 ok,到此为止,第一个大问题解决了,下面该继续剥洋葱,剥到deflate函数内部去了。 |
|