Linux 進程間通信之信號、信號量實踐

這節我們就來分享一下 Linux 的另外兩種進程間通信的方式:信號、信號量。

1、信號

我們使用過 windows 的都知道,當一個程序被卡死的時候不管怎樣都沒反應,這樣我們就可以打開任務管理器直接強制性的結束這個進程,這個方法的實現就是和 Linux 上通過生成信號和捕獲信號來實現相似的,運行過程中進程捕獲到這些信號做出相應的操作使最終被終止。

信號的主要來源是分爲兩部分,一部分是硬件來源,一部分是軟件來源;進程在實際中可以用三種方式來響應一個信號:一是忽略信號,不對信號做任何操作,其中有兩個信號是不能別忽略的分別是 SIGKILL 和 SIGSTOP。二是捕捉信號,定義信號處理函數,當信號來到時做出響應的處理。三是執行缺省操作,Linux 對每種信號都規定了默認操作。注意,進程對實時信號的缺省反應是立即終止。

發送信號的函數有很多,主要使用的有:kill()、raise()、abort()、alarm()

先來熟悉下 kill 函數,進程可以通過 kill() 函數向包括它本身在內的其它進程發送一個信號,如果程序沒有發送這個信號的權限,對 kill 函數的調用將會失敗,失敗的原因通常是由於目標進程由另一個用戶所擁有。

kill 函數的原型爲:

它的作用是把信號 sig 發送給進程號爲 pid 的進程,成功時返回 0。kill 調用失敗返回 - 1,調用失敗通常有三大原因:

還有一個非常重要的函數,信號處理 signal 函數。程序可以用 signal 函數來處理指定的信號,主要通過恢復和忽略默認行爲來操作。signal 函數原型如下:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

我們來看一個例程瞭解一下 signal 函數。signal.c

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

//函數ouch對通過參數sig傳遞進來的信號作出響應。
void ouch(int sig)
{
 printf("signal %d\n", sig);
 //恢復終端中斷信號SIGINT的默認行爲
 (void) signal(SIGINT, SIG_DFL);
}
int main()
{
  //改變終端中斷信號SIGINT的默認行爲,使之執行ouch函數
  (void) signal(SIGINT, ouch);
 
  while(1)
  {
   printf("Hello World!\n");
   sleep(1); 
  }
 return 0;
}

運行結果:

可以看出當我按下 ctrl+c 的時候並不會退出,只有當再次按下 ctrl+c 的時候纔會退出。造成的原因是因爲 SIGINT 的默認行爲被 signal 函數改變了,當進程接受到信號 SIGINT 時,它就去調用函數 ouch 去處理,注意 ouch 函數把信號 SIGINT 的處理方式改變成默認的方式,所以當你再按一次 ctrl+c 時,進程就像之前那樣被終止了。

下面是幾種常見的信號:

信號發送主要函數有 kill 和 raise。上面我們知道 kill 函數的用法也清楚 kill 函數是可以向自身發送信號和其它進程發送信號,raise 與之不同的是隻可以向本身發送信號。

通過 raise 函數向自身發送數據,使子進程暫停通過測試如下: raise.c

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  pid_t pid;
  int ret;
  if((pid=fork())<0)
  {
   printf("Fork error\n");
   exit(1);
  }
  //子進程
  if(pid==0)
  {
   //在子進程中使用raise()函數發出SIGSTOP信號,使子進程暫停
   printf("I am child pid:%d.I am waiting for any signal\n",getpid());
   raise(SIGSTOP);
   printf("I am child pid:%d.I am killed by progress:%d\n",getpid(),getppid());
   exit(0);
  }
  //父進程
  else  
  {
   sleep(2);  
   //在父進程中收集子進程發出的信號,並調用kill()函數進行相應的操作
   if((waitpid(pid,NULL,WNOHANG))==0) 
   { 
  //若pid指向的子進程沒有退出,則返回0,且父進程不阻塞,繼續執行下邊的語句
    if((ret=kill(pid,SIGKILL))==0)
    {
     printf("I am parent pid:%d.I am kill %d\n",getpid(),pid);
    }
   }
   //等待子進程退出,否則就一直阻塞
   waitpid(pid,NULL,0);
   exit(0);
  }
}

當調用 raise 的時候子進程就會暫停:

信號是對終端機的一種模擬,也是一種異步通信方式。

2、信號量

主要作爲進程間,以及同一進程不同線程之間的同步手段。信號量是用來解決進程之間的同步與互斥問題的一種進程之間的通信機制,包括一個稱爲信號量的變量和在該信號量下等待資源的進程等待隊列,以及對信號量進行的兩個原子操作。信號量對應於某一種資源,取一個非負的整形值。信號量的值是指當前可用的資源數量。

由於信號量只有兩種操作,一種是等待信號,另一種是發送信號。即 P 和 V,它們的行爲如下:

Linux 特別提供了一組信號量接口來對信號操作,它們不只是侷限的針對二進制信號量,下面我們來對每個函數介紹,需要注意的是這些函數都是用來成對組的信號量值進行操作的。

2.1、semget 函數

它的作用是創建一個新信號量或取得一個已有信號量。

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

第一個參數是 key 整數型,不相關的進程可以通過它訪問一個信號量,它代表程序可能要使用的某個資源,程序對所有信號量的訪問都是間接的,先通過調用 semget 函數並提供一個鍵,再由系統生成一個相應的信號標識符(semget 函數的返回值),只有 semget 函數才直接使用信號量鍵,所有其他的信號量函數使用由 semget 函數返回的信號量標識符。如果多個程序使用相同的 key 值,key 將負責協調工作。

第二個參數是制定需要的信號數量,通常情況下爲 1。

第三個參數是一組標誌位,當想要當信號量不存在時創建一個新的信號量,可以和值 IPC_CREAT 做按位或操作。設置了 IPC_CREAT 標誌後,即使給出的鍵是一個已有信號量的鍵,也不會產生錯誤。而 IPC_CREAT | IPC_EXCL 則可以創建一個新的,唯一的信號量,如果信號量已存在,返回一個錯誤。

semget 函數成功返回一個相應信號標識符(非零),失敗返回 - 1。

2.2、semop 函數

它的作用是改變信號量的值。

int semop(int semid, struct sembuf *sops, unsigned nsops);

sops 是一個指針,它指向這樣一個數組: 元素用來描述對 semid 代表的信號量集合中第幾個信號進行怎麼樣的操作。nops 規定該數組中操作的數量。

semop 函數返回 0 表示成功,返回 - 1 表示失敗。

2.3、semctl 函數

該函數用來直接控制信號量信息。

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

semget 並不會初始化每個信號量的值,這個初始化必須通過 SETVAL 命令或 SETALL 命令調用 semctl 來完成。

例程:semctl.c

#include <stdio.h>
#include <linux/sem.h>
#define NUMS 10  

int get_sem_val(int sid,int semnum)//取得當前信號量
{  
  return(semctl(sid,semnum,GETVAL,0));  
}  

int main(void)
{  
  int I ;
  int sem_id;  
  int pid;  
  int ret;  
  struct sembuf sem_op;//信號集結構
  union semun sem_val;//信號量數值

  //建立信號量集,其中只有一個信號量
  sem_id = semget(IPC_PRIVATE,1,IPC_CREAT|0600);
  //IPC_PRIVATE私有,只有本用戶使用,如果爲正整數,則爲公共的;1爲信號集的數量;
  if (sem_id==-1)
  {  
    printf("create sem error!\n");  
    exit(1);      
  }  
  printf("create %d sem success!\n",sem_id);      
  //信號量初始化
  sem_val.val=1;  
  //設置信號量,0爲第一個信號量,1爲第二個信號量,...以此類推;SETVAL表示設置
   ret = semctl(sem_id,0,SETVAL,sem_val);  
  if (ret < 0){  
    printf("initlize sem error!\n");  
    exit(1);      
   }  
   //創建進程
  pid = fork();  
  if (pid < 0)
  {  
    printf("fork error!\n");  
    exit(1);             
  }  
  else if(pid == 0)
  {
      //一個子進程,使用者
      for ( i=0;i<NUMS;i++)
      {  
        sem_op.sem_num=0;  
        sem_op.sem_op=-1;  
        sem_op.sem_flg=0;  
        semop(sem_id,&sem_op,1);//操作信號量,每次-1                  
        printf("%d 使用者: %d\n",i,get_sem_val(sem_id,0));  
      }       
  }  
  else
  {
      //父進程,製造者
     for (i=0;i<NUMS;i++)
     {  
          sem_op.sem_num=0;  
          sem_op.sem_op=1;  
          sem_op.sem_flg=0;  
          semop(sem_id,&sem_op,1);//操作信號量,每次+1                   
          printf("%d 製造者: %d\n",i,get_sem_val(sem_id,0));  
     }       
 }  
 exit(0);  
}

運行結果:

信號量的出現就是保證資源在一個時刻只能有一個進程(線程),所以例子當中只有製造者在製造(+1 操作)過程中,使用者這個進程是無法隨 sem_id 進行操作的。也就是說信號量是協調進程對共享資源操作的,起到了類似互斥鎖的作用,但卻比鎖擁有更強大的功能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/T9u8CGcl7l1v40D2gmLI-w