最近的文章主要是关于 Postgresql 方向的源代码的学习与使用的,
分析的版本是postgresql-8.4.1
下载地址是:http://www.postgresql.org/ftp/source/v8.4.1
每一份源码文件主要将分成两篇来进行:
第一篇中主要内容是阅读、注释经典的Postgresql内核中的源代码,也就是所谓的初级篇Primary。
第二篇中主要内容是在熟练使用前一篇中所提到的 unix/linux API 的基础之上,对该API进行各种实验测试,
并结合前一篇所介绍的经典内核代码来进行修改按照自己的想法实现一些小的变动,并对该代码进行编译与试运行。
--------------------------------------------------------------------------------------
文件名称:stringinfo.h stringinfo.c
文件所在目录:
stringinfo.h : ..\src\include\lib
stringinfo.c : ..\src\backend\lib
----------------------------------------------------------------------------------------
上篇文章中主要分析的是 stringinfo.h
stringinfo.h 源码分析 是关于 StringInfoData 结构体数据结构描述及基于该数据结构执行的操作方法的声明与定义。
而这篇文章主要是分析stringinfo.c ,即头文件中所声明的方法的具体实现过程。
其中多数方法并不难分析,只要仔细阅读API文档和根据方法的命名规范进行推测,即可得知该方法的功能。
不过仍旧有功能上比较复杂,并值得读者仔细分析与揣摩的两个函数:
在英文文档注释中,对于程序一词常见的描述单词有:
routine,function,method,process
1.appendStringInfoVA
2.enlargeStringInfo
其中 appendStringInfoVA 的函数原型是:
bool appendStringInfoVA(StringInfo str,const char *fmt,va_list args)
该方法是作为 appendStringInfo 方法调用的子方法。它的作用是将从调用方法:appendStringInfo 方法中获取的
指向存放的变参(可变参数)参数列表缓冲区的指针(也就是地址)va_list args,作为参数接收,并正在方法中对
变参缓冲区中的参数进行读取,并将读取出来的参数按照 fmt 所描述的格式化格式进行格式化。
将格式化之后的字符串存放于 StringInfoData 指针类型的 str 所指向的缓冲区中。
其中,通过前一篇的API注释可知,StringInfo 是 StringInfoData 的指针,所以这里的 str 所实现的是 unix 编程中
比较常用的API 设计模式,即方法的返回值用来表明方法是否正确执行,而方法中的某一个参数是以 “值- 地址” 的方式
来进行子方法返回值的传递的。这也就是我们常说的通过指针变量指向子函数与主函数共享的一块缓冲区,
子函数将执行结果存放于该缓冲区中,而指向该缓冲区的指针,则是主方法调用子方法的一个传入参数。
等到子方法返回的时候,主调方法通过传入至子方法中的指针,即可从共享缓冲区中访问到子函数执行的结果。
又由于该共享缓冲区是由主方法申请的空间,所以不会因为子方法的返回而将该共享缓冲空间释放掉。
这样,appendStringInfoVA 的主调方法 appendStringInfo 每次调用appendStringInfoVA之后,
只需要根据该方法的返回值来判断方法执行是否正确,如果正确的话,
则将传入至 appendStringInfoVA 方法中的第一个参数 str 的data(char*)中的字符串提取出来即为此次格式化好的字符串了。
有关 appendStringInfo 调用 appendStringInfoVA 的方法可由下面的图示看出:
经过注释后的 appendStringInfoVA 方法源代码如下:
bool appendStringInfoVA(StringInfo str,va_list args) { int avail,nprinted; /* 此处的两个变量,avail是用来记录当前 StringInfo 对象所指向的 StringInfoData中的data字符串指针所指向的空间中还可容纳字符的个数。 并使用 Assert方法 来下断言: str 必须不为空,否则程序出错,跳出当前程序,打印错误信息。 断言:只有当传入断言方法中的判断条件为假的时候,才会触发一系列的报错信息,并以合法方式退出程序。 */ Assert(str != NULL); avail = str->maxlen - str->len - 1; /* avail = str中最大容量 - str中当前存放字符个数 - 结尾标志符('\0') */ /* 如果当前的剩余可获得的容量太小,小于一定阈值的时,就直接返错误信息, 不需要继续执行后续的步骤了。 主调方法接收到它返回错误信息之后会自动 调用 enlargeStringInfo 方法来为其扩容的 */ if (avail < 16) return false; /* 下面的宏实现的功能是: 若 str->data的最大访问下标所指定的存放元素str->data[str->maxlen-1] 不是 '\0' 结束符,那么将其置为'\0' 。目的是为了后面方便判断, 执行 vsnprintf 进行数据拷贝之后是否发生越界访问的错误。<span style="white-space:pre"> </span> */ #ifdef USE_ASSERT_CHECKING str->data[str->maxlen - 1] = '\0'; #endif /* vsnprintf 函数原型: int vsnprintf( char *str,size_t size,va_list args ); [in]: 表示的是该位置上的参数是方法的传入值 [out]:表示的是该位置上的参数是方法的返回值 1.char *str [out] 典型的 “值-地址” 类型参数,把函数执行生成的格式化的字符串 存放至 str 字符指针指向的空间中。 2.size_t size [in] 该数值是用来描述 str 所指向空间的大小,防止溢出错误。 即对数组 str[] 的越界访问错误。 3.const char *fmt [in] 用于指定格式输出的字符串,类似于printf 中的 "%d%d%s" 这种描述信息。通过 fmt 函数的调用者可以决定你需要格式化可变参数的 类型、个数、顺序。 4.va_list args [in] va_list (variable argument list) 可变参数列表, 它是一个 C 语言中的库函数,通过宏定义实现。 函数在接收可变参数的时候, 会为其开辟一块临时的缓冲区用以存放不定长的(可变)参数, 而类型为 va_list 的变量就是指向该块缓冲区的指针,也就是其缓冲区的首地址。 通常的做法是,逐一判断出每个参数的类型,得知类型之后即可得知该类型 所占字节数的多少,依此向下移动指针,将各个参数读取出来。 而通过vsnprintf 函数即可将读取出来的参数,存放至 str 所指向的空间中, vsnprintf 实质上实现的就是一个将参数列表(va_list:args)中的参数逐一读出, 并按照格式说明(const char *fmt)进行格式化, 并将其拷贝至另一个缓冲区(char *str )中这样一个功能。 然后将拷贝到缓冲区中的字符个数作为返回值进行返回。 */ nprinted = vsnprintf(str->data + str->len,avail,fmt,args); /* 看到后面的断言函数 Assert 中的参数了吗? 它是一个条件判断语句, 如果条件判断为假,则会触发一系列的警告和退出程序的操作, 如果正常,则什么都不做。 你应该还记得我们在前面看到的这样一条注释语句: str->data[str->maxlen-1]='\0' ; 如果,在上面的vsnprintf 操作中很不幸地出现了越界, 那么必将有 str->data[str->maxlen-1]上面的数值被赋值成了 格式化后的参数列表中的某一数值,即一定不会是 ‘\0’ 这一字符了。 根据此条件来判断上述 vsnprintf 是否越界是最合适不过的了。 */ Assert(str->data[str->maxlen - 1] == '\0'); /* 如果程序执行到这里还没有触发一次断言函数的话,很有可能执行成功。 不过在这里仍旧要注意的是, vsnprintf 表示方法正确与错误执行情况返回值 是不同的: 如果返回值 >= 0 的数值说明执行成功,并且该正值代表的是 vsnprintf 所成功格式化并拷贝到缓冲区中的字符的个数。 如果返回负值(该负值最大值为 -1 )说明方法执行失败。 不过在本方法中,仅仅判断返回值 >= 0 还是不够的, 该返回值必须要 < avail -1 ( 也就是说 vsnprintf 返回值的计数是不包括字符串结尾的 '\0', 但是实际拷贝到 str 所指向缓冲区中的字符是包含 '\0' 的, 所以拷贝到 str 中的字符串总个数为 vsnprintf 的返回值 +1 , 应该保证 (vsnprintf返回值+1) < str 总空间的容量大小才对。 这也是为什么有:nprinted < avail -1 ) 这一判断条件。 另外对于边界条件 : vsnprintf 函数的返回值为 0 这种情况,是执行拷贝正确, 但是由于可变参数列表为空,所以执行 0 个字符串的信息拷贝。 */ if (nprinted >= 0 && nprinted < avail - 1) { /* 只有在 vsnprintf正确执行且不越界的情况下,才将str 中用以表示 缓冲区中当前存放字符串个数的 len 变量的数值加上刚刚追加至缓冲区中 的字符的个数。 并且在执行上述操作之后,返回 true 。 appendStringInfo 也就是 appendStringInfoVA 的主调方法, 在接到 true 的返回值之后,就会从 for (;;) 循环中跳出来。 */ str->len += nprinted; return true; } /* 如果 nprinted (vsnprintf 函数执行的返回值) , 不满足要求的话,说明 vsnprintf 函数没有被成功的执行, 而没有成功执行的主要原因是由于str 所指向的内存不足造成的。 这时,需要向主调函数返回 false 来告知主调函数,对其扩容。 不过,在这里我们知道,我们是先调用 vsnprintf 进行数据向 缓冲区 str->data 中的拷贝,然后再来判断该方法的返回值得知执行 拷贝错误的。所以在返回 false 之前,我们要将拷贝错误的脏数据 删除才行,这里的删除很简单,由于用于记录拷贝前缓冲区中字符串个数的len 数值一直都没有变动,只要将其作为缓冲区中有效字符的最后一位即可。 */ str->data[str->len] = '\0'; return false; }
而 enlargeStringInfo 的函数原型是
void enlargeStringInfo(StringInfo str,int needed)参数分析:
1. StringInfo str [out] 它是指向一个StringInfoData 对象的指针,将作为函数执行的返回值被返回到主调方法中。
2. int needed [in] 该数值是用来表示需要为 StringInfoData 对象中的 data 字符指针所指向的空间追加多少内存
的数值。其中内存空间的大小是以字节来计数的。
由于方法中所涉及到的是关于在系统中堆栈上面的空间分配,所以在子方法中追加的空间并不会因为子方法
的执行结束而自动地被系统所回收。如果想对其分配的空间进行回收的话,是通过手动对空间进行释放。
void enlargeStringInfo(StringInfo str,int needed) { int newlen; /* 在这里一定要对传入参数 needed 的取值范围合法性进行检查, 否则的话,在后续的循环中会出现越界访问或是死循环等错误。 */ /* 如果数值 < 0 明显是非法数值,输出错误信息并退出 */ if (needed < 0) elog(ERROR,"invalid string enlargement request size: %d",needed); /* 如果申请使用的空间大小 + str中原有的已使用的缓冲区大小 >= 系统允许 使用的最大空间上限。MaxAllocSize 在 “utils/memutils.h” 文件中定义 报告错误,并记录错误类型及其相关错误细节信息 */ if (((Size) needed) >= (MaxAllocSize - (Size) str->len)) ereport(ERROR,(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),errmsg("out of memory"),errdetail("Cannot enlarge string buffer containing %d bytes by %d more bytes.",str->len,needed))); /* 下面的语句是将申请空间大小 + 当前已经使用的空间大小 + 结束'\0' 标识符所占用的 1 个字节 的数值赋值给 needed 变量。 */ needed += str->len + 1; /* 由于上面的判断,当程序执行到这里的时候,我们已经知道 (申请空间+已经使用的空间大小+1) <= 系统允许的内存空间分配上限的 。 所以系统限定判断合法之后,接下来我们所要判断的是(申请空间大小+ 已使用空间大小+1) 与已经分配给 str 的最大空间 二者的大小关系。如果前者小于等于后者,说明无需 通过 repalloc ,分配给 str 的空间大小足够划分给申请的空间大小。 这种情况直接返回即可,无需执行后续操作。 */ if (needed <= str->maxlen) return; /* got enough space already */ /* 反之,则需要调用repalloc 函数来在已经分配给 str 的空间上面进行追加分配新的空间, 但是,为了不能每次仅因为一点点的空间不足就频繁的进行空间追加分配,这样会降低程序 的执行效率。 所以,作者在这里使用的是每次将空间扩大至原有空间的二倍。 即原来分配的空间大小为 maxlen,在调用 repalloc 方法的时候,应该为其追加 原有空间的二倍。 在这里,系统允许分配内存的上限是 INT_MAX/2 即 MaxAllocSize 的数值, 如此定义的原因是为了防止执行循环的时候,循环变量的数值会发生溢出错误。 但是,同样也会存在,即便将空间大小调至原有分配空间的 2 倍之后(2 * maxlen), 其数值仍旧小于 所需要申请分配的空间大小( 2*maxlen < needed ) 此时,就需要一个循环来不断地以成二倍的方式来增加追加分配空间的大小, 直至该追加分配空间的大小 > 请求申请空间的大小(needed) */ newlen = 2 * str->maxlen; while (needed > newlen) newlen = 2 * newlen; /* 到这里问题又出现了,虽然在前面比较过了needed 的数值一定是小于 系统分配内存的上限大小(MaxAllocSize)的, 但是并不能够保证上述通过循环成二倍增长的 追加分配空间的大小(newlen)一定是小于系统内存分配上(MaxAllocSize)限大小的。 所以执行下述的判断,一旦追加分配空间大小(newlen)大于系统内存分配上限(MaxAllocSize) 则将 newlen 赋值为 系统内存分上限大小,即 MaxAllocSize */ if (newlen > (int) MaxAllocSize) newlen = (int) MaxAllocSize; str->data = (char *) repalloc(str->data,newlen); /* 在成功追加分配之后,不要忘了更新记录分配给 str 空间大小数值的变量 str->maxlen */ str->maxlen = newlen; }