Os threads são implementados como processos no Linux?

59

Eu estou passando por este livro , Advanced Linux Programming por Mark Mitchell, Jeffrey Oldham e Alex Samuel. É de 2001, um pouco velho. Mas eu acho muito bom de qualquer maneira.

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

The implementation of POSIX threads on GNU/Linux differs from the thread implementation on many other UNIX-like systems in an important way: on GNU/Linux, threads are implemented as processes.

Este parece ser um ponto chave e é posteriormente ilustrado com um código C. A saída no 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 output suporta isso.

Acho que algo deve ter mudado entre 2001 e agora.

O próximo subcapítulo na próxima página, 4.5.1 Tratamento de Sinais, se baseia na declaração anterior:

The behavior of the interaction between signals and threads varies from one UNIX-like system to another. In GNU/Linux, the behavior is dictated by the fact that threads are implemented as processes.

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

Eu vi este Os threads de 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;
}
    
por Tomasz 12.05.2017 / 15:58

7 respostas

46

Acho que essa parte da página clone(2) man pode esclarecer diferença re. o PID:

CLONE_THREAD (since Linux 2.4.0-test8)
If CLONE_THREAD is set, the child is placed in the same thread group as the calling process.
Thread groups were a feature added in Linux 2.4 to support the POSIX threads notion of a set of threads that share a single PID. Internally, this shared PID is the so-called thread group identifier (TGID) for the thread group. Since Linux 2.4, calls to getpid(2) return the TGID of the caller.

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

O tratamento de sinal era outra área problemática com a implementação antiga. Isso é descrito em mais detalhes no artigo @FooF refere-se a em sua resposta .

Como observado nos comentários, o Linux 2.4 também foi lançado em 2001, o mesmo ano que o livro, então não surpreende que as notícias não tenham sido impressas.

    
por 12.05.2017 / 16:08
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 (veja também artigo Wikipedia para alguns).

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

Ainda hoje em dia alguns SDKs Linux incorporados usam a antiga implementação LinuxThreads porque eles estão usando uma versão menor de memória do LibC chamada uClibc (também chamada µClibc) , e levou uma quantidade considerável de anos antes que a implementação do espaço do usuário NPTL do GNU LibC fosse portada e assumida como implementação padrão de threading POSIX, já que geralmente essas plataformas especiais não se esforçam para seguir as últimas modas com Velocidade da luz. Isso pode ser observado notando que, de fato, os PIDs para diferentes threads nessas plataformas também são diferentes, ao contrário do padrão POSIX especifica - exatamente como o livro que você está lendo descreve. Na verdade, uma vez que você chamou pthread_create() , de repente você aumentou a contagem de processos de um para três, pois era necessário um processo adicional para manter a bagunça.

A página de manual pthreads (7) do Linux fornece uma visão abrangente e interessante de as diferenças entre os dois. Outra descrição esclarecedora, embora fora de moda, das diferenças é o 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 eu recomendo os tópicos POSIX de programação de Butenhof e páginas de manual POSIX e Linux sobre o assunto. Muitos tutoriais sobre o assunto são imprecisos.

    
por 12.05.2017 / 20:31
22
Os encadeamentos

(Userspace) não são implementados como processos no Linux, na medida em que eles não têm 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, recebem seu próprio ID de encadeamento (TID), mas recebem o mesmo PID e ID do grupo de encadeamentos (TGID). em contraste 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, é isso que é diferente para threads, um snippet de código adequado para mostrar isso em cada thread_function principal () acima é:

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

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

Dando uma saída de exemplo de:

main TID is 17963
main thread pid is 17963
thread TID is 17964
child thread pid is 17963
    
por 12.05.2017 / 16:11
7

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

link

These confusions all stem from the fact that the kernel developers originally held an irrational and wrong view that threads could be implemented almost entirely in userspace using kernel processes as the primitive, as long as the kernel offered a way to make them share memory and file descriptors. This lead to the notoriously bad LinuxThreads implementation of POSIX threads, which was rather a misnomer because it did not give anything remotely resembling POSIX thread semantics. Eventually LinuxThreads was replaced (by NPTL), but a lot of the confusing terminology and misunderstandings persist.

The first and most important thing to realize is that "PID" means different things in kernel space and user space. What the kernel calls PIDs are actually kernel-level thread ids (often called TIDs), not to be confused with pthread_t which is a separate identifier. Each thread on the system, whether in the same process or a different one, has a unique TID (or "PID" in the kernel's terminology).

What's considered a PID in the POSIX sense of "process", on the other hand, is called a "thread group ID" or "TGID" in the kernel. Each process consists of one or more threads (kernel processes) each with their own TID (kernel PID), but all sharing the same TGID, which is equal to the TID (kernel PID) of the initial thread in which main runs.

When top shows you threads, it's showing TIDs (kernel PIDs), not PIDs (kernel TGIDs), and this is why each thread has a separate one.

With the advent of NPTL, most system calls that take a PID argument or act on the calling process were changed to treat the PID as a TGID and act on the whole "thread group" (POSIX process).

    
por 13.05.2017 / 06:17
7

Internamente, não existem processos ou threads no kernel do Linux. Processos e encadeamentos são um conceito predominantemente userland, 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. Threads são tarefas que foram configuradas para compartilhar a maioria de seus recursos (espaço de endereço, mmaps, pipes, manipuladores de arquivos abertos, sockets, etc) com a tarefa pai, e processos são tarefas que foram configuradas para compartilhar recursos mínimos com a tarefa pai. .

Quando você usa a API do Linux diretamente ( clone () , em vez de < href="https://linux.die.net/man/2/fork"> fork () e pthread_create () ), então você tem muito mais flexibilidade na definição de quantos recursos compartilhar ou não, e pode criar tarefas que não são totalmente um processo nem totalmente um thread. Se você usar essas chamadas de baixo nível diretamente, também é possível criar uma tarefa com um novo TGID (assim tratado como um processo pela maioria das ferramentas userland) que realmente 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 de usuário) que não compartilha nenhum recurso com sua tarefa pai.

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

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

    
por 13.05.2017 / 15:28
5

Linus Torvalds afirmou em um post na lista de discussão do kernel em 1996 que “ambos os threads e processos são tratados como um 'contexto de execução'”, que é “apenas um conglomerado de todo o estado do CoE ... coisas como estado da CPU, 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 irá gerar 25 threads de uma só vez, cada um dos quais durará 100 segundos e depois voltará ao programa principal. Depois de todos os 25 tópicos terem se juntado ao programa, o programa está pronto e sairá.

Usando top , você poderá ver 25 instâncias do programa "threads2". Mas kidna chato. A saída de ps auwx é ainda menos interessante ... MAS ps -eLf fica meio excitante.

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 programa thread2 criou. Todos compartilham o mesmo ID de processo (PID) e ID de 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.

    
por 12.05.2017 / 17:07
3

Quando se trata de processos e encadeamentos do Linux, a espécie de é a mesma coisa. O que significa que eles são criados com a mesma chamada de sistema: clone .

Se você pensar sobre isso, a diferença entre encadeamentos e processos é que os objetos do kernel serão compartilhados pelo pai e pelo filho. Para processos, não é muito: descritores de arquivos abertos, segmentos de memória que não foram escritos, provavelmente alguns outros que eu não consigo pensar em cima da minha cabeça. Para encadeamentos, muito mais objetos são compartilhados, mas não todos.

O que torna as threads e os objetos mais próximos no Linux é a chamada do sistema unshare . Os objetos do kernel que começam como compartilhados podem ser descompartilhados após a criação do encadeamento. Portanto, você pode, por exemplo, ter dois encadeamentos do mesmo processo que tenham espaço de descritor de arquivo diferente (revogando o compartilhamento de descritores de arquivo após a criação dos encadeamentos). Você mesmo pode testá-lo criando um thread, chamando unshare em ambos os threads e, em seguida, fechando todos os arquivos e abrindo novos arquivos, pipes ou objetos em ambos os threads. Então, olhe em /proc/your_proc_fd/task/*/fd e você verá que cada task (que você criou como um encadeamento) terá fds diferentes.

Na verdade, tanto a criação de novos encadeamentos quanto de novos processos são rotinas de bibliotecas que chamam clone underneath e especificam quais objetos do kernel o recém-criado process-thread-thingamajig (ie, task ) irá compartilhar com o chamando processo / thread.

    
por 15.05.2017 / 06:41