Skip to content

Commit 2bdd6c0

Browse files
realFlowControlGirgiaskocsismate
authored
Add ZendMM internals, lifecycle and how to hook into docs (#145)
Co-authored-by: George Peter Banyard <[email protected]> Co-authored-by: Máté Kocsis <[email protected]>
1 parent c6a282d commit 2bdd6c0

File tree

1 file changed

+130
-1
lines changed

1 file changed

+130
-1
lines changed

Book/php7/memory_management/zend_memory_manager.rst

+130-1
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,139 @@ Beware however:
220220
What you must remember is that ZendMM leak tracking is a nice bonus tool to have, but it does not replace a
221221
:doc:`true C memory debugger <./memory_debugging>`.
222222

223+
Lifecycle
224+
*********
225+
226+
PHP will call the ``start_memory_manager()`` function during its startup phase, specifically when the PHP process is
227+
started (for instance, when the PHP-FPM service is started, or when a PHP CLI script is run). This will allocate the
228+
heap and the first chunk.
229+
230+
During a request the ZendMM will allocate chunks as needed.
231+
232+
On every request shutdown (during the ``RSHUTDOWN`` phase), the Zend Engine will call the ``shutdown_memory_manager()``
233+
function (which calls the ``zend_mm_shutdown()`` function) with the boolean argument ``full`` set to ``false``. This
234+
will cleanup for the next request, but not do a full shutdown of the memory manager. For example it will not free the
235+
heap and keep the average amount of chunks used during the current request alive in the ``cached_chunks`` pointer on the
236+
heap to be reused in the next request.
237+
238+
In the module shutdown phase (``MSHUTDOWN``) the Zend Engine will call the ``shutdown_memory_manager()`` function (which
239+
calls the ``zend_mm_shutdown()`` function) with the boolean argument ``full`` set to ``true``, which will trigger a full
240+
shutdown and free all cached chunks as well as the heap itself.
241+
223242
ZendMM internal design
224243
**********************
225244

226-
.. todo:: todo
245+
The root of the ZendMM is the ``_zend_mm_heap`` struct (as defined in `Zend/zend_alloc.c
246+
<https://github.com/php/php-src/blob/c3b910370c5c92007c3e3579024490345cb7f9a7/Zend/zend_alloc.c#L239>`__) which will be
247+
created for every request during request init and stored in the ``alloc_globals->mm_heap``. This heap also comes with
248+
the first chunk that is allocated with it. Chunks are then subdivided into pages. Smaller allocations are stored in bins
249+
which may fit into one page but some also span multiple pages.
250+
251+
Internal memory organisation
252+
---------------------------
253+
254+
Heap
255+
++++
256+
257+
The heap, as defined in the struct ``_zend_mm_heap``, holds links to chunks (``main_chunk`` and ``cached_chunks``, for
258+
small and large allocations), ``huge_list`` for huge allocations (>= 2MB) and to bins (for small allocations) in
259+
``free_slots[BIN]``. After initialisation only the ``main_chunk`` exists and none or some ``cached_chunks``.
260+
261+
Chunks
262+
++++++
263+
264+
Each chunk is 2 MB in size and consists of 512 pages. The first page of every chunk is reserved for the chunk header as
265+
defined in the struct ``_zend_mm_chunk`` (as defined in `Zend/zend_alloc.c
266+
<https://github.com/php/php-src/blob/c3b910370c5c92007c3e3579024490345cb7f9a7/Zend/zend_alloc.c#L286>`__). Chunks are
267+
organised in a linked list with ``prev`` and ``next`` pointers.
268+
269+
Each chunk holds a bit mask in ``free_map`` (512 bits) where a single bit indicates if a page is in use or free.
270+
Information on what is in a page is stored in ``map`` which is an array of 512 32 bit integers. Each of those integers
271+
is used as a bitmap and holds the meta information about that page.
272+
273+
Pages
274+
+++++
275+
276+
A page is 4096 bytes in size and can either hold a bin (for small allocations) or be part of a large allocation. What is
277+
in it can be found in the map of the chunk the page belongs to.
278+
279+
Bins
280+
++++
281+
282+
Small allocations are grouped together in bins. Bin sizes are predefined and come in 30 different sizes (8, 16, 24, 32,
283+
... 3072 bytes). A bin holds same sized values and is linked from the heap directly.
284+
285+
A bin can consist of multiple pages. Example: There exists a bin that holds elements ranging from 257 bytes to 320 bytes
286+
which occupies 5 pages, and therefore has room for 64 (derived from 4096*5/320) elements of that size.
287+
288+
Allocation categories
289+
---------------------
290+
291+
Small allocations
292+
+++++++++++++++++
293+
294+
Allocations less or equal than 3072 bytes are organised in bins.
295+
296+
If a bin is already initialised, the ``free_slot`` pointer on the ``zend_mm_heap`` struct is the address to be used
297+
(this address will be returned by the call to ``emalloc()`` and will be incremented to point to the next free slot, see
298+
implementation in ``zend_mm_alloc_small``).
299+
300+
If the bin for this specific size is not initialised already, it will be created in the ``zend_mm_alloc_small_slow``
301+
function and a pointer to the first element of the bin is returned.
302+
303+
Large allocations
304+
+++++++++++++++++
305+
306+
Allocations bigger than 3072 bytes, but small enough to fit in a chunk (2 MB chunk size - 4096 bytes chunk header (first
307+
page) makes 2093056 bytes) are directly stored in the pages. The first page will be marked ``LRUN`` in the map of the
308+
chunk and also hold the number of allocated pages.
309+
310+
Huge allocations
311+
++++++++++++++++
312+
313+
If an allocation is larger than the chunk size minus one page (2 MB chunk size - 4096 bytes chunk header (first page)
314+
makes 2093056 bytes) the memory is allocated using ``mmap()`` and put on the ``huge_list`` linked list on the heap.
315+
316+
Hooking into the ZendMM
317+
***********************
318+
319+
You can call the ``zend_mm_set_custom_handlers()`` function and give it pointers to your ``malloc``, ``free`` and
320+
``realloc`` handlers as well as your custom heap or the current heap that you may fetch via ``zend_mm_get_heap()``.
321+
322+
.. code-block:: c
323+
324+
void* my_malloc(size_t len) {
325+
return malloc(len);
326+
}
327+
328+
void my_free(void* ptr) {
329+
free(ptr);
330+
}
331+
332+
void* my_realloc(void* ptr, size_t len) {
333+
return realloc(ptr, len);
334+
}
335+
336+
PHP_MINIT_FUNCTION(my_extension) {
337+
zend_mm_set_custom_handlers(
338+
zend_mm_get_heap(),
339+
my_malloc,
340+
my_free,
341+
my_realloc
342+
);
343+
return SUCCESS;
344+
}
345+
346+
You may also bring your own heap and inject it via ``zend_mm_set_heap()`` which returns a pointer to the current (or
347+
old) heap. Beware that on a heap with custom handlers, ZendMM's behaviour will be different:
348+
349+
* ZendMM will not run cleanup during ``zend_mm_shutdown()`` (which is called during PHP request shutdown phase), leaving
350+
you with a memory leak if your custom handlers just forward calls to the ZendMM internal functions.
351+
* ZendMM's garbage collector implemented in ``zend_mm_gc()`` will not be doing anything. This also means it will not try
352+
to free chunks in case you reach the memory limit during an allocation in one of the ZendMM internal functions.
353+
* The only way to detect that a full shutdown is in progress in your heap with custom handlers is that your ``free``
354+
function will be called with the address of your heap.
355+
* There is no chance of knowing when ``zend_mm_shutdown()`` will perform a request shutdown.
227356

228357
Common errors and mistakes
229358
**************************

0 commit comments

Comments
 (0)