Menu

SMP Kernel

Mike Jones

The eCos kernel has some modifications for SMP. The main problems addressed are: startup, and handling of scheduler locking during interrupts.

During startup the schedule locking can cause problems, so a new get lock function was added that is only used when a thread is started, and only active when SMP and ARM MULTICORE are active. This prevents interactions with the i386 SMP application. If it is ever tested with i386 and works, the ARM MULTICORE check could be removed.

void Cyg_Scheduler::thread_entry( Cyg_Thread *thread )
{
    clear_need_reschedule();            // finished rescheduling
    set_current_thread(thread);         // restore current thread pointer

    CYG_INSTRUMENT_THREAD(ENTER,thread,0);

    thread->timeslice_reset();
    thread->timeslice_restore();

    // Finally unlock the scheduler. As well as clearing the scheduler
    // lock this allows any pending DSRs to execute. The new thread
    // must start with a lock of zero, so we keep unlocking until the
    // lock reaches zero.

#if defined(CYGPKG_HAL_SMP_SUPPORT) && defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)
    // This removes dependencies on start order of multiple cores. It also
    // addressed other startup problems on the Cortex A9 hal.
    while( get_smp_sched_lock() != 0 )
#else
    while( get_sched_lock() != 0 )
#endif
        unlock();
}

    static cyg_ucount32 get_sched_lock()
    {
        return sched_lock;
    };

#if defined(CYGPKG_HAL_SMP_SUPPORT) && defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)
    static cyg_ucount32 get_smp_sched_lock()
    {
        return (lock_data.holder != CYG_KERNEL_CPU_THIS()) ? 0 : sched_lock;
    }
#endif

The above code change allows core 0 to start a thread when a secondary core has the scheduler locked.

If this is removed, there is an ASSERT in this code:

    static void zero_sched_lock()
    {
        CYG_INSTRUMENT_SMP(LOCK_ZERO,CYG_KERNEL_CPU_THIS(),0);
        CYG_ASSERT( sched_lock != 0, "Scheduler lock already zero");
        HAL_SMP_SCHEDLOCK_ZERO( sched_lock, lock_data );
    };

The assert comes from the macro HAL_SMP_SCHEDLOCK_ZERO.

The real time clock was modified properly handle SMP interrupts.

Cyg_RealTimeClock::Cyg_RealTimeClock()
    : Cyg_Clock(rtc_resolution),
      interrupt(CYGNUM_HAL_INTERRUPT_RTC,
                CYGNUM_KERNEL_COUNTERS_CLOCK_ISR_PRIORITY,
                (CYG_ADDRWORD)this, isr, dsr)
{
    CYG_REPORT_FUNCTION();

    HAL_CLOCK_INITIALIZE( CYGNUM_KERNEL_COUNTERS_RTC_PERIOD );

#if defined(CYGPKG_HAL_SMP_SUPPORT) && defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)
    interrupt.set_cpu(CYGNUM_HAL_INTERRUPT_RTC, 0);
#endif

    interrupt.attach();

    interrupt.unmask_interrupt(CYGNUM_HAL_INTERRUPT_RTC);

    Cyg_Clock::real_time_clock = this;
}

Initializing the clock now sets the target cpu, which is core 0. This insures all interrupts are delivered to core 0. The interrupt strategy is for all interrupts to be sent to core 0, and processed by core 0. The one exception is inter-cpu interrupts.

In most HALs the scheduler is locked in its Vectors.S code and unlocked in interrupt_end. However, this causes problems for SMP. The main challenge is dealing with the fact that if an interrupt arrives at core 0, the scheduler may be locked by another core. If the interrupt tries to unlock with the normal unlock methods, which includes a spinlock ,a deadlock typically follows.

The solution was to have the Vectors.S implementation do a trylock, which uses a tryspin. Just before interrupt_end Vectors.S checks if it owns the scheduler lock. If it owns the lock, it calls interrupt_end. And if it does not own the lock, it skips interrupt_end. This means that a DSR that needs to be posted in interrupt_end will wait until another interrupt or a thread switch. Given that there is a real time clock, it will add some latency in the posting of DSRs.

However, this prevents random failures from assertions. If Vectors.S increments a lock it does not own, it always leads to an assertion, which comes pretty quick if you are running a network. If it waits on a spinlock, the application typically deadlocks.

#if defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)
        .extern cyg_scheduler_trylock
        stmfd   sp!,{r0,r1,r2,r3,r4}

    bl      cyg_scheduler_trylock

    ldmfd   sp!,{r0,r1,r2,r3,r4}
    b       1f
#endif

#if defined(CYGFUN_HAL_COMMON_KERNEL_SUPPORT)
        .extern cyg_scheduler_sched_lock
        ldr     r3,.cyg_scheduler_sched_lock
        ldr     r4,[r3]
        add     r4,r4,#1
        str     r4,[r3]
#endif
1:

The code that does the trylock is shown above. In ARM MULTICORE mode it issues the trylock, and then jumps around the manual scheduler lock handling. In the i386 version, the schedule lock is not incremented, and I was unable to find any place in the code where it this is done. But it is not done in their interrupt handling assembly.

#if defined(CYGINT_HAL_ARM_ARCH_ARM_CORTEXA9)
        .extern cyg_scheduler_read_lock_holder
        stmfd   sp!,{r0,r1,r2,r3,r4}

        mrc     p15,0,r1,c0,c0,5         // Read multiprocessor affinity register
        and     r1, r1, #3               // Mask off, leaving CPU ID field

        bl  cyg_scheduler_read_lock_holder
    cmp r1, r0
    ldmfd   sp!,{r0,r1,r2,r3,r4} 

    bne 17f
#endif

Just before interrupt_end is called, the lock is checked, and if we (core #) are not the holder, we jump around interrupt_end.

To support the trylock and reading the holder, there is kernel code in kapi and in the scheduler to support these calls. Furthermore, there is assembly for the spinlock. The only difference in the assembly is to check the lock only once to see if it is available, and to only attempt to take the lock once.

There were a few assertion problems that were addressed.

void
idle_thread_main( CYG_ADDRESS data )
{
CYG_REPORT_FUNCTION();

for(;;)
{
    idle_thread_loops[CYG_KERNEL_CPU_THIS()]++;

    HAL_IDLE_THREAD_ACTION(idle_thread_loops[CYG_KERNEL_CPU_THIS()]);

if !defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)

    CYG_ASSERT( Cyg_Scheduler::get_sched_lock() == 0, "Scheduler lock not zero" );

endif

if 0

    // For testing, it is useful to be able to fake
    // clock interrupts in the idle thread.

    Cyg_Clock::real_time_clock->tick();

endif

ifdef CYGIMP_IDLE_THREAD_YIELD

    // In single priority and non-preemptive systems,
    // the idle thread should yield repeatedly to
    // other threads.
    Cyg_Thread::yield();

endif

}

}

In idle_thread_main an assertion was disabled because an idle thread on one core can interact with locking on another core, causing an assert.

Cyg_Check_Structure_Sizes::Cyg_Check_Structure_Sizes(int x) __THROW
{
cyg_bool fail = false;

dummy = x+1;

CYG_CHECK_SIZES( cyg_thread, Cyg_Thread );
CYG_CHECK_SIZES( cyg_interrupt, Cyg_Interrupt );
CYG_CHECK_SIZES( cyg_counter, Cyg_Counter );
CYG_CHECK_SIZES( cyg_clock, Cyg_Clock );
CYG_CHECK_SIZES( cyg_alarm, Cyg_Alarm );
CYG_CHECK_SIZES( cyg_mbox, Cyg_Mbox );
CYG_CHECK_SIZES( cyg_sem_t, Cyg_Counting_Semaphore );
CYG_CHECK_SIZES( cyg_flag_t, Cyg_Flag );
CYG_CHECK_SIZES( cyg_mutex_t, Cyg_Mutex );
CYG_CHECK_SIZES( cyg_cond_t, Cyg_Condition_Variable );

if !defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)

// The ARM spinlock is not a simple value that this can handle.
CYG_CHECK_SIZES( cyg_spinlock_t, Cyg_SpinLock );

endif

CYG_ASSERT( !fail, "Size checks failed");

}

Spinlocks were an assertion problem because the kernel expected a simple integer value. The iMX6 SDK used a whole line in the cache and the macros for spin locks would not support it. Therefore, the above assertion was removed for ARM MULTICORE.

class Cyg_SpinLock
{
HAL_SPINLOCK_TYPE lock;

public:

// Constructor, initialize the lock to clear

if defined(CYGPKG_HAL_SMP_SUPPORT) && defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)

// The ARM spinlock is a pointer type to a value that may be locked in
// a cache line. Rather than use double pointers, it is easier to pass
// lock into the macro.
Cyg_SpinLock() { HAL_SPINLOCK_INIT_CLEAR(lock); };

else

Cyg_SpinLock() { lock = HAL_SPINLOCK_INIT_CLEAR; };

endif

Also, to support a strutted spinlock value, the above initialization was changed to pass the lock.

Due to the lack of a simple variable, the macro was redefined to accept a parameter. This allows the macro to operate on a variable with structure.

Cyg_Check_Structure_Sizes::Cyg_Check_Structure_Sizes(int x) __THROW
{
cyg_bool fail = false;

dummy = x+1;

CYG_CHECK_SIZES( cyg_thread, Cyg_Thread );
CYG_CHECK_SIZES( cyg_interrupt, Cyg_Interrupt );
CYG_CHECK_SIZES( cyg_counter, Cyg_Counter );
CYG_CHECK_SIZES( cyg_clock, Cyg_Clock );
CYG_CHECK_SIZES( cyg_alarm, Cyg_Alarm );
CYG_CHECK_SIZES( cyg_mbox, Cyg_Mbox );
CYG_CHECK_SIZES( cyg_sem_t, Cyg_Counting_Semaphore );
CYG_CHECK_SIZES( cyg_flag_t, Cyg_Flag );
CYG_CHECK_SIZES( cyg_mutex_t, Cyg_Mutex );
CYG_CHECK_SIZES( cyg_cond_t, Cyg_Condition_Variable );

if !defined(CYGINT_HAL_ARM_ARCH_ARM_MULTICORE)

// The ARM spinlock is not a simple value that this can handle.
CYG_CHECK_SIZES( cyg_spinlock_t, Cyg_SpinLock );

endif

CYG_ASSERT( !fail, "Size checks failed");

}
~~~~~~

To remedy a side effect, a check size was removed. This could be improved by using a check that is a HAL defined macro.

If all the #defines are correct, these changes should not affect other targets. It is quite possible the iMX6 HAL could be changed to work around the problem by someone more knowledgable of the kernel. What I can say is that SMP startup seems reliable with these changes and I have not had any problems.

Testing: 01/26/2014 SVN Rev 52 tested ok for 2/4 CPU iMX6


Related

Wiki: iMX6