星期四, 四月 19, 2007

C语言 结构对齐

在上次的一次结构设计中招到PK后,回家恶补了结构对齐,参考了多人的Blog和一些专业论坛,由于原始内容比较乱且我经过重新整理,就不给出下面内容的原始出处。

什么是内存对齐?

考虑下面的结构:
struct foo
{
char c1;
short s;
char c2;
int i;
};

假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该是1,c2的地址就是3,i的地址就是4。也就是
c1 00000000, s 00000001, c2 00000003, i 00000004。

可是,我们在Visual c/c++ 6中写一个简单的程序:

struct foo a;
printf("c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
运行,输出:
c1 00000000, s 00000002, c2 00000004, i 00000008。

为什么会这样?这就是内存对齐而导致的问题。

在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间;各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。

例如,下面的结构各成员空间分配情况
struct test
{
char x1; //0
short x2; //2
float x3; //4
char x4; //8
};
结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

为什么会有内存对齐?


以下内容节选自《Intel Architecture 32 Manual》。
字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。

编译器对内存对齐的处理


缺省情况下,c/c++编译器默认将结构、栈中的成员数据进行内存对齐。因此,
struct foo
{
char c1;
short s;
char c2;
int i;
};
用上面的程序输出就变成了:
c1 00000000, s 00000002, c2 00000004, i 00000008。
编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。
也正是这个原因,我们不可以断言sizeof(foo) == 8。在这个例子中,sizeof(foo) == 12。

如何避免内存对齐的影响


那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:

struct bar
{
char c1;
char c2;
short s;
int i;
};
这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar) == 8。

这个技巧有一个重要的作用,尤其是这个结构作为API的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,从而造成这个结构在你的发行的DLL中使用某种对齐方式,而在第三方开发者哪里却使用另外一种对齐方式。这将会导致重大问题。

比如,foo结构,我们的DLL使用默认对齐选项,对齐为
c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(foo) == 12。
而第三方将对齐选项关闭,导致
c1 00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(foo) == 8。

如何使用c/c++中的对齐选项


编译器默认都是8字节对齐,更改C编译器的缺省分配策略,一般地,可以通过下面的两种方法改变缺省的对界条件:
  · 使用伪指令#pragma pack ([n])
  · 在编译时使用命令行参数
#pragma pack ([n])伪指令允许你选择编译器为数据分配空间所采取的对界策略:
  
例如,在使用了#pragma pack (1)伪指令后,test结构各成员的空间分配情况就是按照一个字节对齐了
#pragma pack(push) //保存对齐状态
#pragma pack(1)
#pragma pack(pop)
vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
min ( sizeof ( member ), n)
实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
/Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member alignment可以选择。

要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令。指令语法如下:
#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )
意义和/Zpn选项相同。比如:

#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()

栈内存对齐


我们可以观察到,在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。验证代码如下:

#include <stdio.h>

struct foo
{
char c1;
short s;
char c2;
int i;
};

struct bar
{
char c1;
char c2;
short s;
int i;
};

#pragma pack(1)
struct foo_pack
{
char c1;
short s;
char c2;
int i;
};
#pragma pack()


int main(int argc, char* argv[])
{
char c1;
short s;
char c2;
int i;

struct foo a;
struct bar b;
struct foo_pack p;

printf("stack c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
(unsigned int)(void*)&s - (unsigned int)(void*)&i,
(unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
(unsigned int)(void*)&i - (unsigned int)(void*)&amp;i);

printf("struct foo c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
(unsigned int)(void*)&a.i - (unsigned int)(void*)&amp;a);

printf("struct bar c1 %p, c2 %p, s %p, i %pn",
(unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
(unsigned int)(void*)&b.i - (unsigned int)(void*)&amp;b);

printf("struct foo_pack c1 %p, s %p, c2 %p, i %pn",
(unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
(unsigned int)(void*)&p.i - (unsigned int)(void*)&amp;p);

printf("sizeof foo is %dn", sizeof(foo));
printf("sizeof bar is %dn", sizeof(bar));
printf("sizeof foo_pack is %dn", sizeof(foo_pack));

return 0;
}


内存对齐与ANSI C中struct型数据的内存布局

  当在C中定义了一个结构类型时,它的大小是否等于各字段(field)大小之和?编译器将如何在内存中放置这些字段?ANSI C对结构体的内存布局有什么要求?而我们的程序又能否依赖这种布局?这些问题或许对不少朋友来说还有点模糊,那么本文就试着探究它们背后的秘密。
  
  首先,至少有一点可以肯定,那就是ANSI C保证结构体中各字段在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个字段的首地址等于整个结构体实例的首地址。比如有这样一个结构体:
  
  struct vector{int x,y,z;} s;
  int *p,*q,*r;
  struct vector *ps;
  
  p = &s.x;
  q = &s.y;
  r = &s.z;
  ps = &s;
  
  assert(p < q);
  assert(p < r);
  assert(q < r);
  assert((int*)ps == p);
  // 上述断言一定不会失败
  
  这时,有朋友可能会问:"标准是否规定相邻字段在内存中也相邻?"。 唔,对不起,ANSI C没有做出保证,你的程序在任何时候都不应该依赖这个假设。那这是否意味着我们永远无法勾勒出一幅更清晰更精确的结构体内存布局图?哦,当然不是。不过先让我们从这个问题中暂时抽身,关注一下另一个重要问题————内存对齐。
  
  许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。Win32平台下的微软C编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗?)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如long,double)都以4为对齐模数。
  
  现在回到我们关心的struct上来。ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。嗯?填充区?对,这就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。那么结构体本身有什么对齐要求吗?有的,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格(但此非强制要求,VC7.1就仅仅是让它们一样严格)。我们来看一个例子(以下所有试验的环境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,内存对齐编译选项是"默认",即不指定/Zp与/pack选项):
  
  typedef struct ms1
  {
  char a;
  int b;
  } MS1;
  
  假设MS1按如下方式内存布局(本文所有示意图中的内存地址从左至右递增):

  _____________________________

  |    |          |

  |  a  |    b     |

  |    |          |

  +---------------------------+

  Bytes:  1       4

  
  因为MS1中有最强对齐要求的是b字段(int),所以根据编译器的对齐规则以及ANSI C标准,MS1对象的首地址一定是4(int类型的对齐模数)的倍数。那么上述内存布局中的b字段能满足int类型的对齐要求吗?嗯,当然不能。如果你是编译器,你会如何巧妙安排来满足CPU的癖好呢?呵呵,经过1毫秒的艰苦思考,你一定得出了如下的方案:
  
  _______________________________________   |    |\\\\\|         |   |  a  |\padding\|    b     |   |    |\\\\\|         |   +-------------------------------------+
  Bytes:  1     3       4
  
  这个方案在a与b之间多分配了3个填充(padding)字节,这样当整个struct对象首地址满足4字节的对齐要求时,b字段也一定能满足int型的4字节对齐规定。那么sizeof(MS1)显然就应该是8,而b字段相对于结构体首地址的偏移就是4。非常好理解,对吗?现在我们把MS1中的字段交换一下顺序:
  
typedef struct ms2
{
  int a;
  char b;
} MS2;
  
  或许你认为MS2比MS1的情况要简单,它的布局应该就是
  
  _______________________   |       |    |   |   a    |  b  |   |       |    |   +---------------------+   Bytes:   4      1
  
  因为MS2对象同样要满足4字节对齐规定,而此时a的地址与结构体的首地址相等,所以它一定也是4字节对齐。嗯,分析得有道理,可是却不全面。让我们来考虑一下定义一个MS2类型的数组会出现什么问题。C标准保证,任何类型(包括自定义结构类型)的数组所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话说,数组各元素之间不会有空隙。按照上面的方案,一个MS2数组array的布局就是:
  
  |<-  array[1]   ->|<-  array[2]   ->|<- array[3] .....      __________________________________________________________   |       |    |       |   |   |   a    |  b  |   a    |  b |.............   |       |    |       |   |   +----------------------------------------------------------   Bytes: 4     1     4      1
  
  当数组首地址是4字节对齐时,array[1].a也是4字节对齐,可是array[2].a呢?array[3].a ....呢?可见这种方案在定义结构体数组时无法让数组中所有元素的字段都满足对齐规定,必须修改成如下形式:
  
  ___________________________________   |       |    |\\\\\|   |   a    |  b  |\padding\|   |       |    |\\\\\|   +---------------------------------+   Bytes:   4      1     3
  
  现在无论是定义一个单独的MS2变量还是MS2数组,均能保证所有元素的所有字段都满足对齐规定。那么sizeof(MS2)仍然是8,而a的偏移为0,b的偏移是4。
  
  好的,现在你已经掌握了结构体内存布局的基本准则,尝试分析一个稍微复杂点的类型吧。
  
typedef struct ms3
{
  char a;
  short b;
  double c;
} MS3;
  
  我想你一定能得出如下正确的布局图:
  
  padding   |   _____v_________________________________   |  ||   |\\\\|        |   | a || b |padding|    c    |   |  ||   |\\\\|        |   +-------------------------------------+   Bytes: 1 1  2    4      8
  
  sizeof(short)等于2,b字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double)等于8,c字段要从8倍数地址开始,前面的a、b字段加上填充字节已经有4 bytes,所以b后面再填充4个字节就可以保证c字段的对齐要求了。sizeof(MS3)等于16,b的偏移是2,c的偏移是8。接着看看结构体中字段还是结构类型的情况:
  
typedef struct ms4 {   char a;   MS3 b; } MS4;
  
  MS3中内存要求最严格的字段是c,那么MS3类型数据的对齐模数就与double的一致(为8),a字段后面应填充7个字节,因此MS4的布局应该是:
  _______________________________________   |    |\\\\\|         |   |  a  |\padding\|    b     |   |    |\\\\\|         |   +-------------------------------------+   Bytes:  1     7       16
  
  显然,sizeof(MS4)等于24,b的偏移等于8。
  
  在实际开发中,我们可以通过指定/Zp编译选项来更改编译器的对齐规则。比如指定/Zpn(VC7.1中n可以是1、2、4、8、16)就是告诉编译器最大对齐模数是n。在这种情况下,所有小于等于n字节的基本数据类型的对齐规则与默认的一样,但是大于n个字节的数据类型的对齐模数被限制为n。事实上,VC7.1的默认对齐选项就相当于/Zp8。仔细看看MSDN对这个选项的描述,会发现它郑重告诫了程序员不要在MIPS和Alpha平台上用/Zp1和/Zp2选项,也不要在16位平台上指定/Zp4和/Zp8(想想为什么?)。改变编译器的对齐选项,对照程序运行结果重新分析上面4种结构体的内存布局将是一个很好的复习。

星期四, 四月 12, 2007

LEX 从易到难

Lex 入门

First!
lex程序的结构是这样的!
定义
%%
规则
%%
用户代码

一个 Lex 程序分为三个段:第一段是 C 和 Lex 的全局声明,第二段包括模式(C 代码),第三段是补充的 C 函数。 这些段以%%来分界。 下面是一个行数与字数的统计工具。

int num_lines = 0, num_chars = 0;
%%
n ++num_lines; ++num_chars;
. ++num_chars;

%%
main()
{
yylex();
printf( "# of lines = %d, # of chars = %dn",
num_lines, num_chars );
}

Second!
对First内容的回顾
C 和 Lex 的全局声明
这一段中我们可以增加 C 变量声明。这里我们将为字数统计程序声明一个整型变量,来保存程序统计出来的字数。我们还将进行 Lex 的标记声明。

字数统计程序的声明
%{
int wordCount = 0;
%}
chars [A-za-z_'."]
numbers ([0-9])+
delim [" "nt]
whitespace {delim}+
words {chars}+
%%

两个百分号标记指出了 Lex 程序中这一段的结束和三段中第二段的开始。

Lex 的模式匹配规则
让我们看一下 Lex 描述我们所要匹配的标记的规则。(我们将使用 C 来定义标记匹配后的动作。)继续看我们的字数统计程序,下面是标记匹配的规则。
字数统计程序中的 Lex 规则
{words} { wordCount++; /*
increase the word count by one*/ }
{whitespace} { /* do
nothing*/ }
{numbers} { /* one may
want to add some processing here*/ }
%%
C 代码
Lex 编程的第三段,也就是最后一段覆盖了 C 的函数声明(有时是主函数)。注意这一段必须包括 yywrap() 函数。 Lex 有一套可供使用的函数和变量。 其中之一就是 yywrap。一般来说,yywrap() 的定义如下例。我们将在 高级 Lex 中探讨这一问题。
字数统计程序的 C 代码段
void main()
{
yylex(); /* start the
analysis*/
printf(" No of words:
%dn", wordCount);
}
int yywrap()
{
return 1;
}

Lex 编程的基本元素就这样搞定了,它将帮助你编写简单的词法分析程序。
Third
高级Lex
Lex 有几个函数和变量提供了不同的信息,可以用来编译实现复杂函数的程序。下表中列出了一些变量和函数,以及它们的使用。 详尽的列表请参考 Lex 手册。
Lex 变量
yyin FILE* 类型。 它指向 lexer 正在解析的当前文件。
yyout FILE* 类型。 它指向记录 lexer 输出的位置。 缺省情况下,yyin 和 yyout 都指向标准输入和输出。
yytext 匹配模式的文本存储在这一变量中(char*)。
yyleng 给出匹配模式的长度。
yylineno 提供当前的行数信息。(lexer不一定支持。)

Lex 函数
yylex() 这一函数开始分析。 它由 Lex 自动生成。
yywrap() 这一函数在文件(或输入)的末尾调用。如果函数的返回值是1,就停止解析。 因此它可以用来解析多个文件。代码可以写在第三段,这就能够解析多个文件。 方法是使用 yyin 文件指针(见上表)指向不同的文件,直到所有的文件都被解析。最后,yywrap() 可以返回 1 来表示解析的结束。
yyless(int n) 这一函数可以用来送回除了前�n? 个字符外的所有读出标记。
yymore() 这一函数告诉 Lexer 将下一个标记附加到当前标记后。
到此为止,可能你看到lex程序还会范晕,没关系,下面我们接着来,分析一个类pascal语法的极简析器!
/* 这个就是注释了*/
/* scanner for a toy Pascal-like language */
申明部分开始
%{ 内的东西会原封不动地出现在输出文件中 }%
%{
/* need this for the call to atof() below */
#include <math.h>
%}
DIGIT [0-9]
ID [a-z][a-z0-9]*
%%
模式部分开始
{DIGIT}+ {
printf( "An integer: %s (%d)n", yytext,
atoi( yytext ) );
}
{DIGIT}+"."{DIGIT}* {
printf( "A float: %s (%g)n", yytext,
atof( yytext ) );
}
if|then|begin|end|procedure|function {
printf( "A keyword: %sn", yytext );
}
{ID} printf( "An identifier: %sn", yytext );
"+"|"-"|"*"|"/" printf( "An operator: %sn", yytext );
"{"[^}n]*"}" /* eat up one-line comments */
[ tn]+ /* eat up whitespace */
. printf( "Unrecognized character: %sn", yytext );
%%
补充部分开始
main( argc, argv )
int argc;
char **argv;
{
++argv, --argc; /* skip over program name */
if ( argc > 0 )
yyin = fopen( argv[0], "r" );
else
yyin = stdin;
yylex();
}
想要真正了解lex, [[正则表达式]] 是关键!
Four
yytext 匹配模式的文本存储变量, 可以通过在申明阶段使用%pointer或%array来控制是一个字符指针还是一个字符数组。指针模式与数组模式各有特点,导致在yytex申明上也不一样,具体请参考lex手册!
在模式阶段中
模式 动作
[ t]+ putchar( ' ' );
[ t]+$ /* ignore this token */
模式部分是正则表达式,动作部分是处理方法,动作部分如果时{开头,那么,动作将会持续到},如果动作中出现了括号{},开始采用 %{ %}来表示动作去区段。动作部分如果时 |,就表示与下一条规则执行相同的动作。
好的,我们来看一个更为实用一点的lex程序。
我们先定义三个动作:
ECHO 将yytext输出
BEGIN 开始一个条件处理块
REJECT 指示简析器对当前规则不做处理,而是采用第二匹配规则。
int word_count = 0;
%%
frob special(); REJECT;
[^ tn]+ ++word_count;
如果frob没有REJECT动作,frob将不会被计数,因为解析器在通常情况下,每个被匹配的对象只会对一个动作生效,多个REJECT也是允许的,会寻找下一个最配的规则来做处理。所以,下面的规则会把输入的"abcd"处理后输出"abcdabcaba".
%%
a |
ab |
abc |
abcd ECHO; REJECT;
.|n /* eat up any unmatched character */

`yymore()' 告诉解析器下一次匹配的规则,满足的部分将会添加到当前yytext值得后面而不是替换它。 例如,指定的输入"mega-kludge"经过下面的程序处理后将会输出"mega-mega-kludge"。
%%
mega- ECHO; yymore();
kludge ECHO;
第一个 "mega-" 被满足并且输出. 然后 "kludge" 满足, 但是并没有替换之前的"mega-"而是"kludge"附加到他的后面,然后输出的其实是"mega-kludge".
yymore()需要两件事情需要注意。第一,yymnore()依赖于表现当前匹配项的长度yyleng的值,所以使用yymore不允许改变yyleng的值。第二,yymore()的使用会使解析器付出一点点性能的代价。
有yymore()就有yyless()
yyless(n) 返回当前匹配项除了开始的n个字符内的所有的内容到输入缓存区,解析器处理下一个匹配时,它们将会被重新解析。yyless将会导致yytext与yyleng的调整。(yyleng将会等于=n) 如输入"foobar"被下面的程序处理后,将会输出"boobarbar". 因为前n=3个字符foo外的字符bar被重新返回到输入缓存区了。
%%
foobar ECHO; yyless(3);
[a-z]+ ECHO;
参数0对于yyless将会导致整个当前匹配将会被重新解析。除非你改变了解析器本来的处理流程(如使用begin),这将会导致循环结束。需要注意的是,yyless是一个宏,并且在flex输入文件中使用,不能在其他源文件中使用。
unput(c) 将字符c放回到输入流中,该字符可以重新被解析。下面的动作将当前的匹配值附上括号后重新进行匹配。
{
int i;
/* Copy yytext because unput() trashes yytext */
char *yycopy = strdup( yytext );
unput( ')' );
for ( i = yyleng - 1; i >= 0; --i )
unput( yycopy[i] );
unput( '(' );
free( yycopy );
}
注意: 由于每次unput()将指定的字符添加到输入源的开头,所以将字符串添加到输入源开头必须从后道前处理。一个比较重要的潜在问题是使用unput()的时候,如果采用了%pointer指针模式保存yytext,unput会破坏yytext的内容,从最右边的字符开始将会破坏左边的一个字符。如果在unput()后要用到yytext,你首先必须复制一份yytext,或者用%array模式来保存yytext. 最后你不能放一个EOF去试图标志输入流的结束。
input 从输入源中读取下一个字符。例如,下面有的例子将会吃掉C语言注释
%%
"/*" {
register int c;
for ( ; ; )
{
while ( (c = input()) != '*' &&
c != EOF )
; /* eat up text of comment */
if ( c == '*' )
{
while ( (c = input()) == '*' )
;
if ( c == '/' )
break; /* found the end */
}
if ( c == EOF )
{
error( "EOF in comment" );
break;
}
}
}
注意: 如果简析器采用用C++编译,input()被yyinput()的替代,因为input()与C++中的流名称input冲突。
YY_FLUSH_BUFFER 刷新解析器内部缓存以便于下一次的匹配工作,首先它会使用YY_INPUT填充缓存区。这是通用yy_flush_buffer()的一个特例,将会在多输入缓存中描述。
yyterminate()可以在动作内部返回描述区域中使用,它将终止解析器并返回0给解析器调用者,表示操作完成。缺省情况下,到达文件结束位置也会被调用,它是一个宏,并且可能重定义。

Lex进阶

模式
模式在第一阶段或第二个阶段使用,也就是在申明或规则阶段中出现,模式定义了匹配的目标,目标被匹配后将会执行动作。
对于模式不想做太多说明,使用正则表达式定义,可以参看 regex 或 pcre.

开始条件
lex提供了根据条件激活规则的机制。在<sc>前缀的规则将会在解析器在"sc"的开始条件下被匹配。
<STRING>[^"]*        { /* eat up the string body ... */
...
}
将会在启动条件"STRING"的情况下被激活。
<INITIAL,STRING,QUOTE>.        { /* handle an escape ... */
...
}
将会在 "INITIAL", "STRING", "QUOTE"三者之一的条件下被激活。

开始条件在输入源的定义(第一)部分被申明,在‘%s' 或 ’%x'后跟随着名字列表。 %s申明了包含的开始条件,%x申明了排他的开始条件。开始条件被BEGIN动作激活。直到下一个BEGIN动作,满足开始条件名称的规则将会被规则,不满足启动条件的规则将不会被执行。

如果是包含条件,没有开始条件的规则也会被激活执行,如果时排他条件,只有满足开始条件的规则才会被执行。
具有相同排他条件的规则的集合可以使解析器独立于其他的规则。 因此,排他条件可以容易地创建微型解析器处理输入源中的独立与其他部分的一部分(如,注释)。如果对于包含与排他条件还有混淆,可以看下面的例子。
%s example
%%

<example>foo do_something();

bar something_else();

等同于

%x example
%%

<example>foo do_something();

<INITIAL,example>bar something_else();
上面的程序中如果没有<INITIAL,example>,在example条件下bar规则将永远不会被激活。如果使用<example>,将会导致只能在exmaple开始条件下激活,而INITIAL条件下不会被激活。而第一个程序中在任何条件下bar都被会激活。因为第一个程序用example时%s,时包含条件。页可以通过特殊开始条件<*>来配置任何开始条件,上面的程序还可以写为:
%x example
%%

<example>foo do_something();

<*>bar something_else();
缺省规则(显示任何未被匹配的字符)在开始条件下仍然生效。等同于:
<*>.|\n     ECHO;
‘BEGIN(0)’在无开始条件的规则激活条件下返回原始状态,这个状态同于开始条件下的'INITIAL',所以‘BEGIN(INITIAL)'等同于’BEGIN(0)'。
BEGIN行为在规则部分的开头是默认的代码(BEGIN actions can also be given as indented code at the beginning of the rules section.请翻译) 例如,下面的代码将会仅需SPECIAL开始条件,不管合适yylex()被调用并且全局变量enter_special是true。
        int enter_special;

%x SPECIAL
%%
if ( enter_special )
BEGIN(SPECIAL);

<SPECIAL>blahblahblah
...more rules follow...
为了说明开始条件,我们用两种方法处理"123.456".缺省将会被解析为 '123','.','456'三个标记,如果expect-floats后面将会被解析为浮点数 123.456
%{
#include <math.h>
%}
%s expect

%%
expect-floats BEGIN(expect);

<expect>[0-9]+"."[0-9]+ {
printf( "found a float, = %fn",
atof( yytext ) );
}
<expect>n {
/* that's the end of the line, so
* we need another "expect-number"
* before we'll recognize any more
* numbers
*/
BEGIN(INITIAL);
}

[0-9]+ {

printf( "found an integer, = %dn",
atoi( yytext ) );
}

"." printf( "found a dotn" );
下面的代码能够是被C语言注释并且统计行数。
%x comment
%%
int line_num = 1;

"/*" BEGIN(comment);

<comment>[^*n]* /* eat anything that's not a '*' */
<comment>"*"+[^*/n]* /* eat up '*'s not followed by '/'s */
<comment>n ++line_num;
<comment>"*"+"/" BEGIN(INITIAL);
实际上,编写高速解析程序的办法时在每个规则中做尽可能多的匹配。

This scanner goes to a bit of trouble to match as much text as possible with each rule. In general, when attempting to write a high-speed scanner try to match as much possible in each rule, as it's a big win.

注意: 开始条件的名字实际上时一个整形值并且能够被保存,所以,上面的代码可以扩展为:
%x comment foo
%%
int line_num = 1;
int comment_caller;

"/*" {
comment_caller = INITIAL;
BEGIN(comment);
}

...

<foo>"/*" {
comment_caller = foo;
BEGIN(comment);
}

<comment>[^*n]* /* eat anything that's not a '*' */
<comment>"*"+[^*/n]* /* eat up '*'s not followed by '/'s */
<comment>n ++line_num;
<comment>"*"+"/" BEGIN(comment_caller);
而且,可能易使用YY_START宏来访问当前的开始条件。如上面的赋值条件可以改写为
comment_caller = YY_START
YYSTATE是YY_START的别名(因为AT&T lex使用了YYSTATE)。
注意 开始条件没有他们的名字空间; %s 与 %x 申明与 #define形式一样。

到这里,时一个使用排他开始条件如何匹配C风格的引用字符串的处理。包含的扩展的转义,但不包括检查,因为代码太长。
%x str

%%
char string_buf[MAX_STR_CONST];
char *string_buf_ptr;

" string_buf_ptr = string_buf; BEGIN(str);

<str>" { /* saw closing quote - all done */
BEGIN(INITIAL);
*string_buf_ptr = '0';
/* return string constant token type and
* value to parser
*/
}

<str>n {
/* error - unterminated string constant */
/* generate error message */
}

<str>\[0-7]{1,3} {
/* octal escape sequence */
int result;

(void) sscanf( yytext + 1, "%o", &result );

if ( result > 0xff )
/* error, constant is out-of-bounds */

*string_buf_ptr++ = result;
}

<str>\[0-9]+ {
/* generate error - bad escape sequence; something
* like '48' or '0777777'
*/
}

<str>\n *string_buf_ptr++ = 'n';
<str>\t *string_buf_ptr++ = 't';
<str>\r *string_buf_ptr++ = 'r';
<str>\b *string_buf_ptr++ = 'b';
<str>\f *string_buf_ptr++ = 'f';

<str>\(.|n) *string_buf_ptr++ = yytext[1];

<str>[^\n"]+ {
char *yptr = yytext;

while ( *yptr )
*string_buf_ptr++ = *yptr++;
}
通常,如上面的例子中所看到你,会有许多相同开始条件的处理。开始条件范围可以简化重复操作。

<SCs>{
}

SCs 是一个或开始条件的列表。在这个开始条件范围内,每个规则将会自动具有前缀 `<SCs>' 直到 `}' 与开始的 `{' 匹配. 例如

<ESC>{
"\n" return 'n';
"\r" return 'r';
"\f" return 'f';
"\0" return '0';
}

等价于

<ESC>"\n"  return 'n';
<ESC>"\r" return 'r';
<ESC>"\f" return 'f';
<ESC>"\0" return '0';

开始条件页可以嵌套,下面时三个管理开始条件堆栈的参数。

`void yy_push_state(int new_state)'
将当前的开始条件压栈,切换到 new_state 与使用 `BEGIN new_state'类似。
`void yy_pop_state()'
从栈顶弹出,类似于 BEGIN.
`int yy_top_state()'
返回栈顶值,不改变栈内容。

开始条件栈动态增长,没有固定限制,如果内容用尽,程序竟会终止。

为了使用开始条件栈,需要使用 `%option stack' 指令。



多输入缓存区


一些允许include文件解析器的解析器要求从几个输入流中读取内容。YY_INPUT只在结束缓存时被调用,碰到 include 后需要切换输入源,而解析一个描述也许需要很长时间。为了解决此类问题,解析器提供了创建并在多个输入缓存中创建的机制。输入缓存可以通过下面的方式创建:

YY_BUFFER_STATE yy_create_buffer( FILE *file, int size )

参数为与缓存关联的输入文件指针,以及足够的可维持size字符(如果不确定,size可以使用YY_BUF_SIZE)。返回一个YY_BUFFER_STATE,可以传递到其他的处理过程。YY_BUFFER_STATE是一个不可见结构yy_buffer_state的指针,所以可以安全地使用`((YY_BUFFER_STATE) 0)'来初始化YY_BUFFER_STATE,如果你愿意,你可以在解析器之外的源程序中引用这个不透明结构来正确的申明输入缓存。可以通过下面的参数来选择一个缓存区。

void yy_switch_to_buffer( YY_BUFFER_STATE new_buffer )

切换解析器的输入缓存将会导致记接下来的匹配项来自于新的缓存中。yy_switch_to_buffer可能出现在yywrap中为继续解析做准备,替换打开一个新的文件并执行yyin. 通过yy_switch_to_buffer 或 yywrap切换输入源不改变开始条件。


void yy_delete_buffer( YY_BUFFER_STATE buffer )

用于收回与缓存关联的空间。你可以使用下面的函数清空当前内容:
void yy_flush_buffer( YY_BUFFER_STATE buffer )

此函数废弃缓存内容,下一个解析器试图匹配一个内容时将会使用YY_INPUT来更新缓存区。

`yy_new_buffer()' 是 `yy_create_buffer()' 的一个别名,用于提供C++使用new 与 delete操作创建与销毁动态对象的兼容性。

最后, YY_CURRENT_BUFFER 宏返回 YY_BUFFER_STATE 指针,表示当前的缓存。

这里是一个扩展include使用的一个解析器 (`<<EOF>>' 特性将会在以后讨论):

/* "incl" 状态用于获取include的文件名 */
%x incl

%{
#define MAX_INCLUDE_DEPTH 10
YY_BUFFER_STATE include_stack[MAX_INCLUDE_DEPTH];
int include_stack_ptr = 0;
%}

%%
include BEGIN(incl);

[a-z]+ ECHO;
[^a-zn]*n? ECHO;

<incl>[ t]* /* eat the whitespace */
<incl>[^ tn]+ { /* got the include file name */
if ( include_stack_ptr >= MAX_INCLUDE_DEPTH )
{
fprintf( stderr, "Includes nested too deeply" );
exit( 1 );
}

include_stack[include_stack_ptr++] =
YY_CURRENT_BUFFER;

yyin = fopen( yytext, "r" );

if ( ! yyin )
error( ... );

yy_switch_to_buffer(
yy_create_buffer( yyin, YY_BUF_SIZE ) );

BEGIN(INITIAL);
}

<<EOF>> {
if ( --include_stack_ptr < 0 )
{
yyterminate();
}

else
{
yy_delete_buffer( YY_CURRENT_BUFFER );
yy_switch_to_buffer(
include_stack[include_stack_ptr] );
}
}

提供三个过程来实现内存字符串而不是文件输入缓存的解析。它们都要创建一个输入缓存来解析字符串,并且返回YY_BUFFER_STATE (可以在完成解析后用 `yy_delete_buffer()' 删除).,也可以通过`yy_switch_to_buffer()'来切换, 下一次调用`yylex()' 将会解析字符串。
`yy_scan_string(const char *str)' 解析0结尾字符串。
`yy_scan_bytes(const char *bytes, int len)' 解析bytes开始的len个字符(可能包含 0 字符)

注意,上面的两个函数会创建字符串或字节串的副本。(这也许时期望的,因为`yylex()' 会修改被解析缓存的内容) 可以使用下面的方式来拒绝使用副本:
`yy_scan_buffer(char *base, yy_size_t size)'
将会从base开始解析,包含size个字节, 最后的两个字节必须是 YY_END_OF_BUFFER_CHAR (ASCII NUL)。他们不会被解析, 解析范围从 `base[0]' 到 `base[size-2]'(包含)。如果你没能按照这种规定使用base(如,忘记了最后的两个YY_END_OF_BUFFER_CHAR字节), `yy_scan_buffer()' 将会返回空指针而不创建YY_BUFFER_STATE。yy_size_t类型是个整型,可以转化为整数来反映buffer的长度。


文件结束规则

特殊规则 "<<EOF>>" 只是规则在文件结束位置发生且yywrap()返回非0值。(如,没有更多的文件要处理). 这个动作必须完成下面四件事情之一:
赋值给yyin一个新的文件 (早期版本的flex, 此操作后必须调用特殊动作 YY_NEW_FILE; 这个操作已经不需要了);
执行一个返回申明;
执行一个特殊的`yyterminate()' 动作;
或者使用`yy_switch_to_buffer()' 切换到一个新的输入缓存区.

<<EOF>> 不能与其他模式一起使用;它也许仅在开始条件列表申明。如果指定了不合法 <<EOF>> 规则, 它将会应用到所有的开始条件而不仅是 <<EOF>> 动作. 指定 <<EOF>> 规则仅在 initial 开始条件下匹配,就是用:
<INITIAL><<EOF>>

下面的规则可以发现象不关闭的注释类的问题。
%x quote
%%

...other rules for dealing with quotes...

<quote><<EOF>> {
error( "unterminated quote" );
yyterminate();
}
<<EOF>> {
if ( *++filelist )
yyin = fopen( *filelist, "r" );
else
yyterminate();
}


星期二, 四月 03, 2007

俩大Win32牛人

Bjarke Viksoe http://www.viksoe.dk/code/index.htm
PJ Naughter http://www.naughter.com/

星期六, 三月 31, 2007

Apache 中内存管理的三种境界

Apache 中内存管理的三种境界

zhaozg http://zhaozg.googlepages.com

中国文化中几乎所有的数字都被冠以特殊的意义,《易》曰:"道生一,一生二,二生三,三生万物"。由于个人知识与经历的限制,无法完成对于万物的探究,但是三以下的数字数字还是可以追述的。

Apache作为万维网首屈一指的高性能Web服务器,如果能够从科学与哲学的角度进行分析,将会对我们的软件开发者的学习工作工作带来极大的好处.

正如《易》曰"书不尽言,言不尽意",写出来的未必能够表达我说出来的,说出来的未必能够表到我想说出来的。所以如果我不能描绘出我体会到的Apache中内存管理的三种境界,请不要责怪。

APR(Apache Platform Protable) Library 是Apache为了实现跨平台而抽象出来的一套开发库,内存管理作为与系统紧密相关的软件开发的基础之一,在APR中得到了充分的体现。

第一层境界,基于apr_pool的内存管理。apr_poll简单易用,但是只能申请内存而无法释放或返回缓存池,只能到apr_pool_t到清除(clear)或消毁(destory)才能够继续使用,而且如果根pool没有被消毁之前,内存是无法返回系统的,不够灵活,apr_pool中用了一个全局的根pool,而高级方式的apr_pool可以在第三层境界的基础上直接创建。所以如果Apache中使用了比较大的内存,尤其是在频繁使用的情况下,最好不要用Apache中已近存在的pool结构,而是采用apache中的第二层境界来解决。

第二层境界,基于apr_bucket_alloc的内存管理。与apr_pool向相比,apr_bucket_alloc,最大的特点是可以将以申请的内存通过释放而返回到内存池,但不返回系统,以后申请内存可以直接从内存池中获取,并且对于小块内存的管理作了优化,接单易用而且灵活,并且apr_bucket_alloc在apr_util中实现,apache中内存管理的第一层与第三曾境界是在apr中实现的。可笑的是apr_bucket_alloc可以通过第一层apr_pool来创建,不过在创建的时候使用的是apr_pool中的allocator(第三层结构的指针)。

第三层境界,基于apr_allocator的内存管理。apr_allocator的内存管理更为低级,内存申请返回一个表示内存的结构而不是实现可用的内存指针,可以在此基础上实现诸如小块内存的优化管理,apr_pool与apr_bucket_alloc都是在apr_allocator的基础上实现的。apr_allocator的基础就是标准的malloc/free函数了。

整理一下,关系如下

malloc/free-->apr_allocator-------->apr_bucket_alloc
/
/
/
apr_pool


第二层境界,基于apr_bucket_alloc的内存管理使用举例

初始化操作

       apr_pool_t *pool;
apr_allocator_t* alloc;
apr_bucket_alloc_t* balloc;
       apr_allocator_create(&lloc);
apr_pool_create_ex(&pool, NULL, NULL, alloc);
balloc = apr_bucket_alloc_create(pool);

内存申请与释放

char* p= apr_bucket_alloc(size, balloc);
apr_bucket_free(p);
         

终止化操作:

       apr_bucket_alloc_destroy(balloc);
apr_pool_destroy(pool);
apr_allocator_destroy(alloc);

星期五, 二月 02, 2007

gcc的语法检查

在编译mod_lua 0.5的过程中,下面的代码出现警告
LuaState *L;
void** data = (void**) &L;
warning: dereferencing type-punned pointer will break strict-aliasing rules

-------------
gcc version 4.1.1 20061011 (Red Hat 4.1.1-30)


somebody say that:

Yes, by implying -fstrict-aliasing, so using -fno-strict-aliasing is a
workaround. The problem is caused by the i386 PCPU_GET/PCPU_SET
implementation:

#define __PCPU_GET(name) ({ \
__pcpu_type(name) __result; \
\
[...]
} else if (sizeof(__result) == 4) { \
u_int __i; \
__asm __volatile("movl %%fs:%1,%0" \
: "=r" (__i) \
: "m" (*(u_int *)(__pcpu_offset(name)))); \
__result = *(__pcpu_type(name) *)&__i; \
[...]

In this case, the PCPU_GET is used to retrieve curthread, causing
sizeof(__result) to be 4, so the cast at the end of the code snippet
is from a u_int * to struct thread *, and __i is accessed through the
casted pointer, which violates the C99 aliasing rules.
An alternative is to type-pun via a union, which is also a bit ugly,
but explicitly allowed by C99. Patch attached (but only superficially
tested).

mod_lua 0.5终于发布了

在mod_lua 0.5上已经花费好大精力了,在经过一番修改与测试之后,在http://mod-lua.sourceforge.net上发布了这个版本,继续其它的工作! 

星期四, 一月 25, 2007

网络好像好了,开发庆贺

访问sourceforge.net已经快多了,blogger.com也可以快速打开!



使用uTorrent带起了BitTorrent,让BitTorrent见鬼去吧!