从源码分析PHP的SESSION实现机制
2015-03-23 21:15
661 查看
Session是以扩展的形式嵌入到PHP内核的,所以我们可以把Session当成扩展来看待。
一般扩展被载入到PHP会经过下面几个过程
MINIT -> RINIT ->RSHUTDOWN -> MSHUTDOWN
详细可以查看博文:PHP生命周期
SessionHandlerInterface $sessionhandler )的方式自定义Session的处理机制,而不必像之前一样使用冗长的boolsession_set_save_handler (
callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid ] )):
相关阅读 :PHP语言中的超级全局变量(Superglobals) http://www.walu.cc/phpbook/12.5.md
REGISTER_INI_ENTRIES();实际上是一个宏定义:
#define REGISTER_INI_ENTRIES() zend_register_ini_entries(ini_entries, module_number TSRMLS_CC)
因此,实际上是调用zend_register_ini_entries(ini_entries, module_number TSRMLS_CC)。关于ini文件的解析和配置,已经超出了本文的范畴,可以参考这篇文章:/article/5247200.html 。
扩展中读取和设置ini的相关配置位于PHP_INI_BEGIN和PHP_INI_END宏之间。对于session而言,实际上包括:
因此还需要对这两个Class做相关的初始化工作。这是通过:
INIT_CLASS_ENTRY(ce, PS_IFACE_NAME, php_session_iface_functions);
INIT_CLASS_ENTRY(ce, PS_CLASS_NAME, php_session_class_functions);
来实现的,有兴趣的同学可以查看具体的实现过程,这里不再赘述。
确定是user还是files来处理session的逻辑是由_php_find_ps_module来完成的,这个函数会依次查找ps_modules中预定义的module, 一旦查找成功,立即返回:
ps_module 定义:
ps_files_ptr和ps_user_ptr,是在mod_*.h中定义
而每一个ps_module,实际上是一个struct:
特别说明一下:上面的struct中的int (*s_open)(PS_OPEN_ARGS); 等 为函数指针,也就是说只需要对其进行函数名的赋值就可以调用了,关于函数指针不了解的可以百度。
PS_MOD_SID(files)是一个宏定义:
所以,ps_mod_files展开后就是:
所以 PS(mod)->s_open(PS_OPEN_ARGS)
其实调用的是 PS(mod)->ps_open_files(PS_OPEN_ARGS) 函数
也就是 PS_OPEN_FUNC(files)
#define PS_OPEN_FUNC(x) int ps_open_##x(PS_OPEN_ARGS)
这意味着,每一个处理session的mod,不管是files, user还是其他扩展的模块,都应该包含ps_module中定义的字段,
分别是:
module的名称(s_name),
打开句柄函数(s_open),
关闭句柄函数(s_close),
读取函数(s_read) ,
写入函数(s_write),
销毁函数(s_destroy),
gc函数(s_gc),
生成session_id的函数(s_create_sid)。
我们花费了大量的精力来说session.save_handler, 其实是想说明:原则上,session可以存储在任何可行的存储中的(例如文件,数据库,memcache和redis),如果你自己开发了一个存储系统,比memcache的性能更好,那么OK, 你只要按照session存储的规范,设置好session.save_handler,不管是你在脚本中提供接口还是使用扩展,可以很方便的操作session数据,这块也是分布式。
则相应session文件的内容是:
查找serializer的过程与查找PS(mod)的方式类似:
_php_find_ps_serializer也是在预定义的ps_serializers数组中查找:
同样,每一个serializer都是一个struct:
内部是调用php_session_start完成session相关上下文的设置, 其基本步骤是:
那么可能的情况有:
(a). session_status = php_session_active
表明已经开启了session。那么忽略本次的session_start(), 但同时会产生一条警告信息:
(b). session_status = php_session_ disabled
这种情况可能发生在RINIT的过程中,前面我们看到:
如果session_status = php_session_ disabled, 无法确定session是否真不可用(比如我们在脚本中设置了session_set_save_handler),还要做进一步的分析。查找mod和serializer的过程与RINIT的类似。
(c). session_status = php_session_none
在session_status= php_session_ disabled和php_session_none的情况下,都会继续向下执行。
(为了方便起见,我们直接使用了$_COOKIE, $_GET, $_POST,实际上这样是不严谨的,因为这些超级全局变量是php内核生成并提供给应用程序的,内核实际上是在全局的symbol_table中查找)
a. $_COOKIE中
b. $_GET中
c. $_POST中
任何一此查找成功都会设置PS(id),不再继续查找。
如果客户端cookies没有开启,并且没有找到PS(id),将会检查REQUEST_URI,这里就是对 禁用cookie后session是如何设置的 实现机制。
安全性检查
正常情况下,生成的session_id不会包含html标签,单双引号和空白字符的,如果session_id中包含了这些非法的字符,那么很有可能session_id是伪造的。对于这种情况,处理很简单,释放session_id的空间,并标志为NULL,这样与第一次访问页面时的逻辑就基本一致了:
这里需要对PS(mod)->s_open(...)的调用需要介绍一下。
还记得我们在PHP_RINIT_FUNCTION是对PS(mod)的分析,PS(mod)其实就是一个struct ps_module_struct的实例
ps_module然后调用对应的ps_open_files即PS_OPEN_FUNC(x)。这块的具体调用实现,可以通过之前对PS(mod)的分析详细了解。
需要注意的是第二步,PS(mod)->s_open(...)函数是对文件相关的属性赋值,并不是真正的打开文件,具体数据结构如下ps_files *data
在第二步中通过PS_OPEN_FUNC(files)对参数赋值
而在第五步读取session文件中的数据的时候才打开文件,即调用PS_READ_FUNC(files)
而这里面有一段很重要的语句:
PHP在打开文件后,通过flock(data->fd, LOCK_EX);语句对结构体struct flock进行赋值,并通过fcntl来设置排它锁,
因为设置了排它锁,所以在文件锁定期间,即使是读取文件的数据也是不允许的。这就造成要写入或读取的进程必须等待,直到前一进程释放锁(这通常发生在脚本执行完毕或者用户调用session_commit/session_write_close)。
有关文件锁这一块涉及到linux系统文件锁相关知识,如果要深入理解请自行百度。
在session_start的最后,通过下面代码来触发SESSION的GC:
从代码中可以看到,在判别s_gc是否运行的时候, 有俩个关键变量: PS(gc_divisor)和PS(gc_probability), 这俩个变量分别对应着session的运行时配置项的俩个同名配置项:
session.gc_probability和session.gc_divisor, 他们分别默认为1和100.
而php_combined_lcg是一个随机数发生器, 生成0到1范围的随机数, 所以上面的判别相当于:
rand < probability / gc_divisor
也就是说, 默认情况下, 差不多是100次能调用1次gc过程.
a.文件锁带来的性能问题
前面我们已经提到, 由于是LOCK_EX(互斥锁),因而在文件锁定期间,即使是读取文件的数据也是不允许的。这就造成要写入或读取的进程必须等待,直到前一进程释放锁(这通常发生在脚本执行完毕或者用户调用session_commit/session_write_close)。
b.分布式服务器环境下session共享的问题
session文件存储实际上是存储在服务器的磁盘上的,这样在分布式服务器环境下会造成一定的问题:假如你有a,b,c三台服务器。则用户的多次请求可能按照负载均衡策略定向到不同的服务器,由于服务器之间并没有共享session文件,这在表象看来便发生了session丢失。这虽然可以通过用户粘滞会话解决,但会带来更大的问题:无法服务器的负载均衡,增加了服务器的复杂性。
c.高并发场景下session,大量磁盘I/O
基于以上一些原因,在实际应用中,很多都是使用分布式内存缓存memcache或者redis来存储和共享session的。当然这个不是本章索讨论的范围。
感兴趣的同学可以进一步追踪一下源码实现,由于个人水平有限,文中若出现错误,欢迎大家指出交流。
参考资料:
1. /article/5247200.html
2. http://blog.csdn.net/ohmygirl/article/details/43152683
3. /article/9287843.html
4. http://www.laruence.com/2011/03/29/1949.html
5. http://php.net/manual/en/book.session.php
一般扩展被载入到PHP会经过下面几个过程
#define PHP_MINIT_FUNCTION ZEND_MODULE_STARTUP_D // 初始化module时运行 #define PHP_MSHUTDOWN_FUNCTION ZEND_MODULE_SHUTDOWN_D // 当module被卸载时运行 #define PHP_RINIT_FUNCTION ZEND_MODULE_ACTIVATE_D // 当一个REQUEST请求初始化时运行 #define PHP_RSHUTDOWN_FUNCTION ZEND_MODULE_DEACTIVATE_D // 当一个REQUEST请求结束时运行 #define PHP_MINFO_FUNCTION ZEND_MODULE_INFO_D // 这个是设置phpinfo中这个模块的信息 #define PHP_GINIT_FUNCTION ZEND_GINIT_FUNCTION // 初始化全局变量时 #define PHP_GSHUTDOWN_FUNCTION ZEND_GSHUTDOWN_FUNCTION // 释放全局变量时具体的执行顺序跟PHP的生命周期相同
MINIT -> RINIT ->RSHUTDOWN -> MSHUTDOWN
详细可以查看博文:PHP生命周期
1. SESSION模块的初始化 PHP_MINIT_FUNCTION
对于Session而言,PHP_MINIT_FUNCTION主要完成的初始化工作包括(注:不同版本的PHP具体处理过程并不完全相同,如PHP 5.4+提供了SessionHandlerInterface,这样可以通过session_set_save_handler (SessionHandlerInterface $sessionhandler )的方式自定义Session的处理机制,而不必像之前一样使用冗长的boolsession_set_save_handler (
callable $open , callable $close , callable $read , callable $write , callable $destroy , callable $gc [, callable $create_sid ] )):
(1). 注册$_SESSION超全局变量:
zend_register_auto_global("_SESSION", sizeof("_SESSION")-1, NULL TSRMLS_CC);就是说,$_SESSION超全局变量实际上是在session的MINIT阶段被注册的。
相关阅读 :PHP语言中的超级全局变量(Superglobals) http://www.walu.cc/phpbook/12.5.md
(2). 读取ini文件中的相关配置。
REGISTER_INI_ENTRIES();
REGISTER_INI_ENTRIES();实际上是一个宏定义:
#define REGISTER_INI_ENTRIES() zend_register_ini_entries(ini_entries, module_number TSRMLS_CC)
因此,实际上是调用zend_register_ini_entries(ini_entries, module_number TSRMLS_CC)。关于ini文件的解析和配置,已经超出了本文的范畴,可以参考这篇文章:/article/5247200.html 。
扩展中读取和设置ini的相关配置位于PHP_INI_BEGIN和PHP_INI_END宏之间。对于session而言,实际上包括:
/* {{{ PHP_INI */ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("session.save_path", "", PHP_INI_ALL, OnUpdateSaveDir,save_path, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.name", "PHPSESSID", PHP_INI_ALL, OnUpdateName, session_name, php_ps_globals, ps_globals) PHP_INI_ENTRY("session.save_handler", "files", PHP_INI_ALL, OnUpdateSaveHandler) STD_PHP_INI_BOOLEAN("session.auto_start", "0", PHP_INI_PERDIR, OnUpdateBool, auto_start, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.gc_probability", "1", PHP_INI_ALL, OnUpdateLong, gc_probability, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.gc_divisor", "100", PHP_INI_ALL, OnUpdateLong, gc_divisor, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.gc_maxlifetime", "1440", PHP_INI_ALL, OnUpdateLong, gc_maxlifetime, php_ps_globals, ps_globals) PHP_INI_ENTRY("session.serialize_handler", "php", PHP_INI_ALL, OnUpdateSerializer) STD_PHP_INI_ENTRY("session.cookie_lifetime", "0", PHP_INI_ALL, OnUpdateLong, cookie_lifetime, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.cookie_path", "/", PHP_INI_ALL, OnUpdateString, cookie_path, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.cookie_domain", "", PHP_INI_ALL, OnUpdateString, cookie_domain, php_ps_globals, ps_globals) STD_PHP_INI_BOOLEAN("session.cookie_secure", "", PHP_INI_ALL, OnUpdateBool, cookie_secure, php_ps_globals, ps_globals) STD_PHP_INI_BOOLEAN("session.cookie_httponly", "", PHP_INI_ALL, OnUpdateBool, cookie_httponly, php_ps_globals, ps_globals) STD_PHP_INI_BOOLEAN("session.use_cookies", "1", PHP_INI_ALL, OnUpdateBool, use_cookies, php_ps_globals, ps_globals) STD_PHP_INI_BOOLEAN("session.use_only_cookies", "1", PHP_INI_ALL, OnUpdateBool, use_only_cookies, php_ps_globals, ps_globals) STD_PHP_INI_BOOLEAN("session.use_strict_mode", "0", PHP_INI_ALL, OnUpdateBool, use_strict_mode, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.referer_check", "", PHP_INI_ALL, OnUpdateString, extern_referer_chk, php_ps_globals, ps_globals) #if HAVE_DEV_URANDOM STD_PHP_INI_ENTRY("session.entropy_file", "/dev/urandom", PHP_INI_ALL, OnUpdateString, entropy_file, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.entropy_length", "32", PHP_INI_ALL, OnUpdateLong, entropy_length, php_ps_globals, ps_globals) #elif HAVE_DEV_ARANDOM STD_PHP_INI_ENTRY("session.entropy_file", "/dev/arandom", PHP_INI_ALL, OnUpdateString, entropy_file, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.entropy_length", "32", PHP_INI_ALL, OnUpdateLong, entropy_length, php_ps_globals, ps_globals) #else STD_PHP_INI_ENTRY("session.entropy_file", "", PHP_INI_ALL, OnUpdateString, entropy_file, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.entropy_length", "0", PHP_INI_ALL, OnUpdateLong, entropy_length, php_ps_globals, ps_globals) #endif STD_PHP_INI_ENTRY("session.cache_limiter", "nocache", PHP_INI_ALL, OnUpdateString, cache_limiter, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.cache_expire", "180", PHP_INI_ALL, OnUpdateLong, cache_expire, php_ps_globals, ps_globals) PHP_INI_ENTRY("session.use_trans_sid", "0", PHP_INI_ALL, OnUpdateTransSid) PHP_INI_ENTRY("session.hash_function", "0", PHP_INI_ALL, OnUpdateHashFunc) STD_PHP_INI_ENTRY("session.hash_bits_per_character", "4", PHP_INI_ALL, OnUpdateLong, hash_bits_per_character, php_ps_globals, ps_globals) /* Upload progress */ STD_PHP_INI_BOOLEAN("session.upload_progress.enabled", "1", ZEND_INI_PERDIR, OnUpdateBool, rfc1867_enabled, php_ps_globals, ps_globals) STD_PHP_INI_BOOLEAN("session.upload_progress.cleanup", "1", ZEND_INI_PERDIR, OnUpdateBool, rfc1867_cleanup, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.upload_progress.prefix", "upload_progress_", ZEND_INI_PERDIR, OnUpdateSmartStr, rfc1867_prefix, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.upload_progress.name", "PHP_SESSION_UPLOAD_PROGRESS", ZEND_INI_PERDIR, OnUpdateSmartStr, rfc1867_name, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.upload_progress.freq", "1%", ZEND_INI_PERDIR, OnUpdateRfc1867Freq, rfc1867_freq, php_ps_globals, ps_globals) STD_PHP_INI_ENTRY("session.upload_progress.min_freq", "1", ZEND_INI_PERDIR, OnUpdateReal, rfc1867_min_freq,php_ps_globals, ps_globals) /* Commented out until future discussion */ /* PHP_INI_ENTRY("session.encode_sources", "globals,track", PHP_INI_ALL, NULL) */ PHP_INI_END() /* }}} */如果在ini文件中没有配置相关的参数项,在session的MINIT阶段,参数会被初始化为默认的值。
(3). 注册SessionHandler和SessionHandlerInterface这两个Class
自php 5.4起,php提供了SessionHandler和SessionHandlerInterface这两个Class,因此还需要对这两个Class做相关的初始化工作。这是通过:
INIT_CLASS_ENTRY(ce, PS_IFACE_NAME, php_session_iface_functions);
INIT_CLASS_ENTRY(ce, PS_CLASS_NAME, php_session_class_functions);
来实现的,有兴趣的同学可以查看具体的实现过程,这里不再赘述。
2. session请求时的准备RINIT PHP_RINIT_FUNCTION
static PHP_RINIT_FUNCTION(session) /* {{{ */ { return php_rinit_session(PS(auto_start) TSRMLS_CC); } /* ******************************** * Module Setup and Destruction * ******************************** */ static int php_rinit_session(zend_bool auto_start TSRMLS_DC) /* {{{ */ {// 初始化session相关的全局变量 php_rinit_session_globals(TSRMLS_C); // 根据ini的配置查找session.save_handler,从而确定是使用files还是user( 或者是其他的扩展方式)来处理session: if (PS(mod) == NULL) { char *value; value = zend_ini_string("session.save_handler", sizeof("session.save_handler"), 0); if (value) { PS(mod) = _php_find_ps_module(value TSRMLS_CC); } } // 确定完session的save_handler之后。需要确定serializer,Serializer用于完成session数据的序列化和反序列化 if (PS(serializer) == NULL) { char *value; value = zend_ini_string("session.serialize_handler", sizeof("session.serialize_handler"), 0); if (value) { PS(serializer) = _php_find_ps_serializer(value TSRMLS_CC); } } // mod和serializer 如果有一个不成功,更新session_status 状态 if (PS(mod) == NULL || PS(serializer) == NULL) { /* current status is unusable */ PS(session_status) = php_session_disabled; return SUCCESS; } // 如果ini 中 session.auto_start 为1 自动session_start if (auto_start) { php_session_start(TSRMLS_C); } return SUCCESS; } /* }}} */正如上面的代码所写,PHP_RINIT_FUNCTION(session)主要经过下面几个步骤:
(1).初始化session相关的全局变量,这是通过php_rinit_session_globals来完成的:
/* Dispatched by RINIT and by php_session_destroy */ static inline void php_rinit_session_globals(TSRMLS_D) /* {{{ */ { PS(id) = NULL; PS(session_status) = php_session_none; PS(mod_data) = NULL; PS(mod_user_is_open) = 0; /* Do NOT init PS(mod_user_names) here! */ PS(http_session_vars) = NULL; } /* }}} */
(2).根据ini的配置查找session.save_handler,从而确定是使用files还是user( 或者是其他的扩展方式)来处理session:
if (PS(mod) == NULL) { char *value; value = zend_ini_string("session.save_handler", sizeof("session.save_handler"), 0); if (value) { PS(mod) = _php_find_ps_module(value TSRMLS_CC); } }
确定是user还是files来处理session的逻辑是由_php_find_ps_module来完成的,这个函数会依次查找ps_modules中预定义的module, 一旦查找成功,立即返回:
PHPAPI ps_module *_php_find_ps_module(char *name TSRMLS_DC) /* {{{ */ { 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_module 定义:
/* ******************* * Storage Modules * ******************* */ #define MAX_MODULES 10 #define PREDEFINED_MODULES 2 static ps_module *ps_modules[MAX_MODULES + 1] = { ps_files_ptr, ps_user_ptr };
ps_files_ptr和ps_user_ptr,是在mod_*.h中定义
ps_files_ptr和ps_user_ptr,是在mod_*.h中定义 ext/session/mod_files.h extern ps_module ps_mod_files; #define ps_files_ptr &ps_mod_files ps_mod_files的内容是 ps_module ps_mod_files = { PS_MOD_SID(files) };
而每一个ps_module,实际上是一个struct:
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); int (*s_write)(PS_WRITE_ARGS); int (*s_destroy)(PS_DESTROY_ARGS); int (*s_gc)(PS_GC_ARGS); char *(*s_create_sid)(PS_CREATE_SID_ARGS); } ps_module;
特别说明一下:上面的struct中的int (*s_open)(PS_OPEN_ARGS); 等 为函数指针,也就是说只需要对其进行函数名的赋值就可以调用了,关于函数指针不了解的可以百度。
PS_MOD_SID(files)是一个宏定义:
#define PS_MOD_SID(x) \ #x, ps_open_##x, ps_close_##x, ps_read_##x, ps_write_##x, \ ps_delete_##x, ps_gc_##x, ps_create_sid_##x
所以,ps_mod_files展开后就是:
ps_module ps_mod_files = { files, ps_open_files, ps_close_files, ps_read_files, ps_write_files, ps_delete_files, ps_gc_files, php_session_sid_files };
所以 PS(mod)->s_open(PS_OPEN_ARGS)
其实调用的是 PS(mod)->ps_open_files(PS_OPEN_ARGS) 函数
也就是 PS_OPEN_FUNC(files)
#define PS_OPEN_FUNC(x) int ps_open_##x(PS_OPEN_ARGS)
这意味着,每一个处理session的mod,不管是files, user还是其他扩展的模块,都应该包含ps_module中定义的字段,
分别是:
module的名称(s_name),
打开句柄函数(s_open),
关闭句柄函数(s_close),
读取函数(s_read) ,
写入函数(s_write),
销毁函数(s_destroy),
gc函数(s_gc),
生成session_id的函数(s_create_sid)。
我们花费了大量的精力来说session.save_handler, 其实是想说明:原则上,session可以存储在任何可行的存储中的(例如文件,数据库,memcache和redis),如果你自己开发了一个存储系统,比memcache的性能更好,那么OK, 你只要按照session存储的规范,设置好session.save_handler,不管是你在脚本中提供接口还是使用扩展,可以很方便的操作session数据,这块也是分布式。
(3).session数据的序列化和反序列化
确定完session的save_handler之后。需要确定serializer, 这个也是必须的。Serializer用于完成session数据的序列化和反序列化,我们在session.save_handler=files的情况下可以看到,session数据并不是直接写入文件的,而是通过一定的序列化机制序列化之后存储到文件的,在读取session数据时需要对文件的内容进行反序列化:session_save_path('/tmp/session'); session_start(); $_SESSION['key'] = 'value'; session_write_close();
则相应session文件的内容是:
key|s:5:"value"
查找serializer的过程与查找PS(mod)的方式类似:
if (PS(serializer) == NULL) { char *value; value = zend_ini_string("session.serialize_handler", sizeof("session.serialize_handler"), 0); if (value) { PS(serializer) = _php_find_ps_serializer(value TSRMLS_CC); } }
_php_find_ps_serializer也是在预定义的ps_serializers数组中查找:
PHPAPI const ps_serializer *_php_find_ps_serializer(char *name TSRMLS_DC) { const ps_serializer *ret = NULL; const ps_serializer *mod; for (mod = ps_serializers; mod->name; mod++) { if (!strcasecmp(name, mod->name)) { ret = mod; break; } } return ret; } static ps_serializer ps_serializers[MAX_SERIALIZERS + 1] = { PS_SERIALIZER_ENTRY(php_serialize), PS_SERIALIZER_ENTRY(php), PS_SERIALIZER_ENTRY(php_binary) };
同样,每一个serializer都是一个struct:
typedef struct ps_serializer_struct { const char *name; int (*encode)(PS_SERIALIZER_ENCODE_ARGS); int (*decode)(PS_SERIALIZER_DECODE_ARGS); } ps_serializer;这时,如果mod不存在(设置的session.save_handler错误)或者serializer不存在,那么直接标记session_status为php_session_disabled,并返回,后面的代码不再执行。否则,确定了mod和serializer,如果设置了session.auto_start,那么就自动开启session:
if (auto_start) { php_session_start(TSRMLS_C); }
3.session_start
session_start用于开启或者重用现有的会话,在底层,其实现为:/* {{{ proto bool session_start(void) Begin session - reinitializes freezed variables, registers browsers etc */ static PHP_FUNCTION(session_start) { /* skipping check for non-zero args for performance reasons here ?*/ php_session_start(TSRMLS_C); if (PS(session_status) != php_session_active) { RETURN_FALSE; } RETURN_TRUE; } /* }}} */
内部是调用php_session_start完成session相关上下文的设置, 其基本步骤是:
(1). 检查当前会话的session状态。
php_session_status用于标志所有可能的会话状态,它是一个enum://php_session.h line: 95 typedef enum { php_session_disabled, php_session_none, php_session_active } php_session_status;
那么可能的情况有:
(a). session_status = php_session_active
表明已经开启了session。那么忽略本次的session_start(), 但同时会产生一条警告信息:
A session had already been started - ignoring session_start()
(b). session_status = php_session_ disabled
这种情况可能发生在RINIT的过程中,前面我们看到:
if (PS(mod) == NULL || PS(serializer) == NULL) { /* current status is unusable */ PS(session_status) = php_session_disabled; return SUCCESS; }
如果session_status = php_session_ disabled, 无法确定session是否真不可用(比如我们在脚本中设置了session_set_save_handler),还要做进一步的分析。查找mod和serializer的过程与RINIT的类似。
(c). session_status = php_session_none
在session_status= php_session_ disabled和php_session_none的情况下,都会继续向下执行。
(2). 获取session_id
如果session_id不存在,那么内核会依次尝试下列方法获取session_id(为了方便起见,我们直接使用了$_COOKIE, $_GET, $_POST,实际上这样是不严谨的,因为这些超级全局变量是php内核生成并提供给应用程序的,内核实际上是在全局的symbol_table中查找)
a. $_COOKIE中
b. $_GET中
c. $_POST中
任何一此查找成功都会设置PS(id),不再继续查找。
如果客户端cookies没有开启,并且没有找到PS(id),将会检查REQUEST_URI,这里就是对 禁用cookie后session是如何设置的 实现机制。
安全性检查
正常情况下,生成的session_id不会包含html标签,单双引号和空白字符的,如果session_id中包含了这些非法的字符,那么很有可能session_id是伪造的。对于这种情况,处理很简单,释放session_id的空间,并标志为NULL,这样与第一次访问页面时的逻辑就基本一致了:
if (PS(id) && strpbrk(PS(id), "\r\n\t <>'\"\\")) { efree(PS(id)); PS(id) = NULL; }
(3). 执行php_session_initialize完成session的初始化工作。
static void php_session_initialize(TSRMLS_D) /* {{{ */ { char *val = NULL; int vallen; // 第一步,验证PS(mod) 是否存在 if (!PS(mod)) { php_error_docref(NULL TSRMLS_CC, E_ERROR, "No storage module chosen - failed to initialize session"); return; } // 第二步,打开session文件 /* Open session handler first */ if (PS(mod)->s_open(&PS(mod_data), PS(save_path), PS(session_name) TSRMLS_CC) == FAILURE) { php_error_docref(NULL TSRMLS_CC, E_ERROR, "Failed to initialize storage module: %s (path: %s)", PS(mod)->s_name, PS(save_path)); return; } // 第三步,判断session_id,如果session_id 不存在,创建一个 /* If there is no ID, use session module to create one */ if (!PS(id)) { PS(id) = PS(mod)->s_create_sid(&PS(mod_data), NULL TSRMLS_CC); if (!PS(id)) { php_error_docref(NULL TSRMLS_CC, E_ERROR, "Failed to create session ID: %s (path: %s)", PS(mod)->s_name, PS(save_path)); return; } if (PS(use_cookies)) { PS(send_cookie) = 1; } } // 第四步 session.use_strict_mode指定是否将使用严格的会话ID模式。如果该模式被激活,模块不接受未初始化会话ID。 // 如果未初始化会话ID从浏览器发送的,新的会话ID被发送到浏览器。 // 应用程序通过会议通过严格的方式保护会话固定。默认为0 (禁用) 。 /* Set session ID for compatibility for older/3rd party save handlers */ if (!PS(use_strict_mode)) { php_session_reset_id(TSRMLS_C); PS(session_status) = php_session_active; } // 第五步 读取session数据到val中 /* Read data */ php_session_track_init(TSRMLS_C); // 无条件地摧毁现有的session 数组,可能是脏数据 if (PS(mod)->s_read(&PS(mod_data), PS(id), &val, &vallen TSRMLS_CC) == FAILURE) { /* Some broken save handler implementation returns FAILURE for non-existent session ID */ /* It's better to raise error for this, but disabled error for better compatibility */ /* php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Failed to read session data: %s (path: %s)", PS(mod)->s_name, PS(save_path)); */ } // 如果使用严格会话ID模式,在session不活跃状态下重置 // 这里涉及到session安全机制,详细可以看http://php.net/manual/zh/session.security.php /* Set session ID if session read didn't activated session */ if (PS(use_strict_mode) && PS(session_status) != php_session_active) { php_session_reset_id(TSRMLS_C); PS(session_status) = php_session_active; } // 第六步 对文件中读取到的session数据进行反序列化 if (val) { php_session_decode(val, vallen TSRMLS_CC); str_efree(val); } // 第七步 session全局配置参数的值use_cookie_only和use_trans_sid, // 两者分别表示sessionid在客户端只能通过cookie保存和只能通过url传递 if (!PS(use_cookies) && PS(send_cookie)) { if (PS(use_trans_sid) && !PS(use_only_cookies)) { PS(apply_trans_sid) = 1; } PS(send_cookie) = 0; } } /* }}} */
这里需要对PS(mod)->s_open(...)的调用需要介绍一下。
还记得我们在PHP_RINIT_FUNCTION是对PS(mod)的分析,PS(mod)其实就是一个struct ps_module_struct的实例
ps_module然后调用对应的ps_open_files即PS_OPEN_FUNC(x)。这块的具体调用实现,可以通过之前对PS(mod)的分析详细了解。
需要注意的是第二步,PS(mod)->s_open(...)函数是对文件相关的属性赋值,并不是真正的打开文件,具体数据结构如下ps_files *data
// mod_files.c line:59 typedef struct { int fd; char *lastkey; char *basedir; size_t basedir_len; size_t dirdepth; size_t st_size; int filemode; } ps_files;
在第二步中通过PS_OPEN_FUNC(files)对参数赋值
ps_files *data; data = ecalloc(1, sizeof(*data)); // mod_files.c line:303 data->fd = -1; //初始化资源id data->dirdepth = dirdepth; // 目录深度 比如 N;save_path 值就是N data->filemode = filemode; // 文件模式filemode data->basedir_len = strlen(save_path); //路径长度 data->basedir = estrndup(save_path, data->basedir_len); //路径地址
而在第五步读取session文件中的数据的时候才打开文件,即调用PS_READ_FUNC(files)
而这里面有一段很重要的语句:
flock(data->fd, LOCK_EX);
PHP在打开文件后,通过flock(data->fd, LOCK_EX);语句对结构体struct flock进行赋值,并通过fcntl来设置排它锁,
因为设置了排它锁,所以在文件锁定期间,即使是读取文件的数据也是不允许的。这就造成要写入或读取的进程必须等待,直到前一进程释放锁(这通常发生在脚本执行完毕或者用户调用session_commit/session_write_close)。
有关文件锁这一块涉及到linux系统文件锁相关知识,如果要深入理解请自行百度。
(4). session的gc
在PHP中, 如果使用file_handler作为Session的save handler, 那么就有概率在每次session_start的时候运行Session的Gc过程。在session_start的最后,通过下面代码来触发SESSION的GC:
if ((PS(mod_data) || PS(mod_user_implemented)) && PS(gc_probability) > 0) { int nrdels = -1; nrand = (int) ((float) PS(gc_divisor) * php_combined_lcg(TSRMLS_C)); if (nrand < PS(gc_probability)) { PS(mod)->s_gc(&PS(mod_data), PS(gc_maxlifetime), &nrdels TSRMLS_CC); #ifdef SESSION_DEBUG if (nrdels != -1) { php_error_docref(NULL TSRMLS_CC, E_NOTICE, "purged %d expired session objects", nrdels); } #endif } }
从代码中可以看到,在判别s_gc是否运行的时候, 有俩个关键变量: PS(gc_divisor)和PS(gc_probability), 这俩个变量分别对应着session的运行时配置项的俩个同名配置项:
session.gc_probability和session.gc_divisor, 他们分别默认为1和100.
而php_combined_lcg是一个随机数发生器, 生成0到1范围的随机数, 所以上面的判别相当于:
rand < probability / gc_divisor
也就是说, 默认情况下, 差不多是100次能调用1次gc过程.
4. session请求结束 RSHUTDOWN PHP_RSHUTDOWN_FUNCTION
static PHP_RSHUTDOWN_FUNCTION(session) /* {{{ */ { int i; <span style="white-space:pre"> </span>// (1). 将session序列化后写入文件,并unlock文件,关闭文件句柄 zend_try { php_session_flush(TSRMLS_C); } zend_end_try(); php_rshutdown_session_globals(TSRMLS_C); //(2) 将http_session_vars变量refcount减一,即destroy全局变量 /* this should NOT be done in php_rshutdown_session_globals() */ for (i = 0; i < 7; i++) { if (PS(mod_user_names).names[i] != NULL) { zval_ptr_dtor(&PS(mod_user_names).names[i]); PS(mod_user_names).names[i] = NULL; } } return SUCCESS; } /* }}} */从上面的代码中我们可以知道,php在脚本执行过程中,并不会对session的数据进行文件写入,而是在请求结束后,再进行写入,并关闭句柄,这里不做深入研究,大家可以查看源码来进一步深入。
5. session扩展模块结束 MSHUTDOWN PHP_MSHUTDOWN_FUNCTION
模块结束后,主要对于一些全局变量和配置的销毁static PHP_MSHUTDOWN_FUNCTION(session) /* {{{ */
{
UNREGISTER_INI_ENTRIES();// 配置销毁
#ifdef HAVE_LIBMM
PHP_MSHUTDOWN(ps_mm) (SHUTDOWN_FUNC_ARGS_PASSTHRU);
#endif
/* reset rfc1867 callbacks */
php_session_rfc1867_orig_callback = NULL;
if (php_rfc1867_callback == php_session_rfc1867_callback) {
php_rfc1867_callback = NULL;
}
ps_serializers[PREDEFINED_SERIALIZERS].name = NULL;
memset(&ps_modules[PREDEFINED_MODULES], 0, (MAX_MODULES-PREDEFINED_MODULES)*sizeof(ps_module *));
return SUCCESS;
}
/* }}} */
6. session文件存储的问题
在session.save_handler=files的情况下,会有哪些性能问题和瓶颈?a.文件锁带来的性能问题
前面我们已经提到, 由于是LOCK_EX(互斥锁),因而在文件锁定期间,即使是读取文件的数据也是不允许的。这就造成要写入或读取的进程必须等待,直到前一进程释放锁(这通常发生在脚本执行完毕或者用户调用session_commit/session_write_close)。
b.分布式服务器环境下session共享的问题
session文件存储实际上是存储在服务器的磁盘上的,这样在分布式服务器环境下会造成一定的问题:假如你有a,b,c三台服务器。则用户的多次请求可能按照负载均衡策略定向到不同的服务器,由于服务器之间并没有共享session文件,这在表象看来便发生了session丢失。这虽然可以通过用户粘滞会话解决,但会带来更大的问题:无法服务器的负载均衡,增加了服务器的复杂性。
c.高并发场景下session,大量磁盘I/O
基于以上一些原因,在实际应用中,很多都是使用分布式内存缓存memcache或者redis来存储和共享session的。当然这个不是本章索讨论的范围。
7.总结
session探索到这里就基本结束了,上面基本对session的时间通过以PHP的生命周期将其实现机制详细介绍了一遍,当然,其中还有很多细节和函数没有涉及到。感兴趣的同学可以进一步追踪一下源码实现,由于个人水平有限,文中若出现错误,欢迎大家指出交流。
参考资料:
1. /article/5247200.html
2. http://blog.csdn.net/ohmygirl/article/details/43152683
3. /article/9287843.html
4. http://www.laruence.com/2011/03/29/1949.html
5. http://php.net/manual/en/book.session.php
相关文章推荐
- 分布式文件系统KFS源码阅读与分析(三):RPC实现机制(MetaServer端)
- php与memcached服务器交互的分布式实现源码分析[memcache版]
- struts2源码分析-IOC容器的实现机制(上篇)
- struts2源码分析-IOC容器的实现机制(上篇)(转载)
- OpenStack建立实例完整过程源码详细分析(15)----依据AMQP通信架构实现消息接收机制解析之二
- PHP源码分析-弱类型变量实现
- php与memcached服务器交互的分布式实现源码分析[memcache版]
- PHP源码分析-弱类型变量实现
- zookeeper源码阅读分析笔记--客户端服务端通信机制以及session超时、过期处理
- CI框架源码分析之Session类Session.php
- 分布式系统Hadoop源码阅读与分析(一):作业调度器实现机制
- [转]php与memcached服务器交互的分布式实现源码分析[memcache版]
- PHP与Memcached服务器交互的分布式实现源码分析
- 深入分析Linux内核源码-Linux管道的实现机制
- php与memcached服务器交互的分布式实现源码分析 [memcache版]
- 分布式文件系统KFS源码阅读与分析(四):RPC实现机制(KfsClient端)
- struts2源码分析-IOC容器的实现机制(上篇)
- php与memcached服务器交互的分布式实现源码分析[memcache版]
- OpenStack建立实例完整过程源码详细分析(13)----依据AMQP通信架构实现消息发送机制解析之二
- OpenStack建立实例完整过程源码详细分析(14)----依据AMQP通信架构实现消息接收机制解析之一