Linxu:程序訊號:(訊號的產生方式)(訊號的註冊,阻塞遮蔽,登出,不同的處理方式)(重入函式)(volatile)(競態條件)
目錄
訊號的基本概念
通知事件的發生,並且事件比較緊急,會打斷當前的操作去處理事件,又因為這是所說的訊號
是針對程序,所以又稱為軟中斷
- 從一個簡單的場景說起
- 使用者輸入命令,在shell下啟動一個前臺程序
- 使用者按下這個ctrl + c 這個鍵盤輸入產生一個硬體中斷
- 如果CPU當前正在執行這個程式碼,則該程序的使用者空間程式碼暫停執行,CPU從使用者態切換到核心態處理硬體中斷
- 終端驅動程式將按鍵解釋為一個SIGINT訊號,記載該程序的PCB中
- 當某個時刻要從核心態返回到該程序的使用者空間程式碼繼續執行之前,首先處理PCB中的訊號,發現有一個訊號待處理,而這個訊號的預設處理動作是終止程序,所以直接終止程序而不再返回它的使用者空間執行程式碼
kill -l
//可以檢視系統定義的訊號列表
kill
//並不是為了殺死一個程序而設計的,而是為了給某一個指定程序傳送訊號
-
linux下有62個訊號,並且分了兩類,通過kill -l 檢視
-
訊號的功能:
實際上就是為了通知程序發生了那些事件,應該怎麼處理
訊號實際也可以歸為一類程序間通訊方式
-
1-31是非可靠訊號(非實時訊號)
1- 31號訊號時繼承於unix而來的,每一個訊號都對應了一個指定的事件
非可靠代表:這個訊號可能會丟失,如果有相同的訊號已經註冊到這個程序沒有
被處理,那麼接下來的相同型號就會被丟掉(只能被註冊一次) -
34-64是可靠訊號(實時訊號)
訊號不會丟失
-
訊號的生命週期:
訊號的產生——>訊號的註冊——>( 訊號的阻塞\遮蔽)——>訊號的登出——>訊號的處理
訊號的產生方式
- 硬體中斷
- 程式異常((段錯誤)記憶體訪問異常SIGSEG)
- 介面呼叫傳送訊號(命令產生),kill函式(如果不明確指定傳送訊號則傳送SIGTERM訊號,該訊號的預設處理為終止程序)
- 軟體條件產生:kill函式,raise函式(給自己傳送訊號),alarm函式(定時器函式),sigqueue函式
int kill(pid_t pid ,int sig);
向程序傳送指定訊號,
pid 程序id,用於指定傳送訊號到那個程序
sig訊號編碼:用於指定發什麼信
int raise(int sig) ;
給呼叫(自己)的程序或執行緒傳送訊號
sig:訊號編碼,用於傳送指定訊號
int sigqueue(pid_T pid, int sig ,union sigval value);
給指定的程序傳送指定的訊號,同時可以攜帶一個引數過去
pid 程序id,用於指定訊號傳送給那個程序
sig訊號編碼,用於指定傳送什麼訊號
value:要攜帶的數
union:聯合體(一個int,一個指標):同一個程序之間可以使用指標,不同程序使用int
unsigned int alarm(unsigned int seconds)
指定在seconds秒後傳送一個SIGALRM訊號到程序
可以返回上一個定時器的剩餘時間(定時器可以被替換)每次呼叫都會替換上一個
如果值為0,取消以前的定時器
產生訊號
SIGINT的預設處理動作是終止程序,SIGQOUIT的預設處理動作是終止程序並且Core Dump
Core Dump
當一個程序要異常終止時,可以選擇把程序的使用者空間記憶體資料全部儲存到磁碟上,
檔名通常是core,這叫做Core Dump(核心轉儲)。程序異常終止通常是因為有Bug,比如非法訪問
記憶體導致段錯誤,事後可以用偵錯程式檢查core檔案以查清楚錯誤原因。這個叫Post mortern Debug(事後除錯)
一個程序允許產生多大的core檔案取決於程序的Resource Limit(儲存在PCB中),
預設是不允許產生core檔案的,因為這裡包含了很多敏感資訊,可以使用ulimit命令改變限制
允許產生檔案最大為1024k
預設關閉,預設的core檔案大小為0,ulimit -c size
訊號的註冊
-
訊號註冊就是將資訊這個傳遞給程序,讓程序知道有這麼一個訊號,訊號是記錄在PCB中的
-
程序記錄一個訊號的時候是通過這個結構體的pending點陣圖來記錄的,點陣圖的位數+1就代表指定的訊號儲存位置
訊號的阻塞與遮蔽
阻塞:將訊號新增到blocked點陣圖,向程序備註說明這些訊號暫時不處理
在pcb中有一個pending的結構中儲存當前接收到的訊號,還有一個結構體blocked(點陣圖)用於儲存現在都有那些訊號要被阻塞(訊號的阻塞就是組織訊號的遞達(訊號處理))
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
訊號阻塞遮蔽驗證程式碼
//這是一個驗證訊號阻塞遮蔽的程式碼
//sigpromask
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
int main()
{
sigset_t mask;
sigemptyset(&mask);//清空集合資料,防止出現意外
sigaddset(&mask,SIGINT);//向集合新增指定訊號
// int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// how:對集合所做的操作
// SIG_BLOCK:對set集合中的訊號進行阻塞,oldset保留原有
// SIG_UNBLOCK:對集合中的訊號解除阻塞,oldset忽略
// SIG_SETMASK 將set集合中的訊號設定到blocked集合中
sigset_t oldmask;
sigemptyset(&oldmask);
sigprocmask(SIG_BLOCK,&mask,&oldmask);
while(1)
{
printf("this is porc mask!\n");
sleep(1);
}
return 0;
}
程式一開始就把第2位置為1,所以第2號訊號在block中記錄阻塞,執行時發現被阻塞不進行處理
所以ctrl + c 無法退出,這時來了一個ctrl \ (SIGQUIT)訊號,block中沒有置1,無阻塞所以直接終止
訊號的登出
從pending集合中將要處理的訊號移除,但是這個移除分情況
- 不管是註冊還是登出都區分了可靠訊號和非可靠訊號
- 非可靠訊號
註冊:非可靠訊號註冊的時候,是給sigqueue連結串列新增一個訊號節點,並且將pending集合對應的點陣圖置1,當這個訊號註冊的時候
如果點陣圖已經置1,代表訊號已經被註冊過了,因此不做任何操作,不會新增新的訊號節點
登出:刪除連結串列中的節點,並將對應的點陣圖置0 - 可靠訊號
註冊:可靠訊號註冊的時候,是給sigqueue連結串列新增一個訊號節點(不管這個訊號是否已經註冊),如果沒有註冊則新增新節點的同時更改對應的點陣圖置1
登出:刪除一個節點,然後檢視連結串列中還有沒有相同訊號的節點,如果還有,訊號對應的點陣圖組依然置1,如果沒有相同的節點,代表這個訊號已經全部被處理了,因此將對應的點陣圖置0 - 可靠訊號因為每次訊號到來都會新增新的節點,因此可靠訊號不會丟失
訊號的處理
每一個訊號實際都對應了某個時間,當程序收到了一個訊號,那麼就意味著現在有一個重要的事件需要處理
因此會打斷我們當前的操作,然後去處理這個事件
訊號處理還有一個名字:訊號的遞達
那麼程序到底什麼時候才會去檢測pending集合,看有沒有訊號需要處理呢?什麼時候處理訊號?
- 程序是在從核心態切換到使用者態的時候回去檢測一下是否有訊號需要被處理
訊號的處理方式
- 忽略處理方式:忽略和阻塞完全不同,一個被忽略的訊號來時直接被丟棄
SIG_IGN
- 預設處理方式:作業系統原有定義好的,對於一個訊號所對應事件的處理
SIG_DFL
- 自定義處理方式:提供一個訊號處理函式,要求核心在處理訊號時切換到使用者態執行這個處理函式
這種方式稱為捕捉一個訊號(void (*函式指標)(int signo))
訊號的忽略處理程式碼實現
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//訊號的處理方式測試
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
int main()
{
//2訊號的忽略處理
//sighandler_t signal(int signum, sighandler_t handler)
//用於修改一個訊號的處理方式
//signum:用於指定修改哪個的處理
//handler:用於指定處理方式(函式)
//SIG_IGN忽略處理
//SIG_DFL預設處理
//
//體現阻塞和忽略的區別:
//阻塞一個訊號:訊號依然會註冊在pending集合中
//忽略一個訊號:訊號來了就直接丟棄,不會被註冊
signal(SIGINT,SIG_IGN);//忽略中斷處理
getchar();//回車之後變成預設處理方式
signal(SIGINT,SIG_DFL);
while(1)
{
printf("xixi~~~\n");
sleep(1);
}
return 0;
}
開始中斷,也無法退出因為忽略退出訊號,回車之後變成預設處理方式,正常執行
訊號的自定義處理程式碼實現(sigcb)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void sigcb(int signo)
{
printf("recv sigo :%d\n",signo);
}
int main()
{
//訊號的自定義處理方式
//sigcb是使用者自己定義的訊號處理方式
//void sigcb(int signo)
signal(SIGINT,sigcb);
while(1)
{
printf("xixi~~~\n");
sleep(1);
}
return 0;
}
呼叫自定義的處理方式,列印訊號號碼不退出
訊號的自定義處理程式碼實現(sigaction推薦)
//訊號的處理方式測試
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void sigcb(int signo)
{
printf("recv sigo:%d\n",signo);
sleep(5);
}
int main()
{
// struct sigaction {
// 用於自定義處理方式
// void (*sa_handler)(int);
// 自定義處理方式,可以接受訊號攜帶的引數
// void (*sa_sigaction)(int, siginfo_t *, void *);
//處理訊號的時候,希望這個處理過程不被其他訊號到來打擾
// sa_mask 就是用於在處理訊號時要阻塞的訊號
// sigset_t sa_mask;
// 選項標誌,決定用哪一個成員函式作為訊號處理將介面
// int sa_flags; // 0-sa_handler SA_SIGINFO-sa_sigaction
// void (*sa_restorer)(void)
// };
//
// 訊號的自定義處理
//
//int sigaction(int signum, const struct sigaction *act,
// struct sigaction *oldact);
//signum:用於指定修改哪個訊號的處理動作
//act:給指定訊號要指定的處理動作
//oldact:用於儲存這個訊號原來的處理動作
struct sigaction n_act, o_act;
sigemptyset(&n_act.sa_mask);
//在處理訊號期間,不希望受到SIGQUIT影響
//因此在處理期間將sa_mask中的訊號全部(SIGQUIT)阻塞
sigaddset(&n_act.sa_mask,SIGQUIT);
n_act.sa_handler = sigcb;
n_act.sa_flags = 0;//0就是預設使用handler
//痛過sa_flags判斷呼叫那個函式(函式區別:有無攜帶引數)
sigaction(SIGINT,&n_act,&o_act);
while(1)
{
printf("xixi~~~\n");
sleep(1);
}
return 0;
}
自定義將中斷訊號放在sa_mask阻塞,程式正常迴圈,一旦接收到中斷訊號,執行函式列印訊號編號,暫停五秒,繼續迴圈
訊號的捕捉流程:
主要是針對自定義訊號處理方式
訊號不是立即處理的,會選擇一個合適的時機去處理
(從核心態轉變到使用者態)
如何從使用者態切換到核心態?
程式異常,系統呼叫,中斷
可重入函式不可重入函式
可重入函式
這個函式如果中間操作被打斷,在其他地方多次呼叫,並不會對執行結果造成影響
不可重入函式:一旦重入就會出問題
這個函式如果中間操作被打斷,在其他地方多次呼叫,會對執行結果造成影響,這類函式稱之為不可重入函式
(但凡函式中涉及到全域性資料的修改並且這個資料修改沒有被保護起來,這個函式就是不可重入函式)
可重入程式碼演示
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
//可重入函式
//這個函式呼叫的時候如果中間操作被打斷,在其他的地方有多次呼叫
//但是並不會對執行結果造成影響,那麼這種函式叫做可重入函式
//不可重入函式:一旦重入就會出現問題
//這個函式呼叫的時候如果中間操作被打斷,在其他地方有多次呼叫
//這些多次呼叫會對執行結果造成影響,那麼這類函式叫不可重入函式
//
//特點:操作了一些公共資料
int a = 10;
int b = 20;
int sum( )
{
printf("%d + %d\n",a + 1,b + 1);
a++;
sleep(5);
b++;
return (a + b);
}
void sigcb(int signo)
{
printf("sig sum = %d\n",sum());
return;
}
int main()
{
signal(SIGINT,sigcb);//修改一個訊號的處理方式
printf("sum = %d\n", sum());
while(1)
{
sleep(1);
}
return 0;
}
1:主函式中呼叫sum函式,列印a自加與b自加之和,然後死迴圈停止。
2:如果按 ctrl + c 發訊號,在sleep時打斷sum函式(通過signal修改指定的處理方式呼叫sum函式),只有a已經自加
這時訊號返回的求和時b還未自加,然後重入到打斷之前的sum函式,b在自加,列印b自加後的求和
可重入函式被打斷不會對執行結果造成影響
如果插入節點時:insert函式被不同的控制流呼叫,發生重會造成錯亂,可能只會成功的插入一個節點
這種函式為不可重入函式
如果一個函式滿足以下條件則是不可重入
- 呼叫了malloc或free,因為malloc也是用全域性連結串列管理堆的
- 呼叫了標準IO庫函式,因為很多庫函式的實現都是不可重入的方式使用的全域性資料結構
volatile保持記憶體可見性
每次處理這個被volatile修飾的變數時,都會從記憶體中重新載入變數的值到暫存器,因為程式做優化的時候,如果與個變數使用的頻率非常高,那麼這個變數有可能就會被優化為只向暫存器載入一次,往後直接使用暫存器中的保留值,而不關心這個變數記憶體裡邊的值,因此就有可能造成程式的邏輯錯誤。
簡而言之:指定是cpu每次操作一個被volatile修飾的變數的時候,都需要取記憶體重新載入變數資料
程式碼演示
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdint.h>
#include<signal.h>
uint64_t a = 1;
void sigcb(int so)
{
printf("recv :%d\n",so);
a = 0;
}
int main()
{
signal(SIGINT,sigcb);
while(a)
{}
return 0;
}
gcc -O1:編譯時優化
優化後會直接呼叫從記憶體中載入到暫存器的保留值,如果改變這個值下一次呼叫則會使用改變後的值。造成問題
如果不優化會一直使用記憶體中的值,即是暫存器中的值被修改,下一次載入記憶體的值也不會被影響
競態條件
如果我們的操作不是原子操作,意味著這個操作有可能被打斷,然後去做其他的事情,如果這時候做其他的事情有可能對我們的程式的整個邏輯或者函式的執行產生不良的影響。(當兩個執行緒競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件)
alarm(n);
pause();收到任何訊號都會打斷暫停,喚醒等待
雖然alarm緊接著下一行就是pause,但是我們無法保證pause一定會在alarm之後呼叫(因為可能出現優先順序更高的程序),如果我們寫的時序考慮不周密,就可能出現時序問題而導致錯誤
即是這樣,訊號依然可能在呼叫到pause之前的間隙遞達
所以“解除訊號遮蔽”和“掛起等待訊號”能合二為一變成原子操作就好了
這個正是sigsuspend函式的功能,函式包含了pause的掛起等待的功能,解決了競態條件的問題
對時序要求嚴格的場合都可以呼叫這個函式
int sigsuspend(const sigset_t *mask);
臨時使用mask中的訊號替換阻塞結合blocked中的訊號,然後進入阻塞等待,喚醒後還原
也就是說,臨時替換看一下訊號阻塞集合,然後進入休眠,當休眠被喚醒時,再將原來的
阻塞訊號替換回去
alarm(5)
pause()收到任何訊號都會打斷暫停,喚醒等待
SIGCHLD
在一個程序終止或者停止時,將SIGCHLD訊號傳送給其父程序。按系統預設將忽略此訊號。如果父程序希望被告知其子系統的這種狀態,則應捕捉此訊號。訊號的捕捉函式中通常呼叫wait函式以取得程序ID和其終止狀態。