信號生產
關于信號篇,本只想寫一篇,但發現把它想簡單了,內容不多,難度極大.整理了好長時間,理解了為何<<深入理解linux內核>>要單獨為它開一章,原因有二
- 信號相關的結構體多,而且還容易搞混.所以看本篇要注意結構體的名字和作用.
- 系統調用太多了,涉及面廣,信號的來源分硬件和軟件.相當于軟中斷和硬中斷,這就會涉及到匯編代碼,但信號的處理函數又在用戶空間,CPU是禁止內核態執行用戶態代碼的,所以運行過程需在用戶空間和內核空間來回的折騰,頻繁的切換上下文.
信號思想來自Unix,它老人家已經五十多歲了,但很有活力,許多方面幾乎沒發生大的變化.信號可以由內核產生,也可以由用戶進程產生,并由內核傳送給特定的進程或線程(組),若這個進程定義了自己的信號處理程序,則調用這個程序去處理信號,否則則執行默認的程序或者忽略.
信號為系統提供了一種進程間異步通訊的方式,一個進程不必通過任何操作來等待信號的到達。事實上,進程也不可能知道信號到底什么時候到達。一般來說,只需用戶進程提供信號處理函數,內核會想方設法調用信號處理函數,網上查閱了很多的關于信號的資料.個人想換個視角去看信號.把異步過程理解為生產者(安裝和發送信號)和消費者(捕捉和處理信號)兩個過程
信號分類
每個信號都有一個名字和編號,這些名字都以SIG
開頭,例如SIGQUIT
、SIGCHLD
等等。
信號定義在signal.h頭文件中,信號名都定義為正整數。
具體的信號名稱可以使用kill -l
來查看信號的名字以及序號,信號是從1開始編號的,不存在0號信號。不過kill
對于信號0有特殊的應用。啥用呢? 可用來查詢進程是否還在. 敲下 kill 0 pid
就知道了.
信號分為兩大類:可靠信號與不可靠信號,前32種信號為不可靠信號,后32種為可靠信號。
不可靠信號: 也稱為非實時信號,不支持排隊,信號可能會丟失, 比如發送多次相同的信號, 進程只能收到一次. 信號值取值區間為1~31;
可靠信號: 也稱為實時信號,支持排隊, 信號不會丟失, 發多少次, 就可以收到多少次. 信號值取值區間為32~64
#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 SIGUSR1 10 //用戶自定義信號1
#define SIGSEGV 11 //無效的內存引用, 段違例(segmentation violation),進程試圖去訪問其虛地址空間以外的位置
#define SIGUSR2 12 //用戶自定義信號2
#define SIGPIPE 13 //向某個非讀管道中寫入數據
#define SIGALRM 14 //由alarm(2)發出的信號,默認行為為進程終止
#define SIGTERM 15 //終止信號
#define SIGSTKFLT 16 //棧溢出
#define SIGCHLD 17 //子進程結束信號
#define SIGCONT 18 //進程繼續(曾被停止的進程)
#define SIGSTOP 19 //終止進程 | 不能被忽略、處理和阻塞
#define SIGTSTP 20 //控制終端(tty)上 按下停止鍵
#define SIGTTIN 21 //進程停止,后臺進程企圖從控制終端讀
#define SIGTTOU 22 //進程停止,后臺進程企圖從控制終端寫
#define SIGURG 23 //I/O有緊急數據到達當前進程
#define SIGXCPU 24 //進程的CPU時間片到期
#define SIGXFSZ 25 //文件大小的超出上限
#define SIGVTALRM 26 //虛擬時鐘超時
#define SIGPROF 27 //profile時鐘超時
#define SIGWINCH 28 //窗口大小改變
#define SIGIO 29 //I/O相關
#define SIGPOLL 29 //
#define SIGPWR 30 //電源故障,關機
#define SIGSYS 31 //系統調用中參數錯,如系統調用號非法
#define SIGUNUSED SIGSYS//不使用
#define _NSIG 65
信號來源
信號來源分為硬件類和軟件類:
- 硬件類
- 用戶輸入:比如在終端上按下組合鍵
ctrl+C
,產生SIGINT
信號; - 硬件異常:CPU檢測到內存非法訪問等異常,通知內核生成相應信號,并發送給發生事件的進程;
- 用戶輸入:比如在終端上按下組合鍵
- 軟件類
- 通過系統調用,發送signal信號:
kill()
,raise()
,sigqueue()
,alarm()
,setitimer()
,abort()
-
kill
命令就是一個發送信號的工具,用于向進程或進程組發送信號.例如:kill 9 PID
(SIGKILL
)來殺死PID
進程. - sigqueue():只能向一個進程發送信號,不能向進程組發送信號;主要針對實時信號提出,與sigaction()組合使用,當然也支持非實時信號的發送;
- alarm():用于調用進程指定時間后發出SIGALARM信號;
- setitimer():設置定時器,計時達到后給進程發送SIGALRM信號,功能比alarm更強大;
- abort():向進程發送SIGABORT信號,默認進程會異常退出。
- raise():用于向進程自身發送信號;
-
- 通過系統調用,發送signal信號:
信號與進程的關系
主要是通過系統調用 sigaction
將用戶態信號處理函數注冊到PCB保存.所有進程的任務都共用這個信號注冊函數sigHandler
,在信號的消費階段內核用一種特殊的方式'回調'它.
typedef struct ProcessCB {//PCB中關于信號的信息
UINTPTR sigHandler; /**< signal handler */ //捕捉信號后的處理函數
sigset_t sigShare; /**< signal share bit */ //信號共享位,64個信號各站一位
}LosProcessCB;
typedef unsigned _Int64 sigset_t; //一個64位的變量,每個信號代表一位.
struct sigaction {//信號處理機制結構體
union {
void (*sa_handler)(int); //信號處理函數——普通版
void (*sa_sigaction)(int, siginfo_t *, void *);//信號處理函數——高級版
} __sa_handler;
sigset_t sa_mask;//指定信號處理程序執行過程中需要阻塞的信號;
int sa_flags; //標示位
// SA_RESTART:使被信號打斷的syscall重新發起。
// SA_NOCLDSTOP:使父進程在它的子進程暫停或繼續運行時不會收到 SIGCHLD 信號。
// SA_NOCLDWAIT:使父進程在它的子進程退出時不會收到SIGCHLD信號,這時子進程如果退出也不會成為僵 尸進程。
// SA_NODEFER:使對信號的屏蔽無效,即在信號處理函數執行期間仍能發出這個信號。
// SA_RESETHAND:信號處理之后重新設置為默認的處理方式。
// SA_SIGINFO:使用sa_sigaction成員而不是sa_handler作為信號處理函數。
void (*sa_restorer)(void);
};
typedef struct sigaction sigaction_t;
解讀
- 每個信號都對應一個位. 信號從1開始編號 [1 ~ 64] 對應
sigShare
的[0 ~ 63]位,所以中間會差一個.記住這點,后續代碼會提到. -
sigHandler
信號處理函數的注冊過程,由系統調用sigaction
(用戶空間) ->OsSigAction
(內核空間)完成綁定動作.
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
int OsSigAction(int sig, const sigaction_t *act, sigaction_t *oact)
{
UINTPTR addr;
sigaction_t action;
if (!GOOD_SIGNO(sig) || sig < 1 || act == NULL) {
return -EINVAL;
}
//將數據從用戶空間拷貝到內核空間
if (LOS_ArchCopyFromUser(&action, act, sizeof(sigaction_t)) != LOS_OK) {
return -EFAULT;
}
if (sig == SIGSYS) {//鴻蒙此處通過錯誤的系統調用 來安裝信號處理函數,有點巧妙.
addr = OsGetSigHandler();//獲取進程信號處理函數
if (addr == 0) {//進程沒有設置信號處理函數時
OsSetSigHandler((unsigned long)(UINTPTR)action.sa_handler);//設置進程信號處理函數——普通版
return LOS_OK;
}
return -EINVAL;
}
return LOS_OK;
}
* `sigaction(...)`第一個參數是要安裝的信號; 第二個參數與sigaction函數同名的結構體,這里會讓人很懵,函數名和結構體一直,沒明白為毛要這么搞? 結構體內定義了信號處理方法;第三個為輸出參數,將信號的當前的sigaction結構帶回.但鴻蒙顯然沒有認真對待第三個參數.把`musl`實現給閹割了.
* 對結構體的`sigaction`鴻蒙目前只支持信號處理函數——普通版,`sa_handler`表示自定義信號處理函數,該函數返回值為void,可以帶一個int參數,通過參數可以得知當前信號的編號,這樣就可以用同一個函數處理多種信號。
* `sa_mask`指定信號處理程序執行過程中需要阻塞的信號。
* `sa_flags`字段包含一些選項,具體看注釋
* `sa_sigaction`是實時信號的處理函數,`union`二選一.鴻蒙暫時不支持這種方式.
信號與任務的關系
typedef struct {//TCB中關于信號的信息
sig_cb sig; //信號控制塊,用于異步通信,類似于 linux singal模塊
} 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;
解讀
- 系列篇已多次說過,進程只是管理資源的容器,真正讓cpu干活的是任務
task
,所以發給進程的信號最終還是需要分發給具體任務來處理.所以能想到的是關于任務部分會更復雜. -
context
信號處理很復雜的原因在于信號的發起在用戶空間,發送需要系統調用,而處理信號的函數又是用戶空間提供的, 所以需要反復的切換任務上下文.而且還有硬中斷的問題,比如 ctrl + c ,需要從硬中斷中回調用戶空間的信號處理函數,處理完了再回到內核空間,最后回到用戶空間.沒聽懂吧,我自己都說暈了,所以需要專門的一篇來說清楚信號的處理問題.本篇不展開說. -
sig_cb
結構體是任務處理信號的結構體,要響應,屏蔽哪些信號等等都由它完成,這個結構體雖不復雜,但是很繞,很難搞清楚它們之間的區別.筆者是經過一番痛苦的閱讀理解后才明白各自的含義.并想通過用打比方的例子試圖讓大家明白. - 以下用追女孩打比方理解.任務相當于某個男,沒錯說的就是屏幕前的你,除了苦逼的碼農誰會有耐心能堅持看到這里.64個信號對應64個女孩.允許一男同時追多個女孩,女孩也可同時被多個男追.女孩也可以主動追男的.理解如下:
-
waitList
等待信號的任務鏈表,上面掛的是因等待信號而被阻塞的任務.眾男在排隊追各自心愛的女孩們,處于無所事事的掛起的狀態,等待女孩們的出現. -
sigwaitmask
任務在等待的信號集合,只有這些信號能喚醒任務.相當于列出喜歡的各位女孩,只要出現一位就能讓你滿血復活. -
sigprocmask
指任務對哪些信號不感冒.來了也不處理.相當于列出不喜歡的各位女孩,請她們別來騷擾你,嘚瑟. -
sigPendFlag
信號到達但并未喚醒任務.相當于喜歡你的女孩來追你,但她不在你喜歡的列表內,結果是不搭理人家繼續等喜歡的出現. -
sigFlag
記錄不屏蔽的信號集合,相當于你并不反感的女孩們.記錄來過的那些女孩(除掉你不喜歡的).
信號發送過程
用戶進程調用kill()
的過程如下:
kill(pid_t pid, int sig) - 系統調用
| 用戶空間
---------------------------------------------------------------------------------------
| 內核空間
SysKill(...)
|---> OsKillLock(...)
|---> OsKill(.., OS_USER_KILL_PERMISSION)
|---> OsDispatch() //鑒權,向進程發送信號
|---> OsSigProcessSend() //選擇任務發送信號
|---> OsSigProcessForeachChild(..,ForEachTaskCB handler,..)
|---> SigProcessKillSigHandler() //處理 SIGKILL
|---> OsTaskWake() //喚醒所有等待任務
|---> OsSigEmptySet() //清空信號等待集
|---> SigProcessSignalHandler()
|---> OsTcbDispatch() //向目標任務發送信號
|---> OsTaskWake() //喚醒任務
|---> OsSigEmptySet() //清空信號等待集
流程
通過 系統調用
kill
陷入內核空間-
因為是用戶態進程,使用
OS_USER_KILL_PERMISSION
權限發送信號#define OS_KERNEL_KILL_PERMISSION 0U //內核態 kill 權限 #define OS_USER_KILL_PERMISSION 3U //用戶態 kill 權限
-
鑒權之后進程輪詢任務組,向目標任務發送信號.這里分三種情況:
-
SIGKILL
信號,將所有等待任務喚醒,拉入就緒隊列等待被調度執行,并情況信號等待集 - 非
SIGKILL
信號時,將通過sigwaitmask
和sigprocmask
過濾,找到一個任務向它發送信號OsTcbDispatch
.
-
代碼細節
int OsKill(pid_t pid, int sig, int permission)
{
siginfo_t info;
int ret;
/* Make sure that the para is valid */
if (!GOOD_SIGNO(sig) || pid < 0) {//有效信號 [0,64]
return -EINVAL;
}
if (OsProcessIDUserCheckInvalid(pid)) {//檢查參數進程
return -ESRCH;
}
/* Create the siginfo structure */ //創建信號結構體
info.si_signo = sig; //信號編號
info.si_code = SI_USER; //來自用戶進程信號
info.si_value.sival_ptr = NULL;
/* Send the signal */
ret = OsDispatch(pid, &info, permission);//發送信號
return ret;
}
//信號分發
int OsDispatch(pid_t pid, siginfo_t *info, int permission)
{
LosProcessCB *spcb = OS_PCB_FROM_PID(pid);//找到這個進程
if (OsProcessIsUnused(spcb)) {//進程是否還在使用,不一定是當前進程但必須是個有效進程
return -ESRCH;
}
#ifdef LOSCFG_SECURITY_CAPABILITY //啟用能力安全模式
LosProcessCB *current = OsCurrProcessGet();//獲取當前進程
/* If the process you want to kill had been inactive, but still exist. should return LOS_OK */
if (OsProcessIsInactive(spcb)) {//如果要終止的進程處于非活動狀態,但仍然存在,應該返回OK
return LOS_OK;
}
/* Kernel process always has kill permission and user process should check permission *///內核進程總是有kill權限,用戶進程需要檢查權限
if (OsProcessIsUserMode(current) && !(current->processStatus & OS_PROCESS_FLAG_EXIT)) {//用戶進程檢查能力范圍
if ((current != spcb) && (!IsCapPermit(CAP_KILL)) && (current->user->userID != spcb->user->userID)) {
return -EPERM;
}
}
#endif
if ((permission == OS_USER_KILL_PERMISSION) && (OsSignalPermissionToCheck(spcb) < 0)) {
return -EPERM;
}
return OsSigProcessSend(spcb, info);//給參數進程發送信號
}
//給參數進程發送參數信號
int OsSigProcessSend(LosProcessCB *spcb, siginfo_t *sigInfo)
{
int ret;
struct ProcessSignalInfo info = {
.sigInfo = sigInfo, //信號內容
.defaultTcb = NULL, //以下四個值將在OsSigProcessForeachChild中根據條件完善
.unblockedTcb = NULL,
.awakenedTcb = NULL,
.receivedTcb = NULL
};
//總之是要從進程中找個至少一個任務來接受這個信號,優先級
//awakenedTcb > receivedTcb > unblockedTcb > defaultTcb
/* visit all taskcb and dispatch signal */ //訪問所有任務和分發信號
if ((info.sigInfo != NULL) && (info.sigInfo->si_signo == SIGKILL)) {//需要干掉進程時 SIGKILL = 9, #linux kill 9 14
(void)OsSigProcessForeachChild(spcb, SigProcessKillSigHandler, &info);//進程要被干掉了,通知所有task做善后處理
OsSigAddSet(&spcb->sigShare, info.sigInfo->si_signo);
OsWaitSignalToWakeProcess(spcb);//等待信號喚醒進程
return 0;
} else {
ret = OsSigProcessForeachChild(spcb, SigProcessSignalHandler, &info);//進程通知所有task處理信號
}
if (ret < 0) {
return ret;
}
SigProcessLoadTcb(&info, sigInfo);
return 0;
}
//讓進程的每一個task執行參數函數
int OsSigProcessForeachChild(LosProcessCB *spcb, ForEachTaskCB handler, void *arg)
{
int ret;
/* Visit the main thread last (if present) */
LosTaskCB *taskCB = NULL;//遍歷進程的 threadList 鏈表,里面存放的都是task節點
LOS_DL_LIST_FOR_EACH_ENTRY(taskCB, &(spcb->threadSiblingList), LosTaskCB, threadList) {//遍歷進程的任務列表
ret = handler(taskCB, arg);//回調參數函數
OS_RETURN_IF(ret != 0, ret);//這個宏的意思就是只有ret = 0時,啥也不處理.其余就返回 ret
}
return LOS_OK;
}
- 如果是
SIGKILL
信號,讓spcb
的所有任務執行SigProcessKillSigHandler
函數,查看旗下的所有任務是否又在等待這個信號的,如果有就將任務喚醒,放在就緒隊列等待被調度執行.
//進程收到 SIGKILL 信號后,通知任務tcb處理.
static int SigProcessKillSigHandler(LosTaskCB *tcb, void *arg)
{
struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//轉參
if ((tcb != NULL) && (info != NULL) && (info->sigInfo != NULL)) {//進程有信號
sig_cb *sigcb = &tcb->sig;
if (!LOS_ListEmpty(&sigcb->waitList) && OsSigIsMember(&sigcb->sigwaitmask, info->sigInfo->si_signo)) {//如果任務在等待這個信號
OsTaskWake(tcb);//喚醒這個任務,加入進程的就緒隊列,并不申請調度
OsSigEmptySet(&sigcb->sigwaitmask);//清空信號等待位,不等任何信號了.因為這是SIGKILL信號
}
}
return 0;
}
- 非
SIGKILL
信號,讓spcb
的所有任務執行SigProcessSignalHandler
函數
static int SigProcessSignalHandler(LosTaskCB *tcb, void *arg)
{
struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//先把參數解出來
int ret;
int isMember;
if (tcb == NULL) {
return 0;
}
/* If the default tcb is not setted, then set this one as default. */
if (!info->defaultTcb) {//如果沒有默認發送方的任務,即默認參數任務.
info->defaultTcb = tcb;
}
isMember = OsSigIsMember(&tcb->sig.sigwaitmask, info->sigInfo->si_signo);//任務是否在等待這個信號
if (isMember && (!info->awakenedTcb)) {//是在等待,并尚未向該任務時發送信號時
/* This means the task is waiting for this signal. Stop looking for it and use this tcb.
* The requirement is: if more than one task in this task group is waiting for the signal,
* then only one indeterminate task in the group will receive the signal.
*/
ret = OsTcbDispatch(tcb, info->sigInfo);//發送信號,注意這是給其他任務發送信號,tcb不是當前任務
OS_RETURN_IF(ret < 0, ret);//這種寫法很有意思
/* set this tcb as awakenedTcb */
info->awakenedTcb = tcb;
OS_RETURN_IF(info->receivedTcb != NULL, SIG_STOP_VISIT); /* Stop search */
}
/* Is this signal unblocked on this thread? */
isMember = OsSigIsMember(&tcb->sig.sigprocmask, info->sigInfo->si_signo);//任務是否屏蔽了這個信號
if ((!isMember) && (!info->receivedTcb) && (tcb != info->awakenedTcb)) {//沒有屏蔽,有喚醒任務沒有接收任務.
/* if unblockedTcb of this signal is not setted, then set it. */
if (!info->unblockedTcb) {
info->unblockedTcb = tcb;
}
ret = OsTcbDispatch(tcb, info->sigInfo);//向任務發送信號
OS_RETURN_IF(ret < 0, ret);
/* set this tcb as receivedTcb */
info->receivedTcb = tcb;//設置這個任務為接收任務
OS_RETURN_IF(info->awakenedTcb != NULL, SIG_STOP_VISIT); /* Stop search */
}
return 0; /* Keep searching */
}
解讀
- 函數的意思是,當進程中有多個任務在等待這個信號時,發送信號給第一個等待的任務
awakenedTcb
. - 如果沒有任務在等待信號,那就從不屏蔽這個信號的任務集中隨機找一個
receivedTcb
接受信號. - 只要不屏蔽
unblockedTcb
就有值,隨機的. - 如果上面的都不滿足,信號發送給
defaultTcb
. - 尋找發送任務的優先級是
awakenedTcb
>receivedTcb
>unblockedTcb
>defaultTcb
信號相關函數
信號集操作函數
- sigemptyset(sigset_t *set):信號集全部清0;
- sigfillset(sigset_t *set): 信號集全部置1,則信號集包含linux支持的64種信號;
- sigaddset(sigset_t *set, int signum):向信號集中加入signum信號;
- sigdelset(sigset_t *set, int signum):向信號集中刪除signum信號;
- sigismember(const sigset_t *set, int signum):判定信號signum是否存在信號集中。
信號阻塞函數
- sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); 不同how參數,實現不同功能
- SIG_BLOCK:將set指向信號集中的信號,添加到進程阻塞信號集;
- SIG_UNBLOCK:將set指向信號集中的信號,從進程阻塞信號集刪除;
- SIG_SETMASK:將set指向信號集中的信號,設置成進程阻塞信號集;
- sigpending(sigset_t *set)):獲取已發送到進程,卻被阻塞的所有信號;
- sigsuspend(const sigset_t *mask)):用mask代替進程的原有掩碼,并暫停進程執行,直到收到信號再恢復原有掩碼并繼續執行進程。
寫在最后
- 如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
- 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
- 關注小編,同時可以期待后續文章ing??,不定期分享原創知識。
- 想要獲取更多完整鴻蒙最新學習知識點,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu