本篇有相當的難度,涉及用戶棧和內核棧的兩輪切換,CPU四次換棧,寄存器改值,將圍繞下圖來說明.
解讀
為本篇理解方便,把圖做簡化標簽說明:
user:用戶空間
kernel:內核空間
source(...):源函數
sighandle(...):信號處理函數,
syscall(...):系統調用,參數為系統調用號,如sigreturn,N(表任意)
user.source():表示在用戶空間運行的源函數
系列篇已多次說過,用戶態的任務有兩個運行棧,一個是用戶棧,一個是內核棧.??臻g分別來自用戶空間和內核空間.兩種空間是有嚴格的地址劃分的,通過虛擬地址的大小就能判斷出是用戶空間還是內核空間.系統調用本質上是軟中斷,它使CPU執行指令的場地由用戶棧變成內核棧.怎么變的并不復雜,就是改變(sp和cpsr寄存器的值).sp指向哪個棧就代表在哪個棧運行, 當cpu在用戶棧運行時是不能訪問內核空間的,但內核態任務可以訪問整個空間,而且內核態任務沒有用戶棧.
理解了上面的說明,再來說下正常系統調用流程是這樣的: user.source() -> kernel.syscall(N) - > user.source() ,想要回到user.source()繼續運行,就必須保存用戶?,F場各寄存器的值.這些值保存在內核棧中,恢復也是從內核?;謴?
信號消費的過程的上圖可簡化表示為: user.source() -> kernel.syscall(N) ->user.sighandle() ->kernel.syscall(sigreturn) -> user.source() 在原本要回到user.source()的中間插入了信號處理函數的調用. 這正是本篇要通過代碼來說清楚的核心問題.
順著這個思路可以推到以下幾點,實際也是這么做的:
kernel.syscall(N) 中必須要再次保存user.source()的上下文sig_switch_context,為何已經保存了一次還要再保存一次?
因為第一次是保存在內核棧中,而內核棧這部分數據會因回到用戶態user.sighandle()運行而被恢復現場出棧了.保存現場/恢復現場是成雙出隊的好基友,注意有些文章說會把整個內核棧清空,這是不對的.
第二次保存在任務結構體中,任務來源于任務池,是內核全局變量,常駐內存的.兩次保存的都是user.source()運行時現場信息,再回顧下相關的結構體.關鍵是sig_switch_context
typedef struct { // ... sig_cb sig;//信號控制塊,用于異步通信 } LosTaskCB; typedef struct {//信號控制塊(描述符) sigset_t sigFlag; //不屏蔽的信號集 sigset_t sigPendFlag; //信號阻塞標簽集,記錄那些信號來過,任務依然阻塞的集合.即:這些信號不能喚醒任務 sigset_t sigprocmask; /* Signals that are blocked */ //任務屏蔽了哪些信號 sq_queue_t sigactionq; //信號捕捉隊列 LOS_DL_LIST waitList; //等待鏈表,上面掛的是等待信號到來的任務, 請查找 OsTaskWait(&sigcb->waitList, timeout, TRUE) 理解 sigset_t sigwaitmask; /* Waiting for pending signals */ //任務在等待哪些信號的到來 siginfo_t sigunbinfo; /* Signal info when task unblocked */ //任務解鎖時的信號信息 sig_switch_context context; //信號切換上下文, 用于保存切換現場, 比如發生系統調用時的返回,涉及同一個任務的兩個棧進行切換 } sig_cb;
還必須要改變原有PC/R0/R1寄存器的值.想要執行user.sighandle(),PC寄存器就必須指向它,而R0,R1就是它的參數.
信號處理完成后須回到內核態,怎么再次陷入內核態? 答案是:__NR_sigreturn,這也是個系統調用.回來后還原sig_switch_context,即還原user.source()被打斷時SP/PC等寄存器的值,使其跳回到用戶棧從user.source()的被打斷處繼續執行.
有了這三個推論,再理解下面的代碼就是吹灰之力了,涉及三個關鍵函數OsArmA32SyscallHandle,OsSaveSignalContext,OsRestorSignalContext本篇一一解讀,徹底挖透.先看信號上下文結構體sig_switch_context.
sig_switch_context
//任務中斷上下文 #define TASK_IRQ_CONTEXT \ unsigned int R0; \ unsigned int R1; \ unsigned int R2; \ unsigned int R3; \ unsigned int R12; \ unsigned int USP; \ unsigned int ULR; \ unsigned int CPSR; \ unsigned int PC; typedef struct {//信號切換上下文 TASK_IRQ_CONTEXT unsigned int R7; //存放系統調用的ID unsigned int count; //記錄是否保存了信號上下文 } sig_switch_context;
保存user.source()現場的結構體,USP,ULR代表用戶棧指針和返回地址.
CPSR寄存器用于設置CPU的工作模式,CPU有7種工作模式,具體可前往翻看
v36.xx (工作模式篇) | cpu是韋小寶,有哪七個老婆?談論的用戶態(usr普通用戶)和內核態(sys超級用戶)對應的只是其中的兩種.二者都共用相同的寄存器.還原它就是告訴CPU內核已切到普通用戶模式運行.
其他寄存器沒有保存的原因是系統調用不會用到它們,所以不需要保存.
R7是在系統調用發生時用于記錄系統調用號,在信號處理過程中,R0將獲得信號編號,作為user.sighandle()的第一個參數.
count記錄是否保存了信號上下文
OsArmA32SyscallHandle 系統調用總入口
/* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */ /****************************************************************** 由匯編調用,見于 los_hw_exc.s / BLX OsArmA32SyscallHandle SYSCALL是產生系統調用時觸發的信號,R7寄存器存放具體的系統調用ID,也叫系統調用號 regs:參數就是所有寄存器 注意:本函數在用戶態和內核態下都可能被調用到 //MOV R0, SP @獲取SP值,R0將作為OsArmA32SyscallHandle的參數 ******************************************************************/ LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器記錄了觸發了具體哪個系統調用 if (cmd >= SYS_CALL_NUM) {//系統調用的總數 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; } //用戶進程信號處理函數完成后的系統調用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢復信號上下文,回到用戶棧運行. return regs; } handle = g_syscallHandle[cmd];//拿到系統調用的注冊函數,類似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//獲取參數個數 if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系統調用必須有參數且參數不能大于8個 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; } //regs[0-6] 記錄系統調用的參數,這也是由R7寄存器保存系統調用號的原因 switch (nArgs) {//參數的個數 case ARG_NUM_0: case ARG_NUM_1: ret = (*(SyscallFun1)handle)(regs[REG_R0]);//執行系統調用,類似 SysUnlink(pathname); break; case ARG_NUM_2://如何是兩個參數的系統調用,這里傳三個參數也沒有問題,因被調用函數不會去取用R2值 case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//類似 SysExecve(fileName, argv, envp); break; case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break; default: //7個參數的情況 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); } regs[REG_R0] = ret;//R0保存系統調用返回值 OsSaveSignalContext(regs);//如果有信號要處理,將改寫pc,r0,r1寄存器,改變返回正常用戶態路徑,而先去執行信號處理程序. /* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with the SYS_context_switch system call. */ return regs;//返回寄存器的值 }
解讀
這是系統調用的總入口,所有的系統調用都要跑這里要統一處理.通過系統號(保存在R7),找到注冊函數并回調.完成系統調用過程.
關于系統調用可查看v37.xx (系統調用篇) | 系統調用到底經歷了什么本篇不詳細說系統調用過程,只說跟信號相關的部分.
OsArmA32SyscallHandle總體理解起來是被信號的保存和還原兩個函數給包夾了.注意要在運行過程中去理解調用兩個函數的過程,對于同一個任務來說,一定是先執行OsSaveSignalContext,第二次進入OsArmA32SyscallHandle后再執行OsRestorSignalContext.
看OsSaveSignalContext,由它負責保存user.source() 的上下文,其中改變了sp,r0/r1寄存器值,切到信號處理函數user.sighandle()運行.
在函數的開頭,碰到系統調用號__NR_sigreturn,直接恢復信號上下文就退出了,因為這是要切回user.source()繼續運行的操作.
//用戶進程信號處理函數完成后的系統調用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢復信號上下文,回到用戶棧運行. return regs; }
OsSaveSignalContext 保存信號上下文
有了上面的鋪墊,就不難理解這個函數的作用.
/********************************************** 產生系統調用時,也就是軟中斷時,保存用戶棧寄存器現場信息 改寫PC寄存器的值 **********************************************/ void OsSaveSignalContext(unsigned int *sp) { UINTPTR sigHandler; UINT32 intSave; LosTaskCB *task = NULL; LosProcessCB *process = NULL; sig_cb *sigcb = NULL; unsigned long cpsr; OS_RETURN_IF_VOID(sp == NULL); cpsr = OS_SYSCALL_GET_CPSR(sp);//獲取系統調用時的 CPSR值 OS_RETURN_IF_VOID(((cpsr & CPSR_MASK_MODE) != CPSR_USER_MODE));//必須工作在CPU的用戶模式下,注意CPSR_USER_MODE(cpu層面)和OS_USER_MODE(系統層面)是兩碼事. SCHEDULER_LOCK(intSave);//如有不明白前往 https://my.oschina.net/weharmony 翻看工作模式/信號分發/信號處理篇 task = OsCurrTaskGet(); process = OsCurrProcessGet(); sigcb = &task->sig;//獲取任務的信號控制塊 //1.未保存任務上下文任務 //2.任何的信號標簽集不為空或者進程有信號要處理 if ((sigcb->context.count == 0) && ((sigcb->sigFlag != 0) || (process->sigShare != 0))) { sigHandler = OsGetSigHandler();//獲取信號處理函數 if (sigHandler == 0) {//信號沒有注冊 sigcb->sigFlag = 0; process->sigShare = 0; SCHEDULER_UNLOCK(intSave); PRINT_ERR("The signal processing function for the current process pid =%d is NULL!\n", task->processID); return; } /* One pthread do the share signal */ sigcb->sigFlag |= process->sigShare;//擴展任務的信號標簽集 unsigned int signo = (unsigned int)FindFirstSetedBit(sigcb->sigFlag) + 1; OsProcessExitCodeSignalSet(process, signo);//設置進程退出信號 sigcb->context.CPSR = cpsr; //保存狀態寄存器 sigcb->context.PC = sp[REG_PC]; //獲取被打斷現場寄存器的值 sigcb->context.USP = sp[REG_SP];//用戶棧頂位置,以便能從內核棧切回用戶棧 sigcb->context.ULR = sp[REG_LR];//用戶棧返回地址 sigcb->context.R0 = sp[REG_R0]; //系統調用的返回值 sigcb->context.R1 = sp[REG_R1]; sigcb->context.R2 = sp[REG_R2]; sigcb->context.R3 = sp[REG_R3]; sigcb->context.R7 = sp[REG_R7];//為何參數不用傳R7,是因為系統調用發生時 R7始終保存的是系統調用號. sigcb->context.R12 = sp[REG_R12];//詳見 https://my.oschina.net/weharmony/blog/4967613 sp[REG_PC] = sigHandler;//指定信號執行函數,注意此處改變保存任務上下文中PC寄存器的值,恢復上下文時將執行這個函數. sp[REG_R0] = signo; //參數1,信號ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //參數2 /* sig No bits 00000100 present sig No 3, but 1<< 3 = 00001000, so signo needs minus 1 */ sigcb->sigFlag ^= 1ULL << (signo - 1); sigcb->context.count++; //代表已保存 } SCHEDULER_UNLOCK(intSave); }
解讀
先是判斷執行條件,確實是有信號需要處理,有處理函數.自定義處理函數是由用戶進程安裝進來的,所有進程旗下的任務都共用,參數就是信號signo,注意可不是系統調用號,有區別的.信號編號長這樣.
#define SIGHUP 1 //終端掛起或者控制進程終止 #define SIGINT 2 //鍵盤中斷(ctrl + c) #define SIGQUIT 3 //鍵盤的退出鍵被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟蹤陷阱(trace trap),啟動進程,跟蹤代碼的執行 #define SIGABRT 6 //由abort(3)發出的退出指令 #define SIGIOT SIGABRT //abort發出的信號 #define SIGBUS 7 //總線錯誤 #define SIGFPE 8 //浮點異常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、處理和阻塞
系統調用號長這樣,是不是看到一些很熟悉的函數.
#define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 #define __NR_mknod 14 #define __NR_chmod 15 #define __NR_lchown 16 #define __NR_break 17
最后是最最最關鍵的代碼,改變pc寄存器的值,此值一變,在_osExceptSwiHdl中恢復上下文后,cpu跳到用戶空間的代碼段 user.sighandle(R0,R1) 開始執行,即執行信號處理函數.
sp[REG_PC] = sigHandler;//指定信號執行函數,注意此處改變保存任務上下文中PC寄存器的值,恢復上下文時將執行這個函數. sp[REG_R0] = signo; //參數1,信號ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //參數2
OsRestorSignalContext 恢復信號上下文
/**************************************************** 恢復信號上下文,由系統調用之__NR_sigreturn產生,這是一個內部產生的系統調用. 為什么要恢復呢? 因為系統調用的執行由任務內核態完成,使用的棧也是內核棧,CPU相關寄存器記錄的都是內核棧的內容, 而系統調用完成后,需返回任務的用戶棧執行,這時需將CPU各寄存器回到用戶態現場 所以函數的功能就變成了還原寄存器的值 ****************************************************/ void OsRestorSignalContext(unsigned int *sp) { LosTaskCB *task = NULL; /* Do not adjust this statement */ LosProcessCB *process = NULL; sig_cb *sigcb = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); task = OsCurrTaskGet(); sigcb = &task->sig;//獲取當前任務信號控制塊 if (sigcb->context.count != 1) {//必須之前保存過,才能被恢復 SCHEDULER_UNLOCK(intSave); PRINT_ERR("sig error count : %d\n", sigcb->context.count); return; } process = OsCurrProcessGet();//獲取當前進程 sp[REG_PC] = sigcb->context.PC;//指令寄存器 OS_SYSCALL_SET_CPSR(sp, sigcb->context.CPSR);//重置程序狀態寄存器 sp[REG_SP] = sigcb->context.USP;//用戶棧堆棧指針, USP指的是 用戶態的堆棧,即將回到用戶棧繼續運行 sp[REG_LR] = sigcb->context.ULR;//返回用戶棧代碼執行位置 sp[REG_R0] = sigcb->context.R0; sp[REG_R1] = sigcb->context.R1; sp[REG_R2] = sigcb->context.R2; sp[REG_R3] = sigcb->context.R3; sp[REG_R7] = sigcb->context.R7; sp[REG_R12] = sigcb->context.R12; sigcb->context.count--; //信號上下文的數量回到減少 process->sigShare = 0; //回到用戶態,信號共享清0 OsProcessExitCodeSignalClear(process);//清空進程退出碼 SCHEDULER_UNLOCK(intSave); }
解讀
在信號處理函數完成之后,內核會觸發一個__NR_sigreturn的系統調用,又陷入內核態,回到了OsArmA32SyscallHandle.
恢復的過程很簡單,把之前保存的信號上下文恢復到內核棧sp開始位置,數據在棧中的保存順序可查看 用棧方式篇 ,最重要的看這幾句.
sp[REG_PC] = sigcb->context.PC;//指令寄存器 sp[REG_SP] = sigcb->context.USP;//用戶棧堆棧指針, USP指的是 用戶態的堆棧,即將回到用戶棧繼續運行 sp[REG_LR] = sigcb->context.ULR;//返回用戶棧代碼執行位置
注意這里還不是真正的切換上下文,只是改變內核棧中現有的數據.這些數據將還原給寄存器.USP和ULR指向的是用戶棧的位置.一旦PC,USP,ULR從棧中彈出賦給寄存器.才真正完成了內核棧到用戶棧的切換.回到了user.source()繼續運行.
真正的切換匯編代碼如下,都已添加注釋,在保存和恢復上下文中夾著OsArmA32SyscallHandle
@ Description: Software interrupt exception handler _osExceptSwiHdl: @軟中斷異常處理,注意此時已在內核棧運行 @保存任務上下文(TaskContext) 開始... 一定要對照TaskContext來理解 SUB SP, SP, #(4 * 16) @先申請16個??臻g單元用于處理本次軟中斷 STMIA SP, {R0-R12} @TaskContext.R[GEN_REGS_NUM] STMIA從左到右執行,先放R0 .. R12 MRS R3, SPSR @讀取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 獲取中斷模式 CMP R1, #CPSR_USER_MODE @ User mode 是否為用戶模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用戶模式下跳轉 @ 當為用戶模式時,獲取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @獲取SP值,R0將作為OsArmA32SyscallHandle的參數 STMFD SP!, {R3} @ Save the CPSR 入棧保存CPSR值 => TaskContext.regPSR ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存儲位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 從右向左 保存 => TaskContext.LR和SP SUB SP, SP, #4 @ => TaskContext.resved PUSH_FPU_REGS R1 @保存中斷模式(用戶模式) @保存任務上下文(TaskContext) 結束 MOV FP, #0 @ Init frame pointer CPSIE I @開中斷,表明在系統調用期間可響應中斷 BLX OsArmA32SyscallHandle /*交給C語言處理系統調用,參數為R0,指向TaskContext的開始位置*/ CPSID I @執行后續指令前必須先關中斷 @恢復任務上下文(TaskContext) 開始 POP_FPU_REGS R1 @彈出FPU值給R1 ADD SP, SP,#4 @ 定位到保存舊SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 彈出舊SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢復該模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢復R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢復用戶模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存舊PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用戶模式運行 @恢復任務上下文(TaskContext) 結束
編輯:hfy
-
寄存器
+關注
關注
31文章
5317瀏覽量
120010 -
cpu
+關注
關注
68文章
10825瀏覽量
211155 -
信號處理
+關注
關注
48文章
1000瀏覽量
103201
發布評論請先 登錄
相關推薦
評論