前言
進程間通信的方式包括管道、消息隊列、共享內存等,共享內存是一種幾乎沒有上限的通信方式,但同時,使用共享內存時需要自己進行加鎖等訪問控制。
共享內存是開闢一塊公共的邏輯內存,通常也是一塊公共的物理內存,兩個不相關的進程可以共同訪問這塊內存中的地址,就如同malloc分配的內存一樣。如果某個進程向內存中寫入數據,這些變化將直接反應到其他進程中。
共享內存沒有提供同步機制,在一個進程寫入動作結束以前,並不存在一種自動的機制阻止另外一個進程對內存的讀寫,所以需要使用者自己進行讀寫控制,包括信號量、各種鎖等。
API
Linux提供了一組標準的接口用於對共享內存的訪問。
#include <sys/shm.h>
/*
* 創建共享內存
* @ key 訪問共享內存所需的
* @ size 指定共享內存的大小(字節)
* @ shmflg 權限標誌,與open函數的mode參數一致,
* 如果要想在key標識的共享內存不存在時,創建它的話,可以與IPC_CREAT做或操作
* 共享內存的權限標誌與文件的讀寫權限一樣,
* 舉例來說,0644,它表示允許一個進程創建的共享內存被內存創建者所擁有的進程向共享內存讀取和寫入數據,
* 同時其他用戶創建的進程只能讀取共享內存
* return: 成功時返回與共享內存相關的標識符
* 失敗時返回-1
* */
int shmget(key_t key, size_t size, int shmflg);
/*
* 啓動對該共享內存的訪問,並把共享內存連接到當前進程的地址空間
* (在第一次創建完共享內存時,不能被任何進程訪問,需要藉助該函數啓動)
* @ shm_id shmget返回的共享內存標識
* @ shm_addr 希望將內存綁定到當前進程的什麼位置,一般爲NULL,即由系統決定
* @ shmflg 一組標識,通常爲0,(?)
* return: 成功時返回共享內存第一個字節的位置;
* 失敗時返回-1;
* */
void *shmat(int shm_id, const void *shm_addr, int shmflg);
/*
* 解除對共享內存的綁定,將共享內存分離,分離以後,將無法繼續訪問;分離並非刪除,其他進程還可以繼續訪問
* @ shm_addr shmat返回的內存地址
* return: 成功時返回0;
* 失敗時返回-1;
* */
int shmdt(const void *shmaddr);
/*
* 共享內存的訪問控制結構
* */
struct shmid_ds
{
uid_t shm_perm.uid; // user id
uid_t shm_perm.gid; // group id
mode_t shm_perm.mode; // 訪問模式
};
/*
* 控制共享內存,
* @ shm_id shmget返回的共享內存標識
* @ command 需要採取的動作
* IPC_STAT:共享內存的當前關聯值覆蓋shmid_ds的值。
* IPC_SET:如果進程有足夠的權限,就把共享內存的當前關聯值設置爲shmid_ds結構中給出的值
IPC_RMID:刪除共享內存
* return: 成功時返回0;
* 失敗時返回-1;
* */
int shmctl(int shm_id, int command, struct shmid_ds *buf);
舉例
下面的例子是一個訪問共享內存的例子,shmwrite在written爲0的情況下向其中寫入數據,shmread在written非0的情況下,從共享內存中讀取數據。(這裏只是演示共享內存的使用,下面的程序是不安全的,多個進程同時讀寫可能會有問題)
shmwrite.c
寫入程序
首先通過shmget打開內存空間,此時需要帶着IPC_CREAT,以便在沒有這塊內存的情況下創建;而後通過shmat掛載內存;循環判斷written,可寫的情況下從標準輸入讀取數據並寫入共享內存;在讀到end
的情況下,寫入共享內存,同時結束循環,分離共享空間。(此處不能刪除內存,shmread還需要讀出來,這裏也可以看出,多個進程讀的情況下,刪除內存的時機也需要考慮。)
/*
* !!! 這個程序是不安全的,僅用做訪問的例子
* 對written賦值的動作不是原子的,多進程訪問時可能出錯
* */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
int main()
{
int running = 1;
void *shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[TEXT_SZ + 1]; //用於保存輸入的文本
int shmid;
//創建共享內存
shmid = shmget((key_t)SHM_KEY, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
LOGEI("shmget failed");
exit(EXIT_FAILURE);
}
//將共享內存連接到當前進程的地址空間
shm = shmat(shmid, (void*)0, 0);
if(shm == (void*)-1)
{
LOGEI("shmat failed");
exit(EXIT_FAILURE);
}
LOGI("Memory attached at %X", (int)shm);
//設置共享內存
shared = (struct shared_use_st*)shm;
while(running)//向共享內存中寫數據
{
//數據還沒有被讀取,則等待數據被讀取,不能向共享內存中寫入文本
while(shared->written == 1)
{
sleep(1);
LOGI("Waiting...");
}
//向共享內存中寫入數據
LOGI("Please Enter some text: ");
fgets(buffer, TEXT_SZ, stdin);
strncpy(shared->text, buffer, TEXT_SZ);
//寫完數據,設置written使共享內存段可讀
shared->written = 1;
//輸入了end,退出循環
if(strncmp(buffer, "end", 3) == 0)
running = 0;
}
//把共享內存從當前進程中分離
if(shmdt(shm) == -1)
{
LOGEI("shmdt failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
shmread.c
讀取共享內存中的數據。
首先通過shmget打開內存空間,此時需要帶着IPC_CREAT,以便在沒有這塊內存的情況下創建;而後通過shmat掛載內存;循環判斷written,可讀的情況下讀取數據並輸出;在讀到end
的情況下結束循環,分離共享空間,並刪除之。
/*
* !!! 這個程序是不安全的,僅用做訪問的例子
* 對written賦值的動作不是原子的,多進程訪問時可能出錯
* */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include "common.h"
#include "shmdata.h"
int main()
{
int running = 1;
void *shm = NULL; //分配的共享內存的原始首地址
struct shared_use_st *shared; //指向需要讀寫的數據
int shmid; //共享內存標識符
//創建共享內存
shmid = shmget((key_t)SHM_KEY, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
LOGEI("shmget failed");
exit(EXIT_FAILURE);
}
//將共享內存連接到當前進程的地址空間
shm = shmat(shmid, 0, 0);
if(shm == (void*)-1)
{
LOGEI("shmat failed");
exit(EXIT_FAILURE);
}
LOGI("Memory attached at %X", (int)shm);
//設置共享內存
shared = (struct shared_use_st*)shm;
shared->written = 0;
//讀取共享內存中的數據
while(running)
{
//沒有進程向共享內存寫數據, 有數據可讀取
if(shared->written != 0)
{
LOGI("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)
{
LOGEI("shmdt failed");
exit(EXIT_FAILURE);
}
//刪除共享內存
if(shmctl(shmid, IPC_RMID, 0) == -1)
{
LOGEI("shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
shmdata.h
定義了buffer的大小,共享內存的標識。
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#define TEXT_SZ 2048
#define SHM_KEY 9876 //用來標識共享內存名稱的整型變量
struct shared_use_st
{
int written; //作爲一個標誌,非0:表示可讀,0表示可寫
char text[TEXT_SZ]; //記錄寫入和讀取的文本
};
#endif
common.h
用於更好地顯示調試信息
#ifndef __UT_COMMON_H__
#define __UT_COMMON_H__
#define LOGE(fmt, args...) \
do { \
printf("[%s(%d)]error:"fmt"\n", __FILE__, __LINE__, ##args ); \
} while(0)
#define LOGI(fmt, args...) \
do { \
printf("[%s(%d)]info:"fmt"\n", __FILE__, __LINE__, ##args ); \
} while(0)
#define LOGD(fmt, args...) \
do { \
printf("[%s(%d)]debug:"fmt"\n", __FILE__, __LINE__, ##args ); \
} while(0)
#include <errno.h>
#define LOGEI(fmt, args...) \
do { \
LOGE("(%d/%s)", errno, strerror(strno)); \
} while(0)
#endif /*__UT_COMMON_H__*/
編譯運行:
$gcc shmread.o -o shmread
$gcc shmwrite.o -o shmwrite
分別在兩個終端中運行讀寫程序,並通過標準輸入寫入內容:
終端1:
$ ./shmread
Memory attached at A7C91000
You wrote: abc
You wrote: end
$
終端2:
$ ./shmwrite
Memory attached at 10F58000
Enter some text:abc
Waiting...
Waiting...
Enter some text:end
$
說明
該程序的不安全性體現在對written的賦值上,設想一下,如果有多個進程同時讀寫數據時,其中一個進程發現written=0,並向其中寫入數據,並將written賦值爲1,而另一個進程只有在written=1的情況下才讀,同時賦值written爲0。乍看似乎沒有問題,但是對written的賦值不是原子的,試想當written爲0時,兩個進程同時訪問,發現written爲0,都進行了寫入操作,顯然不行。
關於共享內存多進程的訪問同步,後續的繼續研究。