Quais resources são compartilhados entre threads?

Recentemente, me perguntaram em uma entrevista qual é a diferença entre um processo e um thread. Realmente, eu não sabia a resposta. Eu pensei por um minuto e dei uma resposta muito estranha.

Threads compartilham a mesma memory, processos não. Depois de responder isso, o entrevistador me deu um sorriso maligno e me fez as seguintes perguntas:

P. Você conhece os segmentos nos quais um programa é dividido?

Minha resposta: sim (achei fácil) Stack, Data, Code, Heap

P. Então, me diga: quais segmentos os threads compartilham?

Eu não pude responder isso e acabei dizendo todos eles.

Por favor, alguém pode apresentar as respostas corretas e impressionantes para a diferença entre um processo e um segmento?

    Você está bem correto, mas os threads compartilham todos os segmentos, exceto a pilha. Threads têm pilhas de chamadas independentes, no entanto a memory em outras pilhas de discussão ainda é acessível e, em teoria, você poderia manter um ponteiro para a memory no quadro de pilha local de outro segmento (embora você provavelmente deva encontrar um lugar melhor para colocar essa memory!).

    Da Wikipedia (acho que isso seria uma boa resposta para o entrevistador: P)

    Threads diferem dos processos do sistema operacional multitarefa tradicional em que:

    • processos são tipicamente independentes, enquanto os threads existem como subconjuntos de um processo
    • os processos transportam informações de estado consideráveis, enquanto vários segmentos dentro de um processo compartilham estado, bem como memory e outros resources
    • os processos têm espaços de endereço separados, enquanto os segmentos compartilham seu espaço de endereço
    • os processos interagem somente através de mecanismos de comunicação entre processos fornecidos pelo sistema.
    • A alternância de contexto entre threads no mesmo processo é tipicamente mais rápida que a alternância de contexto entre processos.

    Algo que realmente precisa ser apontado é que existem realmente dois aspectos para essa questão – o aspecto teórico e o aspecto das implementações.

    Primeiro, vamos olhar para o aspecto teórico. Você precisa entender o que é um processo conceitualmente para entender a diferença entre um processo e um segmento e o que é compartilhado entre eles.

    Nós temos o seguinte da seção 2.2.2 O Modelo de Rosca Clássica em Sistemas Operacionais Modernos 3e por Tanenbaum:

    O modelo de processo é baseado em dois conceitos independentes: agrupamento e execução de resources. Às vezes é útil separá-los; é aqui que entram os fios …

    Ele continua:

    Uma maneira de analisar um processo é que é uma maneira de agrupar resources relacionados. Um processo possui um espaço de endereço contendo dados e dados do programa, além de outros resources. Esses resources podem include arquivos abertos, processos filho, alarmes pendentes, manipuladores de sinais, informações contábeis e muito mais. Ao colocá-los juntos na forma de um processo, eles podem ser gerenciados mais facilmente. O outro conceito que um processo possui é um encadeamento de execução, geralmente encurtado para apenas encadear. O segmento tem um contador de programa que controla quais instruções executar em seguida. Possui registros, que mantêm suas variables ​​de trabalho atuais. Ele tem uma pilha, que contém o histórico de execução, com um quadro para cada procedimento chamado, mas ainda não retornado. Embora um thread deva ser executado em algum processo, o thread e seu processo são conceitos diferentes e podem ser tratados separadamente. Processos são usados ​​para agrupar resources; threads são as entidades agendadas para execução na CPU.

    Mais abaixo, ele fornece a seguinte tabela:

    Per process items | Per thread items ------------------------------|----------------- Address space | Program counter Global variables | Registers Open files | Stack Child processes | State Pending alarms | Signals and signal handlers | Accounting information | 

    O que precede é o que você precisa para os threads funcionarem. Como outros salientaram, coisas como segmentos são detalhes de implementação dependentes do sistema operacional.

    Diga ao entrevistador que isso depende inteiramente da implementação do sistema operacional.

    Tome o Windows x86 por exemplo. Existem apenas 2 segmentos [1], código e dados. E ambos são mapeados para todo o espaço de endereço de 2 GB (linear, usuário). Base = 0, limite = 2 GB. Eles teriam feito um, mas o x86 não permite que um segmento seja Read / Write e Execute. Então eles fizeram dois, e setam CS para apontar para o descritor de código, e o resto (DS, ES, SS, etc) para apontar para o outro [2]. Mas ambos apontam para o mesmo material!

    A pessoa entrevistando você fez uma suposição escondida de que ele / ela não declarou, e esse é um truque idiota de se fazer.

    Então, quanto

    Q. Então me diga qual segmento de segmento compartilha?

    Os segmentos são irrelevantes para a questão, pelo menos no Windows. Segmentos compartilham o espaço de endereço inteiro. Há apenas 1 segmento de pilha, SS, e aponta para o mesmo material que o DS, ES e CS fazem [2]. Ou seja, todo o espaço do usuário . 0-2 GB. Claro, isso não significa que os tópicos tenham apenas 1 pilha. Naturalmente, cada um tem sua própria pilha, mas os segmentos x86 não são usados ​​para essa finalidade.

    Talvez o * nix faça algo diferente. Quem sabe. A premissa da questão foi quebrada.


    1. Pelo menos para o espaço do usuário.
    2. Do ntsd notepad : cs=001b ss=0023 ds=0023 es=0023

    Geralmente, os Threads são chamados de processo leve. Se dividirmos a memory em três seções, ela será: Código, dados e Pilha. Todo processo tem seu próprio código, dados e seções de pilha e devido a esse tempo de troca de contexto é um pouco alto. Para reduzir o tempo de comutação de contexto, as pessoas criaram o conceito de thread, que compartilha dados e segmento de código com outro segmento / processo e tem seu próprio segmento STACK.

    Um processo tem segmentos de código, dados, heap e pilha. Agora, o ponteiro de instrução (IP) de um encadeamento OR threads aponta para o segmento de código do processo. Os segmentos de dados e heap são compartilhados por todos os segmentos. Agora, e a área da pilha? O que é realmente a área da pilha? É uma área criada pelo processo apenas para seu thread usar … porque as pilhas podem ser usadas de uma maneira muito mais rápida do que pilhas etc. A área de pilha do processo é dividida entre threads, ou seja, se houver 3 threads, o área de pilha do processo é dividida em 3 partes e cada uma é dada aos 3 threads. Em outras palavras, quando dizemos que cada thread tem sua própria pilha, essa pilha é, na verdade, uma parte da área da pilha de processos alocada para cada thread. Quando um encadeamento termina sua execução, a pilha do encadeamento é recuperada pelo processo. Na verdade, não apenas a pilha de um processo é dividida entre os threads, mas todo o conjunto de registros que um thread usa como SP, PC e registradores de estado são os registros do processo. Portanto, quando se trata de compartilhamento, o código, as áreas de dados e de heap são compartilhados, enquanto a área de pilha é dividida entre os segmentos.

    Os segmentos compartilham o código e os segmentos de dados e o heap, mas não compartilham a pilha.

    Threads compartilham dados e código enquanto os processos não. A pilha não é compartilhada para ambos.

    Os processos também podem compartilhar memory, código mais preciso, por exemplo, após um Fork() , mas este é um detalhe de implementação e otimização (sistema operacional). O código compartilhado por vários processos será (espero) duplicado na primeira gravação do código – isso é conhecido como copy-on-write . Não tenho certeza sobre a semântica exata do código de threads, mas presumo que seja um código compartilhado.

                Rosca de processo
    
        Pilha particular privada
        Dados privados compartilhados
        Código privado 1 partilhado 2
    

    1 O código é logicamente privado, mas pode ser compartilhado por motivos de desempenho. 2 não tenho 100% de certeza.

    Tópicos compartilham tudo [1]. Há um espaço de endereço para todo o processo.

    Cada thread tem sua própria pilha e registros, mas as pilhas de todas as threads são visíveis no espaço de endereçamento compartilhado.

    Se um thread aloca algum object em sua pilha e envia o endereço para outro thread, ambos terão access igual a esse object.


    Na verdade, acabei de notar um problema mais amplo: acho que você está confundindo dois usos do segmento de palavras.

    O formato de arquivo para um executável (por exemplo, ELF) possui seções distintas, que podem ser referidas como segmentos, contendo código compilado (texto), dados inicializados, símbolos de vinculação, informações de debugging etc. Não há segmentos de pilha ou pilha aqui, já que essas são construções somente em tempo de execução.

    Esses segmentos de arquivos binários podem ser mapeados para o espaço de endereço do processo separadamente, com permissions diferentes (por exemplo, executável somente leitura para código / texto e copy-on-write não executável para dados inicializados).

    As áreas desse espaço de endereçamento são usadas para finalidades diferentes, como alocação de heap e pilhas de encadeamentos, por convenção (imposta pelas bibliotecas de tempo de execução de sua linguagem). É tudo apenas memory, e provavelmente não segmentado, a menos que você esteja executando no modo 8086 virtual. A pilha de cada thread é uma parte da memory alocada no tempo de criação do thread, com o endereço principal da pilha atual armazenado em um registrador de ponteiro de pilha, e cada thread mantém seu próprio ponteiro de pilha juntamente com seus outros registradores.


    [1] OK, eu sei: máscaras de sinal, TSS / TSD etc. O espaço de endereço, incluindo todos os seus segmentos de programa mapeados, ainda é compartilhado.

    Em uma estrutura x86, pode-se dividir o mesmo número de segmentos (até 2 ^ 16-1). As diretivas ASM SEGMENT / ENDS permitem isso, e os operadores SEG e OFFSET permitem a boot de registradores de segmento. CS: IP geralmente são inicializados pelo carregador, mas para DS, ES, SS o aplicativo é responsável pela boot. Muitos ambientes permitem as chamadas “definições simplificadas de segmento” como .code, .data, .bss, .stack etc. e, dependendo também do “modelo de memory” (pequeno, grande, compacto etc.), o carregador inicializa os registradores de segmento. adequadamente. Normalmente .data, .bss, .stack e outros segmentos usuais (eu não faço isso há 20 anos, então não me lembro de todos) são agrupados em um único grupo – é por isso que geralmente DS, ES e SS apontam para o mesma área, mas isso é apenas para simplificar as coisas.

    Em geral, todos os registradores de segmento podem ter valores diferentes em tempo de execução. Então, a pergunta da entrevista estava certa: qual dos CÓDIGOS, DADOS e PILHAS são compartilhados entre os tópicos. O gerenciamento de heap é outra coisa – é simplesmente uma sequência de chamadas para o sistema operacional. Mas e se você não tiver um sistema operacional, como em um sistema embarcado – você ainda pode ter novo / excluir em seu código?

    Meu conselho para os jovens – leia um bom livro de programação de assembly. Parece que os currículos universitários são muito pobres a esse respeito.

    O segmento compartilha o heap (há uma pesquisa sobre o heap específico do encadeamento), mas a implementação atual compartilha o heap. (e, claro, o código)

    No processo, todos os threads compartilham resources do sistema, como memory heap, etc., enquanto o Thread possui sua própria pilha

    Portanto, seu ans deve ser memory heap que todos os threads compartilham para um processo.