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