[置顶] php源码之路第一章(环境配置及常用代码)

从今天开始正式学习php的内核源码,没有想象的那么简单,放平心态,慢慢来。

                               --------写给自己的话   
首先源码手册及下载的网址是:http://www.php-internals.com/

我们的一些准备工作和环境配置:

在开始学习PHP实现之前,我们需要一个实验和学习的环境。下面介绍一下怎样在*nix环境下准备和搭建PHP环境。

(*nix指的是类Unix环境,比如各种Linux发行版,FreeBSD, OpenSolaris, Mac OS X等操作系统)

首先我们需要获得php的源代码,下载源码首选是去PHP官方网站http://php.net/downloads.php下载。

在这里我推荐大家使用git或者svn(由于php已经完全迁移到git上面了,所以在svn上获取的镜像也停止了更新,不建议使用)等版本控制软件来操作,代码如下:
# git 官方地址
git clone https://git.php.net/repository/php-src.git
# 也可以访问github官方镜像
git clone git://github.com/php/php-src.git
cd php-src && git checkout origin PHP-5.3 # 签出5.3分支

# svn地址不变,不过不推荐从这里签出代码
cd ~
svn co http://svn.php.net/repository/php/php-src/branches/PHP_5_2 php-src-5.2 #5.2版本
svn co http://svn.php.net/repository/php/php-src/branches/PHP_5_3 php-src-5.3 #5.3版本
使用版本控制软件的好处就是能看到PHP每次修改的内容及日志信息,如果自己修改了其中的某些内容也能快速的查看到,如果你想修复PHP的某个Bug或者提交新功能的话,有版本控制也会容易的多。

然后我们开始准备编译环境

在*nix环境下,需要安装编译构建环境。如果你用的是Ubuntu或者是用apt做为包管理的系统,可以通过如下命令快速安装:
sudo apt-get install build-essential
如果你使用的是Mac OS X,则需要安装Xcode。Xcode可以在Mac OS X的安装盘中找到,如果你有Apple ID的话,也可以登陆苹果开发者网站http://developer.apple.com/下载。


如果你不愿意下载庞大的Xcode,也可以去https://github.com/kennethreitz/osx-gcc-installer下载安装包, 只安装所需的命令行工具。

然后我们进行编译:

cd ~/php-src
./buildconf
执行完以后就可以开始configure了,configure有很多的参数,比如指定安装目录,是否开启相关模块等选项:

有的系统自带的autoconf程序版本会有Bug,可能导致扩展的配置无法更新,如果在执行./buildconf时 报错,可以根据出错信息安装合适版本的autoconf工具。
./configure --help # 查看可用参数
为了尽快得到可以测试的环境,我们仅编译一个最精简的PHP。通过执行 ./configure --disable-all来进行配置。以后如果需要其他功能可以重新编译。如果configure命令出现错误,可能是缺少PHP所依赖的库,各个系统的环境可能不一样。出现错误可根据出错信息上网搜索。 直到完成configure。configure完成后我们就可以开始编译了。
./configure --disable-all
make
在*nix下编译过程序的读者应该都熟悉经典的configure make,make install吧。执行make之后是否需要make install就取决于你了。如果install的话最好在configure的时候是用prefix参数指定安装目录, 不建议安装到系统目录, 避免和系统原有的PHP版本冲突。在make 完以后,在sapi/cli目录里就已经有了php的可以执行文件. 执行一下命令:
./sapi/cli/php -v
-v参数表示输出版本号,如果命令执行完后看到输出php版本信息则说明编译成功。如果是make install的话可以执行$prefix/bin/php这个路径的php。当然如果是安装在系统目录或者你的prefix目录在$PATH环境变量里的话,直接执行php就行了。

在只进行make而不make install时,只是编译为可执行二进制文件,所以在终端下执行的php-cli所在路径就是php-src/sapi/cli/php。

后续的学习中可能会需要重复configure make 或者 make && make install 这几个步骤。

Windows环境的编译

Windows环境下的编译可以参考官方Wiki的说明:https://wiki.php.net/internals/windows/stepbystepbuild

源码的目录结构:

讲一下各个目录都放的什么文件:

 1.  / 这个目录包含的东西比较多,主要包含一些说明文件以及设计方案。 其实项目中的这些README文件是非常值得阅读的例如: 
1)/README.PHP4-TO-PHP5-THIN-CHANGES 这个文件就详细列举了PHP4和PHP5的一些差异。 
2)还有有一个比较重要的文件/CODING_STANDARDS,如果要想写PHP扩展的话,这个文件一定要阅读一下,不管你个人的代码风格是什么样,怎么样使用缩进和花括号,既然来到了这样一个团体里就应该去适应这样的规范,这样在阅读代码或者别人阅读你的代码是都会更轻松。
2. build 顾名思义,这里主要放置一些和源码编译相关的一些文件,比如开始构建之前的buildconf脚本等文件,还有一些检查环境的脚本等。
3. ext 官方扩展目录,包括了绝大多数PHP的函数的定义和实现,如array系列,pdo系列,spl系列等函数的实现,都在这个目录中。个人写的扩展在测试时也可以放到这个目录,方便测试和调试。 
4. main 这里存放的就是PHP最为核心的文件了,主要实现PHP的基本设施,这里和Zend引擎不一样,Zend引擎主要实现语言最核心的语言运行环境。
5. Zend Zend引擎的实现目录,比如脚本的词法语法解析,opcode的执行以及扩展机制的实现等等。 
6. pear “PHP 扩展与应用仓库”,包含PEAR的核心文件。 
7. sapi 包含了各种服务器抽象层的代码,例如apache的mod_php,cgi,fastcgi以及fpm等等接口。
8. TSRM PHP的线程安全是构建在TSRM库之上的,PHP实现中常见的*G宏通常是对TSRM的封装,TSRM(Thread Safe Resource Manager)线程安全资源管理器。
9. tests PHP的测试脚本集合,包含PHP各项功能的测试文件 
10.win32 这个目录主要包括Windows平台相关的一些实现,比如sokcet的实现在Windows下和*Nix平台就不太一样,同时也包括了Windows下编译PHP相关的脚本。 

PHP源码阅读工具

使用VIM + Ctags
    通常在Linux或其他*Nix环境我们都使用VIM作为代码编辑工具,在纯命令终端下,它几乎是无可替代的。它具有非常强大的扩展机制,在文字编辑方面基本上无所不能。不过Emacs用户请不要激动,笔者还没有真正使用Emacs,虽然我知道它甚至可以煮咖啡,还是等笔者有时间了或许会试试煮杯咖啡边喝边写。

    推荐在Linux下编写代码的读者或多或少的试一试ctags。 ctags支持非常多的语言,可以将源代码中的各种符号(如:函数、宏类等信息)抽取出来做上标记并保存到一个文件中,供其他文本编辑工具(VIM,EMACS等)进行检索。它保存的文件格式符合UNIX的哲学(小即是美),使用也比较简洁:
#在PHP源码目录(假定为/server/php-src)执行:
$ cd /server/php-src
$ ctags -R

#小技巧:在当前目录生成的tags文件中使用的是相对路径,
#若改用 ctags -R /server/ ,可以生成包含完整路径的ctags,就可以随意放到任意文件夹中了。 

#在~/.vimrc中添加:
set tags+=/server/php-src/tags
#或者在vim中运行命令:
:set tags+=/server/php-src/tags
上面代码会在/sever/php-src目录下生成一个名为tags的文件,这个文件的格式如下:
{tagname}<Tab>{tagfile}<Tab>{tagaddress}

EG  Zend/zend_globals_macros.h  /^# define EG(/;" d
它的每行是上面的这样一个格式,第一列是符号名(如上例的EG宏),第二列是该符号的文件位置以及这个符号所在的位置。 VIM可以读取tags文件,当我们在符号上(可以是变量名之类)使用CTRL+]时VIM将尝试从tags文件中检索这个符号。如果找到则根据该符号所在的文件以及该符号的位置打开该文件,并将光标定位到符号定义所在的位置。 这样我们就能快速的寻找到符号的定义。

使用 Ctrl+] 就可以自动跳转至定义,Ctrl+t 可以返回上一次查看位置。这样就可以快速的在代码之间“游动”了。

习惯这种浏览代码的方式之后,大家会感觉很方便的。不过若你不习惯使用VIM这类编辑器,也可以看看下面介绍的IDE。

如果你使用的Mac OS X,运行ctags程序可能会出错,因为Mac OS X自带的ctags程序有些问题, 所以需要自己下载安装ctags,笔者推荐使用homebrew来安装。 如果执行还是会出错,请执行下ctags -v 或着 which ctags确保你执行的是新安装的ctags。

使用IDE查看代码

如果不习惯使用VIM来看代码,也可以使用一些功能较丰富的IDE,比如Windows下可以使用Visual Studio 2010 Express。或者使用跨平台的Netbeans、Eclipse来查看代码,当然,这些工具都相对较重量级一些,不过这些工具不管是调试还是查看代码都相对较方便,

在Eclipse及Netbeans下查看符号定义的方式通常是将鼠标移到符号上,同时按住CTRL,然后单击,将会跳转到符号定义的位置。

而如果使用VS的话, 在win32目录下已经存在了可以直接打开的工程文件,如果由于版本原因无法打开,可以在此源码目录上新建一个基于现有文件的Win32 Console Application工程。

常用快捷键:

F12 转到定义
CTRL + F12转到声明

F3: 查找下一个
Shift+F3: 查找上一个

Ctrl+G: 转到指定行

CTRL + -向后定位
CTRL + SHIFT + -向前定位

常用的代码:

  1. “##”和”#”

    宏是C/C++是非常强大,使用也很多的一个功能,有时用来实现类似函数内联的效果,或者将复杂的代码进行简单封装,提高可读性或可移植性等。在PHP的宏定义中经常使用双井号。下面对"##"及"#"进行详细介绍。
    

    双井号(##)
    在C语言的宏中,”##”被称为 连接符(concatenator),它是一种预处理运算符,用来把两个语言符号(Token)组合成单个语言符号。这里的语言符号不一定是宏的变量。并且双井号不能作为第一个或最后一个元素存在。如下所示源码:

#define PHP_FUNCTION ZEND_FUNCTION
#define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define ZEND_FN(name) zif_##name
#define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS)
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, \
zval *this_ptr, int return_value_used TSRMLS_DC

PHP_FUNCTION(count);

// 预处理器处理以后, PHP_FUCNTION(count);就展开为如下代码
void zif_count(int ht, zval *return_value, zval **return_value_ptr,
        zval *this_ptr, int return_value_used TSRMLS_DC)
宏ZEND_FN(name)中有一个"##",它的作用一如之前所说,是一个连接符,将zif和宏的变量name的值连接起来。以这种连接的方式以基础,多次使用这种宏形式,可以将它当作一个代码生成器,这样可以在一定程度上减少代码密度,我们也可以将它理解为一种代码重用的手段,间接地减少不小心所造成的错误。

单井号(#)

"#"是一种预处理运算符,它的功能是将其后面的宏参数进行 字符串化操作 ,简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号,用比较官方的话说就是将语言符号(Token)转化为字符串。 例如:
#define STR(x) #x

int main(int argc char** argv)
{
    printf("%s\n", STR(It's a long string)); // 输出 It's a long string
    return 0;
}
如前文所说,It's a long string 是宏STR的参数,在展开后被包裹成一个字符串了。所以printf函数能直接输出这个字符串,当然这个使用场景并不是很适合,因为这种用法并没有实际的意义,实际中在宏中可能会包裹其他的逻辑,比如对字符串进行封装等等。
  1. 关于宏定义中的do-while循环
PHP源码中大量使用了宏操作,比如PHP5.3新增加的垃圾收集机制中的一段代码:

#define ALLOC_ZVAL(z) \
do {                                                \
    (z) = (zval*)emalloc(sizeof(zval_gc_info));     \
    GC_ZVAL_INIT(z);                                \
} while (0)
这段代码,在宏定义中使用了 do{ }while(0) 语句格式。如果我们搜索整个PHP的源码目录,会发现这样的语句还有很多。在其他使用C/C++编写的程序中也会有很多这种编写宏的代码,多行宏的这种格式已经是一种公认的编写方式了。为什么在宏定义时需要使用do-while语句呢? 我们知道do-while循环语句是先执行循环体再判断条件是否成立,所以说至少会执行一次。当使用do{ }while(0)时由于条件肯定为false,代码也肯定只执行一次,肯定只执行一次的代码为什么要放在do-while语句里呢? 这种方式适用于宏定义中存在多语句的情况。如下所示代码:
#define TEST(a, b) a++;b++;

if (expr)
    TEST(a, b);
else
    do_else();

代码进行预处理后,会变成:

if (expr)
    a++;b++;
else
    do_else();
这样if-else的结构就被破坏了if后面有两个语句,这样是无法编译通过的,那为什么非要do-while而不是简单的用{}括起来呢。这样也能保证if后面只有一个语句。例如上面的例子,在调用宏TEST的时候后面加了一个分号, 虽然这个分号可有可无,但是出于习惯我们一般都会写上。 那如果是把宏里的代码用{}括起来,加上最后的那个分号。 还是不能通过编译。所以一般的多表达式宏定义中都采用do-while(0)的方式。

了解了do-while循环在宏中的作用,再来看"空操作"的定义。由于PHP需要考虑到平台的移植性和不同的系统配置,所以需要在某些时候把一些宏的操作定义为空操作。例如在sapi\thttpd\thttpd.c文件中的VEC_FREE():
#ifdef SERIALIZE_HEADERS
    # define VEC_FREE() smart_str_free(&vec_str)
#else
    # define VEC_FREE() do {} while (0)
#endif
这里涉及到条件编译,在定义了SERIALIZE_HEADERS宏的时候将VEC_FREE()定义为如上的内容,而没有定义时,不需要做任何操作,所以后面的宏将VEC_FREE()定义为一个空操作,不做任何操作,通常这样来保证一致性,或者充分利用系统提供的功能。

有时也会使用如下的方式来定义“空操作”,这里的空操作和上面的还是不一样,例如很常见的Debug日志打印宏:
#ifdef DEBUG
# define LOG_MSG printf
#else
# define LOG_MSG(...)
#endif
在编译时如果定义了DEBUG则将LOG_MSG当做printf使用,而不需要调试,正式发布时则将LOG_MSG()宏定义为空,由于宏是在预编译阶段进行处理的,所以上面的宏相当于从代码中删除了。

上面提到了两种将宏定义为空的定义方式,看上去一样,实际上只要明白了宏都只是简单的代码替换就知道该如何选择了。
  1. #line 预处理
#line 838 "Zend/zend_language_scanner.c"
#line预处理用于改变当前的行号(__LINE__)和文件名(__FILE__)。 如上所示代码,将当前的行号改变为838,文件名Zend/zend_language_scanner.c 它的作用体现在编译器的编写中,我们知道编译器对C 源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名是固定的,不会被这些中间文件代替,有利于进行调试分析。

4.PHP中的全局变量宏

在PHP代码中经常能看到一些类似PG(), EG()之类的函数,他们都是PHP中定义的宏,这系列宏主要的作用是解决线程安全所写的全局变量包裹宏,如$PHP_SRC/main/php_globals.h文件中就包含了很多这类的宏。例如PG这个PHP的核心全局变量的宏。如下所示代码为其定义。
#ifdef ZTS // 编译时开启了线程安全则使用线程安全库
# define PG(v) TSRMG(core_globals_id, php_core_globals *, v)
extern PHPAPI int core_globals_id;
#else
# define PG(v) (core_globals.v) // 否则这其实就是一个普通的全局变量
extern ZEND_API struct _php_core_globals core_globals;
#endif
如上,ZTS是线程安全的标记,这个在以后的章节会详细介绍,这里就不再说明。下面简单说说,PHP运行时的一些全局参数,这个全局变量为如下的一个结构体,各字段的意义如字段后的注释:
struct _php_core_globals {
        zend_bool magic_quotes_gpc; // 是否对输入的GET/POST/Cookie数据使用自动字符串转义。
        zend_bool magic_quotes_runtime; //是否对运行时从外部资源产生的数据使用自动字符串转义
        zend_bool magic_quotes_sybase;  // 是否采用Sybase形式的自动字符串转义

        zend_bool safe_mode;    // 是否启用安全模式

        zend_bool allow_call_time_pass_reference;   //是否强迫在函数调用时按引用传递参数
        zend_bool implicit_flush;   //是否要求PHP输出层在每个输出块之后自动刷新数据

        long output_buffering;  //输出缓冲区大小(字节)

        char *safe_mode_include_dir;    //在安全模式下,该组目录和其子目录下的文件被包含时,将跳过UID/GID检查。
        zend_bool safe_mode_gid;    //在安全模式下,默认在访问文件时会做UID比较检查
        zend_bool sql_safe_mode;
        zend_bool enable_dl;    //是否允许使用dl()函数。dl()函数仅在将PHP作为apache模块安装时才有效。

        char *output_handler;   // 将所有脚本的输出重定向到一个输出处理函数。

        char *unserialize_callback_func;    // 如果解序列化处理器需要实例化一个未定义的类,这里指定的回调函数将以该未定义类的名字作为参数被unserialize()调用,
        long serialize_precision;   //将浮点型和双精度型数据序列化存储时的精度(有效位数)。

        char *safe_mode_exec_dir;   //在安全模式下,只有该目录下的可执行程序才允许被执行系统程序的函数执行。

        long memory_limit;  //一个脚本所能够申请到的最大内存字节数(可以使用K和M作为单位)。
        long max_input_time;    // 每个脚本解析输入数据(POST, GET, upload)的最大允许时间(秒)。

        zend_bool track_errors; //是否在变量$php_errormsg中保存最近一个错误或警告消息。
        zend_bool display_errors;   //是否将错误信息作为输出的一部分显示。
        zend_bool display_startup_errors;   //是否显示PHP启动时的错误。
        zend_bool log_errors;   // 是否在日志文件里记录错误,具体在哪里记录取决于error_log指令
        long      log_errors_max_len;   //设置错误日志中附加的与错误信息相关联的错误源的最大长度。
        zend_bool ignore_repeated_errors;   // 记录错误日志时是否忽略重复的错误信息。
        zend_bool ignore_repeated_source;   //是否在忽略重复的错误信息时忽略重复的错误源。
        zend_bool report_memleaks;  //是否报告内存泄漏。
        char *error_log;    //将错误日志记录到哪个文件中。

        char *doc_root; //PHP的”根目录”。
        char *user_dir; //告诉php在使用 /~username 打开脚本时到哪个目录下去找
        char *include_path; //指定一组目录用于require(), include(), fopen_with_path()函数寻找文件。
        char *open_basedir; // 将PHP允许操作的所有文件(包括文件自身)都限制在此组目录列表下。
        char *extension_dir;    //存放扩展库(模块)的目录,也就是PHP用来寻找动态扩展模块的目录。

        char *upload_tmp_dir;   // 文件上传时存放文件的临时目录
        long upload_max_filesize;   // 允许上传的文件的最大尺寸。

        char *error_append_string;  // 用于错误信息后输出的字符串
        char *error_prepend_string; //用于错误信息前输出的字符串

        char *auto_prepend_file;    //指定在主文件之前自动解析的文件名。
        char *auto_append_file; //指定在主文件之后自动解析的文件名。

        arg_separators arg_separator;   //PHP所产生的URL中用来分隔参数的分隔符。

        char *variables_order;  // PHP注册 Environment, GET, POST, Cookie, Server 变量的顺序。

        HashTable rfc1867_protected_variables;  // RFC1867保护的变量名,在main/rfc1867.c文件中有用到此变量

        short connection_status;    // 连接状态,有三个状态,正常,中断,超时
        short ignore_user_abort;    // 是否即使在用户中止请求后也坚持完成整个请求。

        unsigned char header_is_being_sent; // 是否头信息正在发送

        zend_llist tick_functions;  // 仅在main目录下的php_ticks.c文件中有用到,此处定义的函数在register_tick_function等函数中有用到。

        zval *http_globals[6];  // 存放GET、POST、SERVER等信息

        zend_bool expose_php;   // 是否展示php的信息

        zend_bool register_globals; // 是否将 E, G, P, C, S 变量注册为全局变量。
        zend_bool register_long_arrays; // 是否启用旧式的长式数组(HTTP_*_VARS)。
        zend_bool register_argc_argv;   // 是否声明$argv和$argc全局变量(包含用GET方法的信息)。
        zend_bool auto_globals_jit; // 是否仅在使用到$_SERVER和$_ENV变量时才创建(而不是在脚本一启动时就自动创建)。

        zend_bool y2k_compliance;   //是否强制打开2000年适应(可能在非Y2K适应的浏览器中导致问题)。

        char *docref_root;  // 如果打开了html_errors指令,PHP将会在出错信息上显示超连接,
        char *docref_ext;   //指定文件的扩展名(必须含有’.')。

        zend_bool html_errors;  //是否在出错信息中使用HTML标记。
        zend_bool xmlrpc_errors;   

        long xmlrpc_error_number;

        zend_bool activated_auto_globals[8];

        zend_bool modules_activated;    // 是否已经激活模块
        zend_bool file_uploads; //是否允许HTTP文件上传。
        zend_bool during_request_startup;   //是否在请求初始化过程中
        zend_bool allow_url_fopen;  //是否允许打开远程文件
        zend_bool always_populate_raw_post_data;    //是否总是生成$HTTP_RAW_POST_DATA变量(原始POST数据)。
        zend_bool report_zend_debug;    // 是否打开zend debug,仅在main/main.c文件中有使用。

        int last_error_type;    // 最后的错误类型
        char *last_error_message;   // 最后的错误信息
        char *last_error_file;  // 最后的错误文件
        int  last_error_lineno; // 最后的错误行

        char *disable_functions;    //该指令接受一个用逗号分隔的函数名列表,以禁用特定的函数。
        char *disable_classes;  //该指令接受一个用逗号分隔的类名列表,以禁用特定的类。
        zend_bool allow_url_include;    //是否允许include/require远程文件。
        zend_bool exit_on_timeout;  // 超时则退出
#ifdef PHP_WIN32
        zend_bool com_initialized;
#endif
        long max_input_nesting_level;   //最大的嵌套层数
        zend_bool in_user_include;  //是否在用户包含空间

        char *user_ini_filename;    // 用户的ini文件名
        long user_ini_cache_ttl;    // ini缓存过期限制

        char *request_order;    // 优先级比variables_order高,在request变量生成时用到,个人觉得是历史遗留问题

        zend_bool mail_x_header;    // 仅在ext/standard/mail.c文件中使用,
        char *mail_log;

        zend_bool in_error_log;
};
上面的字段很大一部分是与php.ini文件中的配置项对应的。 在PHP启动并读取php.ini文件时就会对这些字段进行赋值,而用户空间的ini_get()及ini_set()函数操作的一些配置也是对这个全局变量进行操作的。

在PHP代码的其他地方也存在很多类似的宏,这些宏和PG宏一样,都是为了将线程安全进行封装,同时通过约定的 G 命名来表明这是全局的,一般都是个缩写,因为这些全局变量在代码的各处都会使用到,这也算是减少了键盘输入。

如果你阅读过一些PHP扩展话应该也见过类似的宏,这也算是一种代码规范,在编写扩展时全局变量最好也使用这种方式命名和包裹,因为我们不能对用户的PHP编译条件做任何假设。

你可能感兴趣的:(源码,PHP,内核)