linux進程通訊

  • 2019 年 10 月 3 日
  • 筆記

linux常用進程通訊方式包括管道(pipe)、有名管道(FIFO)、訊號(signal)、消息隊列、共享記憶體、訊號量、套接字(socket)。

管道

管道是單向、先進先出的無結構的位元組流。用於父子進程之間的通訊。關鍵系統調用如下:
int pipe( int fd[2] );fd數組用於返回兩個fd,分別表示通道的兩端。
int main(){      int pid;      int fd[2];      if(pipe(fd)<0){//父進程創建管道          perror("Fail to pipe");          exit(EXIT_FAILURE);      }      if((pid=fork())<0){          perror("Fail to fork");          exit(EXIT_FAILURE);      }else if(pid == 0){          close(fd[1]);//表示管道的方向,fd[1]用於寫          child_read_pipe(fd[0]);//子進程讀取管道      }else{          close(fd[0]);//fd[0]用於讀          father_write_pipe(fd[1]);//父進程寫入管道      }  }

有名管道

有名管道以設備文件的形式存在,可被任意知道名字的進程使用,而不止在只有親緣關係的進程之間。
要使用有名管道,必須先建立它,並與他的一段相連,才能打開進行讀寫。當文件不再需要時,要顯示刪除。系統調用:
int mknod( char *pathname, mode_t mode, dev_t dev);
server端,創建管道:
#define FIFO_FILE       "MYFIFO"  //有名管道server端  int main(void)  {          FILE *fp;          char readbuf[80];            /* Create the FIFO if it does not exist */          umask(0);          mknod(FIFO_FILE, S_IFIFO|0666, 0);            while(1)          {                  fp = fopen(FIFO_FILE, "r");                  fgets(readbuf, 80, fp);                  printf("Received string: %sn", readbuf);                  fclose(fp);          }            return(0);  }

client端:
//有名管道client端  int main(int argc, char *argv[])  {          FILE *fp;            if ( argc != 2 ) {                  printf("USAGE: fifoclient [string]n");                  exit(1);          }            if((fp = fopen(FIFO_FILE, "w")) == NULL) {                  perror("fopen");                  exit(1);          }            fputs(argv[1], fp);            fclose(fp);          return(0);  }

有名管道創建後,以設備文件形式存在,標誌位有p。
 

訊號

一個進程發出訊號,另一個進程捕獲此訊號並作出動作。比如linux下ctrl+z快捷鍵終止進程,其實是向進程發送了SIGINT訊號,進程捕獲該訊號並終止。
常用訊號如下:
SIGALRM 由alarm函數的定時器產生。
SIGHUP SIGHUP和控制台操作有關,當控制台被關閉時系統會向擁有控制台sessionID的所有進程發送HUP訊號,默認HUP訊號的action是 exit,如果遠程登陸啟動某個服務進程並在程式運行時關閉連接的話會導致服務進程退出,所以一般服務進程都會用nohup工具啟動或寫成一個 daemon。
SIGINT 鍵盤中斷訊號,比ctrl+z
SIGKILL 強制kill進程,不可被捕獲或忽略。
SIGPIPE Broken Pipe,向通道寫時沒有對應的讀進程。
SIGTERM 要求進程結束運行,linux系統關機時會發送此訊號,kill命令默認也是發送此訊號。
SIGUSER1 SIGUSER2 進程之間用這兩個訊號通訊。
註冊訊號處理函數:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
signum為訊號,act為包含新的訊號處理函數,oldact保存舊的訊號處理函數。
sigaction結構體定義如下:
struct sigaction {  void (*sa_handler)(int);  void (*sa_sigaction)(int, siginfo_t *, void *);  sigset_t sa_mask;  int sa_flags;  void (*sa_restorer)(void);  };

sa_handler和sa_sigaction為訊號處理函數,一般以聯合體的形式存在,由sa_handler指定的處理函數只有一個參數,即訊號值,所以訊號不能傳遞除訊號值之外的任何資訊;由sa_sigaction指定的訊號處理函數帶有三個參數,是為實時訊號而設的(當然同樣支援非實時訊號),它指定一個3參數訊號處理函數。第一個參數為訊號值,第三個參數沒有使用(posix沒有規範使用該參數的標準),第二個參數是指向siginfo_t結構的指針,結構中包含訊號攜帶的數據值。
siginfo_t結構如下,siginfo_t結構中的si_value要麼持有一個4位元組的整數值,要麼持有一個指針,這就構成了與訊號相關的數據
typedef struct {  int si_signo;  int si_errno;  int si_code;  union sigval si_value;  } siginfo_t;  union sigval {  int sival_int;  void *sival_ptr;  }

sigset_t訊號集用來描述訊號的集合,linux所支援的所有訊號可以全部或部分的出現在訊號集中,主要與訊號阻塞相關函數配合使用。sa_mask指定在訊號處理程式執行過程中,哪些訊號應當被阻塞。預設情況下當前訊號本身被阻塞,防止訊號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標誌位。
sa_flags中包含了許多標誌位,比較重要的標誌位是SA_SIGINFO,表示訊號處理函數是用sa_handler還是sa_sigaction。
指定訊號處理函數實例如下:
void ouch(int sig)  {      printf("nOUCH! - I got signal %dn", sig);  }    int main()  {      struct sigaction act;      act.sa_handler = ouch;      //創建空的訊號屏蔽字,即不屏蔽任何資訊      sigemptyset(&act.sa_mask);      //使sigaction函數重置為默認行為      act.sa_flags = SA_RESETHAND;        sigaction(SIGINT, &act, 0);        while(1)      {          printf("Hello World!n");          sleep(1);      }      return 0;  } 

 
發送訊號系統調用:
int kill(pid_t pid, int sig);
alarm函數在經過預定時間後向發送一個SIGALRM訊號,seconds為0表示所有已設置的鬧鐘請求。
unsigned int alarm(unsigned int seconds);
static int alarm_fired = 0;    void ouch(int sig)  {      alarm_fired = 1;  }    int main()  {      //關聯訊號處理函數      signal(SIGALRM, ouch);      //調用alarm函數,5秒後發送訊號SIGALRM      alarm(5);      //掛起進程      pause();      //接收到訊號後,恢復正常執行      if(alarm_fired == 1)          printf("Receive a signal %dn", SIGALRM);      exit(0);    }

 

消息隊列

消息隊列是存在於內核的鏈表結構。消息含有一個類型,接收進程可以獨立地接收含有不同類型的數據結構。消息隊列的消息不能超過4056 bytes。
消息隊列相關的系統調用如下:
int msgget(key_t key, int msgflg);
創建或獲取隊列,key用來標識隊列,msgflag表示消息隊列的訪問許可權,和文件的訪問許可權一樣。msgflg可以與IPC_CREAT做或操作,表示當key所命名的消息隊列不存在時創建一個消息隊列,如果key所命名的消息隊列存在時,IPC_CREAT標誌會被忽略,而只返回一個標識符。
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
發送消息,msgid表示消息隊列標識,msg_ptr表示消息指針,msg_ptr所指向結構體第一個成員必須為長整型消息類型,如下所示,mtext除可為char數組類型外,還可為任意其它類型
struct msgbuf {  long mtype; /* type of message */  char mtext[1]; /* message text */  };

msg_sz表示消息長度,不包括類型欄位,msgflagmsgflg用於控制當前消息隊列滿或隊列消息到達系統範圍的限制時將要發生的事情。
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
接收消息,msgtype>0表示接收消息的類型,msgtype為0表示不分類型接收第一個消息。如果它小於零,就獲取類型等於或小於msgtype的絕對值的第一個消息。其它參數和msgsend類似。
int msgctl(int msgid, int command, struct msgid_ds *buf);
msgctl用於控制消息隊列,每個消息隊列在內核中存在相應的數據結構msgid_ds,其結構如下:
/* one msqid structure for each queue on the system */  struct msqid_ds {  struct ipc_perm msg_perm;  struct msg *msg_first; /* first message on queue */  struct msg *msg_last; /* last message in queue */  time_t msg_stime; /* last msgsnd time */  time_t msg_rtime; /* last msgrcv time */  time_t msg_ctime; /* last change time */  struct wait_queue *wwait;  struct wait_queue *rwait;  ushort msg_cbytes;  ushort msg_qnum;  ushort msg_qbytes; /* max number of bytes on queue */  ushort msg_lspid; /* pid of last msgsnd */  ushort msg_lrpid; /* last receive pid */  };

command是將要採取的動作,它可以取3個值:
IPC_STAT 獲取消息隊列在內核中的msgid_ds結構,並存儲到buf中。
IPC_SET 設置消息隊列的ipc_perm成員
IPC_RMID 從內核中移除消息隊列。
接受者:
struct msg_st  {      long int msg_type;      char text[BUFSIZ];  };    int main()  {      int running = 1;      int msgid = -1;      struct msg_st data;      long int msgtype = 0; //注意1        //建立消息隊列      msgid = msgget((key_t)1234, 0666 | IPC_CREAT);      if(msgid == -1)      {          fprintf(stderr, "msgget failed with error: %dn", errno);          exit(EXIT_FAILURE);      }      //從隊列中獲取消息,直到遇到end消息為止      while(running)      {          if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)          {              fprintf(stderr, "msgrcv failed with errno: %dn", errno);              exit(EXIT_FAILURE);          }          printf("You wrote: %sn",data.text);          //遇到end結束          if(strncmp(data.text, "end", 3) == 0)              running = 0;      }      //刪除消息隊列      if(msgctl(msgid, IPC_RMID, 0) == -1)      {          fprintf(stderr, "msgctl(IPC_RMID) failedn");          exit(EXIT_FAILURE);      }      exit(EXIT_SUCCESS);  }

 
發送者:
#define MAX_TEXT 512  struct msg_st  {      long int msg_type;      char text[MAX_TEXT];  };    int main()  {      int running = 1;      struct msg_st data;      char buffer[BUFSIZ];      int msgid = -1;        //建立消息隊列      msgid = msgget((key_t)1234, 0666 | IPC_CREAT);      if(msgid == -1)      {          fprintf(stderr, "msgget failed with error: %dn", errno);          exit(EXIT_FAILURE);      }        //向消息隊列中寫消息,直到寫入end      while(running)      {          //輸入數據          printf("Enter some text: ");          fgets(buffer, BUFSIZ, stdin);          data.msg_type = 1;    //注意2          strcpy(data.text, buffer);          //向隊列發送數據          if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)          {              fprintf(stderr, "msgsnd failedn");              exit(EXIT_FAILURE);          }          //輸入end結束輸入          if(strncmp(buffer, "end", 3) == 0)              running = 0;          sleep(1);      }      exit(EXIT_SUCCESS);  }

 

共享記憶體

不同進程之間共享的記憶體通常安排為同一段物理記憶體。進程可以將同一段共享記憶體連接到它們自己的地址空間中,所有進程都可以訪問共享記憶體中的地址。共享記憶體並未提供同步機制,也就是說,在第一個進程結束對共享記憶體的寫操作之前,並無自動機制可以阻止第二個進程開始對它進行讀取。所以我們通常需要用其他的機制來同步對共享記憶體的訪問,例如前面說到的訊號量。
系統調用如下:
int shmget(key_t key, size_t size, int shmflg);
key為共享記憶體段命名,size以位元組為單位指定需要共享的記憶體容量,shmflg是許可權標誌,它的作用與open函數的mode參數一樣,將其與IPC_CREAT做或操作表示共享記憶體不存在則創建。共享記憶體的許可權標誌與文件的讀寫許可權一樣。
void *shmat(int shm_id, const void *shm_addr, int shmflg);
啟動對該共享記憶體的訪問,並把共享記憶體連接到當前進程的地址空間。shm_addr指定共享記憶體連接到當前進程中的地址位置,通常為空,表示讓系統來選擇共享記憶體的地址。shm_flg是一組標誌位,通常為0。調用成功時返回一個指向共享記憶體第一個位元組的指針,如果調用失敗返回-1.
int shmdt(const void *shmaddr);
將共享記憶體從當前進程中分離。
int shmctl(int shm_id, int command, struct shmid_ds *buf);和消息隊列含義相似。
讀進程:
int main()  {      int running = 1;//程式是否繼續運行的標誌      void *shm = NULL;//分配的共享記憶體的原始首地址      struct shared_use_st *shared;//指向shm      int shmid;//共享記憶體標識符      //創建共享記憶體      shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);      if(shmid == -1)      {          fprintf(stderr, "shmget failedn");          exit(EXIT_FAILURE);      }      //將共享記憶體連接到當前進程的地址空間      shm = shmat(shmid, 0, 0);      if(shm == (void*)-1)      {          fprintf(stderr, "shmat failedn");          exit(EXIT_FAILURE);      }      printf("nMemory attached at %Xn", (int)shm);      //設置共享記憶體      shared = (struct shared_use_st*)shm;      shared->written = 0;      while(running)//讀取共享記憶體中的數據      {          //沒有進程向共享記憶體定數據有數據可讀取          if(shared->written != 0)          {              printf("You wrote: %s", shared->text);              sleep(rand() % 3);              //讀取完數據,設置written使共享記憶體段可寫              shared->written = 0;              //輸入了end,退出循環(程式)              if(strncmp(shared->text, "end", 3) == 0)                  running = 0;          }          else//有其他進程在寫數據,不能讀取數據              sleep(1);      }      //把共享記憶體從當前進程中分離      if(shmdt(shm) == -1)      {          fprintf(stderr, "shmdt failedn");          exit(EXIT_FAILURE);      }      //刪除共享記憶體      if(shmctl(shmid, IPC_RMID, 0) == -1)      {          fprintf(stderr, "shmctl(IPC_RMID) failedn");          exit(EXIT_FAILURE);      }      exit(EXIT_SUCCESS);  }

 
寫進程:
int main()  {      int running = 1;      void *shm = NULL;      struct shared_use_st *shared = NULL;      char buffer[BUFSIZ + 1];//用於保存輸入的文本      int shmid;      //創建共享記憶體      shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);      if(shmid == -1)      {          fprintf(stderr, "shmget failedn");          exit(EXIT_FAILURE);      }      //將共享記憶體連接到當前進程的地址空間      shm = shmat(shmid, (void*)0, 0);      if(shm == (void*)-1)      {          fprintf(stderr, "shmat failedn");          exit(EXIT_FAILURE);      }      printf("Memory attached at %Xn", (int)shm);      //設置共享記憶體      shared = (struct shared_use_st*)shm;      while(running)//向共享記憶體中寫數據      {          //數據還沒有被讀取,則等待數據被讀取,不能向共享記憶體中寫入文本          while(shared->written == 1)          {              sleep(1);              printf("Waiting...n");          }          //向共享記憶體中寫入數據          printf("Enter some text: ");          fgets(buffer, BUFSIZ, stdin);          strncpy(shared->text, buffer, TEXT_SZ);          //寫完數據,設置written使共享記憶體段可讀          shared->written = 1;          //輸入了end,退出循環(程式)          if(strncmp(buffer, "end", 3) == 0)              running = 0;      }      //把共享記憶體從當前進程中分離      if(shmdt(shm) == -1)      {          fprintf(stderr, "shmdt failedn");          exit(EXIT_FAILURE);      }      sleep(2);      exit(EXIT_SUCCESS);  }

 

訊號量

訊號量用來協調對共享資源的操作,只允許對它進行等待(即P(訊號變數))和發送(即V(訊號變數))資訊操作,且對訊號量的操作都是原子操作。
系統調用:
int semget(key_t key, int num_sems, int sem_flags);
num_sems表示訊號量數目。其它兩個參數和消息隊列的msgget函數參數含義相似。
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
改變訊號量的值,sem_id是由semget返回的訊號量標識符,sem_opa指向訊號量集操作的數組。sembuf結構的定義如下:
struct sembuf{  short sem_num;//除非使用一組訊號量,否則它為0  short sem_op;//訊號量在一次操作中需要改變的數據,-1,即P(等待)操作+1,即V(發送訊號)操作,0表示sleep直到訊號量的值為0,即資源得到百分百利用。  short sem_flg;//通常為SEM_UNDO,使作業系統跟蹤訊號,並在進程沒有釋放該訊號量而終止時,作業系統釋放訊號量;IPC_NOWAIT得不到資源立刻返回。  };

int semctl ( int semid, int semnum, int cmd, union semun arg );
控制訊號量資訊。semnum表示semid訊號量集中的哪一個訊號,cmd代表對訊號的操作,
IPC_STAT/IPC_SET/IPC_RMID等。
/* arg for semctl system calls. */  union semun {  int val; /* value for SETVAL */  struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */  ushort *array; /* array for GETALL & SETALL */  struct seminfo *__buf; /* buffer for IPC_INFO */  void *__pad;  };

兩個進程列印字元的實例,競爭打到控制台:
int main(int argc, char *argv[])  {      char message = 'X';      int i = 0;      //創建訊號量      sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);      if(argc > 1)      {          //程式第一次被調用,初始化訊號量          if(!set_semvalue())          {              fprintf(stderr, "Failed to initialize semaphoren");              exit(EXIT_FAILURE);          }          //設置要輸出到螢幕中的資訊,即其參數的第一個字元          message = argv[1][0];          sleep(2);      }      for(i = 0; i < 10; ++i)      {          //進入臨界區          if(!semaphore_p())              exit(EXIT_FAILURE);          //向螢幕中輸出數據          printf("%c", message);          //清理緩衝區,然後休眠隨機時間          fflush(stdout);          sleep(rand() % 3);          //離開臨界區前再一次向螢幕輸出數據          printf("%c", message);          fflush(stdout);          //離開臨界區,休眠隨機時間後繼續循環          if(!semaphore_v())              exit(EXIT_FAILURE);          sleep(rand() % 2);      }      sleep(10);      printf("n%d - finishedn", getpid());      if(argc > 1)      {          //如果程式是第一次被調用,則在退出前刪除訊號量          sleep(3);          del_semvalue();      }      exit(EXIT_SUCCESS);  }  static int set_semvalue()  {      //用於初始化訊號量,在使用訊號量前必須這樣做      union semun sem_union;      sem_union.val = 1;      if(semctl(sem_id, 0, SETVAL, sem_union) == -1)          return 0;      return 1;  }  static void del_semvalue()  {      //刪除訊號量      union semun sem_union;      if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)          fprintf(stderr, "Failed to delete semaphoren");  }  static int semaphore_p()  {      //對訊號量做減1操作,即等待P(sv)      struct sembuf sem_b;      sem_b.sem_num = 0;      sem_b.sem_op = -1;//P()      sem_b.sem_flg = SEM_UNDO;      if(semop(sem_id, &sem_b, 1) == -1)      {          fprintf(stderr, "semaphore_p failedn");          return 0;      }      return 1;  }  static int semaphore_v()  {      //這是一個釋放操作,它使訊號量變為可用,即發送訊號V(sv)      struct sembuf sem_b;      sem_b.sem_num = 0;      sem_b.sem_op = 1;//V()      sem_b.sem_flg = SEM_UNDO;      if(semop(sem_id, &sem_b, 1) == -1)      {          fprintf(stderr, "semaphore_v failedn");          return 0;      }      return 1;  }

 

各ipc方式比較

管道用於具有親緣關係的進程間通訊,有名管道的每個管道具有名字,使沒有親緣關係的進程間也可以通訊。
訊號是比較複雜的通訊方式,用於通知接受進程有某種事件發生,除了用於進程間通訊外,進程還可以發送訊號給進程本身。
消息隊列是消息的鏈接表,消息隊列克服了訊號承載資訊量少,管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
共享記憶體使得多個進程可以訪問同一塊記憶體空間,是最快的可用IPC形式,針對其他通訊機制運行效率較低而設計的。往往與其它通訊機制,如訊號量結合使用,來達到進程間的同步及互斥。
訊號量(semaphore)主要作為進程間以及同一進程不同執行緒之間的同步手段。
套介面(Socket)更為一般的進程間通訊機制,可用於不同機器之間的進程間通訊。
 

參考文獻

Linux Interprocess Communications.https://www.tldp.org/LDP/lpg/node7.html
Linux下的多進程通訊.https://www.tianmaying.com/tutorial/LinuxIPC
Linux進程間通訊——使用訊號.https://blog.csdn.net/ljianhui/article/details/10128731
訊號(上).https://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html
Linux進程間通訊——使用消息隊列.https://blog.csdn.net/ljianhui/article/details/10287879
Linux進程間通訊——使用共享記憶體.https://blog.csdn.net/ljianhui/article/details/10253345
Linux進程間通訊——使用訊號量.https://blog.csdn.net/ljianhui/article/details/10243617
Linux進程間通訊——使用流套接字.https://blog.csdn.net/ljianhui/article/details/10477427
Linux進程間通訊——使用數據報套接.https://blog.csdn.net/ljianhui/article/details/10697935
深刻理解Linux進程間通訊(IPC).https://www.ibm.com/developerworks/cn/linux/l-ipc/index.html