Swoole 核心開發備忘:記憶體管理優化(swString)

韓天峰發表於2020-06-28

最新的優化,減少了從recv_bufferphp zval的記憶體copy,可以從recv_buffer變為PHP層的string型別變數,相當於直接從Socket接收快取區中讀取到了PHP層。

GitHub PR:https://github.com/swoole/swoole-src/pull/3423

swString 結構體

typedef struct _swString
{
    size_t length;
    size_t size;
    off_t offset;
    char *str;
    const swAllocator *allocator;
} swString;

在設計上,這幾個欄位的作用分別是:

  • size:記憶體容量長度,進行append寫入操作時,如果有空餘的空間,無需擴容。當實際資料長度等於size時,需要進行記憶體擴容,通過呼叫swString_extend()完成
  • length:實際資料長度,必須小於或等於size否則會記憶體越界,底層的swString_*系列函式會檢查邊界,避免越界讀寫
  • offset:操作遊標,記錄應用層實際處理資料的位置,offset必須小於或等於length

swString.jpg

新增了 allocator 欄位,可以設定記憶體分配器,目前有3種。

  • SwooleG.std_allocator:標準的glibc malloc
  • SWOOLE_G(php_allocator):PHP 的 emalloc
  • SWOOLE_G(zend_string_allocator)zend_string_alloc

資料處理

coroutine::Socket::recv_packet() 分為兩個階段從Socket中讀取資料。

  1. 讀取PacketHeader資料,可能是一個比較小的值,如 recv(sizeof(PacketHeader)),在頭部中包含PacketLength欄位,獲取整個包的總長度
  2. 讀取Payload資料,recv(PacketLength - sizeof(PacketHeader)

底層會預先將offset值設定為PacketLength,然後分段從網路收取資料,呼叫recv操作,將資料追加到快取區,並更新length值。當length==offset時表示,接收完畢。coroutine::Socket::recv_packet()返回到應用層。這時應用層可以有兩個操作。

  • 使用memcpy將資料從recv_buffer中讀取出來,下一次呼叫recv_packet時,底層會自動呼叫swString_reduce()重置read_buffer快取區,這會存在一次記憶體拷貝,但是複用一塊記憶體的
  • 使用swString_pop()read_buffer->str整塊記憶體彈出,在應用層使用,底層會自動分配新的記憶體,用於接收下一個包
zval zdata;
ssize_t retval = sock->recv_packet();

// 複製記憶體
ZVAL_STRINGL(&zdata, sock->get_read_buffer()->str, retval);

// 彈出記憶體
ZVAL_STR(&zdata, sw_get_zend_string(sock->pop_packet()));

本次的 zerocopy 就是使用第二種方式,read_buffer 使用了 SWOOLE_G(zend_string_allocator) 記憶體分配器,彈出來的 read_buffer->str 記憶體,正好是一個 zend_stringval,再使用 sw_get_zend_string(read_buffer->str) 就可以得到 zend_string 物件的記憶體地址,最後使用 ZVAL_STR() 或者 RETURN_STR() 可直接將 zend_string 物件作為 PHP 層函式呼叫的返回值。

相關文章