linux進程同步之信號量

      首先了解一下,信號量機概念是由荷蘭科學家Dijkstr引入,值得一提的是,它提出的Dijksrtr算法解決了最短路徑問題。

      信號量又稱爲信號燈,它是用來協調不一樣進程間的數據對象的,而最主要的應用是共享內存方式的進程間通訊。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取情況,信號量是一個特殊的變量,而且只有兩個操做能夠改變其值:等待(wait)與信號(signal)。html

 

由於在Linux與UNIX編程中,"wait"與"signal"已經具備特殊的意義了(暫不知這特殊意義是啥),因此原始概念:
     用於等待(wait)的P(信號量變量) ;
     用於信號(signal)的V(信號量變量) ;
這兩字母來自等待(passeren:經過,如同臨界區前的檢測點)與信號(vrjgeven:指定或釋放,如同釋放臨界區的控制權)的荷蘭語。
linux

 

P操做 負責把當前進程由運行狀態轉換爲阻塞狀態,直到另一個進程喚醒它。算法

操做爲:申請一個空閒資源(把信號量減1),若成功,則退出;若失敗,則該進程被阻塞;編程

 

V操做 負責把一個被阻塞的進程喚醒,它有一個參數表,存放着等待被喚醒的進程信息。數組

操做爲:釋放一個被佔用的資源(把信號量加1),若是發現有被阻塞的進程,則選擇一個喚醒之。  數據結構

 

補充:查看共享信息的內存的命令是ipcs [-m|-s|-q] (所有的話是ipcs -a) ;查看共享信息的內存的命令是ipcs [-m|-s|-q]。ide

(一)系統調用函數semget()

函數原型:int semget(key_t key,int nsems,int semflg); 函數

功能描述: 建立一個新的信號量集,或者存取一個已經存在的信號量集。測試

當調用semget建立一個信號量時,他的相應的semid_ds結構被初始化。ipc_perm中各個量被設置爲相應
值:
        sem_nsems被設置爲nsems所示的值;    
        sem_otime被設置爲0; 
        sem_ctime被設置爲當前時間
ui

參數介紹:
         key:所建立或打開信號量集的鍵值,
鍵值是IPC_PRIVATE,該值一般爲0,建立一個僅能被進程進程給個人信號量, 鍵值不是IPC_PRIVATE,咱們能夠指定鍵值,例如1234;也能夠一個ftok()函數來取得一個惟一的鍵值。
         nsems:建立的信號量集中的信號量的個數,該參數只在建立信號量集時有效。
         semflg:調用函數的操做類型,也可用於設置信號量集的訪問權限,二者經過or表示:

                有IPC_CREAT,IPC_EXCL兩種:

IPC_CREAT若是信號量不存在,則建立一個信號量,不然獲取。

IPC_EXCL只有信號量不存在的時候,新的信號量才創建,不然就產生錯誤。


返回值說明:
若是成功,則返回信號量集的IPC標識符,其做用與信息隊列識符同樣。
若是失敗,則返回-1,errno被設定成如下的某個值
EACCES:沒有訪問該信號量集的權限
EEXIST:信號量集已經存在,沒法建立
EINVAL:參數nsems的值小於0或者大於該信號量集的限制;或者是該key關聯的信號量集已存在,而且nsems
大於該信號量集的信號量數
ENOENT:信號量集不存在,同時沒有使用IPC_CREAT
ENOMEM :沒有足夠的內存建立新的信號量集
ENOSPC:超出系統限制

圖解:

7QTBL958849ZUXZ6HQD_thumb4

每一個信號量都有一些相關值:

      semval 信號量的值,通常是一個正整數,它只能經過信號量系統調用semctl函數設置,程序沒法直接對它進行修改。

      sempid 最後一個對信號量進行操做的進程的pid.

      semcnt 等待信號量的值大於其當前值的進程數。

      semzcnt 等待信號量的值歸零的進程數。

 

(二)信號量的控制 semctl()

原型:int semctl(int semid,int semnum,int cmd,union semun ctl_arg); 
參數介紹: semid爲信號量集引用標誌符,即semget 的返回值。 
               semnum第二個參數是信號量數目;

               cmd表示調用該函數執行的操做,其取值和對應操做以下:

標準的IPC函數

(注意在頭文件<sys/sem.h>中包含semid_ds結構的定義)

IPC_STAT 把狀態信息放入ctl_arg.stat中

IPC_SET 用ctl_arg.stat中的值設置全部權/許可權

IPC_RMID 從系統中刪除信號量集合

單信號量操做

(下面這些宏與sem_num指定的信號量合semctl返回值相關)

GETVAL 返回信號量的值(也就是semval)

SETVAL 把信號量的值寫入ctl_arg.val中

GETPID 返回sempid值

GETNCNT 返回semncnt(參考上面內容)

GETZCNT 返回semzcnt(參考上面內容)

全信號量操做

GETALL 把全部信號量的semvals值寫入ctl_arg.array

SETALL 用ctl_arg.array中的值設置全部信號量的semvals

 

參數arg表明一個union的semun的實例。semun是在linux/sem.h中定義的:

union semun {
int val; //執行SETVAL命令時使用
struct semid_ds *buf; //在IPC_STAT/IPC_SET命令中使用
unsigned short *array; //使用GETALL/SETALL命令時使用的指針
}


聯合體中每一個成員都有各自不一樣的類型,分別對應三種不一樣的semctl 功能,若是semval 是SETVAL.則使用的將是ctl_arg.val.

。     

功能:smctl函數依據command參數會返回不一樣的值。它的一個重要用途是爲信號量賦初值,由於進程沒法直接對信號量的值進行修改。

(三)信號量操做semop函數

在 Linux 下,PV 操做經過調用semop函數來實現,也只有它能對PV進行操做

調用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);
返回值:0,若是成功。-1,若是失敗:errno=E2BIG(nsops大於最大的ops數目)
EACCESS(權限不夠)
EAGAIN(使用了IPC_NOWAIT,但操做不能繼續進行)
EFAULT(sops指向的地址無效)
EIDRM(信號量集已經刪除)
EINTR(當睡眠時接收到其餘信號)
EINVAL(信號量集不存在,或者semid無效)
ENOMEM(使用了SEM_UNDO,但無足夠的內存建立所需的數據結構)
ERANGE(信號量值超出範圍)

參數介紹:

第一個參數semid 是信號量集合標識符,它多是從前一次的semget調用中得到的。


第二個參數是一個sembuf結構的數組每一個 sembuf 結構體對應一個特定信號的操做sembuf結構在,<sys/sem.h>中定義

struct sembuf{
usign short sem_num;/*信號量索引*/
short sem_op;/*要執行的操做*/
short sem_flg;/*操做標誌*/
}

sem_num 存放集合中某一信號量的索引,若是集合中只包含一個元素,則sem_num的值只能爲0。

----------------------------------------------------------------------------------------------

Sem_op取得值爲一個有符號整數,該整數實際給定了semop函數將完成的功能。包括三種狀況:

      若是sem_op是負數,那麼信號量將減去它的值,對應於p()操做。這和信號量控制的資源有關。若是沒有使用IPC_NOWAIT,那麼調用進程將進入睡眠狀態,直到信號量控制的資源可使用爲止。

      若是sem_op是正數,則信號量加上它的值。對應於v()操做。這也就是進程釋放信號量控制的資源。

      最後,若是sem_op是0,那麼調用進程將調用sleep(),直到信號量的值爲0。這在一個進程等待徹底空閒的資源時使用。

----------------------------------------------------------------------------------------------

sem_flag是用來告訴系統當進程退出時自動還原操做,它維護着一個整型變量semadj(信號燈的計數器),可設置爲 IPC_NOWAIT 或 SEM_UNDO 兩種狀態。只有將 sem_flg 指定爲 SEM_UNDO 標誌後,semadj (所指定信號量針對調用進程的調整值)纔會更新,即減去減去sem_num的值。 此外,若是此操做指定SEM_UNDO,系統更新過程當中會撤消此信號燈的計數(semadj)。此操做能夠隨時進行---它永遠不會強制等待的過程。調用進程必須有改變信號量集的權限。


 

第三個參數是sembuf組成的數組中索引。參數sops指向由sembuf組成的數組,結構數組中的一員。

實驗代碼:

實驗所需頭文件:放在/usr/include目錄下

//pv.h頭文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>

#define SEMPERM 0600
#define TRUE 1
#define FALSE 0
typedef union _semun {
  int val;
  struct semid_ds *buf;
  ushort *array;
} semun;
View Code

信號量賦初值以及獲取信號量標識符函數:

//initsem.c  對信號量賦初值,初值固定爲1
#include "pv.h"
int initsem(key_t semkey)
{
   int status=0,semid;                    //信號量標識符semid
  if ((semid=semget(semkey,1,SEMPERM|IPC_CREAT|IPC_EXCL))==-1)
  {
    if (errno==EEXIST)               //EEXIST:信號量集已經存在,沒法建立
      semid=semget(semkey,1,0);      //建立一個信號量
  }
  else
  {
    semun arg;
    arg.val=1;                                        //信號量的初值
    status=semctl(semid,0,SETVAL,arg);      //設置信號量集中的一個單獨的信號量的值。
  }
  if (semid==-1||status==-1)
  {
    perror("initsem failed");
    return(-1);
  }
  /*all ok*/
  return(semid);
}
View Code

v操做

//v.c  V操做
#include "pv.h"
int v(int semid)
{
  struct sembuf v_buf;

  v_buf.sem_num=0;
  v_buf.sem_op=1;    //信號量加1
  v_buf.sem_flg=SEM_UNDO;
  
  if (semop(semid, &v_buf, 1)==-1)
  {
    perror("v(semid)failed");
    exit(1);
  }
  return(0);
}
View Code

p操做

//p.c  P操做
#include "pv.h"
int p(int semid)
{
  struct sembuf p_buf;

  p_buf.sem_num=0;
  p_buf.sem_op=-1;        //信號量減1,注意這一行的1前面有個負號
  p_buf.sem_flg=SEM_UNDO;
  
  //p_buf = {0,-1,SEM_UNDO};
  if (semop(semid, &p_buf, 1)==-1)   
  {
    perror("p(semid)failed");
    exit(1);
  }
  return(0);
}
View Code

測試函數一(使用PV操做實現三個進程的互斥)

//testsem.c  主程序,使用PV操做實現三個進程的互斥
#include "pv.h"
void handlesem(key_t skey);
main()
{
  key_t semkey=0x200;
  int i;
  for (i=0;i<3;i++)
  {
    if (fork()==0)           //父進程負責產生3個子進程
      handlesem(semkey);  //子進程中才執行handlesem,作完後就exit。
  }
}

void handlesem(key_t skey)
{
  int semid;
  pid_t pid=getpid();
  
  if ((semid=initsem(skey))<0)
    exit(1);
  printf("進程 %d 在臨界資源區以前 \n",pid);
  p(semid);                                      //進程進入臨界資源區,信號量減小1
  printf("進程 %d 在使用臨界資源時,中止10s \n",pid);

  /*in real life do something interesting */
  sleep(10);
  printf("進程 %d 退出臨界區後 \n",pid);

  v(semid);                                //進程退出臨界資源區,信號量加1

  printf("進程 %d 徹底退出\n",pid);
  exit(0);
}
View Code

測試結果截圖:

X8}GM8XO8(}01A6L}UA5]SS_thumb[3]

測試函數二(實現兩個進程交替輸出A和B,並在程序中查看信號量的值)

//ab.c  主程序,使用PV操做,兩個進程交替輸出A和B,實現臨界區的互斥訪問的基本模型
#include "pv.h"
main()
{
  key_t semkey_A=0x200;
  key_t semkey_B=0x220;
  int semid_A,semid_B;
  if ((semid_A=initsem(semkey_A,1))<0) exit(1);
  if ((semid_B=initsem(semkey_B,0))<0) exit(1);
  printf("A   進程A的信號量標識符%d,它的初始值爲%d\n",
           semid_A,semctl(semid_A, 0, GETVAL)); 
  printf("B   進程B的信號量標識符%d,它的初始值爲%d\n",
           semid_B,semctl(semid_B, 0, GETVAL)); 
          
 if (fork()!=0)    //父進程先執行
  {
    int i;
    for (i=0;i<10;i++)
    {
      p(semid_A);        
      printf("A   進程A的信號量值爲%d\n",semctl(semid_A, 0, GETVAL));  
      v(semid_B);
    }
  }
  else
  {
    int j;
    for (j=0;j<10;j++)
    {
      p(semid_B);
      printf("B   進程B的信號量值爲%d\n",semctl(semid_B, 0, GETVAL));  
      v(semid_A);
    }
  }
}
View Code

測試結果

8VV7HI0KYFNU]5)VSF3_J$7_thumb[2]

}@$HQ2L3WXFVO$R6$N367@4

實驗思考:

(1)信號量一經建立就存在在內存中,這會影響到其餘用戶及其程序。所以妥善的作法是在程序結束時,若再也不須要該信號量,則能夠將其從內存中刪除,要求實現刪除信號量以及輸出信號量的值,使用semctl的刪除命令就能夠了,代碼以下:

#include "pv.h"
main()
{
  key_t semkey_A=0x200;
  key_t semkey_B=0x220;
  int semid_A,semid_B;
  if ((semid_A=initsem(semkey_A,1))<0) exit(1);
  if ((semid_B=initsem(semkey_B,0))<0) exit(1);
  printf("A   進程A的信號量標識符%d,它的初始值爲%d\n",
           semid_A,semctl(semid_A, 0, GETVAL)); 
  printf("B   進程B的信號量標識符%d,它的初始值爲%d\n",
           semid_B,semctl(semid_B, 0, GETVAL)); 
          
 if (fork()!=0)    //父進程先執行
  {
    int i;
    for (i=0;i<10;i++)
    {
      p(semid_A);        
      printf("A   進程A的信號量值爲%d\n",semctl(semid_A, 0, GETVAL));  
      v(semid_B);
    }
  }
  else
  {
    int j;
    for (j=0;j<10;j++)
    {
      p(semid_B);
      printf("B   進程B的信號量值爲%d\n",semctl(semid_B, 0, GETVAL));  
      v(semid_A);
    }
  }
 
  if((semctl(semid_A,0,IPC_RMID))<0)   //刪除進程Ad的信號量值,IPC_RMID是刪除命令
   {
   perror("semctl error");
   exit(1);
   }
   if((semctl(semid_B,0,IPC_RMID))<0)
   {
   perror("semctl error");
   exit(1);
   }
}
View Code

結果截圖:

8GNW@6TLG5E%UN(NKPI8WDY

}@9ALZ_N05EKBG%XDD`T1_H

(實驗先後信號量的值與標識符都不在。)

實驗測試三:用信號量機制解決實際的進程同步問題。有三個進程分別用P一、P二、P3表示,其中P1輸出字符A,P2輸出字符B,P3輸出字符C;現要求三個進程協做完成以下的輸出序列:

ABABABCABABABCABABABC…

本身寫的代碼:

//abc.c  主程序,使用PV操,在實驗二的基礎上輸出ABABABCABABABCABABABC…
#include "pv.h"
main()
{
  key_t semkey_A=0x200;
  key_t semkey_B=0x220;
  key_t semkey_C=0x240;
   int semid_A,semid_B,semid_C;     
 
  if ((semid_A=initsem(semkey_A))<0) exit(1);
  if ((semid_B=initsem(semkey_B))<0) exit(1);
  if ((semid_C=initsem(semkey_C))<0) exit(1);
 
  printf("A   進程A的信號量%d,它的初始值爲%d\n",
           semid_A,semctl(semid_A, 0, GETVAL)); 
  printf("B   進程B的信號量%d,它的初始值爲%d\n",
           semid_B,semctl(semid_B, 0, GETVAL)); 
  printf("C   進程B的信號量%d,它的初始值爲%d\n",
           semid_C,semctl(semid_C, 0, GETVAL));    
     
            
  int count=0;  
  if (fork()!=0)      //父進程先執行
  {
    int i;
    for (i=0;i<10;i++)
    {
      p(semid_B);        
      printf("A 進程A的信號量值爲%d\n",semctl(semid_A, 0, GETVAL));  
      v(semid_A);
    }
  }
 
  else
  {
    int j;
    for (j=0;j<10;j++)
    {
      p(semid_A);
      printf("B 進程B的信號量值爲%d\n",semctl(semid_B, 0, GETVAL)); count++;
      if (count==3)
      { v(semid_C);  printf("C 進程C的信號量值爲%d,couont=%d\n",semctl(semid_C, 0, GETVAL),count) ;
          v(semid_B); count=0;}
      else
      v(semid_B);
   }
 }

  if((semctl(semid_A,0,IPC_RMID))<0)   //刪除進程A的信號量值,IPC_RMID是刪除命令
   {
        perror("semctl error");
        exit(1);
   }
   if((semctl(semid_B,0,IPC_RMID))<0)
   {
        perror("semctl error");
        exit(1);
   }
   if((semctl(semid_C,0,IPC_RMID))<0)
   {
        perror("semctl error");
        exit(1);
   }
}
View Code

 實驗結果截圖:

Q7}R%K}{A64(1F{CZ{XY(@8

實驗分析

觀察到C是出如今第3個B後面的,就在輸出B的控制語句里加一個判斷就能夠了。

本身寫出代碼後,發覺實驗指導書後面給了答案,坑:

//abc.c  主程序,使用PV操做,三個進程分別輸出A和B和C
//同步輸出格式爲:ABABABC-ABABABC-ABABABC-ABABABC-
#include "pv.h"
main()
{
  key_t semkey_A=0x200;  key_t semkey_B=0x220;
  key_t semkey_C=0x260;  int semid_A,semid_B,semid_C;
  if ((semid_A=initsem(semkey_A,1))<0) exit(1);
  if ((semid_B=initsem(semkey_B,0))<0) exit(1);
  if ((semid_C=initsem(semkey_C,0))<0) exit(1);
  if (fork()>0)//父進程
  {
    if (fork()>0) {//父進程
      int i;
      for (i=0;i<90;i++)
      {
        p(semid_A);
        printf("A\n");
        v(semid_B);
      }
    }
    else {//第二次fork的子進程
      int j;
      int count=0;
      for (j=0;j<90;j++)
      {
        p(semid_B);
        printf("B\n");
        count++;
        if (count==3) {
          v(semid_C);
          count=0;
        }
        else {
          v(semid_A);
        }
      }  
    }
  }
  else//第一次fork的子進程
  {
    int k;
    for (k=0;k<30;k++)
    {
      p(semid_C);
      printf("C-\n");
      v(semid_A);
    }
  }
}
View Code

 實驗思考:若將輸出語句中的「\n」去掉,程序執行會有什麼不一樣,你推測多是什麼緣由形成的?

若是去掉\n等相關輸出,代碼以下:

//abc.c  主程序,使用PV操,在實驗二的基礎上輸出ABABABCABABABCABABABC…
#include "pv.h"
main()
{
  key_t semkey_A=0x200;
  key_t semkey_B=0x220;
  key_t semkey_C=0x240;
   int semid_A,semid_B,semid_C;     
 
  if ((semid_A=initsem(semkey_A))<0) exit(1);
  if ((semid_B=initsem(semkey_B))<0) exit(1);
  if ((semid_C=initsem(semkey_C))<0) exit(1);
 
  printf("A   進程A的信號量%d,它的初始值爲%d\n",
           semid_A,semctl(semid_A, 0, GETVAL)); 
  printf("B   進程B的信號量%d,它的初始值爲%d\n",
           semid_B,semctl(semid_B, 0, GETVAL)); 
  printf("C   進程B的信號量%d,它的初始值爲%d\n",
           semid_C,semctl(semid_C, 0, GETVAL));    
     
            
  int count=0;  
  if (fork()!=0)      //父進程先執行
  {
    int i;
    for (i=0;i<10;i++)
    {
      p(semid_B);        
      printf("A");  
      v(semid_A);
    }
  }
 
  else
  {
    int j;
    for (j=0;j<10;j++)
    {
      p(semid_A);
      printf("B"); count++;
      if (count==3)
      { v(semid_C);  printf("C") ;
          v(semid_B); count=0;}
      else
      v(semid_B);
   }
 }

  if((semctl(semid_A,0,IPC_RMID))<0)   //刪除進程A的信號量值,IPC_RMID是刪除命令
   {
        perror("semctl error");
        exit(1);
   }
   if((semctl(semid_B,0,IPC_RMID))<0)
   {
        perror("semctl error");
        exit(1);
   }
   if((semctl(semid_C,0,IPC_RMID))<0)
   {
        perror("semctl error");
        exit(1);
   }
}
View Code

 結果截圖:

8F7{4FA}V%BPM5%P9J4XBJS

緣由分析:

這和緩衝機制有關(參考:這個寫的很不錯http://www.myexception.cn/linux-unix/1442125.html):
緩衝機制通常分爲:全緩衝、行緩衝、無緩衝。

  • 全緩衝:緩衝區滿了之後,才發生真正的IO。咱們一般用的磁盤文件IO就是這樣的。固然你能夠調用flush類函數強制刷新緩衝。
  • 行緩衝:緩衝區滿了之後或者緩衝區收到一個換行符(表示已輸入或輸出一行),後才發生真正的IO,好比標準輸出和標準輸入默認的緩衝機制就是行緩衝。(行緩衝還有一些規則,參考APUE)
  • 無緩衝:當即發生IO,一般標準出錯是不帶緩衝的。因此建議用輸出信息來調試程序時,最後用標準出錯IO,以避免調試信息延遲輸出。

顯然這裏printf採用的是標準IO,只有當遇到換行符號後,纔會輸出,如若沒有,則父子進程只能一次性輸出緩衝區裏內容,就會有上面的結果

參考:

http://www.cnblogs.com/lixiaofei1987/p/3208414.html semop函數詳解

http://www.cnblogs.com/hjslovewcl/archive/2011/03/03/2314341.html 信號量介紹

http://blog.chinaunix.net/uid-23193900-id-3221978.html 三個函數的介紹

相關文章
相關標籤/搜索