Linux下的信號(一)----信號的基本概念與產生

一,信號的基本概念

1,什麼是信號?
日常生活中,當我們走到馬路上時,看到的綠燈是一種信號,它能提示我們怎樣安全的過馬路。又比如,新學期開始學校給每個班發的課表也是一種信號,它能提示同學們在適當的時間地點去上相應的課程而不是虛度光陰……生活中其實我們忽略了很多信號,正是由於這些信號的存在,才使得我們的生活方便而有序。
總結一下你會發現信號是什麼,信號就是當你看到它是知道它是什麼,並且知道看到信號之後應該做什麼,至於你遵不遵守就是你自己的事了, 計算機中的信號也不例外。

2,計算機中的信號
同日常生活中的信號一樣,計算機在收到信號之後,並不一定會立即處理它,它會將收到的信號記錄在其相應進程的PCB中的信號部分,等待合適的時間再去處理它。換句話說,一個進程是否收到信號,需要查看其進程PCB中的信號信息,給進程發信號實則是向進程PCB中寫入信號信息。同時,我們的操作系統是很智能的,當任何一個進程接收到任何一個信號時,操作系統會自動地知道各信號應作何處理。
查看系統定義的信號列表:kill -l
這裏寫圖片描述
注意列表中不是64個信號而是62個信號。
1-31:普通信號
34-64:實時信號
———-以下內容只針對普通信號進行講解———-
每個信號都有一個宏名稱和一個信號編號,這些宏定義可以在signal.h中找到,這些信號各自在什麼條件下產生,默認的處理動作是什麼,在signal(7)中都有詳細說明:
查看命令:man 7 signal
這裏寫圖片描述
各種信號產生的原因:
[http://blog.sina.com.cn/s/blog_7ee076050101bz5v.html]

3,普通信號的存儲:
試想一下,如果你是操作系統的設計人員,你會用什麼來存儲這31個信號呢?當然,設計人員都很聰明,他們採用的是位圖存儲,每一個bit位存儲一個相應信號,31個信號只需4個字節(僅一個整形的大小)即可存儲,bit位的位置表示信號的編號,bit位的值則用來表示是否收到該信號(0—-未收到該信號,1—-收到該信號)。
信號量的本質就是修改PCB中管理信號變量中的某個bit位。

4,信號產生的主要條件
背景知識:
前臺進程:基本不用和用戶交互,優先級稍微低一些(運行前臺進程: ./test.c)
後臺進程:需要和用戶進行交互,需要較高的相應速度,優先級別高(運行後臺進程: ./test.c &)

1〉用戶在終端按下某些組合鍵時,終端驅動程序會發送信號給前臺進程。例如:
Ctrl-C產生SIGINT信號(2號信號,終止前臺進程),
Ctrl-\產生SIGQUIT信號(3號信號,捕捉信號),
Ctrl-Z產生SIGTSTP信號(20號信號,可使前臺進程停止)。
2〉硬件異常產生信號,這些條件由硬件檢測到並通知內核,然後內核向當前進程發送適當的信號
例如:當前進程執行了除以0的指令,CPU的運算單元會產生異常,內核將這個異常解釋 爲SIGFPE信號發送給進程。再比如當前進程訪問了非法內存地址,,MMU(內存管理單元)會產生異常,內核將這個異常解釋SIGSEGV信號發送給進程。
3〉一個進程調用kill(2)函數可以發送信號給另一個進程
可以用kill(1)命令發送信號給某個進程,kill(1)命令也是調用kill(2)函數實現的,如果不明確指定信號則發送SIGTERM信號,該信號的默認處理動作是終止進程。當內核檢測到某種軟件條件發生時也可以通過信號通知進程,例如鬧鐘超時產生SIGALRM信號,向讀端已關閉的管道寫數據時產生SIGPIPE信號。 如果不想按默認動作處理信號,用戶程序可以調用sigaction(2)函數告訴內核如何處理某種信號。

5,處理信號的方法
1〉忽略此信號(大多數信號都可以使用該方法處理)。
特例:SIGKILL , SIGSTOP
原因:它們向超級用戶提供一種使進程終止或停止的方法,同時,如果忽略某些由硬件異常產生的信號,則進程的行爲是未定義的。
2〉執行該信浩的默認處理動作(與信號的種類有關,大多數信號會直接終止該進程)。
3〉用信號捕捉函數爲該信號指定自定義動作。
特例:不能捕捉SIGKILL , SIGSTOP信號
原因:爲了防止非法用戶的惡意入侵使得進程永遠殺不掉。
信號捕捉函數:修改信號的默認動作,有些信號是不能被捕捉的,如9號信號SIGKILL等。

#include <signal.h>

       typedef void (*sighandler_t)(int);

       sighandler_t signal(int signum, sighandler_t handler);

參數:signum:信號編號 handler:指向怎樣捕捉該信號的函數
返回值:signal函數的返回值是一個函數指針,成功返回返回以前的處理 配置,失敗返回錯誤碼對應的錯誤提示。
makefile:

 signal:signal.c
  2     gcc -o signal signal.c
  3 .PHONY:clean
  4 clean:
  5     rm -f signal

signal.c:

#include<unistd.h>
#include<stdio.h>
#include<signal.h>

void myhandler(int signo)
{
    printf("got SIGINT\n");
}
int main()
{
   signal(2,myhandler);//捕捉2號信號SIGINT:Ctrl C
   while(1)
   {
       sleep(1);
   }
   return 0;
}

運行結果:
捕捉2號信號:
這裏寫圖片描述

殺死進程(此時ctrl C不再能夠結束進程):
這裏寫圖片描述

進程結束:
這裏寫圖片描述

改進:signal.c

#include<unistd.h>
#include<stdio.h>
#include<signal.h>

typedef void (*sighandler_t)(int);//函數指針
sighandler_t old_handler = NULL;

void myhandler(int signo)
{
      printf("got SIGINT\n");//第一次按ctrlC捕捉2號信號SIGINT
      signal(2, old_handler);//恢復默認的處理,再按ctrl C的話,就會終止程序
}
int main()
{
   old_handler = signal(2,myhandler);//捕捉2號信號SIGINT:Ctrl C
   while(1)
   {
       sleep(1);
   }
   return 0;
}

運行結果:
這裏寫圖片描述

二,產生信號

1,通過終端按鍵產生信號
上面說過,SIGINT(ctrl C)的默認處理動作是終止進程,SIGQUIT(ctrl )的默認處理動作是終止進程並且Core Dump,那麼什麼是Core Dump呢?
Core Dump:核心轉儲。當一個進程要異常終止時,可以選擇把進程的用戶空間內存數據全部保存到磁盤上,文件名通常是core,這叫做Core Dump。
進程異常終止通常是因爲有Bug,比如非法內存訪問導致段錯誤,事後可以用調試器檢查core文件以查清錯誤原因,這叫做Post-mortem Debug(事後調試)。一個進程允許產生多大的core文件取決於進程的Resource Limit(這個信息保存 在PCB中)。默認是不允許產生core文件的,因爲core文件中可能包含用戶密碼等敏感信息,不安全。在開發調試階段可以用ulimit命令改變這個限制,允許產生core文件。
代碼驗證:
1>首先用ulimit命令改變Shell進程的Resource Limit,允許core文件最大爲1024K:ulimit -c 1024
這裏寫圖片描述
2>然後寫一個死循環程序:
makefile:

   .PHONY: all
   all:signal sig
   signal:signal.c
       gcc -o $@ $^
   sig:sig.c
       gcc -o $@ $^
   .PHONY:clean
   clean:
       rm -f signal sig

sig.c:

 #include<stdio.h>
 #include<sys/types.h>
 #include<unistd.h>

 int main()
 {
     printf("pid is %d\n",getpid());
     while(1);
     return 0;
 }

運行結果:
這裏寫圖片描述
ulimit命令改變了Shell進程的Resource Limit,test進程的PCB由Shell進程複製而來,所以也具 有和Shell進程相同的Resource Limit值,這樣就可以產生Core Dump了。

2,軟硬件異常產生信號
軟硬件異常信號 其他信號 SIGCHLD or SIGCLD 子進程結束時, 父進程會收到這個信號。 如果父進程沒有處理這個信號,也沒有等待(wait)子進程,子進程雖然終止,但是還會在內核進程表中佔有表項,這時的子進程稱爲殭屍進程。這種情 況我們應該避免(父進程或者忽略SIGCHILD信號,或者捕捉它,或者wait它派生的子進程,或者父進程先終止,這時子進程的終止自動由init進程 來接管)。

3,調用系統函數想進程發信號
1〉首先在後臺執行死循環程序,然後用kill命令給它發SIGSEGV信號。
這裏寫圖片描述
3603是sig進程的id。之所以要再次回車才顯示“段錯誤”是因爲在3603程終止掉 之前已經回到了Shell提示符等待用戶輸入下一條令,Shell不希望“段錯誤”信息和用戶的輸入交錯在一起,所以等用戶輸入命令之後才顯示。
指定某種信號的kill命令可以有多種寫 法,上面的命令還可以寫成:
kill -SIGSEGV 3603或kill -11 3603, 11是信號SIGSEGV的編號。
以往遇 到的段錯誤都是由非法內存訪問產生的,而這個程序本身沒錯,給它發SIGSEGV也能產生段錯誤。

kill命令是調用kill函數實現的。kill函數可以給一個指定的進程發送指定的信號。
raise函數可以給當前進程發送指定的信號(自己給自己發信號)。

#include <signal.h>
int kill(pid_t pid, int signo);//給任意進程發信號
int raise(int signo);//自己給自己發任意信號

這兩個函數都是成功返回0,錯誤返回-1。

kill函數實例:
makefile:

  1 .PHONY: all
  2 all: signal sig kill
  3 signal:signal.c
  4     gcc -o $@ $^
  5 sig:sig.c
  6     gcc -o $@ $^
  7 kill:kill.c
  8     gcc -o $@ $^
  9 .PHONY:clean
 10 clean:
 11     rm -f signal sig kill

kill.c:

  7 #include<stdio.h>
  8 #include<signal.h>
  9 #include<stdlib.h>
 10 
 11 static void usage(const char* proc)//.kill用法
 12 {
 13     printf("usage:%s sig pid\n",proc);
 14 }
 15 int main(int argc,char* argv[])
 16 {
 17     if(argc != 3)
 18     {
 19         usage(argv[0]);
 20         return 1;
 21     }
 22     int pid = atoi(argv[2]);//進程號
 23     int sig = atoi(argv[1]);//信號編號
 24     kill(pid,sig);//用當前進程給pid號進程發送sig號信號
 25     return 0;
 26 }

1〉首先在前臺運行上面的死循環程序
這裏寫圖片描述

2〉使用kill函數向死循環進程發送2號信號終止該進程
這裏寫圖片描述

這裏寫圖片描述

raise函數的實例:
makefile:

  1 .PHONY: all
  2 all: signal sig kill raise
  3 signal:signal.c
  4     gcc -o $@ $^
  5 sig:sig.c
  6     gcc -o $@ $^
  7 kill:kill.c
  8     gcc -o $@ $^
  9 raise:raise.c
 10     gcc -o $@ $^
 11 .PHONY:clean
 12 clean:
 13     rm -f signal sig kill raise

raise.c:

  7 #include<stdio.h>
  8 #include<signal.h>
  9 #include<stdlib.h>
 10 #include<unistd.h>
 11 
 12 void myhandler()
 13 {
 14     printf("this is a raise test:pid is %d\n",getpid());
 15 }
 16 int main()
 17 {
 18     signal(2,myhandler);
 19     while(1)
 20     {
 21         raise(2);//給自己發2號信號
 22         sleep(1);
 23     }
 24     return 0;
 25 }

運行中用kill命令殺死該進程:kill -9 4009
這裏寫圖片描述

abort函數使當前進程接收到信號而異常終止。

#include <stdlib.h>
void abort(void);//自己給自己發終止信號

就像exit函數一樣,abort函數總是會成功的,所以沒有返回值。
makefile:

  1 .PHONY: all
  2 all: signal sig kill raise abort
  3 signal:signal.c
  4     gcc -o $@ $^
  5 sig:sig.c
  6     gcc -o $@ $^
  7 kill:kill.c
  8     gcc -o $@ $^
  9 raise:raise.c
 10     gcc -o $@ $^
 11 abort:abort.c
 12     gcc -o $@ $^
 13 .PHONY:clean
 14 clean:
 15     rm -f signal sig kill raise abort

abort.c:

  7 #include<stdio.h>
  8 #include<unistd.h>
  9 #include<stdlib.h>
 10 #include<signal.h>
 11 
 12 int count = 0;
 13 void myhandler(int sig)
 14 {
 15     printf("count is %d,sig is:%d\n",count++,sig);
 16 }
 17 int main()
 18 {
 19     for(int i = 1; i <= 31; ++i)
 20     {
 21         signal(i,myhandler);
 22     }
 23     while(1)
 24     {
 25         sleep(1);
 26         abort();//使當前進程接收到信號而異常止
 27     }
 28     return 0;
 29 }

這裏寫圖片描述

4,由軟件條件產生信號
由軟件條件產生的信號:SIGPIPE,SIGALRM(主要介紹)
alarm函數可以設定一個鬧鐘,也就是告訴內核在seconds秒之後給當前進程發SIGALRM信號, 該信號的默認處理動作是終止當前進程。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

返回值:是0或者是以前設定的鬧鐘時間還餘下 的秒數
如果seconds值爲0,表示取消以前設定的鬧鐘,函數的返回值仍然是以前設定的鬧鐘時間還餘下的秒數。
makefile:

  1 .PHONY: all
  2 all: signal sig kill raise abort alarm
  3 signal:signal.c
  4     gcc -o $@ $^
  5 sig:sig.c
  6     gcc -o $@ $^
  7 kill:kill.c
  8     gcc -o $@ $^
  9 raise:raise.c
 10     gcc -o $@ $^
 11 abort:abort.c
 12     gcc -o $@ $^
 13 alarm:alarm.c
 14     gcc -o $@ $^
 15 .PHONY:clean
 16 clean:
 17     rm -f signal sig kill raise abort alarm

alarm.c:

  7 #include<stdio.h>
  8 #include<signal.h>
  9 #include<stdlib.h>
 10 #include<unistd.h>
 11 
 12 int count = 0;
 13 void myhandler()
 14 {
 15     printf("count is %d\n",count);
 16     exit(1);
 17 }
 18 int main()
 19 {
 20     signal(SIGALRM,myhandler);
 21     alarm(1);//設置鬧鐘
 22     while(1)
 23     {
 24         count++;
 25     }
 26     return 0;
 27 }

這個程序的作用是1秒鐘之內不停地數數,1秒鐘到了就被SIGALRM信號終止。
這裏寫圖片描述

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