Apache HTTP Server与PHP7

一些学习记录

Apache httpd

项目架构

运行过程

  1. 解析配置文件:解析httpd.conf
    • LoadModule加载模块,impl by mod_so
    • DocumentRoot设置Web服务可见的根目录,impl by core
    • SetEnv设置环境变量,impl by mod_env
    • Containers多行的一个指令,例如<VirtualHost>
  2. 加载动态模块:根据LoadModule , AddType
  3. 系统资源初始化:初始化日志文件,共享内存段,数据库连接
  4. 进入对应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即可

内存管理

垃圾回收

  1. 每次refcount减少时,垃圾回收器会把这个object或者array当作可能的垃圾,放入垃圾回收buffer
  2. 等到buffer中的对象达到一定数量时,遍历垃圾回收器里所有的对象并设置为灰色,然后对每个对象进行深度优先遍历,把他所有的成员标记成灰色,每次标记成灰色时,refcount-1
  3. 重复上一步的遍历buffer,检查refcount是否为0,如果为0说明只有内部成员对其的引用,确实是垃圾,标记为白色,refcount不等于0的标记为黑色,并把减去的refcount+1
  4. 再次遍历,把不是白色的对象删除,然后释放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太多时,也会停掉一些,这样就提高了性能,也节约了资源。


http://cdn.octo-dev.co.uk/publications/The%20Apache%20Modules%20Book%20-%20Application%20Development%20with%20Apache.pdf

发表评论

邮箱地址不会被公开。 必填项已用*标注