雙核系統調用(ipipe)
解析系統調用是了解內核架構最有力的一把鑰匙。
在Linux內核基礎上加入xenomai實時系統內核后,在內核空間兩個內核共存,實時任務需要xenomai內核來完成實時的服務,如果實時任務需要用到linux的服務,還可以調用linux內核的系統調用,你可能會好奇xenomai與linux兩個內核共存后系統調用是如何實現的?

為什么需要系統調用?現代操作系統中,處理器的運行模式一般分為兩個空間:內核空間和用戶空間,大部分應用程序運行在用戶空間,而操作系統內核和設備驅動程序運行在內核空間,如果應用程序需要訪問硬件資源或者需要內核提供服務,該怎么辦?
為了向用戶空間上運行的應用程序提供服務,內核提供了一組接口。透過該接口,應用程序可以訪問硬件設備和其他操作系統資源。這組接口在應用程序和內核之間扮演了使者的角色,應用程序發送各種請求,而內核負責滿足這些請求,這些接口就是系統調用,它是用戶空間和內核空間一個中間層。
系統調用層主要作用有三個:
-
它為用戶空間提供了一種統一的硬件的抽象接口。比如當需要讀些文件的時候,應用程序就可以不去管磁盤類型和介質,甚至不用去管文件所在的文件系統到底是哪種類型。
-
系統調用保證了系統的穩定和安全。應用程序要訪問內核就必須通過系統調用層,內核可以在系統調用層對應用程序的訪問權限、用戶類型和其他一些規則進行過濾,這避免了應用不正確地訪問內核,保證了系統和各個應用程序的安全性。
-
可移植性。可以讓應用程序在不修改源代碼的情況下,在不同的操作系統或擁有不同硬件架構的系統中重新編譯運行。
回到本文開頭的問題,該問題細分為如下兩個問題:
-
雙核共存時,如何區分應用發起的系統調用是xenomai內核調用還是linux內核調用?
-
一個xenomai實時任務既可以調用xenomai內核服務,也可以調用linux內核服務,這是如何做到的?
本文通過分析源代碼為你解答問題1,對于問題2,涉及雙核間的調度,本文暫不涉及,后面的文章揭曉答案。
一、32位Linux系統調用
我們先來看沒有ipipe和xenomai內核時的linux系統調用流程是怎樣的。linux操作系統的API通常以C標準庫的方式提供,比如linux中的libc庫。C標準庫中提供了POSIX的絕大部分API實現,glibc為了提高應用程序的性能,還對一些系統調用進行了封裝。此外,由于32位系統系統調用使用軟中斷 int0x80指令實現,應用程序也可以通過匯編直接進行系統調用。軟中斷屬于異常的一種,通過執行該指令陷入(trap)內核,trap在整理的文檔 x86Linux中斷系統有說明。內核初始化過程中,通過函數 tarp_init()設置IDT(Interrupt Descriptor Table 記錄每個中斷異常處理程序的地址的一張表),有關 int0x80的IDT表項如下:
static const __initconst struct idt_data def_idts[] = {......SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),......};
當產生系統調用時,硬件根據向量號在 IDT 中找到對應的表項,即中斷描述符,進行特權級檢查,發現 DPL = CPL = 3 ,允許調用。然后硬件將切換到內核棧 (tss.ss0 : tss.esp0)。接著根據中斷描述符的 segment selector 在 GDT / LDT 中找到對應的段描述符,從段描述符拿到段的基址,加載到 cs 。將 offset 加載到 eip。最后硬件將 ss / sp / eflags / cs / ip / error code 依次壓到內核棧。于是開始執行 entry_INT80_32函數,該函數在 entry_32.S定義:
ENTRY(entry_INT80_32)ASM_CLACpushl %eax /* pt_regs->orig_ax */SAVE_ALL pt_regs_ax=$-ENOSYS /* *存儲當前用戶態寄存器,保存在pt_regs結構里*//** User mode is traced as though IRQs are on, and the interrupt gate* turned them off.*/TRACE_IRQS_OFFmovl %esp, %eaxcall do_int80_syscall_32.Lsyscall_32_done:........Lirq_return:INTERRUPT_RETURN/*iret 指令將原來用戶態保存的現場恢復回來,包含代碼段、指令指針寄存器等。這時候用戶態進程恢復執行。*/
在內核棧的最高地址端,存放的是結構 ptregs,首先通過 push 和 SAVEALL 將當前用戶態的寄存器,保存在棧中 ptregs 結構里面.保存完畢后,關閉中斷,將當前棧指針保存到 eax,即doint80syscall32的參數1。調用doint80syscall32=>dosyscall32irqs_on。先看看沒有ipipe時Linux實現如下:
__always_inline void do_syscall_32_irqs_on(struct pt_regs *regs){struct thread_info *ti = pt_regs_to_thread_info(regs);unsigned int nr = (unsigned int)regs->orig_ax;.....if (likely(nr < IA32_NR_syscalls)) {nr = array_index_nospec(nr, IA32_NR_syscalls);regs->ax = ia32_sys_call_table[nr]( /*根據系統調用號索引直接執行*/(unsigned int)regs->bx, (unsigned int)regs->cx,(unsigned int)regs->dx, (unsigned int)regs->si,(unsigned int)regs->di, (unsigned int)regs->bp);}syscall_return_slowpath(regs);}
在這里,將系統調用號從pt_reges中eax 里面取出來,然后根據系統調用號,在系統調用表中找到相應的函數進行調用,并將寄存器中保存的參數取出來,作為函數參數。如果仔細比對,就能發現,這些參數所對應的寄存器,和 Linux 的注釋是一樣的。ia32_sys_call_table系統調用表生成后面解析(此圖來源于網絡)。

相關內核調用執行完后,一直返回到 dosyscall32irqson ,如果系統調用有返回值,會被保存到 regs->ax 中。接著返回 entryINT8032 繼續執行,最后執行 INTERRUPTRETURN 。INTERRUPTRETURN 在 arch/x86/include/asm/irqflags.h 中定義為 iret ,iret 指令將原來用戶態保存的現場恢復回來,包含代碼段、指令指針寄存器等。這時候用戶態進程恢復執行。
系統調用執行完畢。
二、32位實時系統調用
xenomai+linux雙內核架構下,通過I-pipe 攔截系統調用,并將系統調用定向到實現它們的系統。
實時系統調用,除了直接通過匯編系統調用外,xenomai還實現了libcoblat實時庫,相當于glibc,通過libcoblat進行xenomai系統調用,以libcoblat庫函數sem_open為例,libcolat庫中C函數實現如下:
COBALT_IMPL(sem_t *, sem_open, (const char *name, int oflags, ...)){......err = XENOMAI_SYSCALL5(sc_cobalt_sem_open,&rsem, name, oflags, mode, value);if (err == 0) {if (rsem != sem)free(sem);return &rsem->native_sem;}.......return SEM_FAILED;}
libcolat庫調用系統調用使用宏 XENOMAI_SYSCALL5,XENOAI_SYSCALL宏在 includeasmxenomaisyscall.h中聲明, XENOMAI_SYSCALL5中的'5'代表'該系統調用有五個參數:
({unsigned __resultvar;asm volatile (LOADARGS_##nrDOSYSCALLRESTOREARGS_##nr: (__resultvar): (__xn_syscode(op)) ASMFMT_##nr(args): , );(int) __resultvar;})
每個宏中,內嵌另一個宏DOSYSCALL,即實現系統調用的int指令:int$0x80。
defineDOSYSCALL"int$0x80
"
系統調用過程硬件處理及中斷入口上節一致,從 do_syscall_32_irqs_on開始不同,有ipipe后變成下面這樣子:
static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs){struct thread_info *ti = current_thread_info();unsigned int nr = (unsigned int)regs->orig_ax;/*取出系統調用號*/int ret;ret = pipeline_syscall(ti, nr, regs);/*pipeline 攔截系統調用*/......done:syscall_return_slowpath(regs);}
套路和ipipe接管中斷類似,在關鍵路徑上攔截系統調用,然后調用 ipipe_handle_syscall(ti,nr,regs)讓ipipe來接管處理:
int ipipe_handle_syscall(struct thread_info *ti,unsigned long nr, struct pt_regs *regs){unsigned long local_flags = READ_ONCE(ti->ipipe_flags);int ret;if (nr >= NR_syscalls && (local_flags & _TIP_HEAD)) {/*運行在head域且者系統調用號超過linux*/ipipe_fastcall_hook(regs); /*快速系統調用路徑*/local_flags = READ_ONCE(ti->ipipe_flags);if (local_flags & _TIP_HEAD) {if (local_flags & _TIP_MAYDAY)__ipipe_call_mayday(regs);return 1; /* don't pass down, no tail work. */} else {sync_root_irqs();return -1; /* don't pass down, do tail work. */}}if ((local_flags & _TIP_NOTIFY) || nr >= NR_syscalls) {ret =__ipipe_notify_syscall(regs);local_flags = READ_ONCE(ti->ipipe_flags);if (local_flags & _TIP_HEAD)return 1; /* don't pass down, no tail work. */if (ret)return -1; /* don't pass down, do tail work. */}return 0; /* pass syscall down to the host. */}
這個函數的處理邏輯是這樣,怎樣區分xenomai系統調用和linux系統調用?每個CPU架構不同linux系統調用總數不同,在x86系統中有300多個,用變量 NR_syscalls表示,系統調用號與系統調用一一對應。首先獲取到的系統調用號 nr>=NR_syscalls,不用多想,那這個系統調用是xenomai內核的系統調用。另外還有個問題,如果是Linux非實時任務觸發的xenomai系統調用,或者xenomai 實時任務要調用linux的服務,這些交叉服務涉及實時任務與非實時任務在兩個內核之間運行,優先級怎么處理等問題。這些涉及 cobalt_sysmodes[].
首先看怎么區分一個任務是realtime還是norealtime。在 task_struct結構的頭有一個成員結構體 thread_info,存儲著當前線程的信息,ipipe在結構體 thread_info中增加了兩個成員變量 ipipe_flags和 ipipe_data, ipipe_flags用來來標示一個線程是實時還是非實時,TIPHEAD置位表示已經是實時上下文。對于需要切換到xenomai上下文的系統調用TIP_NOTIFY置位。
struct thread_info {unsigned long flags; /* low level flags */u32 status; /* thread synchronous flags */unsigned long ipipe_flags;struct ipipe_threadinfo ipipe_data;};
ipipe_handle_syscall處理邏輯:1.對于已經在實時上下文的實時任務發起xenomai的系統調用,使用快速調用路徑函數 ipipe_fastcall_hook(regs);2.需要切換到實時上下文或者非實時調用實時的,使用慢速調用路徑:
_ipipenotifysyscall(regs)->ipipesyscallhook(callerdomain, regs)
快速調用 ipipe_fastcall_hook(regs)內直接 handle_head_syscall執行代碼如下:
static int handle_head_syscall(struct ipipe_domain *ipd, struct pt_regs *regs){....code = __xn_syscall(regs);nr = code & (__NR_COBALT_SYSCALLS - 1);......handler = cobalt_syscalls[code];sysflags = cobalt_sysmodes[nr];........ret = handler(__xn_reg_arglist(regs));.......ret);.......}
這個函數很復雜,涉及xenomai與linux之間很多聯系,代碼是簡化后的,先取出系統調用號,然后從 cobalt_syscalls取出系統調用入口handler,然后執行 handler(__xn_reg_arglist(regs))執行完成后將執行結果放到寄存器 ax,后面的文章會詳細分析ipipe如何處理系統調用。
三、 64位系統調用
我們再來看 64 位的情況,系統調用,不是用中斷了,而是改用 syscall 指令。并且傳遞參數的寄存器也變了。
({unsigned long __resultvar;LOAD_ARGS_LOAD_REGS_asm volatile ("syscall ": "=a" (__resultvar): "0" (name) ASM_ARGS_: "memory", "cc", "r11", "cx");(int) __resultvar;})DO_SYSCALL(__xn_syscode(op), nr, args)XENOMAI_DO_SYSCALL(1,sc_cobalt_bind,breq)
這里將系統調用號使用 __xn_syscode(op)處理了一下,把最高位置1,表示Cobalt系統調用,然后使用syscall 指令。
syscall 指令還使用了一種特殊的寄存器,我們叫特殊模塊寄存器(Model Specific Registers,簡稱 MSR)。這種寄存器是 CPU 為了完成某些特殊控制功能為目的的寄存器,其中就有系統調用。在系統初始化的時候,trapinit 除了初始化上面的中斷模式,這里面還會調用 cpuinit->syscall_init。這里面有這樣的代碼:
wrmsrl(MSR_LSTAR,(unsignedlong)entry_SYSCALL_64);
rdmsr 和 wrmsr 是用來讀寫特殊模塊寄存器的。MSRLSTAR 就是這樣一個特殊的寄存器, 當 syscall 指令調用的時候,會從這個寄存器里面拿出函數地址來調用,也就是調entrySYSCALL64。該函數在'entry64.S'定義:
ENTRY(entry_SYSCALL_64)UNWIND_HINT_EMPTY......swapgs/** This path is only taken when PAGE_TABLE_ISOLATION is disabled so it* is not required to switch CR3.*/movq %rsp, PER_CPU_VAR(rsp_scratch)movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp/* Construct struct pt_regs on stack */pushq $__USER_DS /* pt_regs->ss */pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */pushq %r11 /* pt_regs->flags */pushq $__USER_CS /* pt_regs->cs */pushq %rcx /* pt_regs->ip *//*保存用戶太指令指針寄存器*/GLOBAL(entry_SYSCALL_64_after_hwframe)pushq %rax /* pt_regs->orig_ax */PUSH_AND_CLEAR_REGS rax=$-ENOSYSTRACE_IRQS_OFF/* IRQs are off. */movq %rsp, %rdicall do_syscall_64 /* returns with IRQs disabled */TRACE_IRQS_IRETQ /* we're about to change IF *//** Try to use SYSRET instead of IRET if we're returning to* a completely clean 64-bit userspace context. If we're not,* go to the slow exit path.*/movq RCX(%rsp), %rcxmovq RIP(%rsp), %r11cmpq %rcx, %r11 /* SYSRET requires RCX == RIP */jne swapgs_restore_regs_and_return_to_usermode.......testq $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11jnz swapgs_restore_regs_and_return_to_usermode/* nothing to check for RSP */cmpq $__USER_DS, SS(%rsp) /* SS must match SYSRET */jne swapgs_restore_regs_and_return_to_usermode/** We win! This label is here just for ease of understanding* perf profiles. Nothing jumps here.*/syscall_return_via_sysret:/* rcx and r11 are already restored (see code above) */UNWIND_HINT_EMPTYPOP_REGS pop_rdi=0 skip_r11rcx=1/** Now all regs are restored except RSP and RDI.* Save old stack pointer and switch to trampoline stack.*/movq %rsp, %rdimovq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsppushq RSP-RDI(%rdi) /* RSP */pushq (%rdi) /* RDI *//** We are on the trampoline stack. All regs except RDI are live.* We can do future final exit work right here.*/SWITCH_TO_USER_CR3_STACK scratch_reg=%rdipopq %rdipopq %rspUSERGS_SYSRET64END(entry_SYSCALL_64)
這里先保存了很多寄存器到 pt_regs 結構里面,例如用戶態的代碼段、數據段、保存參數的寄存器.

然后調用 entry_SYSCALL64_slow_pat->do_syscall_64。
__visible void do_syscall_64(struct pt_regs *regs){struct thread_info *ti = current_thread_info();unsigned long nr = regs->orig_ax; /*取出系統調用號*/int ret;enter_from_user_mode();enable_local_irqs();ret = ipipe_handle_syscall(ti, nr & __SYSCALL_MASK, regs);if (ret > 0) {disable_local_irqs();return;}if (ret < 0)goto done;......if (likely((nr & __SYSCALL_MASK) < NR_syscalls)) {nr = array_index_nospec(nr & __SYSCALL_MASK, NR_syscalls);regs->ax = sys_call_table[nr](regs->di, regs->si, regs->dx,regs->r10, regs->r8, regs->r9);}done:syscall_return_slowpath(regs);}
與32位一樣,ipipe攔截了系統調用,后面的處理流程類似所以,無論是 32 位,還是 64 位,都會到linux系統調用表 sys_call_table和xenomai系統調用表 cobalt_syscalls[]這里來。
四、 實時系統調用表cobalt_syscalls
xenomai每個系統的系統系統調用號在 cobaltuapisyscall.h中:
......
bind()函數在內核代碼中對應的聲明和實現為:
/*聲明*/long CoBaLt_static COBALT_SYSCALL_DECL(bind, lostage,(struct cobalt_bindreq __user *u_breq));/*實現*/#define COBALT_SYSCALL(__name, __mode, __args)long CoBaLt_ ## __name __argsstatic COBALT_SYSCALL(bind, lostage,(structcobalt_bindreq__user*u_breq)){......}
其中 __name表示系統調用名對應bind、 __mode表示該系統調用模式對應lostage。 COBALT_SYSCALL展開定義的bind函數后如下:
longCoBaLt_bind(structcobalt_bindreq__user*u_breq){......}
怎么將 CoBaLt_bind與系統調用號 sc_cobalt_bind聯系起來后放入 cobalt_syscalls[]的呢?在編譯過程中Makefile使用腳本 gen-syscall-entries.sh處理各個 .c文件中的COBALTSYSCALL宏,生成一個頭文件 syscall_entries.h,里面是對每個COBALTSYSCALL宏處理后后的項,以上面 COBALT_SYSCALL(bind,...)為例 syscall_entries.h中會生成如下兩項,第一項為系統調用入口,第二項為系統調用的模式:
實時系統調用表 cobalt_syscalls[]定義在文件 kernelcobaltposixsyscall.c中:
[] = __COBALT_NI,__COBALT_CALL32_INITHAND(__COBALT_NI)[] = 0,__COBALT_CALL32_INITMODE(0)[] = __syshand__(__name),__COBALT_CALL32_ENTRY(__name, __syshand__(__name))[] = __xn_exec_static const cobalt_syshand cobalt_syscalls[] = {__COBALT_CALL_NI__COBALT_CALL_ENTRIES};static const int cobalt_sysmodes[] = {__COBALT_CALL_NFLAGS__COBALT_CALL_MODES};
_COBALTCALLNI宏表示數組空間大小為__NRCOBALTSYSCALLS(128),每一項由COBALTCALL_ENTRIES定義,即腳本頭文件 syscall_entries.h中生成的每一項來填充:
#define __COBALT_CALL_ENTRY(__name)[sc_cobalt_ ## __name] = __syshand__(__name),__COBALT_CALL32_ENTRY(__name,__syshand__(__name))
__COBALT_CALL32_ENTRY是定義兼容的系統調用,宏展開如下,相當于在數組的多個位置定義包含了同一項CoBaLt_bind:
#define __COBALT_CALL32_ENTRY(__name, __handler)__COBALT_CALL32x_ENTRY(__name, __handler)__COBALT_CALL32emu_ENTRY(__name, __handler)#define __COBALT_CALL32emu_ENTRY(__name, __handler)[sc_cobalt_ ## __name + 256] = __handler,#define __COBALT_CALL32x_ENTRY(__name, __handler)[sc_cobalt_##__name+128]=__handler,
最后bind系統調用在cobalt_syscalls[]中如下
static const cobalt_syshand cobalt_syscalls[] = {[sc_cobalt_bind] = CoBaLt_bind,[sc_cobalt_bind + 128] = CoBaLt_bind, /*x32 support */[sc_cobalt_bind + 256] = CoBaLt_bind, /*ia32 emulation support*/.....};
相應的數組cobalt_sysmodes[]中的內容如下:
static const int cobalt_sysmodes[] = {[sc_cobalt_bind] = __xn_exec_bind,[sc_cobalt_bind + 256] = __xn_exec_lostage, /*x32 support */[sc_cobalt_bind + 128] = __xn_exec_lostage, /*ia32 emulation support*/......};
五、實時系統調用權限控制cobalt_sysmodes
上面說到,ipipe管理應用的系統調用時需要分清該系統調用是否合法,是否需要域切換等等。cobalt_sysmodes[]就是每個系統調用對應的模式,控制著每個系統調用的調用路徑。系統調用號為下標,值為具體模式。每個系統調用的sysmode如何生成見上一節,還是以實時應用的bind系統調用為例:
static const int cobalt_sysmodes[] = {[] = __xn_exec_bind,[] = __xn_exec_lostage, /*x32 support */[] = __xn_exec_lostage, /*ia32 emulation support*/......};
xenomai中所有的系統調用模式定義如下:
/*xenomaiposixsyscall.c*/#define __xn_exec_lostage 0x1 /*必須在linux域運行該系統調用*/#define __xn_exec_histage 0x2 /*必須在Xenomai域運行該系統調用*/#define __xn_exec_shadow 0x4 /*影子系統調用:必須映射調用方*/#define __xn_exec_switchback 0x8 /*切換回切換;調用者必須返回其原始模式*/#define __xn_exec_current 0x10 /*在不管域直接執行。*/#define __xn_exec_conforming 0x20 /*在兼容域(Xenomai或Linux)中執行*/#define __xn_exec_adaptive 0x40 /* 先直接執行如果返回-ENOSYS,則嘗試在相反的域中重新執行系統調用 */#define __xn_exec_norestart 0x80 /*收到信號后不要重新啟動syscall*//*Shorthand初始化系統調用的簡寫*/#define __xn_exec_init __xn_exec_lostage/*Xenomai空間中shadow系統調用的簡寫*/#define __xn_exec_primary (__xn_exec_shadow|__xn_exec_histage)/*Linux空間中shadow系統調用的簡寫*/#define __xn_exec_secondary (__xn_exec_shadow|__xn_exec_lostage)/*Linux空間中syscall的簡寫,如果有shadow則切換回linux*/#define __xn_exec_downup (__xn_exec_lostage|__xn_exec_switchback)/* 主域系統不可重啟調用的簡寫 */#define __xn_exec_nonrestartable (__xn_exec_primary|__xn_exec_norestart)/*域探測系統調用簡寫*/#define __xn_exec_probing (__xn_exec_conforming|__xn_exec_adaptive)/*將模式選擇移交給syscall。*/#define__xn_exec_handover(__xn_exec_current|__xn_exec_adaptive)
使用一個無符號32 位數的每一位來表示一種模式,各模式注釋已經很清楚,不在解釋,后面文章解析ipipe是如何根據mode來處理的。
參考
英特爾 64 位和 IA-32 架構軟件開發人員手冊第 3 卷 :系統編程指南極客時間專欄-趣談Linux操作系統《linux內核源代碼情景分析》
審核編輯 :李倩
-
Linux
+關注
關注
88文章
11758瀏覽量
219009 -
操作系統
+關注
關注
37文章
7401瀏覽量
129284 -
雙核
+關注
關注
0文章
40瀏覽量
15586
原文標題:雙核系統調用(ipipe)
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
雙編碼器:解鎖未來科技的“雙核密碼”
深入了解系統調用API:探索操作系統底層的關鍵接口
如何在rt-thread studio上開發STM32H747這類雙核的單片機?
單核CPU網關和雙核CPU網關有什么區別
鴻雁電器攜手智芯科推出離線雙核語音控制單火線開關
Analog Devices Inc. ADSP-SC592 SHARC+?雙核DSP數據手冊
DA14594 SmartBond雙核低功耗藍牙5.3 SoC 數據手冊和產品介紹
請問canmv-k230支持雙核嗎?如何調用另一個核心工作?
RZT2H CR52雙核BOOT流程和例程代碼分析
適用于單核、雙核和四核應用處理器的PMIC DA9063L-A數據手冊
雙核系統調用(ipipe)
評論