Os threads são implementados como processos no Linux?

65

Estou analisando este livro , Programação Avançada em Linux por Mark Mitchell, Jeffrey Oldham e Alex Samuel. É de 2001, um pouco velho. Mas acho isso muito bom de qualquer maneira.

No entanto, cheguei a um ponto em que ele diverge do que meu Linux produz na saída do shell. Na página 92 ​​(116 no visualizador), o capítulo 4.5 Implementação de Thread GNU / Linux começa com o parágrafo que contém esta declaração:

A implementação de threads POSIX no GNU / Linux difere da implementação de threads em muitos outros sistemas semelhantes ao UNIX de uma maneira importante: no GNU / Linux, os threads são implementados como processos.

Este parece ser um ponto-chave e é posteriormente ilustrado com um código C. A saída do livro é:

main thread pid is 14608
child thread pid is 14610

E no meu Ubuntu 16.04 é:

main thread pid is 3615
child thread pid is 3615

ps saída suporta isso.

Eu acho que algo deve ter mudado entre 2001 e agora.

O próximo subcapítulo na próxima página, 4.5.1 Manipulação de sinais, se baseia na instrução anterior:

O comportamento da interação entre sinais e threads varia de um sistema semelhante ao UNIX para outro. No GNU / Linux, o comportamento é ditado pelo fato de os threads serem implementados como processos.

E parece que isso será ainda mais importante mais adiante neste livro. Alguém poderia explicar o que está acontecendo aqui?

Eu já vi este. Os threads do kernel do Linux são realmente processos do kernel? , mas isso não ajuda muito. Estou confuso.

Este é o código C:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* thread_function (void* arg)
{
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());
    /* Spin forever. */
    while (1);
    return NULL;
}

int main ()
{
    pthread_t thread;
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());
    pthread_create (&thread, NULL, &thread_function, NULL);
    /* Spin forever. */
    while (1);
    return 0;
}
Tomasz
fonte
11
Não entendo qual é a fonte da sua confusão. Os encadeamentos são implementados como processos que compartilham o espaço de endereço com seus pais.
Johan Myréen
2
@ JohanMyréen Então, por que os pids de tópicos são iguais?
Tomasz
Ah, agora eu vejo. Sim, algo realmente mudou. Veja a resposta de @ ilkkachu.
Johan Myréen
5
Os encadeamentos ainda são implementados como processos - no entanto, agora getpidretorna o que seria chamado de ID do grupo de encadeamentos e para obter um ID exclusivo para um processo que você precisa usar gettid. No entanto, além do kernel, a maioria das pessoas e ferramentas chama um grupo de encadeamentos de processo e chama um processo de encadeamento, por consistência com outros sistemas.
User253751
Na verdade não. Um processo tem seus próprios descritores de memória e arquivo, ele nunca é chamado um fio, isso seria em consistente com outros sistemas.
Reinierpost

Respostas:

50

Eu acho que essa parte da clone(2)página de manual pode esclarecer a diferença. o PID:

CLONE_THREAD (desde o Linux 2.4.0-test8)
Se CLONE_THREAD estiver definido, o filho será colocado no mesmo grupo de encadeamentos do processo de chamada.
Grupos de threads foram um recurso adicionado no Linux 2.4 para suportar a noção de threads POSIX de um conjunto de threads que compartilham um único PID. Internamente, esse PID compartilhado é o chamado TGID (identificador de grupo de encadeamentos) para o grupo de encadeamentos. Desde o Linux 2.4, as chamadas para getpid (2) retornam o TGID do chamador.

A frase "threads são implementados como processos" refere-se ao problema de threads que tiveram PIDs separados no passado. Basicamente, o Linux originalmente não tinha threads em um processo, apenas processos separados (com PIDs separados) que poderiam ter alguns recursos compartilhados, como memória virtual ou descritores de arquivo. CLONE_THREADea separação do ID do processo (*) e do ID do encadeamento fazem com que o comportamento do Linux se pareça mais com outros sistemas e mais com os requisitos do POSIX nesse sentido. Embora tecnicamente o sistema operacional ainda não tenha implementações separadas para threads e processos.

O manuseio de sinais foi outra área problemática da antiga implementação. Isso é descrito com mais detalhes no documento que @FooF se refere em sua resposta .

Conforme observado nos comentários, o Linux 2.4 também foi lançado em 2001, no mesmo ano do livro, portanto, não é de surpreender que as notícias não cheguem a essa impressão.

ilkkachu
fonte
2
processos separados que podem ter alguns recursos compartilhados, como memória virtual ou descritores de arquivo. Ainda é assim que os encadeamentos do Linux funcionam, com os problemas que você menciona terem sido limpos. Eu diria que chamar as unidades de agendamento usadas nos "threads" ou "processos" do kernel é realmente irrelevante. O fato de terem começado no Linux sendo chamado apenas de "processos" não significa que isso é tudo o que são agora.
Andrew Henle
@AndrewHenle, sim, editou um pouco. Espero que capte o seu pensamento, apesar de parecer difícil escrever. (vá em frente e edite essa parte, se quiser.) Entendi que alguns outros sistemas operacionais semelhantes ao Unix têm uma separação mais distinta de threads versus processos, com o Linux sendo uma espécie de exceção por realmente ter apenas um tipo de veiculação ambas as funções. Mas eu não sei o suficiente sobre outros sistemas e não tenho fontes úteis, por isso é difícil dizer algo concreto.
Ilkkachu
@tomas Observe que esta resposta explica como o Linux funciona agora. Como ilkkachu sugere, funcionou de maneira diferente quando o livro foi escrito. A resposta do FooF explica como o Linux funcionava na época.
Gilles 'SO- stop be evil'
38

Você está certo, de fato "algo deve ter mudado entre 2001 e agora". O livro que você está lendo descreve o mundo de acordo com a primeira implementação histórica de threads POSIX no Linux, chamada LinuxThreads (consulte também o artigo da Wikipedia para alguns).

O LinuxThreads teve alguns problemas de compatibilidade com o padrão POSIX - por exemplo, threads que não compartilham PIDs - e alguns outros problemas sérios. Para corrigir essas falhas, outra implementação chamada NPTL (Native POSIX Thread Library) foi liderada pela Red Hat para adicionar o suporte necessário à biblioteca do kernel e do espaço do usuário para alcançar uma melhor conformidade com o POSIX (tirando boas partes de outro projeto de reimplementação da IBM chamado NGPT (" Threads Posix da próxima geração "), consulte o artigo da Wikipedia sobre NPTL ). Os sinalizadores adicionais adicionados à clone(2)chamada do sistema (principalmente os CLONE_THREADque @ikkkachuapontam em sua resposta ) são provavelmente a parte mais evidente das modificações do kernel. A parte do espaço do usuário do trabalho acabou sendo incorporada à GNU C Library.

Ainda hoje em dia, alguns SDKs Linux embarcados usam a antiga implementação LinuxThreads porque estão usando uma versão menor da LibC, chamada uClibc (também chamada µClibc) , e levou um período considerável de anos até que a implementação do espaço do usuário NPTL do GNU LibC fosse portada e assumida como implementação de encadeamento padrão do POSIX, como geralmente essas plataformas especiais não se esforçam para seguir as novas modas com a velocidade da luz. Isso pode ser observado ao observar que, de fato, os PIDs para diferentes threads nessas plataformas também são diferentes, diferentemente do padrão do POSIX especificado - assim como o livro que você está lendo descreve. Na verdade, uma vez que você ligoupthread_create(), você subitamente aumentou a contagem de processos de um para três, pois eram necessários processos adicionais para manter a bagunça unida.

A página de manual do Linux pthreads (7) fornece uma visão geral abrangente e interessante das diferenças entre os dois. Outra descrição esclarecedora, embora desatualizada, das diferenças é este artigo de Ulrich Depper e Ingo Molnar sobre o design do NPTL.

Eu recomendo que você não leve essa parte do livro muito a sério. Em vez disso, recomendo os tópicos de programação do POSIX de Butenhof e as páginas de manual do POSIX e Linux sobre o assunto. Muitos tutoriais sobre o assunto são imprecisos.

FooF
fonte
22

Os encadeamentos (espaço de usuário) não são implementados como processos no Linux, na medida em que não possuem seu próprio espaço de endereço privado, eles ainda compartilham o espaço de endereço do processo pai.

No entanto, esses encadeamentos são implementados para usar o sistema de contabilidade de processo do kernel; portanto, eles recebem seu próprio ID de encadeamento (TID), mas recebem o mesmo PID e 'ID do grupo de encadeamentos (TGID) que o processo pai - isso contrasta com um fork, onde um novo TGID e PID são criados, e o TID é o mesmo que o PID.

Portanto, parece que os kernels recentes tinham um TID separado que pode ser consultado; é diferente para os threads; um trecho de código adequado para mostrar isso em cada uma das principais () threads_funções () acima é:

    long tid = syscall(SYS_gettid);
    printf("%ld\n", tid);

Portanto, o código inteiro com isso é:

#include <pthread.h>                                                                                                                                          
#include <stdio.h>                                                                                                                                            
#include <unistd.h>                                                                                                                                           
#include <syscall.h>                                                                                                                                          

void* thread_function (void* arg)                                                                                                                             
{                                                                                                                                                             
    long tid = syscall(SYS_gettid);                                                                                                                           
    printf("child thread TID is %ld\n", tid);                                                                                                                 
    fprintf (stderr, "child thread pid is %d\n", (int) getpid ());                                                                                            
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return NULL;                                                                                                                                              
}                                                                                                                                                             

int main ()                                                                                                                                                   
{                                                                                                                                               
    pthread_t thread;                                                                               
    long tid = syscall(SYS_gettid);     
    printf("main TID is %ld\n", tid);                                                                                             
    fprintf (stderr, "main thread pid is %d\n", (int) getpid ());                                                    
    pthread_create (&thread, NULL, &thread_function, NULL);                                           
    /* Spin forever. */                                                                                                                                       
    while (1);                                                                                                                                                
    return 0;                                                                                                                                                 
} 

Fornecendo um exemplo de saída de:

main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
einonm
fonte
3
@tomas einonm está certo. Desconsidere o que o livro diz, é terrivelmente confuso. Não sei que idéia o autor queria transmitir, mas falhou muito. Portanto, no Linux você tem threads do Kernel e threads do espaço do usuário. Os threads do kernel são essencialmente processos sem o espaço do usuário. Os encadeamentos do espaço do usuário são encadeamentos POSIX normais. Os processos de espaço do usuário compartilham descritores de arquivo, podem compartilhar segmentos de código, mas residem em espaços de endereço virtual completamente separados. Os encadeamentos de espaço do usuário em um processo compartilham segmento de código, memória estática e heap (memória dinâmica), mas possuem conjuntos e pilhas de registros de processador separados.
Boris Burkov
8

Basicamente, as informações em seu livro são historicamente precisas, devido a um histórico de implementação vergonhosamente ruim de threads no Linux. Esta resposta minha para uma pergunta relacionada ao SO também serve como resposta para sua pergunta:

https://stackoverflow.com/questions/9154671/distinction-between-processes-and-threads-in-linux/9154725#9154725

Essas confusões decorrem do fato de que os desenvolvedores do kernel originalmente tinham uma visão irracional e errada de que os threads podiam ser implementados quase inteiramente no espaço do usuário usando os processos do kernel como primitivos, desde que o kernel oferecesse uma maneira de fazê-los compartilhar descritores de memória e arquivo . Isso levou à implementação LinuxThreads notoriamente ruim dos encadeamentos POSIX, que era um nome impróprio porque não dava nada remotamente semelhante à semântica de encadeamentos POSIX. Eventualmente, o LinuxThreads foi substituído (por NPTL), mas muitas terminologias e mal-entendidos confusos persistem.

A primeira e mais importante coisa a se perceber é que "PID" significa coisas diferentes no espaço do kernel e no espaço do usuário. O que o kernel chama de PIDs são na verdade IDs de encadeamento no nível do kernel (geralmente chamados TIDs), que não devem ser confundidos com o pthread_tqual é um identificador separado. Cada encadeamento no sistema, seja no mesmo processo ou em outro, possui um TID exclusivo (ou "PID" na terminologia do kernel).

O que é considerado um PID no sentido POSIX de "processo", por outro lado, é chamado de "ID do grupo de encadeamentos" ou "TGID" no kernel. Cada processo consiste em um ou mais encadeamentos (processos do kernel), cada um com seu próprio TID (PID do kernel), mas todos compartilhando o mesmo TGID, que é igual ao TID (PID do kernel) do encadeamento inicial em que mainé executado.

Quando topmostra os threads, está mostrando TIDs (PIDs do kernel), não PIDs (TGIDs do kernel), e é por isso que cada thread possui um separado.

Com o advento do NPTL, a maioria das chamadas de sistema que usam um argumento PID ou atuam no processo de chamada foram alteradas para tratar o PID como um TGID e atuar em todo o "grupo de encadeamentos" (processo POSIX).

R ..
fonte
8

Internamente, não existem processos ou threads no kernel do linux. Processos e threads são um conceito principalmente de usuário, o próprio kernel vê apenas "tarefas", que são um objeto escalonável que pode compartilhar nenhum, alguns ou todos os seus recursos com outras tarefas. Os encadeamentos são tarefas configuradas para compartilhar a maioria de seus recursos (espaço de endereço, mmaps, pipes, manipuladores de arquivos abertos, soquetes etc.) com a tarefa pai, e processos são tarefas configuradas para compartilhar recursos mínimos com a tarefa pai .

Ao usar a API do Linux diretamente ( clone () , em vez de fork () e pthread_create () ), você tem muito mais flexibilidade para definir a quantidade de recursos que deseja compartilhar ou não e pode criar tarefas que não são totalmente processo nem totalmente um thread. Se você usar essas chamadas de baixo nível diretamente, também será possível criar uma tarefa com um novo TGID (assim tratado como um processo pela maioria das ferramentas da terra do usuário) que compartilhe todos os seus recursos com a tarefa pai ou vice-versa, para criar uma tarefa com TGID compartilhado (assim tratado como um encadeamento pela maioria das ferramentas da terra do usuário) que não compartilha nenhum recurso com sua tarefa pai.

Enquanto o Linux 2.4 implementa o TGID, isso é principalmente para o benefício da contabilidade de recursos. Muitos usuários e a ferramenta de espaço de usuário acham útil poder agrupar tarefas relacionadas e relatar o uso de recursos.

A implementação de tarefas no Linux é muito mais fluida do que a visão de mundo dos processos e threads apresentada pelas ferramentas de espaço do usuário.

Lie Ryan
fonte
O documento @FooF vinculado a descreve vários pontos em que o kernel precisa considerar processos e threads como entidades separadas (por exemplo, manipulação de sinal e exec ()); portanto, depois de lê-lo, eu realmente não diria que "não existe tal como processos ou threads no kernel do Linux. "
Ilkkachu 13/05
5

Linus Torvalds declarou em uma lista de discussão do kernel postada em 1996 que “tanto os threads quanto os processos são tratados como um 'contexto de execução'", que é "apenas um conglomerado de todo o estado desse CoE .... inclui coisas como CPU estado, estado da MMU, permissões e vários estados de comunicação (arquivos abertos, manipuladores de sinais, etc) ".

// simple program to create threads that simply sleep
// compile in debian jessie with apt-get install build-essential
// and then g++ -O4 -Wall -std=c++0x -pthread threads2.cpp -o threads2
#include <string>
#include <iostream>
#include <thread>
#include <chrono>

// how many seconds will the threads sleep for?
#define SLEEPTIME 100
// how many threads should I start?
#define NUM_THREADS 25

using namespace std;

// The function we want to execute on the new thread.
void threadSleeper(int threadid){
    // output what number thread we've created
    cout << "task: " << threadid << "\n";
    // take a nap and sleep for a while
    std::this_thread::sleep_for(std::chrono::seconds(SLEEPTIME));
}

void main(){
    // create an array of thread handles
    thread threadArr[NUM_THREADS];
    for(int i=0;i<NUM_THREADS;i++){
        // spawn the threads
        threadArr[i]=thread(threadSleeper, i);
    }
    for(int i=0;i<NUM_THREADS;i++){
        // wait for the threads to finish
        threadArr[i].join();
    }
    // program done
    cout << "Done\n";
    return;
}

Como você pode ver, este programa gerará 25 threads de uma só vez, cada um dos quais dormirá por 100 segundos e depois ingressará no programa principal novamente. Depois que todos os 25 threads retornaram ao programa, o programa está concluído e será encerrado.

Usando topvocê poderá ver 25 instâncias do programa "threads2". Mas é muito chato. A saída de ps auwxé ainda menos interessante ... MAS ps -eLffica meio emocionante.

UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
debian     689   687   689  0    1 14:52 ?        00:00:00 sshd: debian@pts/0  
debian     690   689   690  0    1 14:52 pts/0    00:00:00 -bash
debian    6217   690  6217  0    1 15:04 pts/0    00:00:00 screen
debian    6218  6217  6218  0    1 15:04 ?        00:00:00 SCREEN
debian    6219  6218  6219  0    1 15:04 pts/1    00:00:00 /bin/bash
debian    6226  6218  6226  0    1 15:04 pts/2    00:00:00 /bin/bash
debian    6232  6219  6232  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6233  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6234  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6235  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6236  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6237  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6238  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6239  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6240  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6241  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6242  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6243  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6244  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6245  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6246  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6247  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6248  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6249  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6250  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6251  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6252  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6253  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6254  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6255  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6256  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6232  6219  6257  0   26 15:04 pts/1    00:00:00 ./threads2
debian    6260  6226  6260  0    1 15:04 pts/2    00:00:00 ps -eLf

Você pode ver aqui todos os 26 CoEs que o thread2programa criou. Todos eles compartilham o mesmo ID do processo (PID) e ID do processo pai (PPID), mas cada um tem um ID LWP diferente (processo leve), e o número de LWPs (NLWP) indica que existem 26 CoEs - o programa principal e o 25 tópicos gerados por ele.

ivanivan
fonte
Correto, um fio é apenas um processo leve (LWP)
fpmurphy
2

Quando se trata de processos e threads Linux são espécie da mesma coisa. O que quer dizer que eles são criados com a mesma chamada de sistema: clone.

Se você pensar bem, a diferença entre threads e processos está em que objetos do kernel serão compartilhados pelo filho e pelo pai. Para processos, não é muito: descritores de arquivos abertos, segmentos de memória que não foram gravados, provavelmente alguns outros nos quais não consigo pensar em detalhes. Para threads, muito mais objetos são compartilhados, mas não todos.

O que aproxima threads e objetos no Linux é a unsharechamada do sistema. Os objetos do kernel que começam como compartilhados podem não ser compartilhados após a criação do thread. Assim, você pode, por exemplo, ter dois encadeamentos do mesmo processo que possuem espaço diferente para o descritor de arquivo (revogando o compartilhamento de descritores de arquivo após a criação dos encadeamentos). Você pode testá-lo criando um encadeamento, chamando unsharenos dois encadeamentos e fechando todos os arquivos e abrindo novos arquivos, canais ou objetos nos dois encadeamentos. Então olhe para dentro /proc/your_proc_fd/task/*/fde verá que cada um task(que você criou como um thread) terá diferentes fd's.

De fato, tanto a criação de novos threads quanto de novos processos são rotinas de bibliotecas que chamam clonepor baixo e especificam quais objetos do kernel o processo-thread-thingamajig recém criado (ou seja, task) compartilhará com o processo / thread de chamada.

Dmitry Rubanovich
fonte