在git的源码中有这样一句
#define cache_entry_size(len) ((offsetof(struct cache_entry,name)+(len)+8)\ &~7)
开始我死活看不懂这什么意思,8这样的幻数已经够吓人的了,再来个对7取反码,实在看不懂。但是经过几天的研究终于明白这是什么意思了。
背景
前面的代码是描述cache的,cache在git中可以类比于CPU的高速缓存——为了加快访问速度,cache主要考虑的是工作目录和暂存对象(已经加到cache中但是并没有提交的对象)的差别,因此它只要将工作目录中的文件的stat信息保存即可。在新版的git中这样的概念改名叫做index了,所以现在所有的cache信息都存在.git/index文件中。保存信息的结构体原型如下:
struct cache_entry { struct cache_time ce_ctime; struct cache_time ce_mtime; unsigned int ce_dev; unsigned int ce_ino; unsigned int ce_mode; unsigned int ce_uid; unsigned int ce_gid; unsigned int ce_size; unsigned char sha1[20]; unsigned short ce_flags; char name[0]; };
其中需要注意的是结构体的最后一个成员name,它用来存暂存对象的相对路径,是一个长度为0的数组,在gcc中使用这样的定义来让name的长度不定长,不用指针的原因就是git总是使用mmap(2)来读取index文件,之后直接将cache_entry指针指向正确的位置就行了,而且写到磁盘的时候也不需要再对文件名取指针内容,直接write(2)就行。而路径的长度信息就存在ce_flags中,但是它仅仅用低12位来存长度(这些已经足够存路径名了,因为在Linux系统中PATH_MAX == 4096 == 212),其他位用做其他用途,这就是为什么程序源码中还有一句:
#define ce_namelen(ce) (CE_NAMEMASK & ntohs((ce)->ce_flags))
这样一个宏函数可以求出ce指针的文件名长度。如果有一个cache_entry的指针ce,那么就可以通过cache_entry_size(ce_namelen(ce))来求得ce的实际大小。
正由于cache是为了快速访问的,所以它的设计需要最大限度的考虑速度,这就是文章开头定义存在的原因:为了内存对齐——CPU访问对齐的变量更快,之所以必须要进行手工的对齐就是因为git是使用mmap(2)进行文件的读取。如果考虑到这一点那么最开始的代码也好解释了:对7取反码可以让结果能被8整除,但是如果一开始就不能被8整除那么结果就会小于另一个运算子,这样一来加8就是为了防止结果变小。但是这样怎么保证内存对齐的呢?因为在内存中所有cache元素都是存在数组中的,前一个元素的长度能被8整除,那么后一个元素的起始位置就能被8整除,而在mmap(2)时内核会选择一个对齐的地址放整个数组,这样一来数组中所有的元素都是对齐的了。
读源码就像做侦探游戏一样,总是要猜作者到底要表达什么,而读好的代码更可以让人学到很多。