O que a chamada do sistema brk () faz?

184

De acordo com o manual do programador Linux:

brk () e sbrk () alteram o local da interrupção do programa, que define o final do segmento de dados do processo.

O que o segmento de dados significa aqui? É apenas o segmento de dados ou dados, BSS e heap combinados?

De acordo com o wiki:

Às vezes, os dados, o BSS e as áreas de heap são coletivamente chamados de "segmento de dados".

Não vejo razão para alterar o tamanho apenas do segmento de dados. Se forem dados, BSS e heap coletivamente, faz sentido, pois o heap terá mais espaço.

O que me leva à minha segunda pergunta. Em todos os artigos que li até agora, o autor diz que a pilha cresce para cima e a pilha para baixo. Mas o que eles não explicam é o que acontece quando o heap ocupa todo o espaço entre o heap e a pilha?

insira a descrição da imagem aqui

nik
fonte
1
Então, o que você faz quando está sem espaço? você troca para o HDD. Quando você usa o espaço, libera-o para outro tipo de informação.
Igoris Azanovas 08/08/11
29
@Igoris: Você está confundindo memória física (que pode ser trocada para o disco conforme necessário, usando memória virtual) e espaço de endereço . Quando você preenche seu espaço de endereço, nenhuma quantidade de troca devolve esses endereços no meio.
Daniel Pryden
7
Apenas como lembrete, a brk()chamada do sistema é mais útil na linguagem assembly do que em C. Em C, malloc()deve ser usada em vez de brk()para fins de alocação de dados - mas isso não invalida a pergunta proposta de forma alguma.
alecov
2
@Brian: O heap é uma estrutura de dados complexa para lidar com regiões de tamanhos e alinhamentos variados, pool gratuito, etc. As pilhas de threads são sempre sequências contíguas (no espaço de endereço virtual) de páginas completas. Na maioria dos sistemas operacionais, há um alocador de páginas subjacente a pilhas, heap e arquivos mapeados na memória.
Ben Voigt
2
@ Brian: Quem disse que há algum "Stack" sendo manipulado por brk()e sbrk()? As pilhas são gerenciadas pelo alocador de páginas, em um nível muito mais baixo.
Ben Voigt

Respostas:

233

No diagrama que você postou, a "quebra" - o endereço manipulado por brke sbrk- é a linha pontilhada na parte superior da pilha.

imagem simplificada do layout da memória virtual

A documentação que você leu descreve isso como o final do "segmento de dados" porque no mmapUnix tradicional (bibliotecas pré-compartilhadas, pré- ) o segmento de dados era contínuo com a pilha; antes do início do programa, o kernel carregava os blocos "texto" e "dados" na RAM, começando no endereço zero (na verdade, um pouco acima do endereço zero, de modo que o ponteiro NULL realmente não apontava para nada) e configurava o endereço de interrupção como o fim do segmento de dados. A primeira chamada para mallocentão seria usada sbrkpara mover a divisão e criar o heap entre a parte superior do segmento de dados e o novo endereço de interrupção mais alto, como mostrado no diagrama, e o uso subsequente de mallocusá-lo para aumentar o heap como necessário.

Enquanto isso, a pilha começa no topo da memória e cresce. A pilha não precisa de chamadas explícitas do sistema para aumentá-la; ou ele começa com o máximo de RAM alocado possível (essa era a abordagem tradicional) ou há uma região de endereços reservados abaixo da pilha, na qual o kernel aloca automaticamente a RAM quando nota uma tentativa de escrever nele (esta é a abordagem moderna). De qualquer forma, pode ou não haver uma região de "guarda" na parte inferior do espaço de endereço que pode ser usada para a pilha. Se essa região existe (todos os sistemas modernos fazem isso), ela é permanentemente mapeada; se quera pilha ou a pilha tenta crescer nela, você recebe uma falha de segmentação. Tradicionalmente, porém, o kernel não fez nenhuma tentativa de impor um limite; a pilha pode crescer na pilha, ou a pilha pode crescer na pilha, e de qualquer maneira eles escreviam sobre os dados uns dos outros e o programa travava. Se você tivesse muita sorte, ele falharia imediatamente.

Não sei de onde vem o número de 512 GB neste diagrama. Isso implica em um espaço de endereço virtual de 64 bits, que é inconsistente com o mapa de memória muito simples que você possui lá. Um espaço de endereço real de 64 bits se parece mais com isso:

espaço de endereço menos simplificado

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

Isso não é remotamente escalável, e não deve ser interpretado como exatamente como qualquer sistema operacional faz as coisas (depois que o desenhei, descobri que o Linux realmente coloca o executável muito mais próximo do endereço zero do que eu pensava, e as bibliotecas compartilhadas em endereços surpreendentemente altos). As regiões negras deste diagrama não são mapeadas - qualquer acesso causa um segfault imediato - e são gigantescas em relação às áreas cinzentas. As regiões cinza claro são o programa e suas bibliotecas compartilhadas (pode haver dezenas de bibliotecas compartilhadas); cada um tem um independentesegmento de texto e dados (e segmento "bss", que também contém dados globais, mas é inicializado com todos os bits zero, em vez de ocupar espaço no executável ou na biblioteca em disco). A pilha não é mais necessariamente contínua com o segmento de dados do executável - eu desenhei dessa maneira, mas parece que o Linux, pelo menos, não faz isso. A pilha não está mais vinculada à parte superior do espaço de endereço virtual e a distância entre o heap e a pilha é tão grande que você não precisa se preocupar em atravessá-la.

A quebra ainda é o limite superior da pilha. No entanto, o que eu não mostrei é que poderia haver dezenas de alocações independentes de memória no escuro em algum lugar, feitas com e mmapnão brk. (O sistema operacional tentará mantê-los longe da brkárea para que não colidam.)

zwol
fonte
7
+1 para uma explicação detalhada. Você sabe se mallocainda depende brkou se está usando mmappara poder "devolver" blocos de memória separados?
Anders Abel
18
Depende da implementação específica, mas o IIUC muitas mallocs atuais usam a brkárea para pequenas alocações e mmaps individuais para grandes (digamos,> 128K) alocações. Veja, por exemplo, a discussão de MMAP_THRESHOLD na página de malloc(3)manual do Linux .
Zwol
1
De fato, uma boa explicação. Mas, como você disse, o Stack não fica mais no topo do espaço de endereço virtual. Isso é verdade apenas para o espaço de endereço de 64 bits ou mesmo para o espaço de endereço de 32 bits. E se a pilha fica na parte superior do espaço de endereço, onde os mapas de memória anônima acontecem? Está no topo do espaço de endereço virtual, pouco antes da pilha.
nik
3
@ Nikhil: é complicado. A maioria dos sistemas de 32 bits coloca a pilha no topo do espaço de endereço no modo de usuário, que geralmente é apenas os 2 ou 3G inferiores do espaço de endereço completo (o espaço restante é reservado para o kernel). Atualmente, não consigo pensar em um que não tenha, mas não conheço todos. A maioria das CPUs de 64 bits não permite que você use todo o espaço de 64 bits; os 10 a 16 bits altos do endereço devem ser todos zero ou todos um. A pilha geralmente é colocada perto do topo dos endereços baixos utilizáveis. Não posso lhe dar uma regra mmap; é extremamente dependente do sistema operacional.
Zwol 9/08
3
@RiccardoBestetti Desperdiça espaço de endereçamento , mas isso é inofensivo - um espaço de endereçamento virtual de 64 bits é tão grande que se você consumisse um gigabyte a cada segundo , ainda levaria 500 anos para ficar sem. [1] A maioria dos processadores nem permite o uso de mais de 2 ^ 48 a 2 ^ 53 bits de endereço virtual (a única exceção que conheço é o POWER4 no modo de tabela de páginas com hash). Não desperdiça RAM física; os endereços não utilizados não são atribuídos à RAM.
Zwol 27/07/16
26

Exemplo mínimo executável

O que a chamada do sistema brk () faz?

Solicita que o kernel permita que você leia e grave em um pedaço de memória contíguo chamado heap.

Se você não perguntar, isso pode ser um defeito.

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

Com 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 upstream .

O exemplo acima pode não chegar a uma nova página e não segfault, mesmo sem o brk, então aqui está uma versão mais agressiva que aloca 16MiB e é muito provável que segfault sem o 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;
}

Testado no Ubuntu 18.04.

Visualização do espaço de endereço virtual

Antes brk:

+------+ <-- Heap Start == Heap End

Depois brk(p + 2):

+------+ <-- Heap Start + 2 * sizof(int) == Heap End 
|      |
| You can now write your ints
| in this memory area.
|      |
+------+ <-- Heap Start

Depois brk(b):

+------+ <-- Heap Start == Heap End

Para entender melhor os espaços de endereço, familiarize-se com a paginação: Como funciona a paginação x86? .

Por que precisamos de ambos brke sbrk?

brkclaro que poderia ser implementado com sbrkcálculos de deslocamento, ambos existem apenas por conveniência.

No back-end, o kernel do Linux v5.0 possui uma única chamada do sistema brkusada para implementar os dois: https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64. tbl # L23

12  common  brk         __x64_sys_brk

É brkPOSIX?

brkcostumava ser POSIX, mas foi removido no POSIX 2001, portanto, a necessidade de _GNU_SOURCEacessar o wrapper glibc.

A remoção provavelmente ocorre devido à introdução mmap, que é um superconjunto que permite que vários intervalos sejam alocados e mais opções de alocação.

Eu acho que não há um caso válido em que você deva usar em brkvez de mallocou mmaphoje em dia.

brk vs malloc

brké uma antiga possibilidade de implementação malloc.

mmapé o novo mecanismo estritamente mais poderoso que provavelmente todos os sistemas POSIX usam atualmente para implementar malloc. Aqui está um exemplo mínimo de mmapalocação de memória executável .

Posso misturar brke malloc?

Se o seu mallocfor implementado brk, não tenho idéia de como isso não pode explodir as coisas, pois brkgerencia apenas um único intervalo de memória.

No entanto, não consegui encontrar nada sobre isso nos documentos da glibc, por exemplo:

As coisas provavelmente funcionarão lá, suponho, uma vez que provavelmente serão mmapusadas malloc.

Veja também:

Mais informações

Internamente, o kernel decide se o processo pode ter tanta memória e marca as páginas de memória para esse uso.

Isso explica como a pilha se compara à pilha: Qual é a função das instruções push / pop usadas nos registradores no assembly x86?

Ciro Santilli adicionou uma nova foto
fonte
4
Como pé um ponteiro para digitar int, não deveria ter sido brk(p + 2);?
Johan Boulé 30/05
Nota pequena: A expressão no loop for da versão agressiva provavelmente deve ser*(p + i) = 1;
lima.sierra 14/11
A propósito, por que precisamos usar um em brk(p + 2)vez de simplesmente aumentá-lo sbrk(2)? Brk é realmente necessário?
Yi Lin Liu
1
@YiLinLiu Acho que são apenas duas interfaces C muito semelhantes para um único back-end do kernel ( brksyscall). brké um pouco mais conveniente restaurar a pilha alocada anteriormente.
Ciro Santilli escreveu
1
@CiroSantilli 中心 改造 中心 996ICU 六四 Considerando o tamanho de int como 4 bytes e o tamanho de um int * como 4 bytes (em uma máquina de 32 bits), eu estava pensando se não deveria ser incrementado em apenas 4 bytes (em vez de 8 - (2 * tamanho de int)). Não deveria apontar para o próximo armazenamento de heap disponível - que fica a 4 bytes de distância (não a 8). Corrija-me se estiver faltando alguma coisa aqui.
Saket Sharad
10

Você pode usar brke sbrk-se para evitar a "sobrecarga malloc" todo mundo está sempre reclamando. Mas você não pode usar esse método facilmente em conjunto com, mallocpor isso é apropriado apenas quando você não precisa fazer freenada. Porque você não pode. Além disso, você deve evitar chamadas de biblioteca que possam ser usadas mallocinternamente. Ou seja. strlenprovavelmente é seguro, mas fopenprovavelmente não é.

Ligue sbrkcomo você chamaria malloc. Ele retorna um ponteiro para a interrupção atual e incrementa a interrupção nesse valor.

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

Embora não seja possível liberar alocações individuais (porque não há sobrecarga de malloc , lembre-se), você pode liberar todo o espaço chamando brkcom o valor retornado pela primeira chamada para sbrk, rebobinando assim o brk .

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

Você pode até empilhar essas regiões, descartando a região mais recente, rebobinando a interrupção no início da região.


Mais uma coisa ...

sbrktambém é útil no código de golfe, pois é 2 caracteres menor que malloc.

luser droog
fonte
7
-1 porque: malloc/ freecertamente pode (e faz) devolver memória ao sistema operacional. Eles nem sempre o fazem quando você deseja, mas isso é uma questão de as heurísticas serem ajustadas imperfeitamente para o seu caso de uso. Mais importante, não é seguro chamar sbrkcom um argumento diferente de zero em qualquer programa que possa chamar malloc- e quase todas as funções da biblioteca C podem chamar mallocinternamente. Os únicos que definitivamente não serão as funções de segurança de sinal assíncrono .
Zwol
E com "não é seguro", quero dizer "seu programa falhará".
Zwol
Eu editei para remover o retorno da memória retornada e mencionei o perigo das funções da biblioteca usarem internamente malloc.
Luser droog
1
Se você quiser fazer uma alocação de memória sofisticada, baseie-a no malloc ou no mmap. Não toque brk e sbrk, são relíquias do passado que fazem mais mal do que bem (mesmo as manpages dizer-lhe para ficar longe deles!)
Eloff
3
Isso é bobo. Se você deseja evitar a sobrecarga do malloc para muitas pequenas alocações, faça uma grande alocação (com malloc ou mmap, não sbrk) e distribua você mesmo. Se você mantiver os nós da sua árvore binária em uma matriz, poderá usar índices 8b ou 16b em vez de ponteiros 64b. Isso funciona muito bem quando você não precisa excluir nenhum nó até estar pronto para excluir todos os nós. (por exemplo, crie um dicionário ordenado em tempo real.) Usar sbrkpara isso é útil apenas para golfe de código, porque o uso manual mmap(MAP_ANONYMOUS)é melhor em todos os aspectos, exceto no tamanho do código-fonte.
Peter Cordes
3

Há um mapeamento de memória privada anônima designado especial (tradicionalmente localizado logo além dos dados / bss, mas o Linux moderno realmente ajustará o local com o ASLR). Em princípio, não é melhor do que qualquer outro mapeamento você poderia criar com é mmap, mas o Linux tem algumas otimizações que tornam possível para expandir o final deste mapeamento (usando o brksyscall) para cima com bloqueio custo reduzido em relação ao que mmapou mremapincorreria. Isso torna atraente o mallocuso das implementações ao implementar o heap principal.

R .. GitHub PARE DE AJUDAR O GELO
fonte
Você significou possível para expandir o final deste mapeamento para cima, sim?
Zwol 09/08/19
Sim, consertado. Me desculpe por isso!
R .. GitHub Pare de ajudar o gelo
0

Eu posso responder sua segunda pergunta. Malloc falhará e retornará um ponteiro nulo. É por isso que você sempre procura um ponteiro nulo ao alocar dinamicamente a memória.

Brian Gordon
fonte
então qual é a utilidade de brk e sbrk?
Nik8 /
3
@ NikhilRathod: malloc()irá usar brk()e / ou sbrk()sob o capô - e você também pode, se quiser implementar sua própria versão personalizada do malloc().
Daniel Pryden
@ Daniel Pryden: como brk e sbrk funcionam no heap quando está entre a pilha e o segmento de dados, como mostra o diagrama acima. para que isso funcione heap deve estar no final. Estou certo?
Nik
2
@Brian: Daniel disse que o sistema operacional gerencia o segmento da pilha , não o ponteiro da pilha ... coisas muito diferentes. O ponto é que não há syscall sbrk / brk para o segmento de pilha - o Linux aloca automaticamente páginas após tentativas de gravação no final do segmento de pilha.
Jim Balter
1
E Brian, você respondeu apenas metade da metade da pergunta. A outra metade é o que acontece se você tentar empurrar para a pilha quando não houver espaço disponível ... você receberá uma falha de segmentação.
Jim Balter
0

A pilha é colocada por último no segmento de dados do programa. brk()é usado para alterar (expandir) o tamanho do heap. Quando o heap não puder crescer mais, qualquer mallocchamada falhará.

Anders Abel
fonte
Então você está dizendo que todos os diagramas da Internet, como o da minha pergunta, estão errados. Se possível, você pode me indicar um diagrama correto.
Nik8 /
2
@ Nikkhil Lembre-se de que a parte superior desse diagrama é o fim da memória. A parte superior da pilha se move para baixo no diagrama à medida que a pilha cresce. A parte superior do heap se move para cima no diagrama à medida que é expandida.
Brian Gordon
0

O segmento de dados é a parte da memória que contém todos os seus dados estáticos, lidos no executável na inicialização e geralmente preenchidos com zero.

monchalve
fonte
Ele também contém dados estáticos não inicializados (não presentes no executável) que podem ser lixo.
Luser droog
Dados estáticos não inicializados ( .bss) são inicializados em todos os bits-zero pelo sistema operacional antes do início do programa; isso é realmente garantido pelo padrão C. Alguns sistemas embarcados podem não incomoda, acho que (eu nunca vi um, mas eu não trabalho tudo o que incorporado)
Zwol
@zwol: O Linux tem uma opção em tempo de compilação para não zerar as páginas retornadas mmap, mas eu suponho .bssque ainda seria zerado. O espaço do BSS é provavelmente a maneira mais compacta de expressar o fato de que um programa deseja algumas matrizes zerod.
Peter Cordes
1
@PeterCordes O que o padrão C diz é que variáveis ​​globais declaradas sem inicializador são tratadas como se fossem inicializadas com zero. A implementação de CA que coloca essas variáveis .bsse não zera .bssseria, portanto, não conforme. Mas nada obriga uma implementação C a usar .bssou mesmo ter uma coisa dessas.
Zwol
@PeterCordes Além disso, a linha entre a "implementação C" e o programa pode ser muito confusa, por exemplo, geralmente há um pequeno pedaço de código da implementação, vinculado estaticamente a cada executável, que é executado anteriormente main; esse código poderia zerar a .bssárea em vez de ter o kernel, e isso ainda estaria em conformidade.
Zwol
0

O malloc usa a chamada do sistema brk para alocar memória.

incluir

int main(void){

char *a = malloc(10); 
return 0;
}

executar este programa simples com strace, ele chamará o sistema brk.

skanzariya
fonte