共享內存可以從字面上去理解,就把一片邏輯內存共享出來,讓不同的進程去訪問它,修改它。共享內存是在兩個正在運行的進程之間共享和傳遞數據的一種非常有效的方式。不同進程之間共享的內存通常安排為同一段物理內存。進程可以將同一段共享內存連接到它們自己的地址空間中,所有進程都可以訪問共享內存中的地址,就好像它們是由用C語言函數malloc分配的內存一樣。而如果某個進程向共享內存寫入數據,所做的改動將立即影響到可以訪問同一段共享內存的任何其他進程。
但有一點特別要注意:共享內存並未提供同步機制。也就是說,在第一個進程結束對共享內存的寫操作之前,並無自動機制可以阻止第二個進程開始對它進行讀取。所以我們通常需要用其他的機制來同步對共享內存的訪問,例如信號量。
int shmget(key_t key, size_t size, int shmflg);
◇第一個參數,共享內存段的命名,shmget函數成功時返回一個與key相關的共享內存標識符(非負整數),用於後續的共享內存函數。調用失敗返回-1.
☆其它的進程可以通過該函數的返回值訪問同一共享內存,它代表進程可能要使用的某個資源,程序對所有共享內存的訪問都是間接的,程序先通過調用shmget函數並提供一個鍵,再由系統生成一個相應的共享內存標識符(shmget函數的返回值),只有shmget函數才直接使用信號量鍵,所有其他的信號量函數使用由semget函數返回的信號量標識符。
◇第二個參數,size以字節為單位指定需要共享的內存容量。
◇第三個參數,shmflg是權限標志,它的作用與open函數的mode參數一樣,如果要想在key標識的共享內存不存在時,創建它的話,可以與IPC_CREAT做或操作。共享內存的權限標志與文件的讀寫權限一樣,舉例來說,0644,它表示允許一個進程創建的共享內存被內存創建者所擁有的進程向共享內存讀取和寫入數據,同時其他用戶創建的進程只能讀取共享內存。
void *shmat(int shm_id, const void *shm_addr, int shmflg);
◇第一次創建完共享內存時,它還不能被任何進程訪問,shmat函數的作用就是用來啟動對該共享內存的訪問,並把共享內存連接到當前進程的地址空間。
◇第一個參數,shm_id是由shmget函數返回的共享內存標識。
◇第二個參數,shm_addr指定共享內存連接到當前進程中的地址位置,通常為空,表示讓系統來選擇共享內存的地址。
◇第三個參數,shm_flg是一組標志位,通常為0。
◇調用成功時返回一個指向共享內存第一個字節的指針,如果調用失敗返回-1.
int shmdt(const void *shmaddr);
◇該函數用於將共享內存從當前進程中分離。注意,將共享內存分離並不是刪除它,只是使該共享內存對當前進程不再可用。
◇參數shmaddr是shmat函數返回的地址指針,調用成功時返回0,失敗時返回-1。
int shmctl(int shm_id, int command, struct shmid_ds *buf);
◇第一個參數,shm_id是shmget函數返回的共享內存標識符。
◇第二個參數,command是要采取的操作,它可以取下面的三個值 :
IPC_STAT:把shmid_ds結構中的數據設置為共享內存的當前關聯值,即用共享內存的當前關聯值覆蓋shmid_ds的值。
IPC_SET:如果進程有足夠的權限,就把共享內存的當前關聯值設置為shmid_ds結構中給出的值
IPC_RMID:刪除共享內存段
◇第三個參數,buf是一個結構指針,它指向共享內存模式和訪問權限的結構。
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
shmdata.h的源碼:
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#define TEXT_SZ 2048
struct shared_use_st
{
int written;/* 作為一個標志,非0:表示可讀,0表示可寫 */
char text[TEXT_SZ];/* 記錄寫入和讀取的文本 */
};
#endif
shmread.c的源代碼
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<sys/shm.h>
#include"shmdata.h"
int main()
{
int running =1; //程序是否繼續運行的標志
void*shm = NULL; //分配的共享內存的原始首地址
struct shared_use_st *shared;//指向shm
int shmid; //共享內存標識符
//創建共享內存
shmid = shmget((key_t)MEM_KEY,sizeof(struct shared_use_st),0666|IPC_CREAT);
if(shmid ==-1)
{
fprintf(stderr,"shmget failed\n");
exit(EXIT_FAILURE);
}
//將共享內存連接到當前進程的地址空間
shm = shmat(shmid,0,0);
if(shm ==(void*)-1)
{
fprintf(stderr,"shmat failed\n");
exit(EXIT_FAILURE);
}
printf("\nMemory attached at %X\n",(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 failed\n");
exit(EXIT_FAILURE);
}
//刪除共享內存
if(shmctl(shmid, IPC_RMID,0)==-1)
{
fprintf(stderr,"shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
shmwrite.c的源代碼
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/shm.h>
#include"shmdata.h"
#define MEM_KEY (1234)
int main()
{
int running =1;
void*shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[BUFSIZ +1];//用於保存輸入的文本
int shmid;
//創建共享內存
shmid = shmget((key_t)MEM_KEY,sizeof(struct shared_use_st),0666|IPC_CREAT);
if(shmid ==-1)
{
fprintf(stderr,"shmget failed\n");
exit(EXIT_FAILURE);
}
//將共享內存連接到當前進程的地址空間
shm = shmat(shmid,(void*)0,0);
if(shm ==(void*)-1)
{
fprintf(stderr,"shmat failed\n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %X\n",(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 failed\n");
exit(EXIT_FAILURE);
}
sleep(2);
exit(EXIT_SUCCESS);
}
◇程序shmread創建共享內存,然後將它連接到自己的地址空間。在共享內存的開始處使用了一個結構struct_use_st。該結構中有個標志written,當共享內存中有其他進程向它寫入數據時,共享內存中的written被設置為0,程序等待。當它不為0時,表示沒有進程對共享內存寫入數據,程序就從共享內存中讀取數據並輸出,然後重置設置共享內存中的written為0,即讓其可被shmwrite進程寫入數據。
◇程序shmwrite取得共享內存並連接到自己的地址空間中。檢查共享內存中的written,是否為0,若不是,表示共享內存中的數據還沒有被完,則等待其他進程讀取完成,並提示用戶等待。若共享內存的written為0,表示沒有其他進程對共享內存進行讀取,則提示用戶輸入文本,並再次設置共享內存中的written為1,表示寫完成,其他進程可對共享內存進行讀操作。
這個程序是不安全的,當有多個程序同時向共享內存中讀寫數據時,問題就會出現。可能你會認為,可以改變一下written的使用方式,例如,只有當written為0時進程才可以向共享內存寫入數據,而當一個進程只有在written不為0時才能對其進行讀取,同時把written進行加1操作,讀取完後進行減1操作。這就有點像文件鎖中的讀寫鎖的功能。咋看之下,它似乎能行得通。但是這都不是原子操作,所以這種做法是行不能的。試想當written為0時,如果有兩個進程同時訪問共享內存,它們就會發現written為0,於是兩個進程都對其進行寫操作,顯然不行。當written為1時,有兩個進程同時對共享內存進行讀操作時也是如些,當這兩個進程都讀取完是,written就變成了-1.
要想讓程序安全地執行,就要有一種進程同步的進制,保證在進入臨界區的操作是原子操作。例如,可以使用前面所講的信號量來進行進程的同步。因為信號量的操作都是原子性的。
3 使用共享內存的優缺點
◇優點:我們可以看到使用共享內存進行進程間的通信真的是非常方便,而且函數的接口也簡單,數據的共享還使進程間的數據不用傳送,而是直接訪問內存,也加快了程序的效率。同時,它也不像匿名管道那樣要求通信的進程有一定的父子關系。
◇缺點:共享內存沒有提供同步的機制,這使得我們在使用共享內存進行進程間通信時,往往要借助其他的手段來進行進程間的同步工作。
1、server.c
/*server.c:向共享內存中寫入People*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#include<string.h>
#include"credis.h"
int semid;
int shmid;
/*信號量的P操作*/
void p()
{
struct sembuf sem_p;
sem_p.sem_num=0;/*設置哪個信號量*/
sem_p.sem_op=-1;/*定義操作*/
if(semop(semid,&sem_p,1)==-1)
printf("p operation is fail\n");
/*semop函數自動執行信號量集合上的操作數組。
int semop(int semid, struct sembuf semoparray[], size_t nops);
semoparray是一個指針,它指向一個信號量操作數組。nops規定該數組中操作的數量。*/
}
/*信號量的V操作*/
void v()
{
struct sembuf sem_v;
sem_v.sem_num=0;
sem_v.sem_op=1;
if(semop(semid,&sem_v,1)==-1)
printf("v operation is fail\n");
}
int main()
{
structPeople{
char name[10];
int age;
};
key_t semkey;
key_t shmkey;
semkey=ftok("../test/VenusDB.cbp",0);//用來產生唯一的標志符,便於區分信號量及共享內存
shmkey=ftok("../test/main.c",0);
/*創建信號量的XSI IPC*/
semid=semget(semkey,1,0666|IPC_CREAT);//參數nsems,此時為中間值1,指定信號燈集包含信號燈的數目
//0666|IPC_CREAT用來表示對信號燈的讀寫權限
/*
從左向右:
第一位:0表示這是一個8進制數
第二位:當前用戶的經權限:6=110(二進制),每一位分別對就 可讀,可寫,可執行,6說明當前用戶可讀可寫不可執行
第三位:group組用戶,6的意義同上
第四位:其它用戶,每一位的意義同上,0表示不可讀不可寫也不可執行
*/
if(semid==-1)
printf("creat sem is fail\n");
//創建共享內存
shmid=shmget(shmkey,1024,0666|IPC_CREAT);//對共享內存
if(shmid==-1)
printf("creat shm is fail\n");
/*設置信號量的初始值,就是資源個數*/
union semun{
int val;
struct semid_ds *buf;
unsignedshort*array;
}sem_u;
sem_u.val=1;/*設置變量值*/
semctl(semid,0,SETVAL,sem_u);//初始化信號量,設置第0個信號量,p()操作為非阻塞的
/*將共享內存映射到當前進程的地址中,之後直接對進程中的地址addr操作就是對共享內存操作*/
structPeople*addr;
addr=(structPeople*)shmat(shmid,0,0);//將共享內存映射到調用此函數的內存段
if(addr==(structPeople*)-1)
printf("shm shmat is fail\n");
/*向共享內存寫入數據*/
p();
strcpy((*addr).name,"xiaoming");
/*注意:①此處只能給指針指向的地址直接賦值,不能在定義一個 struct People people_1;addr=&people_1;因為addr在addr=(struct People*)shmat(shmid,0,0);時,已經由系統自動分配了一個地址,這個地址與共享內存相關聯,所以不能改變這個指針的指向,否則他將不指向共享內存,無法完成通信了。
注意:②給字符數組賦值的方法。剛才太虎了。。*/
(*addr).age=10;
v();
/*將共享內存與當前進程斷開*/
if(shmdt(addr)==-1)
printf("shmdt is fail\n");
}
/*client.c:從共享內存中讀出People*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semid;
int shmid;
/*信號量的P操作*/
void p()
{
struct sembuf sem_p;
sem_p.sem_num=0;
sem_p.sem_op=-1;
if(semop(semid,&sem_p,1)==-1)
printf("p operation is fail\n");
}
/*信號量的V操作*/
void v()
{
struct sembuf sem_v;
sem_v.sem_num=0;
sem_v.sem_op=1;
if(semop(semid,&sem_v,1)==-1)
printf("v operation is fail\n");
}
int main()
{
key_t semkey;
key_t shmkey;
semkey=ftok("../test/client/VenusDB.cbp",0);
shmkey=ftok("../test/client/main.c",0);
structPeople{
char name[10];
int age;
};
/*讀取共享內存和信號量的IPC*/
semid=semget(semkey,0,0666);
if(semid==-1)
printf("creat sem is fail\n");
shmid=shmget(shmkey,0,0666);
if(shmid==-1)
printf("creat shm is fail\n");
/*將共享內存映射到當前進程的地址中,之後直接對進程中的地址addr操作就是對共享內存操作*/
structPeople*addr;
addr=(structPeople*)shmat(shmid,0,0);
if(addr==(structPeople*)-1)
printf("shm shmat is fail\n");
/*從共享內存讀出數據*/
p();
printf("name:%s\n",addr->name);
printf("age:%d\n",addr->age);
v();
/*將共享內存與當前進程斷開*/
if(shmdt(addr)==-1)
printf("shmdt is fail\n");
/*IPC必須顯示刪除。否則會一直留存在系統中*/
if(semctl(semid,0,IPC_RMID,0)==-1)
printf("semctl delete error\n");
if(shmctl(shmid,IPC_RMID,NULL)==-1)
printf("shmctl delete error\n");
}
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define SHM_KEY 0x33
#define SEM_KEY 0x44
union semun {
int val;
struct semid_ds *buf;
unsignedshort*array;
};
int P(int semid)
{
struct sembuf sb;
sb.sem_num =0;
sb.sem_op =-1;
sb.sem_flg = SEM_UNDO;
if(semop(semid,&sb,1)==-1){
perror("semop");
return-1;
}
return0;
}
int V(int semid)
{
struct sembuf sb;
sb.sem_num =0;
sb.sem_op =1;
sb.sem_flg = SEM_UNDO;
if(semop(semid,&sb,1)==-1){
perror("semop");
return-1;
}
return0;
}
int main(int argc,char**argv)
{
pid_t pid;
int i, shmid, semid;
int*ptr;
union semun semopts;
/* 創建一塊共享內存, 存一個int變量 */
if((shmid = shmget(SHM_KEY,sizeof(int), IPC_CREAT |0600))==-1){
perror("msgget");
}
/* 將共享內存映射到進程, fork後子進程可以繼承映射 */
if((ptr =(int*)shmat(shmid, NULL,0))==(void*)-1){
perror("shmat");
}
*ptr =0;
/* 創建一個信號量用來同步共享內存的操作 */
if((semid = semget(SEM_KEY,1, IPC_CREAT |0600))==-1){
perror("semget");
}
/* 初始化信號量 */
semopts.val =1;
if(semctl(semid,0, SETVAL, semopts)<0){
perror("semctl");
}
if((pid = fork())<0){
perror("fork");
}elseif(pid ==0){/* Child */
/* 子進程對共享內存加1 */
for(i =0; i <100; i++){
P(semid);
(*ptr)++;
V(semid);
printf("child: %d\n",*ptr);
}
}else{/* Parent */
/* 父進程對共享內存減1 */
for(i =0; i <100; i++){
P(semid);
(*ptr)--;
V(semid);
printf("parent: %d\n",*ptr);
}
waitpid(pid);
sleep(2);
/* 如果同步成功, 共享內存的值為0 */
printf("finally: %d\n",*ptr);
}
return0;
}
# 管道( pipe ):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關系的進程間使用。進程的親緣關系通常是指父子進程關系。
# 有名管道 (named pipe) : 有名管道也是半雙工的通信方式,但是它允許無親緣關系進程間的通信。
# 信號量( semophore ) : 信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。
# 消息隊列( message queue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩沖區大小受限等缺點。
# 信號 ( sinal ) : 信號是一種比較復雜的通信方式,用於通知接收進程某個事件已經發生。
# 共享內存( shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信。
# 套接字( socket ) : 套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同及其間的進程通信。
管道(pipe) 管道是Linux支持的最初IPC方式,管道可分為無名管道,有名管道等。 (一)無名管道,它具有幾個特點: 1) 管道是半雙工的,只能支持數據的單向流動;兩進程間需要通信時需要建立起兩個管道; 2) 無名管道使用pipe()函數創建,只能用於父子進程或者兄弟進程之間; 3) 管道對於通信的兩端進程而言,實質上是一種獨立的文件,只存在於內存中; 4) 數據的讀寫操作:一個進程向管道中寫數據,所寫的數據添加在管道緩沖區的尾部;另一個進程在管道中緩沖區的頭部讀數據。 (二)有名管道 有名管道也是半雙工的,不過它允許沒有親緣關系的進程間進行通信。具體點說就是,有名管道提供了一個路徑名與之進行關聯,以FIFO(先進先出)的形式存在於文件系統中。這樣即使是不相干的進程也可以通過FIFO相互通信,只要他們能訪問已經提供的路徑。 值得注意的是,只有在管道有讀端時,往管道中寫數據才有意義。否則,向管道寫數據的進程會接收到內核發出來的SIGPIPE信號;應用程序可以自定義該信號處理函數,或者直接忽略該信號。 二。信號量(semophore) 信號量是一種計數器,可以控制進程間多個線程或者多個進程對資源的同步訪問,它常實現為一種鎖機制。實質上,信號量是一個被保護的變量,並且只能通過初始化和兩個標准的原子操作(P/V)來訪問。(P,V操作也常稱為wait(s),signal(s)) 三。信號(Signal) 信號是Unix系統中使用的最古老的進程間通信的方法之一。操作系統通過信號來通知某一進程發生了某一種預定好的事件;接收到信號的進程可以選擇不同的方式處理該信號,一是可以采用默認處理機制-進程中斷或退出,一是忽略該信號,還有就是自定義該信號的處理函數,執行相應的動作。 內核為進程生產信號,來響應不同的事件,這些事件就是信號源。信號源可以是:異常,其他進程,終端的中斷(Ctrl-C,Ctrl+\等),作業的控制(前台,後台進程的管理等),分配額問題(cpu超時或文件過大等),內核通知(例如I/O就緒等),報警(計時器)。 四。消息隊列(Message Queue) 消息隊列就是消息的一個鏈表,它允許一個或者多個進程向它寫消息,一個或多個進程向它讀消息。Linux維護了一個消息隊列向量表:msgque,來表示系統中所有的消息隊列。 消息隊列克服了信號傳遞信息少,管道只能支持無格式字節流和緩沖區受限的缺點。 五。共享內存(shared memory) 共享內存映射為一段可以被其他進程訪問的內存。該共享內存由一個進程所創建,然後其他進程可以掛載到該共享內存中。共享內存是最快的IPC機制,但由於linux本身不能實現對其同步控制,需要用戶程序進行並發訪問控制,因此它一般結合了其他通信機制實現了進程間的通信,例如信號量。 五。套接字(socket) socket也是一種進程間的通信機制,不過它與其他通信方式主要的區別是:它可以實現不同主機間的進程通信。