(四):映射設備內核空間到用戶態

一個進程的內存映象由下面幾部分組成:代碼段、數據段、BSS段和堆棧段,以及內存映射的區域等部分,內存映射函數mmap(), 負責把文件內容映射到進程的虛擬內存空間, 通過對這段內存的讀取和修改,來實現對文件的讀取和修改,而文件可以是設備驅動文件節點。通過把內核驅動的內存空間映射到應用層,可以實現應用和內核空間的數據交換。

linux設備分三種,字符設備、塊設備、網絡接口設備。每一個字符設備或塊設備都在/dev目錄下對應一個設備文件。linux用戶態程序通過設備文件(或稱設備節點)來使用驅動程序操作字符設備和塊設備。

本節使用字符設備驅動為例來實現映射,有關字符設備驅動相關內容可參考作者這篇文章: http://blog.csdn.net/shallnet/article/details/17734309 實現內存映射的關鍵在於實現字符設備驅動的mmap()函數,mmap()函數原型為:

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

該函數負責把文件內容映射到進程的虛擬地址空間,通過對這段內存的讀取和修改來實現對文件的讀取和修改,而不需要再調用read和write;

mmap方法是file_operations結構的成員,在mmap系統調用的發出時被調用。mmap設備方法所需要做的就是建立虛擬地址到物理地址的頁表。其實在在我們調用系統調用mmap時,內核中的sys_mmap函數首先根據用戶提供給mmap的參數(如起始地址、空間大小、行為修飾符等)創建新的vma。然後再調用相應文件的file_operations中的mmap函數。

進程虛擬地址空間相關內容可參考作者這篇文章: http://blog.csdn.net/shallnet/article/details/47701225 使用remap_pfn_range()函數將設備內存線性地映射到用戶地址空間中。該函數原型為:

/**
 * remap_pfn_range - remap kernel memory to userspace
 * @vma: user vma to map to
 * @addr: target user address to start at
 * @pfn: physical address of kernel memory
 * @size: size of map area
 * @prot: page protection flags for this mapping
 *
 *  Note: this is only safe if the mm semaphore is held when called.
 */
int remap_pfn_range(struct vm_area_struct* vma, unsigned long addr,
                    unsigned long pfn, unsigned long size, pgprot_t prot)

其中vma為虛擬內存區域,在一定範圍內的頁將被映射到該區域內。 addr為重新映射時的起始地址,該函數為處於addr和addr+size之間的虛擬地址建立頁表。 pfn為與物理內存對於的頁幀號,頁幀號只是將物理地址右移PAGE_SHIFT位。 size為以字節為單位,被重新映射的大小。 prot為新VMA要求的“保護”屬性。 下面看一看file_operations中的mmap成員的實現:

static struct vm_operations_struct sln_remap_vm_ops = {  
    .open   = sln_vma_open,  
    .close  = sln_vma_close  
};  
  
static int chrmem_dev_mmap(struct file*filp, struct vm_area_struct *vma)  
{  
      struct mem_dev *dev = filp->private_data;  
       
      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))  
          return  -EAGAIN;  
                  
    vma->vm_ops = &sln_remap_vm_ops;  
  
    sln_vma_open(vma);  
      return 0;  
}

該函數中函數page_to_pfn(shm_page)將表示物理頁面的page結構轉換為其對應的頁幀號。該字符設備驅動的主要思想是建立一個字符設備,在它的驅動程序中申請一塊物理內存區域,並利用mmap將這段物理內存區域映射到進程的地址空間中。該驅動源碼如下:

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>

#include <linux/kernel.h>
#include "chr_memdev.h"

int                 chrmem_major;
struct chrmem_dev*   chrmem_devp;

int chrmem_open(struct inode* inode, struct file* filp)
{
    filp->private_data = chrmem_devp;

    return 0;
}

void sln_vma_open(struct vm_area_struct* vma)
{
    printk("===vma_open: %s===\n", chrmem_devp->data);
}

void sln_vma_close(struct vm_area_struct* vma)
{
    printk("===vma_close: %s===\n", chrmem_devp->data);
}

static struct vm_operations_struct sln_remap_vm_ops = {
    .open   = sln_vma_open,
    .close  = sln_vma_close
};

int chrmem_release(struct inode* inode, struct file* filp)
{
    return 0;
}

static int chrmem_dev_mmap(struct file* filp, struct vm_area_struct* vma)
{
    struct chrmem_dev* dev = filp->private_data;

    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)) {
        return  -EAGAIN;
    }

    vma->vm_ops = &sln_remap_vm_ops;

    sln_vma_open(vma);
    return 0;
}

static const struct file_operations chrmem_fops = {
    .owner    = THIS_MODULE,
    .open     = chrmem_open,
    .release  = chrmem_release,
    .read     = chrmem_read,
    .write    = chrmem_write,
    .llseek   = chrmem_llseek,
    .ioctl    = chrmem_ioctl,
    .mmap     = chrmem_dev_mmap

};

static int chrmem_dev_init(void)
{
    int result;
    dev_t devno;

    /* 分配設備號 */
    result = alloc_chrdev_region(&devno, 0, 1, "chrmem_dev");

    if (result < 0) {
        return result;
    }


    // 為自定義設備結構體分配內存空間
    
    mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);

    if (!mem_devp) {
        result =  -ENOMEM;
        goto err;
    }

    memset(mem_devp, 0, sizeof(struct mem_dev));

    /*初始化字符設備*/
    cdev_init(&mem_devp->cdev, &mem_fops);
    mem_devp->cdev.owner = THIS_MODULE;

    /*添加註冊字符設備 */
    mem_major = MAJOR(devno);
    cdev_add(&mem_devp->cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);

    /*初始化自定義設備數據內容*/
    mem_devp->data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
    memset(mem_devp->data, '*', MEMDEV_SIZE / 100);

    return 0;

err:
    unregister_chrdev_region(devno, 1);

    return result;
}

static int chrmem_dev_init(void)
{
    int result;
    dev_t devno;

    /* 分配設備號 */
    result = alloc_chrdev_region(&devno, 0, 1, "chrmem_dev");

    if (result < 0) {
        return result;
    }

    // 為自定義設備結構體分配內存空
    chrmem_devp = kmalloc(CHR_MEMDEV_NUM * sizeof(struct chrmem_dev), GFP_KERNEL);

    if (!chrmem_devp) {
        result =  - ENOMEM;
        goto err;
    }

    memset(chrmem_devp, 0, sizeof(struct chrmem_dev));

    /*初始化字符設備*/
    cdev_init(&chrmem_devp->cdev, &chrmem_fops);
    chrmem_devp->cdev.owner = THIS_MODULE;

    /*添加註冊字符設備 */
    chrmem_major = MAJOR(devno);
    cdev_add(&chrmem_devp->cdev, MKDEV(chrmem_major, 0), CHR_MEMDEV_NUM);

    /*初始化自定義設備數據內容*/
    chrmem_devp->data = kmalloc(CHR_MEMDEV_DATA_SIZE, GFP_KERNEL);
    memset(chrmem_devp->data, '*', CHR_MEMDEV_DATA_SIZE / 100);

    return 0;

err:
    unregister_chrdev_region(devno, 1);

    return result;
}

static void chrmem_dev_exit(void)
{
    cdev_del(&chrmem_devp->cdev); //delete device
    kfree(chrmem_devp); // release device memory
    unregister_chrdev_region(MKDEV(chrmem_major, 0),
                             1); // unregister char device No.
}

module_init(chrmem_dev_init);
module_exit(chrmem_dev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("shallnet");
MODULE_DESCRIPTION("blog.csdn.net/shallnet");

在應用程序中調用mmap來實現內存映射,應用程序代碼如下:

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

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>

#define SHR_MEMSIZE             4096
#define MEM_CLEAR               0x0
#define MEM_RESET               0x1
#define MEM_DEV_FILENAME       "/dev/sln_memdev"

int main()
{
    int         fd;
    char*        shm = NULL;

    fd = open(MEM_DEV_FILENAME, O_RDWR);

    if (fd < 0) {
        printf("open(): %s\n", strerror(errno));
        return -1;
    }

    shm = mmap(NULL, SHR_MEMSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    if (MAP_FAILED == shm) {
        printf("mmap: %s\n", strerror(errno));
    }

    printf("Before Write, shm = %s\n", shm);

    strcpy(shm, "User write to share memory!");

    printf("After write, shm = %s\n", shm);

    if (0 > ioctl(fd, MEM_CLEAR, NULL)) {
        printf("ioctl: %s\n", strerror(errno));
        return -1;
    }

    printf("After clear, shm = %s\n", shm);

    if (0 > ioctl(fd, MEM_RESET, NULL)) {
        printf("ioctl: %s\n", strerror(errno));
        return -1;
    }

    printf("After reset, shm = %s\n", shm);

    munmap(shm, SHR_MEMSIZE);
    close(fd);
    return 0;
}

應用程序在實現映射之後,首先讀取輸出共享內存內容,然後寫入,然後清空該共享內存內容以及重設共享內存。在編譯驅動和應用程序之後首先插入驅動,在創建設備節點,最後運行應用程序看是否成功,如下:

# insmod memdev.ko  
# cat /proc/devices | grep chrmem_dev  
248 chrmem_dev  
# mknod  /dev/sln_memdev c 248 0  
# ls  
app  app_read  drv  Makefile  mem_app  memdev.ko  read_app  
# ./mem_app  
Before Write, shm = ****************************************  
After write, shm = User write to share memory!  
After clear, shm =  
After reset, shm = hello, user!  
#  

可以看到字符設備驅動的內核空間被成功映射到用戶態,現在用戶空間的一段內存關聯到設備內存上,對用戶空間的讀寫就相當於對字符設備的讀寫。

本節源碼下載: http://download.csdn.net/detail/gentleliu/9035831


书籍推荐