TNeo  v1.08
Why reimplement TNKernel

Table of Contents

Explanation of essential TNKernel problems as well as several examples of poor implementation.

Essential problems of TNKernel

As a result of the two above, the kernel is buggy. And even more, the kernel is really hard to maintain because of inconsistency, so when we add new features or change something, we are likely to add new bugs as well.

Examples of poor implementation

One entry point, one exit point

The most common example that happens across all TNKernel sources is code like the following:

int my_function(void)
{
//-- do something
if (error()){
//-- do something
return ERROR;
}
//-- do something
return SUCCESS;
}

If you have multiple return statements or, even more, if you have to perform some action before return (tn_enable_interrupt() in the example above), it's great job for goto:

int my_function(void)
{
int rc = SUCCESS;
//-- do something
if (error()){
//-- do something
rc = ERROR;
goto out;
}
//-- do something
out:
return rc;
}

I understand there are a lot of people that don't agree with me on this (mostly because they religiously believe that goto is unconditionally evil), but anyway I decided to explain it. And, let's go further:

While multiple goto-s to single label are better than multiple return statements, it becomes less useful as we get to something more complicated. Imagine we need to perform some checks before disabling interrupts, and perform some other checks after disabling them. Then, we have to create two labels, like that:

int my_function(void)
{
int rc = SUCCESS;
if (error1()){
rc = ERROR1;
goto out;
}
if (error2()){
rc = ERROR2;
goto out_ei;
}
if (error3()){
rc = ERROR3;
goto out_ei;
}
//-- perform job
out_ei:
out:
return rc;
}

For each error handling, we should specify the label explicitly, and it's easy to mix labels up, especially if we add some new case to check in the future. So, I believe this approach is a superior:

int my_function(void)
{
int rc = SUCCESS;
if (error1()){
rc = ERROR1;
} else {
if (error2()){
rc = ERROR2;
} else if (error3()){
rc = ERROR3;
} else {
//-- perform job
}
}
return rc;
}

Then, for each new error handling, we should just add new else if block, and there's no need to care where to go if error happened. Let the compiler do the branching job for you. More, this code looks more compact.

Needless to say, I already found such bug in original TNKernel 2.7 code. The function tn_sys_tslice_ticks() looks as follows:

int tn_sys_tslice_ticks(int priority,int value)
{
TN_CHECK_NON_INT_CONTEXT
if(priority <= 0 || priority >= TN_NUM_PRIORITY-1 ||
value < 0 || value > MAX_TIME_SLICE)
tn_tslice_ticks[priority] = value;
return TERR_NO_ERR;
}

If you look closely, you can see that if wrong params were given, TERR_WRONG_PARAM is returned, and interrupts remain disabled. If we follow the one entry point, one exit point rule, this bug is much less likely to happen.

Don't repeat yourself

Original TNKernel 2.7 code has a lot of code duplication. So many similar things are done in several places just by copy-pasting the code.

As a result of the violation of the rule Don't repeat yourself, when we need to change something, we need to change it in several places. Needless to say, it is very error-prone practice, and of course there are bugs in original TNKernel because of that (refer to Bugs of TNKernel 2.7).

Macros that return from function

TNKernel uses architecture-depended macros like TN_CHECK_NON_INT_CONTEXT. This macro checks the current context (task or ISR), and if it is ISR, it returns TERR_WRONG_PARAM.

It isn't obvious to the reader of the code, but things like returning from function must be as obvious as possible.

It is better to invent some function that tests current context, and return the value explicitly:

enum TN_RCode my_function(void)
enum TN_RCode rc = TN_RC_OK;
// ...
goto out;
}
// ...
out:
return rc
}

Code for doubly-linked lists

TNKernel uses doubly-linked lists heavily, which is very good. I must admit that I really like the way data is organized in TNKernel. But, unfortunately, code that manages data is far from perfect, as I already mentioned.

So, let's get to the lists. I won't paste all the macros here, just make some overview. If we have a list, it's very common task to iterate through it. Typical snippet in TNKernel looks like this:

CDLL_QUEUE * curr_que;
TN_MUTEX * tmp_mutex;
curr_que = tn_curr_run_task->mutex_queue.next;
while(curr_que != &(tn_curr_run_task->mutex_queue))
{
tmp_mutex = get_mutex_by_mutex_queque(curr_que);
/* now, tmp_mutex points to the next object, so,
we can do something useful with it */
curr_que = curr_que->next;
}

This code is neither easy to read nor elegant. It's much better to use special macro for that (actually, similar macros are used across the whole Linux kernel code) :

TN_MUTEX * tmp_mutex;
tn_list_for_each_entry(tmp_mutex, &(tn_curr_run_task->mutex_queue), mutex_queue){
/* now, tmp_mutex points to the next object, so,
we can do something useful with it */
}

Much shorter and intuitive, isn't it? We even don't have to keep special curr_que.

Bugs of TNKernel 2.7

TNKernel 2.7 has several bugs, which are caught by detailed unit tests and fixed.

Bugs with mutexes are the direct result of the inconsistency and copy-pasting the code, as well as lack of unit tests.