tcache : pourquoi free() est devenu
si rapide
Le tcache (thread local cache) est arrivé avec glibc 2.26 : structures tcache_entry et tcache_perthread_struct, indexation par taille, et pourquoi free l'atteint avant les fastbins.
own2pwn··4 min de lecture
Prérequis
- Langage C
- Syscalls Linux
- Maniement de GCC
- Introduction à ptmalloc2
- Chunks
- Arenas
- Bins
Hello ! o/
Le tcache (thread local cache, ou Thread Cache) a été implémenté à partir de la glibc 2.26, soit fin août 2017. Il a permis de gagner d'importantes performances.
Chaque thread contient une variable locale threadée qui se souvient de quelle arena il a utilisé en dernier. Si cette arena est en cours d'utilisation quand notre thread veut l'utiliser, le thread se bloque et attend que le mutex soit libéré.
Par conséquent, on introduit 2 nouvelles structures de données :
typedef struct tcache_entry
{
struct tcache_entry *next;
struct tcache_perthread_struct *key; // double free detection
} tcache_entry;
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;Chaque thread contient un pointeur tcache vers une tcache_perthread_struct, qui stocke un petit nombre de chunks libérés accessibles sans avoir à locker une arena. tcache_entry pointe les chunks dans une liste chaînée, comme pour les fastbins, mais pointe sur le user data. Le tableau entries de tcache_perthread_struct est indexé par taille de chunk :
| Architecture | Taille min (bytes) | Taille max (bytes) | Pas (bytes) |
|---|---|---|---|
| x86 (32 bits) | 12 | 516 | 8 |
| x86-64 (64 bits) | 24 | 1032 | 16 |
Thread A
┌─────────────────────────────────────────────────────────┐
│ tcache (tcache_perthread_struct*) │
│ │
│ counts[0..63] entries[0..63] │
│ ┌────────────┐ ┌────────────────────────────────────┐ │
│ │ count[0] │ │ entries[0] → tcache_entry → NULL │ │
│ │ count[1] │ │ entries[1] → tcache_entry → NULL │ │
│ │ ... │ │ ... │ │
│ │ count[63] │ │ entries[63]→ NULL │ │
│ └────────────┘ └────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
tcache_entry (pointe sur le user data, pas le header)
┌──────────────────────────────────────────────┐
│ next → tcache_entry suivant (ou NULL) │
│ key → tcache_perthread_struct du thread │
└──────────────────────────────────────────────┘Source : MallocInternals - glibc wiki
Priorité sur les fastbins
Du coup, avant de tomber dans un fastbin, free() peut faire tomber le chunk dans le tcache ! L'accès au tcache sera donc plus rapide qu'aux fastbins, ce qui en fait une structure très intéressante d'un point de vue performance.
Ordre de priorité de free()
Pour aller plus loin
Merci pour votre lecture :)
Articles liés
heap
Les 4 bins de ptmalloc2, à quoi ils servent vraiment
Les quatre familles de bins ptmalloc2 : fast bins (sans fusion), unsorted bin (DCLL pour la seconde chance), small bins (moins de 512 octets) et large bins triees par classes de taille.
heap
Où vit ton heap : arenas et malloc_state
Structure malloc_state au coeur des arenas ptmalloc2 : mutex, fastbinsY, top chunk, last_remainder, bins et binmap. Cas particulier de la main_arena et schéma multi-threadé via heap_info.
heap
Anatomie d'un chunk ptmalloc2
Anatomie d'un chunk ptmalloc2 : champs mchunk_prev_size et mchunk_size, bits NON_MAIN_ARENA / IS_MMAPPED / PREV_INUSE et chainage FD/BK pour les chunks liberes.