Linux——程序通訊
管道
管道是Linux支援的最初程序間通訊形式之一,具有以下特點:
管道是半雙工的,資料只能向一個方向流動;需要雙方通訊時,需要建立起兩個管道;
只能用於父子程序或者兄弟程序之間(具有親緣關係的程序);
單獨構成一種獨立的檔案系統:管道對於管道兩端的程序而言,就是一個檔案,但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在與記憶體中。
資料的讀出和寫入:一個程序向管道中寫的內容被管道另一端的程序讀出。
利用系統呼叫pipe()可建立一個簡單的管道。該系統呼叫只需要1個引數,由兩個整陣列成的1個數組。若呼叫成功,則該陣列將會包含管道所使用的兩個新的檔案描述符。
int fd[2];
pipe(fd);
呼叫成功,fd[0]存放供讀程序使用的檔案描述符(管道出口),fd[1]存放供寫程使用的檔案描述符(管道入口)。
Pipe()函式建立的管道的兩端處於一個程序中間,在實際應用中沒有太大意義,因此,一個程序在由pipe()建立管道後,一般再fork一個子程序,然後通過管道實現父子程序間的通訊,子程序將從父程序那裡繼承所有開啟的檔案描述符。則父子兩個程序都能訪問組成管道的兩個檔案描述符,這樣子程序可以向父程序傳送訊息(或者相反)。管道是單方向的,資料只能向一個方向移動,從fd[1]寫入,從fd[0]讀出。
傳送程序利用檔案系統的系統呼叫write(fd[1],buf,size),把buf中size個字元送入fd[1],接收程序利用read(fd[0],buf,size),把從fd[0]讀出的size個字元置入buf中。這樣管道按FIFO(先進先出)方式傳送資訊。
兩個程序一般把自己用不到的管道的一端關掉。例如:父程序從子程序接收資料,則關掉fd[1],而子程序關掉fd[0]。
兩程序間要想雙向通訊,則要通過建立兩個管道,並在兩程序中正確分配好檔案描述符。
main() {int x,fd[2]; char buf[30],s[30]; pipe(fd); while ((x=fork())==-1); if (x==0) { close(fd[0]); printf(“Child Process!\n”); strcpy(buf, “This is an example\n”); write(fd[1],buf,30); exit(0); } else{ close(fd[1]); printf(“Parent Process!\n”); read(fd[0],buf,30); printf(“%s”,s); } }
建立一個管道,同時,父程序生成子程序p1、p2,這兩個子程序分別向管道中寫入各自的字串,父程序讀出它們。
#include “stdio.h”
main()
{int p1,p2,fd[2];
char buf[50],s[50];
pipe(fd);
while ((p1=fork())==-1);
if (p1==0)
{
close(fd[0]);
printf(“Child Process1!\n”);
strcpy(buf,“Child Process1 is sending messages!\n”);
write(fd[1],buf,50);
exit(1);
}
else {
while ((p2=fork())==-1);
if (p2==0)
{
close(fd[0]);
printf(“Child Process2!\n”);
strcpy(buf,“Child Process2 is sending messages!\n”);
write(fd[1],buf,50);
exit(2);
}
close(fd[1]);
printf(“Parent Process!\n”);
read(fd[0],s,50);
printf(“%s”,s);
read(fd[0],s,50);
printf(“%s”,s);
exit(0);
}
}
共享記憶體
1.IPC簡介
IPC(Interprocess Communication)是UNIX System v的一個核心程式包,它負責完成System V程序之間的大量資料傳送工作。在IPC包被開發出來之前,通訊能力一直是UNIX系統的一個弱點,因為只能利用pipe來傳遞大量資料。而pipe又存在著只有呼叫pipe的程序的子孫後代才能使用它進行通訊的缺點。
UNIXSystem V IPC軟體包分三個組成部分:
(1) 共享儲存器(shared memory)方式可使得不同程序通過共享彼此的虛擬空間而達到互相對共享區操作和資料通訊的目的。
(2) 訊息(message)用於程序之間傳遞分類的格式化資料。
(3) 訊號量(semaphore)機制用於通訊程序之間往前推進時的同步控制。訊號量總是和共享儲存器方式一起使用。
由於上述三種方式在UNIXSystem V中是作為一個整體實現的,因此, 它們具有下述
共同性質;
(1)每種機制都用兩種基本資料結構來描述該機制。即:
①索引表:其中一個表項由關鍵字、訪問控制結構及操作狀態資訊組成。每個索引表項描述一個通訊例項或通訊例項的集合。
②例項表:一個例項表項描述一個通訊例項的有關特徵。例如,訊息機制中訊息隊列表相當於索引表,而訊息頭表則相當於例項表。
(2)索引表項中的關鍵字是一個大於零的整數,它由使用者選擇名字。
(3)索引表的訪問控制結構中含有建立該表項程序的使用者id和使用者組id。由後述“control”類系統呼叫,可為使用者和同組使用者設定讀一寫一執行許可權,從而起到通訊保護作用。
(4)每種通訊機制的“control”類系統呼叫可用來查詢索引表項中的狀態,以及置狀態資訊或從系統中刪除表項。
(5)除了“control”類系統呼叫之外,每種通訊機制還含有一個"get"類系統呼叫,以建立一個新的索引表項或者用於獲得已建立的索引表項的描述字。
(6)每一種索引表項都使用下列公式計算索引表項的描述字;
描述字=索引表長度×分配序號+索引表項下標
例如,如果訊息隊列表長100,表項1的描述字可以是100×(分配序號)+1=1、101、201等。這樣做的好處是,當程序釋放了一箇舊的索引表項,且該索引表項又分配給另外的程序時,因為分配序號的增加將使得描述字改變.從而原來的程序不可能再次訪問該表項。由此,可以起到通訊保護作用。
其他系統呼叫訪問索引表項時的索引值為描述字mod(索引表長度)。
2.共享記憶體概念
多個程序共享一塊存貯空間,諸程序可通過對共享存貯區中的資料進行讀和寫來實現通訊。首先由一個程序建立一個共享記憶體段,設定大小和訪問許可權,之後程序即可掛接該存貯段,同時將其對映到自己當前的資料空間中。然後其他的程序在許可權許可的情況下,也掛接該存貯段,並將其對映到自己當前的資料空間中。每個程序通過掛接的地址訪問共享記憶體段。當一個程序對共享記憶體段的操作已經結束時,可以脫接該段。當所有程序都已完成對該共享記憶體段的操作時,通常由段的建立程序負責將其刪除。
每個共享記憶體段都有一個shmid_ds型別的控制結構,該結構中包括對共享記憶體段的訪問許可權,其資料結構如下:
struct shmid_ds
{
struct ipc_perm shm_perm; /*操作許可權結構 */
int shm_segsz; /*以位元組為單位的共享段大小*/
struct region *shm_reg; /*指向共享段的指標*/
char pad[4]; /*系統使用*/
ushort shm_lpid; /*最後使用shmop的時間*/
ushort shm_cpid; /*建立程序的id*/
ushort shm_nattch; /*系統使用*/
ushort shm_cnattc; /*系統使用*/
time_t shm_atime; /*最後使用shmat的時間*/
time_t shm_dtime; /*最後使用shmdt的時間*/
time_t shm_ctime; /*共享記憶體段最後修改時間*/
};
3.共享記憶體的系統呼叫
操縱共享記憶體共有4個系統呼叫。它們是:
(1) shmget()
建立一個新的共享區或返回一個已存在的共享儲存區描述字;
int shmget(key_t key,int size,int shmflag),其中,key是使用者指定的共享區號,size是共享儲存區的長度,而shmflag用來標識共享記憶體段的建立條件機以及訪問許可權。
成功,返回共享記憶體段的識別符號,核心中用於唯一的標識一個物件。對存在於核心存貯空間中的每個共享記憶體段,核心均為其維護著一個數據結構shmid_ds。
失敗,返回-1,設定errno。
①第一個引數key(鍵值),預定義的資料型別key_t,其型別是長整型。用來建立IPC識別符號,shmget()返回的識別符號與key值一一對應,不同的key值返回不同的識別符號。
②第二個引數size,決定了共享記憶體段的大小(若訪問已存在的記憶體段,該引數可設為0)。有最大位元組數的限制,0x2000000=32M,極限值,可檢視/usr/include/linux/shm.h。
③第三個引數shmflag,用於設定訪問許可權及標識建立條件。
在/usr/include/linux/ipc.h中可找到一些預定義的常量:
#define IPC_PRIVATE ((key_t)0)
#define IPC_CREAT 00001000(八進位制)
#define IPC_EXCL 00002000(八進位制)
key值為IPC_PRIVATE時,呼叫shmget將生成一個新的共享記憶體段。
Shmflag為0666|IPC_CREAT時,如果key值是新的,返回新建立的記憶體段識別符號。
若key值是舊的,返回已存在核心中的具有相同關鍵字值的記憶體段識別符號。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
main{
key_t key=15;
int shmid_1,shmid_2;
if ((shmid_1=shmget(key,1000,0644|IPC_CREAT))==-1){
perror(“shmget shmid_1”);exit(1);
}
printf(“First shared memory identifier is %d\n”,shmid_1);
if ((shmid_2=shmget(IPC_PRIVATE,20,0644))==-1){
perror(“shmget shmid_2”);exit(2);
}
printf(“Second shared memory identifier is %d\n”,shmid_2);
exit(0);
}
執行兩次上面的程式,生成三個共享記憶體段。
ipcs命令可顯示三種類型IPC物件的狀況。
(2)shmat()
連線記憶體段,對映到自己的地址空間中;
int shmat(int shmid,void *shmaddr,int shmflag)
成功:返回該共享記憶體段連線到呼叫程序地址空間上的地址(指標)
錯誤:返回-1。
一旦正確地連上一個記憶體段,該共享記憶體段就成為程序虛地址空間地一部分,進行讀、寫就很簡單,利用指標。
①第一個引數shmid,是一個有效的共享記憶體識別符號;
②第二個引數shmaddr,非0,該值作為掛接的地址,設為0,則由系統來選擇掛接地址;
③第三個引數shmflag,可指定掛接後的訪問許可權,(預設情況,允許讀、寫)。
(3)shmdt()
當某個程序不需要一個共享記憶體段時,呼叫該系統呼叫,斷開與該記憶體段的連線。
int shmdt(void *shmaddr)
成功:返回0;
錯誤:返回-1。
引數shmaddr指向一個已掛接的記憶體段。
(4)shmctl()
使用者對一個存在的共享記憶體段進行一系列的操作;
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
成功:返回0;
錯誤:返回-1,設定errno。
①第一個引數shmid,前面shmget()呼叫返回的有效的共享記憶體識別符號;
②第二個引數cmd,指明shmctl將要執行的操作;
③第三個引數buf,指向一個shmid_ds類的結構。
cmd可執行的操作:(這些也是在/usr/include/linux/ipc.h中定義的)
IPC_STAT:返回由shmid值指定的存貯段shmid_ds結構的當前值。
IPC_SET:修改shmid_ds結構中反問許可權子結構的若干成員
IPC_RMID:刪除shmid指向的記憶體段(並非真正刪除,只有當前連線到該記憶體段的最後一個程序正確地斷開了與它的連線,實際的刪除操作才會發生)。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
main()
{
key_t key=15; /*在實際實驗過程中,為了避免每個同學建立的共享儲存區關鍵字一樣而相互干擾,關鍵字請用學號末3位*/
int shmid_1,shmid_2;
if ((shmid_1=shmget(key,1000,0644|IPC_CREAT))==-1){
perror("shmget shmid_1");exit(1);
}
printf("First shared memory identifier is %d\n",shmid_1);
if ((shmid_2=shmget(IPC_PRIVATE,20,0644))==-1){
perror("shmget shmid_2");exit(2);
}
printf("Second shared memory identifier is %d\n",shmid_2);
exit(0);
}
程式1
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHMKEY 75
#define K 1024
int shmid
main ()
{
int i,*pint;
char *addr;
extern char * shmat ();
extern cleanup ();
shmid=shmget(SHMKEY,16*K,0777|lPC_CREAT); /* 建立16K共享區SHMKEY */
addr=shmat(shmid,0,0); /* 共享區首地址 */
printf ("addr 0x%x\n",addr);
pint=(int *)addr;
for (i=0;i<256;i++) *pint++=i; /* 共享區第一個字中寫入長度256,以便接收程序讀*/
*pint=256;
pause(); /*等待接收程序讀 */
}
cleanup()
{
shmctl (shmid,IPC_RMID,0);
exit ();
}
在程式1中,該程序建立了16K位元組的共享儲存區,並將儲存區附接到了虛擬地址addr上。然後,從該儲存區的起始單元開始,順序寫入0到255個自然數。如果該程序捕捉到一個軟中斷訊號(SIGKILL除外),則由系統呼叫shmctl刪除該共享區。
程式2
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHMKEY 75
#define K 1024
int shmid
main ()
{
int i,*pint;
char *addr;
extern char * shmat ();
shmid=shmget(SHMKEY,8*K,0777); /* 取共享區SHMKEY的id * /
addr=shmat(shmid,0,0); /* 連線共享區* /
pint=(int *)addr;
while (*pint==0);
for (i=0;i<256;*pint++) /* 共享區的第一個位元組為0時,等待* /
printf(“%d\n”,*pint++); /* 列印共享區中的內容
}
在程式2中,另一個程序附接到與關鍵字SHMKEY相關聯的儲存區上。也就是與程式1所述的同一個儲存區上.為了表明每個程序可以附接一個共享儲存區的不同總量,程式2中只取該儲存區的8K位元組.該程序等待著直到程式1中程序在共享儲存區中的第一個位元組寫入一個非零值後讀出該儲存區,此時程式1的程序暫停以使程式2的程序執行讀出列印操作.
需要指出的是,共享儲存區機制只為通訊程序提供了訪問共享儲存區的操作條件,而對通訊的同步控制則要依靠訊號量機制等才能完成。
參考文章
浙江理工大學——作業系統實驗指導(2020.10)