在前面大致预览了常用变量的结构之后,我们今天来仔细的剖析一下字符串的具体实现。

一、字符串的结构

struct _zend_string {
zend_refcounted_h gc; /* 字符串类别及引用计数 */
zend_ulong h; /* 字符串的哈希值 */
size_t len; /* 字符串的长度 */
char val[1]; /* 柔性数组,字符串存储位置 */
};

zend_refcounted_h对应的结构体:

typedef struct _zend_refcounted_h {
uint32_t refcount; /* 引用计数 */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* 字符串的类型 */
uint16_t gc_info /* 垃圾回收信息 */
)
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;

下面我们来了解一下具体每个成员的作用:

  • gc:就是_zend_refcounted_h结构体,主要作用是引用计数以及标记变量的类别。
  • h:字符串的哈希值,在字符串被用来当数组的key时才初始化,这样如果同一个字符串被多次用来做key,就不会重复计算了。
  • val:这里的char[1]并不意味着只存储1位,char[1]被称为柔性数组,下面来了解一下PHP在字符串内存分配时做了什么。
static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent)
{
zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
......
}

宏替换后:

static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent)
{
zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(XtOffsetOf(zend_string, val) + len + 1), persistent);
......
}

示例中的代码XtOffsetOf(zend_string, val)表示计算出zend_string结构体的大小,而len就是要分配字符串的长度,最后的+1是留给结束字符\0的。也就是说,分配内存时不仅仅分配结构体大小的内存,还要顾及到长度不可控的val,这样不仅柔性的分配了内存,还使它与其他成员存储在同一块连续的空间中,在分配、释放内存时可以把struct统一处理。

  • len:字符串的长度,避免重复计算浪费时间,典型的空间换时间做法。

二、字符串的二进制安全

学习过C语言的应该知道,字符串中除了最后一个字符外不允许含有\0,否则会被认为是字符串的结束字符,这就导致了C语言的字符串有很多的限制,比如不存储图片、文件等二进制数据。但是PHP就没有这样的限制,它的字符串可以存储二进制数据,并不会出现任何报错,而PHP的这种能力就叫做字符串的二进制安全。

C语言代码如下:

main() {
char a[] = "aaa\0b"; /* 含有\0的字符串 */
printf("%d\n", strlen(a)); /* 长度为3,\0后的b被忽略 */
}

PHP代码:

<?php
$a = "aaa\0b";
echo strlen($a); //输出5
?>

但是PHP不是C语言写的吗?为什么PHP不会报错?我们再来回顾一下zend_string结构体,还记得成员变量len吗?它是实现二进制安全的关键,我们不需要像C一样通过\0来判定字符串是否被读取完成,而是通过长度len来判断,这样就保证了字符串的二进制安全。

三、zend_string API

在了解了zend_string结构之后,我们来了解一下用来操作zend_string的函数集合。

函数 作用
zend_interned_strings_init 初始化内部字符串存储哈希表,并把PHP的关键字等字符串信息写进去
zend_new_interned_string 把一个zend_string写入CG(interned_strings)哈希表中
zend_interned_strings_snapshot 将CG(interned_strings)哈希表中的字符串标记为永久字符串,这里标记的只有PHP关键字、内部函数名、内部方法名等
zend_interned_strings_restore 销毁CG(interned_strings)哈希表中类型为非永久字符串的值,在php_request_shutdown阶段释放
zend_interned_strings_dtor 销毁整个CG(interned_strings)哈希表,在php_module_shutdown阶段释放
zend_string_hash_val 得到字符串的哈希值,没有则实时计算
zend_string_forget_hash_val 将字符串的哈希值置为0
zend_string_refcount 读取字符串的引用计数
zend_string_addref 引用计数+1
zend_string_delref 引用计数-1
zend_string_alloc 分配内存及初始化字符串的值
zend_string_init 初始化字符串并在最后追加\0
zend_string_cop 使用引用计数方式复制字符串
zend_string_dup 直接复制一个字符串
zend_string_extend 扩容到len,保留原来的值
zend_string_truncate 截断到len,保留开头到len的值
zend_string_free 释放字符串内存
zend_string_release GC引用递减,直到为0时释放内存
zend_string_equals 普通判等
zend_string_equals_ci 基于二进制安全,两个zend_string类型字符串判等
zend_string_equals_literal_ci 基于二进制安全,zend_string类型和char*字符串判等
zend_inline_hash_func 计算字符串的哈希值
zend_intern_known_strings 往zend_intern_known_strings全局数组写入str

下面挑几个函数来介绍一下。

3.1、zend_string_init函数

zend_string_init函数主要负责把一个普通的字符串转化为zend_string结构体。

static zend_always_inline zend_string *zend_string_init(const char *str, size_t len, int persistent)
{
zend_string *ret = zend_string_alloc(len, persistent); memcpy(ZSTR_VAL(ret), str, len);
ZSTR_VAL(ret)[len] = '\0';
return ret;
}
  • 申请一块连续的内存,这个在上文中已经提到,申请的内存大小是zend_string结构体大小+字符串长度+1。
  • 指针偏移到val位置,开始字符串拷贝。
  • 在zend_string.val结尾追加\0

3.2、zend_string_extend函数

该函数主要用于对字符串的扩容,注意这里扩容不会改变原来保存的值,只是把长度扩大到len。

static zend_always_inline zend_string *zend_string_extend(zend_string *s, size_t len, int persistent)
{
zend_string *ret; ZEND_ASSERT(len >= ZSTR_LEN(s));
if (!ZSTR_IS_INTERNED(s)) {
if (EXPECTED(GC_REFCOUNT(s) == 1)) {
ret = (zend_string *)perealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
ZSTR_LEN(ret) = len;
zend_string_forget_hash_val(ret);
return ret;
} else {
GC_REFCOUNT(s)--;
}
}
ret = zend_string_alloc(len, persistent);
memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), ZSTR_LEN(s) + 1);
return ret;
}
  • 如果不是内部字符串并且引用计数为1时,直接调用perealloc分配内存。
  • 如果字符串的引用计数大于1或者是内部字符串时,就不能在原来的基础上扩容了,先通过zend_string_alloc申请一块新内存,让后将旧内容拷贝到新内存中。

3.3、zend_string_equals_ci函数

主要基于二进制安全对两个字符串进行判等,我们来看下PHP是怎么比较两个字符串的。

#define zend_string_equals_ci(s1, s2) \
(ZSTR_LEN(s1) == ZSTR_LEN(s2) && !zend_binary_strcasecmp(ZSTR_VAL(s1), ZSTR_LEN(s1), ZSTR_VAL(s2), ZSTR_LEN(s2)))
  • 先比较两个字符串的长度是否相等,注意这里是通过zend_string中的len来比较的。
  • zend_binary_strcasecmp函数在长度比较完成后,进行逐个字符进行比较。先遍历整个字符串数组,取出每个字符,转换为ASC码进行判等,如果不等则返回差值。循环完了还没发现差异的话就返回两者的长度差,如果长度相等就返回0。感觉这里做的有点多余,参数传进来之前就已经做了长度判等了。
ZEND_API int ZEND_FASTCALL zend_binary_strcasecmp(const char *s1, size_t len1, const char *s2, size_t len2) /* {{{ */
{
size_t len;
int c1, c2; if (s1 == s2) {
return 0;
} len = MIN(len1, len2);
while (len--) {
c1 = zend_tolower_ascii(*(unsigned char *)s1++);
c2 = zend_tolower_ascii(*(unsigned char *)s2++);
if (c1 != c2) {
return c1 - c2;
}
} return (int)(len1 - len2);
}

感兴趣的同学可以到源码中查看。

四、参考文献

  • 《PHP7底层设计与源码实现》
  • 《PHP7内核剖析》

跟厂长学PHP7内核(八):深入理解字符串的实现的更多相关文章

  1. 跟厂长学PHP7内核(六):变量之zval

    记得网上流传甚广的段子"PHP是世界上最好的语言",暂且不去讨论是否言过其实,但至少PHP确实有独特优势的,比如它的弱类型,即只需要$符号即可声明变量,使得PHP入手门槛极低,成为 ...

  2. 跟厂长学PHP7内核(七):常见变量类型的基本结构

    上篇文章讲述了变量的存储结构zval,今天我们就来学习一下几个常见变量类型的基本结构. 一.类型一览 zval中的u1.v.type用来存储变量的类型,而zval.value存储的是不同类型对应的值, ...

  3. 跟厂长学PHP7内核(四):生命周期之开始前的躁动

    上一章我们对PHP的源码目录结构有了初步了解,本章我们继续从生命周期的维度对PHP进行剖析. 一.概览 生命周期是什么呢?你可以把它看作执行过程,PHP的生命周期也就是它从开始执行到结束执行的过程. ...

  4. 跟厂长学PHP7内核(三):源码目录结构

    上篇文章我们已经介绍了源码分析工具的安装.配置以及调试方法,本文我们来讲述一下PHP源码的目录结构. 一.目录概览 以php-7.0.12为例,看过源码的同学们应该发现源码目录多达十多个,下面是每个目 ...

  5. 跟厂长学PHP7内核(一):发展史

    PHP1 1994年,一位名叫Rasmus lerdorf的兄台为了在网上展示自己的履历和网页流量的统计,用Perl开发了一套脚本,后来因与日俱增的需求无法得到满足,lerdorf便使用c语言进行了重 ...

  6. 跟厂长学PHP7内核(二):源码分析的环境与工具

    本文主要介绍分析源码的方式,其中包含环境的搭建.分析工具的安装以及源码调试的基本操作. 一.工具清单 PHP7.0.12 GDB CLion 二.源码下载及安装 $ wget http://php.n ...

  7. 跟厂长学PHP7内核(五):系统分析生命周期

    上篇文章讲述了模块初始化阶段之前的准备工作,本篇我来详细介绍PHP生命周期的五个阶段. 一.模块初始化阶段 我们先来看一下该阶段的每个函数的作用. 1.1.sapi_initialize_reques ...

  8. PHP7内核(八):深入理解字符串的实现

    在前面大致预览了常用变量的结构之后,我们今天来仔细的剖析一下字符串的具体实现. 一.字符串的结构 struct _zend_string { zend_refcounted_h gc; /* 字符串类 ...

  9. 菜鸟学自动化测试(八)----selenium 2.0环境搭建(基于maven)

    菜鸟学自动化测试(八)----selenium 2.0环境搭建(基于maven) 2012-02-04 13:11 by 虫师, 11419 阅读, 5 评论, 收藏, 编辑 之前我就讲过一种方试来搭 ...

随机推荐

  1. 部署tms过程记录

    背景:工作中经常需要进行工程部署,将过程记录下来,减少不必要的时间浪费. 1 问题总结 dubbo服务启动注册报 java.net.UnknownHostException 未知的名称或服务解决办法 ...

  2. websocket使用nginx作为反向代理

    需要nginx作为websocket的反向代理,没有nginx反向代理时候没有问题,通过nginx反向代理后会报400错误,查后台调试信息: tornado.general – DEBUG – Can ...

  3. Codeforces Round #481 (Div. 3) G. Petya's Exams

    http://codeforces.com/contest/978/problem/G 感冒是真的受不了...敲代码都没力气... 题目大意: 期末复习周,一共持续n天,有m场考试 每场考试有如下信息 ...

  4. tweenMax学习笔记

    tweenMax是一款缓动插件,能实现很多牛逼的效果,在网上看了些demo,确实很吊,虽说很多用CSS3也能做出来,但是技多不压身,学之. 网上的demo还是很多的,但是资料不多,唯一能够让我有思绪的 ...

  5. html js点击按钮滚动跳转定位到页面指定位置(DIV)的方法代码

    一:通过html锚点实现滚动定位到页面指定位置(DIV):    如果我们要点击实现跳转的地方是一个html锚点,也就是点击一个A标签超链接实现跳转,可以把A标签的href属性直接指向跳转指定位置的d ...

  6. AngularJs-$parsers自我理解-解析

    $parsers 首先先了解下它具体的作用,当用户与控制器进行交互的时候.ngModelController中的$setViewValue()方法就会被调用,$parsers的数组中函数就会以流水线的 ...

  7. 20145226 《Java程序设计》第七周学习总结

    教材学习内容总结 学习目标 · 了解Lambda语法 · 了解方法引用 · 了解Fucntional与Stream API · 掌握Date与Calendar的应用 · 会使用JDK8新的时间API ...

  8. size属性

    size 属性 size 属性规定输入字段的尺寸(以字符计): <form action=""> First name:<br> <input typ ...

  9. struts2 中 Preparable 接口实现数据准备

    今天才知道struts还有Preparable接口,实现此接口需要实现其prepare()方法,调用action中其他方法之前会先调用prepare()方法.此接口和方法可以用于初始化一些数据. 测试 ...

  10. python_ssh连接

    首先下载paramikopip install paramiko查看并启动ssh服务service ssh status 添加用户:useradd -d /home/zet zetpasswd zet ...