作者 彭東林 QQ 405728433 平臺 Linux-4.10.17 Qemu-2.8 + vexpress-a9 DDR:1GB 參考 Linux 虛擬記憶體和物理記憶體的理解 Linux進程分配記憶體的兩種方式--brk() 和mmap() Linux中的mmap的使用 程式(進程)記憶體分佈 解析 ...
作者
彭東林 QQ 405728433平臺
Linux-4.10.17 Qemu-2.8 + vexpress-a9 DDR:1GB參考
Linux 虛擬記憶體和物理記憶體的理解 Linux進程分配記憶體的兩種方式--brk() 和mmap() Linux中的mmap的使用 程式(進程)記憶體分佈 解析概述
Linux內核提供了remap_pfn_range函數來實現將內核空間的記憶體映射到用戶空間:1 /** 2 * remap_pfn_range - remap kernel memory to userspace 3 * @vma: user vma to map to 4 * @addr: target user address to start at 5 * @pfn: physical address of kernel memory 6 * @size: size of map area 7 * @prot: page protection flags for this mapping 8 * 9 * Note: this is only safe if the mm semaphore is held when called. 10 */ 11 int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, 12 unsigned long pfn, unsigned long size, pgprot_t prot);上面的註釋對參數進行了說明。當用戶調用mmap時,驅動中的file_operations->mmap會被調用,可以在mmap中調用remap_pfn_range,它的大部分參數的值都由VMA提供。具體可以參考LDD3的P420.
正文
下麵結合一個簡單的例子學習一下。在驅動中申請一個32個Page的緩衝區,這裡的PAGE_SIZE是4KB,所以內核中的緩衝區大小是128KB。user_1和user_2將前64KB映射到自己的用戶空間,其中user_1向緩衝區中寫入字元串,user_2去讀取。user_3和user_4將後64KB映射到自己的用戶空間,其中user_3向緩衝區中寫入字元串,user_4讀取字元串。user_5將整個128KB映射到自己的用戶空間,然後將緩衝區清零。此外,在驅動中申請緩衝區的方式有多種,可以用kmalloc、也可以用alloc_pages,當然也可用vmalloc,下麵會分別針對這三個介面實現驅動。 涉及到的測試程式和驅動程式可以到下麵的鏈接下載: https://github.com/pengdonglin137/remap_pfn_demo一、驅動程式
下麵現以kzalloc申請緩衝區的方式為例介紹,調用kmalloc申請32個頁,我們知道kzalloc返回的虛擬地址的特點是對應的物理地址也是連續的,所以在調用remap_pfn_range的時候很方便。首先在驅動init的時候申請128KB的緩衝區:1 static int __init remap_pfn_init(void) 2 { 3 int ret = 0; 4 5 kbuff = kzalloc(BUF_SIZE, GFP_KERNEL); // 這裡的BUF_SIZE是128KB 6 if (!kbuff) { 7 ret = -ENOMEM; 8 goto err; 9 } 10 11 ret = misc_register(&remap_pfn_misc); // 註冊一個misc設備 12 if (unlikely(ret)) { 13 pr_err("failed to register misc device!\n"); 14 goto err; 15 } 16 17 return 0; 18 19 err: 20 return ret; 21 }
第11行註冊了一個misc設備,相關信息如下:
1 static struct miscdevice remap_pfn_misc = { 2 .minor = MISC_DYNAMIC_MINOR, 3 .name = "remap_pfn", 4 .fops = &remap_pfn_fops, 5 };
這樣載入驅動後會在/dev下生成一個名為remap_pfn的節點,用戶程式可以通過這個節點跟驅動通信。其中remap_pfn_fops的定義如下:
1 static const struct file_operations remap_pfn_fops = { 2 .owner = THIS_MODULE, 3 .open = remap_pfn_open, 4 .mmap = remap_pfn_mmap, 5 };
第3行的open函數這裡沒有做什麼實際的工作,只是列印一些log,比如將進程的記憶體佈局信息輸出
第4行,負責處理用戶的mmap請求,這是需要關心的。 先看一下open函數具體列印了那些內容:1 static int remap_pfn_open(struct inode *inode, struct file *file) 2 { 3 struct mm_struct *mm = current->mm; 4 5 printk("client: %s (%d)\n", current->comm, current->pid); 6 printk("code section: [0x%lx 0x%lx]\n", mm->start_code, mm->end_code); 7 printk("data section: [0x%lx 0x%lx]\n", mm->start_data, mm->end_data); 8 printk("brk section: s: 0x%lx, c: 0x%lx\n", mm->start_brk, mm->brk); 9 printk("mmap section: s: 0x%lx\n", mm->mmap_base); 10 printk("stack section: s: 0x%lx\n", mm->start_stack); 11 printk("arg section: [0x%lx 0x%lx]\n", mm->arg_start, mm->arg_end); 12 printk("env section: [0x%lx 0x%lx]\n", mm->env_start, mm->env_end); 13 14 return 0; 15 }第5行將進程的名字以及pid列印出來 第6行列印進程的代碼段的範圍 第7行列印進程的data段的範圍,其中存放的是已初始化全局變數。而bss段存放的是未初始化全局變數,存放位置緊跟在data段後面,堆區之前 第8行列印進程的堆區的起始地址和當前地址 第9行列印進程的mmap區的基地址,這裡的mmap區是向下增長的。具體mmap區的基地址跟系統允許的當前進程的用戶棧的大小有關,用戶棧的最大size越大,mmap區的基地址就越小。修改用戶棧的最大尺寸需要用到ulimit -s xxx命令,單位是KB,表示用戶棧的最大尺寸,用戶棧的尺寸可以上G,而內核棧卻只有區區的2個頁。 第10行列印進程的用戶棧的起始地址,向下增長 第11行和第12行的暫不關心。 下麵是remap_pfn_mmap的實現:
1 static int remap_pfn_mmap(struct file *file, struct vm_area_struct *vma) 2 { 3 unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; 4 unsigned long pfn_start = (virt_to_phys(kbuff) >> PAGE_SHIFT) + vma->vm_pgoff; 5 unsigned long virt_start = (unsigned long)kbuff + offset; 6 unsigned long size = vma->vm_end - vma->vm_start; 7 int ret = 0; 8 9 printk("phy: 0x%lx, offset: 0x%lx, size: 0x%lx\n", pfn_start << PAGE_SHIFT, offset, size); 10 11 ret = remap_pfn_range(vma, vma->vm_start, pfn_start, size, vma->vm_page_prot); 12 if (ret) 13 printk("%s: remap_pfn_range failed at [0x%lx 0x%lx]\n", 14 __func__, vma->vm_start, vma->vm_end); 15 else 16 printk("%s: map 0x%lx to 0x%lx, size: 0x%lx\n", __func__, virt_start, 17 vma->vm_start, size); 18 19 return ret; 20 }
第3行的vma_pgoff表示的是該vma表示的區間在緩衝區中的偏移地址,單位是頁。這個值是用戶調用mmap時傳入的最後一個參數,不過用戶空間的offset的單位是位元組(當然必須是頁對齊),進入內核後,內核會將該值右移PAGE_SHIFT(12),也就是轉換為以頁為單位。因為要在第9行列印這個編譯地址,所以這裡將其再左移PAGE_SHIFT,然後賦值給offset。
第4行計算內核緩衝區中將被映射到用戶空間的地址對應的物理頁幀號。virt_to_phys接受的虛擬地址必須在低端記憶體範圍內,用戶將虛擬地址轉換為物理地址,而vmaloc返回的虛擬地址不在低端記憶體範圍內,所以需要用專門的函數。 第5行計算內核緩衝區中將被映射到用戶空間的地址對應的虛擬地址 第6行計算該vma表示的記憶體區間的大小 第11行調用remap_pfn_range將物理頁幀號pfn_start對應的物理記憶體映射到用戶空間的vm->vm_start處,映射長度為該虛擬記憶體區的長度。由於這裡的內核緩衝區是用kzalloc分配的,保證了物理地址的連續性,所以會將物理頁幀號從pfn_start開始的(size >> PAGE_SHIFT)個連續的物理頁幀依次按序映射到用戶空間。 將驅動編譯成模塊後,insmod到內核。二、用戶測試程式
這裡的五個測試程式都很簡單,只是為了證明他們之間確實共用了同一塊記憶體。 user_1.c:1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (0) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 sprintf(addr, "I am %s\n", argv[0]); 15 16 while(1) 17 sleep(1); 18 return 0; 19 }
第10和第12行,打開設備節點,然後從內核空間映射64KB的記憶體到用戶空間,首地址存放在addr中,由於後面既要寫入也要共用,所以設置了對應的flags。這裡指定的offset是0,即映射前64KB。
第14行輸出字元串到addr指向的虛擬地址空間 user_2.c:1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (0) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 printf("%s", addr); 15 16 while(1) 17 sleep(1); 18 19 return 0; 20 }
user_2跟user_1實現一般一樣,不同之處是將addr指向的虛擬地址空間的內容列印出來。
user_3.c:1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (16*PAGE_SIZE) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 sprintf(addr, "I am %s\n", argv[0]); 15 16 while(1) 17 sleep(1); 18 return 0; 19 }
第12行的OFFSET設置的是64KB,表示將內核緩衝區的後64KB映射到用戶空間
第14行,向緩衝區中輸入字元串 user_4.c:1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (16*PAGE_SIZE) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 printf("%s", addr); 15 16 while(1) 17 sleep(1); 18 return 0; 19 }第12行的OFFSET設置的是64KB,表示將內核緩衝區的後64KB映射到用戶空間 第14行,輸出緩衝區中內容 user_5.c:
1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (32*PAGE_SIZE) 3 #define OFFSET (0) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 int *brk; 10 11 fd = open("/dev/remap_pfn", O_RDWR); 12 13 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0); 14 memset(addr, 0x0, BUF_SIZE); 15 16 printf("Clear Finished\n"); 17 18 while(1) 19 sleep(1); 20 return 0; 21 }第13行,將內核緩衝區的整個128KB都映射到用戶空間 第14行,清除緩衝區中內容
三、測試
1、內核空間的虛擬記憶體佈局
在內核的啟動log里可以看到內核空間的虛擬記憶體佈局信息:1 [ 0.000000] Virtual kernel memory layout: 2 [ 0.000000] vector : 0xffff0000 - 0xffff1000 ( 4 kB) 3 [ 0.000000] fixmap : 0xffc00000 - 0xfff00000 (3072 kB) 4 [ 0.000000] vmalloc : 0xf0800000 - 0xff800000 ( 240 MB) 5 [ 0.000000] lowmem : 0xc0000000 - 0xf0000000 ( 768 MB) 6 [ 0.000000] pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB) 7 [ 0.000000] modules : 0xbf000000 - 0xbfe00000 ( 14 MB) 8 [ 0.000000] .text : 0xc0008000 - 0xc0800000 (8160 kB) 9 [ 0.000000] .init : 0xc0b00000 - 0xc0c00000 (1024 kB) 10 [ 0.000000] .data : 0xc0c00000 - 0xc0c7696c ( 475 kB) 11 [ 0.000000] .bss : 0xc0c78000 - 0xc0cc9b8c ( 327 kB)用kzalloc分配的記憶體會落在第5行表示的虛擬記憶體範圍內 用vmalloc分配的記憶體會落在第4行表示的虛擬記憶體範圍內
2、用戶虛擬地址空間的佈局
下麵是Linux系統下用戶的虛擬記憶體佈局大致信息:3、user_1和user_2
運行user1:[root@vexpress mnt]# ./user_1
可以看到如下內核log:
1 [ 2494.835749] client: user_1 (870) 2 [ 2494.835918] code section: [0x8000 0x87f4] 3 [ 2494.836047] data section: [0x107f4 0x1092c] 4 [ 2494.836165] brk section: s: 0x11000, c: 0x11000 5 [ 2494.836307] mmap section: s: 0xb6f17000 6 [ 2494.836441] stack section: s: 0xbe909e20 7 [ 2494.836569] arg section: [0xbe909f23 0xbe909f2c] 8 [ 2494.836689] env section: [0xbe909f2c 0xbe909ff3] 9 [ 2494.836943] phy: 0x8eb60000, offset: 0x0, size: 0x10000 10 [ 2494.837176] remap_pfn_mmap: map 0xeeb60000 to 0xb6d75000, size: 0x10000
進程號是870,可以分別用下麵的查看一下該進程的地址空間的map信息:
1 [root@vexpress mnt]# cat /proc/870/maps 2 00008000-00009000 r-xp 00000000 00:12 1179664 /mnt/user_1 3 00010000-00011000 rw-p 00000000 00:12 1179664 /mnt/user_1 4 b6d75000-b6d85000 rw-s 00000000 00:10 8765 /dev/remap_pfn 5 b6d85000-b6eb8000 r-xp 00000000 b3:01 143 /lib/libc-2.18.so 6 b6eb8000-b6ebf000 ---p 00133000 b3:01 143 /lib/libc-2.18.so 7 b6ebf000-b6ec1000 r--p 00132000 b3:01 143 /lib/libc-2.18.so 8 b6ec1000-b6ec2000 rw-p 00134000 b3:01 143 /lib/libc-2.18.so 9 b6ec2000-b6ec5000 rw-p 00000000 00:00 0 10 b6ec5000-b6ee6000 r-xp 00000000 b3:01 188 /lib/libgcc_s.so.1 11 b6ee6000-b6eed000 ---p 00021000 b3:01 188 /lib/libgcc_s.so.1 12 b6eed000-b6eee000 rw-p 00020000 b3:01 188 /lib/libgcc_s.so.1 13 b6eee000-b6f0e000 r-xp 00000000 b3:01 165 /lib/ld-2.18.so 14 b6f13000-b6f15000 rw-p 00000000 00:00 0 15 b6f15000-b6f16000 r--p 0001f000 b3:01 165 /lib/ld-2.18.so 16 b6f16000-b6f17000 rw-p 00020000 b3:01 165 /lib/ld-2.18.so 17 be8e9000-be90a000 rw-p 00000000 00:00 0 [stack] 18 bed1c000-bed1d000 r-xp 00000000 00:00 0 [sigpage] 19 bed1d000-bed1e000 r--p 00000000 00:00 0 [vvar] 20 bed1e000-bed1f000 r-xp 00000000 00:00 0 [vdso] 21 ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
上面的每一行都可以表示一個vma的映射信息,其中第4行是需要關心的:
1 b6d75000-b6d85000 rw-s 00000000 00:10 8765 /dev/remap_pfn
含義:
"b6d75000"是vma->vm_start的值,"b6d85000"是vma->vm_end的值,b6d85000減b6d75000是64KB,即給vma表示的虛擬記憶體區域的大小。 "rw-s"表示的是vma->vm_flags,其中's'表示share,'p'表示private "00000000"表示偏移量,也就是vma->vm_pgoff的值 "00:10"表示該設備節點的主次設備號 "8765"表示該設備節點的inode值 "/dev/remap_pfn"表示設備節點的名字。 也可以用pmap查看該進程的虛擬地址空間映射信息:1 [root@vexpress mnt]# pmap -x 870 2 870: {no such process} ./user_1 3 Address Kbytes PSS Dirty Swap Mode Mapping 4 00008000 4 4 0 0 r-xp /mnt/user_1 5 00010000 4 4 4 0 rw-p /mnt/user_1 6 b6d75000 64 0 0 0 rw-s /dev/remap_pfn 7 b6d85000 1228 424 0 0 r-xp /lib/libc-2.18.so 8 b6eb8000 28 0 0 0 ---p /lib/libc-2.18.so 9 b6ebf000 8 8 8 0 r--p /lib/libc-2.18.so 10 b6ec1000 4 4 4 0 rw-p /lib/libc-2.18.so 11 b6ec2000 12 8 8 0 rw-p [ anon ] 12 b6ec5000 132 64 0 0 r-xp /lib/libgcc_s.so.1 13 b6ee6000 28 0 0 0 ---p /lib/libgcc_s.so.1 14 b6eed000 4 4 4 0 rw-p /lib/libgcc_s.so.1 15 b6eee000 128 122 0 0 r-xp /lib/ld-2.18.so 16 b6f13000 8 8 8 0 rw-p [ anon ] 17 b6f15000 4 4 4 0 r--p /lib/ld-2.18.so 18 b6f16000 4 4 4 0 rw-p /lib/ld-2.18.so 19 be8e9000 132 4 4 0 rw-p [stack] 20 bed1c000 4 0 0 0 r-xp [sigpage] 21 bed1d000 4 0 0 0 r--p [vvar] 22 bed1e000 4 0 0 0 r-xp [vdso] 23 ffff0000 4 0 0 0 r-xp [vectors] 24 -------- ------ ------ ------ ------ 25 total 1808 662 48 0
然後運行user_2:
1 [root@vexpress mnt]# ./user_2 2 I am ./user_1
可以看到user_1寫入的信息,下麵是內核log以及虛擬地址空間映射信息:
1 [ 2545.832903] client: user_2 (873) 2 [ 2545.833087] code section: [0x8000 0x87e0] 3 [ 2545.833178] data section: [0x107e0 0x10918] 4 [ 2545.833262] brk section: s: 0x11000, c: 0x11000 5 [ 2545.833346] mmap section: s: 0xb6fb5000 6 [ 2545.833423] stack section: s: 0xbea0ee20 7 [ 2545.833499] arg section: [0xbea0ef23 0xbea0ef2c] 8 [ 2545.833590] env section: [0xbea0ef2c 0xbea0eff3] 9 [ 2545.833761] phy: 0x8eb60000, offset: 0x0, size: 0x10000 10 [ 2545.833900] remap_pfn_mmap: map 0xeeb60000 to 0xb6e13000, size: 0x10000 11 12 [root@vexpress mnt]# cat /proc/873/maps 13 00008000-00009000 r-xp 00000000 00:12 1179665 /mnt/user_2 14 00010000-00011000 rw-p 00000000 00:12 1179665 /mnt/user_2 15 b6e13000-b6e23000 rw-s 00000000 00:10 8765 /dev/remap_pfn 16 b6e23000-b6f56000 r-xp 00000000 b3:01 143 /lib/libc-2.18.so 17 b6f56000-b6f5d000 ---p 00133000 b3:01 143 /lib/libc-2.18.so 18 b6f5d000-b6f5f000 r--p 00132000 b3:01 143 /lib/libc-2.18.so 19