歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux 系統應用編程——進程間通信(下)

Linux 系統應用編程——進程間通信(下)

日期:2017/3/1 11:46:29   编辑:關於Linux

在前面,我們學習了傳統的進程間通信方式——無名管道(pipe)、有名管道(fifo)和信號(signal)。

下面我們來學習System V IPC 對象:

1、共享內存(share memory);

2、信號燈(semaohore);

3、消息隊列(message queue);

IPC對象是活動在內核級別的一種進程間通信的工具。存在的IPC對象通過它的標識符來引用和訪問,這個標識符是一個非負整數,它唯一的標識了一個IPC對象,這個IPC對象可以是消息隊列或信號量或共享存儲器中的任意一種類型。

\

Linux系統中標識符被聲明成整數,所以可能存在的最大標識符為65535。當時對於IPC對象,其標識符(id)與文件描述符有所不同,使用open函數打開一個文件時,返回的文件描述符的值為當前進程最小可用的文件描述符數組的下標。IPC對象是系統全局的流水號,其刪除或創建時相應的標識符的值會不斷增加到最大的值,歸零循環分配使用。

IPC的標識符只解決了內部訪問一個IPC對象的問題,如何讓多個進程都訪問某一個特定的IPC對象還需要一個外部鍵(key),一個IPC對象都與一個鍵相關聯。這樣就解決了多進程在一個IPC對象上匯合的問題。IPC對象時需要指定一個鍵值,類型為key_t,在中定義為一個長整型。鍵值到標識符的轉換是由系統內核來維護的,這裡調用IPC對象的創建函數(semget msgget shmget )實現key 值到 id 的轉換。

從上圖中我們可以看到得到這個鍵值 key 有兩種方法:

1)通用方法:調用ftok()函數

函數ftok可以使用兩個參數生成一個鍵值,函數原型如下:

view plaincopy

#include

key_tftok(constchar*path,intid);

函數中參數path是一個文件名。函數中進行的操作是,取該文件的stat結構的st_dev成員和st_ino成員的部分值,然後與參數ID的第八位結合起來生成一個鍵值。由於只是使用st_dew和st_ino的部分值,所以會丟失信息,不排除兩個不同文件使用同一個ID,得到同樣鍵值的情況。

系統為每一個IPC對象保存一個ipc_perm結構體,該結構說明了IPC對象的權限和所有者,每一個版本的內核各有不用的ipc_perm結構成員。

文件 中對其定義:

view plaincopy

structipc_perm

{

ey_tkey;

id_tuid;

id_tgid;

id_tcuid;

id_tcgid;

nsignedshortmode;

nsignedshortseq;

};

2)父子進程之間:

Key 為IPC_PRIVATE,父子進程之間key值為IPC_PRIVATE。

當有了一個IPC對象的鍵值,如何讓多個進程知道這個鍵,可以有多種實現的辦法:

1) 、使用文件來做中間的通道,創建IPC對象進程,使用鍵IPC_PRIVATE成功建立IPC對象之後,將返回的標識符存儲在一個文件中。其他進程通過讀取這個標識符來引用IPC對象通信。

2)、定義一個多個進程都認可的鍵,每個進程使用這個鍵來引用IPC對象,值得注意的是,創建IPC對象的進程中,創建IPC對象時如果該鍵值已經與一個IPC對象結合,則應該刪除該IPC對象,再創建一個新的IPC對象。

3)、多進程通信中,對於指定鍵引用一個IPC對象而言,可能不具有拓展性,並且在該鍵值已經被一個IPC對象結合的情況下。所以必須刪除這個存在對象之後再建立一個新的。這有可能影響到其他正在使用這個對象的進程。函數ftok可以在一定程度上解決這個問題。

但IPC對象存在一些問題,主要集中在以下幾點:

1)、過於繁雜的編程接口,比起使用其他通信方式,IPC所要求的代碼量要明顯增多。

2)、IPC不使用通用的文件系統,這也是飽受指責的原因。所以不能使用標准I/O操作函數來讀寫IPC對象。為此不得不新增加一些函數來支持必要的一些操作(例如msgget msgrev msgctl等)並且對於不同類型的IPC對象都有一系列特定的操作函數。由於IPC不使用文件描述符,所以不能使用多路I/O監控函數select及poll函數來操作IPC對象。

3)、缺少的資源回收機制。由於IPC對象在使用過程中並不保存引用計數,所以當出現一個進程創建了IPC對象然後退出時,則這個對象只有在出現後面幾種情況才會被釋放或者刪除,即由某一個進程讀出消息,或者IPC的所有者或超級用戶刪除了這個對象。這也是IPC相對於管道或FIFO所欠缺的資源回收機制。

下面是文件對象和IPC對象的對比

\

一、共享內存

共享內存可以說是Linux 下最快速、最有效的進程間通信方式。兩個不同進程A 、B 共享內存的意思是,同一塊物理內存被映射到進程A 、B 各自的進程地址空間,進程A 可以即時看到進程B 對共享內存中數據的更新;反之,進程B 也可以即時看到進程A對共享內存中數據的更新。

這裡簡單說下映射的概念:

Linux系統會為每個進程分配 4GB 的虛擬地址空間,一定情況下,需要將虛擬內存轉換成物理內存,這就需要內存映射。為什麼我們需要使用虛擬地址呢?最主要的原因是不同PC的物理內存會不一樣,如果直接使用物理地址,會造成程序的移植性很差,另外虛擬地址訪問內存有以下優勢:

1、程序可以使用一系列相鄰的虛擬地址來訪問物理內存中不相鄰的大內存緩沖區。

2、程序可以使用一系列虛擬地址來訪問大於可用物理內存的內存緩沖區。當物理內存的供應量變小時,內存管理器會將物理內存頁(通常大小為 4 KB)保存到磁盤文件。數據或代碼頁會根據需要在物理內存與磁盤之間移動。

3、不同進程使用的虛擬地址彼此隔離。一個進程中的代碼無法更改正在由另一進程或操作系統使用的物理內存。

進程可用的虛擬地址范圍稱為該進程的“虛擬地址空間”。每個用戶模式進程都有其各自的專用虛擬地址空間。對於 32 位進程,虛擬地址空間通常為4 GB,范圍從 0x00000000 至 0xFFFFFFFF。

1、共享內存的概念

共享內存從字面意義解釋就是多個進程可以把一段內存映射到自己的進程空間,以此來實現數據的共享及傳輸,這也是所有進程間通信方式最快的一種,共享內存是存在於內核級別的一種資源。

在Shell 中可以使用ipcs 命令查看當前系統IPC中的狀態,在文件系統中/proc目錄下有對其描述的相應文件

\

ipcs -m,其中 -m 是 memory的意思。

在系統內核為一個進程分配內存地址時,通過分頁機制可以讓一個進程的物理地址不連續,同時也可以讓一段內存同時分配給不同的進程。共享內存機制就是通過該原理實現的,共享內存機制只是提供數據的傳送,如何控制服務器端和客戶端的讀寫操作互斥,這就需要一些其他的輔助工具,例如信號量。

采用共享內存通信的一個顯而易見的好處就是效率高,因為進程可以直接讀寫內存,而不需要任何數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶控件進行四次數據的拷貝,而共享內存只拷貝兩次數據:一次從輸入文件到共享區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據後就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,知道通信完畢為止,這樣,數據內同一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在接觸映射時才寫回文件的。因此,采用共享內存的通信方式效率是最高的。

共享內存最大不足之處在意,由於多個進程對同一塊內存區域具有訪問的權限,各個進程之間的同步問題顯得尤為重要。必須控制同一時刻只有一個進程對共享內存區域寫入數據,否則會造成數據的混亂。同步控制問題可以由信號量來解決;

對於每一個共享存儲段,內核會為其維護一個shmid_ds類型的結構體,其定義在頭文件中,其定義如下:

view plaincopy

structshmid_ds

{

structipc_permshm_perm//Operationpermissionstructure.

size_tshm_segsz//Sizeofsegmentinbytes.

pid_tshm_lpid//ProcessIDoflastsharedmemoryoperation.

pid_tshm_cpid//ProcessIDofcreator.

shmatt_tshm_nattch//Numberofcurrentattaches.

time_tshm_atime//Timeoflastshmat

time_tshm_dtime//Timeoflastshmdt

time_tshm_ctime//Timeoflastchangebyshmctl

}

2、共享內存的相關操作

1)創建或打開共享內存

要使用共享內存,首先要創建一個共享內存區域,創建共享內存的函數調用如下:

所需頭文件#include

#include

#include

函數原型int shmget(key_t, int size ,int shmflg);

函數參數Key:IPC_PRIVATE 或 ftok 的返回值

size:共享內存區大小

shmflag:同open函數的權限位,也可用8進制表示法

函數返回值成功:共享內存段標識符

出錯:-1

shmget函數除了可以用於創建一個新的共享內存外,也可以用於打開一個已存在的共享內存。其中,參數key表示索要創建或打開的共享內存的鍵值。size表示共享內存區域的大小,只在創建一個新的共享內存時生效。參數shmflag 表示調用函數的操作類型,也可用於設置共享內存的訪問權限,兩者通過邏輯或表示.參數key 和 flag 決定了調用函數 shmget 的作用,相應的約定如下:

1)當 key 為 IPC_PRIVATE 時,創建一個新的共享內存,此時參數 flag 的取值無效;

2)當 key 不為 IPC_PRIVATE時,且flag 設置了IPC_CREAT 位,而沒有設置 IPC_EXCL 位,則執行操作由key取值決定。如果key 為內核中每個已存在的共享內存的鍵值。則執行打開這個鍵的操作;反之,則執行創建共享內存的操作;

3)當 key 不為 IPC_PRIVATE時,且flag 設置了IPC_CREAT 位和 IPC_EXCL 位,則只執行創建共享內存的操作。參數key的取值應與內核中已存在的任何共享內存的鍵值都不相同,否則函數調用失敗,返回EEXIST錯誤,所以一般典型的調用代碼首先會檢查是否返回該錯誤,如果確實返回該錯誤,那麼只要調用打開共享內存的函數就可以了(即將flag 設置為 IPC_CREAT,而不設置IPC_EXCL);

2)附加

當一個共享內存創建或打開後,某個進程如果要使用該共享內存則必須將此內存區附加到它的地址空間,附加操作的系統調用函數如下:

所需頭文件#include

#include

#include

函數原型void *shmat (int shmid, const void *shaddr, int shmflg);

函數參數shmid :要映射的共享內存區標示符

shmaddr:將共享內存映射到指定地址(若為NULL,則表示由系統自動完成映射)

shmflg:默認0:共享內存只讀

函數返回值成功:映射後的地址

出錯:-1

參數shmid 指定要引入的共享內存,參數 addr 和 flag 組合說明要引入的地址值,通常將 shmaddr 設置為NULL ,shmflag為0;

3)分離

當進程對共享內存段的操作完成後,應調用 shmdt 函數,作用是將指定的共享內存段從當前進程空間中脫離出去,函數原型如下:

所需頭文件#include

#include

#include

函數原型int shmdt(const void *shmaddr);

函數參數shmaddr:共享內存映射後的地址

函數返回值成功:0

出錯:-1

此函數僅用於將共享內存區域與進程的地址空間分離,並不刪除共享內存本身。參數addr是調用 shmat 函數時的返回值。

4)共享內存的控制

由於共享內存這一特殊的資源類型,使它不同於普通的文件,因此,系統需要為其提供專有的操作函數,其函數原型如下:

所需頭文件#include

#include

#include

函數原型int shmctl(int shmid, int cmd, struct shmid_ds *buf);

函數參數shmid:要操作的共享內存標示符

cmd: IPC_STAT (獲取對象屬性)

IPC_SET(設置對象屬性)

IPC_RMID(刪除對象)

buf:指定IPC_STAT/ IPC_SET 時用以保存/設置屬性

函數返回值成功:0

出錯:-1

下面是一個實例,兩個進程間實現共享內存進行通信:

view plaincopy

#include

#include

#include

#include

#include

#include

#include

#defineBUFFER_SIZE2048

intmain()

{

pid_tpid;

intshmid;

char*shm_addr;

charflag[]="WROTE";

charbuffer[BUFFER_SIZE];

if((shmid=shmget(IPC_PRIVATE,BUFFER_SIZE,0666))<0)

{

perror("shmget");

exit(-1);

}

else

{

printf("Createshared-memory:%d\n",shmid);

}

system("ipcs-m");

pid=fork();

if(pid<0)

{

perror("forkerror");

exit(-1);

}

elseif(pid==0)

{

shm_addr=shmat(shmid,0,0);

if(shm_addr==(void*)-1)

{

perror("Child:shmat");

exit(-1);

}

else

{

printf("Child:Attachshared-memory:%p\n",shm_addr);

}

system("ipcs-m");

while(strncmp(shm_addr,flag,strlen(flag)))

{

printf("Child:waitforenabledata...\n");

sleep(5);

}

strcpy(buffer,shm_addr+strlen(flag));

printf("Child:shared-memory:%s\n",buffer);

if((shmdt(shm_addr))<0)

{

perror("shmdt");

exit(-1);

}

else

{

printf("Child:Deattachshared-memory\n");

}

system("ipcs-m");

if(shmctl(shmid,IPC_RMID,NULL)==-1)

{

perror("Child:shmctl(IPC_RMID)");

exit(-1);

}

else

{

printf("Deleteshmared-memory\n");

}

system("ipcs-m");

}

else

{

if((shm_addr=shmat(shmid,0,0))==(void*)-1)

{

perror("parent:shmat");

exit(-1);

}

else

{

printf("Parent:Attachshmared-memory:%p\n",shm_addr);

}

sleep(1);

printf("\nInputsomestring:\n");

fgets(buffer,BUFFER_SIZE,stdin);

strncpy(shm_addr+strlen(flag),buffer,strlen(buffer));

strncpy(shm_addr,flag,strlen(flag));

if((shmdt(shm_addr))<0)

{

perror("Parent:shmdt");

exit(-1);

}

else

{

printf("Parent:Deattachshared-memory\n");

}

system("ipcs-m");

waitpid(pid,NULL,0);

printf("Finsihed\n");

}

return0;

}

執行結果如下:

view plaincopy

fs@ubuntu:~/qiang/shm$./shm

Createshared-memory:196613

------SharedMemorySegments--------

keyshmidownerpermsbytesnattchstatus

0x000000000fs700768002dest

0x0000000032769fs70018432002dest

0x0000000065538fs700201962dest

0x0000000098307fs700170282dest

0x00000000131076fs700198002dest

0x00000000196613fs66620480

Parent:Attachshmared-memory:0xb773d000

Child:Attachshared-memory:0xb773d000

------SharedMemorySegments--------

keyshmidownerpermsbytesnattchstatus

0x000000000fs700768002dest

0x0000000032769fs70018432002dest

0x0000000065538fs700201962dest

0x0000000098307fs700170282dest

0x00000000131076fs700198002dest

0x00000000196613fs66620482

Child:waitforenabledata...

Inputsomestring:

xiaoqiang

Parent:Deattachshared-memory

------SharedMemorySegments--------

keyshmidownerpermsbytesnattchstatus

0x000000000fs700768002dest

0x0000000032769fs70018432002dest

0x0000000065538fs700201962dest

0x0000000098307fs700170282dest

0x00000000131076fs700198002dest

0x00000000196613fs66620481

Child:shared-memory:xiaoqiang

Child:Deattachshared-memory

------SharedMemorySegments--------

keyshmidownerpermsbytesnattchstatus

0x000000000fs700768002dest

0x0000000032769fs70018432002dest

0x0000000065538fs700201962dest

0x0000000098307fs700170282dest

0x00000000131076fs700198002dest

0x00000000196613fs66620480

Deleteshmared-memory

------SharedMemorySegments--------

keyshmidownerpermsbytesnattchstatus

0x000000000fs700768002dest

0x0000000032769fs70018432002dest

0x0000000065538fs700201962dest

0x0000000098307fs700170282dest

0x00000000131076fs700198002dest

Finsihed

fs@ubuntu:~/qiang/shm$

通過結果,不斷的調用 system ,可以看到共享內存區的變化;

二、信號量

信號燈(semaphore),也叫信號量,它是不同進程間或一個給定進程內部不同線程間同步的機制。

信號燈種類:1)posix 有名信號燈 2)posix 基於內存的信號燈(無名信號燈) 3)System V 信號燈 (IPC對象);

信號燈:

1)二值信號燈:值為 0 或 1。與互斥鎖類似,資源可用時值為1,不可用時值為 0;

2)計數信號燈:值在 0 到 n 之間。用來統計資源,其值代表可用資源數;

等待操作時等待信號燈的值變為大於0,然後將其減一;而釋放操作則相反,用來喚醒等待資源的進程或者線程;

\

事實上,在信號量的實際應用中,是不能單獨定義一個信號量的,而只能定義一個信號量集,其中包含一組信號量,同一信號量集中的信號量使用同一個引用ID,這樣的設置是為了多個資源和同步操作的需要。每個信號量集都有一個與之對應結構,一種記錄了信號量集的各種信息,該結構的定義如下:

view plaincopy

structsemid_ds

{

structipc_permsem_perm//Operationpermissionstructure.

unsignedshortsem_nsems//Numberofsemaphoresinset.

time_tsem_otime//Lastsemop

time_tsem_ctime//Lasttimechangedbysemctl

}

sem結構記錄一個信號量的信息,其定義如下:

view plaincopy

structsem

{

unsignedshortsemval//Semaphorevalue.

pid_tsempid//ProcessIDoflastoperation.

unsignedshortsemncnt//Numberofprocesseswaitingforsemvaltobecomegreaterthancurrentvalue.

unsignedshortsemzcnt//Numberofprocesseswaitingforsemvaltobecome0.

}

下面是信號量操作有關的函數調用:

函數說明:

在Linux 系統中,使用信號量通常分為以下幾個步驟:

1)創建信號量或獲得系統已存在的信號量,此時需要調用semget() 函數。不同進程通過使用同一個信號鍵值來獲得同一個信號量;

2)初始化信號量,此時使用senctl() 函數的SETVAL 操作。當使用二維信號量時,通常將信號量初始化為1;

3)進行信號量的PV操作,此時調用semop() 函數。這一步是實現進程之間的同步和互斥的核心工作部分;

4)如果不需要信號量,則從系統中刪除它,此時使用semctl() 函數的IPC_RMID 操作。此時需要注意,在程序中不應該出現對已經被刪除的信號量的操作;

下面是具體說明:

1、創建或打開信號量集

使用函數 semget 可以創建或者獲得一個信號量集ID,函數原型如下:

所需頭文件#include

#include

#include

函數原型int semget(key_t key, int nsems, int semflg);

函數參數key :和信號燈集關聯的key 值

nsems:信號燈集中包含的信號燈數目

semflg:信號燈集的訪問權限,通常為IPC_CREAT|0666

函數返回值成功:信號燈集ID

出錯:-1

此函數可以用於創建或打開一個信號量集。其中,參數key 表示要創建或打開的信號量集對於的鍵值。參數 nsems 表示創建的信號量集中包含的信號量的個數,此參數只在創建一個新的信號量集時有效。參數flag表示調用函數的操作類型,也可以用於設置信號量集的訪問權限,兩者通過邏輯或表示。調用函數semget 的作用由參數key和flag 決定。

另外,當semget 成功創建一個新的信號量集時,它相應的semid_ds結構被初始化。ipc_perm 結構中成員被設置成相應的值 ,sem_nsems設置為函數參數nsems的值,sem_otime被設置為0,sem_ctime 設置為系統當前時間。

2、對信號量集的操作

函數semop 用以操作一個信號量集,函數原型如下:

所需頭文件#include

#include

#include

函數原型int semop(int semid,struct sembuf *opsptr,size_t nops);

函數參數semid:信號燈集ID

struct sembuf 結構體每一個元素表示一個操作;

nops:要操作的信號燈的個數

函數返回值成功:0

出錯:-1

此函數是一個原子操作,一旦執行就將執行數組中所有的操作;

結構體sembuf 用來說明所要執行的操作,其定義如下:

view plaincopy

structsembuf

{

unsignedshortsem_num;//要操作的信號燈的編號

shortsem_op;//0:等待,知道信號燈的值變為0

//1:釋放資源,V操作

//-1:分配資源,P操作

shortsem_flg;//0,IPC_NOWAIT,SEM_UNDO

}

3、信號量集的控制

和共享內存的控制一樣,信號量集也有自己的專屬控制函數 semctl ,函數原型如下:

所需頭文件#include

#include

#include

函數原型int semctl(int semid, int semnum, int cmd, union semun arg);

函數參數semid:信號燈集ID

semnum:要修改的信號燈編號

cmd :GETVAL:獲取信號燈的值

SETVAL:設置信號燈的值

IPC_RMID:從系統中刪除信號燈集合

函數返回值成功:0

出錯:-1

參數cmd 定義函數所要進行的操作,其取值及表達的意義與參數arg 的設置有關,最後一個參數arg 是一個聯合體(union),其定義如下:

view plaincopy

unionsemun{

intval;/*ValueforSETVAL*/

structsemid_ds*buf;/*BufferforIPC_STAT,IPC_SET*/

unsignedshort*array;/*ArrayforGETALL,SETALL*/

structseminfo*__buf;/*BufferforIPC_INFO(Linux-specific)*/

};

下面是個應用實例:

view plaincopy

#include

#include

#include

#include

#include

#include

#include

#defineDELAY_TIME3

unionsemun

{

intval;/*ValueforSETVAL*/

structsemid_ds*buf;/*BufferforIPC_STAT,IPC_SET*/

unsignedshort*array;/*ArrayforGETALL,SETALL*/

structseminfo*__buf;/*BufferforIPC_INFO(Linux-specific)*/

};

intinit_sem(intsem_id,intinit_value);

intdel_sem(intsem_id);

intsem_p(intsem_id);

intsem_v(intsem_id);

intinit_sem(intsem_id,intinit_value)

{

unionsemunsem_union;

sem_union.val=init_value;

if(semctl(sem_id,0,SETVAL,sem_union)==-1)

{

perror("Initializesemaphore");

exit(-1);

}

return0;

}

intdel_sem(intsem_id)

{

unionsemunsem_union;

if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)

{

perror("Deletesemaphore");

return-1;

}

}

intsem_p(intsem_id)

{

structsembufsem_b;

sem_b.sem_num=0;

sem_b.sem_op=-1;

sem_b.sem_flg=SEM_UNDO;

if(semop(sem_id,&sem_b,1)==-1)

{

perror("Poperation");

return-1;

}

return0;

}

intsem_v(intsem_id)

{

structsembufsem_b;

sem_b.sem_num=0;

sem_b.sem_op=1;

sem_b.sem_flg=SEM_UNDO;

if(semop(sem_id,&sem_b,1)==-1)

{

perror("Voperation");

return-1;

}

return0;

}

intmain()

{

pid_tpid;

intsem_id;

sem_id=semget(ftok(".",'a'),1,0666|IPC_CREAT);

init_sem(sem_id,0);

pid=fork();

if(pid<0)

{

perror("forkfails");

exit(-1);

}

elseif(pid==0)

{

printf("Childprocesswillwaitforsomeseconds...\n");

sleep(DELAY_TIME);

printf("Thereturnedvalueis%dinthechildprocess(PID=%d)\n",

pid,getpid());

sem_v(sem_id);

}

else

{

sem_p(sem_id);

printf("Thereturnedvalueis%dinthefatherprocess(PID=%d)\n",

pid,getpid());

sem_v(sem_id);

del_sem(sem_id);

}

return0;

}

執行結果如下:

view plaincopy

fs@ubuntu:~/qiang/sem$./sem

Childprocesswillwaitforsomeseconds...

Thereturnedvalueis0inthechildprocess(PID=2882)

Thereturnedvalueis2882inthefatherprocess(PID=2880)

fs@ubuntu:~/qiang/sem$

Copyright © Linux教程網 All Rights Reserved