Linux 常見的六大 IPC 通訊方式

平凡的世界007發表於2019-01-11

Linux環境下,程式地址空間相互獨立,每個程式各自有不同的使用者地址空間。任何一個程式的全域性變數在另一個程式中都看不到,所以程式和程式之間不能相互訪問,要交換資料必須通過核心,在核心中開闢一塊緩衝區,程式1把資料從使用者空間拷到核心緩衝區,程式2再從核心緩衝區把資料讀走,核心提供的這種機制稱為程式間通訊(IPC,InterProcessCommunication)。今天我們來看一下,Linux下常見的六大IPC通訊方式。

1、訊號

訊號是Unix/Linux系統在一定條件下生成的事件。訊號是一種非同步通訊機制,程式不需要執行任何操作來等待訊號的到達。訊號非同步通知接收訊號的程式發生了某個事件,然後作業系統將會中斷接收到訊號的程式的執行,轉而去執行相應的訊號處理程式。

  • (1)註冊訊號處理函式
    #include<
signal.h>
/*typedefvoid (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_thandler);
*/ void (*signal(intsignum, void (*handler)(int)))(int);
//SIG_IGN &
&
SIG_DFL int sigaction(int signum, const struct sigaction*act,struct sigaction *oldact);
複製程式碼
  • (2)傳送訊號
    #include<
signal.h>
intkill(pid_t pid,int sig);
//#include<
sys/types.h>
intraise(intsig);
//kill(getpid(),sig);
unsignedint alarm(unsigned int seconds);
//(#include<
unistd.h>
) seconds秒後,向程式本身傳送SIGALRM訊號。
複製程式碼
  • (3)訊號集 訊號集被定義為:
    typedef struct {unsigned long sig[_NSIG_WORDS];

}sigset_t;
* intsigaddset(sigset_t *set,int sig);
* intsigemptyset(sigset_t *set);
複製程式碼

2、管道(Pipe)

管道用來連線不同程式之間的資料流。

  • (1)在兩個程式之間傳遞資料的最簡單的方法是使用popen()和pclose()函式:
    #include<
stdio.h>
FILE *popen(const char *command,const char *open_mode);
int pclose(FILE *stream);
複製程式碼

popen()函式首先呼叫一個shell,然後把command作為引數傳遞給shell。這樣每次呼叫popen()函式都需要啟動兩個程式;但是由於在Linux中,所有的引數擴充套件(parameter expansion)都是由shell執行的,這樣command中包含的所有引數擴充套件都可以在command程式啟動之前完成。

  • (2)pipe()函式:
    int pipe(int pipefd[2]);
複製程式碼

popen()函式只能返回一個管道描述符,並且返回的是檔案流(filestream),可以使用函式fread()和fwrite()來訪問。pipe()函式可以返回兩個管道描述符:pipefd[0]和pipefd[1],任何寫入pipefd[1]的資料都可以從pipefd[0]讀回;pipe()函式返回的是檔案描述符(file descriptor),因此只能使用底層的read()和write()系統呼叫來訪問。pipe()函式通常用來實現父子程式之間的通訊。

  • (3)命名管道:FIFO
intmkfifo(const char *fifo_name, mode_t mode);
複製程式碼

前面兩種管道只能用在相關的程式之間,使用命名管道可以解決這個問題。在使用open()開啟FIFO時,mode中不能包含O_RDWR。mode最常用的是O_RDONLY,O_WRONLY與O_NONBLOCK的組合。O_NONBLOCK影響了read()和write()在FIFO上的執行方式。

3、訊號量(Semaphores)

System V的訊號量集表示的是一個或多個訊號量的集合。核心為每個訊號量集維護一個semid_ds資料結構,而訊號量集中的每個訊號量使用一個無名結構體表示,這個結構體至少包含以下成員:

    struct{ 
unsigned short semval;
//訊號量值,總是>
=0 pid_t sempid;
//上一次操作的pid …
};
複製程式碼
  • (1)建立或訪問訊號量
    intsemget(key_t key,int nsems,int flag);
複製程式碼

nsems指定訊號量集中訊號量的個數,如果只是獲取訊號量集的識別符號(而非新建),那麼nsems可以為0。flag的低9位作為訊號量的訪問許可權位,類似於檔案的訪問許可權;
如果flag中同時指定了IPC_CREAT和IPC_EXCL,那麼如果key已與現存IPC物件想關聯的話,函式將會返回EEXIST錯誤。例如,flag可以為IPC_CREAT|0666。

  • (2)控制訊號量集
    int semctl(intsemid,int semnum,int cmd,union semun arg);
複製程式碼

對semid訊號量集合執行cmd操作;
cmd常用的兩個值是:SETVAL初始化第semnum個訊號量的值為arg.val;
IPC_RMID刪除訊號量。

  • (3)對一個或多個訊號量進行操作
    int semop(intsemid,struct sembuf *sops,unsigned nsops);
struct sembuf{
unsignedshort sem_num;
//訊號量索引 short sem_op;
//對訊號量進行的操作,常用的兩個值為-1和+1,分別代表P、V操作 short sem_flag;
//比較重要的值是SEM_UNDO:當程式結束時,相應的操作將被取消;同時,如果程式結束時沒有釋放資源的話,系統會自動釋放
};
複製程式碼

4、共享記憶體

共享記憶體允許兩個或多個程式共享一定的儲存區,因為不需要拷貝資料,所以這是最快的一種IPC。

  • (1)建立或訪問共享記憶體
    intshmget(key_t key,size_t size,int shmflg);
複製程式碼
  • (2)附加共享記憶體到程式的地址空間
    void*shmat(int shmid,const void *shmaddr,int shmflg);
// shmaddr通常為NULL,由系統選擇共享記憶體附加的地址;
shmflg可以為SHM_RDONLY複製程式碼
  • (3)從程式的地址空間分離共享記憶體
    * intshmdt(const void *shmaddr);
//shmaddr是shmat()函式的返回值複製程式碼
  • (4)控制共享記憶體
    intshmctl(int shmid,int cmd,struct shmid_ds *buf);
struct shmid_ds{
structipc_perm shm_perm;

};
複製程式碼

cmd的常用取值有:
(a)IPC_STAT獲取當前共享記憶體的shmid_ds結構並儲存在buf中
(b)IPC_SET使用buf中的值設定當前共享記憶體的shmid_ds結構
(c)IPC_RMID刪除當前共享記憶體

5、訊息佇列

訊息佇列儲存在核心中,是一個由訊息組成的連結串列。

  • (1)建立或訪問訊息佇列
   int msgget(key_t key,int msgflg);
複製程式碼
  • (2)操作訊息佇列
    intmsgsnd(int msqid,const void *msg,size_t nbytes,int msgflg);
複製程式碼

msg指向的結構體必須以一個longint成員開頭,作為msgrcv()的訊息型別,必須大於0。nbytes指的是msg指向結構體的大小,但不包括longint部分的大小ssize_t msgrcv(intmsqid,void *msg,size_t nbytes,long msgtype,int msgflg);
如果msgtype是0,就返回訊息佇列中的第一個訊息;
如果是正整數,就返回佇列中的第一個該型別的訊息;
如果是負數,就返回佇列中具有最小值的第一個訊息,並且該最小值要小於等於msgtype的絕對值。

  • (3)控制訊息佇列
    int msgctl(intmsqid,int cmd,struct msqid_ds *buf);
struct msqid_ds{
struct ipc_permmsg_perm;

};
複製程式碼

6、Socket

套接字(Socket)是由Berkeley在BSD系統中引入的一種基於連線的IPC,是對網路介面(硬體)和網路協議(軟體)的抽象。它既解決了無名管道只能在相關程式間單向通訊的問題,又解決了網路上不同主機之間無法通訊的問題。
  套接字有三個屬性:域(domain)、型別(type)和協議(protocol),對應於不同的域,套接字還有一個地址(address)來作為它的名字。
  域(domain)指定了套接字通訊所用到的協議族,最常用的域是AF_INET,代表網路套接字,底層協議是IP協議。對於網路套接字,由於伺服器端有可能會提供多種服務,客戶端需要使用IP埠號來指定特定的服務。AF_UNIX代表本地套接字,使用Unix/Linux檔案系統實現。
  IP協議提供了兩種通訊手段:流(streams)和資料包(datagrams),對應的套接字型別(type)分別為流式套接字和資料包套接字。流式套接字(SOCK_STREAM)用於提供面向連線、可靠的資料傳輸服務。該服務保證資料能夠實現無差錯、無重複傳送,並按順序接收。流式套接字使用TCP協議。資料包套接字(SOCK_DGRAM)提供了一種無連線的服務。該服務並不能保證資料傳輸的可靠性,資料有可能在傳輸過程中丟失或出現資料重複,且無法保證順序地接收到資料。資料包套接字使用UDP協議。
  一種型別的套接字可能可以使用多於一種的協議來實現,套接字的協議(protocol)屬性用於指定一種特定的協議。  

  • (1)建立套接字
int socket(int  domain,int  type,int  protocol);
複製程式碼

對於SOCK_STREAM和SOCK_DGRAM而言,分別只有一種協議支援這種型別的套接字。因此protocol可以為0,表示預設的協議。

  • (2) 繫結套接字
int bind(int  sockfd,const  struct sockaddr *addr,socklen_t addrlen);
//將無名套接字sockfd與addr繫結(bind)複製程式碼
  • (3)監聽套接字
int listen(int  sockfd,int  backlog);
//backlog限定了等待服務的佇列的最大長度複製程式碼
  • (4)等待接受連線
int accept(int  sockfd,struct  sockaddr *addr,socklen_t  *addrlen);
複製程式碼

當客戶端程式嘗試連線sockfd套接字時,accept返回一個新的套接字與客戶端進行通訊。如果addr不是NULL,那麼客戶端的地址將會儲存在addr所指向的結構體中;呼叫accept()前必須先將addrlen初始化為addr所指向結構體的大小,accept()返回以後,addrlen將會被設定成客戶端套接字地址結構體的實際大小。然後,通過對accept()返回的套接字執行read()和write()操作即可實現與客戶端的簡單的通訊。  

  • (5)建立連線(客戶端)
int connect(int  sockfd,const  struct sockaddr *addr,socklen_t addrlen);
複製程式碼

connect()在無名套接字sockfd和addr之間建立連線。addr指向的結構體中可以包含伺服器的IP地址和埠號等資訊。

  • (6)資料傳輸
ssize_t send(int sockfd,const void *buf,size_t  len,int  flags);
ssize_t recv(int sockfd, void *buf, size_t len,int flags);
複製程式碼
  • (7)關閉套接字
int close(int  fd);
複製程式碼
  • (8)主機位元組序和網路位元組序的轉換
#include <
netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
//host to network,long unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
// long型函式用來轉換sockaddr_in.in_addr.s_addr;// short型函式用來轉換sockaddr_in.sin_port。複製程式碼

來源:https://juejin.im/post/5c380d2351882525da264393

相關文章