Chinaunix首页 | 论坛 | 博客
  • 博客访问: 102539
  • 博文数量: 25
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 251
  • 用 户 组: 普通用户
  • 注册时间: 2014-04-29 14:18
个人简介

不以物喜,勿以己悲;乐观向上,持之以恒。

文章分类

全部博文(25)

文章存档

2015年(25)

我的朋友

分类: LINUX

2015-07-19 22:01:13


学习内核模块块设备驱动代码,写一个块设备驱动,正确的加载、卸载,进行读写操作。

在程序中将涵盖以下几点内容:

1. 向系统注册、注销块设备;

2. 增加一个处理加载模块时,处理输入参数的函数;

3. 在程序中使用内存分配的函数;

4. 创建一个线程,并在线程中运用信号量、自旋锁、原子变量;

5. 添加一个块设备,可读写,并运用radixtree实现内存存储块设备的数据功能。

通过实现以上几点,主要理解与学习:

1. 内核块设备驱动向系统注册、注销的接口

2. 从page cache到块设备的调用流程

3. bio的使用方法,软中断中编程的注意事项

4. 信号量、自旋锁、原子变量的使用方法

5. 内存分配和使用方法

6. 内核线程的使用方法

7. radixtree和链表的使用方法

参考书籍               


《LINUX设备驱动程序-第三版》

《深入理解LINUX内核-第三版》


数据结构实现


块设备信息数据结构


struct test_block_dev {

        unsigned int major;

        char name[NAME_LEN];

        spinlock_t lock;

        struct request_queue *queue;

        struct gendisk *disk;

        unsigned long long blkdev_size;

};

结构说明:

major:  主设备号

name:   设备名称

lock:     自旋锁

queue:  请求队列

disk:     gendisk结构描述符

blkdev_size:设备大小

块设备操作数据结构


struct block_device_operationsblkdev_fops = {

        .getgeo = blkdev_getgeo,

        .owner = THIS_MODULE,

};  

结构说明:定义了块设备的操作集合。其中getgeo指向“获得块设备物理结构”函数blkdev_getgeo,主要是根据指定申请的内存大小值,设置结构hd_geometry的磁头数和每磁道扇区数,通过此设置,可以让我们支持分更多的区。


核心线程数据结构


struct test_block_thread {

        unsigned long flags;

 

        spinlock_t thread_lock;

        atomic_t thread_active;

        struct completion thread_status;

        wait_queue_head_t thread_wait;

        struct task_struct *thread_d;

};

结构说明:

       flag:                   标识符

       thread_lock:        线程自旋锁

       thread_active:      原子变量,表示线程的状态

       thread_status:      completion结构,用于启动线程和结束线程操作间的切换

       thread_wait:        线程的等待队列

       thread_d:            描述进程的结构


模块初始化




 

其中几个主要函数getparam、thread_start、add_disk_init以及初始化结构部分、注册设备部分都会在(3.3 详细设计)中说明。


模块卸载



 

其中删除块设备函数add_disk_exit执行的步骤如下:

       del_gendisk(devs->disk);

       free_diskmem();

       put_disk(devs->disk);

       blk_cleanup_queue(devs->queue);

删除进程的函数thread_stop执行的步骤如下:

       test_and_set_bit(THREAD_STOP,&thread->flags);

       wake_up(&thread->thread_wait);

       wait_for_completion(&thread->thread_status);

       //中断,等待线程执行完毕

       thread->thread_d= NULL;

       memset(thread,0, sizeof(struct test_block_thread));

       kfree(thread);

       //释放掉给结构test_block_thread的指针thread申请的内存空间

 

模块加载参数


在加载模块时,允许输入一个参数size,size的值为一个字符串,字符串的格式为:数字+字母[G/M/K/B]

通过在主程序中添加函数module_param_named(size, value, charp, perm)来实现。其中参数perm我们使用的是S_IRUGO,这个参数代表的是所有人可以读取,但不能改变。当然也可以设置其他参数,既可以让其他人读也可以让其他人改。

使用函数int getparam(char *param_size)将按照传入的size值转换成sector的个数,最终将此值返回给全局变量blkdev_bytes,而如果不传入参数,此值默认为16M。


程序说明


申请内存空间,在这里使用函数kmalloc,具体操作如下:

devs =kmalloc(sizeof(struct test_block_dev), GFP_KERNEL);

memset(devs,0, sizeof(struct test_block_dev));

总结学习

在这里学习了内存分配的几个函数,分别有:kmalloc、vmalloc、__get_free_page、__alloc_pages(),以下分别说明这几个函数:

1. kmalloc分配连续的物理地址,用于小内存分配,可能会休眠

2. __get_free_page分配连续的物理地址,用于整页分配

3. vmalloc分配连续的虚拟地址

4. __alloc_pages分配连续的物理地址外,用于整页分配

其中__alloc_pages可以用于申请高端内存;vmalloc与kmalloc申请的地址空间范围也不一样,而且vmalloc的处理时间要比kmalloc长。

实际上,kmalloc与__get_free_page返回的内存地址也是虚拟地址,需要经过内存管理单元处理才能转化为物理内存地址。

vmalloc函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。其不但获取内存,还要建立页表,因此其开销也比kmalloc和__get_free_page大。


注册块设备,代码使用如下:

       register_blkdev(_major,BLKDEV_NAME)

此函数返回值小于0,则失败;其他情况下,当_major为0,则返回值为新动态申请到的主设备号,_major不为,主设备号就为_major。


创建线程


函数名称为thread_start(),具体说明如下:

1. 在函数的开始,先初始化test_block_thread结构:

thread = kmalloc(sizeof(struct test_block_thread),GFP_KERNEL);

memset(thread, 0, sizeof(struct test_block_thread));

 

clear_bit(THREAD_STOP, &thread->flags);

clear_bit(THREAD_BUSY, &thread->flags);

atomic_set(&thread->thread_active, 0);

spin_lock_init(&thread->thread_lock);

init_waitqueue_head(&thread->thread_wait);

init_completion(&thread->thread_status);

此部分主要学习了自旋锁、原子变量的使用。

2. 创建线程,代码如下:

thread->thread_d = kthread_run(thread_run, thread,THREAD_NAME);

使用函数kthread_run创建并启动一个线程。

这里thread_run()函数描述的就是进程的代码,后面将具体说明。

3. 失败则退出,成功则运行下面代码:

wait_for_completion(&thread->thread_status);

与此函数对应的函数为complete(&thread->thread_status),当程序在执行至wait_for_completion(&thread->thread_status)时,将会中断等待complete(&thread->thread_status)后面的程序运行完毕,再返回中断点继续运行代码。这两个程序是有对应的关系的,主要用于进程间的调用等操作。

在实际环境中,在模块卸载时,可能线程在队列中处于等待状态,使用kthread_stop将不得不等待该线程运行才能回收。

这时可以设置时线程触发结束操作的标识位,然后在适当的位置运用complete以及wait_for_completion,就可以成功的结束掉线程。

4. 进程创建结束


线程运行流程


下面将对线程函数thread_run()具体说明,看流程图:

 

可以看到,程序中主要通过原子变量来控制进程的运行情况,其中也包含程序的唤醒操作。

在进程中,因为处于多线程环境中,所以锁的使用就会很重要,对于一些重要的操作,都需要加锁来限制对共享资源的处理,所以在程序的进程部分代码中,也加入了自旋锁的使用。

总结学习

通过添加进程,主要学习了原子变量、信号量、自旋锁的使用以及进程的创建方法。

1. 信号量主要用于互斥模式,主要有以下注意内容:

a)  要使用信号量,代码中必须包括(asm/semaphore.h)

b)  初始化name的信号量为1(DECLARE_MUTEX(name))或者0(DECLARE_MUTEX_LOCKED(name))

c)  有PV操作。P函数被称为down,指减少了信号量的值,也需会将调用者置于休眠状态,但是down的一个版本down_trylock(structsemaphore *name)不会休眠,如果信号量在调用时不可获得,就会返回一个非零值;V函数被称为up,指增加信号量的值

d)  任何拿到信号量的线程必须通过一次(只有一次)对up的调用释放该信号量

e)  如果在拥有信号量时发生错误,必须将错误状态返回给调用者前释放信号量

2. 原子变量在共享资源是简单的整数时,通过位操作来控制标志符的值

3. 自旋锁有如下几个特点:

a)  自旋锁不能休眠

b)  任何拥有自旋锁的代码都必须是原子的

c)  自旋锁必须在可能的最短时间内拥有

d)  在拥有锁时,需禁止本地CPU中断

e)  自旋锁和信号量不同,自旋锁可以在不能休眠的代码中使用, 比如: 中断处理例程。自旋锁通常可以提供比信号量更高的性能,但是调用自旋锁的开销也较大


块设备添加


设备的添加,其中主要包含了结构request_queue和结构gendisk的处理,在程序中这些都封装在函数add_disk_init内,具体程序流程如下:

 

从流程中可以看到,处理io请求的函数blkdev_make_request与申请内存空间的函数alloc_diskmem的流程都没有具体体现出来,下面将会分别说明。

函数说明:

函数allock_diskmem

用于申请内存空间,程序中默认申请16M的内存空间,也可以通过指定参数size=数字+字母(G/M/K/B)来设置。

对于申请的内存空间,可以用数组、radix tree、链表来管理。

 

其中radix tree实现如下(数组和链表实现读者可以作为练手方法):

1. 初始化radix tree

INIT_RADIX_TREE(&blkdev_data,GFP_KERNEL)

2. 按照一次申请的连续的页数,然后通过指定的内存大小计算出总页数,执行循环

for (i = 0; i <pages;i++) {

        page = alloc_pages(GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,ORDER);

        //失败则free_diskmem(),然后退出

        radix_tree_insert(&blkdev_data, i, page)

        //这里blkdev_data为申请的内存块大小失败则调用__free_pages(page, ORDER)和free_diskmem(),然后退出

}

3. 退出

函数free_diskmem()为释放申请的内存空间的函数,其实现步骤如下:

for(i = 0; i <pages; i++) {

    page =radix_tree_lookup(&blkdev_data, i)

    //找到所有数据在radix tree中的位置

    radix_tree_delete(&blkdev_data,i)

    //删除掉radix tree 中的数据内容

    __free_pages(page, ORDER)

}

2. 退出

通过上面的代码,可以看到,在使用radix tree时:

(1)      首先要声明:struct radix_tree_root blkdev_data;

(2)      初始化:  INIT_RADIX_TREE(&blkdev_data, GFP_KERNEL)

(3)      使用方式有查找、添加、删除:

radix_tree_lookup(&blkdev_data,index); //查找

radix_tree_insert(&blkdev_data,index, page); //添加

radix_tree_delete(&blkdev_data,index);//删除

 

函数blkdev_make_request

用于 处理bio请求,程序的核心代码如下:

dsk_offset = bio->bi_sector << 9;

bio_for_each_segment(bvec, bio, i) {

       iovec_mem =kmap(bvec->bv_page) + bvec->bv_offset;

       //为了能够找到下一个读/写的位置,每次读取成功都要加上上次读/写的长度

       //错误返回bio_endio(bio,0, -EIO);退出

       blkdev_deal(dsk_offset, iovec_mem,bvec->bv_len, dir)

       //错误返回bio_endio(bio, 0, -EIO);退出

       kunmap(bvec->bv_page);

       dsk_offset += bvec->bv_len;

}

bio_endio(bio,bio->bi_size, 0);

return0;

可以看到,为了使流程更为清晰,调用了函数blkdev_deal,其参数dir为根据bio_rw(bio)获取到的请求类型的一个标识。

读操作时dir=0,写操作时dir=1。

 

函数blkdev_deal

    核心代码为:

       //blkdev_deal(unsignedlong long dsk_offset, void *buf, unsigned int len, int dir)

       done_cnt= 0;

       while(done_cnt < len) {

              this_off= (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK;

              this_cnt= min(len - done_cnt, (unsigned int)SIMP_BLKDEV_DATASEGSIZE -this_off);

              mutex_lock(&blkdev_datalock);

              //加了个互斥锁,以免读/写操作同时进行radix tree的操作

              this_first_page= radix_tree_lookup(&blkdev_data,

                     (dsk_offset+ done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);

              blkdev_deal_oneseg(this_first_page,this_off, buf + done_cnt, this_cnt, dir);

              done_cnt+= this_cnt;

              mutex_unlock(&blkdev_datalock);

       }

       在这里,将所有的准备工作做好,在函数blkdev_deal_oneseg中处理请求。

 

函数blkdev_deal_oneseg

       // blkdev_deal_oneseg(structpage *start_page,unsigned long offset, void *buf, unsigned int len, int dir)

       done_cnt= 0;

       while(done_cnt < len) {

              this_page= start_page + ((offset + done_cnt) >> PAGE_SHIFT);

              this_off  = (offset + done_cnt) & ~PAGE_MASK;

              this_cnt  = min(len - done_cnt, (unsigned int)PAGE_SIZE- this_off);

              dsk_mem= kmap(this_page);

              dsk_mem+= this_off;

              if(!dir)

                     memcpy(buf+ done_cnt, dsk_mem, len);  //读      

              else

                     memcpy(dsk_mem,buf + done_cnt, len);   //写 

              kunmap(this_page);

              done_cnt+= this_cnt;

       }

       最终调用函数memcpy将数据读出或者写入。


总结学习

blkdev_make_request函数主要是处理io请求,通过使用结构bio来实现。

首先了解下bio:

1. bio对应块设备上一段连续空间的请求,bio中包含的多个bio_vec用来指出这个请求对应的每段内存。

2. bio请求的块设备起始扇区和扇区数存储在bio.bi_sector和bio.bi_size中

通过了解了bio,我们就可以知道了在函数blkdev_make_request中:

a)  首先通过bio.bi_sector获得这个bio请求在我们的块设备内存中的起始部分位置,存入dsk_mem。

b)  然后遍历bio中的每个bio_vec,使用系统提供的bio_for_each_segment宏。

3. 通过函数bio_rw(bio)来确定当前的io请求类型。

在函数中我们也用到了kmap和kunmap两个函数,这两个函数是对应的关系。

bio_vec中的内存地址是使用page *描述的,这也意味着内存页面有可能处于高端内存中而无法直接访问。这种情况下,常规的处理方法是用kmap映射到非线性映射区域进行访问,当然,访问完后使用kunmap把映射的区域还回去。


这里有一个疑问,bio结构中的值哪里来的?报着这个疑问,学习了pagecache到块设备的调用流程,以读操作为例:

1. 用户空间有一个读操作,经过vfs层的时候确定出cache对应的page区域

2. 调用find_get_page(),通过页的index查找该页的描述符

3. 如果该页在页缓存中且是最新的,则直接调用函数file_read_actor()将数据传输到用户空间;如果不存在,先申请一个page,再从磁盘中读取数据填充该页,最后调用mpage_alloc()创建一个bio,然后bio_add_page()将页添加到bio中

通过上面的一系列操作,可以了解,在函数blkdev_make_request中,bio结构中数据已经存在了。


另外,程序结束的时候,调用了bio_endio函数,这个函数通知bio结构的创建者,指出完成,或者失败。这是一个异步处理,属于软中断,下面又学习了软中断:多数软中断是由系统的定时器和io回调造成的,代码需要在软中断上下文中运行,因此不能执行休眠或调度,也不可以使用能够引起中断或者睡眠发声的操作,如:

1. kmalloc、信号量可以引起休眠,不能使用

2. current指针在原子模式下,不可用,也没有任何意义,因为相关代码和被中断的进程没有任何关联。

3. 不允许访问用户空间。

如果判断自己是否正运行于中断上下文,可以使用函数in_interrupt()。


代码测试  


测试过程主要为以下几个步骤:

1. 编译代码make

2. 加载模块insmodblock.ko

3. mkfs.ext3/dev/test_block

4. mount/dev/test_block /mnt

5. cpfile1 file2 /mnt

6. 从目录/mnt下读文件

7. umount/mnt

8. rmmod modblock

9. 查看dmesg内容,看打印信息


遇见的问题  


1. 模块加载初始化函数__init时,需要有非空返回值,而退出模块__exit,需要返回空值

2. 编写块设备注册与注销接口时,对一个结构没有申请内存空间即使用,导致kernelpanic产生

3. 使用自旋锁,需要先对锁初始化,不然会导致kernelpanic

4. 对程序运行出错,而进行错误处理时,需要考虑在错误发生前已经做了什么操作,如:已经申请的空间,错误发生时,需要释放掉该空间。还需要注意在初始化程序完成时完成时,千万不能让其运行错误部分的处理程序,应该直接退出,不然,当加载设备成功后,再卸载掉该模块时会遇到kernelpanic

5. 在编译时,出现错误warning: assignmentfrom incompatible pointer type,经过检查为结构体使用错误导致。
   程序中struct request_queue_t *queue 应该修改为struct request_queue *queue 或者request_queue_t *queue

6. 在按照实现功能拆分代码文件的过程中,编译时,出现变量、函数没有定义就使用的情况。

      问题发生原因:

      原来所有的函数和变量都在一个文件中,但是在拆分过程中,部分变量或者函数的使用在某个拆分文件中,但是定义却在其他文件中,导致出现此种情况

      问题解决办法:

      需要将多文件使用的函数和变量定义在某个公用的.h文件中,都去引用此文件。另外一些变量的定义在.h文件中如:externunsigned long long blkdev_bytes ,如果在某个.c文件中使用,必须先实现再使用

7. 在.h文件里定义一个结构时,如果结束后不加分号(;)将会出错误:twoor more data types in declaration specifiers

8.在声明语句如DECLARE_WAITQUEUE(wait,current)前不能存在任何非声明的代码,比如不能存在打印信息之类的代码

9. 在修改程序时不小心将添加设备部分的devs->disk->fops= &blkdev_fops删掉,导致在加载设备后,设备的blkdev_make_request函数不能工作。
   当执行命令mkfs.ext3/dev/test_block时,出现错误:mkfs.ext3: No such device oraddress while trying to determine filesystem size。

10. 在执行回收操作时,需要保证回收的对象不能为空。

阅读(3744) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~