Linux環境進程間通訊(二):信號(下)

linux下進程間通訊的幾種主要手段:php

  1. 管道(Pipe)及有名管道(named pipe):管道可用於具備親緣關係進程間的通訊,有名管道克服了管道沒有名字的限制,所以,除具備管道所具備的功能外,它還容許無親緣關係進程間的通訊; 
  2. 信號(Signal):信號是比較複雜的通訊方式,用於通知接受進程有某種事件發生,除了用於進程間通訊外,進程還能夠發送信號給進程自己;linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD爲了實現可靠信號機制,又可以統一對外接口,用sigaction函數從新實現了signal函數); 
  3. 報文(Message)隊列(消息隊列):消息隊列是消息的連接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程能夠向隊列中添加消息,被賦予讀權限的進程則能夠讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺點。 
  4. 共享內存:使得多個進程能夠訪問同一塊內存空間,是最快的可用IPC形式。是針對其餘通訊機制運行效率較低而設計的。每每與其它通訊機制,如信號量結合使用,來達到進程間的同步及互斥。 
  5. 信號量(semaphore):主要做爲進程間以及同一進程不一樣線程之間的同步手段。 
  6. 套接口(Socket):更爲通常的進程間通訊機制,可用於不一樣機器之間的進程間通訊。起初是由Unix系統的BSD分支開發出來的,但如今通常能夠移植到其它類Unix系統上:Linux和System V的變種都支持套接字。 

本文講述進程間通訊方法——信號html

原文:http://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.htmllinux

1、信號生命週期程序員

從信號發送到信號處理函數的執行完畢編程

對於一個完整的信號生命週期(從信號發送到相應的處理函數執行完畢)來講,能夠分爲三個重要的階段,這三個階段由四個重要事件來刻畫:信號誕生;信號在進程中註冊完畢;信號在進程中的註銷完畢;信號處理函數執行完畢。相鄰兩個事件的時間間隔構成信號生命週期的一個階段。安全


 

下面闡述四個事件的實際意義:數據結構

  1. 信號"誕生"。信號的誕生指的是觸發信號的事件發生(如檢測到硬件異常、定時器超時以及調用信號發送函數kill()或sigqueue()等)。
  2. 信號在目標進程中"註冊";進程的task_struct結構中有關於本進程中未決信號的數據成員:
    struct sigpending pending:
    struct sigpending{
    	struct sigqueue *head, **tail;
    	sigset_t signal;
    };
    

    第三個成員是進程中全部未決信號集,第1、第二個成員分別指向一個sigqueue類型的結構鏈(稱之爲"未決信號信息鏈")的首尾,信息鏈中的每一個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:
    struct sigqueue{
    	struct sigqueue *next;
    	siginfo_t info;
    }
    

    信號在進程中註冊指的就是信號值加入到進程的未決信號集中(sigpending結構的第二個成員sigset_t signal),而且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。 只要信號在進程的未決信號集中,代表進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

    注: 
    當一個實時信號發送給一個進程時,無論該信號是否已經在進程中註冊,都會被再註冊一次,所以,信號不會丟失,所以,實時信號又叫作"可靠信號"。這意味着同一個實時信號能夠在同一個進程的未決信號信息鏈中佔有多個sigqueue結構(進程每收到一個實時信號,都會爲它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即全部誕生的實時信號都會在目標進程中註冊); 
    當一個非實時信號發送給一個進程時,若是該信號已經在進程中註冊,則該信號將被丟棄,形成信號丟失。所以,非實時信號又叫作"不可靠信號"。這意味着同一個非實時信號在進程的未決信號信息鏈中,至多佔有一個sigqueue結構(一個非實時信號誕生後,(1)、若是發現相同的信號已經在目標結構中註冊,則再也不註冊,對於進程來講,至關於不知道本次信號發生,信號丟失;(2)、若是進程的未決信號中沒有相同信號,則在進程中註冊本身)。函數

  3. 信號在進程中的註銷。在目標進程執行過程當中,會檢測是否有信號等待處理(每次從系統空間返回到用戶空間時都作這樣的檢查)。若是存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中佔有的結構卸掉。是否將信號從進程未決信號集中刪除對於實時與非實時信號是不一樣的。對於非實時信號來講,因爲在未決信號信息鏈中最多隻佔用一個sigqueue結構,所以該結構被釋放後,應該把信號在進程未決信號集中刪除(信號註銷完畢);而對於實時信號來講,可能在未決信號信息鏈中佔用多個sigqueue結構,所以應該針對佔用sigqueue結構的數目區別對待:若是隻佔用一個sigqueue結構(進程只收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號註銷完畢)。不然,不該該在進程的未決信號集中刪除該信號(信號註銷完畢)。 
    進程在執行信號相應處理函數以前,首先要把信號在進程中註銷。
  4. 信號生命終止。進程註銷信號後,當即執行相應的信號處理函數,執行完畢後,信號的本次發送對進程的影響完全結束。

    注: 
    1)信號註冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多隻註冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被註冊)。 
    2)在信號被註銷到相應的信號處理函數執行完畢這段時間內,若是進程又收到同一信號屢次,則對實時信號來講,每一次都會在進程中註冊;而對於非實時信號來講,不管收到多少次信號,都會視爲只收到一個信號,只在進程中註冊一次。工具

2、信號編程注意事項
  1. 防止不應丟失的信號丟失。若是對八中所提到的信號生命週期理解深入的話,很容易知道信號會不會丟失,以及在哪裏丟失。
  2. 程序的可移植性 
    考慮到程序的可移植性,應該儘可能採用POSIX信號函數,POSIX信號函數主要分爲兩類:
    • POSIX 1003.1信號函數: Kill()、sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、sigpending()、sigprocmask()、sigsuspend()。
    • POSIX 1003.1b信號函數。POSIX 1003.1b在信號的實時性方面對POSIX 1003.1作了擴展,包括如下三個函數: sigqueue()、sigtimedwait()、sigwaitinfo()。 其中,sigqueue主要針對信號發送,而sigtimedwait及sigwaitinfo()主要用於取代sigsuspend()函數,後面有相應實例。
      #include <signal.h>
      int sigwaitinfo(sigset_t *set, siginfo_t *info).
      

      該函數與sigsuspend()相似,阻塞一個進程直到特定信號發生,但信號到來時不執行信號處理函數,而是返回信號值。所以爲了不執行相應的信號處理函數,必須在調用該函數前,使進程屏蔽掉set指向的信號,所以調用該函數的典型代碼是:
      sigset_t newmask;
      int rcvd_sig; 
      siginfo_t info;
      sigemptyset(&newmask);
      sigaddset(&newmask, SIGRTMIN);
      sigprocmask(SIG_BLOCK, &newmask, NULL);
      rcvd_sig = sigwaitinfo(&newmask, &info) 
      if (rcvd_sig == -1) {
      	..
      }
      

      調用成功返回信號值,不然返回-1。sigtimedwait()功能類似,只不過增長了一個進程等待的時間。
  3. 程序的穩定性。 
    爲了加強程序的穩定性,在信號處理函數中應使用可重入函數。

    信號處理程序中應當使用可再入(可重入)函數(注:所謂可重入函數是指一個能夠被多個任務調用的過程,任務在調用時沒必要擔憂數據是否會出錯)。由於進程在收到信號後,就將跳轉到信號處理函數去接着執行。若是信號處理函數中使用了不可重入函數,那麼信號處理函數可能會修改原來進程中不該該被修改的數據,這樣進程從信號處理函數中返回接着執行時,可能會出現不可預料的後果。不可再入函數在信號處理函數中被視爲不安全函數。ui

    知足下列條件的函數多數是不可再入的:(1)使用靜態的數據結構,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;(2)函數實現時,調用了malloc()或者free()函數;(3)實現時使用了標準I/O函數的。The Open Group視下列函數爲可再入的:

    _exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown() 、close()、creat()、dup()、dup2()、execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、 geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、kill()、link()、lseek()、mkdir()、mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、setgid()、setpgid()、setsid()、setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。

    即便信號處理函數使用的都是"安全函數",一樣要注意進入處理函數時,首先要保存errno的值,結束時,再恢復原值。由於,信號處理過程當中,errno值隨時可能被改變。另外,longjmp()以及siglongjmp()沒有被列爲可再入函數,由於不能保證緊接着兩個函數的其它調用是安全的。

3、深刻淺出:信號應用實例

linux下的信號應用並無想象的那麼恐怖,程序員所要作的最多隻有三件事情:

  1. 安裝信號(推薦使用sigaction());
  2. 實現三參數信號處理函數,handler(int signal,struct siginfo *info, void *);
  3. 發送信號,推薦使用sigqueue()。

實際上,對有些信號來講,只要安裝信號就足夠了(信號處理方式採用缺省或忽略)。其餘可能要作的無非是與信號集相關的幾種操做。

實例一:信號發送及處理 
實現一個信號接收程序sigreceive(其中信號安裝由sigaction())。

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
	struct sigaction act;	
	int sig;
	sig=atoi(argv[1]);
	
	sigemptyset(&act.sa_mask);
	act.sa_flags=SA_SIGINFO;
	act.sa_sigaction=new_op;
	
	if(sigaction(sig,&act,NULL) < 0)
	{
		printf("install sigal error\n");
	}
	
	while(1)
	{
		sleep(2);
		printf("wait for the signal\n");
	}
}
void new_op(int signum,siginfo_t *info,void *myact)
{
	printf("receive signal %d", signum);
	sleep(5);
}

 

說明,命令行參數爲信號值,後臺運行sigreceive signo &,可得到該進程的ID,假設爲pid,而後再另外一終端上運行kill -s signo pid驗證信號的發送接收及處理。同時,可驗證信號的排隊問題。 
注:能夠用sigqueue實現一個命令行信號發送程序sigqueuesend,見 附錄1

實例二:信號傳遞附加信息 
主要包括兩個實例:

  1. 向進程自己發送信號,並傳遞指針參數;
    #include <signal.h>
    #include <sys/types.h>
    #include <unistd.h>
    void new_op(int,siginfo_t*,void*);
    int main(int argc,char**argv)
    {
    	struct sigaction act;	
    	union sigval mysigval;
    	int i;
    	int sig;
    	pid_t pid;		
    	char data[10];
    	memset(data,0,sizeof(data));
    	for(i=0;i < 5;i++)
    		data[i]='2';
    	mysigval.sival_ptr=data;
    	
    	sig=atoi(argv[1]);
    	pid=getpid();
    	
    	sigemptyset(&act.sa_mask);
    	act.sa_sigaction=new_op;//三參數信號處理函數
    	act.sa_flags=SA_SIGINFO;//信息傳遞開關
    	if(sigaction(sig,&act,NULL) < 0)
    	{
    		printf("install sigal error\n");
    	}
    	while(1)
    	{
    		sleep(2);
    		printf("wait for the signal\n");
    		sigqueue(pid,sig,mysigval);//向本進程發送信號,並傳遞附加信息
    	}
    }
    void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實現
    {
    	int i;
    	for(i=0;i<10;i++)
    	{
    		printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));
    	}
    	printf("handle signal %d over;",signum);
    }
    

    這個例子中,信號實現了附加信息的傳遞,信號究竟如何對這些信息進行處理則取決於具體的應用。

  2. 二、 不一樣進程間傳遞整型參數:把1中的信號發送和接收放在兩個程序中,而且在發送過程當中傳遞整型參數。 
    信號接收程序:
    #include <signal.h>
    #include <sys/types.h>
    #include <unistd.h>
    void new_op(int,siginfo_t*,void*);
    int main(int argc,char**argv)
    {
    	struct sigaction act;
    	int sig;
    	pid_t pid;		
    	
    	pid=getpid();
    	sig=atoi(argv[1]);	
    	
    	sigemptyset(&act.sa_mask);
    	act.sa_sigaction=new_op;
    	act.sa_flags=SA_SIGINFO;
    	if(sigaction(sig,&act,NULL)<0)
    	{
    		printf("install sigal error\n");
    	}
    	while(1)
    	{
    		sleep(2);
    		printf("wait for the signal\n");
    	}
    }
    void new_op(int signum,siginfo_t *info,void *myact)
    {
    	printf("the int value is %d \n",info->si_int);
    }
    

    信號發送程序:命令行第二個參數爲信號值,第三個參數爲接收進程ID。

    #include <signal.h>
    #include <sys/time.h>
    #include <unistd.h>
    #include <sys/types.h>
    main(int argc,char**argv)
    {
    	pid_t pid;
    	int signum;
    	union sigval mysigval;
    	signum=atoi(argv[1]);
    	pid=(pid_t)atoi(argv[2]);
    	mysigval.sival_int=8;//不表明具體含義,只用於說明問題
    	if(sigqueue(pid,signum,mysigval)==-1)
    		printf("send error\n");
    	sleep(2);
    }
    

    注:實例2的兩個例子側重點在於用信號來傳遞信息,目前關於在linux下經過信號傳遞信息的實例很是少,卻是Unix下有一些,但傳遞的基本上都是關於傳遞一個整數,傳遞指針的我還沒看到。我一直沒有實現不一樣進程間的指針傳遞(實際上更有意義),也許在實現方法上存在問題吧,請實現者email我。

實例三:信號阻塞及信號集操做

#include "signal.h"
#include "unistd.h"
static void my_op(int);
main()
{
	sigset_t new_mask,old_mask,pending_mask;
	struct sigaction act;
	sigemptyset(&act.sa_mask);
	act.sa_flags=SA_SIGINFO;
	act.sa_sigaction=(void*)my_op;
	if(sigaction(SIGRTMIN+10,&act,NULL))
		printf("install signal SIGRTMIN+10 error\n");
	sigemptyset(&new_mask);
	sigaddset(&new_mask,SIGRTMIN+10);
	if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))
		printf("block signal SIGRTMIN+10 error\n");
	sleep(10);	
	printf("now begin to get pending mask and unblock SIGRTMIN+10\n");
	if(sigpending(&pending_mask)<0)
		printf("get pending mask error\n");
	if(sigismember(&pending_mask,SIGRTMIN+10))
		printf("signal SIGRTMIN+10 is pending\n");
	if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)
		printf("unblock signal error\n");
	printf("signal unblocked\n");
	sleep(10);
}
static void my_op(int signum)
{
	printf("receive signal %d \n",signum);
}

編譯該程序,並之後臺方式運行。在另外一終端向該進程發送信號(運行kill -s 42 pid,SIGRTMIN+10爲42),查看結果能夠看出幾個關鍵函數的運行機制,信號集相關操做比較簡單。

注:在上面幾個實例中,使用了printf()函數,只是做爲診斷工具,pringf()函數是不可重入的,不該在信號處理函數中使用。

結束語:

系統地對linux信號機制進行分析、總結使我受益不淺!感謝王小樂等網友的支持! 
Comments and suggestions are greatly welcome!

附錄1:

用sigqueue實現的命令行信號發送程序sigqueuesend,命令行第二個參數是發送的信號值,第三個參數是接收該信號的進程ID,能夠配合實例一使用:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char**argv)
{
	pid_t pid;
	int sig;
	sig=atoi(argv[1]);
	pid=atoi(argv[2]);
	sigqueue(pid,sig,NULL);
	sleep(2);
}

參考資料

相關文章
相關標籤/搜索