首页 > 代码库 > 浅析Linux字符设备驱动程序内核机制

浅析Linux字符设备驱动程序内核机制

        前段时间在学习linux设备驱动的时候,看了陈学松著的《深入Linux设备驱动程序内核机制》一书。说实话,这是一本很好的书,作者不但给出了在设备驱动程序开发过程中的所需要的知识点(如相应的函数和数据结构),还深入到linux内核里去分析了这些函数或数据结构的原理,对设备驱动开发的整个过程和原理都分析的很到位。但可能是由于知识点太多,原理也比较深的原因,这本书在知识点的排版上跨度有些大,所以读起来显得有点吃力,但是如果第一遍看的比较认真的话,再回头看第二次就真的能够很好地理解作者的写作思路了。第二章字符设备驱动程序我也是看了两遍才理解过来,趁着这热度,就按照自己的思路总结一下,以便下次再看的话,就可以按照自己比较好理解的方式去看了。


1、字符设备驱动程序框架:

在深入讨论字符设备驱动程序之前,先给出一个设备驱动程序典型框架结构,以便于对字符设备驱动程序有个初步的理解。

<span style="background-color: rgb(255, 255, 255);">/*字符设备驱动程序源代码*/
/*demo_chr_dev.c*/
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/fs.h>
#include<linux/cdev.h>
</span>
static struct cdev chr_dev;//定义一个字符设备对象
static dev_t ndev;//字符设备节点的设备号

static int chr_open(struct inode *nd,struct file *filp)  //打开设备
{
	int major=MAJOR(nd->i_rdev);
	int minor=MINOR(nd->i_rdev);
	printk("chr_open,major=%d,minor=%d\n",major,minor);
	return 0;
}

static ssize_t chr_read(struct file *f,char __user *u,size_t sz,loff_t *off) //读取设备文件内容
{
	printk("In the chr_read() function!\n");
	return 0;
}

//关键数据结构
struct file_operations chr_ops=
{
	.owner=THIS_MODULE,
	.open=chr_open,
	.read=chr_read,
};

static int demo_init(void)  //模块初始化函数
{
	int ret;
	cdev_init(&chr_dev,&chr_ops);//初始化字符设备对象,chr_ops定义在上面
	ret=alloc_chardev_region(&ndev,0,1,"char_dev");//分配设备号
	if(ret<0)
		return ret;
	printk("demo_init():major=%d,minor=%d\n",MAJOR(ndev),MINOR(ndev));
	ret=cdev_add(&chr_dev,ndev,1);//将字符设备对象chr_dev注册到系统中
	if(ret<0)
		return ret;
	return  0;
}

static void demo_exit(void)
{
	printk("Removing chr_dev module...\n");
	cdev_del(&chr_dev);//将字符设备对象chr_dev从系统中注销
	unregister_chr_region(ndev,1);//释放分配的设备号
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");

编译后可以内核模块demo_chr_dev.ko

对驱动程序框架的总体理解:

(1)在linux系统中,各种设备都是以文件的形式存在,因此设备驱动程序包含了用于操作字符设备文件的函数,如打开,读、写等操作函数。如chr_open(),chr_read()等。这些函数都要由程序员自己实现。

(2)驱动程序中包含了类型为struct file_operations的结构体对象如chr_ops,该结构体对象用于存放针对字符设备的各种操作函数。

(3)设备驱动程序作为内核模块.ko安装到系统中,因此在程序框架中,必须要调用module_init()函数完成模块的安装;调用module_exit()函数完成模块的卸载。

(4)在模块初始化函数中完成字符设备对象的初始化,这个初始化过程中调用了程序员定义的数据结构chr_ops作为参数;同时在初始化函数中还完成了分配设备号,设备对象注册等工作。

(5)在模块的卸载函数中,会将对应的字符设备对象从系统中注销掉,并释放已分配的设备号。

2、字符设备驱动程序内核机制详解

为了更容易理解驱动程序,我们结合前一步中给出的框架驱动程序中对应的函数和数据结构进行分析与解释。

(1)结构体structfile_operations

该结构体定义在文件<include/linux/fs.h>中,具体如下: 

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, struct dentry *, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
};

可以看到struct file_operations的成员变量计划全是函数指针。现实中,字符设备驱动程序的编写,基本上是围绕着如何实现struct  file_operations中的那些函数指针成员而展开的。应用程序对文件类函数的调用如read()/open()等,在linux内核的机制下,最终会转到structfile_operations中对应的函数指针成员上。

(2)THIS_MODULE

这是个宏定义#defineTHIS_MODULE(&__this_module)

__this_module内核模块的编译工具链为当前模块产生 的struct module 类对象,所以THIS_MODULE实际上是当前模块对象的指针。

(3)字符设备的抽象struct cdev

字符设备驱动程序管理的核心对象是字符设备,内核为字符设备抽象出了一个具体的数据结构struct cdev,它定义在文件<include/linux/cdev.h>中,如下:

struct cdev {
	struct kobject kobj;				//内嵌的内核对象
	struct module *owner;				//驱动程序所在的内核模块对象指针
	const struct file_operations *ops;  //存放各种操作函数的结构体对象
	struct list_head list;				//字符设备链表
	dev_t dev;							//字符设备的设备号,由主设备号和次设备号组成
	unsigned int count;					//隶属于同一个主设备号的次设备号的个数
};

需要注意的是,内核引入struct cdev数据结构作为字符设备的抽象,仅仅是为了满足系统 对字符设备驱动程序框架结构设计的需要,现实中一个具体的字符硬件设备的数据结构可能更复杂,在这种情况下,struct cdev常常作为一种内嵌的成员变量出现在设备的数据结构中,如:

struct my_keypad_dev
{
	//硬件相关的成员变量
	int a ,*p;
	...
	//内嵌的struct cdev结构对象
	struct cdev c_dev;
}

设备驱动程序中可以使用两种方式来产生struct cdev对象:

静态方式:static struct cdev chr_dev;

动态方式:static struct cdev *p=kmalloc(sizeof(struct cdev),GFP_KERNEL);

(4)初始化函数cdev_init

在(3)中讨论了如何产生一个struct cdev对象,接下来就讨论一下如何初始化一个cdev对象。为此,内核提供了相应的初始化函数cdev_init,定义在<fs/char_dev.c>中,如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

参数说明:

*cdev:指向需要初始化的设备对象

*fops:包含了针对该字符设备的操作函数的结构体指针

(5)设备号

在linux系统中,一个设备号由主设备号和次设备号构成。内核使用主设备号来定位对应的设备驱动程序。而次设备号则由驱动程序使用,用于标识它所管理的若干同类设备。设备号是系统管理设备的有效资源。Linux中使用 dev_t(32位无符号整数)来表示一个设备号。

A、内核提供了以下三个宏用于操作设备号:<include/linux/kdev_t.h>

#define MAJOR(dev) ((unsignedint)((dev)>>MINORBITS))   //提取主设备号
#define MINOR(dev) ((unsignedint)((dev)&MINORBITS))  //提取次设备号       
#define MKDEV(ma,mi)(((ma)<<MINORBITS)|(mi))              //合成设备号

B、为了有效的管理字符设备的设备号,内核定义了一个全局性指针数组chrdevs,该数组中的每一项都是一个指向struct char_device_struct类型的指针。系统中已分配的字符设备号都存放在该数组中。该指针数组定义如下:<fs/char_dev.c>

static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

C、另外内核还提供了两个函数用于分配和管理设备号,定义在<fs/char_dev.c>中

alloc_chrdev_region()函数:该函数用于分配设备号,分配的主设备号范围将在1~254之间。定义如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

这个函数的核心是调用__register_chr_dev_region,而且第一个参数为0,这样将导致

__register_chr_dev_region执行下面的逻辑:

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
			   int minorct, const char *name)
{jkjkhujhklkljkljmn
	struct char_device_struct *cd, **cp;
	int ret = 0;
	int i;

	cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
	if (cd == NULL)
		return ERR_PTR(-ENOMEM);

	mutex_lock(&chrdevs_lock);

	/* temporary */
	if (major == 0) {
		for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
			if (chrdevs[i] == NULL)
				break;
		}

		if (i == 0) {
			ret = -EBUSY;
			goto out;
		}
		major = i;
		ret = major;
	}
	...
}

这段代码的原理是:它在for循环中,从chrdevs数字的最后一项一次向前扫描,如果发现该数组中的某项,比如第i项对应的数值为NULL,那么就把该项对应的索引值i作为分配的主设备号返回给驱动程序,并将其加入到chrdevs[i]对应的哈希链表中。如果分配成功,所分配的主设备号将记录在structchar_device_struct对象cd中(数组存放的都是这种对象),并将cd返回给alloc_chrdev_region函数,后者通过*dev=MKDEV(cd->major,cd->baseminor) 将新分配的设备号返回给函数掉用者。

register_chrdev_region()函数:函数原型如下:

int register_chrdev_region(dev_t from,unsigned count,const  char *name){}

参数说明:

from:表示设备号;count:连续设备编号的个数;name:设备或者驱动的名称;

该函数的作用就是将当前设备驱动程序要使用的设备号记录到chrdev数组中,用于跟踪系统设备号的使用情况,从而避免设备号的冲突。在使用这个函数时,要事先知道它所使用的设备号。

(6)字符设备的注册

在一个字符设备初始化完之后,就可以把它加入系统中,这样别的模块才可以使用它。把一个字符加入系统中需要调用函数cdev_add。其定义如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	p->dev = dev;
	p->count = count;
	return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

cdev_add的核心功能通过kobj_map()函数来实现。调用cdev_add后,把要注册的字符设备对象的指针嵌入到了一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map所实现的哈希链表中。有关struct probe和cdev_map的定义如下:

<fs/char_dev.c>  static sturct kobj_map *cdev; //这是一个struct kobj_map指针类型的全局变量。在Linux系统启动期间由chrdev_init函数负责初始化。struct kobj_map定义如下:

struct kobj_map {
	struct probe {
		struct probe *next;
		dev_t dev;
		unsigned long range;
		struct module *owner;
		kobj_probe_t *get;
		int (*lock)(dev_t, void *);
		void *data;
	} *probes[255];
	struct mutex *lock;
};

kobj_map()函数过程:通过要加入系统的设备的主设备号major来获得probes数组的索引值i,然后把一个类型为struct probe的节点对象加入到probe[i]所管理的链表中。其中probe节点中包含了设备的主设备号,以及指向字符设备对象的指针。如下图:



通过调用cdev_add后,就意味着一个字符设备对象已经加入到了系统中,在需要的时候,系统就可以找到它了。

在cdev_add()函数中,动态分配了struct probe类型的节点。当设备对象从系统中移除时,需要将它们从链表中删除并释放节点所占用的内存空间。这就是cdev_del()函数的作用,定义如下:

void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}

对于以内核模块形式存在的驱动程序,作为通用规则,模块的卸载函数应负责调用这个函数来将所管理的设备对象从系统中移除。

(7)设备文件节点的生成

在linux系统中,硬件设备都是以文件的形式存在于/dev/下的。即对应/dev/下的每个文件节点都代表了一个设备。在linux系统中,每个文件都有两种不同的表示方式。对于任意一个文件,在用户空间一般用文件名来识别如demodev。在内核空间中,一般用inode来表示。如168。它们实际上指向的都是同一个文件。对于设备文件,有:

inode->i_fop=&def_chr_fops;

inode->i_rdev=rdev;

 

(8)字符设备文件的打开操作

作为例子,这里假定前面对应于/dev/demodev设备节点的驱动程序已经实现了如下的struct file_operations对象chr_fops和打开函数chr_open。

struct file_operations chr_ops=

{

       .owner=THIS_MODULE,

       .open=chr_open,

       .read=chr_read,

};

static int chr_open(struct inode *nd,structfile *filp)

{

       intmajor=MAJOR(nd->i_rdev);

       intminor=MINOR(nd->i_rdev);

       printk("chr_open,major=%d,minor=%d\n",major,minor);

       return0;

}

 

用户空间应用程序的open函数原型为:

int open(constchar *filename,int flages,mode_t mode);

 

位于内核空间的驱动程序中open函数的原型为:

structfile_operations

{     ...

       int(*open)(struct inode *,struct file *) ;

       …

}

接下来我们见描述用户态的open函数是如何一步步调用到驱动程序提供的open函数的(在本例子中即chr_open函数)。由前面的三个函数可以看出:用户态open函数返回的是文件描述符fd(整形)。而驱动程序中的参数类型为struct file*file。显然内核需要在打开设备文件时为fd与file建立某种关系,并且为file与驱动程序中的fops建立关联。

用户空间调用open函数,将发起一个系统调用,通过sys_open函数进入内核空间。调用关系如下:


1)do_sys_open函数首先通过get_unused_flags为本次的open操作 分配一个未使用过的文件描述符fd。

2)do_sys_open函数随后调用do_filp_open函数,该函数会查找 “/dev/demodev”设备文件对应的inode。查找到inode之后,接着调用函数get_empty_filp函数为打开的文件分配一个新的struct file类型的内存空间(返回指针)。内核用struct file对象来描述进程打开的每个文件。struct file的定义如下 :

struct file

{     ....

       Const structfile_operations *f_op ;

       ….

}

从struct file的定义可以看出,struct file对象中包含了struct file_operations类型的指针。

3)linux系统为每个进程都维护了一个文件描述符表。进程已打开文件的文件描述符(fd)就是文件描述符表的索引值。文件描述符表中的每一个表项都有一个指向已打开文件的指针。这个指针就是struct file 类型的指针。即:在描述符表中,通过fd索引只可以找到对应的表项,该表项的值就是filp,它指向了内核为刚刚打开的文件所分配的struct file类型空间。

4)在do_sys_open函数的后半部分,调用函数__dentry_open函数将“/dev/demodev”对应节点的inode中的i_fop赋值给filp->f_op。由(7)中节点创建可以知道,inode->i_fop=&def_chr_fops;因此进行赋值操作filp->fop=inode->i_fop后,filp->fop=&def_chr_fops;即file结构成员*fop指向了设备驱动程序中的struct file_operations型数据结构。从而可以调用驱动程序的open函数。

3、总结

待续。。。