C語言回調函數(shù)的概念及其應用
筆者能力有限,如果文中有錯誤的地方,歡迎各位朋友給我及時地指出來,我將不甚感激,謝謝~
概念
引用維基百科上的關于回調函數(shù)的概念:
在計算機程序設計中,回調函數(shù),或簡稱回調(Callback 即call then back 被主函數(shù)調用運算后會返回主函數(shù)),是指通過函數(shù)參數(shù)傳遞到其它代碼的,某一塊可執(zhí)行代碼的引用。這一設計允許了底層代碼調用在高層定義的子程序。
打一個簡單的例子就是說,如果我們在一個 RTOS 的基礎上去編寫應用程序,編寫應用程序的這一層就是應用層,也可以說是高層,那 RTOS 內核所處的就是內核層,也可以說是底層。在編寫應用程序的時候,我們可以函數(shù)調用的形式來在高層調用底層的函數(shù)來實現(xiàn)相關的功能,但是底層的程序在使用過程中,一般是不進行改動的,也就無法通過普通函數(shù)調用的方法去調用在高層定義的函數(shù),而回調函數(shù)則能解決這一問題,使得底層代碼調用在高層定義的子程序,下面通過一個圖簡單說明這個問題:
回調函數(shù)的實現(xiàn)
對于回調函數(shù)一種比較簡單的理解也就是將一個函數(shù)指針以參數(shù)的形式傳遞給另一個函數(shù),在這里不對函數(shù)指針的概念進行展開講解,筆者在《C 語言跳轉表的實現(xiàn)及在嵌入式設備中的應用》中簡單地描述了函數(shù)指針的概念。在大多數(shù)情況下,回調函數(shù)將包括以下三個部分:
定義回調函數(shù)
注冊回調函數(shù)
執(zhí)行回調函數(shù)
下面筆者通過一個簡單的例子將回調函數(shù)的實現(xiàn)與這三部分關聯(lián)起來。
定義回調函數(shù)
回調函數(shù)的定義很簡單,與普通函數(shù)的定義沒有區(qū)別,比如我們定義一個看門狗計時器的回調函數(shù)如下:
/*高層*/
void Watchdog_ExpiredCallback(void)
{
//do something
}
可以看出這就是一個普通的函數(shù)。
注冊回調函數(shù)并執(zhí)行
注冊回調函數(shù)筆者在這里給出兩種實現(xiàn)思路,先是一種比較直觀的:
/*底層*/
void Watchdog_Expired(void (*Callback)(void))
{
Callback();
}
可以看到這個函數(shù)的形參是一個函數(shù)指針,因此我們也就可以將我們定義的函數(shù)的指針作為函數(shù)傳到當前這個函數(shù),從而實現(xiàn)在底層調用高層的代碼。調用方法也有兩種形式,分別是以下兩種:
Watchdog_Expired(Watchdog_ExpiredCallback);
Watchdog_Expired(&Watchdog_ExpiredCallback);
為什么這兩種調用方式結果都一致呢,其實這也就跟數(shù)組的 a 和 &a[0]的關系是一個道理,雖然表征的意義不一致,但是其數(shù)值是相等的。注冊回調函數(shù)的第二種方法在形式上看著要比第一種要復雜一點,我們先采用如下方式定義一個函數(shù)指針:
typedef void (*Callback)(void);
static Callback WatchdogExpired = NULL;
然后就可以這樣實現(xiàn)注冊函數(shù):
void
Watchdog_CallbackRegister(void (*Callback)(void))
{
WatchdogExpired = Callback;
}
然后就可以將我們之前定義的函數(shù)進行注冊:
Watchdog_CallbackRegister(Watchdog_ExpiredCallback);
這里需要注意的是上述的這個函數(shù)應該在系統(tǒng)初始化的時候,就完成調用,然后我們就可以在中斷服務函數(shù)里完成回調函數(shù)的執(zhí)行了:
void watchdog_ISR(void)
{
if (WatchdogExpired != NULL)
{
WatchdogExpired();
}
}
上述便是回調函數(shù)的一個簡單例子,下面筆者將分析回調函數(shù)在 rtthread 上的一個應用。
RT-Thread 空閑線程的鉤子函數(shù)
我們首先來看 RT-Thread 對于空閑線程的介紹:
RT-Thread 空閑線程是系統(tǒng)創(chuàng)建的最低優(yōu)先級的線程,線程狀態(tài)永遠為就緒態(tài)。當系統(tǒng)中無其他就緒線程存在時,調度器將調度到空閑線程,它通常是一個死循環(huán),且永遠不能被掛起。在空閑線程中也提供了接口來運行用戶設置的鉤子函數(shù),在空閑線程運行時會調用該鉤子函數(shù),適合鉤入功耗管理、看門狗喂狗等工作。
在上述介紹中提到空閑線程提供了接口來運行用戶設置的鉤子函數(shù),那這又是基于什么原理呢?首先我們來看空閑函數(shù)是如何設置鉤子函數(shù),代碼如下:
static void (*idle_hook_list[RT_IDEL_HOOK_LIST_SIZE])();
rt_err_t rt_thread_idle_sethook(void (*hook)(void))
{
rt_size_t i;
rt_base_t level;
rt_err_t ret = -RT_EFULL;
/* disable interrupt */
level = rt_hw_interrupt_disable();
for (i = 0; i < RT_IDEL_HOOK_LIST_SIZE; i++)
{
if (idle_hook_list[i] == RT_NULL)
{
idle_hook_list[i] = hook;
ret = RT_EOK;
break;
}
}
/* enable interrupt */
rt_hw_interrupt_enable(level);
return ret;
}
我們可以看到這個函數(shù)的形參是一個函數(shù)指針,自然可以想到是用到了回調函數(shù)的原理。對于此函數(shù)的實現(xiàn),我們可以看到是定義了一個全局的鉤子函數(shù)數(shù)組,也就是說可以注冊多個回調函數(shù),然后會根據(jù)注冊的先后順序進行執(zhí)行。既然可以注冊回調函數(shù)了,那么我們就可以在應用層定義一個回調函數(shù),這里以看門狗喂狗為例,實現(xiàn)代碼如下:
static void idle_hook(void)
{
/*喂狗操作*/
rt_device_control(wdg_dev,RT_DEVICE_CTRL_WDT_KEEPALIVE, NULL);
rt_kprintf("feed the dog!\n");
}
定義了回調函數(shù),我們就可以在主程序里將注冊該回調函數(shù)了:
int main(void)
{
/*省略看門狗設備的相關操作*/
rt_thread_idle_sethook(idle_hook);
}
回調函數(shù)已經注冊,何時會執(zhí)行呢?對于當前系統(tǒng)而言,當當前無其他線程運行時,切換到空閑線程時會運行我們注冊的回調函數(shù),空閑線程里面的內容是這樣的:
static void rt_thread_idle_entry(void *parameter)
{
while (1)
{
#ifdef RT_USING_IDLE_HOOK
rt_size_t i;
for (i = 0; i < RT_IDEL_HOOK_LIST_SIZE; i++)
{
if (idle_hook_list[i] != RT_NULL)
{
idle_hook_list[i]();
}
}
#endif
rt_thread_idle_excute();
#ifdef RT_USING_PM
rt_system_power_manager();
#endif
}
}
上述代碼也印證了剛才所說的,注冊的多個回調函數(shù)會根據(jù)注冊的順序依次執(zhí)行。最后,回顧空閑線程鉤子函數(shù)的運行過程,也就和文章最開始給出的調用關系圖相對應起來了。
總結
在 RT-Thread 中關于回調函數(shù)的例子也不止空閑線程鉤子函數(shù)這一個,還有很多,比如調度器和串口設備里也有,不過原理都是一樣的,最終實現(xiàn)的效果也都是能夠使底層調用高層定義的代碼。
參考資料:
[1] https://www.embedded.com/increasing-code-flexibility-using-callbacks/
[2]https://www.beningo.com/embedded-basics-callback-functions/
您的閱讀是對我最大的鼓勵,您的建議是對我最大地提升,歡迎點擊下方圖片進入小程序進行評論或者添加筆者微信相互交流,二維碼在公眾號底部獲取
免責聲明:本文內容由21ic獲得授權后發(fā)布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!





