简述:
内核映射进程空间,就是由进程分配好空间(属于进程独占资源)后,将用户空间虚拟地址,传递到内核,然后内核映射成内核虚拟地址直接访问,此时内核访问的物理空间是位于用户空间。这样的好处是,内核直接访问进程空间,减少copy动作。
接口:
- 接口要包含的头文件:
#include <linux/mm.h>
- 函数接口:
long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
int write, int force, struct page **pages,
struct vm_area_struct **vmas);
功能:内核用来映射进程内存空间
第一个参数: tsk是进程控制块指针,这个参数几乎一直作为 current 传递。
第二个参数: 一个内存管理结构的指针, 描述被映射的地址空间. mm_struct 结构是捆绑一个进程的虚拟地址空间所有部分. 对于驱动的使用, 这个参数应当一直是current->mm。
第三个参数: start 是(页对齐的)用户空间缓冲的地址。要映射进程空间虚拟地址的起始地址。
第四个参数: nr_pages是映射进程空间的大小,单位页。
第五个参数: write是内核映射这段进程空间来读还是写,非0(一般就是1)表示用来写,当然,对于用户空间就只能是读了;为0表示用来读,此时用户空间就只能是写了。
第六个参数: force 标志告知 get_user_pages 来覆盖在给定页上的保护, 来提供要求的
权限; 驱动应当一直传递 0 在这里。
第七个参数: pages是这个函数的输出参数,映射完成后,pages是指向进程空间的页指针数组,如pages[1],下标最大是nr_pages-1。注意,内核要用pages,还需要映射成内核虚拟地址,一般用kmap(),kmap_atomic()
第八个参数: vmas也是个输出参数,vm_area_struct结构体,是linux用来管理虚拟内存的,映射完成后关于虚拟内存的信息就在这结构体里面。如果驱动不用,vmas可以是NULL。
返回值: 返回实际映射的页数,只会小于等于nr_pages。
映射:
- 映射的时候要上进程读者锁:
进程旗标(锁/信号量),通过current->mm->mmap_sem来获得。
如:
down_read(¤t->mm->mmap_sem);
result = get_user_pages(current, current->mm, ...);
up_read(¤t->mm->mmap_sem);
如果内核映射的空间,用来写,写的时候要上进程写锁,在写的时候去读,就会被阻塞:
down_write(current->mm->mmap_sem);
...
//向映射的空间写数据
//up_write(current->mm->mmap_sem);
current->mm->page_table_lock.rlock也可以用这个自旋锁实现的读者写者,具体情况考虑。
释放映射:
if (! PageReserved(page))
SetPageDirty(page);
page_cache_release(struct page *page);
PageReserved(): 判断是否为保留页,是保留页返回非0,不是返回0。在我们一般映射的页,没经过处理,都不是保留页,返回0。
SetPageDirty(): 简单来说,就是告诉系统这页被修改。
PageReserved(),SetPageDirty()定义在include/linux/page-flags.h,对他们的作用理解,兵不是很深,在一般的驱动中可有可无,安全起见,就按照上面形式放在哪里。
page_cache_release()定义在include/linux/pagemap.h,只能是一次释放一个page,多个page用循环多次调用。
思考:
这种映射,主要是用在进程直接I/O,进程直接读写用户空间内存,就可以达到读写I/O。但是也要具体情况看,相对这种映射对系统开销,还是比较大的。
get_user_pages的用法例子,可以看drivers/scsi/st.c
用这种映射来实现读者写者,进程是读者,驱动是写者。
在“linux驱动—file_operations异步读写aio_read、aio_write”这章,描述的异步例子基础上做,
原始例子如下:
struct kiocb *aki;
struct iovec *aiov;
loff_t aio_off = 0;
struct workqueue *aiowq;
void data_a(struct work_struct *work);
DECLARE_DELAYED_WORK(aio_delaywork,data_a);
ssize_t d_read(struct file *f, char __user *buf, size_t n, loff_t *off)
{
}
void data_a(struct work_struct *work)
{
int ret = 0;
ret = d_read(aki->ki_filp,aiov->iov->iov_base,n,&off);
aio_complete(aki,ret ,0);
}
ssize_t d_aio_read(struct kiocb *ki, const struct iovec *iovs, unsigned long n, loff_t off)
{
if(is_sync_kiocb(ki))
return d_read(ki->ki_filp,iovs->iov->iov_base,n,&off);
else
{
aki = ki;
aiov = iovs;
aio_off = off;
queue_delayed_work(aiowq,aio_delaywork,100);
return -EIOCBQUEUED;//一般都返回这个,
}
}
void init_aio()
{
aiowq= create_workqueue("aiowq");
}
用上get_user_pages()后,将变成:
#include <linux/list.h>
#include <linux/aio.h>
#include <linux/workqueue.h>
#include <linux/mm.h>
struct kiocb *aki;
//struct iovec *aiov;
//loff_t aio_off = 0;
struct workqueue *aiowq;
struct page **aiopages;
LIST_HEAD(custom_aa);
struct custom_async{
list_head list;
task_struct *tsk;
struct page **pg;
long page_num;
ssize_t size;
};
void data_a(struct work_struct *work);
DECLARE_DELAYED_WORK(aio_delaywork,data_a);
DECLARE_DELAYED_WORK(aio_delaywork_c,async_d_writedata);
struct file_operations {
.owner = THIS_MODE,
.read = d_read,
.aio_read = d_aio_read,
...
..
};
ssize_t d_read(struct file *f, char __user *buf, size_t n, loff_t *off)
{
}
ssize_t async_d_writedata(struct work_struct *work)
{
int i,n;
void *p = NULL;
struct page **temp = NULL;
struct custom_async *t = NULL;
struct list_head *tmp = NULL;
list_for_each(custom_aa,tmp){
t = container_of(tmp,custom_async,list);
for(i=0;i<t->page_num;i++)
{
temp = t->pg;
p = kmap(temp[i]);
down_write(&t->tsk->mm->mmap_sem);
...
//向p指定的空间写数据 , n为写了多少个字节数据
...
t->size = n;
up_write(&t->tsk->mm->mmap_sem);
kunmap(aio_pages[i]);
}
}
queue_delayed_work(aiowq,aio_delaywork_c,100);
}
void data_a(struct work_struct *work)
{
int ret = 0;
struct custom_async *t;
//ret = d_read(aki->ki_filp,aiov->iov->iov_base,n,&off);
t = container_of(custom_aa.next,custom_async,list);
async_d_writedata();
ret = t->size;
aio_complete(aki,ret ,0);
}
ssize_t d_aio_read(struct kiocb *ki, const struct iovec *iovs, unsigned long n, loff_t off)
{
if(is_sync_kiocb(ki))
return d_read(ki->ki_filp,iovs->iov->iov_base,n,&off);
else
{
aki = ki;
//aiov = iovs;
//aio_off = off;
struct custom_async *p;
p = kmalloc(sizeof(struct custom_async));
memset(p,0,sizeof(struct custom_async));
INIT_LIST_HEAD(&p->list);
list_add(&p->list,&custom_aa);
p->tsk = current;
down_read(¤t->mm->mmap_sem);
pagenum = get_user_pages(current,current->mm,iovs->iov->iov_base,
n/PAGE_SIZE+(n%PAGE_SIZE)?1:0,
1,0,p->pg,NULL);
up_read(¤t->mm->mmap_sem);
queue_delayed_work(aiowq,aio_delaywork,100);
return -EIOCBQUEUED;//一般都返回这个,
}
}
void init_aio()
{
aiowq= create_workqueue("aiowq");
INIT_LIST_HEAD(custom_aa);
}
void exit()
{
int i;
...
..
for(i=0;i<pagenum;i++)
{
if (! PageReserved(aiopages[i]))
SetPageDirty(aiopages[i]);
page_cache_release(aiopages[i]);
}
...
..
}
上面的例子,进程都可以通过异步调用来申请映射,当进程收到一次异步调用完成后,不用再次异步调用,只需要读就可以,驱动会自动的向里面写数据。只要来异步调用申请过一次的进程,当有数据时,都会向每个进程空间写数据。典型的读者写者,进程是读者,驱动是写者。
上面例子,只是表达了处理逻辑,没有编译过,模块退出时,释放不完全。
对比一般的异步,内核是直接向进程空间写数据,不用在数据完成后,还需要一次copy,理论上会快些。