日志标签 ‘模块编程’

边学边实践:打印VFS中的结构体

2010年11月27日

学习了VFS的基本原理,我们很有必要对这些理论知识进行验证和实践。本文所分析的几个小程序将更具体、直观的展现VFS中一些数据结构之间的逻辑关系。

1.打印超级块和索引结点

通过前面的分析我们知道,系统中所有的超级块都内嵌一个struct list_head类型的字段s_list。通过该字段将系统中所有的超级块链接成一个双联表。因此,如果我们知道这个双联表的头指针以及了解相关便利宏的使用方法,那么我们就可以遍历整个系统中的所有超级块了。
为了解释方便,我们将内嵌的list_head结构体称为内部结构体;将super_block结构体称为外部结构体;

具体的,我们可以通过 list_for_each宏来遍历一个list_head类型的双联表。该宏的定义如下:

 364#define list_for_each(pos, head) \
 365        for (pos = (head)->next; prefetch(pos->next), pos != (head); \
 366                pos = pos->next)

使用该宏时,需要向head参数中传递要遍历双联表的头指针;而每次遍历得到的list_head类型的结点地址会保存在pos这个参数中。不过,这个宏只能遍历内嵌于超级块中的那个list_head结构的链表,并不能得到正在被遍历的那个超级块的地址(也就是指向当前正被遍历的超级块的指针)。也就是说,每次遍历时并不能得到超级块中的其他字段。因此,还应该使用 list_entry宏。该宏通过指向list_head结点的地址来得到外部超级块的首地址。

345#define list_entry(ptr, type, member) \
346        container_of(ptr, type, member)

这个宏的第一个参数是指向内部结构体list_head的指针,第二个参数是外部结构体的类型,第三个参数是list_head类型的变量在外部结构体中的名称。这个宏最后会返回指向当前外部结构体的指针。比如,在super_block结构体中,list_head结构类型的字段名称为s_list,因此可以如下使用该宏:

sb = list_entry(pos, struct super_block, s_list);

对于超级块形成的双联表来说,它的头指针是super_blocks。但是很遗憾,super_blocks这个变量并没有被导出。所谓导出,就是通过EXPORT_SYMBOL将某个函数或者变量对全部内核代码公开。也就是说,使用 EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用 。为了解决这个问题,我们可以在包含super_blocks的这个文件中将这个变量导出,并且重新编译内核。对于我们这里的这个小程序而言,这样做有些不值得。幸好,在/proc/kallsyms文件中,记录了内核中所有符号以及符号的地址。因此,在该文件中查找相应符号就可以得到其地址。

我们使用下述两条命令:

edsionte@edsionte-desktop:~/code/vfs/print_sb$ grep super_blocks /proc/kallsyms
c0772a30 D super_blocks
edsionte@edsionte-desktop:~/code/vfs/print_sb$ grep " sb_lock" /proc/kallsyms
c08c9d60 B sb_lock

就可以得到super_blocks变量的地址。另外,sb_lock超级块对应的自旋锁。

上述都准备好后,我们就可以进行遍历了。关键代码如下:

#define SUPER_BLOCKS_ADDRESS 0xc0772a30
#define SB_LOCK_ADDRESS 0xc08c9d60

static int __init my_init(void)
{
struct super_block *sb;
struct list_head *pos;
struct list_head *linode;
struct inode *pinode;
unsigned long long count = 0;

printk("print some fields of super blocks:\n");
spin_lock((spinlock_t *)SB_LOCK_ADDRESS);
list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){

sb = list_entry(pos, struct super_block, s_list);
printk("dev_t:%d,%d ",MAJOR(sb->s_dev),MINOR(sb->s_dev));
printk("fs_name:%s\n",sb->s_type->name);
printk("\n");
}

spin_unlock((spinlock_t *)SB_LOCK_ADDRESS);
printk("the number of inodes:%llu\n",sizeof(struct inode)*count);

return 0;
}

另外,需要注意的是,每次重启电脑后,都要重新查找上述两个变量的地址。

对于一个超级块中所有的inode,有专门一个链表将所有的inode链接起来。这个链表的头结点是超级块中的s_inode字段。而inode之间是其内部的i_sb_list字段进行链接的。了解了这些,我们可以在上述程序的基础上,再打印每个超级块中的所有inode:

	list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){

		sb = list_entry(pos, struct super_block, s_list);
		printk("dev_t:%d,%d ",MAJOR(sb->s_dev),MINOR(sb->s_dev));
		printk("fs_name:%s\n",sb->s_type->name);

		list_for_each(linode, &sb->s_inodes){

			pinode = list_entry(linode, struct inode, i_sb_list);
			count ++;
			printk("%lu\t",pinode->i_ino);
		}

		printk("\n");
	}

在上面代码的基础上,我们再加深一步。一个索引结点可能对应若干个dentry,这些dentry自身通过其内部的d_alias链接在一起;而整个链表的头结点是inode中的i_dentry字段。因此,根据上面的方法,我们可以在遍历每个inode的同时,继续遍历这个inode对应的所有dentry。部分代码如下:

	list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){
		sb = list_entry(pos, struct super_block, s_list);
		printk("dev_t:%d,%d ",MAJOR(sb->s_dev),MINOR(sb->s_dev));
		printk("fs_name:%s\n",sb->s_type->name);

		list_for_each(linode, &sb->s_inodes){
			pinode = list_entry(linode, struct inode, i_sb_list);
			count ++;
			printk("%lu[",pinode->i_ino);

			list_for_each(ldentry, &pinode->i_dentry){
				pdentry = list_entry(ldentry, struct dentry, d_alias);
				printk("%s->",pdentry->d_name.name);
			}
			printk("]\t");
		}
		
		printk("\n");
	}

上出程序的完整的代码在这里

2.打印文件类型结构体

同样的道理,通过下述的代码可以打印file_system_type结构体。

#define FILE_SYSTEM_ADDRESS 0xc08ca3a4 /* grep file_systems /proc/kallsyms */
#define FILE_SYSTEM_LOCK_ADDRESS 0xc0772de0 /* grep file_systems_lock /proc/kallsyms */

static int __init printfs_init(void)
{
	struct file_system_type **pos;

	printk("\n\nprint file_system_type:\n");

	read_lock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS);
	pos = (struct file_system_type **)FILE_SYSTEM_ADDRESS;

	while(*pos){
		printk("name: %s\n",(*pos)->name);
		pos = &((*pos)->next);
	}

	read_unlock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS);

	return 0;
}

更多的打印信息可以按照上述方法继续添加。开始吧!

makefile再学习

2010年9月27日

前几篇文章中,我们一起分析了字符设备驱动。假如我们已经编写好了驱动代码,那么接下来该如何做?

我们首先要进行make,编译成功后会生成一个globalmem.ko文件。接下来要将这个内核模块插入到内核当中,然后还要利用mknod命令生成一个设备文件节点。接下来我们再利用测试程序,对我们写好的驱动程序进行测试。

那么以上的工作都可以通过下面这个Makefile文件完成,直接在shell终端输入make就可编译这个内核模块,输入make clean就可以清除一些中间文件,输入make install就可以将编译好的内核模块插入到内核当中。更重要的是,这个Makefile文件具有很好的移植性。本文通过分析下面给出的Makefile文件,与大家一起更深入的学习Makefile文件的相关语法以及一些shell编程。

TARGET= globalmem

ifneq ($(KERNELRELEASE),)
 obj-m := $(TARGET).o
 $(TARGET)-objs := module.o global_fops.o
else
 KERNEL := $(shell uname -r)
 KDIR ?= /lib/modules/$(KERNEL)/build
 PWD := $(shell pwd)

.PHONY all
.PHONY clean
.PHONY install
.PHONY remove

all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	make -C $(KDIR) M=$(PWD) clean
install:
	@sudo ./$(TARGET).sh
remove:
	@sudo ./clean.sh
endif

这个Makefile文件(新Makefile)比这里的Makefile文件(旧Makefile)强大了很多。

1.条件语句

首先注意这个新的Makefile文件在逻辑结构上发生了很大的变化,采用了条件语句:ifneq-else-endif。这个条件语句是用来判断括号中逗号前后的两个变量是否不相等。ifneq之后为符合条件时所要执行的语句,相应的else之后为不符合条件时要执行的语句。上述Makefile文件中的ifneq ($(KERNELRELEASE),)是用来判断KERNELRELEASE变量是否为空,不为空则符合条件。

类似的还有下面的条件语句,只不过条件判断的类型不同。

ifeq-else-endif:如果两个变量相等,则满足条件。

下面两种条件语句中,条件判断处为变量名,是用来判断此变量是否被定义过。

ifdef-else-endif:如果变量被定义,满足条件。

ifndef-else-endif:如果变量未被定义,满足条件。

不过,上述两个条件语句所判断的变量定义没有递归性,比如下面例子:

path=
cur_path=$(path)
ifdef cur_path
right=yes
else
right=no
endif

这个例子中最终执行的是right=yes。虽然path为空,但是cur_path=$(path)却被认为是定义了cur_path变量。正如上面所说定义没有递归行。

2.变量赋值

在Makefile文件中定义一个变量的格式为:变量名 赋值符 变量值

赋值符通常有以下四种类型:=,:=,?=,+=。对于赋值符=与我们平日里使用的等号差不多,但是这里我们需要清除一个概念,那就是递归展开变量。为了更清除的说明上面的概念,请看下面的例子:

first=$(second)
second=$(third)
third=yes
all:
       echo $(first)

很显然结果为yes。当执行make时,first首先展开为second,接着second又展开成为third,再后来引用third的值即yes。可以看到first是递归展开而得到最后的yes值的。这便是我们刚才所谓的递归展开变量。

而与上述变量赋值符号不同的是,:=赋值符号是立即展开变量的,同样的例子,只不过这次我们使用:=赋值符:

first:=$(second)
second:=$(third)
third:=yes
all:
       echo $(first)

此时first为空。这是因为在定义first变量时就立即展开了second,因为second此时未定义。即便此句之后为second变量赋了值,但first的值为空。

另外两个赋值符号比较容易理解。首先+=赋值符是在变量原有值的基础上再增加新的值,而不是覆盖原有变量值。而?=赋值符首先会判断变量实现是否已经被赋值,只有之前未被赋值的变量此刻才能被赋值。

OK,了解了赋值符号的含义,那么再次看上述的Makefile文件,就会清晰很多。

3.伪目标

正如上述所言,直接在shell终端输入make就会执行目标all后的命令,这并不是all目标具有什么默认的效果。只不过在Makefile文件中,第一个目标总被认为是最终目标。因此可以想象到,当你交换一下all和clean的位置,直接执行make时会自动执行clean后面的命令。并且不一定总对第一个目标起名为all,你可以使用你喜欢的目标名(也许all是一种无声的约定 e43 )。

通常在clean这样的目标后都没有依赖文件,因为我们的目的是想让make执行这些目标后的命令。但是当Makefile文件所在目录下有一个名为clean的文件时,此时make clean就会被认为是生成clean目标文件。而clean后是没有任何依赖文件的,所以每次make clean后clean目标文件都会被认为是最新,而不去执行下面的命令,这虽然符合语法规则,但并不能达到我们使用clean的目的。

因此我们必须将clean这种目标定义成伪目标。定义方法为:.PHONY:all。这样不管该目录下是否有同名的文件都会执行clean后的命令。现在你应该明被为什么MAKEFILE文件中有这么多以.PHONY开头的目标文件了吧。

4.为什么要用makefile

内核模块化简单实用,但是编译却成了问题:有时候我们只是改动了某个文件的一小部分就不得不编译整个内核,这是个很可怕的事情。但是GNU make引入后,这个问题就迎刃而解了:make只会编译已被改动代码的文件,而不是将所有文件都编译。但是make具体如何对源文件进行编译,怎么编译?这个时候就需要makefile文件了。在之前的文章当中,我们对Makefile文件下过“编译规则”这样的定义,下面通过分析上面的Makefile文件,我们具体感受一下这个“编译规则”。

整个Makefile文件根据KERNELRELEASE的值来划分不同的编译规则(方式),这里的KERNELRELEASE只会在内核源码目录下显示当前内核的版本号。

一般情况下,我们编写的内核模块源文件所在的目录并非位于内核源码根目录(或其子目录)下。那么此时就不符合ifueq条件,即执行else语句下的编译规则。这种情况下,当我们输入make后,就会执行make -C $(KDIR) M=$(PWD) modules这条命令。注意这条命令后的modules,它表示将会编译所有在配置菜单中被选作模块编译的那些内容(也就是赋值给obj-m的那些目标)。接下来由于-C $(KDIR)参数的原因,make会转向内核源码根目录下去执行。根据M后的目录,编译我们写的内核模块源码,生成.o文件。接着联合一些中间文件生成.ko文件。这便是make生成整个内核目标文件的过程。这个过程可以在make之后在终端产生的一系列描述文字得到。

上面这种情况会将我们编写的内核模块源码编译成内核模块目标文件,接下来就是我们熟悉的内核模块插入了。不过当我们所写的内核模块文件处于内核源码目录下时,KERNELRELEASE就会非空(此时为版本号),那么此时就满足ifueq条件了。什么时候我们编写的内核模块源码会处在内核源码目录下?此时的内核编译是那种方式?

在前面的文章中我们假设已经写好了驱动代码,然后在Kconfig文件中为这个驱动编写配置选项。在配置菜单中有了此驱动的相关配置选项后,接下来用户可以选择是否会将此驱动源码一起编译进内核。那么此时Makefile文件的作用就是将此内核模块源码编译进内核(obj-m := $(TARGET).o)。不过注意,只是通知内核下次编译“带上我”,并没有实际编译。

现在应该明白整个Makefile文件的逻辑结构了吧?

Hello,Kernel!

2010年9月13日

学习内核模块编程,第一个小程序当然是hello,kernel!了,这应当算是一个惯例了。以前大三的时候在实验课上做过模块编程,记得当时还是许师兄带我们的实验,不过现在又忘了。晚上试了试,很快就运行成功了,不过还是出现了一些问题。现在将我的步骤记录如下,供和我一样的初学者学习。

1.首先编写hello.c文件

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
//必选
//模块许可声明
MODULE_LICENSE("GPL");
//模块加载函数
static int hello_init(void)
{
	printk(KERN_ALERT "hello,I am edsionte\n");
	return 0;
}
//模块卸载函数
static void hello_exit(void)
{
	printk(KERN_ALERT "goodbye,kernel\n");
}
//模块注册
module_init(hello_init);
module_exit(hello_exit);
//可选
MODULE_AUTHOR("edsionte Wu");
MODULE_DESCRIPTION("This is a simple example!\n");
MODULE_ALIAS("A simplest example");

通常一个模块程序的中,模块加载函数,模块卸载函数以及模块许可声明是必须有的,而象模块参数,模块导出符号以及模块作者信息声明等都是可选的。

我们编写了模块加载函数后,还必须用module_init(mode_name);的形式注册这个函数。因为当我们接下来用insmod加载模块时,内核会自动去寻找并执行内核加载函数,完成一些初始化工作。类似的当我们使用rmmod命令时,内核会自动去执行内核卸载函数。

请注意这里的printk函数,可以简单的理解为它是内核中的printf函数,初次使用很容易将其打成printf。

2.编写Makefile文件

记得大三,那时候实验课上接触到Makefile,只是按照书上的内容敲上去。不过有了上一周对Makefile相关语法的了解,现在看起来已经基本知道为什么要这么写了。那么下面我们看Makefile文件。

obj-m += hello.o
#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#complie object
all:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
#clean
clean:
	make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

首先第一句话指定要被编译的文件。其实Makefile中有这样一句话就可以了,但是这样的话每次make时都要加入其他命令,所以我们不妨就在Make中加入每次要执行的命令(脚本语言的功能体现出来了)。每次只要输入make命令即可。

我们首先获得当前的相对路径(你可以在终端输入pwd试一下这个命令),然后再获得当前内核的版本号,这样就可以直接获得当前内核的绝对路径。当然你可以直接输入当前内核版本,不过这样不方便移植,如果当前内核版本号与此文件中的版本号不同时,就得修改。所以上面的方法有很好的移植性,而且可读性也强。

这里会经常出现$(variable name)这样的字符串,其实这是对括号内变量的一种引用,具体可参考Makefile的相关语法规则。

3.make

完成上述两个文件后,在当前目录下运行make命令,就会生成hello.ko文件,即模块目标文件。

4.insmod,rmmod和dmesg

insmod命令可以使我们写的这个模块加入到内核中,但是一般我们要加上sudo。rmmod当然就是卸载这个模块了。我们在加载或卸载模块时都有一些提示语,即我们printk中显示的语句,这时候可以用dmesg命令来查看。

ok,第一个模块编程就这么简单,try一下!

Update 2011/04/03

本文所描述的程序在ubuntu系统下测试成功。其他的Linux发行版应适当修改源码目录,即修改LINUX_KERNEL_PATH。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key