分类: 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. 在执行回收操作时,需要保证回收的对象不能为空。