Lista ligada – Estrutura “Zero Copy” – Parte 1

Lista ligada
Este post faz parte da série Estruturas de controle de memória

Olá caro leitor, nos meses passados falamos sobre algumas estruturas de dados que podem ser muito úteis quando estamos trabalhando no desenvolvimento do firmware de nosso projeto. Observamos que a partir do buffer circular, conseguimos derivar outras estruturas mais complexas e poderosas. Vamos hoje adicionar ao nosso grupo de ferramentas uma estrutura primitiva, mas muito popular que é a lista ligada, e que também podemos derivar filas, pilhas e até buffers com ela porém com a possibilidade de controlar o local de depósito dos dados.

Estrutura “Zero Copy”?

Antes de entrarmos nos detalhes da nossa lista ligada, primeiro vamos esclarecer o que esse termo significa. Quando falamos de buffer circular nos artigos anteriores, o usuário deve ter notado, que quem controla a forma de armazenar os dados é a própria estrutura. Imagine agora que cada slot não seja de um tipo primitivo, mas sim uma cadeia de bytes, então um processo completo de inserção e remoção terá pelo menos duas operações de cópia, sendo uma da origem dos dados para a memória interna da estrutura, e outra da estrutura para o destino, ou seja, onde os dados serão consumidos. Perceba que isso pode ser irrelevante para cadeias de bytes de pequeno tamanhos, mas que se torna um overhead considerável à medida que o tamanho do bloco a ser enviado cresce.

Para minimizar esse problema, podemos ter apenas memória em quem gera dados, e em vez de propagar os blocos de dados camada por camada, simplesmente podemos propagar a referência (endereço) onde encontram-se os dados a serem consumidos. Sabemos que endereços (ponteiros falando na perspectiva C/C++) são geralmente tipos de dados primitivos, onde o próprio set de instruções do processador consegue cuidar de mover seu valor de um local para outro na memória sem precisar de um processo de cópia (ex.: memcpy).

Partindo dessa abordagem podemos ter um sistema de cadeia de ponteiros, que “viajam” do processo (processo em sistemas embarcados podem também ser threads / tasks, visto que nem todo sistema embarcado possui um processador com gerenciamento de memória virtual, a popular MMU) produtor, e entrega ao IP de transmissão de dados o endereço de onde ele deve copiar os dados, reduzindo para apenas 1 operação de cópia. Se o campo de dados for então de um tipo primitivo, apenas uma de-referência simples (o operador *) permite acessar os dados de origem, essa é a estrutura de dados “Zero Copy”.

Voltando às listas ligadas

Voltando ao assunto principal do post, o mecanismo de propagação de referências (Zero Copy) pode ser empregado nas estruturas aqui já apresentadas, porém por que peguei listas ligadas para ilustrar? Por dois bons motivos. O primeiro, apresentar a versatilidade das listas, quase qualquer estrutura de dados pode ser criada a partir dela. O segundo motivo, é que uma fila construída a partir de listas ligadas por exemplo, podem trabalhar para organizar não apenas filas de dados, mas também de processos, por exemplo ordenando por ordem de chegada (ou prioridade) o acesso a um recurso compartilhado do sistema em execução (UARTs, Sistema de arquivos, I/O em geral).

Agora, o que é uma lista ligada? Bom, já falamos de toda a burocracia que envolve esse artigo, logo responder a pergunta fica fácil, observem a figura abaixo:

Imagine que você possui uma estrutura que possui um campo que contém dois ponteiros, sendo:

  • Um ponteiro chamado value ou data, que aponta para a localidade de onde seus dados estão armazenados (Opa, “Zero Copy” à vista!);
  • Um segundo ponteiro chamado next, que aponta pra justamente outra estrutura igualzinha corrente.

Com 2 ou mais teremos a chamada lista ligada simples, possui esse nome pois inicialmente as estruturas estão separadas e em diferentes pontos da memória. Porém, através da execução de uma operação geralmente chamada de link, elas criam o aspecto da figura acima, ou seja, não são contíguas em memória, mas podem saber uma da existência da outra, pois a função do ponteiro next é dizer onde encontra-se o próximo item da lista e assim sucessivamente até que o último ponteiro next possua valor nulo, geralmente indicando o fim da lista (são comuns implementações onde um valor de marca é utilizado para esse fim). Vejam como ela ficaria na memória do nosso processador:

Lista ligada na memória

Percebam que os itens estão em diversos pontos da memória. Esse tipo de lista é perfeito para implementação de filas que possuem diversos produtores de dados. A segunda vantagem é que sua dimensão  pode ser ajustada sob demanda, ou seja, em tempo de execução. Nas estruturas estáticas que demonstramos, o usuário tem que saber o quanto de memória vai alocar em tempo de compilação. No caso da lista ligada, um novo item da lista só será alocado para um uso, podendo ser liberado para outro fim, sendo indicado para uso com a Memory Pool que apresentamos aqui. Logo, se a aplicação possuir uso de memória de tamanho variável durante o tempo, apenas será necessário prover algum mecanismo de alocação de memória.

Gostei, quero utilizar, tem exemplo desse também?

Trouxemos um exemplo de lista ligada, com funções típicas de seu uso, ou seja, inserção, remoção pela posição, remoção do primeiro item, e do último, sendo esses dois últimos perfeitos para implementação de uma fila “Zero Copy”. Além disso, o código exemplo traz o mecanismo de “encabeçamento” da lista, ou seja, a estrutura slist_info_t contém informações sobre a lista, bem como o ponteiro que aponta ao primeiro item. A figura abaixo mostra como isso ocorre:

Essa estrutura é usada como descritor da lista, ou seja, quando o usuário quiser várias listas diferentes terá que instanciar sempre a cabeça a qual ela pertence, e inserir e remover, sempre em relação à cabeça da lista. Para facilitar, foi provida uma macro para declarar a cabeça de uma lista, inicializada. Vamos dar uma olhada nos arquivos, primeiro a interface:

/**
 * @brief single linked list interface file
 */

#ifndef __SLIST_H_
#define __SLIST_H_

#include <stdbool.h>

/* single linked list data structure */
typedef struct slist_s {
	void *data;
	struct slist_s *next;
}slist_t;

/* single linked list head data structure */
typedef struct {
	slist_t *first;
	int noof_elements;
}slist_info_t;


/**
 * @brief insert an container in the slist
 */
int slist_insert(slist_info_t *s, slist_t *item, void *data);

/**
 * @brief gets data from a specified position with optional remotion
 */
int slist_get_data_from_position(slist_info_t *s, int position,  void *data, bool remove);


/**
 * @brief gets data from head with optional remotion
 */
int slist_get_data_from_head(slist_info_t *s, void *data, bool remove);


/**
 * @brief gets data from tail with optional remotion
 */
int slist_get_data_from_tail(slist_info_t *s,  void *data, bool remove);


/**
 * @brief deletes all the items in current list
 */
int slist_clean(slist_info_t *s);

/**
 * @brief get the current number of entries on list
 */
int slist_get_noof_entries(slist_info_t *s);


/**
 * @brief linked list instantiation macro:
 */
#define SLIST_DECLARE(name)			\
		slist_info_t name = {		\
			.first = (void *)0,	\
			.noof_elements = 0,	\
		}			        


#endif /* SLIST_H_ */

Na implementação alguns cuidados extras foram tomados. Diferentemente das estruturas estáticas, as listas ligadas não tão seguras a “wild pointers” e valores inválidos, por isso foram adicionadas checagem de parâmetros. Percebam a presença das rotinas link e unlink que fazem a lista “olhar” ou não para o próximo elemento (se houver).

/**
 * @brief single linked list interface file
 */

#include <string.h>
#include <stdlib.h>
#include "slist.h"

/*
 * Internal functions
 */

/**
 * @brief makes a link of a outside incoming item to a list
 */
static void slist_link(slist_info_t *s, slist_t *item)
{
	/* in order to ensure O(1) insertion
	 * only does a prepend in target list
	 */
	item->next = s->first->next;
	s->first = item;
	s->noof_elements++;
}

/**
 * @brief breaks the link of specified list item
 */
static void slist_unlink(slist_info_t *s, slist_t *item, int position)
{
	/*
	 * WARN: when using unlink by item, make sure the desired item to
	 *  	unlink is the next linked to the item argument
	 */

	/* check how to link */
	if(item != NULL) {
		/* item based, remove the next entrie, this the desired
		 * entry to unlink and not the current item
		 */
		slist_t *tmp = item->next;
		item->next = tmp->next;
		tmp->next = NULL;
		if(s->noof_elements > 0) s->noof_elements--;

	} else if (position != 0) {
		/*
		 * TODO: position based is used only in special case,
		 * 	     when the list has only 1 position, so if needed in future
		 * 	     implement to handle all the list using position based
		 * 	     remotion
		 */
		if(s->noof_elements > 1) {
			slist_t *tmp = s->first;
			s->first = tmp->next;
			tmp->next = NULL;
			if(s->noof_elements > 0) s->noof_elements--;

		}else {
			s->first = NULL;
			s->noof_elements = 0;
		}
	}

}


/*
 *  Public functions
 */

int slist_insert(slist_info_t *s, slist_t *item, void *data)
{
	int ret = 0;

	/* check arguments */
	if((s == NULL) || (item == NULL) || (data == NULL)) {
		ret = -1;
	} else {

		/* is the first element? */
		if(s->noof_elements == 0) {
			/* links the new element directly */
			item->data = data;
			item->next = NULL;
			s->first = item;
			s->noof_elements++;
		} else {
			item->data = data;
			item->next = NULL;
			/* prepend the new element on list */
			slist_link(s, item);
		}
	}
	return (ret);
}



int slist_get_data_from_position(slist_info_t *s, int position,  void *data, bool remove)
{
	int ret = 0;

	/* check arguments */
	if((s == NULL) || (position == 0)) {
		ret = -1;
	} else {
		/* check if position exists */
		if(position <= s->noof_elements) {
			/* list has only 1 position? */
			if(s->noof_elements == 1) {
				data = s->first->data;
				/* if needs unlink */
				if(remove != false) {
					slist_unlink(s,NULL, position);
				}
			} else {
				/* find the index where the data is located */
				slist_t *tmp = s->first;
				for(int i = 1; i != position -1 ; tmp = tmp->next, i++);
				data = tmp->data;

				/* unlink if needed */
				if(remove != false) {
					slist_unlink(s,tmp, 0);
				}
			}

		} else {
			ret = -1;
		}

	}
	return(ret);
}

int slist_get_data_from_head(slist_info_t *s, void *data, bool remove)
{
	int ret = 0;

	/* check arguments */
	if((s == NULL)) {
		ret = -1;
	} else {

		if(s->noof_elements != 0) {
			/* we want the head of list, so the first item
			 * added in the list, which is located at end of it
			 */
			int position = s->noof_elements;
			slist_t *tmp = s->first;
			for(int i = 1; i != position -1 ; tmp = tmp->next, i++);
			data = tmp->data;

			/* unlink if needed */
			if(remove != false) {
				slist_unlink(s,tmp, 0);
			}
		} else {
			/* list is empty */
			ret = -1;
		}
	}
	return(ret);

}

int slist_get_data_from_tail(slist_info_t *s,  void *data, bool remove)
{
	int ret = 0;

	/* check arguments */
	if((s == NULL)) {
		ret = -1;
	} else {
		/* we want the tail of list, so the last item
		 * added in the list, which is located at beginning of it
		 */
		data = s->first->data;

		/* unlink if needed */
		if(remove != false) {
			slist_unlink(s,NULL, 1);
		}
	}
	return(ret);

}

int slist_clean(slist_info_t *s)
{
	int ret = 0;

	/* check arguments */
	if(s == NULL) {
		ret = -1;
	} else {
		int position = s->noof_elements;
		slist_t *tmp = s->first;

		/* unlink the items chain from root of the list */
		s->first = NULL;
		s->noof_elements = 0;

		/* unlink the further slots */
		for(int i = 1; i != position - 1; i++) {
			slist_t *unlink = tmp->next;
			tmp->next = NULL;
			tmp = unlink;
		}

	}
	return(ret);
}


int slist_get_noof_entries(slist_info_t *s)
{
	int ret = 0;
	/* check arguments */
	if(s == NULL) {
		ret = -1;
	}else {
		ret = s->noof_elements;
	}
	return(ret);
}

Legal, mas por que lista ligada simples?

Bom, o fato é que uma lista ligada pode possuir infinitos ponteiros de ligação. Quando possui apenas um único ponteiro desse tipo, tendemos a chamar ele de lista simples, porém no próximo artigo falaremos sobre um tipo mais popular de lista ligada, a lista dupla, e como ela pode reduzir tempo de busca, inserção e remoção com um ponteiro extra.

Conclusão

Neste artigo executamos um pontapé inicial sobre estruturas que usam referências para armazenamento de dados e que não são contíguas em memória, demonstrando vantagem como flexibilidade e uso sob demanda. Nos próximos artigos vamos apresentar a lista dupla  e derivar outras estruturas como filas, pilhas e memory pools.

Espero que você leitor se beneficie com essa revisão ou apresentação em caso de ser a primeira vez que o vê. Até á próxima.

Links úteis

Repositório no Github contendo código exemplo de uso, clique aqui.

Estruturas de controle de memória

Memory Pool – Gerenciador de memória de bloco fixo real time
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
1 Comentário
recentes
antigos mais votados
Inline Feedbacks
View all comments
Allef Pablo Araujo
Allef Pablo Araujo
21/12/2016 14:11

Felipe Neves, parabéns, o artigo ficou muito bom!
Espero ver mais materiais sobre estruturas de dados por aqui!

Home » Software » Lista ligada – Estrutura “Zero Copy” – Parte 1

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: