PHP カーネルの深い理解 (3) 概要 - SAPI の概要
この記事へのリンク: http://www.orlion.ml/234/
1. PHP ライフ サイクル、一部の関連するサービス関連の操作は、SAPI インターフェイスを通じて実装されます。これらの組み込み実装の物理的な場所は、PHP ソース コードの SAPI ディレクトリ内にあります。このディレクトリには、コマンド ライン プログラムの実装、Apache の mod_php モジュール実装、fastcgi 実装など、各サーバー抽象化レイヤーの PHP コードが保存されます。
ここでは、各サーバー抽象化レイヤー間で同じ規則に従います。これを SAPI インターフェースと呼びます。各 SAPI 実装は、_sapi_module_struct 構造体変数です。 (SAPI インターフェース)。 PHP ソース コードでは、サーバー関連の情報を呼び出す必要がある場合、その情報はすべて SAPI インターフェイスの対応するメソッド呼び出しを通じて実装されます。これらのメソッドは、各サーバー抽象化レイヤーが実装されるときに独自の実装を持ちます。多くの操作には多用途性があるため、インターフェイス メソッドの大部分ではデフォルトのメソッドが使用されます。下の図は SPAI の簡単な図です
CGI モードと apache2 サーバーを例にとると、それらの起動方法は次のとおりです:
cgi_sapi_module.startup(&cgi_sapi_module) // cgi模式 cgi/cgi_main.c文件<span>apache_sapi_module.startup(&apache_sapi_module); // apache服务器 apache2handler/sapi_apache2.c文件</span>
ここでの cgi_sapi_module は、sapi_module_struct 構造体の静的変数です。その起動メソッドは php_cgi_startup 関数ポインタを指します。起動関数ポインターに加えて、この構造には他の多くのメソッドまたはフィールドがあります。これらの構造はサーバーのインターフェイス実装で定義されます。
SAPI 全体はオブジェクト指向に似ています。テンプレートメソッドパターンの適用。 SAPI.c および SAPI.h ファイルに含まれる一部の関数は、テンプレート メソッド パターンの抽象テンプレートであり、各サーバーの sapi_module の定義および関連実装は、特定のテンプレート
2. Apache モジュールです。
(1) PHP を Apache サーバー上で実行する必要がある場合、一般的には mod_php5 モジュールの形式で統合できます。このとき、mod_php5 モジュールの機能は PHP ファイルを受け取ることです。 Aapche によって渡されたリクエストを処理し、処理された結果を Apache に返します。 Apache が起動する前に設定ファイルで PHP モジュールを設定すると、PHP モジュールは apache2 の ap_hook_post_config フックを登録し、Apache が PHP ファイルに対するリクエストの受信を開始したときにこのモジュールを起動します。
起動時のこのロード方法に加えて、Apache モジュールは実行時に動的にロードできます。つまり、ソース コードを再コンパイルしたり、サーバーを再起動したりせずにサーバーを拡張できます。必要なのは、信号 HUP または AP_SIG_GEACEFUL をサーバーに送信して、モジュールをリロードするようにサーバーに通知することだけです。ただし、動的にロードする前に、モジュールをダイナミック リンク ライブラリにコンパイルする必要があります。このときのダイナミックロードとは、ダイナミックリンクライブラリをロードすることです。 Apache のダイナミック リンク ライブラリの処理は、mod_so モジュールを通じて完了します。そのため、mod_so モジュールは Apache のコアに静的にコンパイルすることしかできません。これは、Apache と一緒に起動されることを意味します。
Apache はどのようにモジュールをロードしますか? mod_php5 を例に挙げると、まず httpd.conf に行を追加します:
LoadModule php5_module modules/mod_php5.so
設定ファイルに示されている命令を追加した後、Apache はモジュール名に基づいてモジュールをロードします。モジュールを見つけてロードします。 Apache の各モジュールはモジュール構造の形で存在し、モジュール構造の name 属性は末尾のマクロ STANDARD20_MODULE_STUFF を通じて __FILE__ に反映されます。前の命令で指定されたパスを通じて関連するダイナミック リンク ライブラリ ファイルを見つけた後、Apache は内部関数を通じてダイナミック リンク ライブラリのコンテンツを取得し、モジュールのコンテンツをメモリ内の指定された変数にロードします。
モジュールを実際にアクティブ化する前に、Apache はロードされたすべてのモジュールが実際の Apache モジュールであるかどうかを確認します。最後に、Apache は関連する関数 (ap_add_loaded_module) を呼び出してモジュールをアクティブ化します。ここでのアクティブ化は、モジュールを対応するリンク リスト (ap_top_modules リンク リスト) に入れることです。
Apache は PHP モジュールをロードします。このモジュールはどうでしょうか? Apache2 の mod_php5 モジュールには、sapi/apache2handler と sapi/apache2filter という 2 つのディレクトリが含まれています。 apache2_handle/mod_php5.c ファイルでは、モジュールによって定義されている関連コードは次のとおりです:
AP_MODULE_DECLARE_DATA module php5_module =<span> { STANDARD20_MODULE_STUFF, /* 宏,包括版本,小版本,模块索引,模块名,下一个模块指针等信息,其中模块名以__FILE__体现*/<span> create_php_config, /* create per-directory config structure */<span> merge_php_config, /* merge per-directory config structures */<span> NULL, /* create per-server config structure */<span> NULL, /* merge per-server config structures */<span> php_dir_cmds, /*模块定义的所有命令*/<span> php_ap2_register_hook /*注册钩子,此函数通过ap_hoo_开头的函数在一次处理过程中对于指定的步骤注册钩子*/<span>};</span></span></span></span></span></span></span></span>
Apache のモジュール構造に相当します。モジュールの構造は次のように定義されます。
typedef struct<span> module_struct module;struct<span> module_struct { int<span> version; int<span> minor_version; int<span> module_index; const char *<span>name; void *<span>dynamic_load_handle; struct module_struct *<span>next; unsigned long<span> magic; void (*rewrite_args) (process_rec *<span>process); void *(*create_dir_config) (apr_pool_t *p, char *<span>dir); void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *<span>new_conf); void *(*create_server_config) (apr_pool_t *p, server_rec *<span>s); void *(*merge_server_config) (apr_pool_t *p, void *base_conf, void *<span>new_conf); const command_rec *<span>cmds; void (*register_hooks) (apr_pool_t *<span>p);}</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
上記のモジュール構造は同じです。 mod_php5.c で見た構造とは少し異なりますが、これは STANDARD20_MODULE_STUFF によるものです。このマクロには最初の 8 つのフィールドの定義が含まれています。 STANDARD20_MODULE_STUFF マクロは次のように定義されます。
/** Use this in all standard modules */#define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \<span> MODULE_MAGIC_NUMBER_MINOR, \ -1<span>, \ __FILE__, \ NULL, \ NULL, \ MODULE_MAGIC_COOKIE, \ NULL /* rewrite args spot */</span></span>
php5_module によって定義された構造では、php_dir_cmds はモジュールによって定義されたすべての命令のセットです。定義は次のとおりです。 >
const command_rec php_dir_cmds[] =<span>{ AP_INIT_TAKE2("php_value"<span>, php_apache_value_handler, NULL, OR_OPTIONS, "PHP Value Modifier"<span>), AP_INIT_TAKE2("php_flag"<span>, php_apache_flag_handler, NULL, OR_OPTIONS, "PHP Flag Modifier"<span>), AP_INIT_TAKE2("php_admin_value"<span>, php_apache_admin_value_handler, NULL, ACCESS_CONF|RSRC_CONF, "PHP Value Modifier (Admin)"<span>), AP_INIT_TAKE2("php_admin_flag"<span>, php_apache_admin_flag_handler, NULL, ACCESS_CONF|RSRC_CONF, "PHP Flag Modifier (Admin)"<span>), AP_INIT_TAKE1("PHPINIDir"<span>, php_apache_phpini_set, NULL, RSRC_CONF, "Directory containing the php.ini file"<span>), {NULL}};</span></span></span></span></span></span></span></span></span></span></span>これは、mod_php5 モジュールによって定義されたコマンド リストです。これは実際には commond_rec 構造体の配列です。 Apache は命令を検出すると、各モジュールの命令テーブルを 1 つずつ調べて、その命令を処理できるモジュールがあるかどうかを調べます。見つかった場合は、その命令内のすべてのモジュールが呼び出されます。テーブルが命令を処理できない場合、エラーが報告されます。上記のように、mod_php5 モジュールは php_value と他の 5 つの命令のみを提供します。 php_ap2_register_hook 関数は次のように定義されます:
void php_ap2_register_hook(apr_pool_t *<span>p){ ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE);}</span>
以上代码声明了pre_config,post_config,handler和child_init4个挂钩以及对应的处理函数。其中pre_config,post_config,child_init是启动挂钩,它们在服务器启动时调用。handler挂钩是请求挂钩,它在服务器处理请求时调用。其中在post_config挂钩中启动php。它通过php_apache_server_startup函数实现,php_apache_server_startup函数通过调用sapi_startup启动sapi,并通过调用php_apache2_startup来注册sapi module struct,最后调用php_module_startup初始化php,其中又会初始化Zend引擎,以及填充zend_module_struct中的treat_data成员(通过php_startup_sapi_content_types)等。
到这里,我们知道了Apache加载mod_php5模块的整个过程,可是这个过程与我们的饿SAPI有什么关系呢?mod_php5也定义了属于Apache的sapi_module_struct结构:
static sapi_module_struct apache2_sapi_module =<span> {"apache2handler"<span>,"Apache 2.0 Handler"<span>, php_apache2_startup, /* startup */<span>php_module_shutdown_wrapper, /* shutdown */<span> NULL, /* activate */<span>NULL, /* deactivate */<span> php_apache_sapi_ub_write, /* unbuffered write */<span>php_apache_sapi_flush, /* flush */<span>php_apache_sapi_get_stat, /* get uid */<span>php_apache_sapi_getenv, /* getenv */<span>php_error, /* error handler */<span> php_apache_sapi_header_handler, /* header handler */<span>php_apache_sapi_send_headers, /* send headers handler */<span>NULL, /* send header handler */<span> php_apache_sapi_read_post, /* read POST data */<span>php_apache_sapi_read_cookies, /* read Cookies */<span> php_apache_sapi_register_variables,php_apache_sapi_log_message, /* Log message */<span>php_apache_sapi_get_request_time, /* Request Time */<span>NULL, /* Child Terminate */<span> STANDARD_SAPI_MODULE_PROPERTIES};</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
这些方法都属于Apache服务器,以读取cookie为例,当我们在Apache服务器环境下,在PHP中调用读取Cookie时,最终获取的数据的位置是在激活SAPI时,它所调用的方法是read_cookie。
SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);
对于每一个服务器在加载时,我们都指定了sapi_module,而Apache的sapi_module是apache2_sapi_module。其中对应read_cookie的方法是php_apache_sapi_read_cookie函数。这也是定义SAPI结构的理由:统一接口,面向接口编程,具有更好的扩展性和适应性。
(2)Apache的运行过程
Apache的运行包括启动阶段和运行阶段,启动阶段Apache以root完成启动,整个过程处于单进程单线程的环境中,这个阶段包括配置文件解析、模块加载、系统资源初始化(例如日志文件、共享内存段、数据库连接等)等工作。
在运行阶段,Apache主要工作是处理用户的服务请求,在这个阶段Apache以普通用户运行。主要是安全性考虑,Apache对HTTP的请求可以分为连接、处理和断开连接三个大的阶段。
2、FastCGI
(1)cgi是通用网关接口(Common Gateway Intedface),它可以让一个客户端从网页浏览器向执行在Web服务器上的程序请求数据。CGI描述了客户端和这个程序之间传输数据的标准。CGI的一个目的是独立于任何语言,所以CGI可以用任何语言编写,只要这种语言具有标准输入、输出和环境变量。如PHP、perl、tcl等。
FastCGI是Web服务器和处理程序之间通信的一种协议,是CGI的一种改进方案,FastCGI像是一个常驻型的CGI,它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI对位人诟病的fork-and-execute模式)。正是因为它只是一个通信协议,它还支持分布式的运算,即FastCGI程序可以在网站服务器以外的主机上执行并且接受来自其他网站服务器的请求
FastCGI的整个流程是这样的:
Step1:Web Server启动时载入FastCGI进程管理器(IIS ISAPI或Apache Module)
Step2:FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自web server的连接
Step3:当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web Server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi
Step4:FastCGI子进程完成处理后将标准输出和错误新词从同一连接返回Web Server 当FastCGI子进程关闭连接时,请求便结束。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在Web Server中)的下一个连接。在CGI模式中,php-cgi在此便退出了。
(2)php中CGI实现
PHP的CGI实现了Fastcgi协议。是一个TCP或UDP协议的服务器接受来自Web服务器的请求,当启动时创建TCP/UDP协议的服务器的socket监听,并接受相关请求并进行处理。随后就进入了PHP的生命周期:模块初始化,sapi初始化,处理PHP请求,模块关闭,sapi关闭等 就构成了整个CGI的生命周期。
以TCP为例在,在TCP的服务端,一般会执行这样几个步骤:
1、调用socket函数创建一个TCP用的流式套接字;
2、调用bind函数将服务器的本地地址与前面创建的套接字绑定;
3、调用listen函数将新创建的套接字作为监听,等待客户端发起的连接,当客户端有多个连接连接到这个套接字时,可能需要排队处理;
4、服务器进程调用accept函数进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接;
5、当与客户端创建连接后,服务器调用read_stream函数读取客户端的请求;
6、处理完数据后,服务器调用write函数向客户端发送应答
TCP上客户-服务器事务的时序如图所示:
php的CGI实现从cgi_main.c文件的main函数开始,在main函数中调用了定义在fastcgi.c文件中的初始化,监听等函数。对比TCP的流程,我们查看php对TCP协议的实现,虽然php本身也实现了这些流程,但是在main函数中一些过程被封装成一个函数实现。对应TCP的操作流程,PHP首先会执行创建socket,绑定套接字,创建监听:
if<span> (bindpath) { fcgi_fd = fcgi_listen(bindpath, 128); // socket??2sfcgi_init? <span>? ...}</span></span>
在fastcgi.c文件中,fcig_listen函数主要用于创建、绑定socket并开始监听,它走完了前面所列TCP流程的前三个阶段,
if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 ||<span> ... bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 ||<span> listen(listen_socket, backlog) < 0<span>) { ... }</span></span></span>
当服务端初始化完成后,进程调用accept函数进入阻塞状态,在main函数中我们看到如下代码:
while<span> (parent) { do<span> { pid = fork(); // oÒ?<span>??J switch<span> (pid) { case 0: // ??J parent = 0<span>; /* don't catch our signals */<span> sigaction(SIGTERM, &old_term, 0); // ?â¯? sigaction(SIGQUIT, &old_quit, 0); // ???£? sigaction(SIGINT, &old_int, 0); // ??K?? break<span>; ... default<span>: /* Fine */<span> running++<span>; break<span>; } while (parent && (running <<span> children)); ... while (!fastcgi || fcgi_accept_request(&request) >= 0<span>) { SG(server_context) = (void *) &<span>request; init_request_info(TSRMLS_C); CG(interactive) = 0<span>; ... }</span></span></span>
如上的代码是一个生成子进程,并等待用户请求。在fcgi_accept_request函数中,程序会调用accept函数阻塞新创建的线程。当用户的请求到达时,fcgi_accept_request函数会判断是否处理用户的请求,其中会过滤某些连接请求,忽略受限制客户的请求,如果程序受理用户的请求,他将分析请求的信息,将相关的变量写到对应的变量中。其中在读取请求内容时调用了safe_read方法。如下所示:main()->fcgi_accept_request()->fcgi_read_request()->safe_read()
static inline ssize_t safe_read(fcgi_request *req, const void *<span>buf, size_t count){ size_t n = 0<span>; do<span> { ... // 省略 对win32的处理<span> ret = read(req->fd, ((char*)buf)+n, count-n); // 非win版本的读操作<span>D? ... // 省略 } while (n !=<span> count); }</span></span></span></span></span></span>
如上对应服务器端读取用户的请求数据。
在请求初始化完成,读取请求完毕后,就该处理请求的PHP文件了。假设此次请求为PHP_MODE_STANDARD则会调用php_execute_script执行PHP文件。在此函数中它先初始化此文件相关的一些内容,然后再调用zend_execute_scripts函数,对PHP文件进行词法分析和语法分析,生成中间代码,并执行zend_execute函数,从而执行这些中间代码。
在处理完用户的请求后,服务端将返回信息给客户端,此时在main函数中调用的是fcgi_finish_request(&request , 1);fcgi_finish_request函数定义在fasftcgi.c文件中。
在发送了请求的应答后,服务器端将会执行关闭操作,仅限于CGI本身的关闭,程序执行的是fcgi_close函数。