Apache HTTP Server与PHP7
目录
一些学习记录
Apache httpd
项目架构

运行过程
- 解析配置文件:解析httpd.conf
- LoadModule加载模块,impl by mod_so
- DocumentRoot设置Web服务可见的根目录,impl by core
- SetEnv设置环境变量,impl by mod_env
- Containers多行的一个指令,例如<VirtualHost>
- 加载动态模块:根据LoadModule , AddType
- 系统资源初始化:初始化日志文件,共享内存段,数据库连接
- 进入对应mpm模式开始循环处理请求
MPM
MPM,Multi -Processing Modules,多重处理模块,是Apache httpd的核心组件,Apache通过MPM来使用操作系统的资源,对进程和线程池进行管理。
linux平台有3种模式:prefork,worker,event
prefork
每个子进程只有一个线程,不用担心线程安全问题

worker
与prefork相似,但是每个子进程可以有多个线程,适合高并发场景
event
有管理器,可以管理keep-alive的请求,减少阻塞

源码结构

- server:核心功能,例如请求处理、协议处理等,且main.c文件中的main()是Apache启动时的入口
- modules:模块
- include:头文件
- srclib:基础库,如apr
- os:不同操作系统所需
API
- ap_*.h:底层接口
- util_*.h:较低层接口
- http_request.h
- util_ldap.h
- apr_*.h:APR接口
- http_*.h:application develop比较感兴趣的接口(原文翻译)
- http_core.h
- http_main.h
- http_request.h
- http_log.h
- httpd.h
- 等等
- mod_*.h:模块自定义的接口
数据结构
- APR pools (apr_pool_t)
- Configuration records(ap_conf_vector_t)
- request_rec:定义在httpd.h,存储各种各样关于request的信息
- server_rec:存储关于服务器的信息,httpd启动阶段每个虚拟主机在内存中生成一个自己的server_rec,直到httpd进程结束才会被回收
- conn_rec:接受一个TCP连接时产生,TCP连接结束时回收,TCP级别的filter可能会用到
- process_rec
模块
包含的内容
- 描述模块本身的数据结构
- hook
- 模块配置数据结构:针对各个目录及各个服务器的配置信息
- 指令表:当前模块能够处理的指令及相应的处理程序
- 可选函数
- 过滤器相关处理
指令表
例如mod_so.c,就定义了LoadModule和LoadFile指令

hook
三种hook:
- hook-handlers
- filters,比较简单的例如mod_expires.c里面的expires_filter
- 使用ap_register_output_filter 或 ap_register_input_filter
- 根据修改的内容类型,分为AP_FTYPE_RESOURCE, AP_FTYPE_CONTENT_SET, AP_FTYPE_PROTOCOL, AP_FTYPE_TRANSCODE, AP_FTYPE_CONNECTION, AP_FTYPE_NETWORK
- 有个example
- 其他function
hook运行的先后顺序
HOOK_REALLY_FIRST,HOOK_FIRST, APR_HOOK_MIDDLE ,HOOK_LAST ,HOOK_REALLY_LAST
helloworld
sudo apt-get install apache2-dev
#include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "ap_config.h" #include "string.h" static int printitem(void *rec, const char *key, const char *value) { //打印table的一行 request_rec *r = rec; ap_rprintf(r, "<tr><th scope=\"row\">%s</th><td>%s</td></tr>\n", ap_escape_html(r->pool, key), ap_escape_html(r->pool, value)); return 1; } static void printtable(request_rec *r, apr_table_t *t, const char *caption, const char *keyhead, const char *valhead) { //打印table开始部分 ap_rprintf(r, "<table><caption>%s</caption><thead>" "<tr><th scope=\"col\">%s</th><th scope=\"col\">%s" "</th></tr></thead><tbody>", caption, keyhead, valhead); //循环打印apr_table_t内的每个char* apr_table_do(printitem, r, t, NULL); //打印table结尾部分 ap_rputs("</tbody></table>\n", r); } static int helloworld_handler(request_rec *r) { if (strcmp(r->handler, "helloworld")) { return DECLINED; } r->content_type = "text/html"; if (!r->header_only){ ap_rputs("CCdragon<br/>Args:", r); ap_rputs(r->args,r); ap_rputs("<br/>Host:", r); ap_rputs(apr_table_get(r->headers_in, "Host"),r); ap_rputs("<br/><br/><br/><br/>", r); printtable(r, r->headers_in, "Request Headers", "Header", "Value"); } return OK; } static void helloworld_register_hooks(apr_pool_t *p) { ap_hook_handler(helloworld_handler, NULL, NULL, APR_HOOK_MIDDLE); } /* Dispatch list for API hooks */ module AP_MODULE_DECLARE_DATA helloworld_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-dir config structures */ NULL, /* merge per-dir config structures */ NULL, /* create per-server config structures */ NULL, /* merge per-server config structures */ NULL, /* table of config file commands */ helloworld_register_hooks /* 注册hooks */ };

apxs -c mod_helloworld.c apxs -i mod_helloworld.la apachectl restart

上面这种module是最简单的stdio风格的module,平时一般更多使用bucket brigade这种数据结构来处理数据
mod_security
找了mod_security看一下,注意下载v2版本,看到注册hooks里面的关键在于

在hook_request_early和hook_request_late中,调用modsecurity_process_phase,其中定义了5个阶段,这两个函数分别执行第1和第2个阶段

然后会在msre_ruleset_process_phase中进行正则的匹配

最终执行pcre_exec进行匹配

另外,处理response的两个阶段在一个output_filter中执行,具体的就不再展开

PHP7
架构

- zendvm层,编译执行php代码
- php层 即php代码层
- sapi层,最外层,提供php与外部服务的交互
生命周期

让我们康康php-fpm
入口为php-fpm.c,我们可以看到启动有不同的模式,用switch case方式选择,例如case “m”会列出所有模块然后 goto out,如果没有goto out 就break,继续运行

如上图,cgi_sapi_module.startup(&cgi_sapi_module);这一步将会调用php_module_startup,在这个阶段,主要进行的工作为
- sapi,php_output,gc,zend引擎的初始化(包括注册zend扩展)
- 注册各种常量,如PHP_VERSION等
- 注册php ini和zend ini配置
- 注册_GET,_POST等全局变量
- 注册php静态扩展与内部扩展
- 根据php.ini注册php动态扩展
- 初始化php扩展
然后下一行进入php_request_startup阶段,主要初始化一些全局符号表,激活php_output和zend引擎

然后再进行下一个阶段:php_execute_script


最后进行的是php_request_shutdown和php_module_shutdown,主要销毁各种各样的变量,执行一些hook,关闭gc等等
数据类型
变量
php7的zval_struct
struct _zval_struct { zend_value value; /* value */ union { struct { ZEND_ENDIAN_LOHI_4( //这个宏解决字节序问题 zend_uchar type, //变量类型 zend_uchar type_flags, //类型掩码 zend_uchar const_flags, zend_uchar reserved) /* call info for EX(This) */ } v; uint32_t type_info; } u1; union { uint32_t next; /* hash collision chain */ uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ uint32_t access_flags; /* class constant access flags */ uint32_t property_guard; /* single property guard */ uint32_t extra; /* not further specified */ } u2; //辅助信息,也用来对齐 };
zend_value的结构
typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value;
类型(记一下,调类型混淆的时候可以看一眼)
/* regular data types */ #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 /* constant expressions */ #define IS_CONSTANT 11 #define IS_CONSTANT_AST 12 /* fake types */ #define _IS_BOOL 13 #define IS_CALLABLE 14 #define IS_ITERABLE 19 #define IS_VOID 18 /* internal types */ #define IS_INDIRECT 15 #define IS_PTR 17 #define _IS_ERROR 20
数组
通过哈希表存储
struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar consistency) } v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; //存储元素的数组,每个元素为一个bucket uint32_t nNumUsed;//已使用 uint32_t nNumOfElements;//数组实际元素个数 uint32_t nTableSize;//总容量 uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor; }; typedef struct _Bucket { zval val; zend_ulong h; /* hash value (or numeric index) */ zend_string *key; /* string key or NULL for numerics */ } Bucket;
在实现的过程中还加入了一个中间映射表(为了实现数组的有序性)

如果发生了冲突,则把中间映射表更新为新的Bucket的位置,然后新的Bucket的val中u2->next指向旧的Bucket
引用
struct _zend_reference { zend_refcounted_h gc; zval val; };
例子:
$a=date("Y-m-d");//string $b=&$a;

引用之后会把被引用元素的value修改为zend_reference,然后$a和$b一起指向这个zend_reference,zend_reference内嵌的zval为原来$a的zval
类
struct _zend_class_entry { char type; //类型,内部类和用户自定义类 zend_string *name; //类名 struct _zend_class_entry *parent; //父类 int refcount; uint32_t ce_flags; //掩码,标志普通类,抽象类,接口等 int default_properties_count; //普通属性数量 int default_static_members_count; //静态属性数量 zval *default_properties_table;//普通属性数组 zval *default_static_members_table;//静态属性数组 zval *static_members_table; HashTable function_table; //成员方法符号表 HashTable properties_info; HashTable constants_table; //各种魔术方法的指针 union _zend_function *constructor; union _zend_function *destructor; union _zend_function *clone; union _zend_function *__get; union _zend_function *__set; union _zend_function *__unset; union _zend_function *__isset; union _zend_function *__call; union _zend_function *__callstatic; union _zend_function *__tostring; union _zend_function *__debugInfo; union _zend_function *serialize_func; union _zend_function *unserialize_func; zend_class_iterator_funcs iterator_funcs; //一些钩子函数 zend_object* (*create_object)(zend_class_entry *class_type); zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref); int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */ union _zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method); //序列化和反序列化调用的函数 int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data); int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data); //实现的接口和trait及其数量 uint32_t num_interfaces; uint32_t num_traits; zend_class_entry **interfaces; zend_class_entry **traits; zend_trait_alias **trait_aliases; zend_trait_precedence **trait_precedences; union { struct { zend_string *filename; uint32_t line_start; uint32_t line_end; zend_string *doc_comment; } user; struct { const struct _zend_function_entry *builtin_functions; struct _zend_module_entry *module; } internal; } info; };
自定义的类中各种变量函数都被解析到对应的元素之下,最后和函数一样注册到全局符号表EG(class_table中)
编译过程
对象
struct _zend_object { zend_refcounted_h gc; uint32_t handle; //对象的编号 zend_class_entry *ce; //对象所属的类 const zend_object_handlers *handlers; HashTable *properties; //成员属性哈希表 zval properties_table[1]; //非静态成员属性数组 };
函数
用户自定义函数
需要进行编译,函数通过zend_functions表示
内部函数
定义使用PHP_FUNCTION()或者ZEND_FUNCTION()宏
函数的注册:扩展为每个函数生成一个zend_function_entry结构,然后把自己定义的所有函数的结构数组提供给zend_module_entry->functions即可
内存管理
垃圾回收
- 每次refcount减少时,垃圾回收器会把这个object或者array当作可能的垃圾,放入垃圾回收buffer
- 等到buffer中的对象达到一定数量时,遍历垃圾回收器里所有的对象并设置为灰色,然后对每个对象进行深度优先遍历,把他所有的成员标记成灰色,每次标记成灰色时,refcount-1
- 重复上一步的遍历buffer,检查refcount是否为0,如果为0说明只有内部成员对其的引用,确实是垃圾,标记为白色,refcount不等于0的标记为黑色,并把减去的refcount+1
- 再次遍历,把不是白色的对象删除,然后释放buffer里所有的对象
写时复制
要修改对象的 时候才复制出一个新的对象进行修改
内存池
三种分配内存的粒度
- chunk
- page
- slot
zend_mm_heap负责存储内存池的主要信息
内存的申请:
- Huge,将分配n个对齐的chunk
- Large,分配n个连续的page,此时要在chunk里查找page,会优先填满内存之间的空隙(比如要2个page会优先找刚好2两个连续空闲的page而不是3个连续空闲的page)
- Small,类似large的分配方式
zendVM的一些基本数据结构
opline
opline为指令,有指令和opcode操作码
struct _zend_op { const void *handler; //实际处理的函数 znode_op op1;//操作数1 znode_op op2;//操作数2 znode_op result;//返回值 uint32_t extended_value; uint32_t lineno; zend_uchar opcode;//opcode指令(操作码) zend_uchar op1_type;//操作数1类型 zend_uchar op2_type;//2类型 zend_uchar result_type;//返回类型 };
zend_op_array
struct _zend_op_array { /* Common elements */ zend_uchar type; zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */ uint32_t fn_flags; zend_string *function_name; zend_class_entry *scope; zend_function *prototype; uint32_t num_args; uint32_t required_num_args; zend_arg_info *arg_info; /* END of common elements */ uint32_t *refcount; uint32_t last; zend_op *opcodes; //指令集合 int last_var; uint32_t T; zend_string **vars; //保存所有的CV类型变量名 int last_live_range; int last_try_catch; zend_live_range *live_range; zend_try_catch_element *try_catch_array; /* static variables support */ HashTable *static_variables; zend_string *filename; uint32_t line_start; uint32_t line_end; zend_string *doc_comment; uint32_t early_binding; /* the linked list of delayed declarations */ int last_literal; zval *literals; //存储字面量 int cache_size; void **run_time_cache; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; };
保存了程序的所有opline指令,还有一些运行时状态
zend_execute_data
C语言执行时局部变量和上下文信息都由栈保存,执行新函数时首先将eip入栈保存,然后移动esp,ebp分配新的栈
zendVM也实现了类似c语言的功能
struct _zend_execute_data { const zend_op *opline; /* 相当于eip,当前执行的指令 */ zend_execute_data *call; /* current call */ zval *return_value; //执行完以后把返回值填到这个地址 zend_function *func; /* executed function */ zval This; /* this + call_info + num_args */ zend_execute_data *prev_execute_data;//新函数调用或者include的时候,用来保存当前的execute_data,新函数执行完以后用这个回到原来的位置 zend_array *symbol_table; //全局符号变量表 #if ZEND_EX_USE_RUN_TIME_CACHE void **run_time_cache; /* cache op_array->run_time_cache */ #endif #if ZEND_EX_USE_LITERALS zval *literals; /* cache op_array->literals */ #endif };
另外,zend_execute_data的末尾保存了临时变量,如下图

zend_executor_globals
全局符号表
保存了已经编译的class,function的hash表,常量符号表,运行栈内存池,php.ini配置项,全局变量($_GET等),已经include的脚本等等
EG()宏就是在这里取东西
编译
之后有用到的时候再详细看8
re2c
使用一定的规则将php代码切分成一个一个的token
yacc & bison
yacc是语法分析器,bison是一个语法分析器的生成器
扩展
先创建一个框架
./ext_skel --extname=helloworld

新建一个函数和一个类
zend_class_entry *myclass_ce; static zend_function_entry myclass_method[] = { { NULL, NULL, NULL } }; ZEND_MINIT_FUNCTION(hackphp) { zend_class_entry ce; //"myclass"是这个类的名称。 INIT_CLASS_ENTRY(ce, "myclass",myclass_method); myclass_ce = zend_register_internal_class(&ce TSRMLS_CC); return SUCCESS; } /* {{{ void hackphp_test1() */ PHP_FUNCTION(hackphp_test1) { ZEND_PARSE_PARAMETERS_NONE(); php_printf("The extension %s is loaded and working!\r\n", "hackphp"); }
万万注意要在module_entry里面加上ZEND(PHP)_MINIT方法,才能调用ZEND(PHP)_MINIT_FUNCTION

phpize
./configure 可以选择加不加php-config路径
然后php.ini里面写上extension=xxxxx

Apache http加载php的方式
mod_php
https://github.com/php/php-src/blob/master/sapi/apache2handler/mod_php.c
将php解释器作为apache的一个module来运行
非常方便,起一个能解析php服务器只需要2步
apt-get install apache2 apt-get install php # (会安装一坨东西,其中包括libapache2-mod-php7.2)
然后/etc/apache2/mods-available 里面会自动多出来php (ubuntu)

此时phpinfo会显示sapi为apache2.0 handler,为apache默认运行php的模式

mod_cgi
CGI,通用网关接口,Web服务器(例如Apache)通过这样的接口来和其他程序(例如PHP解释器)通讯.
每次遇到一个动态请求,Web服务器都要去fork一个新进程(例如PHP解释器)来运行,效率很低(每次都要重新解析php.ini文件,初始化执行环境),于是诞生了FastCGI
mod_fcgid
Fastcgi会先启动一个master进程,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求
worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。