Cómo mapear un buffer del kernel de Linux al espacio del usuario?

Digamos que el buffer se asigna usando un esquema basado en página. Una forma de implementar mmap sería usar remap_pfn_range pero LDD3 dice que esto no funciona para la memoria convencional. Parece que podemos evitar esto marcando las páginas reservadas utilizando SetPageReserved para que se bloquee en la memoria. Pero, ¿no es toda la memoria del kernel ya no intercambiable, es decir, ya está reservada? ¿Por qué la necesidad de establecer el bit reservado explícitamente?

¿Tiene esto algo que ver con las páginas asignadas desde HIGH_MEM?

La forma más sencilla de mapear un conjunto de páginas del kernel en su método mmap es usar el controlador de fallas para mapear las páginas. Básicamente terminas con algo como:

static int my_mmap(struct file *filp, struct vm_area_struct *vma) { vma->vm_ops = &my_vm_ops; return 0; } static const struct file_operations my_fops = { .owner = THIS_MODULE, .open = nonseekable_open, .mmap = my_mmap, .llseek = no_llseek, }; 

(donde las otras operaciones de archivo son las que su módulo necesita). También en my_mmap usted hace cualquier comprobación de rango, etc. que se necesite para validar los parámetros de mmap.

Entonces los vm_ops ven así:

 static int my_fault(struct vm_area_struct *vma, struct vm_fault *vmf) { vmf->page = my_page_at_index(vmf->pgoff); get_page(vmf->page); return 0; } static const struct vm_operations_struct my_vm_ops = { .fault = my_fault } 

donde solo necesita averiguar para un vma / vmf dado que pasó a su función de falla qué página mapear en el espacio de usuario. Esto depende exactamente de cómo funciona su módulo. Por ejemplo, si lo hiciste

 my_buf = vmalloc_user(MY_BUF_SIZE); 

entonces la página que usas sería algo así como

 vmalloc_to_page(my_buf + (vmf->pgoff < < PAGE_SHIFT)); 

Pero podría crear fácilmente una matriz y asignar una página para cada entrada, usar kmalloc, lo que sea.

[ my_fault acabo de dar cuenta de que my_fault es un nombre ligeramente divertido para una función]

Ejemplo ejecutable mínimo y prueba userland

Módulo kernel :

 #include  /* copy_from_user */ #include  #include  #include  #include  /* min */ #include  #include  #include  #include  static const char *filename = "lkmc_mmap"; enum { BUFFER_SIZE = 4 }; struct mmap_info { char *data; }; /* After unmap. */ static void vm_close(struct vm_area_struct *vma) { pr_info("vm_close\n"); } /* First page access. */ static int vm_fault(struct vm_area_struct *vma, struct vm_fault *vmf) { struct page *page; struct mmap_info *info; pr_info("vm_fault\n"); info = (struct mmap_info *)vma->vm_private_data; if (info->data) { page = virt_to_page(info->data); get_page(page); vmf->page = page; } return 0; } /* Aftr mmap. TODO vs mmap, when can this happen at a different time than mmap? */ static void vm_open(struct vm_area_struct *vma) { pr_info("vm_open\n"); } static struct vm_operations_struct vm_ops = { .close = vm_close, .fault = vm_fault, .open = vm_open, }; static int mmap(struct file *filp, struct vm_area_struct *vma) { pr_info("mmap\n"); vma->vm_ops = &vm_ops; vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP; vma->vm_private_data = filp->private_data; vm_open(vma); return 0; } static int open(struct inode *inode, struct file *filp) { struct mmap_info *info; pr_info("open\n"); info = kmalloc(sizeof(struct mmap_info), GFP_KERNEL); pr_info("virt_to_phys = 0x%llx\n", (unsigned long long)virt_to_phys((void *)info)); info->data = (char *)get_zeroed_page(GFP_KERNEL); memcpy(info->data, "asdf", BUFFER_SIZE); filp->private_data = info; return 0; } static ssize_t read(struct file *filp, char __user *buf, size_t len, loff_t *off) { struct mmap_info *info; int ret; pr_info("read\n"); info = filp->private_data; ret = min(len, (size_t)BUFFER_SIZE); if (copy_to_user(buf, info->data, ret)) { ret = -EFAULT; } return ret; } static ssize_t write(struct file *filp, const char __user *buf, size_t len, loff_t *off) { struct mmap_info *info; pr_info("write\n"); info = filp->private_data; if (copy_from_user(info->data, buf, min(len, (size_t)BUFFER_SIZE))) { return -EFAULT; } else { return len; } } static int release(struct inode *inode, struct file *filp) { struct mmap_info *info; pr_info("release\n"); info = filp->private_data; free_page((unsigned long)info->data); kfree(info); filp->private_data = NULL; return 0; } static const struct file_operations fops = { .mmap = mmap, .open = open, .release = release, .read = read, .write = write, }; static int myinit(void) { proc_create(filename, 0, NULL, &fops); return 0; } static void myexit(void) { remove_proc_entry(filename, NULL); } module_init(myinit) module_exit(myexit) MODULE_LICENSE("GPL"); 

Prueba de usuario :

 #define _XOPEN_SOURCE 700 #include  #include  #include  #include  #include  /* uintmax_t */ #include  #include  #include  /* sysconf */ #include "common.h" /* virt_to_phys_user */ enum { BUFFER_SIZE = 4 }; int main(int argc, char **argv) { int fd; long page_size; char *address1, *address2; char buf[BUFFER_SIZE]; uintptr_t paddr; if (argc < 2) { printf("Usage: %s \n", argv[0]); return EXIT_FAILURE; } page_size = sysconf(_SC_PAGE_SIZE); printf("open pathname = %s\n", argv[1]); fd = open(argv[1], O_RDWR | O_SYNC); if (fd < 0) { perror("open"); assert(0); } printf("fd = %d\n", fd); /* mmap twice for double fun. */ puts("mmap 1"); address1 = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (address1 == MAP_FAILED) { perror("mmap"); assert(0); } puts("mmap 2"); address2 = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (address2 == MAP_FAILED) { perror("mmap"); return EXIT_FAILURE; } assert(address1 != address2); /* Read and modify memory. */ puts("access 1"); assert(!strcmp(address1, "asdf")); /* vm_fault */ puts("access 2"); assert(!strcmp(address2, "asdf")); /* vm_fault */ strcpy(address1, "qwer"); /* Also modified. So both virtual addresses point to the same physical address. */ assert(!strcmp(address2, "qwer")); /* Check that the physical addresses are the same. * They are, but TODO why virt_to_phys on kernel gives a different value? */ assert(!virt_to_phys_user(&paddr, getpid(), (uintptr_t)address1)); printf("paddr1 = 0x%jx\n", (uintmax_t)paddr); assert(!virt_to_phys_user(&paddr, getpid(), (uintptr_t)address2)); printf("paddr2 = 0x%jx\n", (uintmax_t)paddr); /* Check that modifications made from userland are also visible from the kernel. */ read(fd, buf, BUFFER_SIZE); assert(!memcmp(buf, "qwer", BUFFER_SIZE)); /* Modify the data from the kernel, and check that the change is visible from userland. */ write(fd, "zxcv", 4); assert(!strcmp(address1, "zxcv")); assert(!strcmp(address2, "zxcv")); /* Cleanup. */ puts("munmap 1"); if (munmap(address1, page_size)) { perror("munmap"); assert(0); } puts("munmap 2"); if (munmap(address2, page_size)) { perror("munmap"); assert(0); } puts("close"); close(fd); return EXIT_SUCCESS; } 

Aunque las páginas están reservadas a través de un controlador de kernel, se debe acceder a través de un espacio de usuario. Como resultado, las PTE (entradas de la tabla de páginas) no saben si la pfn pertenece al espacio de usuario o al espacio del kernel (aunque se asignan a través del controlador kernel).

Es por eso que están marcados con SetPageReserved .