分享

gzip1.2.4 源码分析(一)

 yliu277 2016-03-13
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函数内部去了。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多