2.PHP7内核剖析 --- SAPI

	SAPI 是 PHP 框架的接口层,它是进入 PHP 内部的入口。其中 Cli,Fpm SAPI 是完整的应用程序,它们有定义自己的 main 函数,
  方便我们从入口开始逐步调试分析 PHP 的处理,有其是 Cli,非常方便调试。

1.Cli
	cli 即命令行接口,用于在命令行下执行 php 脚本,就像 shell 那样。
	--enable-cli
	--disable-cli

	1.执行流程
		cli 是单进程模式,处理请求后就直接关闭了,生命周期先后经历了 module startup,request startup,execute script,request shutdown,
	  module shutdown,其执行流程比较简单,关键的处理过程如下:
	  main() -> php_cli_startup() -> do_cli() -> php_module_shutdown()

	  1.module startup
	  	cli SAPI 的 main 函数位于 sapi/cli/php_cli.c 中,执行时首先解析命令行参数,然后初始化 sapi_module_struct, 从结构名字可以看出,
	  它记录 SAPI 信息的主要结构,这个结构中有几个函数指针,它们是内核定义的操作接口的具体实现,用来告诉内核如何读取,输出数据。

	  在完成参数解析及 sapi_module_struct 的基本初始化后,接下来进入 module startup 阶段。
	  	if (sapi_module->startup(sapi_module) == FAILURE) { 
	  		...
	  	}
	  前面 cli_sapi_module 变量中定义的 startup 函数为 php_cli_start(),这个函数非常简单,直接调用了 php_module_startup()。
	  

	  2.request startup
	  在 module startup 阶段处理完成后,接下来进入请求初始化阶段。
	  	zend_first_try {
#ifndef PHP_CLI_WIN32_NO_CONSOLE
		if (sapi_module == &cli_sapi_module) {
#endif
			//处理请求
			exit_status = do_cli(argc, argv);
#ifndef PHP_CLI_WIN32_NO_CONSOLE
		} else {
			//内置web服务器的处理
			exit_status = do_cli_server(argc, argv);
		}
#endif
	} zend_end_try();
		do_cli() 将完成请求的处理,此函数一开始对使用到的命令参数进行解析,如果是一些查询的信息(如 -v,-m,-i) 则不需要经历php的生命周期,这里会单独
	  处理。
	  	php 脚本执行的输入形式有很多种,比如文件路径(filepath),文件句柄(FILE),文件描述符(fd)等,zend_file_handle 结构就是用来定义不同的输入形式
	  的,这样可以统一 php 执行函数的输入参数。
	  	cli 此处使用的是文件句柄,在 linux 环境下也就是调用 fopen() 打开一个文件,这样内核就可以直接读取 php 脚本代码了,当然也可以直接把文件路径
	  提供给内核。定义好请求的输入结构后将进行请求初始化操作,即 request startup 阶段,然后开始 php 脚本的执行操作。


	  3. execute script
	  if (interactive && cli_shell_callbacks.cli_shell_run) {
				exit_status = cli_shell_callbacks.cli_shell_run();
			} else {
				//执行 php 脚本
				php_execute_script(&file_handle);
				exit_status = EG(exit_status);
	  }


	  4.request shutdown 
	out:
	//完成脚本后进入 request shutdown 阶段
	if (request_started) {
		php_request_shutdown((void *) 0);
	}

	  5.module shutdown
	  	do_cli() 完成后回到 main() 函数中,进入 module shutdown 阶段,最后进程退出,这就是 cli 下执行一个脚本的生命周期。
	out:
	if (ini_path_override) {
		free(ini_path_override);
	}
	if (ini_entries) {
		free(ini_entries);
	}
	if (module_started) {
		php_module_shutdown();
	}
	if (sapi_started) {
		sapi_shutdown();
	}


2.内置 web 服务器
	cli SAPI 提供了一个内置的 web 服务器,这个内置的 web 服务器主要用于本地开发,不可用于线上环境。URI 请求会被发送到 php 所在的
  工作目录进行处理,除非你使用了 -t 参数来定义不同的目录。
  	如果请求未指定执行哪个 php 文件,则默认执行目录内的 index.php 或者 index.html。如果这2个都不存在,则返回 404。
  	当你在命令行启动这个脚本的 web server 时,如果指定了一个 php 文件,则这个文件会作为一个 '路由' 脚本,意味着每次请求都会先执行
  这个脚本。如果这个脚本返回false,那么直接返回请求的文件。


3.fpm
	fpm(fastcgi process manager)是 php fastcgi 运行模式的一个进程管理器,从它的定义可以看出,fpm 的核心功能是进程管理,那么用它
  来管理什么进程呢?
  	fastcgi 是 web 服务器(nginx等)和处理程序之间的一种通信协议,它是与 http 类似的一种应用层通信协议。
  	php 是一个脚本解析器,可以简单的把它理解为一个黑盒函数,输入的是php脚本,输出的是脚本的执行结果。除了在命令行下执行脚本。能不能让 php
  处理 http 请求呢?这种场景下就涉及网络处理,需要接收请求,解析协议,处理完成后再返回处理结果。在网络应用场景下,php 并没有像 golang 那样
  实现 http 网络库,而是实现了 fastcgi 协议,然后与 web 服务器配合实现 http 的处理,web 服务器来处理 http 请求,然后将解析的结果再通过
  fastcgi 协议转发给处理程序,处理程序处理完成后再将结果返回给 web 服务器,web 服务器再返回给用户。
  	php 实现了 fastcgi 协议的处理,但是并没有实现具体的网络处理,比较常用的网络处理模型有以下两种:
  	1.多进程模型:由一个主进程和多个子进程组成,主进程负责管理子进程,基本的网络事件由各个子进程处理,nginx 就是采用这种模型。
  	2.多线程模型:与多进程类似,只是它是线程粒度,这种模型通常会由主进程监听,接收请求,然后交由子线程处理。memcached 就是这种模式;有的也是
    采用多进程哪种模式---主线程只负责管理子线程不处理网络事件,各个子线程监听,接收,处理请求,memcached 使用 udp 协议时采用的就是这种模式。
    
    1.基本实现
    	fpm 是一种多进程模型,它由一个master进程和多个 worker 进程组成。master 进程启动时会创建一个 socket,但是不会接收,处理请求,而是
      由 fork 出的 worker 子进程完成请求的接收及处理。
      	master 进程的主要工作是管理 worker 进程,负责 fork 或者杀死 worker 进程,比如当请求比较多 worker 进程处理不过来时,master 进程
      会尝试 fork 新的 worker 进程进行处理,而当空闲 worker 进程比较多时则会杀掉部分子进程,避免占用,浪费资源。
      	worker 进程的主要工作仍然是处理请求,每个 worker 进程会竞争的 accept 请求,接收成功后解析 fastcgi,然后执行相应的脚本,处理完成后
      关闭请求,继续等待新的请求,这就是一个worker的生命周期。从 worker 进程的生命周期可以看到:一个 worker 进程只能处理一个请求,只有将一个
      请求处理完成后才能处理下一个请求。这与 nginx 的事件模型有很大区别,nginx 的子进程通过 epoll 管理套接字,如果一个请求数据还未发送完成
      则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。fpm 的这种处理模式大大简化了php的资源管理,使得在 
      fpm 模式下不需要考虑并发导致的资源冲突。
      	master 进程与 worker 进程之间不会直接进行通信,master 通过共享内存获得 worker 进程的信息,比如 worker 进程当前状态,已处理请求数等,
      master 进程通过发送信号的方式杀掉 worker进程。
      	fpm 可以同时监听多个端口,每个端口对应一个 worker pool,每个 pool 下面对应多个 worker 进程,类似 nginx中 server的概念,这些不同的
      pool 的 worker 进程仍由一个 master 管理。worker poll 的结构为 fpm_worker_pool_s,pool 之间构成一个链表。

struct fpm_worker_pool_s {
	//指向下一个pool
	struct fpm_worker_pool_s *next;
	//php-fpm.conf 配置:pm,max_children,start_servers
	struct fpm_worker_pool_config_s *config;
	char *user, *home;									/* for setting env USER and HOME */
	enum fpm_address_domain listen_address_domain;
	//监听的套接字
	int listening_socket;
	int set_uid, set_gid;								/* config uid and gid */
	int socket_uid, socket_gid, socket_mode;

	/* runtime */
	//当前 pool 的 worker 链表,每个 worker 对应一个 fpm_child_s 结构
	struct fpm_child_s *children;
	//当前 pool 的 worker 运行总数
	int running_children;
	int idle_spawn_rate;
	int warn_max_children;
#if 0
	int warn_lq;
#endif
	//记录 worker 的运行信息,比如空闲,忙碌的worker数
	struct fpm_scoreboard_s *scoreboard;
	int log_fd;
	char **limit_extensions;

	/* for ondemand PM */
	struct fpm_event_s *ondemand_event;
	int socket_event_set;

#ifdef HAVE_FPM_ACL
	void *socket_acl;
#endif
};

	在 php-fpm.conf 中通过 [pool name] 声明一个 worker pool, 每个 pool 各自配置监听的地址,进程管理方式,worker 进程数等。
	[web1]
	listen = 127.0.0.1:9000
	[web2]
	listen = 127.0.0.1:9001


	2.fpm 的初始化
		fpm 的 main 函数位于文件 /sapi/fpm/fpm/fpm_main.c 中。fpm 在启动后首先会进行 sapi 的注册操作;接着会进入 php 的生命周期
	  module startup 阶段,在这个阶段会调用各个扩展定义的 MINT 钩子函数。然后会进行一系列的初始化操作,最后 master,worker 进程进入
	  不同的处理环节。

	3.几个关键性操作
		1.fpm_conf_init_main()
		  解析 php-fpm.conf 配置文件,为每个 worker pool 分配一个 fpm_worker_pool_s 结构,
		各个 worker pool 的配置解析保存在 fpm_worker_pool_s->conf 中。
struct fpm_worker_pool_config_s {
	char *name; //pool 名字,即配置: [pool name]
	char *prefix;
	char *user; //fpm 的启动用户,配置 : user
	char *group; // 配置 : group
	char *listen_address; //监听的地址,配置 : listen
	int listen_backlog;
	/* Using chown */
	char *listen_owner;
	char *listen_group;
	char *listen_mode;
	char *listen_allowed_clients;
	int process_priority;
	int process_dumpable;
	int pm; //进程模型 : static,dynamic,ondemand
	int pm_max_children; // 最大 worker 进程数
	int pm_start_servers; //启动时初始化的 worker数
	int pm_min_spare_servers; //最小空闲 worker 数
	int pm_max_spare_servers; //最大空闲 worker 数
	int pm_process_idle_timeout; //worker 空闲时间
	int pm_max_requests; //worker 处理的最多请求数,超过这个值 worker 将被 kill
	char *pm_status_path;
	char *ping_path;
	char *ping_response;
	char *access_log;
	char *access_format;
	char *slowlog;
	int request_slowlog_timeout;
	int request_slowlog_trace_depth;
	int request_terminate_timeout;
	int rlimit_files;
	int rlimit_core;
	char *chroot;
	char *chdir;
	int catch_workers_output;
	int clear_env;
	char *security_limit_extensions;
	struct key_value_s *env;
	struct key_value_s *php_admin_values;
	struct key_value_s *php_values;
#ifdef HAVE_APPARMOR
	char *apparmor_hat;
#endif
#ifdef HAVE_FPM_ACL
	/* Using Posix ACL */
	char *listen_acl_users;
	char *listen_acl_groups;
#endif
};
		2.fpm_scoreboard_init_main()
			分配用于记录 worker 进程运行信息的结构,此结构分配在共享内存上,master 正是通过这种方式获取 worker 的运行信息。分配时按照 worker pool
		  的最大 worker 进程数进行分配,每个 worker pool 分配一个 fpm_scoreboard_s 结构。pool 下面的每个 worker 进程分配一个 fpm_scoreboard_proc_s
		  结构,这些结构的地址保存在 fpm_scoreboard_s->procs 数组中,其下标保存在 fpm_child_s->scoreboard_i 中。

		3.fpm_signals_init_main()
			这一步会通过 socketpair() 创建一个管道,这个管道并不是用于 master 与 worker 进程通信的,它只在 master 进程中使用。同时,设置 master 的信号
		  处理函数为 sig_handler(),当 master 接收到 SIGTERM, SIGINT,SIGUSR1,SRGUSR2 等这些信号时将调用 sig_handler() 进行处理,此函数中会把接收到的
		  信号写入 fpm_singals_init_main() 中创建的管道。

		4.fpm_sockets_init_main()
			创建每个 worker pool 的 socket 套接字,启动后 worker 将监听此 socket 接收请求。

		5.fpm_server_init_main()
			启动 master 的事件管理,fpm 实现了一个事件管理器用于管理 IO,定时事件,其中IO事件根据不同平台选择 kqueue,epoll,poll,select 等管理,定时事件就是
		  定时器,一定事件后启动某个事件。

    fpm_init() 中主要的处理就是上面介绍的几个 init 过程,在完成这些初始化操作后接下来最关键的就是 fpm_run()操作了,此环节将 fork 子进程,启动进程管理器,执行后
  master 进程将不会返回这个函数,只有各 worker 进程返回,也就是说 main() 函数中调用 fpm_run() 之后的操作均是 worker 进程的。
  	由此 fpm_run() 的处理来看,在 fork 出 worker 进程后,子进程将返回 main 中,而 master 进程进入 fpm_event_loop(),这是一个事件循环。

  	4.worker --- 请求处理
  		worker 进程返回 main() 函数后将继续向下执行,此后的流程是 worker 进程不断的 accept 请求,有请求到达后将读取并解析 fastcgi 协议的数据,解析完成后开始
  	  执行 php 脚本,执行完成后关闭请求,继续监听等待新的请求到达。
  	  (1)等待请求
  	  	worker 进程阻塞在 fcgi_accept_request() 中等待请求.
  	  (2)解析请求
  	  	fastcgi 请求到达后被 worker 接收,然后开始接收并解析请求数据,直到 request 数据完全到达。
  	  (3)请求初始化
  	  	执行 php_request_startup(),此阶段会调用每个扩展的 PHP_RINIT_FUNCTION()
  	  (4)执行 php 脚本
  	  	由 php_execute_script() 完成 php 脚本的编译,执行操作。
  	  (5)关闭请求
  	  	请求完成后执行 php_request_shutdown(),此阶段会调用每个扩展的 PHP_RSHUTDOWN_FUNCTION(),然后进入步骤(1)等待下一个请求。

  	  	worker 进程的 fpm_scoreboard_proc_s->request_stage 被用于记录 worker 当前所处的阶段,master 进程也是通过这个值来获取 worker 的状态,一次请求过程中
  	 这个值将先后被置为以下的值:
  	 	1.FPM_REQUEST_ACCEPTING : 等待请求阶段
  	 	2.FPM_REQUEST_READING_HEADERS : 读取 fastcgi 请求的 header 阶段
  	 	3.FPM_REQUEST_INFO : 获取请求信息阶段,此阶段是将请求的 method,query string,request uri 等信息保存到各 worker 进程的 fpm_scoreboard_proc_s 结构
  	    中,此操作需要加锁,因为 master 进程也会操作此结构。
  	    4.FPM_REQUEST_EXECUTING : 执行 PHP 脚本阶段
  	    5.FPM_REQUEST_END : 没有使用
  	    6.FPM_REQUEST_FINISHED : 请求处理完成
  	    通过 gdb 可以清楚的追踪到一个请求的完成处理流程,为了方便找到处理的 worker 进程,可以通过将 worker 数设置为 1,attach 接管 worker 进程后通过 bt 可以看到,
  	  worker 进程阻塞在 fcgi_accept_request() 上。

  	5.master --- 进程管理
  		master 在调用 fpm_run() 后不再返回,而是进入一个事件循环,此后 master 始终围绕着几个事件进行处理。fpm 3种不同的进程管理方式,具体要使用哪个模式可以在 conf
  	  中指定 pm,例如 pm = dynamic。
  	  1.静态模式(static) : 
  	  	这种方式比较简单,在启动时 master 根据 pm.max_children 配置的 fork 出相应数量的 worker 进程,也就是 worker 进程数是固定不变的。

  	  2.动态模式(dynamic) : 
  	  	这种模式比较常用,在 fpm 启动的时候会工具 pm.start_servers 配置初始化一定数量的 worker。运行期间如果 master 发现空闲的 worker 数低于 pm.min_spare_servers
  	  配置(表示请求数比较多,worker 进程处理不过来了)则会 fork worker 进程,但总的 worker 数不能超过 pm.max_children。如果 master 进程发现空闲的 worker 超过了
  	  pm.max_spare_servers(表示闲的 worker 太多了)则会杀掉一些 worker,避免占用太多资源,master 通过4个值动态来控制 worker 数量。

  	  3.按需模式(ondemand) :
  	  	这种模式很像传统的 cgi,在启动时不分配 worker 进程,等到有请求了后再通知 master 进程 fork worker 进程,也就是来了请求后再 fork 子进程进行处理。总的 worker 数
  	  不超过 pm.max_children,处理完成后 worker 进程不会立即退出,当空闲时间超过 pm.process_idle_timeout 后退出。

  	  	master 进程进入 fpm_event_loop() 事件循环,在这个方法中 master 将循环处理 master 注册的几个 IO 及定时器,当有事件触发时将回调具体的 handler 进行处理。
  	  	void fpm_event_loop(int err)
  	  	{
  	  		//...
  	  	}
  	  	master 注册的几个重要事件:
  	  	1.信号事件
  	  		fpm_init() 阶段时分配了一个 sp 管道,当 master 接收到 SIGTERM, SIGINT,SIGUSR1,SRGUSR2 等这些信号时会把对应的信号写到 sp[1] 管道中,那么谁来接收
  	  	  这个信号呢? master 在 fpm_event_loop() 中注册了此管道可读的事件。
  	  	  	fpm_signals_get_fd() 返回的是 sp[0],所以在这里注册了一个 sp[0] 可读的事件,当 sp[0] 可读时将回调fpm_got_signal() 进行处理。总体来看,当向 master
  	  	  发送信号时,首先信号 handler 会把信号通知发送到 sp[1]管道,然后触发 sp[0] 可读事件,回调 fpm_got_signal()进行处理。
  	  	    fpm_got_signal() 根据不同的信号进行相应的处理,不同的信号值对应的处理逻辑。
  	  	    1.SIGINT/SIGTERM/SIGQUIT : 退出 fpm,在 master 接收到退出信号后将所有的 worker 进程发送退出信号,通知 worker 退出,然后 master 退出。
  	  	    2.SIGUSR1 : 重新加载日志文件,生产环境中通常会根据时间对日志进行切割,切割后生成一个新的日志文件,如果进程不重新加载文件,则无法继续写入日志,这时就需要
  	  	    向 master 发送一个 USR1 信号,告诉 master 进程重新加载日志文件。
  	  	    3.SIGUSR2 : 重启fpm,首先 master 也是会向所有的 worker 进程发送退出信号,等全部 worker 成功退出后,master 会调用 execvp() 重新启动一个新的fpm,最后
  	  	    旧的 master 退出。
  	  	    4.这个信号是子进程退出时操作系统发送给父进程的,子进程退出时,操作系统将子进程设置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程
  	  	    查询子进程的退出状态,只有当父进程调用 wait()或者waitpid()函数查询子进程退出状态后子进程才终止,fpm中当 worker 进程因为异常原因(比如 coredump)退出而非master
  	  	    主动杀掉时,master 将收到此信号,这时父进程将调用 waitpid()保证 worker退出,然后检查是不是需要重新 fork 新的 worker。
  	  	    	通过 gdb 进行调试,attach 命令接管 master 进程,然后向 master 进程发送不同的信号。

  	  	2.进程检查定时器
  	  		fpm_event_loop() 中调用 fpm_pctl_perform_idle_server_maintenance_heartbeat() 注册了一个定时器:
  	  		这个定时器是用来定期检查 worker 进程数的,master 通过这个定时器每隔一段时间检查 worker 的数量,根据不同的策略(static/dynamic/ondemand)的配置决定
  	  	  是否fork或者kill进程。

void fpm_pctl_perform_idle_server_maintenance_heartbeat(struct fpm_event_s *ev, short which, void *arg) /* {{{ */
{
	static struct fpm_event_s heartbeat;
	struct timeval now;

	if (fpm_globals.parent_pid != getpid()) {
		return; /* sanity check */
	}

	//定时器触发时回调的逻辑
	if (which == FPM_EV_TIMEOUT) {//确认是一个定时器
		fpm_clock_get(&now);
		//fpm_pctl_can_spawn_children()检查 master 当前状态是否正常,比如 master 收到了退出信号,这个时候就不需要再进行fork,kill操作了
		if (fpm_pctl_can_spawn_children()) {
			//具体的 worker 检查逻辑
			fpm_pctl_perform_idle_server_maintenance(&now);

			/* if it's a child, stop here without creating the next event
			 * this event is reserved to the master process
			 */
			if (fpm_globals.is_child) {
				return;
			}
		}
		return;
	}

	/* first call without setting which to initialize the timer */
	//此后的逻辑只在注册时执行一次,以后不会再到这里了
	//设置定时器,回调函数也是本函数
	fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST, &fpm_pctl_perform_idle_server_maintenance_heartbeat, NULL);
	//注册定时器,第二个参数是每个多少 ms(1000ms) 触发
	fpm_event_add(&heartbeat, FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT);
}
		static 静态模式不会动态的管理 worker 进程,所以 fpm_pctl_perform_idle_server_maintenance() 主要是针对 dynamic,ondemand 两种模式的。检查的过程就是遍历
	  所有的 worker pool,根据不同的策略进行配置不同的处理,具体的 worker 控制策略在一开始已经介绍过了,这里就是具体的实现。
	    检查每个 worker pool 时,首先根据此 pool 下所有的 worker 的状态计算出处于空闲,忙碌状态的 worker 数,这个就是根据每个 worker 进程 
	  fpm_scoreboard_proc_s->request_stage 状态判断的。

	    根据不同的 pm 模式分别处理,如果是按需模式(ondemand),则会判断空闲时间最久的那个 worker 的空闲时间是否达到了 pm.process_idle_timeout 阈值,到了则kill该worker。
	    从处理过程可以看到,ondemand 模式下每次只 kill 空闲时间最久的那个 worker,如果有多个 worker 都达到了 pm.process_idle_timeout 阈值,则需要等到下一个周期进行处理。
	    如果是动态模式(dynamic)模式,首先检查空闲的 worker 数是否超过了 pm.max_spare_servers 配置的数量,若超过了则杀掉空闲时间最久的那个,同样,这里的每个周期只会kill
	  一个 worker;接着检查空闲的 worker 数是否低于 pm.min_spare_servers,如果低于则需要 fork 更多的 worker 进行补充,但总的 worker 不能超过 pm.max_childen.在 fork
	  的过程中通过 idle_spawn_rate 这个值控制频率,默认一个周期内只会 fork 一个进程,但是如果发现几个周期内一直在 fork,咋说明 worker 数远远不足,这时会把 fork数翻一倍,
	  上限是 32。

  	  	3.执行超时检查定时器
  	  		php-fpm.conf 中有一个 request_terminate_timeout 的配置项,如果 worker 处理一个请求的总时长超过了这个值,那么 master 会向此 worker 进程发送 kill -TERM
  	  	  信号杀掉 worker 进程,此配置的单位为秒,默认值为0,表示关闭此机制。
  	  	  	这个功能也是通过定时器实现的,master 每个一段时间检查所有处理中的 worker,如果发现其处理时间达到阈值则杀掉这个 worker。另外, fpm记录的slow log也是通过
  	  	  这个定时器完成的。

  	  	  fpm_pctl_heartbeat 定义了定时器,每个 fpm_globals.heartbeat 毫秒触发一次,这个时间是根据 pm.request_terminate_timeout 计算得到的
  	  	回调函数也是自己。
  	  		定时器触发将调用 fpm_pctl_check_request_timeout() 进行处理,处理逻辑比较简单,遍历 worker pool,然后逐个判断 worker 处理中的请求是否超时,此判断根据请求
  	  	开始时间 fpm_scoreboard_proc_s->accepted 完成,如果超时则 kill。

  	  	还有一个 IO 事件没有提到,这个事件只有在 ondemand 模式下使用。因为 ondemand 模式下 fpm 启动时是不会创建 worker 的,有请求时才会生成子进程,所以有请求到达时
  	  需要同时 master 进程进行 fork。这个事件是在fpm_children_create_initial() 时注册的,事件处理函数为 fpm_pctl_on_socket_accept()。

3.Enbed
	cli,fpm 都是完整的应用程序,它们有自己定义的 main 函数,其应用场景是固定的,另外2个没有介绍的 SAPI(litespeed,apache2handler) 也是配合其他应用使用。
  如果我们在自己的第三方程序中也想使用php,应该怎么办?比如开发一个路由器,想在其中嵌入php来实现web配置的功能,难道要重新开发一个sapi,然后提供给第三方使用?
  php提供了一个用于这类应用场景的sapi,那就是 embed,它在编译后就是普通的库文件(可以选择编译为 静态库,共享库),我们可以在其他c/c++应用中调用php提供的api,
  甚至可以提供给其他语言使用。
  	编译php时通过 --enable-embed=[shared|static]指定库类型,默认是共享库。编译完成后再 php 按照的位置 /lib 目录下看到生成的库文件,同时在 /includ/php/sapi
  目录下会生成一个存放 embed 头文件的目录。

  	1.实现
  		embed 实现它的逻辑非常简单,只是把 php 生命周期的几个处理函数进行了封装,它对外提供了2个 api.

  		1.php_embed_init()
  			这个接口主要进行 php 框架的初始化操作,比如启动 TSRM,初始化 SAPI,初始化信号处理,另外它还完成了两个非常重要的操作,那就是 php_module_startup(),
  		  php_request_startup().
  		  	在第三方应用中嵌入 php 时首先需要调用这个接口,然后就可以使用 PHP/Zend 提供的 api 完成 php 脚本的执行了。

  		2.php_embed_shutdown()
  			此接口与 php_embed_init() 对应,主要完成 php 框架的关闭收尾工作,包括 request shutdown,module shutdown 两个阶段的操作。

    2.使用
    	如在在一个 c 程序中嵌入 php,使用的是共享库。这个例子中,我们自己定义的c程序将调用 php 完成一个普通 php 脚本的执行.
#include <php/sapi/embed/php_embed.h>

int main(int argc, char **argv)
{
    zend_file_handle file_handler;
    //php 框架初始化
    php_embed_init(argc, argv);
    file_handler.type = ZEND_HANDLE_FILENAME;
    file_handler.filename = "call.php";
    
    //execute php script
    php_execute_script(&file_handler);
    
    //关闭 php 框架
    php_embed_shutdown();
    
    return 0;
}
//call.php
function my_func($a,$b) 
{
    return $a + $b;
}

echo my_func(100,200);

	定义完成后编译,编译时需要加上 -I 指定 php 的头文件目录,包括 PHP,ZEND,SAPI,TSRM 的;通过 -l及 -L 指定共享库的目录及名称。另外,还
  需要通过 -rpath 指定运行时共享库的位置。假如 php 安装路径为 /usr/local/php7,则编译参数为 :
  gcc -o my_php test.c \
  -I /usr/local/php7/include/php \
  -I /usr/local/php7/include/php/include \
  -I /usr/local/php7/include/php/main \
  -I /usr/local/php7/include/php/TSRM \
  -I /usr/local/php7/include/php/Zend \
  -L /usr/local/php7/lib \
  -lphp7 \
  -Wl, --rpath /usr/local/php7/lib

  编译后执行:
  ./my_php
  300

    gcc 编译参数 -l,-L 只是在编译时指定了库文件,与执行时无关,在执行时默认会去 /usr/lib 目录下搜索 .so,另外可以通过环境变量 LD_LIBRARY_PATH
指定搜索目录,也可以将搜索目录加到 /etc/ld.so.conf 配置中。
	除了上面方式,还可以在编译时通过 --rpath 指定,其语法需要遵循 '-Wl,...',其中 -Wl 就是告诉 gcc,后面的内容是传递给 linker 的 option,比如
-Wl,--rpath /usr/local/php7/lib 。它的优先级要在 LD_LIBRARY_PATH, /etc/ld.so.conf 之上。

 

SAPI : 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值