1. 程式人生 > 其它 >《Unix/Linux系統程式設計》第六章學習筆記

《Unix/Linux系統程式設計》第六章學習筆記

第六章 訊號和訊號處理

知識點總結

本章講述了訊號和訊號處理;介紹了訊號和中斷的統一處理,有助於從正確的角度看待訊號;將訊號視為程序中斷,將程序從正常執行轉移到訊號處理;解釋了訊號的來源,包括來自硬體、異常和其他程序的訊號;然後舉例說明了訊號在Unix/Linux 中的常見用法;詳細解釋了Unix/Linux中的訊號處理,包括訊號型別、訊號向量位、訊號掩碼位、程序PROC結構體中的訊號處理程式以及訊號處理步驟;用示例展示瞭如何安裝訊號捕捉器來處理程式異常,如使用者模式下的段錯誤;還討論了將訊號用作程序間通訊(IPC)機制的適用性。讀者可藉助該程式設計專案,使用訊號和管道來實現用於程序交換資訊的程序間通訊機制。

訊號和中斷

  • “中斷”是從I/O裝置或協處理器傳送到CPU的外部請求,它將CPU從正常執行轉移 到中斷處理。與傳送給CPU的中斷請求一樣,“訊號”是傳送給程序的請求,將程序從正常執行轉移到中斷處理。
  • 程序:一個“程序”就是一系列活動。廣義的 “程序”包括:從事日常事務的人。在使用者模式或核心模式下執行的Unix/Linux程序。執行機器指令的CPU。
    “中斷”是傳送給“程序”的事件,它將“程序”從正常活動轉移到其他活動,稱為“中斷處理”。“程序”可在完成“中斷”處理後恢復正常活動。
    根據來源,中斷可分為三類:來自硬體的中斷;來自其他人的中斷;自己造成的中斷
    按照緊急程度,中斷可分為以下幾類:不可遮蔽(NMI);可遮蔽。
  • 程序中斷
    這類中斷是傳送給程序的中斷。當某程序正在執行時,可能會收到來自3個不同來源的中斷:
    來自硬體的中斷:終端、間隔定時器的“Ctrl+C”組合鍵等。
    來自其他程序的中斷:kill(pid,SIG#), death_of_child等。
    自己造成的中斷:除以0、無效地址等。
    每個程序中斷都被轉換為一個唯一ID號,傳送給程序。與多種類的人員中斷不同,我們始終可限制在一個程序中的中斷的數量。Unix/Linux中的程序中斷稱為訊號,編號為1到31。程序的PROC結構體中有對應每個訊號的動作函式,程序可在收到訊號後執行該動作函式。與人員類似,程序也可遮蔽某些型別的訊號,以推遲處理。必要時,程序還可能會修改訊號動作函式。
  • 硬體中斷:
    這類中斷是傳送給處理器或CPU的訊號。它們也有三個可能的來源:
    來自硬體的中斷:定時器、I/O裝置等.
    來自其他處理器的中斷:FFP. DMA、多處理器系統中的其他CPU。
    自己造成的中斷:除以0、保護錯誤、INT指令。
    毎箇中斷都有唯一的中斷向量號。動作函式是中斷向量表中的中斷處理程式。
  • 程序的陷阱錯誤
    程序可能會自己造成中斷。這些中斷是由被CPU識別為異常的錯誤引起的,例如除以0、無效地址、非法指令、越權等。當程序遇到異常時,它會陷入作業系統核心,將陷阱原因轉換為訊號編號,並將訊號傳送給自己。如果在使用者模式下發生異常,則程序的預設操作是終止,並使用一個可選的記憶體轉儲進行除錯。

Unix/Linux中的訊號處理

Unix/Linux支援31種不同的訊號,每種訊號在 signal.h檔案中都有定義。

#define SIGHUP
#define SIGINT
#define SIGQUIT
#define SIGILL #define SIGTRAP
#define SIGABRT #define SIGIOT
#define SIGBUS
#define SIGFPE
#define SIGKILL
#define SIGUSR1
#define SIGSEGV
#define SIGUSR2
#define SIGPIPE #define SIGALRM
#define SIGTERM
#define SIGSTKFLT
#define SIGCHLD
#define SIGCONT
#define SIGSTOP
#define SIGTSTP
#define SIGTTIN
#define SIGTTOU
#define SIGURG
#define SIGXCPU
#define SIGXFSZ
#define SIGVTALRM
#define SIGPROF
#define SIGWINCH
#define SIGPOLL
#define SIGPWR
#define SIGSYS

Unix/Linux訊號示例

(1)按“Ctrl+C”組合鍵通常會導致當前執行的程序終止。原因如下:
“Ctr1+C”組合鍵會生成一個鍵盤硬體中斷。鍵盤中斷處理程式將“Ctrl+C”組合鍵轉換為SIGINT(2)訊號,傳送給終端上的所有程序,並喚醒等待鍵盤輸人的程序。在核心模式下,每個程序都要檢查和處理未完成的訊號。程序對大多數訊號的預設操作是呼叫核心的kexit(exitValue)函式來終止。在Linux中,exitValue的低位位元組是導致程序終止的訊號編號。
(2)使用者可使用nohup a.out &命令在後臺執行一個程式。即使在使用者退出後,程序仍將繼續執行。nobup命令會使sh像往常一樣復刻子程序來執行程式,但是子程序會忽略SIGHuP(1)訊號。當用戶退出時,sh會向與終端有關的所有程序傳送一個SIGHUP訊號。後臺程序在接收到這一訊號後,會忽略它並繼續執行。為防止後臺程序使用終端進行I/O,後臺程序通常會斷開與終端的連線(通過將其檔案描述符0、1、2重定向到/dev/null),使其完全不受任何面向終端訊號的影響。
(3) 使用者可以使用sh命令killpid(orkill-s9pia)殺死該程。方法如下。執行殺死的程序向pid標識的目標程序傳送一個SIGTERM ( 15 )訊號,請求它死亡。目標程序將會遵從請求並終止。如果程序選擇忽略SIGTERM訊號,它可能拒絕死亡。

訊號的來源

來自硬體中斷的訊號:在執行過程中,一些硬體中斷被轉換為訊號傳送給程序硬體訊號示例

中斷鍵(Ctrl+C),它產生一個SIGINT(2)訊號。
間隔定時器,當他的時間到期時,會生成一個SIGALRM(14)、SIGTALRM(26)或SIGPROF(27)訊號。
其他硬體錯誤,如匯流排錯誤、IO陷進
來自異常的訊號:常見的陷阱訊號有SIGFPE(8),表示浮點異常(除以0),最常見也是最可怕的時SIGSEGV(11),表示段錯誤

來自其他程序的訊號:程序可以使用kill(pid,sig)系統呼叫向pid標識的目標程序傳送訊號。

訊號處理函式

每個程序PROC 都有一個訊號處理陣列 int sig[32]。Sig[32]陣列的每個條目都指定了如何處理相應的訊號,其中0表示 DEFault(預設).1表示 IGNore(忽略).其他非零值表示使用者模式下預先安裝的訊號捕捉(處理)函式。下圖給出了訊號位向量、遮蔽位向量和訊號處理函式。

訊號處理步驟

  • 當某程序處於核心模式時,會檢查訊號並處理未完成的訊號。如果某訊號有使用者安裝的捕捉函式,該程序會先清除訊號,獲取捕捉函式地址,對於大多數陷阱訊號,則將已安裝的捕捉函式重置為 DEFault。然後,它會在使用者模式下返回,以執行捕捉函式,以這種方式篡改返回路徑。當捕捉函式結束時,它會返回到最初的中斷點,即它最後進入核心模式的地方。

  • 重置使用者安裝的訊號捕捉函式:使用者安裝的陷阱相關訊號捕捉函式用於處理使用者程式碼中的陷阱錯誤。由於捕捉函式也在使用者模式下執行,因此可能會再次出現同樣的錯誤。如果是這樣,該程序最終會陷入無限迴圈,一直在使用者模式和核心模式之間跳躍。為了防止這種情況,Unix 核心通常會在允許程序執行捕捉函式之前先將處理函式重置為 DEFault。這意味著使用者安裝的捕捉函式只對首次出現的訊號有效。

  • 訊號和喚醒:在Unix/Linux,核心中有兩種 SLEEP程序;深度休眠程序和淺度休眠程序。前一種程序不可中斷,而後一種程序可由訊號中斷。如果某程序處於不可中斷的SLEEP 狀態,到達的訊號(必須來自硬體中斷或其他程序)不會喚醒程序。如果它處於可中斷的SLEEP狀態,到達的訊號將會喚醒它。

訊號與異常

Unix訊號最初設計用於以下用途

1.作為程序異常的統一處理方法;
2.讓進城通過預先安裝的訊號捕捉函式使用者模式下的程式錯誤;
3.在特殊情況下,它會讓某一個程序通過訊號殺死另一個程序。

Linux中的IPC

  • 管道和FIFO

管道的主要用途是連線一對管道寫程序和讀程序。管道寫程序可將資料寫入管道,讀程序可從管道中讀取資料。管道控制機制要對管道讀寫操作進行同步控制。未命名管道供相關程序使用。命名管道是FIFO的,可供不相關程序使用。在 Linux中的管道讀取操作為同步和阻塞。如果管道仍有寫程序但沒有資料,讀程序會進行等待。

  • 訊號

程序可使用 kill 系統呼叫向其他程序傳送訊號,其他程序使用訊號捕捉函式處理訊號。將訊號用作IPC的一個主要缺點是訊號只是用作通知,不含任何資訊內容。

  • 執行緒同步機制

Linux 不區分程序和執行緒。在 Linux中,程序是共享某些公共資源的執行緒。如果是使用有共享地址空間的clone(系統呼叫建立的程序,它們可使用互斥量和條件變數通過共享記憶體進行同步通訊。另外,常規程序可新增到共享記憶體,使它們可作為執行緒進行同步。

實踐與截圖

編寫C程式碼使用訊號捕捉函式和long jump來繞過導致段錯誤的程式程式碼,使程式繼續執行或正常終止

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<setjmp.h>
jmp_buf env;
int count = 0;
void handler(int sig,siginfo_t *siginfo,void *context)
{
printf("handler:sig=&d from PID=%d UID=%d count=%d\n",sig,siginfo->si_pid,siginfo->si_uid,++count);
if (count>=4)
longjmp(env,1234);
}
int BAD()
{
int *ip=0;
printf("in BAD():try to dereference NULL pointer\n");
*ip=123;
printf("should not see this line\n");
}
int main(int argc,char *argv[])
{
int r;
struct sigaction act;
memset (&act,0,sizeof(act));
act.sa_sigaction = &handler;
act.sa_flags=SA_SIGINFO;
sigaction(SIGSEGV, &act,NULL);
if((r=setjmp(env))==0)
BAD();
else
printf("proc %d survived SEGMENTATION FAULT:r=%d\n",getpid(),r);
printf("proc %d looping\n");
while(1);
}


在Linux下編寫C語言實現一個訊息的IPC

#include<stdio.h>
#include<signal.h>
#include<string.h>
#define LEN 64
int ppipe[2];
int pid;
char line[LEN];
int parent()
{
printf("parent %d running\n",getpid());
close(ppipe[0]);
while(1){
printf("parent %d: input a line : \n",getpid());
fgets(line,LEN,stdin);
line[strlen(line)-1]=0;
printf("parent %d write to pipe\n",getpid());
write(ppipe[1],line,LEN);
printf("parent %d send signal 10 to %d\n",getpid(),pid);
kill(pid,SIGUSR1);
}
}
void chandler(int sig)
{
printf("\nchild %d got an interrupt sig=%d\n",getpid(),sig);
read(ppipe[0],line,LEN);
printf("child %d get a message = %s\n",getpid(),line);
}
int child()
{
char msg[LEN];
int parent = getppid();
printf("child %d running\n",getpid());
close(ppipe[1]);
signal(SIGUSR1,chandler);
while(1);
}
int main()
{
pipe(ppipe);
pid=fork();
if(pid)
parent();
else
child();
}
  • sigaction函式的功能是檢查或修改與指定訊號相關聯的處理動作(可同時兩種操作)
    執行該程式時,ctrl+c,第一次不會導致程式的結束。而是繼續執行,當用戶再次執行ctrl+c的時候,程式採用結束。
#include <stdio.h>  
#include <signal.h>  
  
  
void WrkProcess(int nsig)  
{  
        printf("WrkProcess .I get signal.%d threadid:%d/n",nsig,pthread_self());  
  
  
        int i=0;  
        while(i<5){  
                printf("%d/n",i);  
                sleep(1);  
                i++;  
        }  
}  
  
int main()  
{  
        struct sigaction act,oldact;  
        act.sa_handler  = WrkProcess;  
        act.sa_flags = SA_NODEFER | SA_RESETHAND;     
  
        sigaction(SIGINT,&act,&oldact);  
  
        printf("main threadid:%d/n",pthread_self());  
  
        while(1)sleep(5);  
  
        return 0;  
}  

sig 是需要捕獲的 signal number, 後一個是捕獲到訊號後的處理函式指標,接收到quit訊號時,結束程序

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

static void
handler(int sig)
{
        printf("Recieved signal: %d\n", sig);
}

int
main(int argc, char *argv[])
{
        signal(SIGINT, handler);

        printf("Caught SIGINT, input 'quit' to exit...\n");
        // wait signal caught
        char buf[1024] = {0};
        while (1) {
                printf("Please input: ");
                scanf("%s", buf);
                if (strcmp(buf, "quit") == 0) {
                        break;
                }
        }
        printf("Exit...\n");
        return 0;
}