1. 程式人生 > 其它 >資訊安全系統設計與實現:第六章學習筆記

資訊安全系統設計與實現:第六章學習筆記

資訊安全系統設計與實現:第六章學習筆記

20191331 lyx

教材學習內容總結

第六章 訊號與訊號處理

學習目標

通過本章的學習,瞭解訊號和中斷的統一處理,並從正確的角度看待訊號,理解訊號的程序中斷的關係,瞭解訊號的來源。理解類Unix系統中的訊號處理,並在類Unix系統下進行相關實踐。

訊號和中斷

中斷,英文名為Interrupt,計算機的世界裡處處都有中斷,任何工作都離不開中斷,可以說整個計算機系統就是由中斷來驅動的。

“中斷”是從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的訊號。它們也有三個可能的來源:
來自硬體的中斷:定時器、1/O裝置等
來自其他處理器的中斷:FFP、DMA、多處理器系統中的其他CPU
自己造成的中斷:除以О、保護錯誤、INT指令。
每個中斷都有唯一的中斷向量號。動作函式是中斷向量表中的中斷處理程式。

程序的陷阱錯誤
程序可能會自己造成中斷。這些中斷是由被CPU識別為異常的錯誤引起的,例如除以0、無效地址、非法指令、越權等。

Unix/Linux訊號示例

  • 按“Ctrl+C”組合鍵通常會導致當前執行的程序終止。原因如下:

    “Ctr1+C”組合鍵會生成一個鍵盤硬體中斷。鍵盤中斷處理程式將“Ctrl+C”組合鍵轉換為SIGINT(2)訊號,傳送給終端上的所有程序,並喚醒等待鍵盤輸人的程序。

  • 使用者可使用nohup a.out &命令在後臺執行一個程式。即使在使用者退出後,程序仍將繼續執行。nobup命令會使sh像往常一樣復刻子程序來執行程式,但是子程序會忽略SIGHuP(1)訊號。當用戶退出時,sh會向與終端有關的所有程序傳送一個SIGHUP訊號。

例項:使用 nohup 命令 將一個jar包在後臺執行,即使關閉中斷仍不會導致程序死亡。

  • 使用者可以使用sh命令kill pid (orkill-s9pia) 殺死該程。

    方法如下。執行殺死的程序向pid標識的目標程序傳送一個SIGTERM ( 15 )訊號,請求它死亡。目標程序將會遵從請求並終止。

Unix/Linux中的訊號處理

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

(下圖例為 OpenEuler 20.03 LTS 作業系統)

使用kill -l命令可以列出該系統所支援的所有訊號的列表:

訊號值在32 之前的則有不同的名稱,而訊號值在32 以後的都是用“SIGRTMIN”或“SIGRTMAX”開頭的,這就是兩類..型的訊號。前者是從UNIX 系統中繼承下來的訊號,為不可靠訊號(也稱為非實時訊號);後者是為了解決前面“不可靠訊號”的問題而進行了更改和擴充的訊號,稱為“可靠訊號”(也稱為實時訊號)。

訊號的來源

  • 來自硬體中斷的訊號
  • 來自異常的訊號
  • 來自其他程序的訊號

訊號處理函式&訊號捕捉函式

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

  • 使用signal()函式處理時,只需要指出要處理的訊號和處理函式即可。它主要是用於前32種非實時訊號的處理,不支援訊號傳遞資訊,但是由於使用簡單、易於理解,因此也受到很多程式設計師的歡迎。

Linux還支援一個更健壯、更新的訊號處理函式sigaction(),推薦使用該函式。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

第一個引數signum:指明瞭所要處理的訊號型別,它可以取除了SIGKILL和SIGSTOP外的任何一種訊號。  

第二個引數handler:描述了與訊號關聯的動作,它可以取以下三種值:

  • SIG_IGN:忽略該訊號
  • SIG_DFL:恢復對訊號的系統預設處理
  • sighandler_t型別的函式指標:使用者自定義的處理函式。

在 實踐環境下檢視 sigaction 使用方法

  • 訊號集函式組

我們可以通過訊號來終止程序,也可以通過訊號來在程序間進行通訊,程式也可以通過指定訊號的關聯處理函式來改變訊號的預設處理方式,也可以遮蔽某些訊號,使其不能傳遞給程序。那麼我們應該如何設定我們需要處理的訊號,我們不需要處理哪些訊號等問題呢?訊號集函式就是幫助我們解決這些問題的。

int sigemptyset(sigset_t *set);
//該函式的作用是將訊號集初始化為空。

int sigfillset(sigset_t *set);
//該函式的作用是把訊號集初始化包含所有已定義的訊號。

int sigaddset(sigset_t *set, int signo);
//該函式的作用是把訊號signo新增到訊號集set中,成功時返回0,失敗時返回-1。

int sigdelset(sigset_t *set, int signo);
//該函式的作用是把訊號signo從訊號集set中刪除,成功時返回0,失敗時返回-1.

int sigismember(sigset_t *set, int signo);
//該函式的作用是判斷給定的訊號signo是否是訊號集中的一個成員,如果是返回1,如果不是,返回0,如果給定的訊號無效,返回-1;

int sigpromask(int how, const sigset_t *set, sigset_t *oset);
//該函式可以根據引數指定的方法修改程序的訊號遮蔽字。新的訊號遮蔽字由引數set(非空)指定,而原先的訊號遮蔽字將儲存在oset(非空)中。如果set為空,則how沒有意義,但此時呼叫該函式,如果oset不為空,則把當前訊號遮蔽字儲存到oset中。

int sigpending(sigset_t *set);
//該函式的作用是將被阻塞的訊號中停留在待處理狀態的一組訊號寫到引數set指向的訊號集中,成功呼叫返回0,否則返回-1,並設定errno表明錯誤原因。

int sigsuspend(const sigset_t *sigmask);
//該函式通過將程序的遮蔽字替換為由引數sigmask給出的訊號集,然後掛起程序的執行。注意操作的先後順序,是先替換再掛起程式的執行。程式將在訊號處理函式執行完畢後繼續執行。如果接收到訊號終止了程式,sigsuspend()就不會返回,如果接收到的訊號沒有終止程式,sigsuspend()就返回-1,並將errno設定為EINTR。

sigaction實踐

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

void handler(int sig,siginfo_t *siginfo,void *context){
printf("handler:sig=%d from PID=%d UID=%d\n",sig,siginfo->si_pid,siginfo->si_uid);
}

int main(int argc,char *argv[]){
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_sigaction = &handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGTERM,&act, NULL);
printf("proc PID=%d looping\n");
printf("enter Kill PID to send SIGTERM signal to it\n",getpid());
while(1){
sleep(10);
}
}

執行結果:

訊號處理步驟

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

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

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

訊號與異常

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

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

訊號用做IPC

實踐:段錯誤捕捉函式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
#include <string.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);
}

執行結果:

管道和FIFO

管道的主要用途是連線一對管道寫程序和讀程序。管道寫程序可將資料寫入管道,讀程序可從管道中讀取資料。管道控制機制要對管道讀寫操作進行同步控制。未命名管道供相關程序使用。

在建立了管道之後,兩個程序就可以把它當作普通檔案一樣進行讀寫操作,使用非常方便。不過值得注意的是,FIFO是嚴格地遵循先進先出規則的,對管道及FIFO的讀總是從開始處返回資料,對它們的寫則把資料新增到末尾,它們不支援如lseek()等檔案定位操作。

訊號

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

執行緒同步機制

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

程式設計實踐:實現一個訊息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();
}

執行結果:

參考資料

一文講透計算機的“中斷” https://zhuanlan.zhihu.com/p/360548214