Que fait l'appel système brk ()?

184

Selon le manuel des programmeurs Linux:

brk () et sbrk () modifient l'emplacement de l'interruption du programme, qui définit la fin du segment de données du processus.

Que signifie le segment de données ici? S'agit-il uniquement du segment de données ou des données, du BSS et du tas combinés?

Selon wiki:

Parfois, les données, le BSS et les zones de tas sont collectivement appelés 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, de BSS et de tas collectivement, cela a du sens car le tas obtiendra 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 se développe vers le haut et la pile se développe vers le bas. Mais ce qu'ils n'expliquent pas, c'est ce qui se passe lorsque le tas occupe tout l'espace entre le tas et la pile?

entrez la description de l'image ici

nik
la source
1
Alors, que faites-vous lorsque vous manquez d'espace? vous passez au disque dur. Lorsque vous avez utilisé de l'espace, vous le libérez pour d'autres types d'informations.
Igoris Azanovas
29
@Igoris: Vous confondez la mémoire physique (que vous pouvez échanger sur le disque selon vos besoins, en utilisant la mémoire virtuelle) et l' espace d'adressage . Lorsque vous remplissez votre espace d'adressage, aucun échange ne vous redonnera ces adresses au milieu.
Daniel Pryden
7
Pour rappel, l' brk()appel système est plus utile en langage assembleur qu'en C. En C, il malloc()doit être utilisé à la place de brk()pour des fins d'allocation de données - mais cela n'invalide en aucun cas la question proposée.
alecov
2
@Brian: Le tas est une structure de données complexe pour gérer des régions de tailles et d'alignements variables, un pool libre, etc. Les piles de threads sont toujours des séquences contiguës (dans l'espace d'adressage virtuel) de pages complètes. Dans la plupart des systèmes d'exploitation, il existe un allocateur de page sous-jacent les piles, le tas et les fichiers mappés en mémoire.
Ben Voigt
2
@Brian: Qui a dit qu'il y avait une "pile" manipulée par brk()et sbrk()? Les piles sont gérées par l'allocateur de page, à un niveau beaucoup plus bas.
Ben Voigt

Réponses:

233

Dans le diagramme que vous avez publié, la "rupture" - l'adresse manipulée par brket sbrk- est la ligne en pointillé en haut du tas.

image simplifiée de la disposition de la mémoire virtuelle

La documentation que vous avez lue décrit cela comme la fin du "segment de données" parce que dans les bibliothèques traditionnelles (pré-partagées, pré- mmap) Unix, le segment de données était continu avec le tas; avant le démarrage du programme, le noyau chargerait les blocs «texte» et «données» dans la RAM en commençant à l'adresse zéro (en fait un peu au-dessus de l'adresse zéro, de sorte que le pointeur NULL ne pointait vraiment pas vers quoi que ce soit) et définissait l'adresse de rupture sur la fin du segment de données. Le premier appel à mallocutiliserait ensuite sbrkpour déplacer la rupture et créer le tas entre le haut du segment de données et la nouvelle adresse de rupture plus élevée, comme indiqué dans le diagramme, et l'utilisation ultérieure de mallocl'utiliserait pour agrandir le tas le cas échéant.

Pendant ce temps, la pile commence au sommet de la mémoire et se développe. La pile n'a pas besoin d'appels système explicites pour l'agrandir; soit il commence avec autant de RAM allouée que possible (c'était l'approche traditionnelle) ou il y a une région d'adresses réservées sous la pile, à laquelle le noyau alloue automatiquement de la RAM lorsqu'il remarque une tentative d'écriture là-bas (c'est l'approche moderne). Dans tous les cas, il peut y avoir ou non une région de «garde» au bas de l'espace d'adressage qui peut être utilisée pour la pile. Si cette région existe (tous les systèmes modernes le font), elle est définitivement non mappée; si soitla pile ou le tas essaie de s'y développer, vous obtenez une erreur de segmentation. Traditionnellement, cependant, le noyau ne faisait aucune tentative pour imposer une limite; la pile pourrait se développer dans le tas, ou le tas pourrait se développer dans la pile, et de toute façon ils griffonneraient sur les données de l'autre et le programme planterait. Si vous étiez très chanceux, il planterait 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 carte mémoire très simple que vous avez là-bas. Un véritable espace d'adressage 64 bits ressemble plus à ceci:

espace d'adressage moins simplifié

              Legend:  t: text, d: data, b: BSS

Ce n'est pas à l'échelle à distance, et cela ne devrait pas être interprété comme exactement comment un système d'exploitation donné fait les choses (après l'avoir dessiné, j'ai découvert que Linux rapproche en fait l'exécutable 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 un segfault immédiat - 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 a un indépendantsegment de texte et de données (et segment "bss", qui contient également des données globales mais est initialisé à tous les bits-zéro plutôt que de prendre de l'espace dans l'exécutable ou la bibliothèque sur le disque). Le tas 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, au moins, ne le fasse pas. La pile n'est plus ancrée au sommet de l'espace d'adressage virtuel, et la distance entre le tas et la pile est si énorme que vous n'avez pas à vous soucier de la traverser.

La rupture est toujours la limite supérieure du tas. Cependant, ce que je n'ai pas montré, c'est qu'il pourrait y avoir des dizaines d'allocations indépendantes de mémoire là-bas dans le noir quelque part, faites avec mmapau lieu de brk. (Le système d'exploitation essaiera de les garder loin de la brkzone afin qu'ils n'entrent pas en collision.)

zwol
la source
7
+1 pour une explication détaillée. Savez-vous s'il mallocrepose toujours sur brkou s'il utilise mmappour pouvoir "rendre" des blocs de mémoire séparés?
Anders Abel
18
Cela dépend de l'implémentation spécifique, mais IIUC beaucoup de mallocs actuels utilisent la brkzone pour les petites allocations et les individus mmappour les grandes allocations (par exemple> 128K). Voir, par exemple, la discussion de MMAP_THRESHOLD dans la malloc(3)page de manuel Linux .
zwol
1
En effet une bonne explication. Mais comme vous l'avez dit, Stack ne se trouve plus en haut de l'espace d'adressage virtuel. Est-ce vrai pour l'espace d'adressage de 64 bits seulement ou même pour l'espace d'adressage de 32 bits. Et si la pile se trouve en haut de l'espace d'adressage, où se produisent les mappages de mémoire anonymes? Est-ce au sommet de l'espace d'adressage virtuel juste avant la pile.
nik
3
@Nikhil: c'est compliqué. La plupart des systèmes 32 bits placent la pile tout en haut de l' espace d'adressage en mode utilisateur, qui n'est souvent que les 2 ou 3G inférieurs de l'espace d'adressage complet (l'espace restant est réservé au noyau). Je ne peux pas penser à un qui ne l'a pas fait mais je ne les connais pas tous. La plupart des processeurs 64 bits ne vous permettent pas d'utiliser tout l'espace 64 bits; les 10 à 16 bits de haut de l'adresse doivent être tout à zéro ou tout un. La pile est généralement placée près du sommet des adresses basses utilisables. Je ne peux pas vous donner de règle pour mmap; c'est extrêmement dépendant du système d'exploitation.
zwol
3
@RiccardoBestetti Cela gaspille de l' espace d'adressage , mais c'est inoffensif - un espace d'adressage virtuel de 64 bits est si grand que si vous en brûliez un gigaoctet chaque seconde, il vous faudrait encore 500 ans pour s'épuiser. [1] La plupart des processeurs ne permettent même pas utiliser plus de 2 ^ 48 à 2 ^ 53 bits d'adresse virtuelle (la seule exception que je connaisse est POWER4 en mode tableau page hachée). Il ne gaspille pas de RAM physique; les adresses inutilisées ne sont pas affectées à la RAM.
zwol le
26

Exemple exécutable minimal

Que fait l'appel système brk ()?

Demande au noyau de vous permettre de lire et d'écrire dans un bloc de mémoire contigu appelé le tas.

Si vous ne le demandez pas, cela pourrait vous déranger.

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;
}

GitHub en amont .

Ce qui précède pourrait ne pas frapper une nouvelle page et pas segfault même sans le brk, voici donc une version plus agressive qui alloue 16 Mo et est très susceptible de segfault 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 d'adressage, vous devez vous familiariser avec la pagination: comment fonctionne la pagination x86? .

Pourquoi avons-nous besoin des deux brket sbrk?

brkpourrait bien sûr être implémenté avec des sbrkcalculs de + offset, les deux existent juste pour des raisons de commodité.

Dans le backend, le noyau Linux v5.0 a un seul appel système brkqui est utilisé pour implémenter les deux: https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64. tbl # L23

12  common  brk         __x64_sys_brk

Est-ce brkPOSIX?

brkAuparavant, c'était POSIX, mais il a été supprimé dans POSIX 2001, d'où la nécessité _GNU_SOURCEd'accéder au wrapper glibc.

La suppression est probablement due à l'introduction mmap, qui est 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 à la brkplace mallocou de mmapnos jours.

brk contre malloc

brkest une ancienne possibilité de mise en œuvre malloc.

mmapest le nouveau mécanisme strictement plus puissant que tous les systèmes POSIX utilisent actuellement pour implémenter malloc. Voici un exemple d'allocation de mémoire exécutable minimalemmap .

Puis-je mélanger brket malloc?

Si votre mallocest implémenté avec brk, je ne sais pas comment cela peut ne pas faire exploser les choses, car brkne gère qu'une seule plage de mémoire.

Je n'ai cependant rien trouvé à ce sujet sur la documentation de la glibc, par exemple:

Les choses fonctionneront probablement là-bas, je suppose, car elles mmapsont probablement utilisées malloc.

Voir également:

Plus d'informations

En interne, le noyau décide si le processus peut avoir autant de mémoire et réserve des pages de 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 dans l'assemblage x86?

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
4
Puisqu'il ps'agit d'un pointeur sur le type int, cela n'aurait-il pas dû l'être brk(p + 2);?
Johan Boulé
Petite note: l'expression dans la boucle for de la version agressive devrait probablement être*(p + i) = 1;
lima.sierra
Au fait, pourquoi devons-nous utiliser a brk(p + 2)au lieu de simplement l'augmenter sbrk(2)? Brk est-il vraiment nécessaire?
Yi Lin Liu
1
@YiLinLiu Je pense que ce ne sont que deux frontends C très similaires pour un seul backend noyau ( brksyscall). brkest un peu plus pratique pour restaurer la pile précédemment allouée.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@CiroSantilli 新疆 改造 中心 996ICU 六四 事件 Considérant que la taille d'un int est de 4 octets et que la taille d'un int * est de 4 octets (sur une machine 32 bits), je me demandais si elle ne devrait pas être incrémentée de seulement 4 octets (au lieu de 8 - (2 * sizeof int)). Ne devrait-il pas pointer vers le prochain stockage de tas disponible - qui sera à 4 octets (pas 8). Corrigez-moi si je manque quelque chose ici.
Saket Sharad
10

Vous pouvez utiliser brket sbrkvous - même pour éviter les «frais généraux de malloc» dont tout le monde se plaint toujours. Mais vous ne pouvez pas facilement utiliser cette méthode en conjonction avec, mallocdonc elle n'est appropriée que lorsque vous n'avez rien à faire free. Parce que tu ne peux pas. De plus, vous devez éviter tout appel de bibliothèque qui pourrait être utilisé en mallocinterne. C'est à dire. strlenest probablement sans danger, mais fopenne l'est probablement pas.

Appelez sbrkcomme vous appelleriez malloc. Il renvoie un pointeur vers la rupture actuelle et incrémente la rupture de ce montant.

void *myallocate(int n){
    return sbrk(n);
}

Bien que vous ne puissiez pas libérer d'allocations individuelles (car il n'y a pas de surcharge de malloc , rappelez-vous), vous pouvez libérer tout l'espace en appelant brkavec la valeur renvoyée par le premier appel à sbrk, rembobinant ainsi le brk .

void *memorypool;
void initmemorypool(void){
    memorypool = sbrk(0);
}
void resetmemorypool(void){
    brk(memorypool);
}

Vous pouvez même empiler ces régions, en supprimant la région la plus récente en rembobinant la pause au début de la région.


Encore une chose ...

sbrkest également utile dans le code golf car il est plus court de 2 caractères que malloc.

Luser droog
la source
7
-1 parce que: malloc/ peutfree très certainement (et le fait) redonner de la mémoire au système d'exploitation. Ils peuvent ne pas toujours le faire quand vous le souhaitez, mais c'est une question d'heuristique imparfaitement adaptée à votre cas d'utilisation. Plus important encore, il n'est pas sûr d'appeler avec un argument différent de zéro dans n'importe quel programme qui pourrait appeler - et presque toutes les fonctions de la bibliothèque C sont autorisées à appeler en interne. Les seuls qui ne le seront certainement pas sont les fonctions de sécurité du signal asynchrone . sbrkmallocmalloc
zwol
Et par "c'est dangereux", je veux dire "votre programme va planter".
zwol
J'ai édité pour supprimer la vantardise de mémoire de retour , et mentionné le danger des fonctions de bibliothèque utilisant en interne malloc.
luser droog
1
Si vous voulez faire une allocation de mémoire sophistiquée, basez-la soit sur malloc, soit sur mmap. Ne touchez pas brk et sbrk, ce sont des reliques du passé qui font plus de mal que de bien (même les pages de manuel vous disent de rester à l'écart!)
Eloff
3
C'est idiot. Si vous voulez éviter la surcharge de malloc pour un grand nombre de petites allocations, faites une grosse allocation (avec malloc ou mmap, pas sbrk) et distribuez-la vous-même. Si vous conservez les nœuds de votre arbre binaire dans un tableau, vous pouvez utiliser des index 8b ou 16b au lieu de pointeurs 64b. Cela fonctionne très bien lorsque vous n'avez pas à supprimer de nœud avant d'être prêt à supprimer tous les nœuds. (par exemple , construire un dictionnaire trié à la volée). En utilisant sbrkpour cela est seulement utile pour le code-golf, car manuellement à l' aide mmap(MAP_ANONYMOUS)est meilleure dans tous les sens , sauf la taille du code source.
Peter Cordes
3

Il existe un mappage de mémoire privée anonyme désigné spécial (situé traditionnellement juste au-delà des données / bss, mais Linux moderne ajustera en fait l'emplacement avec ASLR). En principe , il est pas mieux que toute autre application , vous pouvez créer avec mmap, mais Linux a quelques optimisations qui permettent d'élargir la fin de cette cartographie ( en utilisant le brksyscall) vers le haut avec un coût de blocage réduit par rapport à ce mmapou mremapencourrait. Cela le rend attrayant pour les mallocimplémentations à utiliser lors de l'implémentation du tas principal.

R .. GitHub STOP AIDING ICE
la source
Vous vouliez dire possible d'élargir la fin de cette cartographie vers le haut, oui?
zwol
Oui, corrigé. Désolé pour ça!
R .. GitHub STOP AIDER ICE
0

Je peux répondre à votre deuxième question. Malloc échouera et renverra un pointeur nul. C'est pourquoi vous recherchez toujours un pointeur nul lors de l'allocation dynamique de mémoire.

Brian Gordon
la source
alors quelle est l'utilité de brk et sbrk?
nik
3
@NikhilRathod: malloc()utilisera brk()et / ou sbrk()sous le capot - et vous pouvez aussi, si vous souhaitez implémenter votre propre version personnalisée de malloc().
Daniel Pryden
@Daniel Pryden: comment brk et sbrk peuvent-ils fonctionner sur le tas quand il est entre la pile et le segment de données comme indiqué dans le diagramme ci-dessus. pour que cela fonctionne, le tas devrait être à la fin. Ai-je raison?
nik
2
@Brian: Daniel a dit que le système d'exploitation gère le segment de pile , pas le pointeur de pile ... des choses très différentes. Le fait est qu'il n'y a pas d'appel système sbrk / brk pour le segment de pile - Linux alloue automatiquement les pages lors des tentatives d'écriture à la fin du segment de pile.
Jim Balter
1
Et Brian, vous n'avez répondu qu'à la moitié de la moitié de la question. L'autre moitié est ce qui se passe si vous essayez de pousser sur la pile alors qu'aucun espace n'est disponible ... vous obtenez une erreur de segmentation.
Jim Balter
0

Le tas est placé en dernier dans le segment de données du programme. brk()est utilisé pour modifier (développer) la taille du tas. Lorsque le tas ne peut plus croître, tout mallocappel échoue.

Anders Abel
la source
Vous dites donc que tous les diagrammes sur Internet, comme celui de ma question, sont faux. Si possible, pouvez-vous me diriger vers un diagramme correct.
nik
2
@Nikkhil Gardez à l'esprit que le haut de ce diagramme est la fin de la mémoire. Le haut de la pile se déplace vers le bas sur le diagramme à mesure que la pile grandit. Le haut du tas se déplace vers le haut sur le diagramme à mesure qu'il est développé.
Brian Gordon
0

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éro.

monchalve
la source
Il contient également des données statiques non initialisées (non présentes dans l'exécutable) qui peuvent être des déchets.
luser droog
Les données statiques non .bssinitialisées ( ) sont initialisées à tous les bits-zéro par le système d'exploitation avant le démarrage du programme; ceci est en fait garanti par la norme C. Certains systèmes embarqués pourraient ne pas déranger, je suppose (je n'en ai jamais vu un, mais je ne travaille pas tout ce qui est intégré)
zwol
@zwol: Linux a une option de compilation pour ne pas renvoyer à zéro les pages mmap, mais je suppose que le .bssserait toujours mis à zéro. L'espace BSS est probablement le moyen le plus compact d'exprimer le fait qu'un programme veut des tableaux de zéros.
Peter Cordes
1
@PeterCordes Ce que dit la norme C, c'est que les variables globales déclarées sans initialiseur sont traitées comme si elles étaient initialisées à zéro. Une implémentation AC qui place de telles variables .bsset ne met pas à zéro .bssserait donc non conforme. Mais rien n'oblige une implémentation C à utiliser .bssou même à avoir une telle chose.
zwol
@PeterCordes De plus, la ligne entre "l'implémentation C" et le programme peut être très floue, par exemple il y a généralement un petit morceau de code de l'implémentation, lié statiquement dans chaque exécutable, qui s'exécute avant main; ce code pourrait mettre à zéro la .bsszone plutôt que de laisser le noyau le faire, et cela serait toujours conforme.
zwol
0

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.

Skanzariya
la source