FAVORITAR
FecharPlease login

Desenvolvimento de IPC no QNX – Fundamentos

Introdução

A comunicação entre processos (IPC) é um mecanismo importante em ambientes multitarefa para permitir que processos distintos troquem dados e informações enquanto executam “simultaneamente”.

O QNX Neutrino, um sistema operacional de tempo real, possui uma variedade de formas de comunicação entre processos, sendo a passagem de mensagens sua principal forma de IPC. Ela é síncrona e envolve a cópia de dados entre processos.

Além da passagem de mensagens, o QNX Neutrino oferece outras formas de IPC como sinais, filas de mensagens POSIX, memória compartilhada, Pipes e FIFOS. Cada uma dessas formas tem suas características e aplicações específicas, permitindo aos desenvolvedores escolher a mais adequada com base nos requisitos de seu projeto.

Compreender como este mecanismo funciona no QNX Neutrino é fundamental para o desenvolvimento de aplicações eficientes e confiáveis.

No dia 25 de Junho de 2024, ocorrerá o “Seminário de Sistemas Embarcados e IoT 2024“, no Holiday Inn Anhembi — Parque Anhembi, São Paulo–SP.

Garanta seu ingresso

Neste artigo, vamos explorar um pouco como ocorre IPC no QNX, fornecendo exemplos práticos para melhor entendimento.

Processos e Threads

Antes de começarmos a discutir sobre a comunicação entre processos, é bom entendermos um pouco melhor o que são Processos e Threads.

Processos: Um processo é um conjunto de instruções de um programa com dados e estados. Ele inclui o código do programa, bem como o ambiente em que está sendo executado, como variáveis, recursos de sistema, canais de comunicação, identificadores de usuário, grupos e arquivos abertos, entre outros.

Cada processo tem sua própria área de memória e por isso utilizam mecanismos específicos de IPC (Inter-Process Communication) fornecidos pelo sistema operacional para realizarem a troca de dados entre si, assim, os recursos pertencentes à um processo estão protegidos dos demais.

Threads: São as unidades de execução dentro de um processo. São elas quem de fato executam o processamento de alguma tarefa.

As threads no QNX compartilham o mesmo espaço de endereço virtual dentro de um processo, portanto podem acessar as mesmas variáveis e recursos.

No entanto, isso adiciona alguns desafios de sincronização e agendamento por causa da concorrência, onde várias threads podem tentar acessar e modificar os mesmos recursos simultaneamente.

Os processos “single thread” são aqueles que possuem apenas uma thread associada e ela será responsável por realizar todas as atividades. Todo processo precisa necessariamente possuir ao menos uma thread.

Já os processos “multithread” dividem a responsabilidade da execução de diversas tarefas entre suas várias threads. Isto é importante quando deseja-se executar operações “em paralelo” ou funções independentes.

A figura seguinte ilustra os recursos que são compartilhados pelas threads apenas dentro de um mesmo processo. Reforçando, portanto, que cada processo possui seu próprio conjunto de recursos isolados dos demais.

Figura 1: Recursos compartilhados entre processos e threads Fonte: Autor

Fundamentos de IPC no QNX

O QNX possui uma arquitetura microkernel, modular e escalável onde todos os processos são executados em espaço de usuário. A comunicação entre estes módulos ocorre apenas por meio de troca de mensagens entre si.

O microkernel é basicamente responsável apenas pelo escalonamento, gerenciamento de memória e age como intermediário na comunicação entre processos.

Devido a este total isolamento, a filosofia de design da arquitetura microkernel do QNX Neutrino, desde sua concepção, está intimamente relacionada ao uso de mecanismos de comunicação entre processos, especialmente a passagem de mensagens.

Alguns pontos-chave:

Modularidade: A arquitetura microkernel promove modularidade, separando os serviços essenciais do sistema operacional em pequenos módulos independentes. O incremento de funcionalidades também fica fácil de ser realizado.

Sincronização: A passagem de mensagens no QNX Neutrino é realizada de forma síncrona. Isso implica que quando um thread envia uma mensagem para outro thread, ele aguarda até que o thread de destino receba, processe a mensagem e, em seguida, responda para a thread de origem, garantindo que a comunicação ocorra de forma ordenada e eficiente.

Isolamento de falhas: A arquitetura microkernel do QNX, ao isolar os serviços em módulos independentes, ajuda a evitar que falhas em um serviço afetem diretamente outros componentes do sistema.

Transparência de rede: As mensagens podem circular de forma transparente pelas fronteiras do processador, permitindo a sincronização entre processos distribuídos em uma rede.

O QNX segue o padrão POSIX, e oferece uma ampla gama de primitivas de sincronização, como mutexes, semáforos, barreiras e variáveis de condição.

Somando-se a isso, o mecanismo de IPC do QNX chamado de “message passing” implementa uma sincronização implícita por meio do seu mecanismo de bloqueio Send/Receive/Reply. Nesse modelo, quando uma thread envia uma mensagem ela fica imediatamente bloqueada até que a thread de destino receba a mensagem e então envie uma resposta.

A documentação do QNX enfatiza a passagem de mensagens como sua principal forma de IPC, no entanto existem outras opções que são bastante robustas e simples, permitindo ao desenvolvedor ponderar de acordo com os seus requisitos de projeto e capacidade do sistema.

Passagem de Mensagens (message passing)

O mecanismo eficiente de sincronização na comunicação entre processos (IPC) que ocorre na passagem de mensagens é provido pelas funções MsgSend(), MsgReceive() e MsgReply().

Veja detalhes delas a seguir:

onde:

  • coid: O ID da conexão com o canal para enviar a mensagem, que você estabeleceu chamando ConnectAttach() ou uma de suas funções envolvidas, como name_open() ou open().
  • smsg: Um ponteiro para um buffer que contém a mensagem que você deseja enviar.
  • sbytes: O número de bytes a serem enviados.
  • rmsg: Um ponteiro para um buffer onde a resposta pode ser armazenada.
  • rbytes: O tamanho do buffer de resposta.

onde:

  • chid: O ID da conexão com o canal que você estabeleceu chamando ChannelCreate().
  • msg: Um ponteiro para um buffer onde a função pode armazenar os dados recebidos.
  • bytes: O tamanho do buffer.
  • info: NULL, ou um ponteiro para uma estrutura _msg_info onde a função pode armazenar informações adicionais sobre a mensagem.

onde:

  • rcvid: O ID que MsgReceive*() retornou quando você recebeu a mensagem.
  • status: O status a ser usado ao desbloquear a chamada de MsgSend*() do cliente na thread rcvid.
  • msg: Um ponteiro para um buffer que contém a mensagem com a qual deseja responder.
  • bytes: O tamanho da mensagem.

A figura seguinte mostra como os argumentos se relacionam nestas funções e o fluxo de dados entre elas.

Figura 2: Fluxo dos dados durante as transações Fonte: QNX Developers

  1. O cliente emite um MsgSend() especificando os dados que serão enviados, passando um ponteiro (smsg) e a quantidade de bytes (sbytes) deste buffer para a função. Estes dados são transferidos para o buffer (rmsg) indicado pela função MsgReceive() no servidor (rbytes vai conter a quantidade de bytes recebidos). O cliente agora está bloqueado!
  1. O servidor agora é desbloqueado e a função MsgReceive() do servidor retorna com um rcvid , que o servidor usará posteriormente para a resposta. Neste ponto, os dados estão disponíveis para uso do servidor.
  1. O servidor conclui o processamento da mensagem e agora usa o rcvid obtido de MsgReceive() em MsgReply() para enviar a resposta. Observe que a função MsgReply() usa um buffer (smsg) com tamanho definido (sbytes), que corresponde agora com o resultado do processamento dos dados recebidos.
  1. Finalmente, o parâmetro sts é transferido pelo kernel e aparece como o valor de retorno do MsgSend() do cliente. O cliente agora é desbloqueado!

Em resumo, uma thread que executa um MsgSend() para outra thread (que pode estar dentro de outro processo) será bloqueada até que a thread de destino execute um MsgReceive(), processe a mensagem e execute um MsgReply().

Se a thread de destino executar um MsgReceive() sem uma mensagem enviada anteriormente, ele será bloqueado até que a outra thread execute um MsgSend().

A forma mais didática para explicar este mecanismo é através de um exemplo típico de uma aplicação cliente-servidor em que duas threads trocam mensagens. 

Servidor

Vamos analisar sob o ponto de vista do servidor, o que ocorre na execução de cada uma das funções MsgSend(), MsgReceive() e MsgReply():

O servidor executa MsgReceive() para aguardar a chegada de uma mensagem do cliente. Neste momento, a execução do thread do servidor é bloqueada pelo kernel até que uma mensagem seja recebida. O servidor estará então no estado RECEIVE blocked.

Ao receber a mensagem, seus dados são copiados para um buffer e então o kernel altera o estado do thread para READY, desbloqueando o servidor que continua sua execução processando a mensagem recebida.

Essa abordagem síncrona garante que o servidor aguarde ativamente por mensagens do cliente, processando-as e respondendo conforme necessário. O bloqueio nas operações de recebimento e resposta ajuda a manter a sincronia e a ordem adequada das operações.

A figura 3 mostra uma simplificação das transições que ocorrem no thread do servidor.

Figura 3: Troca de estados da thread servidor Fonte: Autor

Cliente

Agora vamos analisar o que ocorre no lado do cliente.

O cliente envia mensagens ao servidor usando MsgSend() e então é bloqueado, tornando-se SEND ou REPLY blocked conforme o estado do servidor.

Se o servidor ainda não tiver executado a função MsgReceive() então o cliente irá para o estado SEND blocked. Se o servidor já estiver bloqueado em RECEIVE o cliente irá para o estado REPLY blocked.

A figura 4 ilustra exatamente este processo.

Figura 4: Troca de estados da thread cliente Fonte: Autor

Canais e Conexões

Antes de partirmos para um exemplo prático de uma aplicação, vamos entender um pouco o que são canais e conexões.

O QNX Neutrino RTOS utiliza um sistema de canais e conexões para o envio de mensagens e pulsos entre threads.

Uma thread que deseja receber mensagens primeiro cria um canal e outra thread que deseja enviar uma mensagem para essa thread deve fazer uma conexão, “anexando-se” a esse canal. Os threads podem estar no mesmo processo ou em processos diferentes.

Uma vez criado, o canal pertencerá agora ao processo e não mais à thread de criação.

ChannelCreate() é usada para criar um canal de comunicação (pertence ao processo) e retornar um identificador de canal (coid). O comportamento do canal é estabelecido por uma série de flags passadas no seu argumento.

ConnectAttach() é responsável por estabelecer uma conexão entre um processo e um canal previamente criado.

Várias threads em um processo podem se anexar à um mesmo canal.

Figura 5: Relacionamento entre canais e conexões Fonte: QNX Developrers

Exemplo Client-Server usando IPC

Veja o código de exemplo a seguir que implementa um sistema de comunicação entre um cliente e um servidor usando threads.

Apesar de estarmos discutindo o mecanismo de comunicação entre processos, para facilitar o entendimento, o exemplo a seguir implementa esta comunicação entre duas threads (client e server) que estão no mesmo processo.

Não se preocupe, isto é o suficiente para o bom entendimento de como o mecanismo de IPC funciona. Lembre-se que as trocas de mensagens ocorrem de fato entre threads que podem ou não estar no mesmo processo.

A grande diferença estará na forma como os canais e conexões são identificados entre processos distintos.

A figura a seguir mostra o resultado da execução deste programa. Note a identificação do processo (PID = 1597479) e do canal (CHID = 1).

Observe também o sincronismo entre envio, recebimento e resposta de mensagens que ocorre entre as threads cliente e servidor. Elas alternam seus estados de bloqueio e execução com perfeição.

Conclusão

Este artigo fornece uma visão do desenvolvimento de Inter-Process Communication (IPC) no QNX Neutrino, destacando a importância desses mecanismos em ambientes multitarefa.

O sistema operacional oferece uma variedade de mecanismos de comunicação, com a passagem de mensagens sendo a principal forma de IPC.

A arquitetura microkernel do QNX, com seu design modular e escalável, isola os serviços em módulos independentes. Isto requer um robusto mecanismo de comunicação entre estes módulos e uma garantia da execução das tarefas em uma ordem adequada.

Referências

1. QNX. (s.d.). ChannelCreate(). https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.lib_ref/topic/c/channelcreate.html 

2. QNX. (s.d.). Channels and connections. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.sys_arch/topic/ipc_Channels.html 

3. QNX. (s.d.). ConnectAttach(). https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.lib_ref/topic/c/connectattach.html 

4. QNX. (s.d.). Finding the server’s PID/CHID. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.getting_started/topic/s1_msg_find77.html 

5. QNX. (s.d.). Interprocess Communication (IPC). https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.sys_arch/topic/ipc.html 

6. QNX. (s.d.). Message Passing. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.getting_started/topic/s1_msg.html 

7. QNX. (s.d.). Processes and Threads. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.getting_started/topic/s1_procs.html

Outros artigos da série

<< Configurando o Ambiente de Desenvolvimento para QNXDesenvolvendo Resource Managers no QNX – Um Guia Prático >>
Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
Inline Feedbacks
View all comments
Home » Software » Desenvolvimento de IPC no QNX – Fundamentos

EM DESTAQUE

WEBINARS

LEIA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste:


Seminário de
Sistemas Embarcados e IoT 2024
 
Data: 25/06 | Local: Hotel Holiday Inn Anhembi, São Paulo-SP
 
GARANTA SEU INGRESSO

 
close-link