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

results matching ""

    No results matching ""