1. 程式人生 > >程序間通訊(8)

程序間通訊(8)

1.前言

本篇文章的所有例子,基於RHEL6.5平臺(linux kernal: 2.6.32-431.el6.i686)。

2.共享記憶體介紹

前面所講述的Linux下面的各種程序間通訊方式,例如:pipe(管道),FIFO(命名管道),message queue(訊息佇列),它們的共同點都是通過核心來進行通訊(假設posix訊息佇列也是在核心中實現的,因為posix標準沒有規定它的具體實現方式)。向pipe,fifo,message queue寫入資料時,需要把資料從使用者空間(使用者程序)複製到核心,而從這些IPC讀取資料時,又需要把資料從核心複製到使用者空間。因此,所有的這些IPC方式,都需要在核心與使用者程序之間進行2次資料複製,即程序間的通訊必須通過核心來傳遞,如下圖所示:


                 通過核心進行程序間通訊(IPC)

共享記憶體也是一種IPC,它是目前最快的IPC,它的使用方式是將同一個記憶體區對映到共享它的不同程序的地址空間中,這樣這些程序間的通訊就不再需要通過核心,只需對該共享的記憶體區域程序操作就可以了。和其他IPC不同的是,共享記憶體的使用需要使用者自己進行同步操作。下圖是共享記憶體區IPC的通訊:

3.對映函式mmap

每個程序都有自己的虛擬地址空間,我們知道除了堆中的虛擬記憶體我們可以由程式設計師靈活分配和釋放,其他區域的虛擬記憶體都由系統控制,那麼還有沒有其他方法讓程式設計師去靈活控制虛擬記憶體呢?linux下的mmap函式就由此而來。mmap函式可以為我們在程序的虛擬空間開闢一塊新的虛擬記憶體,可以將一個檔案對映到這塊新的虛擬記憶體,所以操作新的虛擬記憶體就是操作這個檔案。因此,
mmap函式主要的功能就是將檔案或裝置對映到呼叫程序的地址空間中,當使用mmap對映檔案到程序後,就可以直接操作這段虛擬地址進行檔案的讀寫等操作,不必再呼叫readwrite等系統呼叫。
UNIX網路程式設計第二捲進程間通訊對mmap函式進行了說明。該函式主要用途有三個:
a)將一個普通檔案對映到記憶體中,通常在需要對檔案進行頻繁讀寫時使用,這樣用記憶體讀寫取代I/O讀寫,以獲得較高的效能。可以提供無親緣程序間的通訊;
b)將特殊檔案進行匿名記憶體對映,可以為親緣程序提供共享記憶體空間;
c)為無親緣的程序提供共享記憶體空間,一般也是將一個普通檔案對映到記憶體中。

#include <sys/mman.h>  
void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);  
               //成功返回對映到程序地址空間的起始地址,失敗返回MAP_FAILED
引數start:指向欲對映的記憶體起始地址,通常設為 NULL,代表讓系統自動選定地址,對映成功後返回該地址。
引數len:對映到程序地址空間的位元組數,它從被對映檔案開頭的第offset個位元組處開始,offset通常被設定為0。

引數prot:對映區域的保護方式。可以為以下幾種方式的組合:

    ·  PROT_READ:資料可讀;
    ·  PROT_WRITE:資料可寫;
    ·  PROT_EXEC:資料可執行;
    ·  PROT_NONE:資料不可訪問;

flags:設定記憶體對映區的型別標誌,POSIX標誌定義了以下三個標誌:
    · MAP_SHARED:該標誌表示,呼叫程序對被對映記憶體區的資料所做的修改對於共享該記憶體區的所有程序都可見,而且確實改變其底層的支撐物件(一個檔案物件或是一個共享記憶體區物件)。
    ·  MAP_PRIVATE:呼叫程序對被對映記憶體區的資料所做的修改只對該程序可見,而不改變其底層支撐物件。
    ·  MAP_FIXED:該標誌表示準確的解釋start引數,一般不建議使用該標誌,對於可移植的程式碼,應該把start引數置為NULL,且不指定MAP_FIXED標誌。

    上面三個標誌是在POSIX.1-2001標準中定義的,其中MAP_SHARED和MAP_PRIVATE必須選擇一個。在Linux中也定義了一些非標準的標誌,例如MAP_ANONYMOUS(MAP_ANON),MAP_LOCKED等,具體參考Linux手冊。

fd:有效的檔案描述符。如果設定了MAP_ANONYMOUS(MAP_ANON)標誌,在Linux下面會忽略fd引數,而有的系統實現如BSD需要置fd為-1;

offset:相對檔案的起始偏移。

從程序的地址空間中刪除一個對映關係,需要用到下面的函式:

4.對映刪除munmap

    從程序的地址空間中刪除一個對映關係,需要用到下面的函式:
#include <sys/mman.h>  
int munmap(void *start, size_t len);   //成功返回0,出錯返回-1

start:被對映到的程序地址空間的記憶體區的起始地址,即mmap返回的地址。

len:對映區的大小。

5.對映同步

對於一個MAP_SHARED的記憶體對映區,核心的虛擬記憶體演算法會保持記憶體對映檔案和記憶體對映區的同步,也就是說,對於記憶體對映檔案所對應記憶體對映區的修改,核心會在稍後的某個時刻更新該記憶體對映檔案。如果我們希望硬碟上的檔案內容和記憶體對映區中的內容實時一致,那麼我們就可以呼叫msync開執行這種同步:

#include <sys/mman.h>  
int msync(void *start, size_t len, int flags);  //成功返回0,出錯返回-1

start:被對映到的程序地址空間的記憶體區的起始地址,即mmap返回的地址。

len:對映區的大小。

flags:同步標誌,有以下三個標誌:

    · MS_ASYNC:非同步寫,一旦寫操作由核心排入佇列,就立刻返回;

    · MS_SYNC:同步寫,要等到寫操作完成後才返回。

    . MS_INVALIDATE:使該檔案的其他記憶體對映的副本全部失效。

6.記憶體對映區的大小

Linux下的記憶體是採用頁式管理機制。通過mmap進行記憶體對映,核心生成的對映區的大小都是以頁面大小PAGESIZE為單位,即為PAGESIZE的整數倍。如果mmap對映的長度不是頁面大小的整數倍,那麼多餘空間也會被閒置浪費。

可以通過下面的方式來檢視Linux的頁面大小:
#include<iostream>
#include<unistd.h>

int main()
{
  int pSize=getpagesize();
  //或者int pSize=sysconf(_SC_PAGE_SIZE);
  std::cout<<"The page size is: "<<pSize<<std::endl;
  return 0;
}
輸出:
The page size is: 4096
從上述的執行結果,可以很明顯的看出,當前系統的頁大小為4k位元組。
下面對對映檔案的大小和對映長度的不同情況進行討論。
1) 對映檔案的大小等於對映長度

#include <iostream>  
#include <cstring>  
#include <cerrno>  

#include <unistd.h>  
#include <fcntl.h>  
#include <sys/mman.h>  

#define  PATH_NAME "/tmp/memmap"  

int main(int argc, char **argv)
{
        int fd;

        fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
        if (fd < 0)
        {
                std::cout << "open file " << PATH_NAME << " failed.";
                std::cout << strerror(errno) << std::endl;
                return -1;
        }

        if (ftruncate(fd, 5000) < 0) //修改檔案大小為5000
        {
                std::cout << "change file size  failed.";
                std::cout << strerror(errno) << std::endl;

                close(fd);
                return -1;
        }

        char *memPtr;

        //指定對映長度為5000,與對映檔案大小相等
        memPtr = (char *)mmap(NULL, 5000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        close(fd);

        if (memPtr == MAP_FAILED)
        {
                std::cout << "mmap failed." << strerror(errno) << std::endl;
                return -1;
        }

        std::cout << "[0]:" << (int)memPtr[0] << std::endl;
        std::cout << "[4999]:" << (int)memPtr[4999] << std::endl;
        std::cout << "[5000]:" << (int)memPtr[5000] << std::endl;
        std::cout << "[8191]:" << (int)memPtr[8191] << std::endl;
        std::cout << "[8192]:" << (int)memPtr[8192] << std::endl;
        std::cout << "[4096*3-1]:" << (int)memPtr[4096*3-1] << std::endl;
        std::cout << "[4096*3]:" << (int)memPtr[4096*3] << std::endl;

        return 0;
}
輸出:
[0]:0
[4999]:0
[5000]:0
[8191]:0
[8192]:60
[4096*3-1]:0
Segmentation fault (core dumped)

可以使用下圖來分析上面的執行結果:

偏移0                         4999       
    |======檔案=======|

下標0                         4999                               4096*2-1                4096*3-1
    |====記憶體對映區====|====第二頁剩餘部分====|====第三頁====|====
    |--------------這段區間內訪問不會出現問題------------|----------SIGSEGV


執行結果可以看到,能夠完整的訪問到前三頁,在訪問第四頁的時候會產生SIGSEGV訊號,發生Segmentation fault段越界訪問錯誤。按照《UNIX 網路程式設計 卷2:程序間通訊》中P257的講解,核心會為該記憶體對映兩個頁面,訪問前兩個頁面不會有問題,但訪問第三個頁面會產生SIGSEGV錯誤訊號。這個差異具體應該是與底層實現有關。

2) 對映檔案的大小小於對映長度

在上面程式碼的基礎上,修改mmap記憶體對映函式中的第二個引數如果,即對映長度修改為4096*3,大於對映檔案的大小(5000)。

memPtr = (char *)mmap(NULL, 4096 * 3, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

則執行結果為:
[root@MiWiFi-R1CM csdnblog]# ./a.out 
[0]:0
[4999]:0
[5000]:0
[8191]:0
Bus error (core dumped)
再次修改訪問程式碼如下,讓程式訪問4096*3以後的記憶體對映區域

        std::cout << "[0]:" << (int)memPtr[0] << std::endl;
        std::cout << "[4999]:" << (int)memPtr[4999] << std::endl;
        std::cout << "[5000]:" << (int)memPtr[5000] << std::endl;
        std::cout << "[8191]:" << (int)memPtr[8191] << std::endl;
        std::cout << "[4096*3]:" << (int)memPtr[4096*3] << std::endl;
        std::cout << "[4096*4-1]:" << (int)memPtr[4096*4-1] << std::endl;
        std::cout << "[4096*4]:" << (int)memPtr[4096*4] << std::endl;
輸出結果:
[0]:0
[4999]:0
[5000]:0
[8191]:0
[4096*3]:60
[4096*4-1]:0
Segmentation fault (core dumped)
使用下圖來分析上面的執行結果:

偏移0                             4999       
    |====檔案=========|

下標0                            4999                                     4096*2-1               4096*3-1               4096*4-1
    |====記憶體對映區====|====第二頁剩餘部分====|====第三頁====|====第四頁====|==========
    |-------------這段區間內訪問不會出現問題------------|-------SIGBUG------|---訪問不出問題--|-----SIGSEGV
    |-------------------------------------mmap大小-------------------------------------|

該執行結果和之前的對映檔案大小和對映長度相同的情況有比較大的區別,在訪問記憶體對映區內部但超出底層支撐物件的大小的區域部分會產生SIGBUS錯誤資訊,產生生BUS error錯誤,但訪問第四頁不會出問題,訪問第四頁以後的記憶體區就會產生 SIGSEGV錯誤資訊。按照《UNIX 網路程式設計 卷2:程序間通訊》中P258的講解,訪問第三個頁面以後的記憶體會產生SIGSEGV錯誤訊號。這個差異具體應該是底層實現有關。

7.使用mmap進行IPC

下面將介紹mmap本身提供的程序間通訊的兩種方式,分別用於無親緣和親緣程序間的通訊。

1).通過匿名記憶體對映提供親緣程序間的通訊

我們可以通過在父程序fork之前指定MAP_SHARED呼叫mmap,通過對映一個檔案來實現父子程序間的通訊,POSIX保證了父程序的記憶體對映關係保留到子程序中,父子程序對記憶體對映區的修改雙方都可以看到。
在Linux 2.4以後,mmap提供匿名記憶體對映機制,即將mmap的flags引數指定為:MAP_SHARED | MAP_ANON。這樣就徹底避免了記憶體對映檔案的建立和開啟,簡化了對檔案的操作。匿名記憶體對映機制的目的就是為了提供一個穿越父子程序間的記憶體對映區,很方便的提供了親緣程序間的通訊。
測試程式碼如下:

#include <iostream>  
#include <cstring>  
#include <cerrno>  
#include <stdlib.h>


#include <unistd.h>  
#include <fcntl.h>  
#include <sys/mman.h>  


int main(int argc, char **argv)
{
        char *memPtr = (char *)mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, 0, 0);
        if (memPtr == MAP_FAILED)
        {
                std::cout << "mmap failed." << strerror(errno) << std::endl;
                return -1;
        }

        *memPtr = 0;

        if (fork() == 0)
        {
                const char* tmp = "hello world";
                memcpy(memPtr, tmp, strlen(tmp));
                std::cout << "child set memory: " << memPtr << std::endl;


                exit(0);
        }

        sleep(1);
        std::cout << "parent get memory: " << memPtr << std::endl;

        return 0;
}
輸出:
[root@MiWiFi-R1CM csdnblog]# ./a.out      
child set memory: hello world
parent get memory: hello world
2).通過記憶體對映檔案提供無親緣程序間的通訊

通過在不同程序間對同一記憶體對映檔案進行對映,來進行無親緣程序間的通訊。
測試程式碼如下:

//程序1
#include <iostream>  
#include <cstring>  
#include <errno.h>  

#include <unistd.h>  
#include <fcntl.h>  
#include <sys/mman.h>  

#define  PATH_NAME "/tmp/memmap"  

int main()
{
	int *memPtr;
	int fd;

	fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
	if (fd < 0)
	{
		std::cout << "open file " << PATH_NAME << " failed.";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	ftruncate(fd, sizeof(int));

	memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	if (memPtr == MAP_FAILED)
	{
		std::cout << "mmap execute failed." << strerror(errno) << std::endl;
		return -1;
	}

	*memPtr = 12345;
	std::cout << "process " << getpid() << " send data: " << *memPtr << std::endl;

	return 0;
}

//程序2  
#include <iostream>  
#include <cstring>  
#include <errno.h>  

#include <unistd.h>  
#include <fcntl.h>  
#include <sys/mman.h>  

#define  PATH_NAME "/tmp/memmap"  

int main()
{
	int *memPtr;
	int fd;

	fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
	if (fd < 0)
	{
		std::cout << "open file " << PATH_NAME << " failed.";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	if (memPtr == MAP_FAILED)
	{
		std::cout << "mmap execute failed." << strerror(errno) << std::endl;
		return -1;
	}

	std::cout << "process " << getpid() << " receive data:" << *memPtr << std::endl;

	return 0;
}
輸出:
[root@MiWiFi-R1CM csdnblog]# ./test1 
process 8454 send data: 12345
[root@MiWiFi-R1CM csdnblog]# ./test2 
process 8463 receive data:12345

上面的程式碼都沒進行同步操作,在實際的使用過程要考慮到程序間的同步,通常會用訊號量來進行共享記憶體的同步。

8.基於mmap的POSIX共享記憶體實現

前面介紹了通過記憶體對映檔案進行程序間的通訊的方式,現在要介紹的是通過POSIX共享記憶體區物件進行程序間的通訊。POSIX共享記憶體使用方法有以下兩個步驟:
    a)通過shm_open建立或開啟一個POSIX共享記憶體物件;
    b)然後呼叫mmap將它對映到當前程序的地址空間;
和通過記憶體對映檔案進行通訊的使用上差別在於mmap描述符引數獲取方式不一樣:通過open或shm_open。如下圖所示:

<---------------------------------------------------------Posix記憶體去物件-------------------------------------------------------------------------------->

POSIX共享記憶體區物件的特殊操作函式就只有建立(開啟)和刪除兩個函式,其他對共享記憶體區物件的操作都是通過已有的函式進行的。

#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);
                              //成功返回非負的描述符,失敗返回-1
int shm_unlink(const char *name);
                              //成功返回0,失敗返回-1

shm_open用於建立一個新的共享記憶體區物件或開啟一個已經存在的共享記憶體區物件。

    --namePOSIX IPC的名字,前面關於POSIX程序間通訊都已講過關於POSIX IPC的規則,這裡不再贅述。

    --oflag:操作標誌,包含:O_RDONLYO_RDWRO_CREATO_EXCLO_TRUNC。其中O_RDONLYO_RDWR標誌必須且僅能存在一項。

    --mode:用於設定建立的共享記憶體區物件的許可權屬性。和open以及其他POSIX IPCxxx_open函式不同的是,該引數必須一直存在,如果oflag引數中沒有O_CREAT標誌,該位可以置0

shm_unlink用於刪除一個共享記憶體區物件,跟其他檔案的unlink以及其他POSIX IPC的刪除操作一樣,物件的析構會等到該物件的所有引用全部關閉才會發生。

POSIX共享記憶體和POSIX訊息佇列,有名訊號量一樣都是具有隨核心持續性的特點。

下面是通過POSIX共享記憶體進行通訊的測試程式碼,程式碼中通過POSIX訊號量來進行程序間的同步操作。

//程序1  
#include <iostream>  
#include <cstring>  
#include <errno.h>  

#include <unistd.h>  
#include <fcntl.h>  
#include <semaphore.h>  
#include <sys/mman.h>  

#define SHM_NAME "/memmap"  
#define SHM_NAME_SEM "/memmap_sem"   

char sharedMem[20];

int main()
{
	int fd;
	sem_t *sem;

	fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
	sem = sem_open(SHM_NAME_SEM, O_CREAT, 0666, 0);

	if (fd < 0 || sem == SEM_FAILED)
	{
		std::cout << "shm_open or sem_open failed...";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	ftruncate(fd, sizeof(sharedMem));

	char *memPtr;
	memPtr = (char *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	char msg[] = "hello world";

	memmove(memPtr, msg, sizeof(msg));
	std::cout << "process:" << getpid() << " send:" << memPtr << std::endl;

	sem_post(sem);
	sem_close(sem);

	return 0;
}

//程序2  
#include <iostream>  
#include <cstring>  
#include <errno.h>  

#include <unistd.h>  
#include <fcntl.h>  
#include <semaphore.h>  
#include <sys/mman.h>
#include <sys/stat.h>

#define SHM_NAME "/memmap"  
#define SHM_NAME_SEM "/memmap_sem"   

int main()
{
	int fd;
	sem_t *sem;

	fd = shm_open(SHM_NAME, O_RDWR, 0);
	sem = sem_open(SHM_NAME_SEM, 0);

	if (fd < 0 || sem == SEM_FAILED)
	{
		std::cout << "shm_open or sem_open failed...";
		std::cout << strerror(errno) << std::endl;
		return -1;
	}

	struct stat fileStat;
	fstat(fd, &fileStat);

	char *memPtr;
	memPtr = (char *)mmap(NULL, fileStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	close(fd);

	sem_wait(sem);

	std::cout << "process:" << getpid() << " recv:" << memPtr << std::endl;

	sem_close(sem);

	return 0;
}
執行結果:
[root@MiWiFi-R1CM csdnblog]# g++ test2.cpp -o test2 -lrt
[root@MiWiFi-R1CM csdnblog]# g++ test1.cpp -o test1 -lrt
[root@MiWiFi-R1CM csdnblog]# ./test1 
process:12621 send:hello world
[root@MiWiFi-R1CM csdnblog]# ./test2 
process:12622 recv:hello world

在Linux 2.6.32中,對於POSIX訊號量和共享記憶體的名字會在/dev/shm下建立對應的路徑名,例如上面的測試程式碼,會生成如下的路徑名:
[root@MiWiFi-R1CM csdnblog]# ll /dev/shm/
total 8
-rw-r--r--. 1 root root 20 Jun 21 20:39 memmap
-rw-r--r--. 1 root root 16 Jun 21 20:36 sem.memmap_sem