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

2023-08-31,,

前段时间在学习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、字符设备驱动程序内核机制具体解释

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

(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、总结

待续。

。。

浅析Linux字符设备驱动程序内核机制的相关教程结束。

《浅析Linux字符设备驱动程序内核机制.doc》

下载本文的Word格式文档,以方便收藏与打印。