This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

RTX - Data integrity

Hello,

I have a pretty generic question about embedded RTOS systems and data integrity.

I'm using RTX and I have several instances of structures that need to be read/written from several task.

As an example, take:

struct demo
{
int var_a;
int var_b;
int var_a_max;
int var_a_min;
}

If I have several global demo instances, what is the 'danger' in allowing several tasks to R/W the global instances?

I understand that the values could change mid execution but say you implemented a simple "lock and copy" procedure before each tasks loop execution.

Would mailboxes really be required in this case?

Further, what if all tasks only ever read/write one of the fields (var_a) do you even need a lock and copy or would simple copy do?

Finally, say I did want to implement a generic mailbox pattern so that any task could request a copy from a managing task and send updates to a managing task. Does any one have an example of such a pattern?

Thanks. I appreciate any discussion and opinion on this topic, I'm looking for the most efficient (time and complexity) way to proceed while still being 'safe' in my logic.

M

  • For writes I would use a semaphore, not a mailbox, to make sure only one task can write at a time. For reads I would figure out whether the read is atomic to avoid seeing intermediate results in case of a preemption in the middle of a modification.

    For Cortex M3 I think 32 bit reads and writes are generally atomic.

    If each task only writes certain fields and only reads certain others you can probably get away without any synchronization but I would definitely look at the assembly code to make sure you know what's going on.

    It's also important to understand which tasks can even interrupt others, most embeeded RTOSes use fixed priorities so you can make assumptions who gets to finish their task and who gets interrupted.

    Andrew

  • If I have several global demo instances, what is the 'danger' in allowing several tasks to R/W the global instances?

    Because assignment/read operations that involve memory are generally not atomic, register values might get corrupted due to a context switch while such an operation is in progress.

    Would mailboxes really be required in this case?

    I would say a mutex would be a better choice - but that totally depends on the details of your application.

    Further, what if all tasks only ever read/write one of the fields (var_a) do you even need a lock and copy or would simple copy do?

    Any write operation requires synchronization primitives. It might be safer to lock the entire instance considering possible future developments and maintenance.

  • Thanks for the input Andrew and Tamir.

    If I'm not mistaken 32bit reads and writes are atomic on Cortex M3 devices.

    This is why I'm unclear as to the need to synchronize access if precaution is taken to always copy the value before using the value in each task?

    Tamir - in your experience do you always synchronize access to any global data in a multi task environment? Wouldn't this bloat quite quickly.

    (Note I realize good design and all that would lead to minimal synch needed but realistically you often need global data...)

    Do you guys use any neat macro wrappers or anything. This really concerns me as I'm only designing half of a system and I need to document the interface for the other half...

    Tamir - can you explain how a register value might get corrupted during a context switch? Have you ever experienced this? Wouldn't this be a major concern above and beyond data integrity?

  • Tamir - can you explain how a register value might get corrupted during a context switch? Have you ever experienced this? Wouldn't this be a major concern above and beyond data integrity?

    Let's say that task1 needs to increment a value of a variable and that the accesses are not atomic. Typically, it will load the value from memory first - let's assume the variable has an initial value 10. A context switch occurs after the load operation and before task1 can decrement the value, allowing task2 to increment the value to 11 without interruption. Task1, having made a copy of the variable before the context switch, decrements the value to 9 and write it to memory. Task2 will then have to deal with what might represent an undefined situation.

    Tamir - in your experience do you always synchronize access to any global data in a multi task environment? Wouldn't this bloat quite quickly.

    Of course not. In some cases such as when using a circular buffer (producer-consumer design pattern) no synchronization is needed. Pure read access do not require synchronization, of course.

    If I'm not mistaken 32bit reads and writes are atomic on Cortex M3 devices.

    Yes, but see the example above. Typically, the data addressed needs to be changed in some way which is the source of the trouble.

    This is why I'm unclear as to the need to synchronize access if precaution is taken to always copy the value before using the value in each task?

    This can save time by reducing overhead, but still requires the usage of synchronization primitives while making the copy to a local variable and when writing back to shared memory.

  • No, I don't use any macro wrappers at the moment. I've seen this done and it can work out but I currently rely on comments and documentation. If you want to do something library-like you could look at core_cm3.c/.h for some ideas.

    In our code we're not concerned about portability to a non-M3 platform so we haven't done much in the way of abstraction. In general, though, it's a good idea to sandbox operations having to do with synchronization and inter-task communications and quarantine them from general-purpose code. That way an upgrade to an OS with proper synchronization primitives will be much easier.

    Andrew

  • Macros (as in #define) is normally a thing to avoid if possible.
    It's too easy to give them side effects that wasn't intended and that isn't visible without looking at preprocessed sources.

    When possible, it is way nicer to implement inline functions, since you can then get the same type checking etc as for normal functions, but still get the compiler to inline the actions.

    In many situations, it is meaningful to use C++ even when the official design does not call for a fully object-oriented application.

    Things like:

    bool my_function(...) {
        ...
        {
            CLockWrite synchronize(my_lock);
            res = do_something();
            if (!res) return false;
        }
        ...
        return true;
    }
    


    Is too valuable to ignore. A constructor to lock a resource, and the destructor to release the lock means the code has a clearly protected section of code without any chance of accidentally forgetting to release the resource lock.

  • Thanks again guys.

    @Tamir - I'm not sure I would describe your example situation as register corruption, but I get your point.

    In my situation I think it will make most sense to have 'task 2' deal with any undefined situation.

    @Andrew - Yes, abstraction is very important to me. This is something I've come to appreciate over the last decade. On this project alone we have already changed the hardware once and switched a (important) 3rd party library component. I very much like well defined abstraction layers. (HAL, DAL...)

    For anyone else reading this thread, based on the discussion above, my plan is to use synchronization mutex objects to lock reads and writes (and some algorithm sections) of some global data.

    What I did that really helped me come to this decision was make a diagram with all the 'global objects' and I illustrated which task write to and which read from each global object. With this diagram synchronization became very clear.

    Thanks.

    M

  • @Per - yes, I really like this kind of thing when using C++. I hadn't thought about using a simple C++ class in my C app but I suppose it's a good idea.
    Do you than compile your entire application as C++? Does this not link in a the C++ runtime? (Unneeded stuff like heap management, RTI...)

    Macros certainly have some issues but I would argue, like you, that they to are too valuable to ignore. Inline functions become slightly less useful across translation units but yes are certainly a good strategy in some cases.

  • Different compilers have different rules.

    You can normally turn off RTI.

    You normally don't get the extra heap code as long as no code plays with new and delete (or you replace them).

    One of the original design goals (at least before C moved ahead and stopped being a strict subset of C++) was to allow a developer to take a C program and recompile as C++, and then decide what parts of the code to rewrite to use an object-oriented model.

    Compiling all C code as C++ also means that you get the type-safe linking unless you explicitly wants symbols to use the C naming convention for external symbols.

  • at least before C moved ahead and stopped being a strict subset of C++

    C never actually was a proper subset of C++. And that's because C++, not C, moved ahead and hast been doinng so ever since it was first given that name.