例说linux内核与应用数据通信(四):映射设备内核空间到用户态

释放双眼,带上耳机,听听看~!

    
   
一个进程的内存映象由下面几部分组成:代码段、数据段、BSS段
和堆栈段,以及内存映射的区域等部分,
内存映射函数mmap()
, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的读取和修改
,而文件可以是设备驱动文件节点。通过把内核驱动的内存空间映射到应用层,可以实现应用和内核空间的数据交换。

        linux设备分三种,字符设备、块设备、网络接口
设备。
每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户态程序通过设备文件(或称设备节点)
来使用驱动程序操作字符设备和块设备。

实现内存映射的关键在于实现字符设备驱动的mmap()函数,mmap()函数原型为:


1
2
3
1#include <sys/mman.h>
2void *mmap(void *addr, size_t length, int prot, int flags,  int fd, off_t offset);
3

该函数
负责把文件内容映射到进程的虚拟地址空间,通过对这段内存的读取和修改来实现对文件的读取和修改,而不需要再调用read和write;

mmap方法是file_operations结构的成员,在mmap系统调用的发出时被调用。
mmap设备方法所需要做的就是建立虚拟地址到物理地址的页

。其实在在我们调用系统调用mmap时,内核中的sys_mmap函数首先根据用户提供给mmap的参数(如起始地址、空间大小、行为修饰符等)创
建新的vma。然后再调用相应文件的file_operations中的mmap函数。

进程虚拟地址空间相关内容可参考作者这篇文章:
http://www.aiuxian.com/article/p-2687955.html

      
使用remap_pfn_range()函数将设备内存线性地映射到用户地址空间中。该函数原型为:


1
2
3
4
5
6
7
8
9
10
11
12
13
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 */
11int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
12            unsigned long pfn, unsigned long size, pgprot_t prot)
13

其中vma为虚拟内存区域,在一定范围内的页将被映射到该区域内。

addr为重新映射时的起始地址,该函数为处于addr和addr+size之间的虚拟地址建立页表。

pfn为与物理内存对于的页帧号,页帧号只是将物理地址右移PAGE_SHIFT位。

size为以字节为单位,被重新映射的大小。

prot为新VMA要求的“保护”属性。

下面看一看
file_operations
中的mmap成员的实现:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1static struct vm_operations_struct sln_remap_vm_ops = {
2    .open   = sln_vma_open,
3    .close  = sln_vma_close
4};
5
6static int chrmem_dev_mmap(struct file*filp, struct vm_area_struct *vma)
7{
8      struct mem_dev *dev = filp->private_data;
9    
10      if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dev->data)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
11          return  -EAGAIN;
12                
13    vma->vm_ops = &sln_remap_vm_ops;
14
15    sln_vma_open(vma);
16      return 0;
17}
18

该函数中函数page_to_pfn
(
shm_page
)
将表示物理页面的page结构转换为其对应的页帧号。该字符设备驱动的主要思想是建立一个字符设备,在它的驱动程序中申请一块物理内存区域,并利用mmap将这段物理内存区域映射到进程的地址空间中。该驱动源码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
1#include <linux/module.h>
2#include <linux/types.h>
3#include <linux/fs.h>
4#include <linux/errno.h>
5#include <linux/mm.h>
6#include <linux/sched.h>
7#include <linux/init.h>
8#include <linux/cdev.h>
9#include <asm/io.h>
10#include <asm/system.h>
11#include <asm/uaccess.h>
12
13#include <linux/kernel.h>
14#include "chr_memdev.h"
15
16int                 chrmem_major;
17struct chrmem_dev   *chrmem_devp;
18
19int chrmem_open(struct inode *inode, struct file *filp)
20{
21    filp->private_data = chrmem_devp;
22    
23    return 0;
24}
25
26......
27
28void sln_vma_open(struct vm_area_struct *vma)
29{
30    printk("===vma_open: %s===\n", chrmem_devp->data);
31}
32
33void sln_vma_close(struct vm_area_struct *vma)
34{
35    printk("===vma_close: %s===\n", chrmem_devp->data);
36}
37
38static struct vm_operations_struct sln_remap_vm_ops = {
39    .open   = sln_vma_open,
40    .close  = sln_vma_close
41};
42
43int chrmem_release(struct inode *inode, struct file *filp)
44{
45  return 0;
46}
47
48static int chrmem_dev_mmap(struct file*filp, struct vm_area_struct *vma)
49{
50      struct chrmem_dev *dev = filp->private_data;
51    
52      if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dev->data)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
53          return  -EAGAIN;
54                
55    vma->vm_ops = &sln_remap_vm_ops;
56
57    sln_vma_open(vma);
58      return 0;
59}
60
61static const struct file_operations chrmem_fops =
62{
63  .owner    = THIS_MODULE,
64  .open     = chrmem_open,
65  .release  = chrmem_release,
66  .read     = chrmem_read,
67  .write    = chrmem_write,
68  .llseek   = chrmem_llseek,
69  .ioctl    = chrmem_ioctl,
70  .mmap     = chrmem_dev_mmap
71
72};
73
74static int chrmem_dev_init(void)
75{
76    int result;
77    dev_t devno;
78
79    /* 分配设备号 */
80    result = alloc_chrdev_region(&devno, 0, 1, "chrmem_dev");
81    if (result < 0) {
82        return result;
83    }
84
85    // 为自定义设备结构体分配内存空间
86    mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
87    if (!mem_devp) {
88        result =  - ENOMEM;
89        goto err;
90    }
91    memset(mem_devp, 0, sizeof(struct mem_dev));
92
93    /*初始化字符设备*/
94    cdev_init(&mem_devp->cdev, &mem_fops);
95    mem_devp->cdev.owner = THIS_MODULE;
96
97    /*添加注册字符设备 */
98    mem_major = MAJOR(devno);
99    cdev_add(&mem_devp->cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
100  
101    /*初始化自定义设备数据内容*/
102    mem_devp->data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
103    memset(mem_devp->data, '*', MEMDEV_SIZE / 100 );
104    
105    return 0;
106
107err:
108  unregister_chrdev_region(devno, 1);
109
110  return result;
111}
112
113
114static int chrmem_dev_init(void)
115{
116    int result;
117    dev_t devno;
118
119    /* 分配设备号 */
120    result = alloc_chrdev_region(&devno, 0, 1, "chrmem_dev");
121    if (result < 0) {
122        return result;
123    }
124
125    // 为自定义设备结构体分配内存空
126    chrmem_devp = kmalloc(CHR_MEMDEV_NUM * sizeof(struct chrmem_dev), GFP_KERNEL);
127    if (!chrmem_devp) {
128        result =  - ENOMEM;
129        goto err;
130    }
131    memset(chrmem_devp, 0, sizeof(struct chrmem_dev));
132
133    /*初始化字符设备*/
134    cdev_init(&chrmem_devp->cdev, &chrmem_fops);
135    chrmem_devp->cdev.owner = THIS_MODULE;
136
137    /*添加注册字符设备 */
138    chrmem_major = MAJOR(devno);
139    cdev_add(&chrmem_devp->cdev, MKDEV(chrmem_major, 0), CHR_MEMDEV_NUM);
140  
141    /*初始化自定义设备数据内容*/
142    chrmem_devp->data = kmalloc(CHR_MEMDEV_DATA_SIZE, GFP_KERNEL);
143    memset(chrmem_devp->data, '*', CHR_MEMDEV_DATA_SIZE / 100 );
144    
145    return 0;
146
147err:
148  unregister_chrdev_region(devno, 1);
149
150  return result;
151}
152
153static void chrmem_dev_exit(void)
154{
155    cdev_del(&chrmem_devp->cdev); //delete device
156    kfree(chrmem_devp); // release device memory
157    unregister_chrdev_region(MKDEV(chrmem_major, 0), 1); // unregister char device No.
158}
159
160module_init(chrmem_dev_init);
161module_exit(chrmem_dev_exit);
162
163MODULE_LICENSE("GPL");
164MODULE_AUTHOR("shallnet");
165MODULE_DESCRIPTION("blog.csdn.net/shallnet");
166

在应用程序中调用mmap来实现内存映射,应用程序代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
1#include <stdio.h>
2#include <sys/types.h>
3#include <sys/stat.h>
4#include <fcntl.h>
5#include <unistd.h>
6#include <sys/mman.h>
7
8#include <stdlib.h>
9#include <string.h>
10#include <errno.h>
11#include <sys/ioctl.h>
12
13#define SHR_MEMSIZE             4096
14#define MEM_CLEAR               0x0
15#define MEM_RESET               0x1
16#define MEM_DEV_FILENAME       "/dev/sln_memdev"
17
18int main()
19{
20    int         fd;
21    char        *shm = NULL;
22    
23    fd = open(MEM_DEV_FILENAME, O_RDWR);
24    if (fd < 0) {
25        printf("open(): %s\n", strerror(errno));
26        return -1;
27    }
28        
29    shm = mmap(NULL, SHR_MEMSIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
30    if (MAP_FAILED == shm) {
31        printf("mmap: %s\n", strerror(errno));
32    }
33    
34    printf("Before Write, shm = %s\n", shm);
35
36    strcpy(shm,"User write to share memory!");
37    
38    printf("After write, shm = %s\n", shm);
39
40    if (0 > ioctl(fd, MEM_CLEAR, NULL)) {
41        printf("ioctl: %s\n", strerror(errno));
42        return -1;
43    }
44
45    printf("After clear, shm = %s\n", shm);
46
47    if (0 > ioctl(fd, MEM_RESET, NULL)) {
48        printf("ioctl: %s\n", strerror(errno));
49        return -1;
50    }
51    printf("After reset, shm = %s\n", shm);
52
53    munmap(shm, SHR_MEMSIZE);
54    close(fd);  
55    return 0;    
56}
57

应用程序在实现映射之后,首先读取输出共享内存内容,然后写入,然后清空该共享内存内容以及重设共享内存。在编译驱动和应用程序之后首先插入驱动,在创建设备节点,最后运行应用程序看是否成功,如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
1# insmod memdev.ko
2# cat /proc/devices | grep chrmem_dev
3248 chrmem_dev
4# mknod  /dev/sln_memdev c 248 0
5# ls
6app  app_read  drv  Makefile  mem_app  memdev.ko  read_app
7# ./mem_app
8Before Write, shm = ****************************************
9After write, shm = User write to share memory!
10After clear, shm =
11After reset, shm = hello, user!
12#
13

可以看到字符设备驱动的内核空间被成功映射到用户态,现在
用户空间的一段内存关联到设备内存上,对用户空间的读写就相当于对字符设备的读写。

给TA打赏
共{{data.count}}人
人已打赏
安全运维

WordPress网站专用docker容器环境带Waf

2020-7-18 20:04:44

安全运维

运维安全-Gitlab管理员权限安全思考

2021-9-19 9:16:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索