上周導師布置了一個小程式,讓我們熟悉unix下共用記憶體的編程方法,題目簡要描述如下:
設計一個C/S結構的程式,在開闢的一片共用記憶體中讀寫程式,server端負責寫資料(每次寫一個長度為100的數組),client端負責讀資料,並把100個數組的元素以tab符號分割的輸出到一個檔案中。在不使用任何鎖和訊號量的情況下完成資料讀寫的同步(既server在對數組中元素賦值的過程中,client端絕對不能讀)。
解決思路:
首先,在不加鎖的情況下,為同步資料的讀寫,共用記憶體多申請一個int變數,作為資料讀寫權限的標誌位(0-空閑,此時生產者可寫,1-繁忙,兩者均不能訪問,2-新資料,此時消費者可讀);Server/Client在讀取標誌位判斷自己得到許可權後首先應該修改標誌位以告訴對方自己要對資料進行操作,然對方等待。
兩個程式配合正常,Server/Client進程中看到的分享記憶體的首地址是不同的,因為作業系統會為每個單獨啟動並執行程式分配自己的進程空間,進程空間以一段地址來表示,每個進程只能運行和操作分配給他的那段地址空間,而不能跨進程地址訪問(出於安全考慮);而共用儲存的目的也是為了讓這樣的進程能相互共用資料,核心會為共用儲存空間單獨開闢一段空間(獨立於任何進程),而掛接到該共用儲存空間的進程則將共用空間的實際地址映射到自己的地址空間中,這樣進程對共用空間的訪問就和對自己空間訪問一樣,也因為這樣所以每個進程看到的共用記憶體的地址是不同的(只看到映射後的地址)。以上為個人理解,具體還需參查資料來確認。
實際效果:
可以看到,在不採用任何緩衝的情況下,Server端每寫一個數組,都需要等待Client端去讀取後才能繼續寫,這樣兩個程式都無法高效運行,空轉、輪詢和等待耗費大部分時間。
改進,採用一個長度為1000的迴圈隊列來快取資料,並引入兩個遊標。不加鎖實現兩個程式對一個隊列的非同步讀取操作,通過定義兩個數組遊標來實現(readIndex,writeIndex),一個表示消費者可讀取資料的起始位置,一個表示為生產者可寫資料的起始位置;
對於生產者,能操作的隊列範圍為[writeIndex, readIndex-1)
對於消費者,能操作的隊列範圍為[readIndex, writeIndex)
為方便判斷隊列狀態,產生者最大可寫資料包數為(隊列長度-1),那麼當readIndex == writeIndex 代表隊列空,(writeIndex + 1) % 隊列長度 == readIndex代表隊列滿,其他情況代表消費者可以對隊列進行寫操作;
而消費者只要在writeIndex != ReadIndex的情況下都可以對隊列進行讀操作。
這裡,消費者和生產者需要在資料操作完之後才修改相應遊標。
方法2和方法3時間對比:
明顯後者效率要遠遠高於前者,採用隊列的方法在訪問一百萬條資料只需18秒cpu時間(使用者時間也仍然需要18s,應該是對檔案寫操作耗時),但方法2在最佳化過後訪問一千條資料都需要4秒cpu時間,十萬條則需要6分40秒。
改進後效果:
讀取一百萬條資料也只要18秒的時間,速度提升非常明顯。
另外關於效能最佳化:程式在運行時空轉會耗費大量時間,以消費者為例,輪到它執行的時候會不斷輪詢對應標誌位看自己是否可讀,可如果生產者還沒有生產資料的時候這種輪詢其實是無謂的,還暫用時間片,這個時候消費者可以主動讓出時間休眠一會,等待對方生產資料再來讀取;對於生產者也是一樣的道理。在加入休眠後發現程式的效能都提高了很多。還有包括使用字串拼接這種自訂緩衝都會降低程式效率。
附代碼:
***********************myshare.h************************
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <memory.h>
#include <signal.h>
#include <string.h>
#define SHM_MODE (SHM_R | SHM_W | IPC_CREAT)
#define PATHNAME "/etc/ssh/sshd_config"
#define CLASSID 1
#define ITEMLEN 100
#define QEUELEN 1000
#define QUIT 1
typedef struct _stItem
{
unsigned int items[ITEMLEN];
}stItem;
typedef struct _stctrl
{
int quit;
int windex;
int rindex;
}stCtrl;
typedef struct _stCItem
{
stItem item[QEUELEN];
int quit;
int windex;
int rindex;
}stCItem;
*************producer.c****************
#include "myshare.h"
/* 自訂接受鍵盤訊號處理函數(ctrl+c,ctrl+\) */
static void sig_quit(int signo);
/* 標誌位,如果有鍵盤中斷置1,否則為0 */
int quit;
int main(int argc, char *argv[])
{
unsigned long int i;
int idx;
/* 共用儲存的標誌ID */
int shmid;
/* 共用儲存指標 */
stCItem *shmptr;
/* 用來產生標誌ID的KEY */
key_t shmkey;
/* 使用ftok函數拉生產KEY */
if ( (shmkey = ftok(PATHNAME, CLASSID)) == -1)
return my_error_sys("ftok error");
/* 根據KEY來申請共用儲存,確保server/client掛載相同空間 */
if ( (shmid = shmget(shmkey, sizeof(stCItem), SHM_MODE)) < 0)
return my_error_sys("shmget error");
/* 串連共用儲存 */
if ( (shmptr = shmat(shmid, 0, 0)) == (void *)-1)
return my_error_sys("shmat error");
/* 註冊處理函數,監聽鍵盤訊號 */
if (signal(SIGINT, sig_quit) == SIG_ERR)
return my_error_sys("signal error");
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
return my_error_sys("signal error");
/* server每次啟動先初始化共用儲存 */
memset((void *)shmptr, 0, sizeof(stCItem));
i = 0;
quit = 0;
printf("produce data start...\n");
while (!quit && !shmptr->quit)
{
if ((shmptr->windex + 1) % QEUELEN != shmptr->rindex)
{
/* 如果隊列有空槽則生產資料 */
for (idx = 0; idx < ITEMLEN; ++idx)
((shmptr->item)[shmptr->windex].items)[idx] = i;
++i;
/* 寫完資料後修改遊標 */
shmptr->windex = (shmptr->windex + 1) % QEUELEN;
}
else
/* 避免空轉,主動放棄時間片 */
usleep(1);
}
/* 通知對方退出 */
shmptr->quit = 1;
/* 清除對應共用儲存ID */
if (shmctl(shmid, IPC_RMID, 0) < 0)
return my_error_sys("shmctrl data error");
printf("produce datas i : %d\n", i);
printf("produce data stop...\n");
return 0;
}
int my_error_sys(const char *pstr)
{
printf(pstr);
printf("\n");
return -1;
}
static void sig_quit(int signo)
{
quit = 1;
}
****************consumer.c********************
#include "myshare.h"
static void sig_quit(int signo);
int quit;
int main(int argc, char *argv[])
{
unsigned long int idx;
unsigned long int i;
unsigned long int datanum;
/* 共用儲存的標誌ID */
int shmid;
/* 共用儲存指標 */
stCItem *shmptr;
/* 用來產生標誌ID的KEY */
key_t shmkey;
FILE *fp;
char *resultfile = "data.txt";
/* 使用參數來控制讀取的資料量,方便測試 */
if( argc != 2)
return my_error_sys("usage: consumer <data number>");
sscanf(argv[1], "%d", &datanum);
if ( (fp = fopen(resultfile, "w")) == NULL )
return my_error_sys("open file error");
/* 使用ftok函數拉生產KEY */
if ( (shmkey = ftok(PATHNAME, CLASSID)) == -1)
return my_error_sys("ftok error");
/* 根據KEY來申請共用儲存,確保server/client掛載相同空間 */
if ( (shmid = shmget(shmkey, sizeof(stCItem), SHM_MODE)) < 0)
return my_error_sys("shmget error");
/* 串連共用儲存 */
if ( (shmptr = shmat(shmid, 0, 0)) == (void *)-1)
return my_error_sys("shmat error");
/* 註冊處理函數,監聽鍵盤訊號 */
if (signal(SIGINT, sig_quit) == SIG_ERR)
return my_error_sys("signal error");
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
return my_error_sys("signal error");
quit = 0;
i = 0;
memset((void *)shmptr, 0, sizeof(stCItem));
printf("start consume data....\n");
while (!quit)
{
/* 如果隊列有資料則消費資料 */
if( shmptr->rindex != shmptr->windex )
{
for (idx = 0; idx < ITEMLEN; ++idx)
{
fprintf(fp, "%u\t", (shmptr->item)[shmptr->rindex].items[idx]);
}
/* 用分行符號重新整理緩衝區 */
fprintf(fp,"\n");
/* 讀取完資料後修改遊標 */
shmptr->rindex = (shmptr->rindex + 1) % QEUELEN;
++i;
if (i >= datanum)
break;
}
else
/* 避免空轉,主動放棄時間片 */
usleep(1);
}
/* 通知對方退出 */
shmptr->quit = 1;
fclose(fp);
/* 清除對應共用儲存ID */
if (shmctl(shmid, IPC_RMID, 0) < 0)
return my_error_sys("shmctl data error");
printf("consumer datas i : %d\n", i);
printf("quit consume...\n");
return 0;
}
int my_error_sys(const char *pstr)
{
printf(pstr);
printf("\n");
return -1;
}
static void sig_quit(int signo)
{
quit = 1;
}