Kamailio: shared memory hashtable

Using shared memory in Kamailio is fundamental and sometimes inevitable. Luckily, Kamailio’s core abstract us from A LOT of the complexity behind this kind of memory management and help us build a ready to use in-memory hash table in no time!

First of all, you need to include the str_hash header file but it is probably implicitly included by some other mandatory header files.


#include <kamailio/str_hash.h>

For this example, I’ll store the hashtable’s (HT from now on) address inside a global variable visible from the entire module. Being that the case, I need to create it as follows:


struct str_hash_table *my_ht;

Later, I have to allocate some memory for the variable and there’s no better place for that than the module’s mod_init() function. Having allocated memory, I need to perform another allocation. This time is for the internal str_hash_table structure that contains the metadata for maintaining our information accessible. Usually, the function in charge of this task is str_hash_alloc() but looking its internals, we will find that it uses pkg_malloc() which certainly won’t work for us. Due this situation, I created an equivalent function for shared memory, mostly because Kamailio’s core doesn’t provide this functionality, or at least, I wasn’t able to find it.

static int shm_str_hash_alloc(struct str_hash_table *ht, int size)
{
ht->table = shm_malloc(sizeof(struct str_hash_head) * size);

if (!ht->table)
return -1;

ht->size = size;
return 0;
}
[/c]

Then, we make the actual allocation:
[c]
static int mod_init(void)
{
my_ht = shm_malloc(sizeof(struct str_hash_table));
if (!my_ht)
{
LM_ERR("No enough memory");
return -1;
}

if (shm_str_hash_alloc(my_ht, 69) != 0)
{
LM_ERR("Error allocating shared memory hash tablen");
return -1;
}

}
return 0;
}

Then, I need to define the structure that will be contained into the shared memory.

typedef struct call
{

unsigned int start_timestamp;
unsigned int end_timestamp;

} call_t;

Storing and retrieving

In this example, I will store the call ID as the key of the HT’s entry and in the useful part, the call’s start and end timestamp.

First, I created a function that encapsulates all the logic for allocating a new call into shared memory. Please notice that having a HT storing your data, it doesn’t implicitly allocate your information in shared memory address space, this is, you have to allocate your structure using shm_malloc() before adding it to the HT.

static call_t *alloc_new_call()
{
call_t *call = shm_malloc(sizeof(call_t));
call->start_timestamp = 0;
call->end_timestamp = 0;
return call;
}

Every entry in the HT is represented by str_hash_entry which is a linked-list ready structure that contains the key and the data part as an union. Take in consideration that an union is NOT an structure and its fields retain only one value at a time. Whatever modification you make to any field it will immediately affect to the others.

static int add_new_call(str *callid, call_t *call)
{
struct str_hash_entry *e = str_hash_get(my_ht, callid->s, callid->len);

/* check if the call id already exists */
if (e != NULL)
{
LM_ALERT("Call already exists: key %.*sn", e->key.len, e->key.s);
return -1;
}

/* we allocate the new entry into shared memory. */
e = shm_malloc(sizeof(struct str_hash_entry));

if (e == NULL)
{
LM_ERR("No shared memory leftn");
return -1;
}

/* duplicate the key, always in shared memory */
if (shm_str_dup(&e->key, client_id) != 0)
{
LM_ERR("No shared memory leftn");
return -1;
}

/* we reference the call using one of the members of the union */
/* a lock_get() may be needed */
e->u.p = call;
/* a lock_release() may be needed */

}

Finally we need a function callable from the script. In this case start_monitoring does this for us and it must be registered as an exported command.

static cmd_export_t cmds[]= {
{"start_monitoring", (cmd_function) start_monitoring, 0, 0, 0, ANY_ROUTE},
{"stop_monitoring", (cmd_function) stop_monitoring, 0, 0, 0, ANY_ROUTE},
{0,0,0,0,0,0}
};

And:

static int start_monitoring(struct sip_msg* msg, char* p1, char* p2)
{

call_t *call = NULL;

if (parse_headers(msg, HDR_CALLID_F, 0) != 0)
{
LM_ERR("Error parsing call-id");
return -1;
}

call = allow_new_call();
if (call == NULL)
return -1;

call->start_timestamp = (unsigned int) time(0);

if (add_new_call(&msg->callid->body, call) != 0)
return -1;

LM_DBG("Call added!");
return 1;

}

To finish this little tutorial, I present the stop_monitoring which tries to retrieve the entry using the callid of the call. In case it was found, it writes the current timestamp to the field end_timestamp.

static int stop_monitoring(struct sip_msg* msg, char* p1, char* p2)
{

call_t *call = NULL;
struct str_hash_entry *e = NULL;

if (parse_headers(msg, HDR_CALLID_F, 0) != 0)
{
LM_ERR("Error parsing call-id");
return -1;
}

e = str_hash_get(my_ht, msg->callid->body.s, msg->callid->body.len);
if (e == NULL)
{
LM_ERR("Call not found");
return -1;
}

((call_t *) e->u.p)->end_timestamp = time(0);

return 1;

}

PS: I am aware that I never deallocated the memory I used. I may write the dealloc function for the next entry 😀