分类 原理 下的文章

PHP4.0整个脚本程序的核心大幅更动,让程序的执行速度,满足更快的要求。在最佳化之后的效率,已较传统CGI或者ASP等程序有更好的表现。而且还有更强的新功能、更丰富的函数库。无论您接不接受,PHP 都将在 Web CGI 的领域上,掀起巅覆性的革命。对于一位专业的Web Master 而言,它将也是必修课程之一。

PHP4.0这些优异的性能是PHP 脚本引擎重新设计产生的结果:引擎由 AndiGutmans 和 Zeev Suraski从底层全面重写。PHP4.0 脚本引擎 ——Zend 引擎,使用了一种更有效的编译——执行方式,而不是PHP 3.0 采用的执行 ——当解析时模型。

在 PHP4.0 版本,用 cloc 统计代码如下。

2023-03-24T04:24:32.png

这个时候的代码量已经接近 20W 行。

上面也提到了PHP4.0 最大的变化 Zend 引擎。

PHP执行这段代码会经过如下4个步骤(确切的来说,应该是PHP的语言引擎Zend)

  1. Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)
  2. Parsing, 将Tokens转换成简单而有意义的表达式
  3. Compilation, 将表达式编译成Opocdes
  4. Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。

1997年中PHP的发展也已经有了一些变化,他已经从Rasmus的宠物项目变成了更加有组织的团体项目。语法分析引擎也由Zeev Suraski 和 Andi Gutmans进行了重新改写,这个引擎构成了PHP3的基础。PHP/FI中的大部分通用代码都经过改写后引入了PHP3中。

目录结构上,PHP 越来越工程化。

  • serverapi: 抽离出了 PHPSAPIs。
  • win32:支持了 Windows
  • VarTree 被替换成了大名鼎鼎的 HashTable,到现在直接变成了恶臭的 PHP 八股文必考内容。
  • convertor 将 PHP/FI 的代码转换为 PHP3 代码的工具,其核心原理是 yacc 和 lex。
  • DL:扩展,在 php3 中开始支持扩展啦!

变量

hashtable 出现了,代替了原来的 VarTree

typedef struct hashtable {
    uint nTableSize;
    uint nHashSizeIndex;
    uint nNumOfElements;
    uint nNextFreeElement;
    uint(*pHashFunction) (char *arKey, uint nKeyLength);
    Bucket *pInternalPointer;    /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    void (*pDestructor) (void *pData);
    unsigned char persistent;
} HashTable;

在 PHP 中 HashTable 中非常重要,在 PHP/FI 中函数被保存在一个链表中,但是链表的时间复杂度是 O(N),在 PHP3 中,由于出现了 HashTable,其查询时间复杂度是 O(1),这样用 HashTable 代替链表存储函数也就成了必然。

PHPAPI int _php3_hash_add_or_update(HashTable *ht, char *arKey, uint nKeyLength, void *pData, uint nDataSize, void **pDest, int flag)

Rasmus Lerdorf 还做一个叫做 Form Interpreter 的项目,可以轻松地将SQL查询嵌入到网页中。它基本上是另一个CGI包装器,可以解析SQL查询,并能在这些查询的基础上轻松创建表单和表格,Rasmus Lerdorf 最终将两个项目合并到了一起,将其合并成为了一个程序,也就是 PHP/FI。现在它已经发展到了嵌入 HTML 文件的简单编程语言的程度,所以代码量飞起,直接涨了接近 20 多倍。

这个阶段的 PHP/FI 此时已经具备了 PHP 的雏形,PHP 完成从“个人主页工具”到 “个人主页建设工具包”。

2023-03-22T15:20:10.png

PHP脚本语言的语法在很多方面与C语言相似。它支持变量、数组、函数调用、不同的变量类型和大多数编写复杂的cgi程序时可能需要的东西。也就是说 PHP 编程语言正式出现了!

在PHP/FI 中引入了 yacc 和 lex 进行词法分析和语法解析,其原理其实还是通过解析 html 中的标记位,然后通过词法解析和语法解析成为C 语言代码进行执行,其核心代码在 lex.cparse.c 两个文件中。

我们先来看一下 PHP/FI 的使用,假设你有一个表单。

<FORM ACTION="/cgi-bin/php.cgi/~userid/display.html" METHOD=POST>。
<INPUT TYPE="text" name="name">
<INPUT TYPE="text" name="age"> <INPUT TYPE="text" name="age" >。
<INPUT TYPE="提交">
</FORM>

然后你的display.html文件可以包含这样的内容。

<?
    if($age>50);
        echo "Hi $name, you are ancient!<p>";
    elseif($age>30);
        echo "Hi $name, you are very old!<p>";
    else;
        echo "Hi $name.";
    endif;
>

就是这么简单! PHP/FI为表单中的每个输入字段自动创建一个变量。然后你可以在ACTION URL文件中使用这些变量。

上面的这段例子是来自于 PHP/FI 的文档,其文档地址在压缩包中也存在,目录是 doc。

2023-03-22T12:54:33.png

这个时候的 PHP/FI 还提供了 mysql 数据库:

2023-03-22T12:57:16.png

    <?
      $name = "bob";    
      $result = mysql($database,"select * from table where firstname='$name'");
      $num = mysql_numrows($result);
      echo "$num records found!<p>";
      $i=0;
      while($i<$num);
        echo mysql_result($result,$i,"lcase(fullname)");
        echo "<br>";
        echo mysql_result($result,$i,"address");
        echo "<br>";
        $i++;
      endwhile;
    >

其实到了这一步,我们的 PHP/FI 其实就已经可以完成目前我们的日常业务开发了,也就是 CURD。

同时我们可以通过 changelog 就可以看出,越来越多的人参与到了 PHP 的研发工作当中,也正是因此,PHP 逐渐壮大到了今天这个地步。

2023-03-13T18:17:10.png

目前 PHP/FI 的文档还可以访问 https://www.php.net/manual/phpfi2.php ,有兴趣的朋友可以去看一下。

源码解析

PHP 的本质其实还是 C,这也是大多数脚本语言的本质,我们可以把脚本语言的解释器理解为就是一个特殊的程序,解释器的将其中的各种语法解析、转换成可以执行的 C 代码而已。如果这么理解,那么对于脚本语言的理解就很容易了。

那么我们在定义一个变量,自定义一个函数的时候,其实解释器需要将这些变量、函数存储起来,然后在需要使用的地方进行调用。

接下来,我们就来看一下,PHP/FI中的自定义函数、变量是如何存储的。

PHP 的函数保存在哪里?

内置函数

在了解自定义函数的存储之前,我们先来看一下内置函数是如何进行处理的,在 PHP/FI 中为我们提供了大量的内置函数。

所有的内置函数都会被存储在一个变量名叫做 cmd_table 的 hashtable 中。

typedef struct _cmd_table_t {
    char *cmd;
    unsigned int token;
    void (*fnc)(void);
} cmd_table_t;

  • cmd 是函数的名称
  • token 是token 的编号
  • 最后一个成员是指向函数的指针

2023-03-22T15:29:12.png

自定义函数

2023-03-22T15:24:40.png

PHP 中的变量

支持的变量类型

支持三种类型的变量。长整数、双精度浮点和字符串。

#define    DNUMBER    258
#define    LNUMBER    259
#define    STRING    260

PHP 通过类型推断来判断变量具体的数据类型。

/*
 * Determines if 'str' is an integer, real number or a string
 *
 * Note that leading zeroes automatically force a STRING type
 */
int CheckType(char *str) {
    char *s;
    int type=LNUMBER;

    s = str;
    if(*s=='0' && *(s+1) && *(s+1)!='.') return(STRING);
    if(*s=='+' || *s=='-' || (*s>='0' && *s <='9') || *s=='.' ) {
        if(*s=='.') type=DNUMBER;
        s++;
        while(*s) {
            if(*s>='0' && *s<='9') { 
                s++; 
                continue; 
            }
            else if(*s=='.' && type==LNUMBER) { type=DNUMBER; s++; continue; }
            else return(STRING);
        }
    } else return(STRING);
    return(type);
} /* CheckType */

表达式的栈管理

/* Expression Stack */
typedef struct Stack {
    short type;
    char *strval;
    long intval;
    double douval;
    VarTree *var;    
    struct Stack *next;
    int flag;
} Stack;

变量的存储

变量的存储,在这个版本中的 PHP 存储变量使用的并不是 HashTable,而是使用的树。这说明 PHP 的设计也是在不断优化调整的过程。


/* Variable Tree */
typedef struct VarTree {
    short type;
    int count;
    char *name;
    char *strval;
    char *iname;
    long intval;
    double douval;
    int flag;
    int scope;                /* 0=local to frame, 4=global, 8=static to frame */
    struct VarTree *left;
    struct VarTree *right;
    struct VarTree *next;
    struct VarTree *prev;
    struct VarTree *lacc;
    struct VarTree *lastnode;
    int deleted;
    int allocated;
} VarTree;

这个时候的变量的值全部都是存在 tree 中的,根据 type 来判断变量的类型。

socpe 实现了变量的作用域。

参考

Personal Home Page Tools

1.0.8 是我们在官网上能够下载到的最古老的版本,虽然这个时候也叫 PHP,但是和现在的 PHP 意思是完全不同的,下面这张图是在源代码中的截图。

2023-03-13T08:49:59.png

PHP Tools - Personal Home Page Tools 个人网站工具集,严格意义上来说,这个时候的 PHP 只是一个 CGI 的工具集,它包含了一个非常简单的语法分析引擎,只能理解一些指定的宏和一些Home Page后台的常见功能,如留言本,计数器和一些其他的素材。

2023-03-22T13:01:01.png

Rasmus Lerdorf,就是这个家伙,在 1995 年仅用了一个下午就完成了 PHP 的第一个版本,对我们的生活其实产生了深远的影响。在 2004 年 PHP 更是成为了年度语言,并且在其后很长的一段时间内保持着前五,虽然近些年来 PHP 一直在被唱衰,甚至在国内大厂不断地用 Go 代替 PHP 的环境下,在 2023 年仍然没有跌出前十。

PHP 源代码下载,历史的归档源代码我们可以到 https://museum.php.net/ 这里进行下载。

2023-03-13T11:15:18.png

我们用 cloc 分析一下源代码发现,这个时候的 'PHP' 其实只有两千多行代码,其中真正用 C 语言实现的代码共计是 2455 行,等到了php2.0.1 的时候, C 语言的代码量就已经打到了进阶 58957 行。

2023-03-13T11:13:10.png

和现在的 PHP 代码行数完全不是一个量级的,这是因为在当年大多数都是静态页面,而开发 PHP 的目的就是为了统计网站的访问人数。

这个时候的 PHP 是如何执行的呢,又或者 CGI 程序是怎么回事?

用 Shell 也能写网站

这里不会跟朋友们扯太多空洞洞的理论,因为理论知识很容易就可以获取到,随便看几本书就可以了,或者去百度百科,都会有非常详细的介绍。

在上世纪 90 年代互联网刚刚起步,那个时候大家都是在用 html 搭建一个个人简历或者是公司简介,这个时代就是 web1.0 时代,很快 CGI 出现了,拜 CGI 之赐,网站不再只有固定不变的图形和文字,借由程序动态产生的网页可以让网站好象『活』了起来。小从简单的网页计数器,留言版,大至处理众多资料的搜寻引擎,可做线上实时交易的电子商务、网络下单等。CGI 简单、开放、跨平台、与程序语言独立的特性,使得编写网站应用程序变得很容易。

  1. 用户访问apache
  2. apache 调用 cgi程序,在这个过程中会将请求数据发送给 cgi 程序。
  3. cgi 输出执行业务逻辑并且输出页面。

按照这个原理,其实任何语言都可以编写 cgi 程序,下面我们用 shell 来编写一个打印 get 参数的 echo 服务。

#!/bin/bash

echo 'Status: 200 OK'
echo 'Content-Type: text/html; charset=utf-8'
echo 'Cache-Control: no-cache'
echo 'Pragma: no-cache'
echo
echo '<html>'
echo '<head>'
echo '<h1>echo query:</h1>'
echo '<p>'
echo  $QUERY_STRING
echo '</p>'
echo '</body>'
echo '</html>'
echo

2023-03-13T11:37:26.png

PHP 的使用案例

这个时候 PHP 提供了四个 cgi 程序:phpl.cgi, phpf.cgi, phplmon.cgi, phplview.cgi

其实这个时候的 PHP 就已经在简化 HTML 开发了,其中提供了一些内置函数可以帮我们进行模板替换。

<ul>
  <li>$today = <!--$today--></li>
  <li>$version = <!--$version--></li>
  <li>$_GET['name'] = <!--$name--></li>
  <li>system load:<pre><!--! w|head -n1 --></pre></li>
</ul>

<br>This page has been accessed a total of <a href="phplmon.cgi"><b>3</b></a> times now! <b>3</b> times today.<br>Page was last updated on Dec 22, 2021 at 7:43.</center></font>

2023-03-22T13:44:38.png

我们可以看到 <!--$today--> <!--$version--> 都已经被 phpl.cgi 程序给替换掉了。

PHP 源代码分析

PHP 的目录结构

这就是这个版本的 PHP 主要执行流程。我们来看一下代码目录

.
├── License
├── README
├── build
│   ├── phpf.cgi
│   ├── phpl.cgi
│   ├── phplmon.cgi
│   └── phplview.cgi
├── common.c
├── common.h
├── config.h
├── error.c
├── html_common.h
├── phpf.c
├── phpl.c
├── phplmon.c
├── phplview.c
├── post.c
├── post.h
├── subvar.c
├── version.h
├── wm.c
└── wm.h
  • common.c & common.h 一些公共函数的声明,我们看到有很多函数其实是没有.h 声明的,其实就放在了这里。
  • config.h : 相当于php.ini 用来存储 PHP 相关的配置信息
  • html_common.h && error.c :公共函数,这个名字起的让人很迷惑,里面有处理html_error 的处理函数,也有公用头部函数。
  • phpl.c
  • phplmon.c
  • phpview.c
  • phpf.c
  • subvar.c :变量替换函数
  • version.h :存放了 PHP 的版本信息,作者名字。
  • vm.c & vmc.h :字符串匹配函数,有趣的是这段代码并不是 Rasmus Lerdorf 写的,而是 COPY 过来的,并且他在代码中感谢了 Justin Slootsky。这说明无论是什么人都会有自己的短板。

在这里的 build 文件其实是不存在的,这是为了方便编译我自己就修改了 Makefile 文件。

PHPL.C 的执行流程

  1. 初始化变量
  2. 获取 Post 数据,如果有则初始化 Post 链表,如果没有则不执行
  3. 校验参数,也就是 CGI 程序发过来的 argv,校验不通过则渲染错误 html
  4. 渲染(其实就是标准输出)HTML,其中包括模板解析,替换等操作
  5. 释放Post 链表,结束程序

PHP 的配置文件

我们先来看配置文件 config.h :

define ROOTDIR "/usr/somewhere/user_id/public_html"
define HTML_DIR "html" 

#define LOGDIR "logs/"

#define ACCDIR "logs/"
#define NOACCESS "NoAccess.html"

这个时候的配置文件可以说是相当简洁了,只有四个参数,所以根本不需要独立的配置文件,我们只需要在编译之前进行修改就可以了。

  • ROOTDIR:项目根目录,在例子中,我们使用 phpl.cgi?var.html, var 就存储在根目录下,如果没有设置的话,回去找 apached 的默认配置,并且增加上 /public_html。
  • HTMLDIR:如果你不想选择使用 rootdir 也可以利用这个进行指定目录
  • LOGDIR:日志
  • ACCDIR:日志
  • NOACCESS:无法访问调转到指定页面。

PHP 的模板替换

在 phpl.c 中定义了两个宏。

#ifndef STARTSEP
#define STARTSEP "<!--"
#endif
#ifndef ENDSEP
#define ENDSEP   "-->"

这两宏的作用其实就是代表着模板替换的开始和结尾。

在这个时候,其实 PHP 有点类似于 Smarty 模板引擎,在 PHP 中预设了很多变量,当加载 HTML 的时候,如果发现 <!-- --> 模板标记,就会将其中的内容替换成已经赋值的变量

2023-03-22T13:47:28.png

我们可以看到截图中代码替换,其实就是检测到 $total 就替换成已经实现准备好的变量。

  • 在 632 和 637 行 其实就是去截取 <!-- --> 中的内容
  • 查找到对应的标记进行替换。

在 651 行中有 $today,我们以此举例。

static int todays_cnt;

在 phpl.c的最上方定义了静态变量 todays_cnt,其实该记录存储的就是今日的访问记录。

if(fgets(cntbuf,127,fp)) {
    s=strchr(cntbuf,' ');
    if(s) *s='\0';
        cnt=atoi(cntbuf);
        if(s) todays_cnt=atoi(s+1);
    } else {
    cnt=0;
    todays_cnt=0;
}

PHP 中的 Post 数据

用链表存储 Post 数据

2023-03-22T15:43:09.png

在 phpl.c 中第 88 行定义了一个全局静态变量, pdtop 其结构定义在 post.h 中。

typedef struct _postdata {
    char    var[32];
    char    val[128];
    struct _postdata *next;
} postdata;

这是一个链表,第一个 char 存储的事 post 的 key,第二个 char 存储的是值,在这里我们可以看出,在这个时候 PHP 最大只能接受 128 个字符。

获取 post 数据

在 cgi 协议中 http 请求中的数据是通过环境变量交给 cgi 程序的。

2023-03-22T15:37:12.png

Post 数据的释放

在我们申请内存后,必须对其进行释放,否则就会造成内存泄露,当 php 执行完成后会对已经申请过的内存地址进行释放。

2023-03-22T15:46:01.png

其实操作非常简单,就是判断第一个节点是否为空,如果不为空则证明有 post 的数据,然后开始遍历链表,释放掉所有申请的内存地址。

参考