c/c++ linux 進程間通信系列5,使用信號量


linux 進程間通信系列5,使用信號量

信號量的工作原理:

由於信號量只能進行兩種操作等待和發送信號,即P(sv)和V(sv),他們的行為是這樣的:

P(sv):如果sv的值大於零,就給它減1;如果它的值為零,就掛起該進程的執行

V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1.

舉個例子,就是兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操作,它將得到信號量,並可以進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會被掛起以等待第一個進程離開臨界區域並執行V(sv)釋放信號量,這時第二個進程就可以恢復執行。

1,semget()函數

int semget(key_t key, int num_sems, int sem_flags);
  • 第二個參數nsems指明要創建的信號量集包含的信號量個數。如果只是打開信號量,把nsems設置為0即可。該參數只在創建信號量集時有效。

  • 第三個參數semflg為操作標識,可取如下值:

    • 0:取信號量集標識符,若不存在則函數會報錯

    • IPC_CREAT:當semflg&IPC_CREAT為真時,如果內核中不存在鍵值與key相等的信號量集,則新建一個信號量集;如果存在這樣的信號量集,返回此信號量集的標識符

    • IPC_CREAT|IPC_EXCL:如果內核中不存在鍵值與key相等的信號量集,則新建一個消息隊列;如果存在這樣的信號量集則報錯

      上述semflg參數為模式標志參數,使用時需要與IPC對象存取權限(如0600)進行|運算來確定信號量集的存取權限

      錯誤代碼:

      EACCESS:沒有權限

      EEXIST:信號量集已經存在,無法創建

      EIDRM:信號量集已經刪除

      ENOENT:信號量集不存在,同時semflg沒有設置IPC_CREAT標志

      ENOMEM:沒有足夠的內存創建新的信號量集

      ENOSPC:超出限制

2,semop()函數

int semop(int sem_id, struct sembuf *sops, size_t nsops);
  • semid:信號量集標識符

  • sops:指向進行操作的信號量集結構體數組的首地址,此結構的具體說明如下:

    如果sembuf里的semnum超過了集合中信號量的最大個數,在執行semop時,會報出:FIle too large。

    struct sembuf {
      short semnum; /*信號量集合中的信號量編號,0代表第1個信號量*/
      short val;/*若val>0進行V操作信號量值加val,表示進程釋放控制的資源 */
      /*若val<0進行P操作信號量值減val,若(semval-val)<0(semval為該信號量值),則調用進程阻塞,直到資源可   用;若設置IPC_NOWAIT不會睡眠,進程直接返回EAGAIN錯誤*/
      /*若val==0時阻塞等待信號量為0,調用進程進入睡眠狀態,直到信號值為0;若設置IPC_NOWAIT,進程不會睡眠,直接返回EAGAIN錯誤*/
      short flag;  /*0 設置信號量的默認操作*/
      /*IPC_NOWAIT設置信號量操作不等待*/
      /*SEM_UNDO 選項會讓內核記錄一個與調用進程相關的UNDO記錄,如果該進程崩潰,則根據這個進程的UNDO記錄自動恢復相應信號量的計數值*/
    };
    
  • nsops:進行操作信號量的個數,即sops結構變量的個數,需大於或等於1。最常見設置此值等於1,只完成對一個信號量的操作

錯誤代碼:

E2BIG:一次對信號量個數的操作超過了系統限制

EACCESS:權限不夠

EAGAIN:使用了IPC_NOWAIT,但操作不能繼續進行

EFAULT:sops指向的地址無效

EIDRM:信號量集已經刪除

sops為指向sembuf數組,定義所要進行的操作序列。下面是信號量操作舉例。

struct sembuf sem_get={0,-1,IPC_NOWAIT}; /將信號量對象中序號為0的信號量減1/

struct sembuf sem_get={0,1,IPC_NOWAIT}; /將信號量對象中序號為0的信號量加1/

struct sembuf sem_get={0,0,0}; /進程被阻塞,直到對應的信號量值為0/

flag一般為0,若flag包含IPC_NOWAIT,則該操作為非阻塞操作。若flag包含SEM_UNDO,則當進程退出的時候會還原該進程的信號量操作,這個標志在某些情況下是很有用的,

比如某進程做了P操作得到資源,但還沒來得及做V操作時就異常退出了,此時,其他進程就只能都阻塞在P操作上,於是造成了死鎖。若采取SEM_UNDO標志,就可以避免因為進程異常退出而造成的死鎖。

semops函數詳細說明參考:semops

3,semctl()函數

int semctl(int semid, int semnum, int cmd, ...);

作用:根據cmd的不同,操作創建成功的semid,比如設置信號量集合里的每個信號,同時可以訪問的進程數,通過cmd:SETALL,來設置,並且后面需要跟一個指向數組的指針。

  • semid:信號量集合的標識符。
  • semnum:信號量集合中的第幾個信號量。當cmd為SETALL時,這個參數感覺沒有實際意義,多少都可以。
  • cmd:根據您cmd的不同,做不同的操作。

下面的例子1創建了一個信號量的集合,可以用【ipcs -s】查看;例子2去訪問例子1創建的信號量集合。

例子1里的關鍵點:

  • 信號集合里信號的個數為16個
  • 因為這句代碼【semun_array[i] = 1;】,所以,每個信號,只能同時有且只有一個進程訪問。如果【semun_array[i] = 2;】,則,每個信號,同時有2個進程可以訪問這個信號。
  • smectl的第一個參數,感覺設置多少都可以。

例子2里的關鍵點:

  • sb.sem_num = 15;//指定要訪問的信號量集合中的哪個信號量
    sb.sem_op = -1;//semop前信號量的值都為1,這里指定的是-1,所以減一后為0,由於為0了,所以下個進程再想去訪問,就需要排隊,等現在訪問這個信號量的進程結束后,才能訪問。
  • SEM_UNDO的作用為,訪問信號量的進程結束后,會自動回復這個信號量被訪問前的狀態,也就是說,某個進程訪問前,信號量的狀態是1,這個進程退出后,把信號量的狀態回復回1.

例子1

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define NSEMS 16

int main(){
  int semid;
  unsigned short semun_array[NSEMS];
  int i;

  semid = semget(IPC_PRIVATE, NSEMS, 0600);
  if(semid < 0){
    perror("semget");
    return 1;
  }

  for(i =0; i < NSEMS; ++i){
    semun_array[i] = 1;
  }

  if(semctl(semid, 1000, SETALL, &semun_array) != 0){
    perror("semctl");
    return 1;
  }

  printf("semid:%d\n", semid);
  return 0;
}

github源代碼

例子2:

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>

#define NSEMS 16

int main(int argc, char* argv[]){
  int semid;
  sembuf sb;

  if(argc != 2){
    printf("arg is wrong\n");
    return 1;
  }

  semid = atoi(argv[1]);

  sb.sem_num = 15;//指定要操作的信號量是信號集合中的號碼為0信號量
  sb.sem_op = -1;
  sb.sem_flg = SEM_UNDO;

  printf("before semop()\n");
  if(semop(semid, &sb, 1) != 0){
    perror("semop");
    return 1;
  }

  printf("after semop()\n");

  printf("press enter to exti\n");
  getchar();
  return 0;
}

github源代碼

例子3:多線程之間,使用信號量。

第一個進程:

  • step1:創建有16個信號量的信號量集合的結果是成功的,所以結果semid >= 0,進入if分支。
  • step2:把每個信號量可同時訪問的進程數目設置為1,在代碼的22-28行。
  • step3:准備訪問16個信號量,首先在31-35行,把訪問設置為減一,並且UNDO。
  • step4:按回車后,開始訪問16個信號量。

第二個進程:

  • step1:因為是用同一個key去創建信號量集合,所以是失敗的,進入else分支。
  • step2:去拿,已經被創建過了的信號量集合的id,semid = semget(MYIPCKEY, NSEMS, 0600);
  • step3:進入while循環,觀察sem_otime是否為0,不為0后,跳出循環,准備訪問第0號信號量和第1號信號量。如果某個進程訪問了信號量集合,sem_otime就從0變為非0.
  • step4:准備訪問第0號信號量和第1號信號量。
  • step5:訪問第0號信號量和第1號信號量,但是發現信號量的值為-1(因為第一個進程訪問是給他減一了),說明已經有個進程(第一個進程)正在訪問,所以就一直等待。
  • step6:按回車,讓第一個進程結束。信號量會從-1變為0。
  • step7:因為訪問的進程退出了(信號量會從-1變為0),在step5處的等待就結束了,才可以訪問第0號信號量和第1號信號量。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MYIPCKEY 0xabcdabcd
#define NSEMS 16

int sem_init(){
  int semid;
  unsigned short semun_array[NSEMS];
  sembuf sb[NSEMS];
  int i;

  semid = semget(MYIPCKEY, NSEMS, 0600 | IPC_CREAT | IPC_EXCL);
  if(semid >= 0){
    for(i = 0; i < NSEMS; ++i){
      semun_array[i] = 1;
    }
    if(semctl(semid, NSEMS, SETALL, &semun_array) != 0){
      perror("semctl");
      return 1;
    }
    printf("[pid:%d] new semaphore set, semid=%d\n", getpid(), semid);

    for(i = 0; i < NSEMS; ++i){
      sb[i].sem_num = i;
      sb[i].sem_op = -1;
      sb[i].sem_flg = SEM_UNDO;
    }

    printf("IF[pid:%d] before semop()\n", getpid());
    printf("IF[pid:%d] press enter to start semop()\n", getpid());
    getchar();

    if(semop(semid, sb, NSEMS)){
      perror("semop");
      return 1;
    }

    printf("IF[pid:%d] press enter to exit this process\n", getpid());
    getchar();

    exit(0);
  }
  else{
    if(errno != EEXIST){
      perror("semget");
      return 1;
    }
    else{
      printf("in else\n");
      semid_ds sds;
      semid = semget(MYIPCKEY, NSEMS, 0600);
      if(semid < 0){
	perror("semget 1");
	return 1;
      }

      printf("ELSE[pid:%d] before semctl()\n", getpid());

      while(true){
	//IPC_STAT的時候,忽略第二個參數
	if(semctl(semid, 0, IPC_STAT, &sds) != 0){
	  perror("semctl 1");
	  return 1;
	}
	printf("###########################\n");
	printf("sem_perm.mode:%d,sem_perm.__seq:%d\n",sds.sem_perm.mode,sds.sem_perm.__seq);
	printf("otime:%ld\n",sds.sem_otime);/* Last semop time */
	printf("ctime:%ld\n",sds.sem_ctime);/* Last change time */
	printf("sem_nsems:%ld\n",sds.sem_nsems);/* No. of semaphores in set */
	printf("###########################\n");
     
	if(sds.sem_otime != 0){
	  break;
	}

	printf("ELSE[pid:%d] waiting otime change...\n", getpid());
	sleep(2);
      }


      sb[0].sem_num = 0;
      sb[0].sem_op = -1;
      sb[0].sem_flg = SEM_UNDO;
      
      sb[1].sem_num = 0;
      sb[1].sem_op = -1;
      sb[1].sem_flg = SEM_UNDO;

      printf("ELSE[pid:%d] before semop()\n", getpid());
      if(semop(semid, sb, 2) != 0){
	perror("semop 1");
	return 1;
      }

      printf("ELSE[pid:%d] after semop()\n", getpid());
    }
  }
  return 0;
}

int main(){
  pid_t pid;
  pid = fork();

  if(sem_init() < 0){
    printf("[pid:%d] sem_init() failed\n", getpid());
  }
}

github源代碼

注意:執行一次后,再次執行前,必須用下面的命令刪除信號量集合。

1,首先找到信號量集合的ID:

ipcs -s

2,刪除信號量集合:

ipcrm -s id

c/c++ 學習互助QQ群:877684253

本人微信:xiaoshitou5854


免責聲明!

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



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