O que se entende por pilha em conexão com um processo?

2 respostas

6

No contexto de um processo Unix ou Linux, a frase "a pilha" pode significar duas coisas.

Primeiro, "a pilha" pode significar os registros de entrada e saída da sequência de chamada do fluxo de controle. Quando um processo é executado, main() é chamado primeiro. main() pode chamar printf() . O código gerado pelo compilador grava o endereço da string de formato e quaisquer outros argumentos para printf() em alguns locais da memória. Em seguida, o código grava o endereço para o qual o fluxo de controle deve retornar após a conclusão de printf() . Em seguida, o código chama um salto ou uma ramificação para o início de printf() . Cada thread possui uma dessas pilhas de registro de ativação de função. Observe que muitas CPUs têm instruções de hardware para configurar e manter a pilha, mas que outras CPUs (IBM 360, ou qualquer que seja o nome dela) realmente usaram listas vinculadas que poderiam estar espalhadas pelo espaço de endereço.

Segundo, "a pilha" pode significar os locais de memória para os quais a CPU grava argumentos para funções, e o endereço para o qual uma função chamada deve retornar. "A pilha" refere-se a uma parte contígua do espaço de endereço do processo.

A memória em um processo Unix ou Linux ou * BSD é uma linha muito longa, começando em cerca de 0x400000 e terminando em aproximadamente 0x7fffffffffff (em CPUs x86_64). O espaço de endereço da pilha começa no maior endereço numérico. Toda vez que uma função é chamada, a pilha de registros de ativação de função "cresce": o código de processo coloca argumentos de função e um endereço de retorno na pilha de registros de ativação e decrementa o ponteiro de pilha, um registro de CPU especial usado para rastrear onde no espaço de endereço da pilha, os valores das variáveis atuais do processo residem.

Cada thread recebe um pedaço da "pilha" (espaço de endereço de pilha) para uso próprio como uma pilha de registros de ativação de função. Em algum lugar entre 0x7fffffffffff e um endereço inferior, cada thread tem uma área de memória reservada para uso em chamadas de função. Geralmente, isso é imposto apenas por convenção, não por hardware, portanto, se o thread chamar função após função aninhada, a parte inferior da pilha desse segmento poderá sobrescrever o topo da pilha de outro segmento.

Assim, cada segmento tem um pedaço da área de memória "pilha" e é daí que vem a terminologia "pilha compartilhada". É uma conseqüência de um espaço de endereço de processo ser um único bloco linear de memória e os dois usos do termo "pilha". Tenho certeza que algumas JVMs mais antigas (na verdade antigas) só tinham um único thread. Qualquer encadeamento dentro do código Java foi realmente feito por um único encadeamento real. JVMs mais novas, JVMs que invocam encadeamentos reais para executar encadeamentos Java, terão a mesma "pilha compartilhada" descrita acima. O Linux e o Plan 9 possuem uma chamada de sistema de início de processo (clone () para Linux, rfork () no Plan 9) que pode configurar processos que compartilham partes do espaço de endereço e talvez diferentes espaços de endereço de pilha, mas esse estilo de encadeamento nunca realmente pegou.

    
por 28.12.2012 / 13:54
1

What does the author mean by stacks here ? I do Java programming and know each thread gets its own stack . So I am confused by shared stacks concept here.

O uso do plural é um pouco estranho e parece enganoso, talvez o ponto é que as múltiplas pilhas de um programa multi-thread compartilham o mesmo espaço de endereço.

Como Bruce Ediger descreve, "stack" refere-se a uma única região de espaço de endereço contíguo em que os dados são colocados no LIFO. No entanto, cada thread nativo em um processo tem uma região contígua - não há um dividido entre cada um deles. Criar um thread requer a alocação de uma pilha adicional do mesmo tamanho (este é um número fixo, por exemplo, o padrão no linux é 8 MB), e é por isso que aplicações excessivamente multi-thread podem consumir quantidades excessivas de memória.

O Java usa encadeamentos nativos para implementar encadeamentos java, mas gerencia uma "pilha" para cada um dos encadeamentos java, independente do kernel. Isso significa colocar alguma memória global de lado para usar com esse propósito; veja a terceira parte da resposta principal aqui:

link

Naturalmente, isso se refere a uma implementação específica (openjdk), mas presumivelmente todos eles precisam fazer isso (alocar heap nativamente para uso interno como uma "pilha de encadeamentos").

Como a pilha individual usada por cada thread nativo é gerenciada pelo kernel, discordo da implicação de Bruce de que isso faz parte de uma pilha geral pertencente ao processo como um todo - novamente, um único (também não) thread O processo tem apenas uma pilha, mas o thread principal em um processo multi-thread não compartilha o espaço da pilha com os outros threads.

Uma "pilha compartilhada" seria aquela em que todos os dados são armazenados começando com um único endereço - este é o sentido em que as chamadas de funções aninhadas compartilham a mesma pilha. No entanto, usando uma versão ajustada de um programa de exemplo mencionado abaixo por Bruce, da versão linux do pthread_create man page página man:

./a.out one two three
In main() stack starts near: 0x7fff17b80f98
Thread 1: top of stack near 0x7f11ac6d3e78; argv_string=one
Thread 2: top of stack near 0x7f11abed2e78; argv_string=two
Thread 3: top of stack near 0x7f11ab6d1e78; argv_string=three

Este programa usa o endereço de uma variável local na função encadeada; o tweak que eu adicionei foi fazer a mesma coisa em main() antes dos threads serem criados. A execução está em um sistema linux recente de 64 bits; note que os endereços iniciais para os threads estão EXATAMENTE separados por 8 MB, e estão muito longe do topo da pilha de threads principal. Compare isso com chamadas de função aninhadas assim:

#include <stdio.h>

void eg (int n) {
    char *p;
    printf("#%d first variable at %p\n", n, &p);
    if (n < 3) eg(n+1);
}

int main(int argc, const char *argv[]) {
    char *p;
    printf("main() first variable at %p\n", &p);
    eg(1);
    return 0;
}

Exemplo de execução:

main() first variable at 0x7fffef0aaf68
#1 first variable at 0x7fffef0aaf38
#2 first variable at 0x7fffef0aaf08
#3 first variable at 0x7fffef0aaed8

Estes são apenas 48 bytes separados - em outras palavras, eles estão sendo colocados um após o outro contiguamente (com outros pequenos bits de dados reais) sem espaço não utilizado entre eles. Se usarmos isso no programa multi-threaded, sem a recursão aninhada, mas chamando eg() uma vez em cada thread:

Thread 1: top of stack near 0x7f4bd5061e78; argv_string=one
Thread 2: top of stack near 0x7f4bd4860e78; argv_string=two
Thread 3: top of stack near 0x7f4bd405fe78; argv_string=three
#3 first variable at 0x7f4bd405fe48
#1 first variable at 0x7f4bd5061e48
#2 first variable at 0x7f4bd4860e48

A variável para cada um está perto do topo das três pilhas separadas.

Tudo isso está na alta região de endereços, conhecida como "espaço de pilha", mas em um programa multiencadeado, que é dividido em várias pilhas ; não é tratado como uma grande estrutura LIFO.

    
por 28.12.2012 / 15:35