1. 程式人生 > >2017-2018-1 20155222 《信息安全系統設計基礎》第10周 Linux下的IPC機制

2017-2018-1 20155222 《信息安全系統設計基礎》第10周 Linux下的IPC機制

mct 執行 除了 comm 同進程 href sem_flag 消息隊列 con

2017-2018-1 20155222 《信息安全系統設計基礎》第10周 Linux下的IPC機制

IPC機制

在linux下的多個進程間的通信機制叫做IPC(Inter-Process Communication),它是多個進程之間相互溝通的一種方法。在linux下有多種進程間通信的方法:半雙工管道、命名管道、消息隊列、信號、信號量、共享內存、內存映射文件,套接字等等。使用這些機制可以為linux下的網絡服務器開發提供靈活而又堅固的框架。

以上內容引用自CSDN

共享內存

共享內存是在多個進程之間共享內存區域的一種進程間的通信方式,由IPC為進程創建的一個特殊地址範圍,它將出現在該進程的地址空間(這裏的地址空間具體是哪個地方?)中。其他進程可以將同一段共享內存連接到自己的地址空間中。所有進程都可以訪問共享內存中的地址,就好像它們是malloc分配的一樣。如果一個進程向共享內存中寫入了數據,所做的改動將立刻被其他進程看到。
共享內存是IPC最快捷的方式,因為共享內存方式的通信沒有中間過程,而管道、消息隊列等方式則是需要將數據通過中間機制進行轉換。共享內存方式直接將某段內存段進行映射,多個進程間的共享內存是同一塊的物理空間,僅僅映射到各進程的地址不同而已,因此不需要進行復制,可以直接使用此段空間。
註意:共享內存本身並沒有同步機制,需要程序員自己控制。

共享內存頭文件:

#include <sys/types.h>   
#include <sys/stat.h>  
#include <sys/shm.h>   

結構shmid_ds結構體(是不是很眼熟,看消息隊列的msgid_ds結構體):

strcut shmid_ds{  
    struct ipc_perm    shm_perm;  
    size_t    shm_segsz;  
    time_t    shm_atime;  
    time_t    shm_dtime;  
    ......  
}  

共享內存函數定義:

int shmget(key_t key,size_t size,int shmflg);  //shmget函數用來創建一個新的共享內存段, 或者訪問一個現有的共享內存段(不同進程只要key值相同即可訪問同一共享內存段)。第一個參數key是ftok生成的鍵值,第二個參數size為共享內存的大小,第三個參數sem_flags是打開共享內存的方式。  
eg.int shmid = shmget(key, 1024, IPC_CREATE | IPC_EXCL | 0666);//第三個參數參考消息隊列int msgget(key_t key,int msgflag);  
void *shmat(int shm_id,const void *shm_addr,int shmflg); //shmat函數通過shm_id將共享內存連接到進程的地址空間中。第二個參數可以由用戶指定共享內存映射到進程空間的地址,shm_addr如果為0,則由內核試著查找一個未映射的區域。返回值為共享內存映射的地址。  
eg.char *shms = (char *)shmat(shmid, 0, 0);//shmid由shmget獲得  
int shmdt(const void *shm_addr); //shmdt函數將共享內存從當前進程中分離。 參數為共享內存映射的地址。  
eg.shmdt(shms);  
int shmctl(int shm_id,int cmd,struct shmid_ds *buf);//shmctl函數是控制函數,使用方法和消息隊列msgctl()函數調用完全類似。參數一shm_id是共享內存的句柄,cmd是向共享內存發送的命令,最後一個參數buf是向共享內存發送命令的參數。

管道

管道實際是用於進程間通信的一段共享內存,創建管道的進程稱為管道服務器,連接到一個管道的進程為管道客戶機。一個進程在向管道寫入數據後,另一進程就可以從管道的另一端將其讀取出來。
管道的特點:
1、管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;
2、只能用於父子進程或者兄弟進程之間(具有親緣關系的進程)。比如fork或exec創建的新進程,在使用exec創建新進程時,需要將管道的文件描述符作為參數傳遞給exec創建的新進程。當父進程與使用fork創建的子進程直接通信時,發送數據的進程關閉讀端,接受數據的進程關閉寫端。
3、單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在與內存中。
4、數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,並且每次都是從緩沖區的頭部讀出數據。
管道的實現機制:
管道是由內核管理的一個緩沖區,相當於我們放入內存中的一個紙條。管道的一端連接一個進程的輸出。這個進程會向管道中放入信息。管道的另一端連接一個進程的輸入,這個進程取出被放入管道的信息。一個緩沖區不需要很大,它被設計成為環形的數據結構,以便管道可以被循環利用。當管道中沒有信息的話,從管道中讀取的進程會等待,直到另一端的進程放入信息。當管道被放滿信息的時候,嘗試放入信息的進程會等待,直到另一端的進程取出信息。當兩個進程都終結的時候,管道也自動消失。
管道只能在本地計算機中使用,而不可用於網絡間的通信。

pipe函數原型:

#include <unistd.h>   
int pipe(int file_descriptor[2]);//建立管道,該函數在數組上填上兩個新的文件描述符後返回0,失敗返回-1。  
eg.int fd[2]  
int result = pipe(fd); 

通過使用底層的read和write調用來訪問數據。 向file_descriptor[1]寫數據,從file_descriptor[0]中讀數據。寫入與讀取的順序原則是先進先出。
管道讀寫規則
當沒有數據可讀時
O_NONBLOCK disable:read調用阻塞,即進程暫停執行,一直等到有數據來到為止。
O_NONBLOCK enable:read調用返回-1,errno值為EAGAIN。
當管道滿的時候
O_NONBLOCK disable: write調用阻塞,直到有進程讀走數據
O_NONBLOCK enable:調用返回-1,errno值為EAGAIN
如果所有管道寫端對應的文件描述符被關閉,則read返回0
如果所有管道讀端對應的文件描述符被關閉,則write操作會產生信號SIGPIPE
當要寫入的數據量不大於PIPE_BUF(Posix.1要求PIPE_BUF至少512字節)時,linux將保證寫入的原子性。
當要寫入的數據量大於PIPE_BUF時,linux將不再保證寫入的原子性。

命名管道(FIFO)

命名管道是一種特殊類型的文件,它在系統中以文件形式存在。這樣克服了管道的弊端,他可以允許沒有親緣關系的進程間通信。
創建管道的兩個系統調用原型:

#include <sys/types.h>   
#include <sys/stat.h>   
int mkfifo(const char *filename,mode_t mode); //建立一個名字為filename的命名管道,參數mode為該文件的權限(mode%~umask),若成功則返回0,否則返回-1,錯誤原因存於errno中。  
eg.mkfifo( "/tmp/cmd_pipe", S_IFIFO | 0666 );  

具體操作方法只要創建了一個命名管道然後就可以使用open、read、write等系統調用來操作。創建可以手工創建或者程序中創建。

int mknod(const char *path, mode_t mode, dev_t dev); //第一個參數表示你要創建的文件的名稱,第二個參數表示文件類型,第三個參數表示該文件對應的設備文件的設備號。只有當文件類型為 S_IFCHR 或 S_IFBLK 的時候該文件才有設備號,創建普通文件時傳入0即可。  
eg.mknod(FIFO_FILE,S_IFIFO|0666,0);    

管道和命名管道的區別:
對於命名管道FIFO來說,IO操作和普通管道IO操作基本一樣,但是兩者有一個主要的區別,在命名管道中,管道可以是事先已經創建好的,比如我們在命令行下執行
mkfifo myfifo
就是創建一個命名通道,我們必須用open函數來顯示地建立連接到管道的通道,而在管道中,管道已經在主進程裏創建好了,然後在fork時直接復制相關數據或者是用exec創建的新進程時把管道的文件描述符當參數傳遞進去。
一般來說FIFO和PIPE一樣總是處於阻塞狀態。也就是說如果命名管道FIFO打開時設置了讀權限,則讀進程將一直阻塞,一直到其他進程打開該FIFO並向管道寫入數據。這個阻塞動作反過來也是成立的。如果不希望命名管道操作的時候發生阻塞,可以在open的時候使用O_NONBLOCK標誌,以關閉默認的阻塞操作。

信號 (signal)

#include <sys/types.h>   
#include <signal.h>   
void (*signal(int sig,void (*func)(int)))(int); //用於截取系統信號,第一個參數為信號,第二個參數為對此信號掛接用戶自己的處理函數指針。返回值為以前信號處理程序的指針。  
eg.int ret = signal(SIGSTOP, sig_handle);  

由於signal不夠健壯,推薦使用sigaction函數。

int kill(pid_t pid,int sig); //kill函數向進程號為pid的進程發送信號,信號值為sig。當pid為0時,向當前系統的所有進程發送信號sig。  
int raise(int sig);//向當前進程中自舉一個信號sig, 即向當前進程發送信號。  
#include <unistd.h>   
unsigned int alarm(unsigned int seconds); //alarm()用來設置信號SIGALRM在經過參數seconds指定的秒數後傳送給目前的進程。如果參數seconds為0,則之前設置的鬧鐘會被取消,並將剩下的時間返回。使用alarm函數的時候要註意alarm函數的覆蓋性,即在一個進程中采用一次alarm函數則該進程之前的alarm函數將失效。  
int pause(void); //使調用進程(或線程)睡眠狀態,直到接收到信號,要麽終止,或導致它調用一個信號捕獲函數。 

消息隊列(Message queues)

消息隊列是內核地址空間中的內部鏈表,通過linux內核在各個進程直接傳遞內容,消息順序地發送到消息隊列中,並以幾種不同的方式從隊列中獲得,每個消息隊列可以用IPC標識符唯一地進行識別。內核中的消息隊列是通過IPC的標識符來區別,不同的消息隊列直接是相互獨立的。每個消息隊列中的消息,又構成一個獨立的鏈表。
消息隊列克服了信號承載信息量少,管道只能承載無格式字符流。
消息隊列頭文件:

#include <sys/types.h>   
#include <sys/stat.h>   
#include <sys/msg.h>   

消息緩沖區結構:

struct msgbuf{  
    long mtype;  
    char mtext[1];//柔性數組  
};  

在結構中有兩個成員,mtype為消息類型,用戶可以給某個消息設定一個類型,可以在消息隊列中正確地發送和接受自己的消息。mtext為消息數據,采用柔性數組,用戶可以重新定義msgbuf結構。例如:

struct msgbuf{  
    long mtype;  
    char mtext[1];//柔性數組  
};

當然用戶不可隨意定義msgbuf結構,因為在linux中消息的大小是有限制的,在linux/msg.h中定義如下:

define MSGMAX 8192

消息總的大小不能超過8192個字節,包括mtype成員(4個字節)。
2、msqid_ds內核數據結構

struct msgid_ds{
    struct ipc_perm msg_perm;
    time_t msg_stime;
    time_t msg_rtime;
    time_t msg_ctime;
    unsigned long _msg_cbuyes;
    ..........
};

Linux內核中,每個消息隊列都維護一個結構體,此結構體保存著消息隊列當前狀態信息,該結構體在頭文件linux/msg.h中定義。
3、ipc_perm內核數據結構

struct ipc_perm{  
  key_t key;  
  uid_t uid;  
  gid_t gid;  
  .......  
}; 

結構體ipc_perm保存著消息隊列的一些重要的信息,比如說消息隊列關聯的鍵值,消息隊列的用戶id組id等。它定義在頭文件linux/ipc.h中。
常用函數:
系統建立IPC通訊 (消息隊列、信號量和共享內存) 時必須指定一個ID值。通常情況下,該id值通過ftok函數得到。

key_t ftok( const char * fname, int id );//參數一為目錄名稱, 參數二為id。如指定文件的索引節點號為65538,換算成16進制為0x010002,而你指定的ID值為38,換算成16進制為0x26,則最後的key_t返回值為0x26010002。  
eg.key_t key = key =ftok(".", 1);  
int msgget(key_t key,int msgflag); //msgget用來創建和訪問一個消息隊列。程序必須提供一個鍵值來命名特定的消息隊列。  
eg.int msg_id = msgget(key, IPC_CREATE | IPC_EXCL | 0x0666);//根據關鍵字創建一個新的隊列(IPC_CREATE),如果隊列存在則出錯(IPC_EXCL),擁有對文件的讀寫執行權限(0666)。  
int msgsnd(int msgid,const void *msgptr,size_t msg_sz,int msgflg); //msgsnd函數允許我們把一條消息添加到消息隊列中。msgptr只想準備發送消息的指針,指針結構體必須以一個長整型變量開始。   
eg.struct msgmbuf{  
    int mtype;  
    char mtext[10];  
};  
struct msgmbuf msg_mbuf;  
msg_mbuf.mtype = 10;//消息大小10字節  
memcpy(msg_mbuf.mtext, "測試消息", sizeof("測試消息"));  
int ret = msgsnd(msg_id, &msg_mbuf, sizeof("測試消息"), IPC_NOWAIT);  
int msgrcv(int msgid, void *msgptr, size_t msg_sz, long int msgtype, int msgflg); //msgrcv可以通過msqid對指定消息隊列進行接收操作。第二個參數為消息緩沖區變量地址,第三個參數為消息緩沖區結構大小,但是不包括mtype成員長度,第四個參數為mtype指定從隊列中獲取的消息類型。  
eg.int ret = msgrcv(msg_id, &msg_mbuf, 10, 10, IPC_NOWAIT | MSG_NOERROR);  
int msgctl(int msqid,int cmd,struct msqid_ds *buf); //msgctl函數主要是一些控制如刪除消息隊列等操作。 cmd值如下:  
IPC_STAT:獲取隊列的msgid_ds結構,並把它存到buf指向的地址。  
IPC_SET:將隊列的msgid_ds設置為buf指向的msgid_ds。  
IPC_RMID:內核刪除消息隊列,最後一項填NULL, 執行操作後,內核會把消息隊列從系統中刪除。  

消息隊列的本質
Linux的消息隊列(queue)實質上是一個鏈表,它有消息隊列標識符(queue ID)。 msgget創建一個新隊列或打開一個存在的隊列;msgsnd向隊列末端添加一條新消息;msgrcv從隊列中取消息, 取消息是不一定遵循先進先出的, 也可以按消息的類型字段取消息。

消息隊列與命名管道的比較
消息隊列跟命名管道有不少的相同之處,通過與命名管道一樣,消息隊列進行通信的進程可以是不相關的進程,同時它們都是通過發送和接收的方式來傳遞數據的。在命名管道中,發送數據用write,接收數據用read,則在消息隊列中,發送數據用msgsnd,接收數據用msgrcv。而且它們對每個數據都有一個最大長度的限制。
與命名管道相比,消息隊列的優勢在於,1、消息隊列也可以獨立於發送和接收進程而存在,從而消除了在同步命名管道的打開和關閉時可能產生的困難。2、同時通過發送消息還可以避免命名管道的同步和阻塞問題,不需要由進程自己來提供同步方法。3、接收程序可以通過消息類型有選擇地接收數據,而不是像命名管道中那樣,只能默認地接收。

2017-2018-1 20155222 《信息安全系統設計基礎》第10周 Linux下的IPC機制