Selon le manuel du programmeur Linux:
brk () et sbrk () modifient l'emplacement de l'interruption de programme, qui définit la fin du segment de données du processus.
Qu'est-ce que le segment de données signifie ici? S'agit-il uniquement du segment de données ou des données, BSS et tas combinés?
Selon wiki:
Parfois, les zones de données, de BSS et de segment de mémoire sont collectivement appelées le "segment de données".
Je ne vois aucune raison de modifier la taille du segment de données uniquement. S'il s'agit de données, BSS et tas collectivement, il est logique que tas ait plus d'espace.
Ce qui m'amène à ma deuxième question. Dans tous les articles que j'ai lus jusqu'à présent, l'auteur dit que le tas grandit et que la pile grandit. Mais ce qu’ils n’expliquent pas, c’est ce qui se passe lorsque tas occupe tout l’espace entre tas et pile?
Dans le diagramme que vous avez posté, la "rupture" - l'adresse manipulée par brk
et sbrk
- est la ligne pointillée en haut du tas.
La documentation que vous avez lue décrit cela comme la fin du "segment de données" car, en mode traditionnel (bibliothèques pré-partagées, pre -mmap
), le segment de données était continu avec le segment de mémoire; avant le démarrage du programme, le noyau chargerait les blocs "text" et "data" dans RAM à partir de l'adresse zéro (en fait un peu au-dessus de l'adresse zéro, de sorte que le pointeur NULL ne pointe pas réellement définissez l’adresse de rupture à la fin du segment de données.Le premier appel à malloc
utilisera ensuite sbrk
pour déplacer la rupture et créer le segment de mémoire entre le haut du segment de données et la nouvelle adresse de rupture supérieure, comme indiqué dans le diagramme, et l’utilisation ultérieure de malloc
l’utiliserait pour agrandir le segment de mémoire si nécessaire.
En attendant, la pile commence en haut de la mémoire et grandit. La pile n'a pas besoin d'appels système explicites pour l'agrandir; soit il commence avec autant RAM qui lui est alloué que possible (approche classique)) ou il y a une région d'adresses réservées sous la pile, à laquelle le noyau alloue automatiquement RAM quand il remarque une tentative d'écriture là-bas (c'est l'approche moderne). Quoi qu'il en soit, il peut y avoir ou non une région "gardien" au bas de l'espace d'adressage qui peut être utilisé pour la pile. Si cette région existe (ce que font tous les systèmes modernes), elle est définitivement non mappée; si soit la pile ou le tas tente de s'y développer, vous obtenez une erreur de segmentation. le noyau n'essayait pas d'imposer une limite: la pile pouvait devenir une pile, ou le tas pouvait s'agréger, et de toute façon, ils gribouilleraient les données de l'autre et le programme se bloquerait. crash immédiatement.
Je ne sais pas d'où vient le nombre 512 Go dans ce diagramme. Cela implique un espace d'adressage virtuel de 64 bits, ce qui est incompatible avec la mappe de mémoire très simple que vous avez là. Un véritable espace d'adressage 64 bits ressemble plus à ceci:
Legend: t: text, d: data, b: BSS
Ce n'est pas à l'échelle, et cela ne doit pas être interprété exactement comme un OS donné (après l'avoir dessiné, j'ai découvert que Linux place l'exécutable plus près de l'adresse zéro que je ne le pensais, et les bibliothèques partagées à des adresses étonnamment élevées). Les régions noires de ce diagramme ne sont pas mappées - tout accès provoque une erreur de segmentation immédiate - et elles sont gigantesques par rapport aux zones grises. Les régions gris clair sont le programme et ses bibliothèques partagées (il peut y avoir des dizaines de bibliothèques partagées); chacun comporte un segment de texte et de données indépendant (et un segment "bss", qui contient également des données globales mais est initialisé à zéro bit au lieu de prendre de la place dans l'exécutable ou la bibliothèque sur le disque). Le segment de mémoire n'est plus nécessairement continu avec le segment de données de l'exécutable - je l'ai dessiné de cette façon, mais il semble que Linux, du moins, ne le fasse pas. La pile n'est plus rattachée au haut de l'espace d'adressage virtuel et la distance entre le tas et la pile est tellement énorme que vous n'avez pas à vous inquiéter de le croiser.
La rupture est toujours la limite supérieure du tas. Cependant, ce que je n’ai pas montré, c’est qu’il pourrait exister des dizaines d’allocations de mémoire indépendantes, réalisées quelque part avec du noir, avec mmap
au lieu de brk
. (Le système d'exploitation essaiera de les garder loin de la zone brk
afin qu'elles ne se touchent pas.)
Exemple minimal exécutable
Que fait l'appel système brk ()?
Demande au noyau de vous permettre de lire et d’écrire sur un bloc de mémoire contigu appelé le segment de mémoire.
Si vous ne demandez pas, cela pourrait vous causer des problèmes.
Sans brk
:
#define _GNU_SOURCE
#include <unistd.h>
int main(void) {
/* Get the first address beyond the end of the heap. */
void *b = sbrk(0);
int *p = (int *)b;
/* May segfault because it is outside of the heap. */
*p = 1;
return 0;
}
Avec brk
:
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b = sbrk(0);
int *p = (int *)b;
/* Move it 2 ints forward */
brk(p + 2);
/* Use the ints. */
*p = 1;
*(p + 1) = 2;
assert(*p == 1);
assert(*(p + 1) == 2);
/* Deallocate back. */
brk(b);
return 0;
}
Ce qui précède pourrait ne pas toucher une nouvelle page et ne pas commettre d'erreur de segmentation même sans le brk
. Voici donc une version plus agressive qui alloue 16 Mo et est très susceptible de segresser par défaut sans le brk
:
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b;
char *p, *end;
b = sbrk(0);
p = (char *)b;
end = p + 0x1000000;
brk(end);
while (p < end) {
*(p++) = 1;
}
brk(b);
return 0;
}
Testé sur Ubuntu 18.04.
Visualisation de l'espace d'adressage virtuel
Avant brk
:
+------+ <-- Heap Start == Heap End
Après brk(p + 2)
:
+------+ <-- Heap Start + 2 * sizof(int) == Heap End
| |
| You can now write your ints
| in this memory area.
| |
+------+ <-- Heap Start
Après brk(b)
:
+------+ <-- Heap Start == Heap End
Pour mieux comprendre les espaces adresse, familiarisez-vous avec la pagination: Comment fonctionne la pagination x86? .
Pourquoi avons-nous besoin de brk
et de sbrk
?
brk
pourrait bien sûr être implémenté avec sbrk
+ calculs de décalage, les deux existent uniquement pour des raisons de commodité.
Dans le backend, le noyau Linux v5.0 a un seul appel système brk
utilisé pour implémenter les deux: https://github.com/torvalds/linux/blob/v5.0/ Arch/x86/entry/syscalls/syscall_64.tbl # L2
12 common brk __x64_sys_brk
Est-ce que brk
POSIX?
brk
était autrefois POSIX, mais il a été supprimé dans POSIX 2001, d'où la nécessité de _GNU_SOURCE
Pour accéder au wrapper de la glibc.
La suppression est probablement due à l'introduction mmap
, un sur-ensemble qui permet d'allouer plusieurs plages et plus d'options d'allocation.
Je pense qu'il n'y a pas de cas valable où vous devriez utiliser brk
au lieu de malloc
ou mmap
aujourd'hui.
brk
vs malloc
brk
est une ancienne possibilité d'implémenter malloc
.
mmap
est le mécanisme le plus récent et le plus puissant, que tous les systèmes POSIX utilisent actuellement pour implémenter malloc
.
Puis-je mélanger brk
et malloc?
Si votre malloc
est implémenté avec brk
, je ne sais pas comment cela peut ne pas faire exploser les choses, car brk
ne gère qu'une seule plage de mémoire.
Je n'ai toutefois rien trouvé à ce sujet dans les documents glibc, par exemple:
Les choses fonctionneront probablement là-bas, je suppose, puisque mmap
est probablement utilisé pour malloc
.
Voir également:
Plus d'infos
En interne, le noyau décide si le processus peut avoir autant de mémoire et marque pages mémoire pour cet usage.
Ceci explique comment la pile se compare au tas: Quelle est la fonction des instructions Push/pop utilisées sur les registres de l’assemblage x86?
Vous pouvez utiliser vous-même brk
et sbrk
pour éviter les "frais généraux de malloc" dont tout le monde se plaint. Mais vous ne pouvez pas facilement utiliser cette méthode en conjonction avec malloc
, alors ce n'est approprié que lorsque vous n'avez pas à free
quoi que ce soit. Parce que tu ne peux pas. En outre, vous devriez éviter les appels de bibliothèque pouvant utiliser malloc
en interne. C'est à dire. strlen
est probablement sans danger, mais fopen
ne l'est probablement pas.
Appelez sbrk
comme vous le feriez pour appeler malloc
. Il renvoie un pointeur sur la pause actuelle et incrémente la pause de ce montant.
void *myallocate(int n){
return sbrk(n);
}
Bien que vous ne puissiez pas libérer des allocations individuelles (car il n'y a pas malloc-overhead , rappelez-vous), vous pouvez libère tout l'espace en appelant brk
avec la valeur renvoyée par le premier appel à sbrk
, donc rembobine le brk .
void *memorypool;
void initmemorypool(void){
memorypool = sbrk(0);
}
void resetmemorypool(void){
brk(memorypool);
}
Vous pouvez même empiler ces régions, en ignorant la région la plus récente en rappelant la pause au début de la région.
Encore une chose ...
sbrk
est également utile dans code golf car ses 2 caractères sont plus courts que malloc
.
Il existe un mappage spécial de mémoire privée anonyme anonyme (généralement situé juste au-delà de data/bss, mais Linux moderne ajustera en fait l'emplacement avec ASLR). En principe, il n’est pas meilleur que tout autre mappage que vous pourriez créer avec mmap
, mais Linux dispose de quelques optimisations qui permettent d’étendre la fin de ce mappage (à l’aide de l’appel système brk
) coût de verrouillage par rapport à ce que mmap
ou mremap
encourrait. Cela rend attrayante l’utilisation des implémentations malloc
lors de l’implémentation du segment de mémoire principal.
malloc utilise l'appel système brk pour allouer de la mémoire.
comprendre
int main(void){
char *a = malloc(10);
return 0;
}
lancez ce programme simple avec strace, il appellera brk system.
Je peux répondre à votre deuxième question. Malloc échouera et retournera un pointeur nul. C'est pourquoi vous recherchez toujours un pointeur null lors de l'allocation dynamique de mémoire.
Le tas est placé en dernier dans le segment de données du programme. brk()
est utilisé pour modifier (développer) la taille du segment de mémoire. Lorsque le tas ne peut plus grandir, tout appel de malloc
échouera.
Le segment de données est la partie de la mémoire qui contient toutes vos données statiques, lues à partir de l'exécutable au lancement et généralement remplies de zéros.