注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

perl基础  

2012-06-03 15:48:05|  分类: 脚本语言 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、perl语言
这种脚本语言和我们之前常见的语言均不相同,这里所谓的常见主要是我以前接触到的语言,例如通常的C代码的静态语言已经以shell脚本为代表的传统脚本语言。对于C语言来说,所有变量在使用之前声明它的类型(当然最为早期的C语言也是可以不声明类型的,默认为int,函数参数个数任意多等,在新的C99、特别是C++之后,这些类型检测得到进一步的强化)。而对于shell(maikefile等)脚本来说,虽然变量使用之前不需要声明类型,但是事实上它的变量类型是确定的,那就是字符串或者说文本类型,所有的变量都是一个类型,所以说常见的脚本语言是对C等类型语言的一种退化。而对于perl(python、ruby等)之类的语言来说,它们更加向前进了一步,它具有shell的变量无需声明特征,而且类型也不再单一,同一个变量本身可以认为具有多态性,也就是变量可以是int类型值,也可以是字符串形式的值,也可以是浮点类型等等,这一个比较有意思的推论就是对于一个数组定义来说,数组中的变量并不是同质的,同一个数组中的变量可以分别是整数、浮点数或者是字符串等形式,所以下面的结构是完全合法的
[tsecer@Harry perl-5.10.1]$ cat demoarry.pl
#!/usr/bin/perl
@array=(1234,"hello world",3.1415,);
foreach (@array)
{
    print;
}
[tsecer@Harry perl-5.10.1]$ perl demoarry.pl
1234hello world3.1415[tsecer@Harry perl-5.10.1]$
这也就是所谓“动态语言”的特征,它的变量可以在不同的地方展现出不同的特征。这一点对于perl语言的使用者来说是比较优势的一个功能,但是对于perl语言的实现者和源代码的阅读者来说,这个代价还是比较大的,特别是对于perl的源代码,号称是支持上百个平台,所以里面的宏非常多,代码读起来还是灰常复杂的。同样为了支持动态语言,每个结构的确需要在每个不同场景表现出不同的行为,所以代码的复杂性也很大,如果有同学在网上搜索源代码的说明的话,可以搜索很多关于perl源代码晦涩的抱怨和吐槽。
事实上,对于一个看惯了各种语言实现的同学来说,语法文件的实现本身没有什么复杂的地方,perl源代码的复杂和它的代码风格有关,或者是一种认为引入的代码复杂性,因为perl的源代码并不多,大部分核心实现都是位于perl代码的根目录下。只是里面的宏太多,导致对于源代码的阅读带来很大障碍。从这里看,perl的语法和最终写出的程序风格都比较类似,特别是perl和早期的C语言更加类似,那就是比较随意,同样功能的实现,使用python来实现或者shell来实现在源代码级别上体现出来就是perl应该是最小的,但是这种表面上的优势常常会带来易读性的问题。这一点和中文的汉族风格类似,据说在联合国的官方文档中,用中文写的文档最薄,但是不可否认的是,中文的阅读和书写是最为复杂的,同样的一个句子,我想通过手动书写的时候中文应该是最慢的,即使使用简体中文也是如此。
但是无论如何,perl语言可能还是使用的比较多,可能是它较少的限制正好和C语言一样迎合了键盘敲击次数最少的特征,它还是一种比较流行的动态语言,在很多的cgi后台中会使用perl来作为解析器。而且perl的正则表达式处理能力比较强大,这一点是shell所不具备的,或者说shell对于正则表达式的粘合粒度过大,在一些文本处理涉及到比较复杂的上下文环境的时候,perl的正则表达式功能可以非常强大,这也是shell所不具备的特征。所以在一些日常自动化事务处理过程中,能够使用一点perl语言还是比较必要的,所以我们这里尝试对perl的实现代码进行简单分析。
二、基本类型及概念
1、代码和数据
这两个事实上是所有程序语言(在冯诺依曼机型下?)都要面临的基本问题,再进一步细化来说,一条基本的CPU指令通常会包含两个部分,那就是我们常说的操作符(操作码)和操作数。对于perl这种动态语言来说,它同样需要解决这两个基本问题,也就是分离出操作码和操作数两种类型的数据。具体来说,它们在perl的源代码中分别对应opXX类和SVXX类。话到这里,似乎还一切比较明了,但是这是整个实现的基础,我们可以认为它们分别是C++的基础类结构,在此基础上建立各种派生类,某些框架只识别这些基本类型,其它类型都在该类型基础上扩展。这一点对于gcc的实现同样成立,它的基本类型为tree结构,所有内部结构都需要在最开始定义为固定结构格式,从而可以生成一个同质化的语法树。
①、操作码
对于op类定义,它们主要位于op.h文件中,下面摘录一下基本定义
    OP*        op_next;        \
    OP*        op_sibling;        \
    OP*        (CPERLscope(*op_ppaddr))(pTHX);        \
    MADPROP_IN_BASEOP            \
    PADOFFSET    op_targ;        \
    PERL_BITFIELD16 op_type:9;        \
    PERL_BITFIELD16 op_opt:1;        \
    PERL_BITFIELD16 op_latefree:1;    \
    PERL_BITFIELD16 op_latefreed:1;    \
    PERL_BITFIELD16 op_attached:1;    \
    PERL_BITFIELD16 op_spare:3;        \
    U8        op_flags;        \
    U8        op_private;

struct op {
    BASEOP
};

struct unop {
    BASEOP
    OP *    op_first;
};
所有的扩展类型的最开始都是一个struct op结构,其中我们通过op_type来知道这个操作的操作码类型,而其它的一些常用结构则可以存放在of_flags成员中。
②、操作数
这个通常来说可以分为各种类型,和op类一样,它也有一个基础类,由于它可能比较重要,所以我们这里还是不厌其烦的将所有内容摘录一下,定义位于sv.h文件中
/* start with 2 sv-head building blocks */
#define _SV_HEAD(ptrtype) \
    ptrtype    sv_any;        /* pointer to body */    \
    U32        sv_refcnt;    /* how many references to us */    \
    U32        sv_flags    /* what we are */

#define _SV_HEAD_UNION \
    union {                \
    IV      svu_iv;            \
    UV      svu_uv;            \
    SV*     svu_rv;        /* pointer to another SV */        \
    char*   svu_pv;        /* pointer to malloced string */    \
    SV**    svu_array;        \
    HE**    svu_hash;        \
    GP*    svu_gp;            \
    }    sv_u


struct STRUCT_SV {        /* struct sv { */
    _SV_HEAD(void*);
    _SV_HEAD_UNION;
#ifdef DEBUG_LEAKING_SCALARS
    PERL_BITFIELD32 sv_debug_optype:9;    /* the type of OP that allocated us */
    PERL_BITFIELD32 sv_debug_inpad:1;    /* was allocated in a pad for an OP */
    PERL_BITFIELD32 sv_debug_cloned:1;    /* was cloned for an ithread */
    PERL_BITFIELD32 sv_debug_line:16;    /* the line where we were allocated */
    U32        sv_debug_serial;    /* serial number of sv allocation   */
    char *    sv_debug_file;        /* the file where we were allocated */
#endif
};

struct gv {
    _SV_HEAD(XPVGV*);        /* pointer to xpvgv body */
    _SV_HEAD_UNION;
};
对于不同的具体操作数,它指向的具体结构也各不相同,例如,gv指向的为一个XPVGV类型结构。在sv.h中定义的svtype说明了各种可能的操作数类型,这里有一个特征,那就是在结构中出现的类型,后出现的类型的结构(真正占用的磁盘空间)要多于之前出现的结构,这一点要注意,因为很多结构的转换需要考虑这个问题,例如Perl_sv_upgrade函数中。
这里和内核中的sock和socket结构实现类似,那就是的确是必须的结构放入基础类,然后不定长的内容均通过指针来指向,这样可以做到对内存的尽可能少的使用。这里同样需要识别操作数的类型,但是和C语言中的类型实现不同,同一个变量可能表现为不同的类型,所以里面定义了union结构,从而在不同的场景下定义不同的变量。这里类型识别没有像op一样有专门的type,而是复用了sv_flags中的低8bits作为了sv结构的表示,代码如下
#define SVTYPEMASK    0xff
#define SvTYPE(sv)    ((svtype)((sv)->sv_flags & SVTYPEMASK))
常见的代码中变量缩写说明
sv  scarlar  value  hv hash value  Iv integer U unsigned  N double C Compound(Code) P point(string)av  arrray
2、变量命名规则
其中Perl的全局函数均以Perl_开头,所有的全局数据均以PL_开头,所有的静态变量和结构均已S_开头,这些本身没有什么意义,只是为了代码的维护以及是避免命名空间干扰。当然由于跨平台和移植性的问题,这里的变量可能有不同的形式,但是它们是作为全局变量的意义是存在的,也就是说PL_引导的变量时可以在函数间共享的。
3、语法定义
由于perl的语法比较复杂,所以它毫无意外的使用了yacc文件来描述自己的语法。顺便说一下没有使用yacc文件的两种语言,一种是gnu make,那是因为make的语法相对比较简单,所以可以手动解析,因为它的语法就是变量定义加上一些<目标、依赖、规则>三元组;另一个是gcc的高版本实现,它同样没有使用语法文件,而是自己手动解析语法。在该文件中,可以看到最为精确的perl语法文件的定义,可以识别perl的各种内置函数,以及各种微妙特征的实现基础,例如perl中"=="操作和"eq"操纵的区别。
4、词法文件
词法分析位于toke.c文件中,这里面的主函数Perl_yylex,它进行词法分析。同样毫无例外的是,它实现了自己的词法分析文件,而没有使用通用的lex工具,因为复杂语言的很多特征都需要在词法分析阶段进行特殊调整和处理,例如perl中对于变量符号$,数组符号@,hash符号%的解析都比较特殊,gcc的typedef处理等,关于perl的这个处理,之后可能会详细说明分析。
例如,对于我们最为关心的关键字识别,就是位于toke.c中的Perl_keyword函数中,大家可以看到,perl事实上内置的大量的函数,所以在编写高亮下的perl程序时,你可以看到大量高亮的单词,那就是因为perl的内置函数(所以也就是关键字)非常多的原因。
5、脚本执行
和前面对应,脚本的执行需要使用语法树,而解析器的本质工作就是通过对语法树的遍历来完成代码的有序执行。由于所有的操作都派生自op结构,所以可以以这个可以依赖的op结构为基础来遍历并执行整个语法树。在之前op的定义中可以看到,每个op结构的开始还有一个op_next结构,用来指明自己执行结束之后执行的下一条指令。然后的重要一点就是操作数,由于perl已经涉及到了词法域的概念,例如my定义变量只有在所在词法域中生效,所以perl内部会维护一些词法解析堆栈以及局部变量堆栈结构,这些同样是通过全局变量来描述。
6、单条指令执行
每条指令(不同的op_type)对应不同的一个实现函数,这些实现函数定义于两个文件中,一个是pp.h,另一个是pp_hot.h,区别在于第二个文件中的函数被执行的概率更大一些,其中pp是push/pop的缩写。在这些函数的开始可能会看到一些dVAR; dSP; 之类的宏,它们的主要功能就是定义一些局部变量,并初始化为特定值(例如SP初始化为堆栈的栈顶),然后可以方便的在之后使用这些局部变量,其中的d是define的缩写。这里大家只要知道是局部变量定义就可以,而这些函数执行需要的参数则可以通过全局的PL_stack获得。
7、变量动态转换实现
在不同的场合需要取变量的不同值,如果说当前值不是该类型,那么就需要进行转换。具体的转换函数同样位于sv.c文件中,其中大量的Perl_XV_2YV之类的函数就是用于完成该功能。当然是为了方便,更上层通常通过不带Perl_前缀的函数来引用这些变量,例如sv_2nv等。
三、调试方法
1、文件编译
在文件编译的时候要禁止优化选项,通过执行
./Configure -d -D optimize=-g
来生成Makefile,从能使能调试功能。perl的编译和通常的开源模式不同,它必须在perl工程根目录下执行Configure,而不能建立单独的obj文件夹进行编译,否则编译不通过,我刚开始编译的时候就出现过这种问题。
2、运行时调试
当使能了调试功能,及定义了DEBUGGING宏,那么可以通过perl的-D选项来输出perl的执行过程的某些行为,例如可以通过-Dc让perl显示执行的类型转换信息,可以通过-DT来显示执行信息等。对于看源代码的同学,可以看到源代码中DEBUG_Xv DEBUG_Uv之类的语句,如果希望是能这些功能,那么就可以通过-DXX来使能这个功能,其中XX是DEBUG_XX中的XX字符串。例如Perl_yylex函数中
        DEBUG_T({ PerlIO_printf(Perl_debug_log,
              "### Saw case modifier\n"); });
就可以通过-DT来打印这个内容。
  评论这张
 
阅读(937)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017