歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux技術 >> Linux之――――信號

Linux之――――信號

日期:2017/3/3 13:41:04   编辑:Linux技術
一. 信號
我們在shell下運行起來一個程序,可以在這個進程正在運行的時候鍵盤輸入一個Ctrl+C,就會看到這個進程被終止掉了,其實當我們鍵入Ctrl+C的時候是向進程發送了一個SIGINT信號,這時候產生了硬件中斷則系統會從執行代碼的用戶態切入到內核態去處理這個信號,而一般這個信號的默認處理動作是終止進程,因此正在運行的進程就會被終止了。像SIGINT就是系統中定義的一個信號,除此之外還有其他信號,可以通過 kill -l 命令來查看:



如上圖中有從1到64的各種信號,但是32和33號信號並沒有,因此系統中一共有62個信號,其中1至31號是系統中的普通信號,而34到64是實時信號,這裡暫不討論。
-------------------------------------------------------------------------------------------
二. 信號的產生
那麼,像上面的那些信號都是如何產生的呢?
首先,是由終端輸入產生的,就像剛才舉的栗子,從鍵盤輸入一條命令其實就是在向一個進程發送特定的信號,但是像Ctrl+C這條命令只能發給前台進程,對後台進程是不起作用的,一個shell可以同時運行一個前台進程和多個後台進程;
其次,可以調用系統函數向進程發送信號,比如kill命令其實是終止一個進程,但kill命令是調用kill函數來實現的,kill函數可以給一個指定的進程發送特定的信號,其命令使用如下:



在上面的命令中,signal是為指定要發的信號,可以是信號的名字也可以是上面圖片中每個信號前面的代碼號;而pid就表示要發送信號的進程的pid號;
下面就可以寫一個死循環的程序並且放在後台運行,當Ctrl+c對後台進程不起作用時就可以使用kill命令來給指定的進程發送2號信號也就是SIGINT信號也就是Ctrl+c,信號的處理方式為默認的也就是終止一個進程:



當輸入一行命令"kill -2 6156"時按一下回車並沒有結果,是因為當按下回車時要先一步回到bash等待用戶輸入下一條命令,這裡並不希望命令的結果和用戶輸入的下條命令產生沖突顯示錯亂,因此要按兩次回車才能看到進程被中斷的結果;
除了kill函數還有一個函數raise函數,該函數可以給當前進程發送信號也就是自己給自己發送信號,這兩個函數都是成功返回0,失敗返回-1:



類似的abort函數是向當前進程發送6號信號也就是SIGABRT信號,該信號是使進程異常終止,和exit函數一樣,該函數一定會成功因此並沒有返回值;
還有一種是由軟件條件產生的信號,比如以前談論匿名管道的時候,當管道的讀端關閉寫端仍繼續往裡寫入的時候進程就會接收到系統發來的一個SIGPIPE信號,該信號的默認處理動作是終止進程;
然而這裡談論一個函數alarm函數,該函數用於設定特定的秒數,而當超時的時候系統就會發送一個SIGALRM信號給該進程,而這個信號的默認處理動作仍然是終止進程:



函數參數seconds是指要設定的秒數,函數的返回值為0或者返回以前設定的鬧鐘余下的秒數,比如當鬧鐘設定時間還未到但又重新設定了一個鬧鐘,那麼原來鬧鐘就會返回余下的時間,而當seconds值為0時,表示清除設定的鬧鐘,鬧鐘的返回值仍然是余下的秒數;
栗子時間:
#include <stdio.h>
#include <unistd.h>

int main()
{
    int i = 0;
    alarm(1);
    while(1)
    {   
        printf("i: %d\n",i++);
    }   

    return 0;
}
上面的程序是設定了一個鬧鐘,鬧鐘的時間為1秒,而在這一秒內不斷地進行i++,當一秒鐘之後,鬧鐘到時,系統就會給進程發送一個SIGALRM的信號,這個信號的處理動作會終止這個進程;
程序運行結果:



結果顯示,也就是在一秒內i進行自加的次數為13628次;
-------------------------------------------------------------------------------------------
三. 信號的處理方式
一般來說,當一個信號產生之後,系統對其的處理方式有如下三種:
可以忽略這個信號,並不執行任何動作;
執行信號的默認處理動作,而大多數普通信號的默認處理動作是終止一個進程;
可以執行用戶自定義的處理動作,這稱為信號的捕捉(catch);
信號被捕捉,也就是當產生了這個信號的時候,要執行用戶自定義的函數處理動作,而捕捉一個信號的處理函數有signal和函數sigaction:



signal函數參數中,
signum是要捕捉信號的代表號碼;
handler是一個函數指針,可以為用戶自定義的一個函數,表示信號要處理的動作函數;



sigaction函數同樣是捕捉一個信號執行用戶自定義的函數,
signum同樣是為要捕捉的信號號碼;
act則是一個數據結構,數據結構的定義如下:



數據結構中,sa_handler和signal函數中是一樣的,是一個函數指針可以指向用戶自定義的函數,當sa_handler賦值為SIG_IGN的時候表示信號的默認處理動作為忽略這個信號,賦值為SIG_DFL的時候表示執行默認處理動作,而sa_mask表示需要額外屏蔽的一些信號,當信號處理函數返回時自動恢復為原來狀態;sa_flag表示一些標志來修改信號的行為,這裡設置為0即可;sa_sigaction表示實時信號的處理函數,這裡不討論;而sa_restorer是過時的且不應該被使用的,POSIX並不提供該元素;
oldact是指信號原來的數據結構信息,當其不為空的時候就將修改之前的信息保存其中以便日後恢復;
除了上面兩種信號的捕捉函數,還有一個函數pause,該函數使進程掛起直到有信號遞達,如果此號的處理動作是執行系統默認處理動作,則進程終止該函數沒有返回值;如果信號的處理動作是忽略,則進程會一直處於掛起狀態,pause同樣沒有機會返回;如果信號的處理動作是捕捉這個信號執行用戶自定義的函數,則pause會出錯返回-1並設置錯誤碼,因此和程序替換函數exec有些類似,只有出錯返回值;
下面舉個栗子來使用信號的捕捉函數,可以自我實現一個sleep函數:
#include <stdio.h>
#include <signal.h>

void handler(int sig)//用戶自定義函數
{
    printf("i get a sig %d\n", sig);
}

void mysleep(unsigned int time)
{
    struct sigaction new, old;//設定兩個結構體
    new.sa_handler = handler;
    sigemptyset(&new.sa_mask);
    new.sa_flags = 0;
    sigaction(SIGALRM, &new, &old);//注冊信號處理函數,捕捉SIGALRM信號
    alarm(time);//設定鬧鐘為自定義時間
    pause();//將進程掛起直到鬧鐘結束向進程發送SIGALRM信號
    alarm(0);//撤銷鬧鐘
    sigaction(SIGALRM, &old, NULL);//恢復對信號的默認處理動作
}

int main()
{
    //主函數內要實現的是每隔兩秒打印一句“hello world...”
    while(1)
    {   
        mysleep(2);//實現mysleep
        printf("hello world...\n");
    }   

    return 0;
}
運行程序,結果如下:



對於上面的過程,其實是有一個重要的點要解釋清楚的,那就是系統是如何完成從信號產生到信號被處理這一系列動作的;這裡需要知道的是,信號並不是一產生就立即被處理的,而是有一個合適的契機來決定是否處理這個信號,具體解釋如下圖:



-------------------------------------------------------------------------------------------
四. 信號的狀態
信號從產生到進程接收到是有一個中間過程的,當進程接收到一個信號也就是信號的遞達(delivery);而信號從產生到遞達這之間的狀態叫信號的未決(pending);但同時,信號還有一種狀態叫信號的阻塞(block),也就是信號產生處於未決狀態但沒有被遞達而是被阻塞住了;這裡需要強調的是,信號的阻塞和信號的忽略是不一樣的,信號的忽略是在信號遞達之後的一種處理方式,而信號的阻塞是信號還沒有被遞達。
信號的狀態在每個進程的PCB中都有一張可以說是相對應的表來表示相應的狀態,信號產生時,內核就會在進程PCB塊中的未決表中設置相應的信號位,同樣的,當一個信號信號被阻塞,也有一個block表來記錄信號是否被阻塞,另外還有一張handler表來記錄信號的處理方式;
因為我們這裡討論1~31個系統的普通信號,而每一個信號是否產生(未決)是否被阻塞都可以用兩個狀態0和1來記錄表示,其實也就類似於32位平台下一個int類型的32個比特位的狀態,這裡的狀態在未決表中0和1表示信號是否產生,而block表則表示信號是否被阻塞;但是,信號的產生並不影響信號的阻塞,當一個信號產生時在未被遞達之前是可以被阻塞的;同樣,信號是否阻塞也並不影響信號是否產生,一個信號被阻塞了,當它產生時只是不會被遞達而已;兩者並沒有關系;
sigset_t是可以用來存儲信號的未決和阻塞狀態的,稱為信號集。它可以被看做是由32個比特位組成的,而每一個比特位表示信號是否有效和無效,至於內部到底是如何存儲的,作為使用者暫時是不用關心的;阻塞信號集也叫作當前信號的信號屏蔽字;
而關於信號集的操作函數如下:



sigemptyset函數表示清空一個信號集,該信號集中不包含任何有效信號;
sigfillset函數表示將一個信號集全部置位,該信號集包含了所有系統中的有效信號;
sigaddset函數表示將一個有效信號添加到某個信號集中;
sigdelset函數表示將一個有效信號從某個信號集中刪除;
sigismember函數的返回值是一個bool類型,用於判斷一個信號集中是否包含某個有效信號;
函數參數中set是指向sigset_t類型的指針,signum則是信號的號碼;
這裡需要強調的是,在使用sigaddset函數和sigdelset函數對一個信號集進行添加或刪除某個有效信號之前,需要調用sigemptyset函數或者sigfillset函數將信號集進行初始化;
上面的函數成功返回0,失敗返回-1;
函數sigprocmask函數可用於修改和讀取進程的信號屏蔽字:



函數參數中,
set表示要修改的信號集;
oldset表示修改之前信號集的狀態,以便恢復;
how表示如何修改,為SIG_BLOCK時,表示將set信號集中的有效信號阻塞;為SIG_UNBLOCK時,表示將信號集中的而有效信號解除阻塞,而為SIG_SETMASK時,表示值就為set;
設置了信號屏蔽字之後,同樣可以讀取當前進程信號的未決狀態,也就是信號產生與否:



函數從內核中讀取出未決信號集狀態並保存到set中;
下面舉個栗子使用上面的信號集操作函數:
#include <stdio.h>
#include <signal.h>

void printpending(sigset_t *p) 
{
    int i = 0;
    for(i = 1; i < 32; ++i)
    {   
        if(sigismember(p, i)) //判斷1~31號信號是否產生
            printf("1");
        else
            printf("0");
    }   
    printf("\n");
}

int main()
{
    sigset_t b, p;//設置兩個信號集block和pending
    sigemptyset(&b);//將block信號集初始化
    sigaddset(&b, SIGINT);//將SIGINT信號也就是Ctrl+c產生的信號添加到block信號集中
    sigprocmask(SIG_BLOCK, &b, NULL);//設置進程的信號屏蔽字,阻塞SIGINT信號

    while(1)
    {   
        sigpending(&p);//每隔1秒獲取當前進程的信號未決狀態
        printpending(&p);
        sleep(1);
    }   
    return 0;
}

上面的程序阻塞掉了2號信號也就是SIGINT信號,因此,當鍵入Ctrl+c的時候,產生了SIGINT信號,但是因為信號被阻塞了因此就一直處於未決狀態並不會遞達,每隔一秒獲取當前進程信號未決狀態查看對應信號是否產生,運行程序:



可以看到,當鍵入Ctrl+c的時候產生了2號信號,而相應的pending信號集中的第二位就會被置為1,說明2號信號也就是SIGINT信號產生。
因為2號信號被阻塞並不會遞達被處理,因此Ctrl+c並不能終止程序,可以用Ctrl+\來終止程序。
-------------------------------------------------------------------------------------------
五. 總結
信號的產生:終端輸入、調用系統函數向進程發送指定的信號、軟件條件產生比如錯誤和異常;
信號的處理方式:忽略、執行默認處理動作(一般是終止進程)、執行用戶自定義動作(捕捉);
信號的狀態:未決、阻塞、遞達;未決和阻塞之間相互不影響,而只有信號處於未決狀態且沒有被阻塞時才會被遞達;
對於信號的捕捉,其實是在用戶和內核之間來回切換的,用戶--(因為錯誤、異常或中斷進入內核)-->內核--(在處理完異常之後檢查是否有信號要處理,若被捕捉進入用戶)-->用戶--(執行完信號處理函數重新返回內核)-->內核--(重新返回異常產生處繼續執行用戶代碼)-->用戶。
《完》
本文出自 “敲完代碼好睡覺zzz” 博客,請務必保留此出處http://2627lounuo.blog.51cto.com/10696599/1770913
Copyright © Linux教程網 All Rights Reserved