【PHP7源碼分析】初探PHP字符串類型中的引用計數

作者:王澍

背景介紹

  • 字符串類型也是我們平時常用的類型,由於字符串的特性,爲了節省內存通常相同字符串變量會共用一塊內存空間,通過引用計數來標記有多變量使用這塊內存。
  • 但是,經過GDB追蹤發現,並不是所有字符串都是正常在操作引用計數,有正常累加的,有時候爲0,又有時候爲1。爲了一探究竟,於是簡單分析了一下各種賦值情況。

環境情況

  • 系統版本:Ubuntu 16.04.3 LTS
  • PHP版本:PHP 7.1.0
  • gdb版本:GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1

一、基礎變量

PHP中zval是所有變量的基礎。(zend_type.h 121行)

clipboard.png

其中zend_value存儲了具體數據,結構如圖: (zend_type.h 101行)

clipboard.png

  • zend_value爲一個聯合體,整體佔用8字節。
  • u1爲一個聯合體,存儲了類型所需要的必要數據,佔用4字節。
  • u2位一個聯合體,存儲了一些額外數據,比如hash碰撞時的next,佔用4字節。

整個zval結構體,佔用16字節,就支持了php所有類型。

clipboard.png

PHP7中用如此簡單而巧妙的zval存儲了所有類型數據,那麼一個不確定長度的字符串又如何能存儲在一個16字節的zval中呢?

二、字符串變量

<?php
$a = "hello world";
echo $a;

通過GDB調試可以看到:

clipboard.png

type = 6,對照類型的定義,可以看到類型是IS_STRING (zend_type.h 303行)

clipboard.png

由於我們的字符串長度不一定,所以單靠zval的16個字節是無法直接存儲的,於是通過value中的str指向真正存儲字符串的內存地址。通過打印我們可以看到,這個地址的類型是zend_string

clipboard.png

1、zend_string結構體

先看一下它的數據結構,如圖 (zend_type.h 169行)

clipboard.png

zend_string結構體中的gc
頭部先是gc,可以看一下其他複雜類型,頭部都有一個gc,它的作用是什麼?
看看gc的數據結構,如圖:

clipboard.png

  • 第一個是refcount,記錄了被引用的次數。
  • 第二個u是一個聯合體,可以看到與zval的u1很像,關鍵是記錄了type。

那麼作用就比較好猜測了,在程序執行gc或其他操作的時候,對於任意一個複雜類型,指針頭部就是gc,裏面不光有引用計數,並且能通過u.v.type確定該複雜類型的真正類型。

zend_string結構體中的h
從名字可以猜測,這是字符串的hash,空間換時間的思想,把計算好的hash保存下來,提高性能。

zend_string中的len
比較明顯,它存儲了字符串的長度。

zend_string中的val[1]
這種寫法是c裏面的柔性數組,這裏存儲了整個字符串,通過這個方式保證字符串所在的內存地址是與該結構體內存地址緊密相連的,減少了去另外一個塊內存取值的時間。
(ps:留個小疑問,gdb就可以追蹤到。柔性數組是否佔內存空間?zend_string的對齊後是什麼結構?整體佔多大?)

2、zend_string實際內容

瞭解過結構本身,可以打印一下內容來看看,如圖

clipboard.png

該地址內存儲的確實是hello world,爲什麼gc中的refcount是0呢?

原因是這樣的:

  • 常量字符串,在PHP代碼中的固定字符串,在編譯階段存儲在全局變量表中,又叫做字面量表,請求結束後纔會被銷燬,所以refcount一直爲0。
  • 臨時字符串,發生在虛擬機執行opcode時計算出來的字符串,存儲在臨時變量區,有正常的refcount。

修改一下代碼,看一下臨時字符串

<?php
$a = "hello world".time();
echo $a;

打印一下這個變量的zval,refcount爲1,如圖

clipboard.png

三、字符串的引用計數

1.臨時字符串直接賦值

對於臨時字符串,應該是每有一個被賦值的變量時,該zend_string中的引用計數+1,並且在引用計數爲0時,釋放這塊內存。
<?php

$a = "hello world".time();
echo $a;
$b = $a;
echo $b;
當爲$a賦值完成時,$a,在棧上第一個位置,類型爲6,IS_STRING,取value中的str,地址爲:**0x7ffff4402c30**,可以看到內容,zend_string引用計數爲1。

clipboard.png

當爲$b賦值完成時,$b在棧上第二個位置,類型爲6,IS_STRING,取value中的str同樣地址爲:**0x7ffff4402c30**,zend_string引用計數爲2。

clipboard.png

大致引用情況可以畫出:

clipboard.png

2.引用賦值

對於變量直接賦值,上面已經畫出了引用關係,那麼如果是引用類型呢?
<?php

$a = "hello world".time();
echo $a;
$b = &$a;
echo $b;
當爲$a賦值完成時,$a,在棧上第一個位置,類型爲6,IS_STRING,取value中的str,地址爲:**0x7ffff4402c30**,可以看到內容,zend_string引用計數爲1。

clipboard.png

當$b賦值爲引用類型時,$b在棧上第二個位置,類型爲10 , IS_REFERENCE,取value中的ref可以看到內容。

clipboard.png

這時$a的類型是否發生改變?是否還是字符串類型?直接打印$a看一下。這時$a的類型變成了10,IS_REFERENCE,打印value中的ref,地址與$b的ref相同!

clipboard.png

在$b引用$a的時候,$a與$b都變成引用類型,該引用類型指向了中有一個zval,類型爲6,IS_STRING,value中的str指向了一個zend_string,並且zend_string引用計數爲1.

clipboard.png

大致引用情況如圖:

clipboard.png

四、字符串變量特殊值

<?php
$a = “string”;
$b = “double”;
 
echo $a;
echo $b;

以我們上面的結論,$a與$b都屬於常量字符串。

打印$a的zend_string,如圖

clipboard.png

打印$b的zend_string,如圖

clipboard.png

可見$b符合預期,但是$a顛覆了以上的理論。
那問題出在了哪?

經過GDB的追蹤,可以看到a和b都在棧上,並且都是string類型。
但是,其中value中的str地址有很大不一樣。
首先看變量a
在棧的第一個位置,str的值是 0x11522c0

clipboard.png

其次看變量b
在棧上第二個位置,str的值是 0x7ffff4401880

clipboard.png

瞭解PHP的內存分配的話,可以看出b的字符串,分配在了 0x7ffff440000 這個chunk上,屬於第一個page頁,0x7ffff4401000

而a的字符串很顯然不是這個規則,他沒有分配在chunk上,而是很特殊的一個地址。
所以string這個字符串,不是_emalloc分配的。

那麼採用個笨辦法,我把 0x11522c0強轉 (zend_string *)0x11522c0 ,然後看裏的值什麼時候放進去的。

PHP版本 7.1.0
第一個節點: php_cli.c中的 1345行

sapi_module->startup(sapi_module)

第二個節點: php_cli.c 中的424 行

php_module_startup(sapi_module, NULL, 0)

第三個節點: main.c 中的 2123行

zend_startup(&zuf, NULL);

第四個節點: zend.c 中的768行

zend_interned_strings_init();

很接近了哦
第五個節點: zend_string.c中的103

zend_intern_known_strings(known_strings, (sizeof(known_strings)

在這裏打印一下,know_strings,這裏可以看到,file,line,function,class,object等等,以及string在這裏就初始化了!

clipboard.png

對應聲明的地址在 zend_string.h 383行

clipboard.png

這裏還沒有初始化字面量,於是這些字符串與字面量的情況有些不一樣。

小結

同樣是字符串在PHP中有很多種不同情況。

  • 1.代碼中直接硬編碼的字符串,在字面量表中,引用計數一直爲0,直到整個腳本執行完畢後,纔會銷燬。
  • 2.在執行階段計算出來的字符串,臨時字符串,引用計數正常計算,每個引用都會加1。在引用計數爲0時回收內存。
  • 3.引用類型的字符串,多個變量引用計數計算在引用類型(zend_reference)上。字符串被zend_reference引用,引用計數爲1。
  • 4.特殊的字符串,在PHP初始化時創建的,整個腳本執行完畢後纔會銷燬,引用計數一直爲1。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章