雖然大部分php工程師都不需要知道php的C代碼核心是如何運作的,有些人可能知道有個dl()函數.或者使用過一些第三方的類庫,這些正是本文的重點之一.
希望本文能對那些想把php帶向更寬的邊界的工程師有所幫助.
先看一個php請求的運行流程:
浏覽器用戶--->web服務器(apache,nginx)--->Zend引擎從文件系統讀取php代碼文件--->Zend解釋器工作
--->執行解釋後的代碼-->Zend引擎注冊的函數接口-->內置模塊或者各個需要的外部模塊擴展-->數據庫memcache等後端資源
其中
Zend引擎注冊的函數接口 就是php工程師經常接觸的各種php函數.
外部模塊擴展 就是php編譯的各個so文件(linux)或者dll文件(windwos).
執行解釋後的代碼 浏覽器的內容就是從這裡返回的.
內置模塊 也就是php每次啟動的時候會攜帶啟動的模塊.
從上面的流程圖可以知道php可以從3個點進行擴展.1 外部模塊擴展 2 Zend引擎 3 內置模塊,下面我將一一討論.
外部模塊擴展.
如果你使用過dl()你就接觸過這些外部的擴展模塊.外部的擴展模塊文件就放在你的硬盤裡,他在php腳本運行時被加載到內存中,而且只有需要的時候才被加載.
當此次的腳本運行完之後他就會被內存釋放掉,總的來說它運行的慢但是不占資源.不需要你重新編譯一個php.
內置模塊
雖然也是Zend引擎之外的模塊,但是與外部模塊擴展有些不同,他已經在php裡邊了.他會使得你編譯的php體積變大,如果有改變,必須重新編譯php才行.內置模塊會使得
php內存變大,但是調用起來也會更加的快速.在我們的測試中一些模塊運行在內置模式會有30%以上的速度提升.
Zend引擎
首先,我絕對不建議你去修改Zend引擎.一些php語言的特性只要在Zend引擎中才能夠實現.比如你要修改數組關鍵字的名字,你可以在這裡實現.
在你下載的php源代碼裡,以zend開頭的都是zend引擎的相關代碼.
一般php源碼目錄結構類似下面:
main php的主要源代碼,
ext php的擴展
sapi 與不同服務器的api交互層代碼
zend zend引擎部分
TSRM 線程安全相關模塊代碼
下面以一個簡單的模塊為例子說明PHP如何擴展:
首先php的代碼有自己的一套標准,你需要遵守,不然可能會導致你的模塊無法釋放變量或者其他的問題,這些標准包括 宏定義,變量聲明等.你可以到官方浏覽詳細的說明.
/* 擴展的標准頭 */
#include "php.h"
/* 聲明這個so被導出的函數 */
ZEND_FUNCTION(helloworld_module);
/* Zend引擎注冊的函數接口 */
zend_function_entry helloworldmod_interfaces[] =
{
ZEND_FE(helloworld_module, NULL)
{NULL, NULL, NULL}
};
/* 這是這個模塊的聲明實體,它的值對模塊編譯的時候起實際作用 */
zend_module_entry helloworldmod_module_entry =
{
STANDARD_MODULE_HEADER,
"Hello world",
helloworldmod_interfaces,
NULL,
NULL,
NULL,
NULL,
NULL,
NO_VERSION_YET,
STANDARD_MODULE_PROPERTIES
};
/* 向zend引擎聲明一個備案,可以說明 helloworldmod_module_entry屬於helloworldmod.so這個動態庫*/
#if COMPILE_DL_helloworld_module
ZEND_GET_MODULE(helloworldmod)
#endif
/* 這就是我們新增的函數的真正代碼 */
ZEND_FUNCTION(helloworld_module)
{
return "Hello,world";
}
我們可以根據其他擴展的config.m4文件來修改成我們的必要編譯配置信息。這裡這個模塊幾乎是一個空的config.m4文件就行,
然後利用phpize來生成configure文件然後是 ./configure && make && make install執行就能編譯一份我們的動態庫
test.php
<?php
echo helloworld_module();
?>
輸出:
"Hello,world"
完成了PHP擴展,我們已經深入了php的c代碼內部,但是在一些情況下,這還不夠。我們需要深入到c語言調用c庫的過程當中,在linux下面一個很給力的工具是LD_PRELOAD環境變量
LD_PRELOAD環境變量是編譯器找到程序中所引用的函數或全局變量所存在的位置的一個過濾器,比如在php的c代碼裡調用一個開始網絡連接的方法connect,事實上就是通過動態鏈接
去尋找linux的c庫的函數connect,這些鏈接文件一般放在lib下面,這也就為我們影響php的代碼執行提供了一個切入點。因為php程序在動態載入lib下面的函數connect之前會檢查LD_PRELOAD
提供的動態庫裡有沒有這個connect函數,我們可以在這裡對php的行為進行干涉。
下面以一個簡單的過濾網絡訪問的例子說明如何實現:
先是一個准備作為LD_PRELOAD環境變量的值的so文件的代碼。
lp_demo.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <dlfcn.h>
//定義我們自己的connect函數
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t
addrlen){
static int (*connect_linuxc)(int, const struct sockaddr*, socklen_t)=NULL;
unsigned char *ip_char;
//利用 lsym的RTLD_NEXT選項繞過LD_PRELOAD環境變量的connect方法找到c庫的函數
if (!connect_linuxc) connect_linuxc=dlsym(RTLD_NEXT,"connect");
ip_char=serv_addr->sa_data;
ip_char+=2;
//192.168.2.3 找到了
if ((*ip_char==192)&&(*(ip_char+1)==168)&&(*(ip_char+2)==2)&&(*(ip_char+3)==3)) {
//簡單返回一個權限錯誤的代碼
return EACCES;
}
// 調用真正的connect方法
return connect_linuxc(sockfd,serv_addr,addrlen);
}
編譯成so文件
$ gcc -o lp_demo.so -shared lp_demo.c -ldl
測試文件 test.php
<?php
file_get_contents("http://192.168.2.3/");
?>
使用方法
LD_PRELOAD=lp_demo.so php test.php
這樣他將不可能訪問的到192.168.2.3這種我們內部的網址。起到一個很好的沙盒作用。
除此之外我們還可以利用fwrite fopen等函數將php對文件系統的讀寫操作轉移到mencache,nosql之類的後端資源當中。
最後,即使我們已經深入了c庫的內部,也不意味著我們走到了最底層,在c庫下面,還有一堆sys_開頭的函數,他們才是內核空間裡的真正函數,在此就不在探討了。