[ULK11]信號(三):從信號傳遞到原程序恢復執行


背景:內核剛剛處理完中斷和異常.現在它要返回到@current進程了.然而,在返回以前,內核總會習慣性檢查一下@current的TIF_SIGPENDING標誌,以便確定是否有尚未處理的信號.很不幸,確實有信號正在掛起隊列上排隊.於是,內核開始啓動do_signal()函數…


一. do_signal()函數

遍歷@current的掛起信號隊列:
1.如果信號的傳遞方式是SIG_IGN,則忽略這個信號;
2.如果信號的傳遞方式是SIG_DFL,則根據具體的信號執行相應的默認操作;
3.如果信號有一個處理函數,終止遍歷,執行這個信號處理函數;
4.如果信號是0,則檢查並處理系統調用的重新執行.
信號處理函數或系統調用的重新執行會返回到內核.緊接着,當內核嘗試恢復原程序的執行時,又會陷入do_signal()函數.最終結果是,do_signal()將處理掛起信號隊列中的每一個信號.

1.參數

  • regs 棧,current在用戶態下寄存器的內容存於此處
  • oldset 阻塞信號的位掩碼數組(已被刪除)

2.說明

1. 通常只在CPU返回到用戶態時才調用此函數:TIF_SIGPENDING標誌的檢查總是在內核準備返回到用戶態時進行.
2. 反覆調用dequeue_signal()直到pending和shared_pending隊列爲空
3. 調用棧

  • do_signal()
    在返回到用戶態前,處理@current的每一個未阻塞的掛起信號.
    • get_signal_to_deliver()
      遍歷掛起信號隊列,自行忽略信號或爲信號執行默認操作.
    • handle_signal()
      爲執行信號處理程序做準備.
      • setup_rt_frame()
        複製,修改@current的硬件上下文.

3.複雜性

  • 競爭條件,凍結系統,產生內存信息轉儲,停止/殺死整個線程組
  • 中斷處理程序調用此函數(可能性?)
  • 當current正受到其他進程監控的時候怎麼辦?
    do_notify_parent_cldstop()和schedule()
  • 待處理的信號是一個被忽略的信號(可能性?)
  • 待處理西信號需要被執行缺省操作
  • 待處理信號有一個信號處理函數

二.get_signal_to_deliver()函數

這個函數遍歷掛起信號隊列,處理被顯式忽略的信號並併爲具體信號執行相應的默認操作.如果遇到信號0(處理系統調用的重新執行)或者遇到註冊了信號處理程序的信號,則終止遍歷,返回到do_signal()

1.函數的執行過程

  • try_to_freeze()
    linux凍結系統在信號系統中的鉤子函數.linux凍結系統利用信號系統完成自己的功能.
  • if(signal->flags & SIGNAL_CLD_MASK)
    每一個停止的進程在甦醒後都會運行這個檢查.在這裏,我們檢查一下是不是需要通知@current的父進程
  • 陷入一個無限循環
    • 在循環的開始,檢查是不是需要停止整個線程組.
    • 從掛起隊列上摘下一個信號
    • 如果信號是0,則返回0
    • 如果信號被顯式忽略,continue
    • 如果信號有一個處理程序,終止循環,返回信號ID
    • 執行默認操作.爲具體的信號執行相應的信號處理程序

2.信號的缺省操作

  • 當接收進程是init()時,丟棄信號;
  • 當信號是SIGCONT, SIGCHLD, SIGWINCH, SIGURG時,忽略信號;
  • 當信號是SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU時,停止整個線程組;
  • 缺省操作:Dump
  • 缺省操作: Terminate: do_group_exit()(組退出)

do_signal_stop()
組停止

P440

三.handle_signal()函數

信號處理程序必須在用戶態執行:

原程序– –信號處理程序— –信號處理程序– –原程序
|1(中斷) |2 |3(系統調用) |4 |5 |
–內核態產生信號– –系統調用—- –內核–

多次狀態切換引起複雜性:
- 內核態向用戶態向用戶態切換時,內核態堆棧被清空.因此,第2次特權級切換以後,原程序的硬件上下文丟失
- 信號處理程序調用系統調用後,第4次特卻級切換時,內核必須返回到信號處理程序而非原程序

信號的處理過程回顧
一個非阻塞的信號被髮送給一個進程.當中斷或異常發生時,進程切換到內核態.正要返回到用戶態前(return_from_intr),內核執行do_signal()函數,這個函數從get_signal_to_deliver()得到一個註冊信號.於是,handle_signal()函數被調用來爲這個註冊信號的處理搭建環境.
當進程又切換回用戶態時,因爲信號處理程序的起始地址已經被放進程序計數器中, 因此開始執行信號處理程序.當信號處理程序終止時, setup_frame()函數放在用戶態堆棧中的返回代碼就被執行.這個代碼調用sigreturn()系統調用,相應的服務例程把原程序的用戶態堆棧的硬件上下文複製到內核態堆棧,並把用戶態堆棧恢復到它原來的狀態(restore_sigcontext()).當這個系統調用結束時,普通進程就因此能恢復自己的執行.

1.背景

CPU處在內核態,即將返回到用戶態.但是,此時捕捉到了一個註冊信號.內核必須做些什麼,以保證:

  • 內核不會返回到原程序,而是返回到信號處理程序;
  • 信號處理程序執行結束後必須返回內核;
  • 從內核再返回到原程序的時候,原程序的硬件上下文不能丟失;

思考一: 在中斷或異常發生的時候,程序是如何陷入內核的?
當執行了一條指令後,cs和eip這對寄存器包含下一條將要執行的指令的邏輯地址.在處理那條指令前,控制單元會檢查是不是已經發生了一箇中斷或異常.如果有的話,控制器就會依次執行下列步驟:
1.確定中斷向量;
2.訪問IDT,找到與中斷向量對應的中斷門或陷阱門;
3.根據IDT中的門描述符,藉助GDT,找到中斷或異常處理程序的邏輯地址.
4.進行特權級檢查.要求CPL大於等於中斷處理程序邏輯地址的DPL,即引起中斷程序的特權必須低於或等於中斷處理程序的特權;
5.如果CPL與DPL不同,這通常意味着在用戶態請求內核態的中斷或用戶處理.此時,利用TSS段把棧切換到內核態.
注意,上述過程是具體於Linux的.ULK145的敘述則不針對任何具體的操作系統,是單純的描述Intel的硬件處理過程,因此敘述過程始終沒有出現”內核態”和”用戶態”等具體於操作系統的術語,十分嚴謹.

思考二: 中斷或異常發生的時候,用戶態進程的硬件上下文需要保存嗎?保存於何處?
不管是中斷還是異常,用戶態進程的硬件上下文都會保存在當前的堆棧中.由於在保存以前已經進行了硬件處理,所當前的堆棧通常是內核棧.具體的:
@error_code的第一步就是”把高級C函數可能用到的寄存器保存在棧中”;
@common_interrupt的第一步就是”SAVE_ALL”

思考三: do_signal()結束以後,如何返回到信號處理程序而不是原程序?
只需要對保存的硬件上下文做一些修改即可.

思考四: 當信號處理程序恢復執行的時候,如何保證原程序的硬件上下文不會丟失?
原程序的硬件上下文會被複制兩份.一份壓入用戶態堆棧中保存起來.一份稍作修改以後,用作信號處理程序的硬件上下文.

思考五: 信號處理程序結束以後如何返回到內核?
信號處理程序被調用的時候,它的返回地址(通常就是下一條指令的地址)會被壓棧保存(是嗎?).信號處理程序在它的返回地址之上建立自己的棧.
當信號處理程序執行結束以後,它的返回地址從棧中彈出.CPU開始從這個地址處繼續執行因此,爲了讓信號處理程序結束以後返回到內核,只需要修改信號處理程序所使用的堆棧即可.

2.這個函數做了什麼?

  • 將原程序的硬件上下文複製成兩份:
setup_sigcontext(&frame->sc, fpstate, regs, set->sig[0])
  • 其中一份保存在幀中,並隨幀一起壓入用戶態堆棧,以備將來恢復原程序的執行.恢復原程序執行的任務由sigreturn()系統調用完成,信號處理程序會通過pretcode返回到這個系統調用.
  • 另一份上下文(regs)作爲信號處理程序的硬件上下文使用.handle_signal()會修改這份硬件上下文,將上下文中的返回地址改爲信號處理程序的地址,並設置正確的棧頂地址;
  • 把幀放在信號處理程序的下面.幀的頂部存放着sigreturn()系統調用的入口地址,這樣當信號處理程序返回時,這個入口地址會被當作返回地址使用,於是系統陷入sigreturn系統調用.
  • 檢查信號標誌.在執行信號的時候,新來的信號是要被阻塞的.哪些信號要被阻塞呢?
    1. 進程描述符裏面規定要阻塞的信號是要被阻塞的: current->blocked
    2. 這個信號處理函數規定要阻塞的信號,是要被阻塞的: ka->->sa.sa_mask
    3. 當前信號,是要被阻塞的: sig

3.幀(sigframe)

pretcode
這個地址被放在幀的最頂部,由於內存中棧是倒着用的,所以以內存的角度敘述的話,這個地址也是幀的起始地址.幀被”悄悄地”放在信號處理程序的下面,因此pretcode會被當作信號處理程序的返回地址使用.
sig
信號編號,這是信號處理程序所需要的參數.
sc
用戶態進程的上下文,這個上下文是原程序第一次陷入內核的時候從用戶態堆棧複製而來的.現在,這個上下文又被內核放入幀中,隨幀一起壓入用戶態堆棧.
fpstate
用戶態進程的浮點寄存器內容.這個也算是硬件上下文的一部分把?
extramask
被阻塞的實時信號的位數組.這是位數組,這裏面都是實時信號,這些實時信號都被阻塞了.問題是,這個幀域有什麼用?
retcode
sigreturn()系統調用的8字節代碼.已不再使用.

關於函數調用時棧狀態的細節,可以百度一下x86上的函數調用.

4.set_frame()函數

計算幀在用戶態堆棧的起始地址,然後認真填入幀的每一個字段.特別地,原程序的硬件上下文會在這個過程中被複制進用戶態對戰.
修改依然留在內核態堆棧中的硬件上下文.在這個過程中,esp和eip會被修改爲正確的值.esp指向幀的起始地址,eip指向信號處理程序的起始地址.

參數
sig: 信號ID
ka: k_sigaction表
oldset: 阻塞信號掩碼
regs: 內核態堆棧中的用戶態硬件上下文的地址

至此,handle_signal()返回到do_signal(), do_signal()也立即返回.do_signal()返回時,當前進程恢復它在用戶態的執行.而由於setup_frame()函數已經偷天換日,所以eip寄存器指向了信號處理程序的第一條指令,esp寄存器已壓入用戶棧頂.所以,信號處理程序開始執行.

當信號處理程序執行結束時,返回到棧頂地址,也就是pretcode.pretcode會稍作準備,然後陷入sigreturn系統調用,這個系統調用結束時,CPU控制權返回到原程序.


四.返回到原程序

信號處理程序結束時,返回棧頂地址.棧頂地址指向幀的pretcode字段所引用的vsyscall頁中的代碼,這段代碼發出0x80中斷,開始調用sigreturn()系統調用.

首先,sys_sigreturn()首先找到幀在用戶態堆棧的地址,這可以通過esp字段輕易完成.

然後,恢復current->blocked字段.何爲恢復?剛纔,爲了執行信號處理程序,我們修改了current->blocked字段,強迫它吞併了信號處理程序的阻塞位和所處理信號的阻塞位.現在,我們要將這個字段恢復到以前的狀態.這樣,爲信號處理函數執行而屏蔽的所有信號被解除阻塞

接下來,我們重新調用recalc_sigpending()函數,.如果有新的阻塞信號,我們就傳遞它們.這裏,我認爲,所謂”阻塞信號”指的是,信號可以產生,但是不會被傳遞.解除阻塞意味着信號終於可以被傳遞了.

最後,我們要訪問幀的sc字段,這個字段指向原程序在用戶棧中的硬件上下文,我們把這個上下文拷貝到內核棧.所有這一切交給一個函數來完成:restore_sigcontext(),這個函數還會將用戶棧中的硬件上下文刪除.

五. 系統調用的重新執行

有的時候,進程通過系統調用向內核請求服務,比如讀寫一個文件.然而,內核並不能總是滿足進程的請求,此時,內核將這個進程置爲TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE狀態.

問題來了,如果進程由於不能完成系統調用而處在I或UI狀態下,一個信號產生在這個進程上,會發生什麼呢?

先來說明處在I狀態的進程把.如果一個進程處在I狀態,並且收到一個信號.那麼內核不會等待系統調用完成,而是直接將進程置爲R態.進程甦醒後,切換會用戶態,此時信號被傳遞給進程.當這種情況發生的時候,系統調用沒有完成它的工作,系統調用歷程會向內核返回一個錯誤碼.注意,用戶進程並不會收到這個錯誤碼,用戶進程獲得的唯一錯誤嗎是EINTER,這個錯誤碼告訴用戶進程系統調用沒有執行完.

讓我們回到內核錯誤碼上來.內核掌握着兩點關鍵信息:

  1. 從系統調用服務歷程返回的錯誤碼
  2. 用戶進程對信號的傳遞方式.

根據這兩點信息,內核從如下動作中選擇一個:

  • 不重新執行系統調用;
  • 重新執行系統調用;
  • 根據SA_RESTART標誌的值決定是否重新執行系統調用.

上述這一切的前提是,進程因執行系統調用失敗而掛起.那麼,內核是怎麼知道進程掛起的原因是系統調用執行失敗的呢?這就是regs硬件上下文的orig_eax字段發揮作用的地方了.P446

系統調用的重新執行需要分情況討論:

  • 系統調用被未捕獲的信號中斷
  • 系統調用被捕獲的信號中斷

這是自然的,因爲前者不需要執行信號處理程序,後者則需要執行信號處理程序.

1.系統調用被未捕獲的信號中斷

在這種情況下,do_signal()修改regs硬件上下文,讓eip指向int $0x80或者sysenter指令.這樣,當原程重新開始執行的時候,它會直接重新開始執行系統調用.

有一種特殊情況,eax中存放的是restart_syscall()的系統調用號,系統調用服務例程返回RESTART_RESTARTBLOCK,這個錯誤代碼僅僅用於與時間有關的系統調用.爲什麼?如果一個系統調用要求進程睡眠20ms,10ms後進程被信號中斷,如果重新執行這個系統調用,那麼進程最終會睡眠30ms.

怎麼解決這個問題呢?這種情況發生時,內核不會忠實地完全重新執行系統調用.當這種情況發生時,正在執行系統調用服務歷程的內核會將一個特別定製的系統調用服務歷程的地值放在thread_info的restart_block字段,並在返回錯誤碼-ERESTART_RESTARTBLOCK.這樣,sys_restart_syscall()服務例程只執行這個特別定製的函數.

2.系統調用被捕獲的信號中斷.

梳理思路.進程因系統調用失敗而掛起.此時捕獲到一個信號,進程被喚醒,並從系統調用服務例程返回,注意,應該不會回到用戶態,而是直接處理信號.

handle_signal()函數會根據內核收到的出錯碼和sigaction表的SA_RESTART標誌來決定是否必須重新執行未完成的系統調用.

如果系統調用需要重新執行,那麼,handle_signal()會修改eip:

regs->ip -= 2; //重新指向剛纔的系統調用指令

然後繼續完成handle_signal()剩餘的工作,即更新內核棧與用戶棧,爲信號處理程序搭建環境.注意,在setup_frame()的時候,被複制進幀中保存起來的@ip是修改(-2)以後的ip,這個ip指向剛剛執行的系統調用指令.

這樣do_signal()在handle_handle()後緊接着返回用戶態執行信號處理程序.信號處理程序執行結束以後,通過sigreturn()系統調用陷入內核,當sigreturn()再次返回用戶態時,原程序的系統調用會被重新執行.用戶程序緊接着陷入內核執行系統調用,不會執行信號處理程序.

否則,系統調用不需要重新執行.這時候,handle_signal()會在@reg->ax中放入EINTR以向原程序返回錯誤代碼.然後繼續進行andle_signal()接下來的工作.即更新內核棧與用戶棧,爲信號處理程序的執行搭建環境.

發佈了25 篇原創文章 · 獲贊 7 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章