mmap那些事之android property實現之二

1 基於tmpfs的mmap系統調用過程

前面一篇blog:mmap那些事之android property實現,講述了android的屬性系統是基於tmpfs的mmap來實現內存的共享,只是論述了應用層的使用,並未涉及到內核空間是怎麼處理的。
包括如下幾個問題:
mmap系統調用過程
tmpfs文件針對mmap做了哪些處理?這裏包括tmpfs是怎樣分配實際的物理內存到共享內存的,然後其他應用進程映射到這個tmpfs的文件時,又是怎麼取得這個共享物理內存的,並且又是怎麼建立到自己所屬進程的地址空間映射頁表的)

2 mmap的系統調用過程

首先簡單說明下mmap的系統調用過程:

do_mmap是應用空間mmap調用在內核空間入口,該函數前面只是做了些參數的合法性檢查,在這裏addr一般爲0,如果不爲0,則說明應用空間希望內核使用該地址作爲虛擬地址的開始地址,但實際返回的地址是由當前進程的地址空間使用情況決定的,所以返回值並不一定是用戶希望的addr的值,應用空間應用使用mmap系統嗲用返回的addr值。len參數指定映射的虛擬內存的長度。我們直接轉到do_mmap_pgoff函數:   
                               ............

                               ............

上面的line998 addr參數就是linux mm系統自動爲我們分配的這段映射內存的開始虛擬地址
mmap_region函數主要做如下幾件事情:
首先將[addr,addr+len]的這段虛擬地址空間的之前的映射拆除掉。

其次將[addr,addr+len]這段地址範圍跟相鄰的wma進行合併

如果不能合併,則分配新的wma結構體來管理[addr,addr+len]這段地址範圍

最重要的地方出現了,如下圖高亮部分,執行該映射文件句柄對應的mmap操作函數,該函數是需要支持mmap系統調用的驅動來實現的。file_operations中的mmap操作函數的實現方法有兩種典型實現,ldd3的參考書籍上有詳細的描述和實例,在這裏,我們發現op->mmap的函數原型跟系統調用的mmap函數原型已經簡化多了,因爲linux的mm子系統已經爲我們做了大部分事情,譬如已經爲我們找到了一塊合適的虛擬內存空間(vma數據結構體來表示)來爲映射具體的物理內存空間做準備,並且在調用了驅動中的mmap函數後,將這個vma結構體連接到當前進程的mm結構體中。

最後,將vma數據結構連接到所屬進程的mm內存管理數據結構中, 

3 tmpfs對mmap調用的支持

現在回到我們的主菜,即android上層在調用open創建一個文件:/dev/__properties__,接着針對該文件執行mmap系統調用,這個時候內存做了些什麼事情?

他們爲什麼選擇在/dev目錄下,而不是其他目錄,譬如/data目錄下創建並映射這個文件可以嗎?
帶着這些問題,開始我們的內核之旅:

在linux控制檯執行mount命令:

如上圖的黑色高亮部分顯示的,/dev/目錄下的文件對應的都是tmpfs文件。所以我們的討論得從tmpfs開始。linux內核部分,tmpfs文件系統的實現時在trunk/mm/shmem.c文件中。在這裏限於篇幅,我就不展開說,一個linux內核是如何實現並註冊一個文件系統的。

3.1 tmpfs的open調用流程

android上層通過調用:fd = open("/dev/__properties__", O_RDWR | O_CREAT, 0600);語句來創建一個tmpfs的文件:/dev/__properties__,該文件由於是首次打開,所以打開的時候就會創建它,見內核的open調用中的如下過程:

line2259 判斷該文件不存在,則在line2281處調用vfs_create來創建目錄dir下的對應於dentry的文件。



由於對應於tmpfs文件系統,所以line2074對應的i_op結構體就是:

所以就調用static int shmem_create(struct inode *dir, struct dentry *dentry, umode_t mode,struct nameidata *nd)函數來創建對應於/dev/__properties__的文件,在執行這個函數時,參數dir對應的目錄名稱應該是dev,參數dentry目錄項對應的文件名字應該就是__properties__。以上函數最終調用如下函數:


繼續展開shmem_get_inode函數


至此,在應用層調用open函數,tmpfs主要是通過shmem_create函數來在/dev/目錄下,創建一個__properties__文件,主要是生成該文件對應的inode節點,並且初始化該inode節點,並將該節點跟dentry關聯起來,最終會將這個兩個重要成員填充到 struct file結構體成員中,並返回對應的文件句柄。

3.2 tmpfs的mmap調用過程

至此android應用在獲取到open返回的文件句柄後,調用如下函數來將共享內存映射到自己的進程地址空間:
data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
該應用層的mmap系統調用在經歷上述描述的mmap內核通用層的調用歷程後,會掉到驅動中的mmap的實現,在這裏的就是上面line1151種的shmem_file_operations結構體中的mmap成員函數:


針對struct file_operations操作中的mmap的實現,ldd3中有詳細的描述,概括的將分爲兩種實現方式:
一種是通過調用remap_pfn_range事先就將虛擬到物理地址的映射表建立好,此種方法在mmap調用完後,虛擬到物理映射的頁表已經建立好了。
一種是通過nopage的方式來實現。這方式,其實在調用完mmap後,虛地到物理地址的映射還沒建立,而是在應用具體到訪問到這個虛地址時,會產生page fault錯誤,在缺頁處理中,會調用nopage獲得虛擬地址對應的物理地址,並將對應的虛擬到物理的映射表建立好。
而tmpfs的mmap的實現是使用的第二種方法:


line1058 最終會調用shmem_getpage_gfp函數,展開如下:

繼續上面的函數,省略部分不相關的代碼


繼續上面的函數,省略部分不相關的代碼



結合上面代碼中的註釋,應該不難理解這個共享物理內存頁的分配及管理。

4 應用層如何使用mmap建立共享內存

這裏假設有兩個進程:進程A和進程B,他們要共享一塊物理內存:region_c(通過/dev/目錄的tmpfs文件: ashmem_test),再具體點,譬如進程A就是camera進程,進程B就是H264的編碼線程所在的進程。則一個camera的錄像過程就是:進程A獲取未壓縮的yuv原始視頻數據,並將該視頻數據數據放置在共享內存region_c區域,然後通過內存共享,進程B可以直接從共享內存region_c區域取出未壓縮的原始視頻數據作爲該編碼進程的輸入,進行編碼。這樣就避免了大量的數據在跨進程間的拷貝,從而結餘了大量的cpu時間和物理內存空間。

下面是一個如何使用共享內存的一個簡單列子,需要特別注意的有以下幾點:

  • 關閉文件句柄,並不會清除映射
  • 只有在調用munmap或是進程被終止時,纔會拆除進程對應的mmap映射
  • mmap系統調用後,並不會馬上建立映射,而是會在隨後對映射後的地址空間進行訪問(讀或寫時)纔會真正去分配物理內存,並建立相應的映射
  • 對零長度的文件,可以進行mmap,但訪問不成功。所以必須設置一個非零的文件長度,一般就設置爲共享內存的大小。
  • 映射和拆除映射都是以物理頁爲單位的

#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE_MAP 1024*4
#define FILE_NAME "/dev/__properties5__"

int printf_buf(unsigned char * buf, int len)
{
	int i;
	for(i =0; i<len; i++)
	{
		printf("buf[%d]:0x%x ",i,buf[i]);
		if(!(i%4))
			printf("\n");
	}
}

int main(int argc, char * argv[])
{
	int ret = 0;
	int fd = 0;
	unsigned int i =0 ;
	unsigned char *buf = NULL;
	if(!strcmp(argv[1],"a"))   //進程A
	{
		fd = open(FILE_NAME,O_RDWR | O_CREAT,0777);
		if(fd > 0)
			printf("open success fd:%d\n",fd);
		else
			printf("open failed\n");
		lseek(fd,10,SEEK_SET);  //注意必須要設置下文件的大小,如果文件大小是0,則不能映射 
		write(fd,"",1); 
		buf = (unsigned char *) mmap(NULL,SIZE_MAP,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0);
		if (buf == MAP_FAILED) {
			printf("mmap failed for fd:%d\n",fd);
            close(fd);
            return -1;
        }
		printf("mmap success buf:%x \n",buf);
		printf_buf(buf, 50);	//打印映射後初始化前的buf的內容
		memset(buf,0x5a,SIZE_MAP);//設置映射後的buf的內容
		close(fd);				//關閉文件句柄,並不清楚映射,映射只有在munmap或進程被終止時纔會被拆除
		for(i=0; i<20; i++)  //設置映射後的buf的內容
		{
			buf[i] = 'a'+i;
		}
		munmap(buf,SIZE_MAP);  //拆除進程A的映射
		printf("set share memory buf to 'a' for process A \n");
		// do not close fd
		
	}else if(!strcmp(argv[1],"b"))  //進程B
	{
		fd = open(FILE_NAME,O_RDWR);
		if(fd > 0)
			printf("open success\n");
		else
			printf("open failed\n");
		buf = (unsigned char *) mmap(NULL,SIZE_MAP,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0); //將內存映射到進程B
		if (buf == MAP_FAILED) {
			printf("mmap failed for fd:%d\n",fd);
            close(fd);
            return -1;
        }
		close(fd); //關閉文件句柄,並不清除映射,映射只有在munmap或進程被終止時纔會被拆除
		printf("mmap for process B success,buf:%x\n",buf);
		printf("printf share memory for process B\n");
		printf_buf(buf, 50);//打印共享內存的內容,在進程B中
		munmap(buf,SIZE_MAP);//拆除進程B的映射
		printf("completed\n");	
	}else
	{
		printf("please inpu the correct parameter:\n");
		printf("shmem a \n");
		printf("or \n");
		printf("shmem b \n");
	}
	return 0;
}

以上測試程序在X86 linux pc機上編譯、測試結果如下:

rubbitxiao@szmce15:~/test/share_memory$ rm -rf shmem
rubbitxiao@szmce15:~/test/share_memory$ 
rubbitxiao@szmce15:~/test/share_memory$ 
rubbitxiao@szmce15:~/test/share_memory$ gcc -o shmem shmem.c
rubbitxiao@szmce15:~/test/share_memory$ 
rubbitxiao@szmce15:~/test/share_memory$ 
rubbitxiao@szmce15:~/test/share_memory$ ls -l
total 24
-rwxrwxr-x 1 rubbitxiao rubbitxiao 13070 Dec  8 20:51 shmem
-rwxr--r-- 1 rubbitxiao rubbitxiao  2447 Dec  8 20:50 shmem.c
-rw-rw-r-- 1 rubbitxiao rubbitxiao  3000 Dec  8 18:13 shmem.o
rubbitxiao@szmce15:~/test/share_memory$ sudo ./shmem a
open success fd:3
mmap success buf錛歝b52d000 
buf[0]:0x0 
buf[1]:0x0 buf[2]:0x0 buf[3]:0x0 buf[4]:0x0 
buf[5]:0x0 buf[6]:0x0 buf[7]:0x0 buf[8]:0x0 
buf[9]:0x0 buf[10]:0x0 buf[11]:0x0 buf[12]:0x0 
buf[13]:0x0 buf[14]:0x0 buf[15]:0x0 buf[16]:0x0 
buf[17]:0x0 buf[18]:0x0 buf[19]:0x0 buf[20]:0x0 
buf[21]:0x0 buf[22]:0x0 buf[23]:0x0 buf[24]:0x0 
buf[25]:0x0 buf[26]:0x0 buf[27]:0x0 buf[28]:0x0 
buf[29]:0x0 buf[30]:0x0 buf[31]:0x0 buf[32]:0x0 
buf[33]:0x0 buf[34]:0x0 buf[35]:0x0 buf[36]:0x0 
buf[37]:0x0 buf[38]:0x0 buf[39]:0x0 buf[40]:0x0 
buf[41]:0x0 buf[42]:0x0 buf[43]:0x0 buf[44]:0x0 
buf[45]:0x0 buf[46]:0x0 buf[47]:0x0 buf[48]:0x0 
buf[49]:0x0 set share memory buf to 'a' for process A 
rubbitxiao@szmce15:~/test/share_memory$ 
rubbitxiao@szmce15:~/test/share_memory$ 
rubbitxiao@szmce15:~/test/share_memory$ sudo ./shmem b
open success
mmap for process B success,buf:25647000
printf share memory for process B
buf[0]:0x61 
buf[1]:0x62 buf[2]:0x63 buf[3]:0x64 buf[4]:0x65 
buf[5]:0x66 buf[6]:0x67 buf[7]:0x68 buf[8]:0x69 
buf[9]:0x6a buf[10]:0x6b buf[11]:0x6c buf[12]:0x6d 
buf[13]:0x6e buf[14]:0x6f buf[15]:0x70 buf[16]:0x71 
buf[17]:0x72 buf[18]:0x73 buf[19]:0x74 buf[20]:0x5a 
buf[21]:0x5a buf[22]:0x5a buf[23]:0x5a buf[24]:0x5a 
buf[25]:0x5a buf[26]:0x5a buf[27]:0x5a buf[28]:0x5a 
buf[29]:0x5a buf[30]:0x5a buf[31]:0x5a buf[32]:0x5a 
buf[33]:0x5a buf[34]:0x5a buf[35]:0x5a buf[36]:0x5a 
buf[37]:0x5a buf[38]:0x5a buf[39]:0x5a buf[40]:0x5a 
buf[41]:0x5a buf[42]:0x5a buf[43]:0x5a buf[44]:0x5a 
buf[45]:0x5a buf[46]:0x5a buf[47]:0x5a buf[48]:0x5a 
buf[49]:0x5a completed
rubbitxiao@szmce15:~/test/share_memory$ ^C
rubbitxiao@szmce15:~/test/share_memory$ 

5 總結

最後回到我們開始提出的幾個問題:
使用mmap實現內存共享的話,如果不想自己專門實現驅動層的mmap函數,則應該使用tmpfs提供的共享內存機制,所以必須要創建在基於tmpfs的文件系統中,至於文件叫什麼名字都不重要
像之前舉例的data分區,由於不是tmpfs文件系統,而是yaffs2文件系統,所以是不能用來實現內存共享的。
android屬性系統對應的共享內存所對應的物理內存頁都是由init進程分配的,並且掛在/dev/__properties__文件對應的file->f_path.dentry.d_inode->i_mapping中的平衡二叉樹中。
所有其他以只讀方式mmap這個/dev/__properties__文件的,則會去將init進程分配的物理內存頁映射到自己的進程的地址空間,從而實現物理內存在多個進程間的共享。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章