Share

O padrão POSIX

POSIX é um conjunto de padrões que objetiva a portabilidade de programas para que eles pudessem rodar em qualquer sistema operacional baseado em Unix. O Linux implementa em grande parte o padrão POSIX no que se refere à interface de programação de processos e threads, o que ajuda bastante na portabilidade do código entre sistemas Unix.

O POSIX 1c se refere especificamente a padrões referentes a threads como: criação, controle e limpeza de threads; sicronização de threads; manipulação de sinais.

Programação multi-thread em Linux

Em Linux há basicamente duas formas de criar programas capazes de realizar multiprocessamento: através da criação de vários processos associado a um processo principal ou através da criação de várias threads associadas à um único processo, conhecida como multi-thread. Atualmente a abordagem mais utilizada é a multi-thread, devido à vantagens como compartilhamento de espaço de memória pelas threads, menos recursos de memória alocada, divisão de tarefas em várias threads, entre outras.

Nesse tipo de programação é necessário levar em consideração o acesso simultâneo a recursos de hardware que podem gerar erros no sistema. Precisa-se usar formas de sincronização entre elas. A forma de programação também muda com a possibilidade de realizar várias tarefas ao mesmo tempo, embora não necessariamente em tempo real. É preciso aprender a pensar de forma diferente para a programação com threads, pois ela é uma forma de programação paralela.

A comunicação entre threads pode ser feita através de escrita e leitura de variáveis comuns a um mesmo espaço de memória. Como dito antes, todas as threads de um dado processo compartilham o mesmo espaço de endereço. Em se tratando de threads, a stack não é realmente um segmento isolado de cada thread: em outras palavras, uma thread pode acessar algum dado da stack de outras threads. Por exemplo, você pode criar uma variável global int x, assim todas as threads terão acesso a essa variável. Todas as stacks das threads de um dado processo estão ainda no mesmo espaço de endereço.

Multiprocesso e Multi-thread

No método de criação de vários processos associados a um processo principal, para cada processo é alocada uma região de memoria diferente e cada um tem uma estrutura de controle que armazena diversas informações. O kernel armazena uma lista de processos em uma lista linkada duplamente circular chamada de lista de tarefas. Cada elemento na lista de tarefas é um descritor de processos do tipo struct task_struct, que é definida em <linux/shed.h>. O descritor do processo contém todas as informações de um processo específico. Abaixo é mostrado parte da estrutura:

A variável state indica o estado do processo. Em Linux um processo pode assumir cinco estados: TASK_RUNNING, TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE, TASK_ZOMBIE e TASK_STOPPED.

TASK_RUNNING – O processo é executável, está atualmente em execução ou em uma fila de execução à espera para executar. Este é o único estado possível para um processo em execução no espaço do usuário. Ele também pode aplicar-se a um processo no espaço do kernel que está em execução ativamente.

TASK_INTERRUPTIBLE – O processo está dormindo, isto é, está bloqueado, esperando alguma condição ocorrer. Quando essa condição ocorre, o kernel define o estado do processo para TASK_RUNNING. O processo também pode acordar prematuramente e torna-se executável se receber um sinal.

TASK_UNINTERRUPTIBLE – Este estado é idêntico ao TASK_INTERRUPTIBLE exceto que não acorda e torna-se executável se receber um sinal. Isto é usado em situações em que o processo deve esperar, sem interrupção ou quando se espera que o evento ocorra rapidamente. Como a tarefa não responde aos sinais nesse estado, TASK_UNINTERRUPTIBLE é menos usado do que TASK_INTERRUPTIBLE, e por este motivo não é recomendado seu uso.

TASK_ZOMBIE – A tarefa foi finalizado, mas o processo pai ainda não emitiu um wait4() para o sistema. O descritor do processo deve permanecer no caso de o pai querer acessá-lo. Se o processo pai chamar wait4(), o descritor de processo é liberado. Mas às vezes o processo pai é encerrado sem chamar a wait4() e o processo filho permanece no estado TASK_ZOMBIE ocupando a memória até que o sistema seja desligado ou reiniciado.

TASK_STOPPED – Execução do processo foi interrompida. A tarefa não está sendo executado nem é elegível para ser executada. Isso ocorre se a tarefa recebe o sinal SIGSTOP, SIGTSTP, SIGTTIN, ou SIGTTOU ou se receber qualquer sinal enquanto ele está sendo debugado.

Quando um processo cria outro processo, dizemos que o primeiro é um processo pai e o segundo, um processo filho. Um processo pai pode ter vários processos filhos associados a ele. A cada processo criado, uma estrutura task_struct é alocada. Mas alguns atributos do processo pai são herdados pelo processo filho. É possível também que cada processo filho crie outros processos, formando uma estrutura em árvore.

 ps_blue

Quando o processo pai recebe um sinal de kill, o processo pai e todos os processos filhos associados são encerrados.

Em relação a um processo, threads são bastante simples. A estrutura associada a threads é thread_info. Para processadores da família x86 é definida em <asm/thread_info.h> como:

Essa estrutura é menor quando comparada à estrutura que guarda as informações dos processos, portanto ocupando menos memória RAM. Assim a criação de uma thread leva menos tempo que a criação de um processo, o que representa certo ganho de desempenho. As trocas de contexto entre threads também é mais rápida. Os estados assumidos por uma thread são os mesmos assumidos por um processo. Cada thread associada um a processo é identificada por um thread ID, chamado TID e um processo por um process ID, chamado PID.

ps-aux-pidof-demo

Sincronização de threads

O uso de varias threads para executar várias tarefas ao mesmo tempo é uma vantagem, mas com isso também surgem alguns problemas. Como as threads compartilham o mesmo espaço de memoria, duas ou mais threads podem tentar acessar a mesma variável ou recurso ao mesmo tempo, ocasionado um deadlock (falha de sistema). Existem vários tipos de sincronização de threads como: mutexes, variáveis condicionais, barreiras, semáforos, spinlocks, etc.

Spinlock é um mecanismo para proteger seções criticas de código. Uma thread irá “girar” enquanto espera por um recurso ficar disponível, e nunca vai dormir(em nosso contexto a palavra dormir significa ficar em modo de espera, sem consumir recursos do processador). É muito usada em sistemas com vários processadores ou vários núcleos, algo bem comum hoje em dia em processadores Intel e alguns ARMs. Eles são intensamente usados no código interno do kernel Linux e em device drivers.

Um mutex (mutal exclusion object) é um tipo de mecanismo de trava em que é possível dormir. Eles não podem ser usados em um contexto de interrupção.

Um semáforo é uma variável ou um tipo de dado abstrato que é usado para controle de acesso, por múltiplas processos, a um recurso compartilhado em um ambiente de programação paralela. É possível usar semáforos contadores (counting semaphores) para proteger seções criticas de código.

Vantagens do uso de multi-thread em Linux Embarcado

Em sistemas embarcados é comum, em alguns projetos, o uso de vários periféricos e sensores ligados em uma mesma placa. Dessa forma, é possível a criação de várias threads para o controle e monitoramento de cada periférico ou sensor. Obviamente existem muitos outros sistemas operacionais usadas em sistemas embarcados que possuem a capacidade de multitarefa, como: FreeRTOS, Texas RTOS, uC/OS, VxWorks, etc. Mas poucos tem a estabilidade e a maturidade que o Linux tem.

Referências

http://pt.wikipedia.org/wiki/POSIX

https://idea.popcount.org/2012-12-11-linux-process-states/

http://www.tldp.org/LDP/tlk/kernel/processes.html

http://www.yolinux.com/TUTORIALS/LinuxTutorialPosixThreads.html

http://www.makelinux.net/books/lkd2/ch03lev1sec1

Translate »