目錄
一、信號概念
二、信號的產生
三、信號的保存
四、信號集函式
五、捕捉信號
六、可重入函式和volatile 關鍵字
七、SIGCHLD信號
一、信號概念
信號是行程之間事件異步通知的一種方式,屬于軟體中斷,

1、Ctrl+c結束前臺行程
當我們在shell中啟動一個前臺行程時,使用Ctrl+c可以結束這個前臺行程,用戶按下Ctrl-C時鍵盤輸入產生一個硬體中斷,被OS獲取,解釋成信號,發送給目標前臺行程,前臺行程因為收到信號,進而引起行程退出,
注意:
1)ctrl+c產生的信號只能發送給前臺行程,一個命令后+&可以放到后臺運行,后臺運行的行程不能使用Ctrl+c終止同時后臺行程shell不進行等待,可以接收其他命令,
2)shell可以同時運行一個前臺行程一個或多個后臺行程,
3)使用Ctrl+c時作業系統會向前臺行程發送一個SIGINT信號(2號信號),
2、kill -l命令查看系統中定義的信號

注意:
1)每個信號都有一個編號和一個宏名稱,使用時兩者都可以,
2)1~31號信號是普通信號,34~64是實時信號(不進行討論),
3)沒有32、33號信號,因此一共有62種信號,
3、信號處理的三種方式
1)忽略信號
2)使用默認處理動作處理
3)自定義處理信號,自定義一個處理函式,當收到該信號時使用該函式對信號進行處理,也成為捕捉一個信號,
4、信號的其他概念
1)實際信號執行的處理動作稱為信號的遞達,
2)信號從產生到遞達之間的程序稱為信號未決,
3)行程可以選擇阻塞某個信號,被阻塞的信號當產生時將保持在未決狀態,直到行程接觸對該信號的阻塞才進行遞達,
注意:信號的阻塞和忽略不是相同的概念,阻塞的信號處于未決狀態當解除阻塞后還會在執行;忽略是遞達的一種方式,
二、信號的產生
1、終端按鍵產生
使用Ctrl+c向前臺行程發送一個SIGINT信號的方式實際上就是通過終端案件產生信號,
SIGINT信號的默認處理動作是終止行程,SIGQUIT信號的默認處理動作是終止行程并且core dump.
1)Core Dump ---核心轉儲
core Dump:當一個行程例外終止時,可以選擇把用戶空間記憶體資料全部保存到磁盤上,檔案名為core,這就稱為Core Dump,
生成core檔案:一個行程終止通常是程式中存在錯誤,例如陣列越界等,當行程終止后我們可以查看core檔案資訊查清錯誤原因(事后除錯),但是在默認情況下時不允許產生core檔案的(存在安全問題等),如果需要產生可以使用ulimit命令改變shell行程的Resource Limit,例如:ulimit -c 1024表示允許產生大小為1024k的core檔案,

使用ulimit -c命令更改core檔案大小

當運行一個存在除0錯誤的程式時,當程式運行起來后作業系統檢測到除0錯誤會發送一個SIGFPE信號,此時在當前目錄下還會產生一個core檔案,在gdb除錯時可以使用core-file查看具體錯誤位置,
2、通過系統呼叫產生
1)kill命令
使用:kill -信號 行程 ---給指定行程發送指定信號
例如:kill -2 2148 ---給2148行程發送二號信號 kill -SIGINT 2148也表示給2148行程發送二號信號
注意:使用kill命令給行程發送信號,實際上作業系統是通過呼叫kill函式發送的信號,
kill函式:int kill(pid_t pid, int sig); //給指定pid的行程發送指定信號sig
引數:pid接收信號的行程id,sig表示信號
回傳值:成功回傳0,失敗回傳-1
2)raise函式
int raise(int sig);//給當前行程發送指定信號
引數:發送的信號
回傳值:成功回傳0,失敗回傳非0.
3)abort函式
void abort();//使當前函式接收到例外信號而終止
注意:abort函式就想exit一樣,總是會成功的,因此沒有回傳值,
3、軟體條件產生
使用pipe創建管道是,發生例外會產生一個SIGPIPE信號,SIGPIPE的產生屬于軟體條件產生,呼叫alarm函式也可以產生信號SIGALRM,這也屬于軟體條件產生,
unsigned int alarm(unsigned int seconds); //在seconds秒之后向該行程發送一個SIGALRM信號,該信號的默認處理動作時終止行程,
引數:等待的時間,單位為秒,
回傳值:回傳剩余的秒數或0
#include<stdio.h>
#include<unistd.h>
int main()
{
unsigned int sec = alarm(5);
while(1)
{
printf("i am runing\n");
sleep(1);
}
return 0;
}
4、硬體產生
硬體例外被硬體以某種方式被硬體檢測到并通知內核,然后內核向當前行程發送適當的信號,例如當前行程執行了除以0的指令,CPU的運算單元會產生例外,內核將這個例外解釋 為SIGFPE信號發送給行程,再比如當前行程訪問了非法記憶體地址,,MMU會產生例外,內核將這個例外解釋為SIGSEGV信號發送給行程,
5、簡單理解信號的捕捉
使用signal函式可以通過自定義的方式去處理一個信號,
typedef void (*sighandler_t)(int);//函式指標,引數為int,回傳值為void的函式,
sighandler_t signal(int signum, sighandler_t handler);//自定義處理信號
引數:signum表示信號,handler表示自定義處理函式的地址,
回傳值:
#include<stdio.h>
#include<signal.h>
void sigcb(int sig)
{
//當從鍵盤輸入ctrl+c時會自動列印這句話
printf("catch a sig: %d\n",sig);
}
int main()
{
//將2號信號的處理方式改為自定義處理方式
signal(SIGINT,sigcb);
while(1);
return 0;
}
三、信號的保存
1、從系統的層面理解信號的保存
信號是作業系統發送給行程的,因此行程應該要知道發送的是什么信號以及是否接收到該信號,從作業系統的層面我們可以理解在行程的task_struct結構體中,有一個保存信號的sigbitmap,其中sigbitmap的每一個位元位的位置表示的是什么信號,位元位的內容表示是否收到該信號(為0表示沒有收到,為1表示收到),當行程收到作業系統發來的信號時,行程會將sigbitmap中的相應的位元位置為1,表示收到了該信號,

因此,作業系統給行程發送信號實際上是給行程“寫信號”,
2、從內核的層面理解信號的保存
在內核中是通過pending、block、handler表來保存信號的資訊的,其中,block表示信號的阻塞,pending表表示信號的未決,它們的表示方式也是位圖方式;而handler表中存盤的是一個函式指標,表示的是信號的處理動作,

一個信號產生時,內核在行程控制塊中設定該信號的未決狀態(pending表)為1,直到信號遞達才清除該標志,如果同時還給該信號設定了了阻塞和忽略,即在該信號的block表中設定了1且handler中設定了SIG_IGN,當接收到該信號時在對該信號解除阻塞之前也不能忽略該信號,
當一個信號被設定為阻塞時,該信號未產生過,一旦產生該信號將被阻塞,且將它的處理動作設定為用戶自定義函式sighandler,如果在行程解除對某信號的阻塞之前這種信號產生過多次,但是只能記一次(這里只考慮常規信號),因為在內核中一個行程只有一張表用來保存信號的未決狀態,
四、信號集函式
1、sigset_t
對于普通信號(非實時性信號)來說,每個信號只有一個位元位來表示未決狀態,并不記錄該信號產生了多少次,阻塞標志也是如此,因此,在內核中阻塞和未決使用相同的資料型別sig_set來存盤,sig_set稱為信號集,這個型別可以表示信號的有效和無效狀態,在阻塞信號中有效和無效表示的是是否被阻塞,在未決信號中有效和無效表示的是是否處于未決狀態,阻塞信號集也稱為當前行程的屏蔽字,
2、操作sigset_t變數的函式
1)int sigismember(const sigset_t *set, int signo); //sigismember是一個布爾函式,用于判斷一個信號集的有效信號中是否包含某種信號,若包含則回傳1,不包含則回傳0,出錯回傳-1,
2)int sigemptyset(sigset_t *set); //初始化set所指向的信號集,使其中所有信號的對應bit清零,表示該信號集不包含任何有效信號,
#include<stdio.h>
#include<signal.h>
void printSig(sigset_t* st)
{
int i = 1;
for(i = 1;i < 32;i++)
{
//使用sigismember判斷該信號集中的有效信號是否包含i號信號
if(sigismember(st, i) == 0)
printf("0");
else
printf("1");
}
printf("\n");
}
int main()
{
sigset_t st;
//初始化信號集
sigemptyset(&st);
//列印信號集
printSig(&st);
return 0;
}
3)int sigfillset((sigset_t *set);//初始化set所指向的信號集,使其中所有信號的對應bit置1,表示該信號集的有效信號包括系統支持的所有信號,
#include<stdio.h>
#include<signal.h>
void printSig(sigset_t* st)
{
int i = 1;
for(i = 1;i < 32;i++)
{
//使用sigismember判斷該信號集中的有效信號是否包含i號信號
if(sigismember(st, i) == 0)
printf("0");
else
printf("1");
}
printf("\n");
}
int main()
{
sigset_t st;
//初始化信號集
sigfillset(&st);
//列印信號集
printSig(&st);
return 0;
}
4)int sigaddset(sigset_t *set, int signo);//在該信號集中添加有效信號
5)int sigdelset(sigset_t *set, int signo);//在該信號集中洗掉有效信號
#include<stdio.h>
#include<signal.h>
void printSig(sigset_t* st)
{
int i = 1;
for(i = 1;i < 32;i++)
{
//使用sigismember判斷該信號集中的有效信號是否包含i號信號
if(sigismember(st, i) == 0)
printf("0");
else
printf("1");
}
printf("\n");
}
int main()
{
sigset_t st;
//初始化信號集
sigemptyset(&st);
//列印信號集
printSig(&st);
//向該信號集中添加2號信號
sigaddset(&st,2);
printSig(&st);
//洗掉該信號集中的2號信號
sigdelset(&st,2);
printSig(&st);
return 0;
}
注意:除了sigismember函式外,其余四個函式的回傳值相同,成功回傳0失敗回傳-1;在使用sigset_t型別之前,一定要對信號集進行sigemptyset或者sigfillset做初始化,使信號集處于確定狀態,
3、sigprocmask函式
int sigprocmask(int how,sigset_t* set,sigset_t* oset);//讀取或更改行程的信號屏蔽字,
回傳值:成功回傳0失敗回傳-1
函式說明:如果oset是非空指標,則讀取行程的當前信號屏蔽字通過oset引數傳出,如果set是非空指標,則更改行程的信號屏蔽字,引數how指示如何更改,如果oset和set都是非空指標,則先將原來的信號屏蔽字備份到oset里,然后根據set和how引數更改信號屏蔽字,假設當前行程的信號屏蔽字為mask,下表說明了how引數的可選值,

注意:如果呼叫sigprocmask解除了對當前若干個未決信號的阻塞,則在sigprocmask回傳前,至少將其中一個信號遞達,
#include<stdio.h>
#include<signal.h>
int main()
{
sigset_t set;
sigset_t oset;
//初始化set為empty
sigemptyset(&set);
//向set中添加2號信號
sigaddset(&set,2);
//使用sigprocmask阻塞二號信號
sigprocmask(SIG_BLOCK,&set,&oset);
while(1);
return 0;
}
4、sigpending函式
int sigpending(sigset_t *set);//讀取當前行程的未決信號集,通過set引數傳出
回傳值:成功回傳0,失敗回傳-1
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void printset(sigset_t* set)
{
int i = 1;
for(;i < 32;i++)
{
if(sigismember(set,i))
printf("1");
else
printf("0");
}
printf("\n");
}
int main()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set,2);
//阻塞二號信號
sigprocmask(SIG_BLOCK,&set,NULL);
int i = 0;
sigemptyset(&set);
for(;i < 10;i++)
{
sigpending(&set);
printset(&set);
sleep(1);
}
return 0;
}
SIG_INT信號被阻塞,當使用ctrl+c時未決信號二號位為1

五、捕捉信號
作業系統是行程的管理者,因此無論通過那種方式產生的信號要想發送給行程都需要經過作業系統,也就是說,一個信號從產生到遞達一定要經過從用戶態->內核態的切換,而信號的遞達就是發生在從內核態切換到用戶態時進行的,
1、內核如何實作信號的捕捉
1)用戶態和內核態的切換
程式在運行時經常需要在用戶態和內核態之間來回切換,因為用戶態沒有權限執行內核態的程式,只有切換到內核態才能執行,如何切換?
在32位系統下,程式地址空間為4G,其中1G是內核空間,其余3G是用戶空間;內核空間和用戶空間有各自獨立的頁表,當需要執行內核的代碼時CPU會通過內核頁表找到相應的地址,(CPU的暫存器會記錄當前是用戶態還是內核態)

2)內核對信號的捕捉

如果信號的處理動作是用戶自定義函式,在信號遞達時就呼叫這個函式,這稱為捕捉信號,由于信號處理函式的代碼是在用戶空間的,處理程序比較復雜,舉例如下: 用戶程式注冊了SIGQUIT信號的處理函式sighandler, 當前正在執行main函式,這時發生中斷或例外切換到內核態, 在中斷處理完畢后要回傳用戶態的main函式之前檢查到有信號SIGQUIT遞達, 內核決定回傳用戶態后不是恢復main函式的背景關系繼續執行,而是執行sighandler函 數,sighandler和main函式使用不同的堆疊空間,它們之間不存在呼叫和被呼叫的關系,是兩個獨立的控制流程, sighandler函式回傳后自動執行特殊的系統呼叫sigreturn再次進入內核態, 如果沒有新的信號要遞達,這次再回傳用戶態就是恢復main函式的背景關系繼續執行了,
2、sigaction函式
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); //讀取和修改與指定信號相關聯的處理動作,
引數:signo 是指定信號的編號,若act指標非空,則根據act修改該信號的處理動作,若oact指標非空,則通過oact傳出該信號原來的處理動作,
回傳值:成功回傳0,失敗回傳-1
1)struct sigaction結構體

將sa_handler賦值為常數SIG_IGN傳給sigaction表示忽略信號,賦值為常數SIG_DFL表示執行系統默認動作,賦值為一個函式指標表示用自定義函式捕捉信號,或者說向內核注冊了一個信號處理函式,該函式回傳值為void,可以帶一個int引數,通過引數可以得知當前信號的編號,這樣就可以用同一個函式處理多種信號,顯然,這也是一個回呼函式,不是被main函式呼叫,而是被系統所呼叫,
六、可重入函式和volatile 關鍵字
1、可重入函式
1)重入函式
當一個函式的主執行流正在向鏈表中插入一個節點時,向該行程發送一個信號,并且該信號的處理方式為自定義,如下:

像這樣的,insert函式被不同的執行流呼叫,有可能第一個執行流還沒有執行完就去執行第二個執行流,insert函式就稱為重入函式,
2)可重入函式
但是上面程式存在一個問題,當程式運行結束解構式釋放鏈表空間時,導致由自定義處理函式插入的鏈表節點沒有辦法釋放造成記憶體泄露問題,因此,該函式稱為不可重入函式,
如果一個函式只訪問自己的區域變數或引數,稱為可重入函式(當重入時不會對程式造成破壞),
3)常見的不可重入函式
-
呼叫了 malloc 或 free, 因為 malloc 也是用全域鏈表來管理堆的,
-
呼叫了標準I/O 庫函式,標準 I/O 庫的很多實作都以不可重入的方式使用全域資料結構,
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
int a = 10;
void handler(int sig)
{
printf("catch a sig :%d\n",sig);
a = 0;
}
int main()
{
signal(2,handler);
printf("%d\n",a);
while(a);
return 0;
}
上面代碼在執行時,當向行程發送一個2號信號時去執行sighandler函式,將a修改成0,回圈應該結束,但是并沒有結束,這是因為,CPU將a保存在暫存器中,而修改的a在記憶體中,因此程式并沒有立即結束,
當我們不需要CPU進行這種優化時,就可以使用volatile關鍵字修飾該變數,使得每次看到的值都是記憶體中最新的值,
七、SIGCHLD信號
在行程中,當子行程退出父行程沒有進行行程等待時子行程會成為僵尸行程,如果使用waitpid函式進行非阻塞等待則父行程可以邊執行自己的程式邊觀察子行程是否退出,但是這樣程式實作復雜,但是如果阻塞等待則父行程不能執行自己的代碼直到子行程退出后對子行程處理完才可以,
實際上子行程在退出時會給父行程發送一個SIGCHILD信號,我們可以使用自定義處理該信號,在該信號的處理函式中呼叫waitpid函式對子行程進行處理即可,這樣,父行程便不再等待子行程還可以“專心”的干自己的事情,
#include<stdio.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
void sighandler(int sig)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD,sighandler);
pid_t id = fork();
if(id == 0)
{
printf("i am child,id : %d\n",getpid());
exit(1);
}
else
{
while(1);
}
return 0;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275562.html
標籤:其他
