Zephyr學習(四)系統時鐘
每一個支援多程序(執行緒)的系統都會有一個滴答時鐘(系統時鐘),這個時鐘就好比系統的“心臟”,執行緒的休眠(延時)和時間片輪轉排程都需要用到它。
Cortex-M系列的核心都有一個systick時鐘,這個時鐘就是設計用來支援作業系統的,是一個24位的自動重灌載向下計數器,中斷入口就位於中斷向量表裡面,定義在zephyr-zephyr-v1.13.0\arch\arm\core\cortex_m\vector_table.S:
1 SECTION_SUBSEC_FUNC(exc_vector_table,_vector_table_section,_vector_table) 2 3/* 4* setting the _very_ early boot on the main stack allows to use memset 5* on the interrupt stack when CONFIG_INIT_STACKS is enabled before 6* switching to the interrupt stack for the rest of the early boot 7*/ 8.word _main_stack + CONFIG_MAIN_STACK_SIZE 9 10.word __reset 11.word __nmi 12 13.word __hard_fault 14.word __mpu_fault 15.word __bus_fault 16.word __usage_fault 17.word __reserved 18.word __reserved 19.word __reserved 20.word __reserved 21.word __svc 22.word __debug_monitor 23 24.word __reserved 25.word __pendsv 26#if defined(CONFIG_CORTEX_M_SYSTICK) 27.word _timer_int_handler 28#else 29.word __reserved 30#endif
第27行,_timer_int_handler()就是systick時鐘的中斷入口函式。
那麼問題來了,前面的啟動過程隨筆裡並沒有分析到systick時鐘是何時被初始化的,事實上systick也是通過裝置巨集定義的方式進行初始化的,定義在zephyr-zephyr-v1.13.0\drivers\timer\sys_clock_init.c:
SYS_DEVICE_DEFINE("sys_clock", _sys_clock_driver_init, sys_clock_device_ctrl, PRE_KERNEL_2, CONFIG_SYSTEM_CLOCK_INIT_PRIORITY);
可知,系統時鐘屬於PRE_KERNEL_2類裝置,同一類裝置也是有分優先順序的,優先順序高的先初始化,初始化函式為_sys_clock_driver_init(),定義在zephyr-zephyr-v1.13.0\drivers\timer\cortex_m_systick.c:
1 int _sys_clock_driver_init(struct device *device) 2 { 3/* enable counter, interrupt and set clock src to system clock */ 4u32_t ctrl = SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_TICKINT_Msk | 5SysTick_CTRL_CLKSOURCE_Msk; 6 7ARG_UNUSED(device); 8 9/* 10* Determine the reload value to achieve the configured tick rate. 11*/ 12 13/* systick supports 24-bit H/W counter */ 14__ASSERT(sys_clock_hw_cycles_per_tick <= (1 << 24), 15"sys_clock_hw_cycles_per_tick too large"); 16sysTickReloadSet(sys_clock_hw_cycles_per_tick - 1); 17 18NVIC_SetPriority(SysTick_IRQn, _IRQ_PRIO_OFFSET); 19 20SysTick->CTRL = ctrl; 21 22SysTick->VAL = 0; /* triggers immediate reload of count */ 23 24return 0; 25}
系統時鐘不一定要使用systick,像Nordic的SOC用的是硬體RTC作為系統時鐘的,只是不過systick是一個通用的時鐘。
第16行,引數sys_clock_hw_cycles_per_tick的含義是多少個systick時鐘計數產生一箇中斷,這裡CPU時鐘為72MHz(systick時鐘源來自CPU),系統時鐘中斷週期為10ms(100Hz,1秒產生100箇中斷),所以sys_clock_hw_cycles_per_tick = 72000000 / 100 = 720000。sysTickReloadSet()函式定義在zephyr-zephyr-v1.13.0\drivers\timer\cortex_m_systick.c:
1static ALWAYS_INLINE void sysTickReloadSet( 2u32_t count /* count from which timer is to count down */ 3) 4{ 5/* 6* Write the reload value and clear the current value in preparation 7* for enabling the timer. 8* The countflag in the control/status register is also cleared by 9* this operation. 10*/ 11SysTick->LOAD = count; 12SysTick->VAL = 0; /* also clears the countflag */ 13 }
第11行,設定重灌載暫存器。
第12行,將計數值置0,在使能systick後就會馬上觸發中斷。
回到_sys_clock_driver_init()函式,第18行,設定systick的中斷優先順序,這裡_IRQ_PRIO_OFFSET的值為1,因此systick的中斷優先順序就為1。
第20行,使能systick。
第22行,馬上觸發systick中斷,並自動重灌計數值。
接下來看systick中斷執行函式_timer_int_handler(),定義在zephyr-zephyr-v1.13.0\drivers\timer\cortex_m_systick.c:
1void _timer_int_handler(void *unused) 2{ 3ARG_UNUSED(unused); 4 5sys_trace_isr_enter(); 6 7/* accumulate total counter value */ 8clock_accumulated_count += sys_clock_hw_cycles_per_tick; 9 10/* 11* one more tick has occurred -- don't need to do anything special since 12* timer is already configured to interrupt on the following tick 13*/ 14_sys_clock_tick_announce(); 15 16extern void _ExcExit(void); 17_ExcExit(); 18 }
第8行,累加系統啟動後經歷了多少個時鐘計數,注意這裡不是累加系統ticks的個數,因為累加時鐘計數會更加精確。
第14行,呼叫_sys_clock_tick_announce()函式,定義在zephyr-zephyr-v1.13.0\include\drivers\ system_timer.h:
#define _sys_clock_tick_announce() \ _nano_sys_clock_tick_announce(_sys_idle_elapsed_ticks)
在沒有使能TICKLESS_KERNEL配置的情況下引數_sys_idle_elapsed_ticks的值為1,實際上呼叫的是_nano_sys_clock_tick_announce()函式,定義在zephyr-zephyr-v1.13.0\kernel\ sys_clock.c:
1 void _nano_sys_clock_tick_announce(s32_t ticks) 2 { 3unsigned intkey; 4 5K_DEBUG("ticks: %d\n", ticks); 6 7/* 64-bit value, ensure atomic access with irq lock */ 8key = irq_lock(); 9_sys_clock_tick_count += ticks; 10irq_unlock(key); 11 12handle_timeouts(ticks); 13 14/* time slicing is basically handled like just yet another timeout */ 15handle_time_slicing(ticks); 16}
第9行,累加系統啟動後所經歷的ticks個數。
在分析第12行的handle_timeouts()函式之前,先說一下執行緒加入到超時佇列的過程。執行緒通過呼叫k_sleep()等函式後,系統會將該執行緒加入到超時佇列裡,然後排程其他執行緒。k_sleep()對應的實現函式為_impl_k_sleep(),定義在zephyr-zephyr-v1.13.0\kernel\ sched.c:
1 void _impl_k_sleep(s32_t duration) 2 { 3/* volatile to guarantee that irq_lock() is executed after ticks is 4* populated 5*/ 6volatile s32_t ticks; 7unsigned int key; 8 9__ASSERT(!_is_in_isr(), ""); 10__ASSERT(duration != K_FOREVER, ""); 11 12K_DEBUG("thread %p for %d ns\n", _current, duration); 13 14/* wait of 0 ms is treated as a 'yield' */ 15if (duration == 0) { 16k_yield(); 17return; 18} 19 20ticks = _TICK_ALIGN + _ms_to_ticks(duration); 21key = irq_lock(); 22 23_remove_thread_from_ready_q(_current); 24_add_thread_timeout(_current, NULL, ticks); 25 26_Swap(key); 27}
第15行,如果傳進來的時引數為0,則直接呼叫k_yield()函式,切換到其他執行緒,具體實現的話在下一篇隨筆裡再分析。
第20行,_TICK_ALIGN的值為1,即將睡眠時間以tick為單位補齊。
第23行,呼叫_remove_thread_from_ready_q()函式,定義在zephyr-zephyr-v1.13.0\kernel\ sched.c:
1void _remove_thread_from_ready_q(struct k_thread *thread) 2{ 3LOCKED(&sched_lock) { 4if (_is_thread_queued(thread)) { 5_priq_run_remove(&_kernel.ready_q.runq, thread); 6_mark_thread_as_not_queued(thread); 7update_cache(thread == _current); 8} 9} 10 }
第4行,執行緒能夠執行,那它的狀態必須是已經_THREAD_QUEUED了的。
第5行,將執行緒從執行佇列移除,那麼執行緒就不會參與執行緒排程了。
第6行,設定執行緒狀態不為_THREAD_QUEUED。
第7行,呼叫update_cache()函式,在上一篇隨筆已經分析過了,這裡不再重複。
回到_impl_k_sleep()函式,第24行,呼叫_add_thread_timeout()函式,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:
1 static inline void _add_thread_timeout(struct k_thread *thread, 2_wait_q_t *wait_q, 3s32_t timeout_in_ticks) 4 { 5_add_timeout(thread, &thread->base.timeout, wait_q, timeout_in_ticks); 6 }
實際上呼叫的是_add_timeout()函式,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:
1 static inline void _add_timeout(struct k_thread *thread, 2struct _timeout *timeout, 3_wait_q_t *wait_q, 4s32_t timeout_in_ticks) 5 { 6__ASSERT(timeout_in_ticks >= 0, ""); 7 8timeout->delta_ticks_from_prev = timeout_in_ticks; 9timeout->thread = thread; 10timeout->wait_q = (sys_dlist_t *)wait_q; 11 12K_DEBUG("before adding timeout %p\n", timeout); 13 14/* If timer is submitted to expire ASAP with 15* timeout_in_ticks (duration) as zero value, 16* then handle timeout immedately without going 17* through timeout queue. 18*/ 19if (!timeout_in_ticks) { 20_handle_one_expired_timeout(timeout); 21return; 22} 23 24s32_t *delta = &timeout->delta_ticks_from_prev; 25struct _timeout *in_q; 26 27SYS_DLIST_FOR_EACH_CONTAINER(&_timeout_q, in_q, node) { 28if (*delta <= in_q->delta_ticks_from_prev) { 29in_q->delta_ticks_from_prev -= *delta; 30sys_dlist_insert_before(&_timeout_q, &in_q->node, 31&timeout->node); 32goto inserted; 33} 34 35*delta -= in_q->delta_ticks_from_prev; 36} 37 38sys_dlist_append(&_timeout_q, &timeout->node); 39 40inserted: 41K_DEBUG("after adding timeout %p\n", timeout); 42}
第19行,很明顯timeout_in_ticks的值不為0。
第27~38行, 按delta_ticks_from_prev的值由小到大插入到_timeout_q超時佇列裡。由此可知,超時佇列裡存放的是與前一個執行緒的時間的差值,而不是絕對值。
回到_impl_k_sleep()函式,第26行,呼叫_Swap()函式,把執行緒切換出去,這在下一篇隨筆再分析。
好了,有了這些基礎之後,現在回到_nano_sys_clock_tick_announce()函式,第12行,呼叫handle_timeouts()函式,定義在zephyr-zephyr-v1.13.0\kernel\ sys_clock.c:
1static inline void handle_timeouts(s32_t ticks) 2{ 3sys_dlist_t expired; 4unsigned int key; 5 6/* init before locking interrupts */ 7sys_dlist_init(&expired); 8 9key = irq_lock(); 10 11sys_dnode_t *next = sys_dlist_peek_head(&_timeout_q); 12struct _timeout *timeout = (struct _timeout *)next; 13 14K_DEBUG("head: %p, delta: %d\n", 15timeout, timeout ? timeout->delta_ticks_from_prev : -2112); 16 17if (!next) { 18irq_unlock(key); 19return; 20} 21 22/* 23* Dequeue all expired timeouts from _timeout_q, relieving irq lock 24* pressure between each of them, allowing handling of higher priority 25* interrupts. We know that no new timeout will be prepended in front 26* of a timeout which delta is 0, since timeouts of 0 ticks are 27* prohibited. 28*/ 29 30while (next) { 31 32/* 33* In the case where ticks number is greater than the first 34* timeout delta of the list, the lag produced by this initial 35* difference must also be applied to others timeouts in list 36* until it was entirely consumed. 37*/ 38 39s32_t tmp = timeout->delta_ticks_from_prev; 40 41if (timeout->delta_ticks_from_prev < ticks) { 42timeout->delta_ticks_from_prev = 0; 43} else { 44timeout->delta_ticks_from_prev -= ticks; 45} 46 47ticks -= tmp; 48 49next = sys_dlist_peek_next(&_timeout_q, next); 50 51if (timeout->delta_ticks_from_prev == 0) { 52sys_dnode_t *node = &timeout->node; 53 54sys_dlist_remove(node); 55 56/* 57* Reverse the order that that were queued in the 58* timeout_q: timeouts expiring on the same ticks are 59* queued in the reverse order, time-wise, that they are 60* added to shorten the amount of time with interrupts 61* locked while walking the timeout_q. By reversing the 62* order _again_ when building the expired queue, they 63* end up being processed in the same order they were 64* added, time-wise. 65*/ 66 67sys_dlist_prepend(&expired, node); 68 69timeout->delta_ticks_from_prev = _EXPIRED; 70 71} else if (ticks <= 0) { 72break; 73} 74 75irq_unlock(key); 76key = irq_lock(); 77 78timeout = (struct _timeout *)next; 79} 80 81irq_unlock(key); 82 83_handle_expired_timeouts(&expired); 84 }
程式碼有點多,但是原理比較簡單。
第7行,初始化一個超時雙向連結串列,用於後面存放已經超時(到期)的執行緒。
第11行,取出超時佇列的頭節點。
第17行,即如果超時佇列為空(沒有超時任務要處理),則直接返回。
第30行,遍歷超時佇列。
第41行,如果取出的執行緒剩餘的超時時間小於ticks(這裡是1),則說面執行緒到期了,第42行將執行緒的超時時間置為0。否則,第44行,將超時時間減ticks。
第47行,剩下的ticks個數,其值可能為負數。
第49行,取出下一個節點。
第51行,如果當前執行緒的超時時間已經到了,則if條件成立。
第54行,將當前執行緒從超時佇列移除。
第67行,將當前執行緒加入到臨時佇列裡,後面會統一處理這個佇列裡的執行緒。
第69行,將當前執行緒的超時時間置為_EXPIRED。
如此迴圈,直到ticks用完(其值小於等於0),然後跳出迴圈,呼叫83行的_handle_expired_timeouts()函式,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:
1static inline void _handle_expired_timeouts(sys_dlist_t *expired) 2{ 3struct _timeout *timeout; 4 5SYS_DLIST_FOR_EACH_CONTAINER(expired, timeout, node) { 6_handle_one_expired_timeout(timeout); 7} 8}
即遍歷臨時佇列,每次呼叫_handle_one_expired_timeout()函式,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:
1static inline void _handle_one_expired_timeout(struct _timeout *timeout) 2{ 3struct k_thread *thread = timeout->thread; 4unsigned int key = irq_lock(); 5 6timeout->delta_ticks_from_prev = _INACTIVE; 7 8K_DEBUG("timeout %p\n", timeout); 9if (thread) { 10_unpend_thread_timing_out(thread, timeout); 11_mark_thread_as_started(thread); 12_ready_thread(thread); 13irq_unlock(key); 14} else { 15irq_unlock(key); 16if (timeout->func) { 17timeout->func(timeout); 18} 19} 20 }
第6行,將超時時間置為_INACTIVE。
超時的方式有兩種,一是執行緒呼叫k_sleep()等函式後將自己掛起導致的超時,二是執行緒呼叫軟體定時器k_timer_start()函式導致的超時,執行緒本身不會掛起,只是開啟了一個定時器。所以就有了第9行和第14行兩種不同路徑。
先看第一種方式,第10行,呼叫_unpend_thread_timing_out()函式,定義在zephyr-zephyr-v1.13.0\kernel\include\timeout_q.h:
1static inline void _unpend_thread_timing_out(struct k_thread *thread, 2struct _timeout *timeout_obj) 3{ 4if (timeout_obj->wait_q) { 5_unpend_thread_no_timeout(thread); 6thread->base.timeout.wait_q = NULL; 7} 8}
第5行,呼叫_unpend_thread_no_timeout()函式,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:
1void _unpend_thread_no_timeout(struct k_thread *thread) 2{ 3LOCKED(&sched_lock) { 4_priq_wait_remove(&pended_on(thread)->waitq, thread); 5_mark_thread_as_not_pending(thread); 6} 7}
第4行,實際上呼叫的是_priq_dumb_remove()函式,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:
void _priq_dumb_remove(sys_dlist_t *pq, struct k_thread *thread) { __ASSERT_NO_MSG(!_is_idle(thread)); sys_dlist_remove(&thread->base.qnode_dlist); }
將執行緒從佇列移除。
回到_unpend_thread_no_timeout()函式,第5行,將執行緒狀態設定為不是_THREAD_PENDING。
回到_handle_one_expired_timeout()函式,第11~12行這兩個函式在上一篇隨筆裡已經分析過了。第16~17行,如果定時器超時函式不為空,則呼叫定時器超時函式。
至此,handle_timeouts()函式分析完了。
回到_nano_sys_clock_tick_announce()函式,第15行,呼叫handle_time_slicing()函式,定義在zephyr-zephyr-v1.13.0\kernel\sys_clock.c:
1 static void handle_time_slicing(s32_t ticks) 2 { 3if (!_is_thread_time_slicing(_current)) { 4return; 5} 6 7_time_slice_elapsed += ticks; 8if (_time_slice_elapsed >= _time_slice_duration) { 9 10unsigned int key; 11 12_time_slice_elapsed = 0; 13 14key = irq_lock(); 15_move_thread_to_end_of_prio_q(_current); 16irq_unlock(key); 17} 18}
第3行,呼叫_is_thread_time_slicing()函式,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:
1int _is_thread_time_slicing(struct k_thread *thread) 2{ 3int ret = 0; 4 5/* Should fix API.Doesn't make sense for non-running threads 6* to call this 7*/ 8__ASSERT_NO_MSG(thread == _current); 9 10if (_time_slice_duration <= 0 || !_is_preempt(thread) || 11_is_prio_higher(thread->base.prio, _time_slice_prio_ceiling)) { 12return 0; 13} 14 15 16LOCKED(&sched_lock) { 17struct k_thread *next = _priq_run_best(&_kernel.ready_q.runq); 18 19if (next) { 20ret = thread->base.prio == next->base.prio; 21} 22} 23 24return ret; 25 }
第10~13行,_time_slice_duration的值在系統啟動時就設定了。_is_preempt()函式:
static inline int _is_preempt(struct k_thread *thread) { /* explanation in kernel_struct.h */ return thread->base.preempt <= _PREEMPT_THRESHOLD; }
_PREEMPT_THRESHOLD的值為127。即如果執行緒的優先順序小於128則_is_preempt()返回1。
_is_prio_higher()比較當前執行緒的優先順序是否高於_time_slice_prio_ceiling的值(也是在系統啟動時就設定了),如果這三個條件有一個成立了,則不會處理時間片相關的內容。
第17行,呼叫_priq_run_best()函式取出執行佇列的頭節點,即優先順序最高的執行緒。只有執行佇列的頭節點的優先順序與當前執行緒的優先順序相等才會繼續往下處理。
回到handle_time_slicing()函式,第7行,累加ticks個數。
第8行,如果累加的ticks個數大於等於配置的時間片數,則if條件成立。
第12行,將累加的ticks個數清0。
第15行,呼叫_move_thread_to_end_of_prio_q()函式,定義在zephyr-zephyr-v1.13.0\kernel\sched.c:
1void _move_thread_to_end_of_prio_q(struct k_thread *thread) 2{ 3LOCKED(&sched_lock) { 4_priq_run_remove(&_kernel.ready_q.runq, thread); 5_priq_run_add(&_kernel.ready_q.runq, thread); 6_mark_thread_as_queued(thread); 7update_cache(0); 8} 9}
第4~7行,這幾個函式前面都已經分析過了。
到這裡就可以知道,要使用時間片輪轉的排程方式,需要以下設定:
1.配置時間片大小(大於0)和優先順序;
2.所有建立的執行緒的優先順序要相同,並且優先順序要比1中的優先順序高;
仔細思考會發現目前這種超時處理機制對延時(休眠)的時間是不準確的,因此這種機制總是以tick為單位進行延時(休眠),也即時間只能精確到tick。那有沒有其他方法可以準確延時(休眠)呢?肯定是有的,就是需要開啟TICKLESS_KERNEL配置,其原理就是不以tick(假如10ms)為固定時間進行定時,而是每次根據需要延時(休眠)的最小時間進行定時,這樣就能實現精確的延時(休眠),zephyr是支援這種精確定時方式的,感興趣的可以去研究研究。