+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 如果你的linux系统是x86平台,并且内存大于896M,那么恭喜你,我们大概可以在这个实验中搞坏你的系统。 反之如果你的系统不符合这些条件,也不用为无法搞坏系统而感到失望,本章的内容同样适合你。 这时作者自然也要申明一下对读者产生的任何损失概不负责, 因为这年头一不小心就可能差点成了被告,比如南京的彭宇和镇江花山湾的小许姑娘。 在实验看到的情况会因为系统的实际状况不同而稍有区别,但我们需要说明的问题倒是相似的。 但希望读者不要把这种相似理解成了ATM机取款17.5万和贪污2.6亿在判决上的那种相似。 首先我们来看看目前系统的内存状况: # cat /proc/meminfo MemTotal: 1552532 kB MemFree: 1529236 kB Buffers: 2716 kB Cached: 10124 kB SwapCached: 0 kB Active: 8608 kB Inactive: 7664 kB HighTotal: 655296 kB HighFree: 640836 kB LowTotal: 897236 kB LowFree: 888400 kB SwapTotal: 522104 kB SwapFree: 522104 kB Dirty: 44 kB Writeback: 0 kB AnonPages: 3440 kB Mapped: 3324 kB Slab: 2916 kB SReclaimable: 888 kB SUnreclaim: 2028 kB PageTables: 272 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 1298368 kB Committed_AS: 10580 kB VmallocTotal: 114680 kB VmallocUsed: 392 kB VmallocChunk: 114288 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 4096 kB DirectMap4k: 12288 kB DirectMap4M: 905216 kB # 输出很多,但我们只关心这几行: MemFree: 1529236 kB --这说明系统中有接近1.5G的空闲内存 HighFree: 640836 kB --这说明空闲内存中,处在高端的有600M左右 LowFree: 888400 kB --这说明空闲内存中,处在低端的有800M左右 现在加载上一章完成的模块,我们指定创建800M的块设备: # insmod simp_blkdev.ko size=800M # 成功了,我们再看看内存状况: # cat /proc/meminfo MemFree: 708812 kB HighFree: 640464 kB LowFree: 68348 kB ... # 我们发现高端内存没怎变,低端内存却已经被耗得差不多了。 我们一不做二不休,继续加大块设备的容量,看看极限能到多少: # rmmod simp_blkdev # insmod simp_blkdev.ko size=860M # cat /proc/meminfo MemFree: 651184 kB HighFree: 641972 kB LowFree: 9212 kB ... # 系统居然还没事,这时虽然高端内存还是没怎么变,但低端内存剩下的得已经很可怜了。 然后进一步加大块设备的容量: # rmmod simp_blkdev # insmod simp_blkdev.ko size=870M ... 这里不用再cat /proc/meminfo了,因为系统已经完蛋了。 如果有些读者嗜好独特,对出错信息情有独钟的话,在这里也满足一下: kernel: [ 3588.769050] insmod invoked oom-killer: gfp_mask=0x80d0, order=2, oomkilladj=0 kernel: [ 3588.769516] Pid: 4236, comm: insmod Tainted: G W 2.6.27.4 #53 kernel: [ 3588.769868] [<c025e61e>] oom_kill_process+0x42/0x183 kernel: [ 3588.771041] [<c025ea5c>] out_of_memory+0x157/0x188 kernel: [ 3588.771306] [<c0260a5c>] __alloc_pages_internal+0x2ab/0x360 kernel: [ 3588.771500] [<c0260b25>] __get_free_pages+0x14/0x24 kernel: [ 3588.771679] [<f8865204>] alloc_diskmem+0x45/0xb5 [simp_blkdev] kernel: [ 3588.771899] [<f8867054>] simp_blkdev_init+0x54/0xc6 [simp_blkdev] kernel: [ 3588.772217] [<c0201125>] _stext+0x3d/0xff kernel: [ 3588.772393] [<f8867000>] ? simp_blkdev_init+0x0/0xc6 [simp_blkdev] kernel: [ 3588.772599] [<c0235f2f>] ? __blocking_notifier_call_chain+0x40/0x4c kernel: [ 3588.772845] [<c0241771>] sys_init_module+0x87/0x19d kernel: [ 3588.773250] [<c02038cd>] sysenter_do_call+0x12/0x21 kernel: [ 3588.773884] ======================= kernel: [ 3588.774237] Mem-Info: kernel: [ 3588.774241] DMA per-cpu: kernel: [ 3588.774404] CPU 0: hi: 0, btch: 1 usd: 0 kernel: [ 3588.774582] Normal per-cpu: kernel: [ 3588.774689] CPU 0: hi: 186, btch: 31 usd: 0 kernel: [ 3588.774870] HighMem per-cpu: kernel: [ 3588.778602] CPU 0: hi: 186, btch: 31 usd: 0 ... 搞坏系统就当是交学费了,但交完学费我们总要学到些东西。 虽然公款出国考察似乎已经斯通见惯,但至少在我们的理解中,学费不是旅游费,更不是家属的旅游费。 我们通过细心观察、周密推理后得出的结论是: 目前的块设备驱动程序会一根筋地使用低端内存,即使系统中低端内存很紧缺的时候, 也会直道把系统搞死却不去动半点的高端内存,这未免也太挑食了, 因此在本章和接下来的几章中,我们将帮助驱动程序戒掉对低端内存的瘾。 相对高端内存而言,低端内存是比较宝贵的,这是因为它不需要影射就能直接被内核访问的特性。 而内核中的不少功能都直接使用低端内存,以保证访问的速度和简便, 但换句话来说,如果低端内存告急,那么系统可能离Panic也不远了。 因此总的来说,对低端内存的使用方法大概应该是:除非有足够理由,否则就别乱占着。 详细来说,就是: 1:不需要使用低端内存的“在内核中不需要映射就能直接访问”这个特性的功能,应该优先使用高端内存 如:分配给用户态进程的内存,和vmalloc的内存 2:需要占用大量内存的功能,并且也可以通过高端内存实现的,应该优先使用高端内存 如:我们的程序 与内存有关的知识我们在以前的章节中已经谈到,因此这里不再重复了, 但需要说明的是在高端内存被映射之前,我们是无法通过指针来指向它的。 因为它不在内核空间的地址范围以内。 虽然如此,我们却无论如何都需要找出一种方法来指定一个没有被映射的高端内存, 这是由于至少在进行映射操作时,我们需要指定去映射谁。 这就像为一群猴子取名的时候,如何来说明是正在给哪只猴子取名一样。 虽然给猴子取名的问题可能比较容易解决,比如我们可以说, 给哪只红屁股的公猴取名叫齐天大圣、给那只瘦瘦的母猴取名叫白晶晶, 但可惜一块高端内存即没有红屁股,又没有胖瘦之分, 它们唯一有的就是地址,因此我们也必须通过地址来指定这段高端内存。 刚才说过,在高端内存被映射之前,他在内核的地址空间中是不存在的, 但虽然如此,它至少存在其物理地址,而我们正是可以通过它的物理地址来指定它。 是的,本质上是这样的,但在linux中,我们还需要再绕那么一丁点: linux在启动阶段为全部物理内存按页为单位建立了的对应的struct page结构,用来管理这些物理内存, 也就是,每个页的物理内存,都有着1对1的struct page结构,而这些struct page结构是位于低端内存中的, 我们只要使用指向某个struct page结构的指针,就能指定物理内存中的一个页。 因此,对于没有被映射到内核空间中的高端内存,我们可以通过对应的struct page结构来指定它。 (如果读者希望了解更详细的知识,可以考虑从virt_to_page函数一路google下去) 我们在这里大肆谈论高端内存的表示方法,因为这是让我们的模块使用高端内存的前提。 我们的驱动程序使用多段内存来存储块设备中的数据。 原先的程序中,我们使用指向这些内存段的指针来指定这些数据的位置,这是没有问题的, 因为当时我们是使用__get_free_pages()来申请内存,__get_free_pages()函数只能用来申请低端内存, 因为这个函数返回的是申请到的内存的指针,而上文中说过,高端内存是不能用这样的指针表示的。 要申请高端内存,明显不能使用这样的函数,因此我们隆重介绍它的代替者出场: struct page *alloc_pages(gfp_t gfp_mask, unsigned int order); 这个函数的参数与__get_free_pages()相同,但区别在于,它返回指向struct page的指针, 这个我们在上文中介绍过的指针赋予了alloc_pages()函数申请高端内存的能力。 其实申请一块高端内存并不难,只要使用__GFP_HIGHMEM参数调用alloc_pages()函数, 就可能返回一块高端内存,之所以说是“可能”,使因为在某些情况下,比如高端内存不够或不存在时,也会但会低端内存充数。 我们的现在的目标是让驱动程序使用高端内存,这需要: 1:让驱动程序申请高端内存 2:让驱动程序使用高端内存 但在这一章中,我们要做的即不是1,也不是2,而是1之前的准备工作。 因为1和2必须一气呵成地改完,而为了让一气呵成的时候不要再面临其他插曲, 我们需要做好充足的准备工作,就像ml前尿尿一样。 对应到程序的修改工作上,我们打算先让程序使用struct page *来指定申请到的内存。 要实现这个目的,我们先要改申请内存的函数,也就是alloc_diskmem()。 刚才我们介绍过alloc_pages(),现在就要用它了: 首先把函数中定义的 void *p; 改成 struct page *page; 因为我们要使用struct page *来指定申请到的内存,而不是地址了。 然后把 p = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER); 改成 page = alloc_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER); 这一行改动的原因大概已经说得很详细了。 还有那个if(!p)改成if (!page) 然后就是把指针加入基树的那一行: ret = radix_tree_insert(&simp_blkdev_data, i, p); 改成 ret = radix_tree_insert(&simp_blkdev_data, i, page); 由于我们使用了struct page *来指定申请到的内存,因此错误处理部分也要小改一下: free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER); 改成 __free_pages(page, SIMP_BLKDEV_DATASEGORDER); 这里补充介绍一下__free_pages()函数,可能大家已经猜到其作用了, 其实与我们原先使用的free_pages()函数相似,都是用来释放一段内存, 但__free_pages()使用struct page *来指定要释放的内存,这也意味着它能够用来释放高端内存。 大家应该已经发现我们虽然改用alloc_pages()函数来申请内存,但并没有指定__GFP_HIGHMEM参数, 这时申请到的仍然是低端内存,因此避免了在这一章中对访问内存那部分代码的大肆改动。 改动过的alloc_pages()函数是这样的: int alloc_diskmem(void) { int ret; int i; struct page *page; INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { page = alloc_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER); if (!page) { ret = -ENOMEM; goto err_alloc; } ret = radix_tree_insert(&simp_blkdev_data, i, page); if (IS_ERR_VALUE(ret)) goto err_radix_tree_insert; } return 0; err_radix_tree_insert: __free_pages(page, SIMP_BLKDEV_DATASEGORDER); err_alloc: free_diskmem(); return ret; } 相应的,释放内存用的free_diskmem()函数也需要一些更改, 为了避免有人说作者唐僧,列出修改后的样子应该已经足够了: void free_diskmem(void) { int i; struct page *page; for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { page = radix_tree_lookup(&simp_blkdev_data, i); radix_tree_delete(&simp_blkdev_data, i); /* free NULL is safe */ __free_pages(page, SIMP_BLKDEV_DATASEGORDER); } } 随后是simp_blkdev_make_request()函数: 首先我们不是把void *dsk_mem改成struct page *dsk_page,而是增加一个 struct page *dsk_page; 变量,因为在访问内存时,我们还是需要用到dsk_mem变量的。 然后是从基数中获取指针的代码,把原先的 dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); 改成 dsk_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); 虽然看起来没什么太大变化,但我们需要知道,这时基树返回的指针已经不是直接指向数据所在的内存了。 还有那个判断是否从基树中获取成功的 if (!dsk_mem) { 用脚丫子也能想得出应该改成这样: if (!dsk_page) { 还有就是我们需要首先将struct page *dsk_page地址转换成内存的地址后,才能对这块内存进行访问。 这里我们使用了page_address()函数。 这个函数可以获得struct page数据结构所对应内存的地址。 这时可能有读者要问了,如果这个struct page对应的是高端内存,那么如何返回地址呢? 实际上,这种情况下如果高端内存中的页面已经被映射到内核的地址空间,那么函数会返回映射到内核空间中的地址, 而如果没有映射的话,函数将返回0。 对于我们目前的程序而言,由于使用的是低端内存,因此struct page对应的内存总是处于内核地址空间中的。 对应到代码中,我们需要在使用dsk_mem之前,也就是 dsk_mem += (dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK; 这条语句之前,让dsk_mem指向struct page *dsk_page对应的内存的实际地址。 这是通过如下代码实现的: dsk_mem = page_address(dsk_page); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": get page's address failed: %p\n", dsk_page); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif 总的来说,修改后的simp_blkdev_make_request()函数是这样的: static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { struct bio_vec *bvec; int i; unsigned long long dsk_offset; if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + bio->bi_size > simp_blkdev_bytes) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": bad request: block=%llu, count=%u\n", (unsigned long long)bio->bi_sector, bio->bi_size); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT; bio_for_each_segment(bvec, bio, i) { unsigned int count_done, count_current; void *iovec_mem; struct page *dsk_page; void *dsk_mem; iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; count_done = 0; while (count_done < bvec->bv_len) { count_current = min(bvec->bv_len - count_done, (unsigned int)(SIMP_BLKDEV_DATASEGSIZE - ((dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK))); dsk_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); if (!dsk_page) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": search memory failed: %llu\n", (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_mem = page_address(dsk_page); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": get page's address failed: %p\n", dsk_page); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif } dsk_mem += (dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK; switch (bio_rw(bio)) { case READ: case READA: memcpy(iovec_mem + count_done, dsk_mem, count_current); break; case WRITE: memcpy(dsk_mem, iovec_mem + count_done, count_current); break; default: printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": unknown value of bio_rw: %lu\n", bio_rw(bio)); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } count_done += count_current; } kunmap(bvec->bv_page); dsk_offset += bvec->bv_len; } #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, bio->bi_size, 0); #else bio_endio(bio, 0); #endif return 0; } 通过对这3个函数的更改,代码可以使用struct page *来定位存储块设备数据的内存了。 这也为将来使用高端内存做了一部分准备。 因为本章修改的代码在外部功能上没有发生变动,所以我们就不在这里尝试编译了运行代码了。 不过感兴趣的读者不妨试一试这段代码能不能进行编译和会不会引起死机。 <未完,待续> |