3.1.3 Session模块在PHP源码中的基本实现
在PHP中,Session是以一个扩展模块的形式而存在的,目前已加入PHP官方扩展之中。由3.1.1小节可知,与常用的PHP扩展模块一样,Session模块的实现也要经过MINIT->RINIT->RSHUETDOWN->MSHUTDOWN四个阶段。
1、MINIT
该过程会调用MINIT方法:PHP_MINIT_FUNCTION,它是一个定义在/main/php.h中的宏,与此类似的还有其他几个步骤的宏:
#define PHP_MINIT_FUNCTION ZEND_MODULE_STARTUP_D
#define PHP_MSHUTDOWN_FUNCTION ZEND_MODULE_SHUTDOWN_D
#define PHP_RINIT_FUNCTION ZEND_MODULE_ACTIVATE_D
#define PHP_RSHUTDOWN_FUNCTION ZEND_MODULE_DEACTIVATE_D
而ZEND_MODULE_STARTUP_D也是一个宏,定义在/Zend/zend_API.h中:
/* Declaration macros */
#define ZEND_MODULE_STARTUP_D(module) int ZEND_MODULE_STARTUP_N(module)(INIT_FUNC_ARGS)
#define ZEND_MODULE_SHUTDOWN_D(module) int ZEND_MODULE_SHUTDOWN_N(module)(SHUTDOWN_FUNC_ARGS)
#define ZEND_MODULE_ACTIVATE_D(module) int ZEND_MODULE_ACTIVATE_N(module)(INIT_FUNC_ARGS)
#define ZEND_MODULE_DEACTIVATE_D(module) int ZEND_MODULE_DEACTIVATE_N(module)(SHUTDOWN_FUNC_ARGS)
/* Name macros */
#define ZEND_MODULE_STARTUP_N(module) zm_startup_##module
#define ZEND_MODULE_SHUTDOWN_N(module) zm_shutdown_##module
#define ZEND_MODULE_ACTIVATE_N(module) zm_activate_##module
#define ZEND_MODULE_DEACTIVATE_N(module) zm_deactivate_##module
即PHP_MINIT_FUNCTION最终调用的是zm_startup_moudle方法。
下面来具体分析下session模块的PHP_MINIT_FUNCTION(即zm_start_module)方法的实现:
源码文件路径:/ext/session/session.c(详见:https://github.com/php/php-src/blob/master/ext/session/session.c)
主要做了以下几件事情:
1)注册$_SESSION变量:初始化$_SESSION变量的存储空间。
zend_register_auto_global(zend_string_init("_SESSION", sizeof("_SESSION") - 1, 1), 0, NULL);
其中,zend_register_auto_global属于zend api,位于/Zend/zend_compile.c文件中,其函数具体实现为:
里面调用了/Zend/zend_hash.h中的zend_hash_add_mem方法,将$_SESSION变量加入全局的HashTable中:
注:HashTable,即哈希表,它是PHP内核的核心数据结构,其中的数组、关联数组、对象属性、函数表、符号表等均是基于HashTable实现的。关于HashTable的详细知识在此不作展开,可详见:http://www.php-internals.com/book/?p=chapt03/03-01-01-hashtable
2)初始化模块信息:从php.ini配置文件中读取模块的初始化配置信息,然后完成模块初始化设置。
REGISTER_INI_ENTRIES()函数是定义在/Zend/zend_ini.h中的宏:
`#define REGISTER_INI_ENTRIES() zend_register_ini_entries(ini_entries, module_number)
...
ZEND_API int zend_register_ini_entries(const zend_ini_entry_def *ini_entry, int module_number);
`
其中zend_register_ini_entries函数定义在/Zend/zend_ini.c的内容可详见:http://www.cnblogs.com/driftcloudy/p/4011954.html
在前面的2.1节中介绍的相关配置项就定义在/ext/session/session.c
文件中的PHP_INI_BEGIN和PHP_INI_END宏之间:
由此可见,如果在php.ini配置文件中没有定义配置项的值,程序会自动有一个初始值。
3)注册SessionHandlerInterface接口与SessionHandler类:由3.1.2小节描述,这两个类是为方便开发者用户可以实现自定义的session机制,自PHP5.4引入。
2、RINIT
同理,该过程会调用RINIT方法:PHP_RINIT_FUNCTION
static PHP_RINIT_FUNCTION(session)
{
return php_rinit_session(PS(auto_start));
}
关于php_rinit_session()函数:
1)session模块结构体变量值的初始化
主要包括session id、状态、存储机制相关变量值的初始化:
/* Dispatched by RINIT and by php_session_destroy */
static inline void php_rinit_session_globals(void) /* {{{ */
{
/* Do NOT init PS(mod_user_names) here! */
/* TODO: These could be moved to MINIT and removed. These should be initialized by php_rshutdown_session_globals() always when execution is finished. */
PS(id) = NULL;
PS(session_status) = php_session_none;
PS(in_save_handler) = 0;
PS(set_handler) = 0;
PS(mod_data) = NULL;
PS(mod_user_is_open) = 0;
PS(define_sid) = 1;
PS(session_vars) = NULL;
ZVAL_UNDEF(&PS(http_session_vars));
}
/* }}} */
2)查找session.save_handler配置项的值:搜素php.ini配置文件中的session.save_handler配置项的值(默认files,也可以是用户自定义存储user - 如实现SaveHandler类的形式,或者其他存储方式如redis/memcache等 - 以扩展模块(redis.so/mysql.so)的形式),以确定seession的最终存储方案
PHPAPI ps_module *_php_find_ps_module(char *name) /* {{{ */
{
ps_module *ret = NULL;
ps_module **mod;
int i;
for (i = 0, mod = ps_modules; i < MAX_MODULES; i++, mod++) {
if (*mod && !strcasecmp(name, (*mod)->s_name)) {
ret = *mod;
break;
}
}
return ret;
}
/* }}} */
ps_modules为全局数组变量,里面每一个数组元素均是一个指向预定义用于处理session数据的模块的指针,该指针指向了定义处理session数据所需的一些需要实现的方法(open/read/write/close/gc等)
#define MAX_MODULES 32 // 支持的session存储方式的最大个数
static ps_module *ps_modules[MAX_MODULES + 1] = {// 初值
ps_files_ptr,// 文件存储
ps_user_ptr// 用户自定义存储
};
// 如果是以扩展模块形式(如redis/memcache)实现sessioni存储,则还需要在ps_modules变量中注册该模块
PHPAPI int php_session_register_module(ps_module *ptr)
{
int ret = FAILURE;
int i;
for (i = 0; i < MAX_MODULES; i++) {
if (!ps_modules[i]) {
ps_modules[i] = ptr;
ret = SUCCESS;
break;
}
}
return ret;
}
其中ps_files_ptr、ps_user_ptr分别定义在/ext/session/mod_files.h和/ext/session/mod_user.h中,它们都是ps_module结构体变量的指针,关于ps_module结构体变量( /ext/session/php_session.h),它定义了每一个处理session数据所需要的一组接口(以函数指针形式),即:如果你需要自定义session数据存储方式,则需要传递实现了以下功能的一组函数指针(函数名),如files或user。
typedef struct ps_module_struct {
const char *s_name;
// 下面每一个都是一个函数指针变量
int (*s_open)(PS_OPEN_ARGS);// 打开句柄
int (*s_close)(PS_CLOSE_ARGS);// 关闭句柄
int (*s_read)(PS_READ_ARGS);// 读session数据函数
int (*s_write)(PS_WRITE_ARGS);// 写入session数据函数
int (*s_destroy)(PS_DESTROY_ARGS);// 销毁
zend_long (*s_gc)(PS_GC_ARGS);// 垃圾回收
zend_string *(*s_create_sid)(PS_CREATE_SID_ARGS);// 创建session id
int (*s_validate_sid)(PS_VALIDATE_SID_ARGS);// 判断session id是否有效
int (*s_update_timestamp)(PS_UPDATE_TIMESTAMP_ARGS);// 更新session文件修改、访问时戳
} ps_module;
由上面的描述、分析可以看出,session数据的存储可以是任何方式,只需要实现ps_modules结构体中定义的函数指针接口即可。
3)php_session_start:启动session
启动session大致分为四步:
- 判断当前session状态
- 获取session id
- session id合法性检查
- 启动session机制的准备工作
3.1)判断当前session状态
session的状态有三种:
- 活跃状态:php_session_active,表示当前进程已经开启session
- 不可用状态:php_session_disabled,属于异常情况
- 初始状态或无效状态:php_session_none
在php7源码中使用一个枚举类型表示(/ext/session/php_session.h):
typedef enum {
php_session_disabled,
php_session_none,
php_session_active
} php_session_status;
其中php_session_disabled表示save_handler或serializer_handler模块不可用。不过是否真的不可用还需要对PS(mod)和PS(serializer)做进一步的检查。部分代码如下(php_session_start()函数中):
3.2)获取session id
有三种方法可以用于在客户端/服务器之间传递session id:
- session id存储在cookie中,从全局的符号表symbol_table中查询 _COOKIE符号通过session name作为key找到session id。
- session id存储在请求参数中,包括get和post方法,则依次从全局的符号表symbol_table中查询 _GET、_POST符号,只要任何一个地方找到session id,则不再继续往下查找
- session id作为REQUEST_URI的一部分,如:
http://yoursite/'session-name'='session-id'/script.php
,同理在全局的zend hashtable查找_SERVER(里面的REQUEST_URI符号)。
详细过程可见代码:
3.3)session id合法性检查
主要检查session的安全性与有效性。
- 安全性:防盗链检查,如果使用非cookie方式传递sesion id,则需要在HTTP_REFERER中包含session.referer_check设置的字符串。
- 有效性:如果session id包含某些字符串(\r\n\t <>'\"\),将视为非法。
3.4)启动session机制的准备工作 - php_session_initialize()
准备工作主要有:
1) 开启session handler - 首要工作
主要调用PS(mod)->s_open(&PS(mod_data), PS(save_path), PS(session_name)方法,该方法对应PS_OPEN_FUNC宏,它并不真正地去打开存储句柄(实际打开在read阶段),而是构建存储模块所需要的初始数据环境。如session采用文件存储时(session.save_handler=files),其主要是构造文件所需资源,如文件存储路径(允许多级子目录)的检查与创建,以及ps_files数据结构变量的设置。总之:为打开文件做好准备。(详见/ext/session/mod_files.c)。
/* Open session handler first */
if (PS(mod)->s_open(&PS(mod_data), PS(save_path), PS(session_name)) == FAILURE
/* || PS(mod_data) == NULL */ /* FIXME: open must set valid PS(mod_data) with success */
) {
php_session_abort();
php_error_docref(NULL, E_WARNING, "Failed to initialize storage module: %s (path: %s)", PS(mod)->s_name, PS(save_path));
return FAILURE;
}
2) session id的double check
如果session id不存在(如服务器第一次response或者3.3步骤中的检测到的非法session id会被置空),则重新生成:PS(id) = PS(mod)->s_create_sid(&PS(mod_data));,同理,如果是严格模式下(use_strict_mode=1)需验证session id有效性,即该ID是否由session模块自身创建:PS(mod)->s_validate_sid(&PS(mod_data), PS(id))。
3) 重置SID常量(中的session id)
SID常量保存了session的『name=ID』形式的字符,当且仅当session.use_trans_sid配置项的值为1(默认为零),否则为空。这也就是说如果session试过cookies来传输的话,SID常量应该重置为空;否则(use_trans_sid=1的话)SID将保存sesson name和ID(等同于session_id()方法返回的ID)。
4) 读取sessoin数据至内存变量中
主要调用PS(mod)->s_read(&PS(mod_data), PS(id), &val, PS(gc_maxlifetime))方法。
该方法会打开存储句柄并试图从存储机制上读取数据。
如session采用文件存储时(session.save_handler=files),它的PS_READ_FUNC方法首先会调用open方法(ps_files_open(data, ZSTR_VAL(key)),此方法会调用flock(data->fd, LOCK_EX)方法,即即使在读取session数据的时候也会设置排它锁,这也是文件存储机制的弊端:无法实现并发操作,只能串行)打开文件,然后在调用read方法读取数据。
5) GC:清楚过期的sessoin数据
GC操作必须发生在read操作之后,因为GC期间会将一些过期的session数据清除掉。
由上面的代码可以看到,session模块是按照一定的概率执行GC。该概率与配置项session.gc_probability与session.gc_divisor有关,并且需要注意一点的是:除非session.gc_probability值为100,否则GC也不会绝对执行。它取决于nrand的值大小,根据「nrand = (zend_long) ((float) PS(gc_divisor) * php_combined_lcg());」代码语句:只有在系统产生的伪随机数目((0,100)开区间)小于设置的gc_probability时,才会执行GC。
具体的GC执行过程,根据存储机制(session.save_handler)选择的不同而不同,如文件存储时:
即根据设置的过期时间,将那些符合条件的文件目录下的sessoin数据文件删除掉。需要注意一点的是:如果session文件目录深度大于0(即session.save_path='N;/tmp',N表示session文件分布的目录深度)则不执行任何清理操作。
3、RSHUTDOWN
该过程将调用PHP_RSHUTDOWN_FUNCTION方法
3、MSHUTDOWN
该过程将调用PHP_MSHUTDOWN_FUNCTION方法
该过程进行全局配置与相关全局变量的销毁。
References
1、https://github.com/php/php-src/tree/master/ext/session
2、http://www.phppan.com/2010/12/php-source-code-37-session-cookie-cache-serialize/
3、http://blog.csdn.net/risingsun001/article/details/44568247
4、http://php.net/manual/en/session.constants.php
5、http://stackoverflow.com/questions/818140/php-sid-not-showing