閱讀內(nèi)核系列之調(diào)度器為何全局導(dǎo)出
關(guān)注、星標(biāo)嵌入式客棧,干貨及時(shí)送達(dá)
[導(dǎo)讀] Linux內(nèi)核代碼龐大,閱讀內(nèi)核書籍總覺得云山霧繞,紙上得來終覺淺,希望通過閱讀代碼撰寫筆記,嘗試將這美人神秘的面紗掀開一角,管中窺豹,見一點(diǎn)真容。水平所限,錯(cuò)誤難免,懇請(qǐng)交流指正。
前情提要
《閱讀內(nèi)核系列之EXPORT_SYMBOL展開》將EXPORT_SYMBOL(schedule)展開:
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}
EXPORT_SYMBOL(schedule);
全部展開后,得到了什么呢(前文中__EXPORT_SYMBOL(sym, sec) sec弄錯(cuò)了,修正如下)?
extern typeof(schedule) schedule; \
extern __visible void *__crc_schedule __attribute__((weak)); \
static const unsigned long __kcrctab_schedule \
__used \
__attribute__((section("___kcrctab" "" "+" "schedule"), unused)) \
= (unsigned long) &__crc_schedule;
static const char __kstrtab_schedule[] \
__attribute__((section("__ksymtab_strings"), aligned(1))) \
= "_" "schedule"; \
extern const struct kernel_symbol __ksymtab_schedule; \
__visible const struct kernel_symbol __ksymtab_schedule \
__used \
__attribute__((section("___ksymtab" "" "+" "schedule"), unused)) \
= { (unsigned long)&schedule, __kstrtab_schedule };
這樣還是不直觀,去掉不必要的換行符,整理一下:
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}
/*以下部分都屬于EXPORT_SYMBOL(schedule)的展開*/
extern typeof(schedule) schedule;
extern __visible void *__crc_schedule __attribute__((weak));
static const unsigned long __kcrctab_schedule __used \
__attribute__((section("___kcrctab+schedule"), unused)) \
= (unsigned long) &__crc_schedule;
static const char __kstrtab_schedule[] __attribute__((section("__ksymtab_strings"), aligned(1))) = "_" "schedule";
extern const struct kernel_symbol __ksymtab_schedule;
__visible const struct kernel_symbol __ksymtab_schedule __used __attribute__((section("___ksymtab" "" "+" "schedule"), unused)) = {
(unsigned long)&schedule, __kstrtab_schedule
};
gcc相關(guān)知識(shí)點(diǎn)梳理
要理解上述代碼,感覺還是很難,先來梳理一下其中一些關(guān)鍵字,好多沒見過?憋急。
-
asmlinkage,其一:用于指定函數(shù)的參數(shù)都棧中,而不應(yīng)在寄存器中;其二,指定一個(gè)函數(shù)為asmlinkage,則匯編代碼中可以調(diào)用該函數(shù)。參考https://kernelnewbies.org/FAQ/asmlinkage
-
閱讀Linux內(nèi)核代碼,發(fā)現(xiàn)大量的文件名同名,如不理清其內(nèi)在機(jī)理,這很讓人頭腦發(fā)脹。這里以linkage.h為例來探討一下。
看到有博文說asmlinkage其根源如下:
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
但仔細(xì)查看代碼,這僅僅是對(duì)x86體系而言,而比如針對(duì)IA64(英特爾安騰架構(gòu)(Intel Itanium architecture))而言:
#define asmlinkage CPP_ASMLINKAGE __attribute__((syscall_linkage))
所以不同的體系結(jié)構(gòu)為實(shí)現(xiàn)前述目的是有差異的。
-
__attribute__ ,關(guān)鍵字__attribute__用來指定變量,函數(shù)參數(shù)或結(jié)構(gòu),聯(lián)合以及在C ++中的類成員的特殊屬性。__attribute__關(guān)鍵字后跟一個(gè)用雙括號(hào)括起來的屬性規(guī)范。當(dāng)前通常為變量定義一些屬性。為特定目標(biāo)系統(tǒng)上的變量定義了其他屬性。其他屬性可用于函數(shù)(請(qǐng)參見“函數(shù)屬性”),標(biāo)簽(請(qǐng)參見“標(biāo)簽屬性”),枚舉(請(qǐng)參見“枚舉器屬性”),語句(請(qǐng)參見“語句屬性”)和類型(請(qǐng)參見“類型屬性”)。有需要的時(shí)候可以去查閱gcc文檔: https://gcc.gnu.org/onlinedocs/gcc/Attribute-Syntax.html
-
typeof,是gcc的擴(kuò)展關(guān)鍵字。參考gcc 9.3.0手冊(cè) P475:
引用表達(dá)式類型的另一種方法是使用typeof。其語法看起來像sizeof,但是該構(gòu)造在語義上類似于使用typedef定義的類型名稱。有兩種方式將參數(shù)寫入typeof:使用表達(dá)式或類型。
很可能沒見過這種特性,那有啥妙用呢?
#define SWAP(a, b) {\
typeof(a) _t=a;\
a=b;\
b=_t;}寫的這么復(fù)雜干啥呢?其一、用大括號(hào)括起來定義的_b作用域限定了,不會(huì)有重名的問題,其二、利用typeof(a) _t=a,則可以獲取a的類型,具有普適性。所謂普適性,即便對(duì)這個(gè)宏傳入兩個(gè)結(jié)構(gòu)體也是運(yùn)行的。如果不這么做,用函數(shù)實(shí)現(xiàn)需要做到普適性則比較麻煩,如果一定要做肯定也有辦法,比如swap(void *a, void * b,int length),直接交換內(nèi)存。但遠(yuǎn)不如這個(gè)宏來的簡(jiǎn)單。
-
表達(dá)式的示例:typeof(x [0](1)) 假設(shè)x是一個(gè)指向函數(shù)的指針數(shù)組;則上述語句描述的類型是函數(shù)值的類型。
-
類型名示例
typeof (int *)
這里描述的類型是指向int的指針類型。
-
__visible ,這在哪里實(shí)現(xiàn)的呢?這是將gcc的__externally_visible__屬性利用宏轉(zhuǎn)定義了,以增加可讀性。用于聲明全局可見。該宏定義位于:
./include/linux/compiler_attributes.h中
#if __has_attribute(__externally_visible__)
#define __visible __attribute__((__externally_visible__))
#else
#define __visible
#endif
-
__sched,這個(gè)咋一看,也是一頭霧水。找到出處:./include/sched/debug.h
/* 聲明存儲(chǔ)位置在.sched.text中. */
#define __sched __attribute__((__section__(".sched.text")))類似地,還有
#define __init_thread_info __attribute__((__section__(".data..init_thread_info")))
-
weak,若兩個(gè)或兩個(gè)以上全局符號(hào)(函數(shù)或變量名)名字一樣,而其中之一聲明為weak symbol(弱符號(hào)),則這些全局符號(hào)不會(huì)引發(fā)重定義錯(cuò)誤。鏈接器會(huì)忽略弱符號(hào),去使用普通的全局符號(hào)來解析所有對(duì)這些符號(hào)的引用,但當(dāng)普通的全局符號(hào)不可用時(shí),鏈接器會(huì)使用弱符號(hào)。當(dāng)有函數(shù)或變量名可能被用戶覆蓋時(shí),該函數(shù)或變量名可以聲明為一個(gè)弱符號(hào)。當(dāng)weak和alias屬性連用時(shí),還可以聲明弱別名。
-
unused,附加到函數(shù)的此屬性意味著如果該函數(shù)未被使用。GCC不會(huì)對(duì)此功能發(fā)出警告。
-
兩個(gè)以雙引號(hào)的字符串,編譯預(yù)處理時(shí),會(huì)自動(dòng)連接為一個(gè)字符串。
"_" "schedule" 變成 “_schedule”
-
__used, __unused__屬性,在./include/compiler.h定義
#define __used __attribute__((__used__))
該屬性附加在函數(shù)上,表示即使未引用該函數(shù),也必須將該函數(shù)鏈接在目標(biāo)文件中。
再看EXPORT_SYMBOL(schedule)展式
好了,前面的都整明白了,再來看前面的那段代碼:
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
}
/*以下部分都屬于EXPORT_SYMBOL(schedule)的展開*/
/*利用typeof全局聲明schedule函數(shù)*/
extern typeof(schedule) schedule;
/*全局聲明__crc_schedule,并聲明為weak屬性*/
extern __visible void *__crc_schedule __attribute__((weak));
/*局部const定義__crc_schedule,指定存儲(chǔ)位置*/
static const unsigned long __kcrctab_schedule __used \
__attribute__((section("___kcrctab + schedule"), unused)) \
= (unsigned long) &__crc_schedule;
static const char __kstrtab_schedule[] __attribute__((section("__ksymtab_strings"), aligned(1))) = "_schedule";
extern const struct kernel_symbol __ksymtab_schedule;
/*將schedule 及字符串屬性利用kernel_symbol封裝對(duì)外可見*/
__visible const struct kernel_symbol __ksymtab_schedule __used __attribute__((section("___ksymtab + schedule"), unused)) = {
(unsigned long)&schedule, __kstrtab_schedule
};
kernel_symbol 位于./include/linux/export.h 中:
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#include <linux/compiler.h>
/*
*將ksymtab條目作為一對(duì)相對(duì)引用鏈接:
*在64位體系結(jié)構(gòu)上,這將大小減小了一半,
*并且消除了需要在可重定位內(nèi)核上進(jìn)行運(yùn)
*行時(shí)處理的絕對(duì)重定位的需求。
*/
#define __KSYMTAB_ENTRY_NS(sym, sec) \
__ADDRESSABLE(sym) \
asm(" .section \"___ksymtab" sec "+" #sym "\", \"a\" \n" \
" .balign 4 \n" \
"__ksymtab_" #sym ": \n" \
" .long " #sym "- . \n" \
" .long __kstrtab_" #sym "- . \n" \
" .long __kstrtabns_" #sym "- . \n" \
" .previous \n")
#define __KSYMTAB_ENTRY(sym, sec) \
__ADDRESSABLE(sym) \
asm(" .section \"___ksymtab" sec "+" #sym "\", \"a\" \n" \
" .balign 4 \n" \
"__ksymtab_" #sym ": \n" \
" .long " #sym "- . \n" \
" .long __kstrtab_" #sym "- . \n" \
" .long 0 \n" \
" .previous \n")
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
#else
#define __KSYMTAB_ENTRY_NS(sym, sec) \
static const struct kernel_symbol __ksymtab_##sym \
__attribute__((section("___ksymtab" sec "+" #sym), used)) \
__aligned(sizeof(void *)) \
= { (unsigned long)&sym, __kstrtab_##sym, __kstrtabns_##sym }
#define __KSYMTAB_ENTRY(sym, sec) \
static const struct kernel_symbol __ksymtab_##sym \
__attribute__((section("___ksymtab" sec "+" #sym), used)) \
__aligned(sizeof(void *)) \
= { (unsigned long)&sym, __kstrtab_##sym, NULL }
struct kernel_symbol {
unsigned long value;
const char *name;
const char *namespace;
};
#endif
為何將主調(diào)度器全局導(dǎo)出
至此,調(diào)度對(duì)外導(dǎo)出就基本明晰了,但是進(jìn)一步引申思考?為什么還要將調(diào)度器schedule以模塊形式對(duì)外導(dǎo)出呢?EXPORT_SYMBOL對(duì)外導(dǎo)出,那么導(dǎo)出的作用域究竟多大呢,所包住的函數(shù)在內(nèi)核代碼中全局可見,也就意味著其他的內(nèi)核模塊可以使用該函數(shù)。但是貌似還是沒有回答說為啥要將調(diào)度器對(duì)外導(dǎo)出,潛意識(shí)我們會(huì)認(rèn)為調(diào)度器直接在后臺(tái)像個(gè)勤勞的大管家,在哪里不停的忙活就完了,難不成其他模塊還要主動(dòng)去調(diào)用調(diào)度器不成。為了驗(yàn)證猜想,搜一下吧:
看來猜想沒錯(cuò),事實(shí)上:schedule就是主調(diào)度器的函數(shù), 在內(nèi)核中的許多地方, 如果要將CPU分配給與當(dāng)前活動(dòng)進(jìn)程不同的另一個(gè)進(jìn)程, 都會(huì)直接主動(dòng)調(diào)用主調(diào)度器函數(shù)schedule.該函數(shù)完成如下工作:
-
確定當(dāng)前就緒隊(duì)列, 并在保存一個(gè)指向當(dāng)前(仍然)活動(dòng)進(jìn)程的task_struct指針; -
檢查死鎖, 關(guān)閉內(nèi)核搶占后調(diào)用__schedule完成內(nèi)核調(diào)度; -
恢復(fù)內(nèi)核搶占, 然后檢查當(dāng)前進(jìn)程是否設(shè)置了重調(diào)度標(biāo)志TLF_NEDD_RESCHED, 如果該進(jìn)程被其他進(jìn)程設(shè)置了TIF_NEED_RESCHED標(biāo)志, 則函數(shù)重新執(zhí)行進(jìn)行調(diào)度。
asmlinkage __visible void __sched schedule(void)
{
/* 獲取當(dāng)前的進(jìn)程 */
struct task_struct *tsk = current;
/* 避免死鎖 */
sched_submit_work(tsk);
do {
preempt_disable(); /* 關(guān)閉內(nèi)核搶占 */
__schedule(false); /* 完成調(diào)度 */
sched_preempt_enable_no_resched(); /* 開啟內(nèi)核搶占 */
} while (need_resched());
/* 如果該進(jìn)程被其他進(jìn)程設(shè)置了TIF_NEED_RESCHED標(biāo)志,則函數(shù)重新執(zhí)行進(jìn)行調(diào)度 */
}
EXPORT_SYMBOL(schedule);
以./drivers/s390/crypto/ap_bus.c 的函數(shù)ap_poll_thread為例:
/*ap_poll_thread():輪詢完成的請(qǐng)求的線程。AP總線輪詢線程
*該線程的目的是在循環(huán)中輪詢存在的請(qǐng)求,如果有一個(gè)“空閑”的cpu,
*就不需要做什么。 只要有其他任務(wù)或所有消息都已傳遞,輪詢就會(huì)停止。*/
static int ap_poll_thread(void *data)
{
DECLARE_WAITQUEUE(wait, current);
set_user_nice(current, MAX_NICE);
set_freezable();
while (!kthread_should_stop()) {
add_wait_queue(&ap_poll_wait, &wait);
set_current_state(TASK_INTERRUPTIBLE);
if (ap_suspend_flag || !ap_pending_requests()) {
schedule();
try_to_freeze();
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&ap_poll_wait, &wait);
if (need_resched()) {
/*主動(dòng)調(diào)用調(diào)度器*/
schedule();
try_to_freeze();
continue;
}
ap_tasklet_fn(0);
}
return 0;
}
關(guān)于內(nèi)核調(diào)度器究竟如何工作,還沒開始讀,如有興趣,請(qǐng)繼續(xù)關(guān)注。
—END—
如果喜歡右下點(diǎn)個(gè)在看,也會(huì)讓我倍感鼓舞
關(guān)注置頂:掃描左下二維碼關(guān)注公眾號(hào)加星
關(guān)注 |
加群 |
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!