信號


異常

  控制流突變,用來響應處理器的某些變化。處理器中,狀態編碼為不同的位和信號,狀態變化稱為事件,處理器檢測到有事件發生時,他會通過一張叫異常表的跳轉表,進行間接調用。

  系統中的每個異常都有一個異常號,當系統啟動時,操作系統分配和初始化一張稱為異常表的跳轉表,當處理器檢測到一個事件發生時,處理器觸發異常,通過異常表目k轉到相應的處理程序。

異常類別

信號

  1. 發送信號。內核通過更新目的進程上下文中的某個狀態,發送一個信號給目的進程。
  2. 接受信號。當目的進程被內核強迫以某種方式對信號的發送做出反應時,他就接受了信號。

  fork后子進程繼承父進程信號處理方式,但是exec后信號處理方式消失。

  當執行一個程序時,所有信號的狀態都是系統默認或忽略,通常所有信號都被設置為他們的默認動作,除非調用exec的進程忽略該信號,確切的來說,exec函數將原先設置為要捕捉的信號都更改為默認動作,其他信號狀態不變(一個進程原先要捕捉信號,當其執行一個新程序后,就不能再捕捉了,因為信號捕捉函數的地址很可能在所執行的新程序中無意義)。

  一旦安裝了信號函數,它便一直安裝着。posix保證信號在其信號處理函數運行期間總是阻塞的。如果一個信號再被阻塞期間產生了一次或多次,那么該信號被解阻塞后通常只遞交一次,也就是UNIX默認不排隊。

  實際執行信號的處理動作稱為信號遞達(Delivery),信號從產生到遞達之間的狀態,稱為信號未決(Pending)。進程可以選擇阻塞(Block)某個信號。被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是在遞達之后可選的一種處理動作

實時信號

  其值在[SIGRTMIN,SIGRTMAX]。如果需要實時行為,必須再安裝信號處理程序時給sigaction指定SA_SIGINFO。Posix要求至少提供RTSIG_MAX中實時信號,改制的最小值是8。

  1. 信號是排隊的。同一信號產生幾次就遞交幾次。它以先進先出的方式排隊。非實時信號產生多次可能只遞交一次。
  2. 當有多個SIGRTMIN、SIGTMAX之間解阻塞的信號排隊時,較小的信號值大於較大的信號值遞交。
  3. 當某個非實時信號遞交時,他傳遞給信號處理程序的唯一參數是信號值。實時信號比其他信號攜帶更多的信息。

非實時信號

  一個發出而沒有被接受的信號叫做待處理信號。一個待處理信號最多只能被接受一次,內核為每個進程在pending位向量中維護着待處理信號的集合。在blocked位向量中維護着被阻塞的信號集合。只要傳送了類為k的信號,就會在pending中設置第k個位,接受了類型為k的信號,內核就會清除pending第k位。pending中k只有一位,所以每種類型信號最多有一個未處理信號

  如SIGALRM、SIGINT、SIGKILL等。

中斷的系統調用

  早期UNIX系統的一個特性是:如果在進程執行一個低速系統調用而阻塞期間捕捉到一個信號,則該系統調用就被中斷不再繼續執行。該系統調用返回出錯,其errno設置為EINTR。

  慢系統調用(slow system call):此術語適用於那些可能永遠阻塞的系統調用。永遠阻塞的系統調用是指調用有可能永遠無法返回,多數網絡支持函數都屬於這一類。如:若沒有客戶連接到服務器上,那么服務器的accept調用就沒有返回的保證。

  1. 在讀某些類型的文件時,如果數據並不存在則可能會使調用者永遠阻塞(管道、終端設備以及網絡設備)。
  2. 在寫這些類型的文件時,如果不能立即接受這些數據,則也可能會使調用者永遠阻塞。
  3. 打開文件,在某種條件發生之前也可能會使調用者阻塞(例如,打開終端設備,它要等待直到所連接的調制解調器回答了電話)。
  4. pause(按照定義,它使調用進程睡眠直至捕捉到一個信號)和wait。
  5. 某種ioctl操作。
  6. 某些進程間通信函數

  當阻塞於慢系統調用的一個進程捕捉到某個信號且響應信號處理函數返回時,系統調用可能返回一個EINTR,有些內核能重啟被中斷的系統調用,有些不能(即便設置了SA_RESTART)。

  自動再起動的系統調用包括:ioctl、 read、readv、write、writev、wait和waitpid。正如前述,其中前五個函數只有對低速設備進行操作時才會被信號中斷。而 wait和waitpid在捕捉到信號時總是被中斷。

  connect返回EINTR時不能再次調用,否則會返回一個錯誤。此時必須調用select等待完成連接。

信號集

  不同的信號編號可能超過一個整型量所包含的位數,所以一般不能用整型量中的一位代表一種信號,也就是不能用一個整型量代表一個信號集。

  所用應用程序在使用信號集前,要對該信號集調用sigemptyset或sigfillset一次, 因為c編譯程序將不賦初值的外部變量和靜態變量都初始化為0,而這是否與給定系統上的信號集的實現相對應並不清楚。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set);
int sigdelset(sigset_t *set);
//以上四個函數若成功返回0,若失敗返回-1
int sigismember(sigset_t *set);//若真:返回1,若假:返回0

信號屏蔽

  一個進程的信號屏蔽字規定了當前阻塞而不能地送給該進程的信號集

int sigprocmask(int how,const sigset_t *set,sigset_t *oset)
//成功:0出錯:-1,錯誤原因存於error中
  1. set非空,how(決定函數的操作方式):

    SIG_BLOCK:該進程新的信號屏蔽字是當前信號屏蔽字和set指向信號集的並集,set包含了希望阻塞的附加信號。不能阻塞SIGKILL和SIGSTOP
    SIG_UNBLOCK:該進程新的信號屏蔽字是當前信號屏蔽字和set所指的信號集補集的交集,set包含了希望解除阻塞的信號
    SIG_SETMASK:該進程新的信號屏蔽字是set所指的值。

  1. oset:非空,當前進程的信號屏蔽字通過oset返回。

signal

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signum:一個int類型的參數(即接收到的信號代碼),
//handler:信號處理函數,可取一下兩個特殊值:① SIG_IGN 屏蔽該信號 ② SIG_DFL 恢復默認行為

  當一個信號的信號處理函數執行時,如果進程又接收到了該信號該信號會自動被儲存而不會中斷信號處理函數的執行,直到信號處理函數執行完畢再重新調用相應的處理函數

  但是如果在信號處理函數執行時進程收到了其它類型的信號,該函數的執行就會被中斷。執行后信號注冊函數signal_hander_fun失效,對SIGINT信號的處理回到操作系統的默認處理方式,當應用進程再次收到SIGINT信號時,會按操作系統默認的處理方式進行處理(即不再執行signal_hander_fun處理函數)

#define SIG_ERR (void (*)())-1
#define SIG_DEL (void (*)())0
#define SIG_IGN (void (*)())1

函數原型

 

void (*signal(int signo,void (*func)(int))) (int)
typedef void Sigfuc(int);//這里可以看成一個返回值
//再對signal函數進行簡化就是這樣的了
Sigfunc *signal(int,Sigfuc *);

 

sigaction 

  每個信號都有與之關聯的處置,稱為信號的行為(action),可以通過sigaction設定一個信號的行為。

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//成功返回0,失敗返回-1,並設置errno
  1. signum:要操作的信號 
  2. act :要設置的對信號的新處理方式 
  3. oldact :原來對信號的處理方式 
struct sigaction
{
    void (*sa_handler)(int);/*信號處理函數指針,與single一樣,如果指定某個信號的行為為默認或忽略該信號,
                                則sa_handler設置為SIG_DFL或SIG_IGN,並不設置SA_SIGINFO*/
    sigset_t sa_mask;//在調用該信號捕捉函數前,這一信號集要加到新進程的信號屏蔽字中,僅當從信號捕捉函數返回時再將進程的
                     //信號屏蔽字恢復為原先值,該信號處理函數被調用時,os建立的信號屏蔽字包括正在被遞送的信號,因此可保證
                     //在處理一個給定的信號時,如果這種信號再次發生,那么他會阻塞到對前一個信號的處理結束為止
    int sa_flags;  //信號處理方式,如果指定新的信號安裝機制,處理函數被調用的時候,不但可以得到信號編號,
                    //而且可以獲悉被調用的原因以及產生問題的上下文的相關信息,函數指針的三個參數含義為
    void (*sa_sigaction)(int iSignNum, siginfo_t* pSignInfo, void *);//一個代替的信號處理程序,在sigaction結構中使用了
             //SA_SIGINFO標志時,使用該信號處理程序,對於sa_sigaction和sa_handler兩者實現可能使用同一存儲區,所以應用只能使用一次這兩個字段
    void     (*sa_restorer)(void); //網上找的資料說明都是說(保留,占時無用)
};
siginfo_t
{
    int si_signo;    /* 信號值,對所有信號有意義 */
    int si_errno;    /* errno 值,對所有信號有意義 ,if nonzero ,errno value form <errno.h>*/
    int si_code;     /* 信號產生的原因,對所有信號有意義 SI_ASYNCIO-信號由某個異步I/O請求完成,
                       這些異步I/O請求是posix的aio_XXX,SI_MESGQ-信號在有個消息被放置到空消息隊列中產生,
                       SI_QUEUE-信號由sigqueue函數發出,SI_TIMER-信號由使用timer_settime函數設置的某個定時器
                       產生,SU_USER-信號由kill發出,如果信號由其他事件產生,這里設置成不同於這里所列的值
                       但是si_value只有si_code的值設置成這里所列的值時才有意義*/

    pid_t    si_pid;      /* 發送信號的進程ID */
    uid_t    si_uid;      /* 發送信號進程的真實用戶ID */
    int      si_status;   /* 對出狀態,對SIGCHLD 有意義 exit value or signal number*/
  void    *si_addr;     /* 觸發fault的內存地址,對SIGILL,SIGFPE,SIGSEGV,SIGBUS 信號有意義 */

    int si_trapno;        /* Trap number that caused hardware-generated signal (unused on most architectures) */
    clock_t  si_utime;    /* 用戶消耗的時間,對SIGCHLD有意義 */
    clock_t  si_stime;    /* 內核消耗的時間,對SIGCHLD有意義 */
    union sigval si_value;    /* 信號值,對所有實時有意義,是一個聯合數據結構,可以為一個整數(由si_int標示,也可以為一個指針,由si_ptr標示) */
    int      si_int;      /* POSIX.1b signal */
    void    *si_ptr;      /* POSIX.1b signal */
    int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
    int      si_timerid;  /* Timer ID; POSIX.1b timers */
    long     si_band;     /* 對SIGPOLL信號有意義 */
    int      si_fd;       /* 對SIGPOLL信號有意義 */
    short    si_addr_lsb; /* Least significant bit of address(since kernel 2.6.32) */
};
/*
1.若信號是SIGCHLD,則設置si_pid,si_status和si_uid字段
2.若信號是SIGBUS,SIGILL,SIGFPE或SIGSEGV,則si_addr包含造成故障的根源地址,該地址可能並不正確
3.si_errno包含錯誤編號,他對應造成信號產生條件,並由實現定義
*/
union sigval
{
    int sival_int;
    void *sival_ptr;
};
//在遞送信號時sigval_int傳遞一個整數或sival_ptr傳遞一個指針

  

  1.通常按下列方式調用信號處理程序

void handler(int signo);

  2.但是如果設置了SA_SIGINFO標志,那么按下列方式調用

void sa_action(int signo,siginfo_t *info,void *context);
/*
context是無類型指針,可被強制轉換為ucontext_t結構類型,該結構表示信號傳遞時進程的上下文,至少包含以下字段
*/
ucontext_t *un_link;//pointer to context resumed when this context returns
sigset_t uc_sigmask;//signal blocked when this context is active
stack_t uc_stack;//stack used by this context
mcontext_t uc_mcontext;//machine-specific representation of saved context
//uc_stack描述了當前上下文使用的棧,至少包括
void *ss_sp;//stack base or pointer
size_t ss_size;//stack size
int ss_flags;//flags

  SA_RESTART標志是可選的,如果設置,相應信號中斷的系統調用將由內核自動重啟。如果信號不是SIGALRM且SA_RSTART有定義,我們就設置該標志(因為SIGFALRM目的在於:通常是為I/O操作超時設置,這時我們希望受阻塞的系統調用被該信號中斷掉,並且定義了SA_INTERRUPT,如果定義了該標志,在被捕獲的信號是SIGALRM時設置它)。

其它一些操作

#include <signal.h>
int sigpending(sigset_t *set);//返回調用進程中阻塞信號不能遞送的,而且也一定是未決的
//成功返回0失敗返回-1

#include <setjmp.h>
int sigsetjmp(sigjmp_buf env,int savemask);
//若直接調用返回0,若從siglongjmp調用返回,返回非0
//savemask保存了當前進程的屏蔽字,調用siglongjmo時,如果帶非0 savemask的sigsetjmp調用已經保存了env,則siglongjmp
//從中恢復保存的信號屏蔽字
void siglongjmp(sigjmp_buf env,int val);

  如果希望對一個信號解除阻塞,然后調用pause以等待以前被阻塞的信號發生,則:

sigset_t newmask,oldmask;
sigemptyset(&newmask);
sigaddset(&newmask,SIGINT);

/*block SIGINT and save current signal mask*/
if(sigpromask(SIG_BLOCK,&newmask,&oldmask)<0)
    err_sys("SIG_BLOCK error");
    
/*critical region of code*/
/*restore signal mask,which unblocks SIGINT*/
if(sigpromask(SIG_SETMASK,&oldmask,nullptr)<0)//$$
    err_sys("SIG_SETMASK error");
/*windows is open*/
pause();//wait for signal to occur//$$
/*continue processing*/

  在兩個$$之間有時間窗口,如果在信號阻塞時,產生了信號,那么信號的遞送在解除阻塞時,該信號好像發生在接觸對SIGINT阻塞和pause之間,如果發生這種情況或在解除阻塞和pause之間確實發生了信號,那么就會產生問題,因為再也看不見該信號,從某種意義上來講,在此時間窗口發生了信號丟失,就是的pause永遠阻塞,這也是不可靠信號機制的另一個問題

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
//他返回給調用者,並總是返回-1並將errno設置為EINTR,沒有成功返回值,
//在捕捉到一個信號或發生了一個會終止該進程的信號之前,該進程被掛起,如果捕捉到一個信號並從該信號處理程序
//中返回,sigsuspend返回並且該進程的信號屏蔽字設置為調用sigsuspend之前的

sigwait

  同步的等待一個異步事件。使用了信號但沒有涉及信號處理程序。

#include <signal.h>
int sigwait( const sigset t* set, int* sig );

  調用sigwait前,阻塞某個信號集,由set指定。sigwait一直阻塞到這些信號中有一個或多個待處理,這時它返回其中一個信號。該信號值通過sig指針存放。sigwait 成功時返回0一旦sigwait 正確返回,我們就可以對接收到的信號做處理了。

  如果我們使用了sigwait,就不應該再為信號設置信號處理函數了。這是因為當程序接收到信號時,二者中只能有一個起作用。

  1. SIGHUP:當掛起進程的控制終端時,SIGHUP將被觸發,對於沒有控制終端的網絡后台程序而言,通常利用SIGHUP來強制服務器重讀配置文件。strace跟蹤調試時系統調用收到和信號。
  2. SIGPIPE:默認情況下,往一個讀端關閉的管逍或socket連接中寫數據將引發SIGPIPE信號。我們需要在代碼中捕獲並處理該信號,或者至少忽略它,因為程序接收到SIGPIPE信號的默認行為是結束進程。引起SIGPIPE信號的寫操作將設置ermo為EPIPE。我們可以使用send的MSG_NOSIGNAL標志來禁止寫操作觸發SIGPIPE信號。在這種情況下,我們應該使用send的數反鎖的ermo值來判斷管道或者socket連接的讀端是否已經關閉。此外,我們也可以利用I/O復用系統調用來檢測管道和sockcer連接的讀端是否已經關閉。以poll為例,當管道的讀端關閉時,寫端文件描述符上的PLLHUP事件將被觸發:當socket連接被對方關團時,socket 上的POLLRDHUP事件將被觸發。
  3. SIGURG:使用此信號接收外帶數據
  4. SIGKILL和SIGSTOP:不能被捕獲。

SIGEV_THREAD

  當使用aio_read或aio_write時初始化一台異步設備讀或寫時,程序員指定一個struct aiocb,其中包含了一個struct sigevent成員,接受struct sigevent的其他函數包括timer_create(他創造一個進程范圍內的定時器)和sigqueue(他將信號送入進程隊列)。

  struct sigevent提供了一種允許程序員指定一個信號是否產生以及如果產生應該使用時什么信號數字的“通知機制”。pthreads增設了一個被稱為SIGEV_THREAD的新通知機制,該通知機制使得信號通知函數向線程氣勢函數一樣運行。

  SIGEV_THREAD通知函數可能無法在一個新的線程中實際運行。系統可以排隊SIGEV_THREAD事件,在一些內部“服務線程”連續調用起始函數,這種差別對於應用程序而言很難有效的區分,使用系統服務線程要很小心的為通知線程指定屬性——調度策略,優先級,競爭范圍,最小棧空間。

  對於傳統的信號機制如:setitimer,SIGCHLD,SIGINT等,SIGEV_THREAD特性不可用。

timer_t timer_id;
struct itimerspec ts;
struct sigevent se;

se.sigev_notify=SIGEV_THREAD;
se.sigev_value.sival_ptr=&timer_id;
se.sigev_notify_function=timer_fun;//一個指向線程起始函數的指針
//期望的線程創建屬性的線程屬性對象---pthread_attr_t,如為空,則與將deatchstate屬性設置為PTHREAD_CREATE_DETACHED一樣
//創建通知線程,這樣避免內存泄漏,因為線程標識符不是對任何其他線程來講都可得到所以說PTHREAD_CREATE_JOINABLE結果是不可知的
se.sigev_notify_attributes=NULL;

ts.it_value.tv_sec=5;
ts.it_value.tv_nsec=0;
ts.it_interval.tv_sec=5;
ts.it_interval.tv_nsec=0;

timer_create(CLOCK_REALTIME,&se,&timer_id);

timer_settime(timer_id,0,&ts,0);

 一些常見的信號

可重入函數

  在信號處理程序中保證安全調用的函數,這些函數是可重入的並稱為異步信號安全的,除了可重入以外,在信號處理操作期間,他會阻塞任何引起不一致得信號發送。由於每個線程只有一個errno,所以信號處理程序會修改其原先的值,所以在調用信號處理程序時,應先保存errno,調用后恢復errno。

  1. 它只訪問局部變量
  2. 它不能被信號處理程序中斷

  所有的I/O函數和pthread_XXX函數都不可在信號處理程序中調用,Unix網絡編程2中所有的IPC函數只有sem_post,read,write(read和write只作用於管道和FIFO時)可以

不可重入的原因:

  1. 已知它們使用靜態數據結構
  2. 它們調用malloc和free因為malloc通常會為所分配的存儲區維護一個鏈接表,而插入執行信號處理函數的時候,進程可能正在修改此鏈接表。
  3. 它們是標准IO函數。因為標准IO庫的很多實現都使用了全局數據結構

  一個信號處理器中中能在調用的函數

不可重入

  1. 函數體內使用了靜態的數據結構;
  2. 函數體內調用了malloc()或者free()函數;
  3. 函數體內調用了標准I/O函數。

  很多標准I/O庫的實現都以不可重入使用全局數據結構,在信號處理程序中調用printf不一定得到可期望的結果,信號處理程序可能中斷主程序的printf函數調用。

  把不可重入寫成可重入的唯一方式是利用可重入的方式來寫,保證中斷是安全的。如果一定要使用全局變量要使用互斥量將它保護起來。

  中斷是嵌入式系統中重要的組成部分,這導致了很多編譯開發商提供一種擴展—讓標准C支持中斷。具代表事實是,產生了一個新的關鍵字 __interrupt。下面的代碼就使用了__interrupt關鍵字去定義了一個中斷服務子程序(ISR),請評論一下這段代碼的。

__interrupt double compute_area (double radius) 
{
    double area = PI * radius * radius;
    printf("\nArea = %f", area);
    return area;
}
  1. ISR 不能返回一個值。
  2. ISR 不能傳遞參數。
  3. 在許多的處理器/編譯器中,浮點一般都是不可重入的。有些處理器/編譯器需要讓額處的寄存器入棧,有些處理器/編譯器就是不允許在ISR中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。
  4. printf()經常有重入和性能上的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM