Introdução
Em sistemas operacionais tradicionais, o gerenciamento de recursos do hardware é normalmente realizado pelo seu núcleo (kernel) que fica responsável pela gestão da memória, processos e dispositivos. Neste contexto, os device drivers incorporados desempenham a função de intermediários na comunicação com os diversos dispositivos permitindo que os programas interajam com eles de forma eficiente fornecendo uma abstração.
No sistema operacional QNX, no entanto, estas funcionalidades são implementadas em processos separados do núcleo através dos Resource Managers (gerenciadores de recursos).
Na arquitetura microkernel do sistema operacional QNX o núcleo é estruturado para oferecer apenas os serviços essenciais para todos os demais processos do sistema. A filosofia aqui é torna-lo modular e escalável.
Novos serviços e funcionalidades podem ser adicionados sem a necessidade de alterações no núcleo do sistema, o que simplifica o processo de desenvolvimento, depuração e manutenção.
Resource Managers são projetados para fornecer uma interface consistente, segura e abstrata para que os processos possam acessar e manipular recursos específicos do sistema (como dispositivos de hardware e sistemas de arquivos) através da implementação de operações POSIX padrão, de abertura, fechamento, leitura e escrita.
Um gerenciador de recursos no QNX é simplesmente um programa separado do núcleo com algumas características bem definidas.
Quais são as vantagens?
A API de comunicação dos programas com os Resource Managers no QNX é em sua maioria POSIX via funções open(), read(), write() e close() bem conhecidas.
Suponha que você deseja comunicar-se com um sensor de temperatura instalado em sua placa (target) cujo resource manager já registrou este dispositivo no nome de caminho “/dev/sensors/temperature”. Basta escrever um simples programa em C como mostrado a seguir para obter o valor atual da temperatura deste sensor.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
int fd = open("/dev/sensors/temperature", O_RDONLY);
if (fd == -1) {
perror("Erro ao abrir o arquivo");
return 1;
}
// Lê a temperatura
float temperatura;
read(fd, &temperatura, sizeof(float));
// Exibe a temperatura
printf("Temperatura: %.02f\n", temperatura);
close(fd);
return 0;
}
Também é possível comunicar-se com os gerenciadores de recursos via utilitários de linha de comando. Considere a facilidade de obter os dados de um GNSS que foi registrado em /dev/gps e então usar no shell algo assim:
# cat /dev/gps/rmc
Obtendo logo em seguida:
$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
Com a mesma facilidade, é possível enviar mensagens para os dispositivos via linha de comando. Imagine que você queira alterar a posição de um servo. Basta digitar no terminal e o gerenciador de recursos ficará responsável por receber a mensagem, trata-la e comunicar-se com este dispositivo.
#echo 90 > /dev/servo/position
Cada Resource Manager pode ficar responsável por um recurso específico (periféricos, sistema de arquivos, dispositivos de hardware, etc.), oferecendo algum determinado serviço de forma abstrata, ou seja, a aplicação não precisa conhecer nenhum detalhe do funcionamento do periférico ou dispositivo. Basta o uso de chamadas de função POSIX para interagir com eles.
Um robusto sistema de comunicação entre processos (IPC) é responsável pela “tradução” das funções POSIX em mensagens que são construídas e então enviadas diretamente aos gerenciadores de recursos.
Estrutura de um Resource Manager
Um resource manager possui uma estrutura relativamente simples e bem definida da seguinte forma de acordo com a documentação do QNX.
- Canal de Conexão (channel):
Um canal de comunicação é criado para que outros programas (clientes) possam se conectar a ele e então enviar e receber mensagens.
- Nome do Caminho (pathname):
Um nome de caminho é registrado no gerenciador de processos (Process Manager) informando que o Resource Manager será responsável por lidar com solicitações de clientes para esses nomes de caminho.
- Loop de Processamento de Mensagens:
O Resource Manager permanece em um loop contínuo recebendo e processando mensagens, executando alguma ação apropriada com base no seu conteúdo.
Desenvolvimento de um Resource Manager simples
A seguir vamos demostrar o desenvolvimento de um gerenciador de recursos, destacando apenas trechos de código com os principais pontos de design, implementação de funcionalidades essenciais e boas práticas.
As funções, macros e estruturas contidas na biblioteca do gerenciador de recursos torna esta tarefa bastante fácil. Sem ela, provavelmente centenas de linhas de código seriam necessárias para realizar o mesmo trabalho.
Interface de despacho (dispatch interface)
Dispatch Interface é o mecanismo que o gerenciador de recursos utiliza para receber mensagens dos clientes. As funções dispatch_create_channel() e dispatch_create() alocam e inicializam uma estrutura de despacho e retornam um identificador para ela (ou NULL) para o caso de falha.
dispatch_t *dpp;
(...)
dpp = dispatch_create_channel(-1, DISPATCH_FLAG_NOLOCK);
Atributos do gerenciador de recursos
A estrutura de controle (resmgr_attr_t) contém atributos do gerenciador de recursos que precisam ser inicializado antes de ser passada mais tarde para a função resmgr_attach().
Aqui estamos configurando quantas estruturas IOV estão disponíveis para respostas (nparts_max) e também o tamanho do buffer de recepção (msg_max_size).
resmgr_attr_t resmgr_attr;
(...)
memset(&resmgr_attr, 0, sizeof resmgr_attr);
resmgr_attr.nparts_max = 1;
resmgr_attr.msg_max_size = 2048;
Estruturas usadas para lidar com as mensagens
Agora vamos indicar as funções que vão ser invocadas quando o gerenciador de recursos receber e identificar uma mensagem específica.
Quando a biblioteca do gerenciador de recursos recebe uma mensagem, após identifica-la, decide o que fazer com ela com base nas estruturas resmgr_connect_funcs_t e resmgr_io_funcs_t.
A estrutura resmgr_connect_funcs_t é responsável por todas as mensagens de conexão e resmgr_io_funcs_t pelas mensagens de I/O.
resmgr_connect_funcs_t connect_funcs;
resmgr_io_funcs_t io_funcs;
(...)
iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_funcs, _RESMGR_IO_NFUNCS, &io_funcs);
connect_funcs.open = my_device_open;
io_funcs.read = my_device_read;
io_funcs.write = my_device_write;Estrutura de atributos usada pelo dispositivo
A estrutura de atributos contém informações sobre nosso dispositivo específico associado ao pathname e as informações sobre permissões, tipo de dispositivo, proprietário e ID do grupo.
Ela descreve os dados e o estado associados a um serviço oferecido por um gerenciador de recursos.
Usamos a função iofunc_attr_init() para inicializar esta estrutura. Destaque para o argumento “modo” (S_IFNAM | 0666) onde definimos o tipo e as permissões de acesso que desejamos usar para o recurso. É necessário restringi-los de acordo com o que se deseja que outros processos e usuários possam fazer com o gerenciador de recursos.
iofunc_attr_t attr;
(...)
iofunc_attr_init(&attr, S_IFNAM | 0666, 0, 0);Anexar um nome de caminho (pathname)
O gerenciador de recursos precisa informar aos outros programas (por meio do gerenciador de processos) que ele é o responsável por um prefixo de nome de caminho específico para que possa começar a receber mensagens. Isso é feito através do registro do nome do caminho.
Depois que o gerenciador de recursos estabelece seu nome, ele recebe mensagens quando qualquer programa cliente tenta executar uma operação (por exemplo, open(), close(), read(), write()) nesse nome.
Neste exemplo, /dev/my_device será o nome associado ao dispositivo gerenciado pelo Resource Manager.
#define MYDEVNAME "/dev/my_device"
(...)
id = resmgr_attach(
dpp, /* dispatch handle */
&resmgr_attr, /* resource manager attributes */
MYDEVNAME, /* device pathname */
_FTYPE_ANY, /* open type */
0, /* flags */
&connect_funcs, /* connect table routines */
&io_funcs, /* I/O table routines */
&attr); /* handle */Alocar a estrutura do contexto
A função dispatch_context_alloc() retorna um ponteiro de contexto de despacho que será utilizada a seguir no loop de mensagens.
dispatch_t *dpp;
dispatch_context_t *ctp;
(...)
ctp = dispatch_context_alloc(dpp);Iniciar o loop de mensagens do gerenciador de recursos
A partir deste momento o gerenciador permanece em loop manipulando as mensagens que receber, disparando funções callbacks de acordo com seu tipo.
while (1)
{
/* Wait here forever, handling messages */
if ((ctp = dispatch_block(ctp)) == NULL)
{
fprintf(stderr, "block error\n");
return EXIT_FAILURE;
}
dispatch_handler(ctp);
}O que ocorre no lado do cliente?
Quando um cliente executa uma função que requer resolução de nome de caminho como em open(), rename(), stat() ou unlink(), mensagens do tipo “connect messages” são enviadas e então cria-se um contexto para esta solicitação.
A biblioteca C do cliente constrói uma mensagem _IO_CONNECT e a envia para o gerenciador de recursos. A mensagem vai ser recebida, decodificada e uma função de manipulação adequada será chamada.
Operações como read() e write() executadas pelo cliente resultam em mensagens _IO_READ e _IO_WRITE respectivamente construídas pela biblioteca C do cliente.
A seguinte figura ilustra o processo de comunicação entre a aplicação cliente, o Resource Manager e o Process Manager.
- Primeiro o cliente envia uma mensagem ao Process Manager solicitando que ele faça uma busca por um nome de caminho em uma tabela.
- O gerenciador de processos indica quem é o responsável por este nome e retorna o nd, pid , chid e handle que estão associados ao prefixo do nome de caminho.
- O cliente envia uma mensagem de conexão (connect message) ao canal do Resource Manager que resulta em um descritor de arquivo (fd).
- Uma mensagem de aprovação (como o descritor de arquivos) ou falha é retornada.
- A partir de agora, o cliente pode trocar mensagens (I/O messages) com o dispositivo através de operações de leituras e escrita sempre intermediadas pelo Resource Manager.
Um exemplo completo
Agora vamos criar um gerenciador de recursos completo para um dispositivo fictício chamado “counter” onde vamos querer ler e escrever dados nele usando funções de operações básicas de entrada e saída em C.
O único objetivo deste dispositivo é manter o valor de um contador interno que é incrementado à cada leitura realizada por um programa cliente. Seu valor também pode ser alterado a qualquer momento.
Importante ter em mente que o código de exemplo a seguir é completamente funcional, mas será apenas um guia de como implementar um Resouce Manager no QNX.
Consulte a documentação oficial para conhecer como implementar diversas melhorias, otimizações e incluir mecanismos de proteções adicionais.
#include <errno.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/iofunc.h>
#include <sys/dispatch.h>
#define MYDEVNAME "/dev/counter"
/* Variável para armazenar dados do dispositivo (contador) */
int device_counter;
int my_resource_open(resmgr_context_t *ctp, io_open_t *msg, RESMGR_HANDLE_T *handle, void *extra);
int my_resource_read(resmgr_context_t *ctp, io_read_t *msg, iofunc_ocb_t *ocb);
int my_resource_write(resmgr_context_t *ctp, io_write_t *msg, iofunc_ocb_t *ocb);
int main(int argc, char *argv[])
{
static dispatch_t *dpp;
static resmgr_attr_t resmgr_attr;
static dispatch_context_t *ctp;
static iofunc_attr_t attr;
static resmgr_connect_funcs_t connect_funcs;
static resmgr_io_funcs_t io_funcs;
/*
* Inicializa a interface de despacho (dispatch interface)
* ---
* Aloca e inicializa uma estrutura de despacho para uso no loop principal.
* Ele receberá as mensagens, analisará o tipo e chamará a função de callback correspondente
*/
dpp = dispatch_create();
if (dpp == NULL)
{
fprintf(stderr, "dispatch_create(): Não foi possível inicializar a interface de despacho: %s\n", strerror(errno));
return (EXIT_FAILURE);
}
/*
* Inicializa os atributos do gerenciador de recursos
* ---
* - O comprimento máximo da mensagem a ser recebida de uma vez (msg_max_size)
* - Quantas estruturas IOV estão disponíveis para respostas do servidor (nparts_max)
*/
memset(&resmgr_attr, 0, sizeof(resmgr_attr));
/* O número de componentes que devem ser alocados para a matriz IOV */
resmgr_attr.nparts_max = 1;
/* O tamanho do buffer da mensagem */
resmgr_attr.msg_max_size = sizeof(device_counter);
/*
* Inicializa as funções para manipular mensagens (callbacks)
* ---
* Agora, vamos inicializar as estruturas de funções de conexão e funções de E/S default,
* depois substituir os padrões pelas funções que personalizadas que escrevemos
*/
iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_funcs, _RESMGR_IO_NFUNCS, &io_funcs);
connect_funcs.open = my_resource_open;
io_funcs.read = my_resource_read;
io_funcs.write = my_resource_write;
/*
* Inicializa a estrutura de atributos usada pelo dispositivo
* ---
* Define pelo menos os seguintes atributos (permissões, tipo de dispositivo, proprietário, ID do grupo)
* Obs: S_IFNAM | 0666, define o tipo e as permissões de acesso que você deseja usar para o recurso
*/
iofunc_attr_init(&attr, S_IFNAM | 0666, NULL, NULL);
attr.nbytes = sizeof(device_counter);
/*
* Registro do nome de caminho (pathname)
* ---
* Antes que um gerenciador de recursos possa receber mensagens de outros programas,
* ele precisa informar aos outros programas (via o gerenciador de processos)
* que ele é o responsável por um prefixo de caminho específico
*/
static char *pathname = MYDEVNAME;
/*
* Criação do canal do Resource Manager e registro do nome do nosso dispositivo (pathname)
* ---
* Em seguida, chamamos resmgr_attach() para registrar o nome do dispositivo com o gerenciador de processos,
* e também para informá-lo sobre nossas funções de conexão e E/S.
*/
int resmgr_id = resmgr_attach(
dpp, // Manipulador de despacho
&resmgr_attr, // Estrutura de controle de atributos do gerenciador de recursos
pathname, // Caminho do dispositivo
_FTYPE_ANY, // Tipo de abertura
0, // Flags
&connect_funcs, // Rotinas da tabela de conexão
&io_funcs, // Rotinas de E/S
&attr); // Manipulador
if (resmgr_id == -1)
{
fprintf(stderr, "resmgr_attach(): Não foi possível anexar o gerenciador de recursos: %s\n", strerror(errno));
return (EXIT_FAILURE);
}
/*
* Aloca a estrutura de contexto
* ---
* Agora alocamos memória para a estrutura de contexto de despacho, que contém
* um buffer onde as mensagens serão recebidas.
* O tamanho do buffer foi definido quando inicializamos a estrutura de atributos do gerenciador de recursos
* A estrutura de contexto também contém um buffer de IOVs que a biblioteca pode usar para responder às mensagens
* O número de IOVs foi definido quando inicializamos a estrutura de atributos do gerenciador de recursos
*/
ctp = dispatch_context_alloc(dpp);
if (ctp == NULL)
{
fprintf(stderr, "dispatch_context_alloc(): Não foi possível alocar a estrutura de contexto: %s\n", strerror(errno));
return (EXIT_FAILURE);
}
/*
* Inicia o loop de mensagens do gerenciador de recursos
* ---
* Feito! Agora podemos entrar em nosso "loop de recebimento" e esperar por mensagens
* A função dispatch_block() está chamando MsgReceive() nos bastidores e recebe a mensagem para nós
* A função dispatch_handler() analisa a mensagem para nós e chama a função de callback apropriada
*/
while (1)
{
if ((ctp = dispatch_block(ctp)) == NULL)
{
fprintf(stderr, "dispatch_block(): Não foi possível iniciar o gerenciador de recursos: %s\n", strerror(errno));
return (EXIT_FAILURE);
}
else
{
/* Chama a função de callback correta para a mensagem recebida.
* Este é um gerenciador de recursos de thread única, então a próxima solicitação será atendida
* apenas quando esta chamada retornar.
* Consulte nossa documentação se você quiser criar um gerenciador de recursos multithread
*/
dispatch_handler(ctp);
}
}
return 0;
}
/**
* @brief Função de callback para abrir o recurso.
*
* @param ctp Contexto do gerenciador de recursos.
* @param msg Mensagem enviada pelo cliente
* @param handle Manipulador de recurso.
* @param extra Dados extras.
* @return Status da operação.
*/
int my_resource_open(resmgr_context_t *ctp, io_open_t *msg, RESMGR_HANDLE_T *handle, void *extra)
{
device_counter = 0;
/* Manipulador default para mensagens _IO_CONNECT */
return (iofunc_open_default(ctp, msg, handle, extra));
}
/**
* @brief Função de callback para leitura do recurso.
*
* @param ctp Contexto do gerenciador de recursos.
* @param msg Mensagem de leitura.
* @param ocb Bloco de controle.
* @return Número de partes da estrutura IOV retornadas ao cliente.
*/
int my_resource_read(resmgr_context_t *ctp, io_read_t *msg, iofunc_ocb_t *ocb)
{
int status = 0;
int nparts = 0;
/* Verifica se o dispositivo está aberto para leitura */
if ((status = iofunc_read_verify(ctp, msg, ocb, NULL)) != EOK)
{
return (status);
}
/* Verifica as informações estendidas sobre o comportamento de uma função de E/S padrão */
if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE)
{
return (ENOSYS); // Desconhecido, falhar
}
/* Quantos bytes retornar ao cliente? */
int nbytes = msg->i.nbytes;
if (nbytes > 0)
{
/* Incrementa nosso contador */
device_counter++;
/* Preenche os campos da estrutura IOV de retorno */
SETIOV(ctp->iov, &device_counter, nbytes);
/* Atualiza o tempo de leitura e gravação */
ocb->attr->flags |= (IOFUNC_ATTR_ATIME | IOFUNC_ATTR_DIRTY_TIME);
/* Define o número de bytes retornado pela função read() */
_IO_SET_READ_NBYTES(ctp, nbytes);
nparts = 1;
}
else
{
_IO_SET_READ_NBYTES(ctp, 0);
nparts = 0;
/* Se não estiver retornando dados, apenas desbloquear o cliente */
MsgReply(ctp->rcvid, EOK, NULL, 0);
}
/* Indica quantas partes da estrutura ctp->iov retornar ao cliente */
return (_RESMGR_NPARTS(nparts));
}
/**
* @brief Função de callback para escrita no recurso.
*
* @param ctp Contexto do gerenciador de recursos.
* @param msg Mensagem de escrita.
* @param ocb Bloco de controle de objeto.
* @return Número de partes da estrutura IOV retornadas ao cliente.
*/
int my_resource_write(resmgr_context_t *ctp, io_write_t *msg, iofunc_ocb_t *ocb)
{
int status = 0;
/* Verifica se o dispositivo está aberto para escrita */
if ((status = iofunc_write_verify(ctp, msg, ocb, NULL)) != EOK)
{
return (status);
}
/* Verifica as informações estendidas sobre o comportamento de uma função de E/S padrão */
if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE)
{
return (ENOSYS); // Desconhecido, falhar
}
/* Extrai o comprimento da mensagem do cliente */
int nbytes = msg->i.nbytes;
/* Lê a mengagem vinda do cliente */
int counter_tmp;
if (resmgr_msgread(ctp, &counter_tmp, nbytes, sizeof(msg->i)) == -1)
{
return (errno);
}
/* Faça algo com os dados (aqui simplesmente altera o contador para o valor recebido pelo cliente) */
device_counter = counter_tmp;
/* Define o número de bytes retornado pela função write() */
_IO_SET_WRITE_NBYTES(ctp, nbytes);
if (nbytes > 0)
{
/* Atualiza o tempo de leitura e gravação */
ocb->attr->flags |= (IOFUNC_ATTR_MTIME | IOFUNC_ATTR_DIRTY_TIME);
}
return (_RESMGR_NPARTS(1));
}Observe counter no diretório /dev no console do target QNX após a execução do gerenciador de recursos.
Para testar o funcionamento, criei um programe cliente bem simples. Ele fará leituras no nosso dispositivo a cada 1 segundo e vai exibir o valor atual do seu contador interno.
Para testar também a escrita no dispositivo, note que após 10 leituras, um novo valor é enviado e então o valor do contador interno é atualizado.
O resultado é mostrado na figura seguinte.
Conclusão
Neste texto, exploramos o conceito e a implementação de Resource Managers (gerenciadores de recursos) no sistema operacional QNX, destacando suas vantagens e funcionalidades.
Diferente dos sistemas operacionais tradicionais, onde o kernel gerencia diretamente os recursos de hardware, o QNX adota uma abordagem modular e escalável utilizando sua arquitetura microkernel. Nesse modelo, os Resource Managers fornecem uma interface consistente e abstrata para a interação com dispositivos e sistemas de arquivos, permitindo que processos acessem recursos específicos de maneira segura e eficiente através de operações POSIX padrão.
Além disso, vimos exemplos práticos de como interagir com dispositivos gerenciados por Resource Managers, tanto através de programas em C quanto por meio de utilitários de linha de comando.
A documentação oficial do QNX oferece recursos adicionais para aprimorar e otimizar a implementação de Resource Managers, garantindo que desenvolvedores possam criar soluções robustas e adaptáveis para diversas aplicações.
Referências
1. QNX Software Systems. (n.d.). Interprocess Communication (IPC) https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.sys_arch/topic/ipc.html
2. QNX Software Systems. (n.d.). Microkernel architecture. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.sys_arch/topic/intro_MICROKERNELARCH.html
3. QNX Software Systems. (n.d.). Resource Managers. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.getting_started/topic/s1_resmgr.html
4. QNX Software Systems. (n.d.). Why write a resource manager? https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.sys_arch/topic/resource_WhyWrite.html
5. QNX Software Systems. (n.d.). Resource manager architecture. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.sys_arch/topic/resource_RESMGRARCH.html
6. QNX Software Systems. (n.d.). Writing a Resource Manager. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.resmgr/topic/about.html
7. QNX Software Systems. (n.d.). Writing a Resource Manager. https://www.qnx.com/developers/docs/8.0/com.qnx.doc.neutrino.getting_started/topic/s1_resmgr_Writing.html
8. QNX Software Systems. (2020, January 22). Demystifying QNX System programming – Device Resource Management [Video]. YouTube. https://www.youtube.com/watch?v=x6BO9bx4uvE









