Linux環境進程間通訊(四):信號燈

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

  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的變種都支持套接字。 

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

原文:http://www.ibm.com/developerworks/cn/linux/l-ipc/part4/數組

1、信號燈概述安全

信號燈與其餘進程間通訊方式不大相同,它主要提供對進程間共享資源訪問控制機制。至關於內存中的標誌,進程能夠根據它斷定是否可以訪問某些共享資源,同時,進程也能夠修改該標誌。除了用於訪問控制外,還可用於進程同步。信號燈有如下兩種類型:網絡

  • 二值信號燈:最簡單的信號燈形式,信號燈的值只能取0或1,相似於互斥鎖。 
    注:二值信號燈可以實現互斥鎖的功能,但二者的關注內容不一樣。信號燈強調共享資源,只要共享資源可用,其餘進程一樣能夠修改信號燈的值;互斥鎖更強調進程,佔用資源的進程使用完資源後,必須由進程自己來解鎖。
  • 計算信號燈:信號燈的值能夠取任意非負值(固然受內核自己的約束)。
2、Linux信號燈

linux對信號燈的支持情況與消息隊列同樣,在red had 8.0發行版本中支持的是系統V的信號燈。所以,本文將主要介紹系統V信號燈及其相應API。在沒有聲明的狀況下,如下討論中指的都是系統V信號燈。數據結構

注意,一般所說的系統V信號燈指的是計數信號燈集。ide

3、信號燈與內核

一、系統V信號燈是隨內核持續的,只有在內核重起或者顯示刪除一個信號燈集時,該信號燈集纔會真正被刪除。所以系統中記錄信號燈的數據結構(struct ipc_ids sem_ids)位於內核中,系統中的全部信號燈均可以在結構sem_ids中找到訪問入口。函數

二、下圖說明了內核與信號燈是怎樣創建起聯繫的:測試

其中:struct ipc_ids sem_ids是內核中記錄信號燈的全局數據結構;描述一個具體的信號燈及其相關信息。ui

 

其中,struct sem結構以下:

struct sem{
int semval;		// current value
int sempid		// pid of last operation
}

從上圖能夠看出,全局數據結構struct ipc_ids sem_ids能夠訪問到struct kern_ipc_perm的第一個成員:struct kern_ipc_perm;而每一個struct kern_ipc_perm可以與具體的信號燈對應起來是由於在該結構中,有一個key_t類型成員key,而key則惟一肯定一個信號燈集;同時,結構struct kern_ipc_perm的最後一個成員sem_nsems肯定了該信號燈在信號燈集中的順序,這樣內核就可以記錄每一個信號燈的信息了。kern_ipc_perm結構參見《Linux環境進程間通訊(三):消息隊列》。struct sem_array見附錄1。

4、操做信號燈

對消息隊列的操做無非有下面三種類型:

一、 打開或建立信號燈 
與消息隊列的建立及打開基本相同,再也不詳述。

二、 信號燈值操做 
linux能夠增長或減少信號燈的值,相應於對共享資源的釋放和佔有。具體參見後面的semop系統調用。

三、 得到或設置信號燈屬性: 
系統中的每個信號燈集都對應一個struct sem_array結構,該結構記錄了信號燈集的各類信息,存在於系統空間。爲了設置、得到該信號燈集的各類信息及屬性,在用戶空間有一個重要的聯合結構與之對應,即union semun。

 

聯合semun數據結構各成員意義參見附錄2

信號燈API

一、文件名到鍵值

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (char*pathname, char proj);

它返回與路徑pathname相對應的一個鍵值,具體用法請參考《Linux環境進程間通訊(三):消息隊列》。

二、 linux特有的ipc()調用:

int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);

參數call取不一樣值時,對應信號燈的三個系統調用: 
當call爲SEMOP時,對應int semop(int semid, struct sembuf *sops, unsigned nsops)調用; 
當call爲SEMGET時,對應int semget(key_t key, int nsems, int semflg)調用; 
當call爲SEMCTL時,對應int semctl(int semid,int semnum,int cmd,union semun arg)調用; 
這些調用將在後面闡述。

注:本人不主張採用系統調用ipc(),而更傾向於採用系統V或者POSIX進程間通訊API。緣由已在Linux環境進程間通訊(三):消息隊列中給出。

三、系統V信號燈API

系統V消息隊列API只有三個,使用時須要包括幾個頭文件:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

1)int semget(key_t key, int nsems, int semflg) 

參數key是一個鍵值,由ftok得到,惟一標識一個信號燈集,用法與msgget()中的key相同;參數nsems指定打開或者新建立的信號燈集中將包含信號燈的數目;semflg參數是一些標誌位。參數key和semflg的取值,以及什麼時候打開已有信號燈集或者建立一個新的信號燈集與msgget()中的對應部分相同,再也不祥述。 
該調用返回與健值key相對應的信號燈集描述字。 
調用返回:成功返回信號燈集描述字,不然返回-1。 
注:若是key所表明的信號燈已經存在,且semget指定了IPC_CREAT|IPC_EXCL標誌,那麼即便參數nsems與原來信號燈的數目不等,返回的也是EEXIST錯誤;若是semget只指定了IPC_CREAT標誌,那麼參數nsems必須與原來的值一致,在後面程序實例中還要進一步說明。

2)int semop(int semid, struct sembuf *sops, unsigned nsops); 
semid是信號燈集ID,sops指向數組的每個sembuf結構都刻畫一個在特定信號燈上的操做。nsops爲sops指向數組的大小。 
sembuf結構以下:

struct sembuf {
	unsigned short  	sem_num;		/* semaphore index in array */
	short			sem_op;		/* semaphore operation */
	short			sem_flg;		/* operation flags */
};

sem_num對應信號集中的信號燈,0對應第一個信號燈。sem_flg可取IPC_NOWAIT以及SEM_UNDO兩個標誌。若是設置了SEM_UNDO標誌,那麼在進程結束時,相應的操做將被取消,這是比較重要的一個標誌位。若是設置了該標誌位,那麼在進程沒有釋放共享資源就退出時,內核將代爲釋放。若是爲一個信號燈設置了該標誌,內核都要分配一個sem_undo結構來記錄它,爲的是確保之後資源可以安全釋放。事實上,若是進程退出了,那麼它所佔用就釋放了,但信號燈值卻沒有改變,此時,信號燈值反映的已經不是資源佔有的實際狀況,在這種狀況下,問題的解決就靠內核來完成。這有點像殭屍進程,進程雖然退出了,資源也都釋放了,但內核進程表中仍然有它的記錄,此時就須要父進程調用waitpid來解決問題了。 

sem_op的值大於0,等於0以及小於0肯定了對sem_num指定的信號燈進行的三種操做。具體請參考linux相應手冊頁。 
這裏須要強調的是semop同時操做多個信號燈,在實際應用中,對應多種資源的申請或釋放。semop保證操做的原子性,這一點尤其重要。尤爲對於多種資源的申請來講,要麼一次性得到全部資源,要麼放棄申請,要麼在不佔有任何資源狀況下繼續等待,這樣,一方面避免了資源的浪費;另外一方面,避免了進程之間因爲申請共享資源形成死鎖。 
也許從實際含義上更好理解這些操做:信號燈的當前值記錄相應資源目前可用數目;sem_op>0對應相應進程要釋放sem_op數目的共享資源;sem_op=0能夠用於對共享資源是否已用完的測試;sem_op<0至關於進程要申請-sem_op個共享資源。再聯想操做的原子性,更不難理解該系統調用什麼時候正常返回,什麼時候睡眠等待。 
調用返回:成功返回0,不然返回-1。

3) int semctl(int semid,int semnum,int cmd,union semun arg) 
該系統調用實現對信號燈的各類控制操做,參數semid指定信號燈集,參數cmd指定具體的操做類型;參數semnum指定對哪一個信號燈操做,只對幾個特殊的cmd操做有意義;arg用於設置或返回信號燈信息。 
該系統調用詳細信息請參見其手冊頁,這裏只給出參數cmd所能指定的操做。

IPC_STAT 獲取信號燈信息,信息由arg.buf返回;
IPC_SET 設置信號燈信息,待設置信息保存在arg.buf中(在manpage中給出了能夠設置哪些信息);
GETALL 返回全部信號燈的值,結果保存在arg.array中,參數sennum被忽略;
GETNCNT 返回等待semnum所表明信號燈的值增長的進程數,至關於目前有多少進程在等待semnum表明的信號燈所表明的共享資源;
GETPID 返回最後一個對semnum所表明信號燈執行semop操做的進程ID;
GETVAL 返回semnum所表明信號燈的值;
GETZCNT 返回等待semnum所表明信號燈的值變成0的進程數;
SETALL 經過arg.array更新全部信號燈的值;同時,更新與本信號集相關的semid_ds結構的sem_ctime成員;
SETVAL 設置semnum所表明信號燈的值爲arg.val;

調用返回:調用失敗返回-1,成功返回與cmd相關:

Cmd return value
GETNCNT Semncnt
GETPID Sempid
GETVAL Semval
GETZCNT Semzcnt
5、信號燈的限制

一、 一次系統調用semop可同時操做的信號燈數目SEMOPM,semop中的參數nsops若是超過了這個數目,將返回E2BIG錯誤。SEMOPM的大小特定與系統,redhat 8.0爲32。

二、 信號燈的最大數目:SEMVMX,當設置信號燈值超過這個限制時,會返回ERANGE錯誤。在redhat 8.0中該值爲32767。

三、 系統範圍內信號燈集的最大數目SEMMNI以及系統範圍內信號燈的最大數目SEMMNS。超過這兩個限制將返回ENOSPC錯誤。redhat 8.0中該值爲32000。

四、 每一個信號燈集中的最大信號燈數目SEMMSL,redhat 8.0中爲250。 SEMOPM以及SEMVMX是使用semop調用時應該注意的;SEMMNI以及SEMMNS是調用semget時應該注意的。SEMVMX同時也是semctl調用應該注意的。

6、競爭問題

第一個建立信號燈的進程同時也初始化信號燈,這樣,系統調用semget包含了兩個步驟:建立信號燈;初始化信號燈。由此可能致使一種競爭狀態:第一個建立信號燈的進程在初始化信號燈時,第二個進程又調用semget,而且發現信號燈已經存在,此時,第二個進程必須具備判斷是否有進程正在對信號燈進行初始化的能力。在參考文獻[1]中,給出了繞過這種競爭狀態的方法:當semget建立一個新的信號燈時,信號燈結構semid_ds的sem_otime成員初始化後的值爲0。所以,第二個進程在成功調用semget後,可再次以IPC_STAT命令調用semctl,等待sem_otime變爲非0值,此時可判斷該信號燈已經初始化完畢。下圖描述了競爭狀態產生及解決方法:

 

實際上,這種解決方法也是基於這樣一個假定:第一個建立信號燈的進程必須調用semop,這樣sem_otime才能變爲非零值。另外,由於第一個進程可能不調用semop,或者semop操做須要很長時間,第二個進程可能無限期等待下去,或者等待很長時間。

7、信號燈應用實例

本實例有兩個目的:一、獲取各類信號燈信息;二、利用信號燈實現共享資源的申請和釋放。並在程序中給出了詳細註釋。

#include <linux/sem.h>
#include <stdio.h>
#include <errno.h>
#define SEM_PATH "/unix/my_sem"
#define max_tries 3 
int semid;
main()
{
int flag1,flag2,key,i,init_ok,tmperrno;
struct semid_ds sem_info;
struct seminfo sem_info2;
union semun arg;       //union semun: 請參考附錄2
struct sembuf askfor_res, free_res;
flag1=IPC_CREAT|IPC_EXCL|00666;
flag2=IPC_CREAT|00666;
key=ftok(SEM_PATH,'a');
//error handling for ftok here;
init_ok=0;
semid=semget(key,1,flag1);
//create a semaphore set that only includes one semphore.
if(semid<0)
{
  tmperrno=errno;
  perror("semget");
if(tmperrno==EEXIST)
//errno is undefined after a successful library call( including perror call) 
//so it is saved  in tmperrno.
    {
    semid=semget(key,1,flag2);
//flag2 只包含了IPC_CREAT標誌, 參數nsems(這裏爲1)必須與原來的信號燈數目一致
    arg.buf=&sem_info;
    for(i=0; i<max_tries; i++)
    {
      if(semctl(semid, 0, IPC_STAT, arg)==-1)
      {  perror("semctl error"); i=max_tries;}
      else
      { 
        if(arg.buf->sem_otime!=0){ i=max_tries;  init_ok=1;}
        else   sleep(1);  
      }
    }
    if(!init_ok)
  // do some initializing, here we assume that the first process that creates the sem
  //  will finish initialize the sem and run semop in max_tries*1 seconds. else it will  
  // not run semop any more.
    {
      arg.val=1;
      if(semctl(semid,0,SETVAL,arg)==-1) perror("semctl setval error");
    } 
  }
  else
  {perror("semget error, process exit");  exit();  }
}
else //semid>=0; do some initializing   
{
  arg.val=1;
  if(semctl(semid,0,SETVAL,arg)==-1)
    perror("semctl setval error");
}
//get some information about the semaphore and the limit of semaphore in redhat8.0
  arg.buf=&sem_info;
  if(semctl(semid, 0, IPC_STAT, arg)==-1)
    perror("semctl IPC STAT");    
  printf("owner's uid is %d\n",   arg.buf->sem_perm.uid);
  printf("owner's gid is %d\n",   arg.buf->sem_perm.gid);
  printf("creater's uid is %d\n",   arg.buf->sem_perm.cuid);
  printf("creater's gid is %d\n",   arg.buf->sem_perm.cgid);
  arg.__buf=&sem_info2;
  if(semctl(semid,0,IPC_INFO,arg)==-1)
    perror("semctl IPC_INFO");
  printf("the number of entries in semaphore map is %d \n",  arg.__buf->semmap);
  printf("max number of semaphore identifiers is %d \n",    arg.__buf->semmni);
  printf("mas number of semaphores in system is %d \n",   arg.__buf->semmns);
  printf("the number of undo structures system wide is %d \n",  arg.__buf->semmnu);
  printf("max number of semaphores per semid is %d \n",   arg.__buf->semmsl);
  printf("max number of ops per semop call is %d \n",  arg.__buf->semopm);
  printf("max number of undo entries per process is %d \n",  arg.__buf->semume);
  printf("the sizeof of struct sem_undo is %d \n",  arg.__buf->semusz);
  printf("the maximum semaphore value is %d \n",  arg.__buf->semvmx);
  
//now ask for available resource:  
  askfor_res.sem_num=0;
  askfor_res.sem_op=-1;
  askfor_res.sem_flg=SEM_UNDO;    
    
    if(semop(semid,&askfor_res,1)==-1)//ask for resource
      perror("semop error");
  
  sleep(3); 
  //do some handling on the sharing resource here, just sleep on it 3 seconds
  printf("now free the resource\n");  
  
//now free resource  
  free_res.sem_num=0;
  free_res.sem_op=1;
  free_res.sem_flg=SEM_UNDO;
  if(semop(semid,&free_res,1)==-1)//free the resource.
    if(errno==EIDRM)
      printf("the semaphore set was removed\n");
//you can comment out the codes below to compile a different version:      
  if(semctl(semid, 0, IPC_RMID)==-1)
    perror("semctl IPC_RMID");
  else printf("remove sem ok\n");
}

注:讀者能夠嘗試一下注釋掉初始化步驟,進程在運行時會出現何種狀況(進程在申請資源時會睡眠),同時能夠像程序結尾給出的註釋那樣,把該程序編譯成兩個不一樣版本。下面是本程序的運行結果(操做系統redhat8.0):

owner's uid is 0
owner's gid is 0
creater's uid is 0
creater's gid is 0
the number of entries in semaphore map is 32000 
max number of semaphore identifiers is 128 
mas number of semaphores in system is 32000 
the number of undo structures system wide is 32000 
max number of semaphores per semid is 250 
max number of ops per semop call is 32 
max number of undo entries per process is 32 
the sizeof of struct sem_undo is 20 
the maximum semaphore value is 32767 
now free the resource
remove sem ok

Summary:信號燈與其它進程間通訊方式有所不一樣,它主要用於進程間同步。一般所說的系統V信號燈其實是一個信號燈的集合,可用於多種共享資源的進程間同步。每一個信號燈都有一個值,能夠用來表示當前該信號燈表明的共享資源可用(available)數量,若是一個進程要申請共享資源,那麼就從信號燈值中減去要申請的數目,若是當前沒有足夠的可用資源,進程能夠睡眠等待,也能夠當即返回。當進程要申請多種共享資源時,linux能夠保證操做的原子性,即要麼申請到全部的共享資源,要麼放棄全部資源,這樣可以保證多個進程不會形成互鎖。Linux對信號燈有各類各樣的限制,程序中給出了輸出結果。另外,若是讀者想對信號燈做進一步的理解,建議閱讀sem.h源代碼,該文件不長,但給出了信號燈相關的重要數據結構。

附錄1: struct sem_array以下:

/*系統中的每一個信號燈集對應一個sem_array 結構 */
struct sem_array {
  struct kern_ipc_perm  sem_perm;    /* permissions .. see ipc.h */
  time_t      sem_otime;      /* last semop time */
  time_t      sem_ctime;      /* last change time */
  struct sem    *sem_base;      /* ptr to first semaphore in array */
  struct sem_queue  *sem_pending;    /* pending operations to be processed */
  struct sem_queue  **sem_pending_last;   /* last pending operation */
  struct sem_undo    *undo;      /* undo requests on this array */
  unsigned long    sem_nsems;    /* no. of semaphores in array */
};

其中,sem_queue結構以下:

/* 系統中每一個由於信號燈而睡眠的進程,都對應一個sem_queue結構*/
 struct sem_queue {
  struct sem_queue *  next;     /* next entry in the queue */
  struct sem_queue **  prev; 
  /* previous entry in the queue, *(q->prev) == q */
  struct task_struct*  sleeper;   /* this process */
  struct sem_undo *  undo;     /* undo structure */
  int   pid;             /* process id of requesting process */
  int   status;           /* completion status of operation */
  struct sem_array *  sma;       /* semaphore array for operations */
  int  id;               /* internal sem id */
  struct sembuf *  sops;       /* array of pending operations */
  int  nsops;             /* number of operations */
  int  alter;             /* operation will alter semaphore */
};

附錄2:union semun是系統調用semctl中的重要參數:

union semun {
	int val;					/* value for SETVAL */
	struct semid_ds *buf;		/* buffer for IPC_STAT & IPC_SET */
	unsigned short *array;		/* array for GETALL & SETALL */
	struct seminfo *__buf;		/* buffer for IPC_INFO */   //test!!
	void *__pad;
};
struct  seminfo {
	int semmap;
	int semmni;
	int semmns;
	int semmnu;
	int semmsl;
	int semopm;
	int semume;
	int semusz;
	int semvmx;
	int semaem;
};

參考資料

[1] UNIX網絡編程第二卷:進程間通訊,做者:W.Richard Stevens,譯者:楊繼張,清華大學出版社。對POSIX以及系統V信號燈都有闡述,對Linux環境下的程序開發有極大的啓發意義。

[2] linux內核源代碼情景分析(上),毛德操、胡希明著,浙江大學出版社,給出了系統V信號燈相關的源代碼分析,尤爲在闡述保證操做原子性方面,以及闡述undo標誌位時,討論的很深入。

[3]GNU/Linux編程指南,第二版,Kurt Wall等著,張輝譯

[4]semget、semop、semctl手冊

相關文章
相關標籤/搜索