Linux 字符设备驱动结构

字符设备驱动

Posted by 咖啡泡泡茶 on July 20, 2018

Linux 字符设备驱动结构

一、cdev 结构体、设备号相关知识解析

1、设备驱动分类

   linux系统将设备分为3类:字符设备、块设备、网络设备。使用驱动程序:

字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。

块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

2、字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

如图,在Linux内核中:

  • 使用cdev结构体来描述字符设备;

  • 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

  • 通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

在Linux字符设备驱动中:

  • 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;

  • 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;

  • 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;

    用户空间访问该设备的程序: 通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定 义字符设备驱动提供给VFS的接口函数;

3、字符设备驱动模型 (实现步骤)

  • 实现入口函数 xxx_init()和卸载函数 xxx_exit()
  • 申请设备号 register_chrdev (与内核相关)
  • 利用udev/mdev机制创建设备文件(节点) class_create, device_create (与内核相关) 硬件部分初始化 io资源映射 ioremap,内核提供gpio库函数 (与硬件相关) 注册中断(与硬件相关)
  • 构建 file_operation结构 (与内核相关)
  • 实现操作硬件方法 xxx_open,xxx_read,xxxx_write

####二、cdev 结构体解析

​ 在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:

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结构的接口主要有以下几个:

  • void cdev_init(struct cdev *, const struct file_operations *);
  • struct cdev *cdev_alloc(void);
  • int cdev_add(struct cdev *p, dev_t dev, unsigned count);
  • void cdev_del(struct cdev *p);

1、该函数主要对struct cdev结构体做初始化

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 和 file_operations之间的连接:

  • 将整个结构体清零;
  • 初始化list成员使其指向自身;
  • 初始化kobj成员;
  • 初始化ops成员;

2、该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即: .ops=xxx_ops).

struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

​ 在上面的两个初始化的函数中,我们没有看到关于owner成员、dev成员、count成员的初始化;其实,owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct module是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE, 该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。

3、 该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。

当然这里还需提供两个参数:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	int error;
	p->dev = dev;
	p->count = count;
	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;
	kobject_get(p->kobj.parent);
	return 0;
}

(1)第一个设备号 dev,

(2)和该设备关联的设备编号的数量。

这两个参数直接赋值给struct cdev 的dev成员和count成员。

4、void cdev_del(struct cdev *p);

​ 该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了。

​ 从上述的接口讨论中,我们发现对于struct cdev的初始化和注册的过程中,我们需要提供几个东西

(1) struct file_operations结构指针;

(2) dev设备号;

(3) count次设备号个数。

三、设备号相应操作

1 主设备号和次设备号(二者一起为设备号):

​ 一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

 linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

  typedef u_long dev_t; 在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。

内核也为我们提供了几个方便操作的宏实现dev_t:

1) – 从设备号中提取major和minor

MAJOR(dev_t dev);                             

MINOR(dev_t dev);

2) – 通过major和minor构建设备号

MKDEV(int major,int minor);

注:这只是构建设备号。并未注册,需要调用 register_chrdev_region 静态申请;

2、分配设备号(两种方法):

静态申请:int register_chrdev_region(dev_t from, unsigned count, const char *name);

动态分配:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

可以看到二者都是调用了__register_chrdev_region 函数 。通过这个函数可以看出 register_chrdev_region和 alloc_chrdev_region 的区别,register_chrdev_region直接将Major 注册进入,而 alloc_chrdev_region从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;

devno =MKDEV(major,minor); ret=register_chrdev_region(devno, 1, “hello”); cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1); alloc_chrdev_region(&devno, minor, 1, “hello”); major = MAJOR(devno); cdev_init(&cdev,&hello_ops); ret = cdev_add(&cdev,devno,1) register_chrdev(major,”hello”,&hello)
register_chrdev_region alloc_chrdev_region register_chrdev

可以看到,除了前面两个函数,还加了一个register_chrdev 函数,可以发现这个函数的应用非常简单,只要一句就可以搞定前面函数所做之事;

下面分析一下register_chrdev 函数,其源代码定义如下:

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

调用了 __register_chrdev(major, 0, 256, name, fops) 函数: 可以看到这个函数不只帮我们注册了设备号,还帮我们做了cdev 的初始化以及cdev 的注册;

如同先前讨论的,字符设备通常通过在路径/dev下的设备文件进行访问。主设备号告诉你哪些驱动模块是用来操纵哪些硬件设备的。从设备号是驱动模块自己使用来区别它操纵的不同设备,当此驱动模块操纵不只一个设备时。

将内核驱动模块加载入内核意味着要向内核注册自己。这个工作是和驱动模块获得主设备号时初始化一同进行的。你可以使用头文件linux/fs.h中的函数register_chrdev来实现。

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

其中unsigned int major是你申请的主设备号,const char *name是将要在文件/proc/devices中显示的名称,struct file_operations *fops是指向你的驱动模块的file_operations表的指针。负的返回值意味着注册失败。注意注册并不需要提供从设备号。内核本身并不在意从设备号。

现在的问题是你如何申请到一个没有被使用的主设备号?最简单的方法是查看文件 Documentation/devices.txt从中挑选一个没有被使用的。这不是一劳永逸的方法因为你无法得知该主设备号在将来会被占用。最终的方法是让内核为你动态分配一个。

如果你向函数register_chrdev传递为0的主设备号,那么返回的就是动态分配的主设备号。副作用就是既然你无法得知主设备号,你就无法预先建立一个设备文件。有多种解决方法:第一种方法是新注册的驱动模块会输出自己新分配到的主设备号,所以我们可以手工建立需要的设备文件。第二种是利用文件/proc/devices新注册的驱动模块的入口,要么手工建立设备文件,要么编一个脚本去自动读取该文件并且生成设备文件。第三种是在我们的模块中,当注册成功时,使用mknod系统调用建立设备文件并且在驱动模块调用函数cleanup_module前,调用rm删除该设备文件。

3、注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count);

void unregister_chrdev(unsigned int major, const char * name);

4、创建设备文件:

​ 利用cat /proc/devices查看申请到的设备名,设备号。

1)使用mknod手工创建:mknod filename type major minor

2)自动创建设备节点:

​ 利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

四、自动创建设备节点

1、利用cat /proc/devices查看申请到的设备名,设备号 。

第一种是使用mknod手工创建:mknod filename type major minor

第二种是自动创建设备节点:利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。

在驱动用加入对udev 的支持主要做的就是:在驱动初始化的代码里调用class_create(…)为该设备创建一个class,再为每个设备调用device_create(…)创建对应的设备。

​ 内核中定义的struct class结构体,顾名思义,一个struct class结构体类型变量对应一个类,内核同时提供了class_create(…)函数,可以用它来创建一个类,这个类存放于sysfs下面,一旦创建好了这个类,再调用 device_create(…)函数来在/dev目录下创建相应的设备节点。

​ 这样,加载模块的时候,用户空间中的udev会自动响应 device_create()函数,去/sysfs下寻找对应的类从而创建设备节点。

2、class_create(…) 函数

功能:创建一个类;

#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

参数: owner:THIS_MODULE name : 名字 返回值:struct class *

销毁函数:void class_destroy(struct class *cls)

void class_destroy(struct class *cls)
{
	if ((cls == NULL) || (IS_ERR(cls)))
		return;
	class_unregister(cls);
}

3、device_create(…) 函数

功能:创建一个字符设备文件

struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)
{
	va_list vargs;
	struct device *dev;
	va_start(vargs, fmt);
	dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
	va_end(vargs);
	return dev;
}

参数:

​ struct class *class :类,即通过之前class_create的返回struct class * 类型的值 struct device *parent :NULL dev_t devt :设备号 、MKDEV(dev_major, 0) void *drvdata :null、 const char *fmt :名字