PHP7内核(八):深入理解字符串的实现
在前面大致预览了常用变量的结构之后,我们今天来仔细的剖析一下字符串的具体实现。
一、字符串的结构
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内核(八):深入理解字符串的实现的更多相关文章
- 跟厂长学PHP7内核(八):深入理解字符串的实现
在前面大致预览了常用变量的结构之后,我们今天来仔细的剖析一下字符串的具体实现. 一.字符串的结构 struct _zend_string { zend_refcounted_h gc; /* 字符串类 ...
- 跟厂长学PHP7内核(六):变量之zval
记得网上流传甚广的段子"PHP是世界上最好的语言",暂且不去讨论是否言过其实,但至少PHP确实有独特优势的,比如它的弱类型,即只需要$符号即可声明变量,使得PHP入手门槛极低,成为 ...
- 深入剖析PHP7内核源码(二)- PHP变量容器
简介 PHP的变量使用起来非常方便,其基本结构是底层实现的zval,PHP7采用了全新的zval,由此带来了非常大的性能提升,本文重点分析PHP7的zval的改变. PHP5时代的ZVAL typed ...
- PHP7内核(六):变量之zval
记得网上流传甚广的段子"PHP是世界上最好的语言",暂且不去讨论是否言过其实,但至少PHP确实有独特优势的,比如它的弱类型,即只需要$符号即可声明变量,使得PHP入手门槛极低,成为 ...
- 跟厂长学PHP7内核(七):常见变量类型的基本结构
上篇文章讲述了变量的存储结构zval,今天我们就来学习一下几个常见变量类型的基本结构. 一.类型一览 zval中的u1.v.type用来存储变量的类型,而zval.value存储的是不同类型对应的值, ...
- PHP7内核(七):常见变量类型的基本结构
上篇文章讲述了变量的存储结构zval,今天我们就来学习一下几个常见变量类型的基本结构. 一.类型一览 zval中的u1.v.type用来存储变量的类型,而zval.value存储的是不同类型对应的值, ...
- linux内核分析第八周-理解进程调度时机跟踪分析进程调度与进程切换的过程
实验原理: 一.调度时机 不同类型的进程有不同的调度需求 第一种分类: I/O-bound 频繁的进行I/O 通常会花费很多时间等待I/O操 ...
- 深入理解PHP内核(八)变量及数据类型-预定义变量
原文链接:http://www.orlion.ga/249/ PHP脚本在执行的时候用户全局变量(在用户空间显示定义的变量)会保存在一个HashTable数据类型的符号表中(symbol_table) ...
- linux内核分析 第八周 理解进程调度时机跟踪分析进程调度与进程切换的过程
笔记: 实验:使用gdb跟踪分析一个schedule()函数
随机推荐
- JS实现总价随数量变化而变化(顾客购买商品表单)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:test.html * 作者:常轩 * 微信公众号:Worldh ...
- 6487. 【GDOI2020模拟02.29】列强争霸war
题目描述 区间绝对众数 即出现次数>len/2下取整的数 对于区间[L,R]扫一遍,维护一个数x和出现次数s 当前数=x则s+1,否则s-1,若s已为0则把x设为当前数 若区间内存在绝对众数,那 ...
- 用table类型布局一个新闻网页
<html><head><meta http-equiv="Content-Type" content="text/html; charse ...
- Java基础--插入排序
直接插入排序算法 (从后往前找到合适位置插入) 基本思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的子序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止. 例: 34,4 ...
- 微服务优化之使用gRPC做微服务的内部通信
使用gRPC做微服务的内部通信 gRPC是一个由Google开源的远程服务调用框架,具有多路复用和双向流式通信的特性. 大家好,在本文中将为大家介绍为什么我们应该使用gRPC代替RESTful或JSO ...
- 前端传json数组 ,后端的接收
前端传输: var updateGoodsId=$(this).val();//get id var updateGoodsPrice=$("#IngoodsPrice"+upda ...
- 如何学习kafka?
本文是我学习kafka的一个思路和总结,希望对刚接触kafka的你有所帮助.在学习kafka之前,最好能对kafka有一个简单的了解,可以提出一些问题,带着问题去学习,就会容易一些. 0 什么是k ...
- 去除 inline-block 元素间距
案例重现 布局时经常能发现inline元素和inline-block元素水平呈现的元素间,会存在着一些意想不到的间距,举例: .inline-block { display: inline-block ...
- MATLAB神经网络(2) BP神经网络的非线性系统建模——非线性函数拟合
2.1 案例背景 在工程应用中经常会遇到一些复杂的非线性系统,这些系统状态方程复杂,难以用数学方法准确建模.在这种情况下,可以建立BP神经网络表达这些非线性系统.该方法把未知系统看成是一个黑箱,首先用 ...
- AspNetCore3.1源码解析_2_Hsts中间件
title: "AspNetCore3.1源码解析_2_Hsts中间件" date: 2020-03-16T12:40:46+08:00 draft: false --- 概述 在 ...