Desenvolvimento de IPC no QNX – Fundamentos

Este post faz parte da série Sistema Operacional de Tempo Real QNX

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.

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:

long MsgSend( int coid, const void* smsg, size_t sbytes, void* rmsg, size_t rbytes );

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.
rcvid_t MsgReceive( int chid, void * msg, size_t bytes, struct _msg_info * info );

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.
int MsgReply( rcvid_t rcvid, long status, const void* msg, size_t bytes );

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.

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/neutrino.h>

// Definição da estrutura de mensagem que será enviada entre o cliente e o servidor
typedef struct
{
    char caractere;
    int inteiro;
    float flutuante;
} Mensagem;

// PID/CHID, identificam exclusivamente o servidor ao qual o cliente deseja se conectar.
pid_t pid; // ID do processo
int chid;  // ID do canal de comunicação

// Função que será executada pela thread do servidor
void *server_thread(void *arg)
{
    int rcvid;          // ID da mensagem recebida do cliente
    long status;        // Valor de retorno recebido no cliente
    Mensagem recv_msg;  // Estrutura de mensagem de recebimento
    Mensagem reply_msg; // Estrutura de mensagem de resposta

    /**
     * [1] Cria um canal de comunicação que será usado para receber as mensagens e pulsos dos clientes
     * ---
     * - Uma vez criado, o canal pertence ao processo e não está vinculado ao thread de criação.
     * - Threads que desejam se comunicar com o canal são anexados a ele chamando 'ConnectAttach()'
     * - Os threads podem estar no mesmo processo ou em um processo diferente.
     * - O valor de retorno de 'ChannelCreate()' é um identificador do canal recém-criado
     */
    chid = ChannelCreate(0);
    if(chid != -1)
    {
    	printf("[SERVER]: Canal %d criado com sucesso no processo %d\n", chid, pid);
    }
    else
    {
        perror("Falha na criacao do canal");
        exit(EXIT_FAILURE);
    }

    printf("[SERVER]: Aguardando mensagens...\n\n");

    // Loop principal do servidor
    while (1)
    {
        /**
         * [2] Aguarda mensagens vindas de um canal
         * ---
         * - A chamada do kernel 'MsgReceive()' aguarda a chegada de uma mensagem ou pulso no canal
         *   identificado por 'chid' e armazena os dados recebidos no buffer apontado por 'recv_msg'
         * - Se o cliente ainda não enviou uma mensagem, a thread do servidor bloqueia (RECEIVE blocked)
         * - Se o cliente já tiver enviado uma mensagem, ela é copiada para o buffer sem bloquear o servidor.
         * - Em caso de sucesso, 'MsgReceive()' retorna um 'rcvid' usado para a resposta em 'MsgReply()'
         */
        rcvid = MsgReceive(chid, &recv_msg, sizeof(recv_msg), NULL);
        if (rcvid == -1)
        {
            perror("MsgReceive failed");
            break;
        }

        // Processa a mensagem recebida (opcional)
        // (neste exemplo, o servidor simula um processamento alterando a mensagem original)
        printf("[SERVER]: Mensagem recebida: (Char = %c) (Int = %d) (Float = %0.1f)\n",
                recv_msg.caractere,
                recv_msg.inteiro,
                recv_msg.flutuante);

        reply_msg.caractere = toupper(recv_msg.caractere);
        reply_msg.inteiro = recv_msg.inteiro * 2;
        reply_msg.flutuante = recv_msg.flutuante * 10.0;

        /**
         * [3] Envia uma resposta de volta para o cliente (thread identificado por 'rcvid')
         * ---
         * - A thread que está sendo respondida deve estar no estado bloqueado por REPLY
         * - Neste momento, a thread cliente (bloqueada no estado 'REPLY') será desbloqueda
         * - Lá no cliente, 'MsgSend()' receberá um valor de retorno espeficado em 'status'
         */
        status = 0; // Especifique aqui um valor relevante que represente sua mensagem de retorno
        if (MsgReply(rcvid, status, &reply_msg, sizeof(reply_msg)) == -1)
        {
            perror("Falha ao responder a mensagem");
            break;
        }

        /**
         * Você pode optar por responder sem dados se o objetivo for apenas desbloquear o cliente.
         * Neste caso, nenhum dado é exigido pela função 'MsgReply()' - o ID de recebimento é suficiente
         */
        // MsgReply(rcvid, EOK, NULL, 0);
    }

    // Destrói o canal de comunicação
    ChannelDestroy(chid);
    return NULL;
}

// Função que será executada pela thread do cliente
void *client_thread(void *arg)
{
    int coid;  // ID da conexão com o servidor
    Mensagem send_msg = {'a', 0, 0.0};  // Estrutura de mensagem de envio
    Mensagem reply_msg = {'a', 0, 0.0}; // Estrutura de mensagem de resposta

    // Conecta-se ao canal de comunicação do servidor
    /**
     * Threads que desejam se comunicar com o canal são anexados a ele chamando ConnectAttach() . Os threads podem estar no mesmo processo ou em um processo diferente.
     */
    /**
     * [1] Estabelecendo uma conexão
     * ---
     * - A primeira coisa que precisamos fazer é estabelecer uma conexão com o canal da thread do servidor
     * - As threads podem estar no mesmo processo ou em um processo diferente.
     * - 'ConnectAttach()' recebe dois identificadores: o 'pid' (ID do processo) e o 'chid' (ID do canal)
     * - PID/CHID, identificam exclusivamente o servidor ao qual o cliente deseja se conectar
     */
    printf("[CLIENT]: Conectando-se com o processo %d, canal %d\n\n", pid, chid);
    coid = ConnectAttach(0, pid, chid, 0, 0);

    if (coid == -1)
    {
        perror("ConnectAttach failed");
        exit(EXIT_FAILURE);
    }

    // Loop principal do cliente
    while (1)
    {
        printf("[CLIENT]: Enviando mensagem: (Char = %c) (Int = %d) (Float = %0.1f)\n",
                send_msg.caractere,
                send_msg.inteiro,
                send_msg.flutuante);

        /**
         * [2] Envia a mensagem para o servidor e espera pela resposta
         * ---
         * - 'MsgSend()' é uma chamada do kernel que envia uma mensagem para o canal de um processo
         *   através da conexão identificada por 'coid'.
         * - Se o servidor já estiver bloqueado (RECEIVE) aguardando mensagens, a thread cliente ficará bloqueada em REPLY.
         *   (ou seja, mensagem foi recebida, mas ainda não foi respondida)
         * - Se o servidor ainda não estiver aguardando mensagens, a thread cliente ficará bloqueada em SEND.
         *   (ou seje, a mensagem foi enviada, mas ainda não recebida)
         */
        if(MsgSend(coid, &send_msg, sizeof(send_msg), &reply_msg, sizeof(reply_msg)) == -1)
        {
            perror("Falha no envio da mensagem");
            break;
        }

        printf("[CLIENT]: Mensagem respondida: (Char = %c) (Int = %d) (Float = %0.1f)\n\n",
                reply_msg.caractere,
                reply_msg.inteiro,
                reply_msg.flutuante);

        // Espera um tempo antes de enviar outra mensagem
        sleep(2);

        // Cria nova mensagem alterando os campos da estrutura com valores arbitrários
        send_msg.caractere = (send_msg.caractere == 'z') ? 'a' : send_msg.caractere + 1;
        send_msg.inteiro++;
        send_msg.flutuante -= 0.2;
    }

    // Desconecta-se do canal de comunicação do servidor
    ConnectDetach(coid);
    return NULL;
}

int main(void)
{
    pthread_t client_tid, server_tid;

    // Obtem o ID do processo
    pid = getpid();

    // Cria a thread do servidor
    if (pthread_create(&server_tid, NULL, server_thread, NULL) != 0)
    {
        perror("pthread_create for server failed");
        exit(EXIT_FAILURE);
    }

    // Aguarda um pouco para que o servidor esteja pronto para receber mensagens
    sleep(1);

    // Cria a thread do cliente
    if (pthread_create(&client_tid, NULL, client_thread, NULL) != 0)
    {
        perror("pthread_create for client failed");
        exit(EXIT_FAILURE);
    }

    // Aguarda as threads do cliente e do servidor terminarem
    pthread_join(client_tid, NULL);
    pthread_join(server_tid, NULL);

    return 0;
}

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

Sistema Operacional de Tempo Real QNX

Configurando o Ambiente de Desenvolvimento para QNX Desenvolvendo 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
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Software » Desenvolvimento de IPC no QNX – Fundamentos

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: