LDD - newer2/Blog GitHub Wiki

设备驱动基本框架

  • 源文件 头文件+模块参数+模块功能函数+其他+模块加载函数+模块卸载函数+模块许可声明
#include <linux/init.h>
#include <linux/module.h>
#include <linux/>
#include "add_sub.h"

static long a=1;	//模块参数
static int b=1;

long add_integer(int a, int b)	//模块功能函数
{

	return a+b;
}
long sub_integer(int a, int b)
{

	return a-b;
}

static int hello_init(void)		//模块加载函数,insmod时自动调用
{
	printk(KERN_ALERT "Hello World\n");
	return 0;
}
static void hello_exit(void)	//模块卸载函数,rmmod时自动调用
{
	printk("KERN_ALTER "Hello World Exit\n");
}

module_param(a, long, S_IRUGO);	//module_param(参数名, 参数数据类型, 参数读写权限)
module_param(b, int, S_IRUGO);	//导出参数没有浮点类型,内核并不能完美的支持浮点类
								//型,printk()也不支持浮点类型

/*+++++导出功能函数以供其他模块调用+++++*/
/*----------
1. 在insmod module.1时,内核发现以ELF格式组织的module.1的.symtab数据结构中有函数可以导出,内核便将其中导出的功能函数的内存地址记录在“内核符号表”;insmd module.2时,在以ELF格式组织的module.2的symtab中发现未解析的函数引用,通过查询“内核符号表”,将对应函数地址写入module.2的symtab中
2. 模块中定义的函数和内核中的函数冲突,怎么办?编译器认为模块中的函数都是私有的,除非使用导出宏
+++++++++++*/
EXPORT_SYMBOL(add_integer);	//导出宏
EXPORT_SYMBOL(sub_integer);

module_init(hello_init);	//模块加载函数和卸载函数的宏定义
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");		//模块许可声明
  • Makefile 正确编译内核模块条件:
  1. 不同版本的内核需要对应版本的编译工具(eg.gcc binutil)
  2. 编译的内核模块应该和测试系统对应同一内核源码
  3. 内核源码应该至少编译过一次
ifeq ($(KERNELRELEASE),)	# 检查变量KERNELRELEASE是否为空
	KERNELDIR ?= /linux-2.6.34.14/linux-2.6.34.14 
	PWD := $(shell pwd)
modules:	# Makefile的一个功能选项
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules 	# -C $(KERNELDIR)使编译器进入内核源码目录,读取其Makefile,变量KERNELRELEASE将在这里被赋值(跟踪的makefile文件可知,类似于KERNELRELEASE的许多变量,KERNELVERSION..等等将在这里被赋值并导出)
												# 编译器根据M=$(PWD)第二次进入模块所在目录,并再一次执行Makefile,此时KERNELRELEASE将是内核发布版本信息,
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions # 删除中间文件
else
	obj-m := hello.o 	# 开始编译模块
endif
  • 模块操作
  1. insmod 模块.ko 参数1=value1 参数2=value2 #加载模块,加载后自动调用hello_init
  2. rmmod 模块.ko #卸载模块,自动调用hello_exit
  3. modprobe 可以解决模块之间依赖性的加载和删除模块命令
  4. lsmod 列出已经加载模块相关信息
  5. modinfo 查询模块作者、版权等信息

模块间通信实例

实例


字符设备驱动分析

追踪流程 ismod-->globalmem_init-->globalmem_setup_cdev-->cdev_init-->cdev_add globalmem.c部分代码

struct globalmem_dev{
	struct cdev cdev;
	unsigned char mem[MAXSIZE];
};
int globalmem_open(struct inode *nodep, struct file *filp)
{
	...
}
int globalmem_release(struct inode *nodep, struct file *filp)
{
	...
}
static ssize_t globalmem_read(struct file * filp, char __user *buf, size_t size, loff_t *ppos)
{
	...
}
static ssize_t globalmem_write(struct file * filp, const char __user *buf, size_t size, loff_t *ppos)
{
	...
}
static int globalmem_ioctl(struct inode * nodep, struct file *filp, unsigned int cmd, unsigned long arg)
{
	...
}
static off_t globalmem_lseek(struct file * filp, loff_t offset, int orig)
{
	...
}
static const struct file_operations globalmem_fops = {
	.owner = THIS_MODULE,
	.open = globalmem_open,
	.read = globalmem_read,
	.write = globalmem_write,
	.unlocked_ioctl = globalmem_ioctl,
	.llseek = globalmem_lseek,
	.release = globalmem_release,
};
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
	int err, devno = MKDEV(globalmajor, index);

	cdev_init(&dev->cdev, &globalmem_fops);
	dev->cdev.owner = THIS_MODULE;
	err = cdev_add(&dev->cdev, devno, 1);
	if(err)
		printk(KERN_NOTICE "Error %d adding globalmem %d",err, index);
}

static int globalmem_init(void)
{
	int result;
	dev_t devno = MKDEV(globalmajor, 0);

	if(globalmajor)
		result = register_chrdev_region(devno, 1, "globalmem");	
	else{
		result = alloc_chrdev_region(&devno, 0, 1, "globalmem");
		globalmajor = MAJOR(devno);
	}
	if(result < 0)
		return result;

	globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
	if(!globalmem_devp){
		result = -ENOMEM;
		goto fail_malloc;
	}

	globalclass = class_create(THIS_MODULE, DEVICE_NAME);
	memset(globalmem_devp, 0, sizeof(struct globalmem_dev));
	globalmem_setup_cdev(globalmem_devp, 0);
	printk("globalmajor is %d\n",(int)globalmajor);
	device_create(globalclass, NULL, devno, NULL, DEVICE_NAME);
	printk("/dev/globalmem created!\n");

	return 0;

fail_malloc:
	unregister_chrdev_region(devno, 1);
	return result;
}
module_init(globalmem_init);
module_exit(globalmem_exit);

cdev_init cdev_init-->kobject_init-->kobject_init_internal

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;
}
  • kobject_init调用kobjectinit_internal,并将kobj->ktype与默认ktype:ktype_cdev_default绑定
  • kobjct_init_internal完成kobject中各个成员初始化

cdev_add cdev_add-->kobj_map

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);
}

kobj_map函数定义

int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
	     struct module *module, kobj_probe_t *probe,
	     int (*lock)(dev_t, void *), void *data)
{
	unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
	unsigned index = MAJOR(dev);
	unsigned i;
	struct probe *p;

	if (n > 255)
		n = 255;

	p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);

	if (p == NULL)
		return -ENOMEM;

	for (i = 0; i < n; i++, p++) {
		p->owner = module;
		p->get = probe;
		p->lock = lock;
		p->dev = dev;
		p->range = range;
		p->data = data;
	}
	mutex_lock(domain->lock);
	for (i = 0, p -= n; i < n; i++, p++, index++) {
		struct probe **s = &domain->probes[index % 255];
		while (*s && (*s)->range < range)
			s = &(*s)->next;
		p->next = *s;
		*s = p;
	}
	mutex_unlock(domain->lock);
	return 0;
}
  • kobj_map 封装好一个probe结构并将它的地址放入probes数组进而封装进cdev_map。根据传入的设备号的个数,将设备号和cdev依次封装到kmalloc_array()分配的n个probe结构中。遍历probs数组,直到找到一个值为NULL的元素,再将probe的地址存入probes, 将设备号对255取余后与probes的下标对应。至此,我们就将我们的cdev放入的内核的数据结构

kobj_map相关数据结构 fs/char_dev.c:static struct kobj_map *cdev_map; drivers/base/map.c

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;
};
  • data:指向cdev结构体
  • next:连接所有的probe
  • get、lock两个函数接口

回溯

当设备对象被放入内核时,如何找到特定的cdev。即用户空间通过设备文件如何首次找到特定cdev结构体,从而获取struct file_operations等数据结构。不过要先了解下面关于struct inodestruct file的描述。 字符设备访问流程: 文件路径-->inode-->chrdev_open-->kobj_lookup-->cdev-->cdev.fops.my_chr_open()

  • 虽然我们有了字符设备的设备文件,inode也被构造并初始化了, 但是在第一次调用chrdev_open()之前,这个inode和具体的chr_dev对象并没有直接关系,而只是通过设备号建立的"间接"关系。在第一次调用chrdev_open()之后, inode->i_cdev才被根据设备号找到的cdev对象赋值,此后inode才和具体的cdev对象直接联系在了一起.

ionde

Linux中一切皆文件,当我们在Linux中创建一个文件时,就会在相应的文件系统创建一个inode与之对应,文件实体和文件的inode是一一对应的,创建好一个inode会存在存储器中,第一次open就会将inode在内存中有一个备份,同一个文件被多次打开并不会产生多个inode,当所有被打开的文件都被close之后,inode在内存中的实例才会被释放。既然如此,当我们使用mknod(或其他方法)创建一个设备文件时,也会在文件系统中创建一个inode,这个inode和其他的inode一样,用来存储关于这个文件的静态信息(不变的信息),包括这个设备文件对应的设备号,文件的路径以及对应的驱动对象etc。inode作为VFS四大对象之一,在驱动开发中很少需要自己进行填充,更多的是在open()方法中进行查看并根据需要填充我们的file结构。 对于不同的文件类型,inode被填充的成员内容也会有所不同,以创建字符设备为例,我们知道,add_chrdev_region其实是把一个驱动对象和一个(一组)设备号联系到一起。而创建设备文件,其实是把设备文件和设备号联系到一起。至此,这三者就被绑定在一起了。这样,内核就有能力创建一个struct inode实例了,下面是4.8.5内核中的inode。这个inode是VFS的inode,是最具体文件系统的inode的进一步封装,也是驱动开发中关心的inode,针对具体的文件系统,还有struct ext2_inode_info 等结构。 strcut inode //include/linux/fs.h

struct inode {
	/* RCU path lookup touches following: */
	umode_t			i_mode;
	uid_t			i_uid;
	gid_t			i_gid;
	const struct inode_operations	*i_op;
	struct super_block	*i_sb;

	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	unsigned int		i_flags;
	unsigned long		i_state;
#ifdef CONFIG_SECURITY
	void			*i_security;
#endif
	struct mutex		i_mutex;


	unsigned long		dirtied_when;	/* jiffies of first dirtying */

	struct hlist_node	i_hash;
	struct list_head	i_wb_list;	/* backing dev IO list */
	struct list_head	i_lru;		/* inode LRU list */
	struct list_head	i_sb_list;
	union {
		struct list_head	i_dentry;
		struct rcu_head		i_rcu;
	};
	unsigned long		i_ino;
	atomic_t		i_count;
	unsigned int		i_nlink;
	dev_t			i_rdev;
	unsigned int		i_blkbits;
	u64			i_version;
	loff_t			i_size;
#ifdef __NEED_I_SIZE_ORDERED
	seqcount_t		i_size_seqcount;
#endif
	struct timespec		i_atime;
	struct timespec		i_mtime;
	struct timespec		i_ctime;
	blkcnt_t		i_blocks;
	unsigned short          i_bytes;
	struct rw_semaphore	i_alloc_sem;
	const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct file_lock	*i_flock;
	struct address_space	*i_mapping;
	struct address_space	i_data;
#ifdef CONFIG_QUOTA
	struct dquot		*i_dquot[MAXQUOTAS];
#endif
	struct list_head	i_devices;
	union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev;
	};

	__u32			i_generation;

#ifdef CONFIG_FSNOTIFY
	__u32			i_fsnotify_mask; /* all events this inode cares about */
	struct hlist_head	i_fsnotify_marks;
#endif

#ifdef CONFIG_IMA
	atomic_t		i_readcount; /* struct files open RO */
#endif
	atomic_t		i_writecount;
#ifdef CONFIG_FS_POSIX_ACL
	struct posix_acl	*i_acl;
	struct posix_acl	*i_default_acl;
#endif
	void			*i_private; /* fs or device private pointer */
};

针对我们的问题,从中主要抽取出i_fop

const struct file_operations def_chr_fops = {
	.open = chrdev_open,
	.llseek = noop_llseek,
};
  • 字符设备作为一种特殊设备,设备文件被创建时,会构造“相应的inode”,拿def_chr_fop来初始化inode的file_operation结构,从而inode调用默认的open函数接口chardev_open

chrdev_open //fs/char_dev.c

static int chrdev_open(struct inode *inode, struct file *filp)
{
	struct cdev *p;
	struct cdev *new = NULL;
	int ret = 0;

	spin_lock(&cdev_lock);
	p = inode->i_cdev;
	if (!p) {
		struct kobject *kobj;
		int idx;
		spin_unlock(&cdev_lock);
		kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
		if (!kobj)
			return -ENXIO;
		new = container_of(kobj, struct cdev, kobj);
		spin_lock(&cdev_lock);
		/* Check i_cdev again in case somebody beat us to it while
		   we dropped the lock. */
		p = inode->i_cdev;
		if (!p) {
			inode->i_cdev = p = new;
			list_add(&inode->i_devices, &p->list);
			new = NULL;
		} else if (!cdev_get(p))
			ret = -ENXIO;
	} else if (!cdev_get(p))
		ret = -ENXIO;
	spin_unlock(&cdev_lock);
	cdev_put(new);
	if (ret)
		return ret;

	ret = -ENXIO;
	filp->f_op = fops_get(p->ops);
	if (!filp->f_op)
		goto out_cdev_put;

	if (filp->f_op->open) {
		ret = filp->f_op->open(inode,filp);
		if (ret)
			goto out_cdev_put;
	}

	return 0;

 out_cdev_put:
	cdev_put(p);
	return ret;
}
  • chrdev_open通过设备号、strcut kobj_map *cdev_map并调用kobj_lookup查找到设备号对应chr_dev的kobject,再通过container_of,找到cdev结构。然后用cdev.fops替换filp->f_op,即填充了一个空的struct file的f_op成员。回调替换之后的filp->f_op->open,由于替换,这个其实就是cdev.fops

file

Linux内核会为每一个进程维护一个文件描述符表,这个表其实就是struct file[]的索引。open()的过程其实就是根据传入的路径填充好一个file结构并将其赋值到数组中并返回其索引。下面是file的主要内容

struct file //include/linux/fs.h

struct file {
	/*
	 * fu_list becomes invalid after file_free is called and queued via
	 * fu_rcuhead for RCU freeing
	 */
	union {
		struct list_head	fu_list;
		struct rcu_head 	fu_rcuhead;
	} f_u;
	struct path		f_path;
#define f_dentry	f_path.dentry
#define f_vfsmnt	f_path.mnt
	const struct file_operations	*f_op;
	spinlock_t		f_lock;  /* f_ep_links, f_flags, no IRQ */
#ifdef CONFIG_SMP
	int			f_sb_list_cpu;
#endif
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;
	loff_t			f_pos;
	struct fown_struct	f_owner;
	const struct cred	*f_cred;
	struct file_ra_state	f_ra;

	u64			f_version;
#ifdef CONFIG_SECURITY
	void			*f_security;
#endif
	/* needed for tty driver, and maybe others */
	void			*private_data;

#ifdef CONFIG_EPOLL
	/* Used by fs/eventpoll.c to link all the hooks to this file */
	struct list_head	f_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
	struct address_space	*f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
	unsigned long f_mnt_write_state;
#endif
};
  • file结构中f_op结构的填充将在设备文件第一次被打开后,调用默认的chrdev_open接口-->kobj_lookup-->cotainer_of-->cdev-->file->f_op = cdev.fops-->回调fop->open

设备驱动模型

设备驱动模型(DDM)完成任务

  • 电源管理和系统关机 设备驱动模型使得电源管理系统能够以正确的顺序遍历系统硬件。例如一个USB宿主适配器,在处理完所有与其连接的设备之前是不能关闭的,这就要求系统提供一个USB宿主适配器之下所连接设备的视图。
  • 与用户空间通信 sysfs虚拟文件系统和设备驱动模型密切相关,通过其,向用户空间提供内核信息。
  • 热插拔设备 设备驱动模型自动捕捉插拔信号,加载驱动程序。

核心数据结构

kobject kset subsystem使设备驱动模型组成一个层次结构。该层次结构将驱动、设备和总线联系起来。
正因为内核中设备驱动模型合理管理系统中各种各样的驱动,使得驱动工程师并不需要了解设备驱动模型细节就可进行驱动开发。

kobject相关数据结构 最初作为设备对象仅仅提供一个引用计数,随着系统功能增加,其任务也越来越多。提供基本的设备对象管理功能,内核中注册的每一个kobject对应sysfs中的一个目录。

struct kobject {
	const char		*name;
	struct list_head	entry;
	struct kobject		*parent;
	struct kset		*kset;
	struct kobj_type	*ktype;
	struct sysfs_dirent	*sd;
	struct kref		kref;
	unsigned int state_initialized:1;
	unsigned int state_in_sysfs:1;
	unsigned int state_add_uevent_sent:1;
	unsigned int state_remove_uevent_sent:1;
	unsigned int uevent_suppress:1;
};
  • name:对应于sysfs中的目录名
  • entry:连接下一个kobject结构,形成以kset设备对象中list开始的环形链表
  • parent:指向其父kobject,形成层次结构,如果存在的话
  • kset:指向kset集合
  • ktype:指向kobj类型描述符
  • sd:对应sysfs的文件目录
  • kref:引用计数,由kobject_get增加引用计数和kobject_put减小引用计数,为零时释放相关设备资源
  • state_initialized:kobj是否初始化
  • state_in_sysfs:是否已加入sysfs
struct kobj_type {
	void (*release)(struct kobject *kobj);
	const struct sysfs_ops *sysfs_ops;
	struct attribute **default_attrs;
	const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj);
	const void *(*namespace)(struct kobject *kobj);
};
  • release:当kobject引用计数为0时,调用过程:kref_put--->kobject_release--->release
  • sysfs_ops:对属性的操作函数,读、写操作,对于sysfs中普通属性文件的读写都是通过kobject->ktype->sysfs_ops完成
  • 设备对象的属性,可能不止一个属性,每个属性对应一个文本文件,放在kobject对应目录下
struct sysfs_ops {
	ssize_t	(*show)(struct kobject *, struct attribute *,char *);
	ssize_t	(*store)(struct kobject *,struct attribute *,const char *, size_t);
};
  • 属性读写函数
struct attribute {
	const char		*name;
	mode_t			mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lock_class_key	*key;
	struct lock_class_key	skey;
#endif
};
  • name:属性名即属性文件名
  • mode:属性读写权限,即sysfs中文件读写权限,eg.S_IRUGO S_IWUGO

kset

struct kset {
	struct list_head list;
	spinlock_t list_lock;
	struct kobject kobj;
	const struct kset_uevent_ops *uevent_ops;
};
  • list:属于该kset的所有kobject对象被list_head结构组织成以list开头的双向循环链表
  • list_lock:从list中添加、删除kobject的自旋锁
  • kobj:内嵌kobject对象,同样对应sysfs中一个目录。所有属于该kset的kobjetc对象的parent指针指向这个内嵌的kobject,kset的引用计数即内嵌的kobjetc对象的引用计数
  • uevent_ops:支持热拔插的函数集
struct kset_uevent_ops {
	int (* const filter)(struct kset *kset, struct kobject *kobj);
	const char *(* const name)(struct kset *kset, struct kobject *kobj);
	int (* const uevent)(struct kset *kset, struct kobject *kobj,
		      struct kobj_uevent_env *env);
};
  • filter:过滤函数,决定热插拔事件发生时,内核是否向用户空间发送事件产生信号
  • name:该函数在用户空间的热插拔程序需要知道子系统名字时被调用
  • uevent:热插拔程序执行前项环境变量中写入值,因为任何热插拔程序需要的信息可以通过环境变量来传递(热插拔程序属于用户空间,当设备插入、拔掉时产生热插拔事件,这时候内核中会有设备的注册或者删除,同时必然也对应这sysfs中相应设备对象目录的改变。信号向用户空间发送后将会执行热插拔程序,udev设备文件系统通过sysfs,完成设备的自动注册或注销)

相关函数操作

kobject初始化

/*
 * This function will properly initialize a kobject such that it can then
 * be passed to the kobject_add() call.
 *
 * After this function is called, the kobject MUST be cleaned up by a call
 * to kobject_put(), not by a call to kfree directly to ensure that all of
 * the memory is cleaned up properly.
 */
void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
{
	char *err_str;

	if (!kobj) {
		err_str = "invalid kobject pointer!";
		goto error;
	}
	if (!ktype) {
		err_str = "must have a ktype to be initialized properly!\n";
		goto error;
	}
	if (kobj->state_initialized) {
		/* do not error out as sometimes we can recover */
		printk(KERN_ERR "kobject (%p): tried to init an initialized "
		       "object, something is seriously wrong.\n", kobj);
		dump_stack();
	}

	kobject_init_internal(kobj);	/*初始化kobject内部成员变量*/
	kobj->ktype = ktype;		/*weikobject绑定一个ktype属性*/
	return;

error:
	printk(KERN_ERR "kobject (%p): %s\n", kobj, err_str);
	dump_stack();
}
static void kobject_init_internal(struct kobject *kobj)
{
	if (!kobj)
		return;
	kref_init(&kobj->kref);
	INIT_LIST_HEAD(&kobj->entry);
	kobj->state_in_sysfs = 0;
	kobj->state_add_uevent_sent = 0;
	kobj->state_remove_uevent_sent = 0;
	kobj->state_initialized = 1;
}

注册kobject到sysfs中的实例

Sysfs

简介 sysfs:sysfs是 Linux 2.6 内核里的一个虚拟文件系统 (/sys)。它把设备和驱动的信息从内核的设备模块导出到用户空间 (userspace)。从该文件系统中,Linux 用户可以获取很多设备的属性。 sysfs目录结构

  • block:过时的接口,按照sysfs设计理念,所有的设备都应该在/sys/device,这个目录只是为了提高兼容性设计,其中文件已全部替换为了符号链接
  • sysfs中保证了系统中设备都只存在一个实例,所以class/ bus/等目录下都是符号链接

Udev

Reference:https://www.ibm.com/developerworks/cn/linux/l-cn-udev/ udev文件系统在用户空间工作,它可以根据sysfs文件系统导出的信息(设备号(dev)等),动态建立和删除设备文件,而不再需要使用mknod来手动建立设备文件,也不必为查找设备号(尤其是驱动中动态申请产生的设备号)而头疼。

  • udev完全在用户态工作,利用设备加入或移除时内核所发送的热插拔事件。在热插拔时,设备的详细信息会由内核输出到位于/sys的sysfs文件系统。udev的设备命名策略权限控制都在用户态完成的,它利用sysfs信息来进行创建设备文件节点。
  • udev根据系统中的硬件设备的状态动态更新设备文件,进行设备文件的创建和删除等。
  • 使用udev,/dev目录下就会只包含系统中真正存在的设备。所有在 sysfs 中显示的设备都可以由 udev 来创建节点。如果内核中增加了其它设备的支持,udev 也就自动地可以为它们工作了。
  • 结合设备驱动模型 设备节点<--udev<--sysfs<--DDM<--kset+kobject-->构成内核DDM结构 而总线、设备、驱动属于上层,驱动工程师不用知道底层 设备对象之间层次关系如何构建??设备和驱动信息如何由内核导出到sysfs??等等问题,这些都将由内核中的设备驱动模型处理

设备树

  • DT基本概念
  • ARM Linux中DT代码分析

DT基本概念

设备树的基本单元是节点node,这些节点被组织成树状结构,除了root节点外,其他节点只有唯一的父节点。 基本单元结构

[label]: node-name[@unit-address]{
	[properties define]
   [child node]
}
  • '[]'代表可选,所以可以定义一些空节点(下面的实例中会讲到)
  • ‘label’即为节点取的别名,方便引用(否则,只能使用从根节点开始的绝对路径,随着设备的增多,这显然不可取),由DEVICE TREE Compile解析翻译。
  • @unit-address 设备对应地址,与具体设备挂在哪个总线下相关。节点名字格式:node-name@unit-addr。若该节点没有reg属性,节点名就不能包括“@unit-addr”
  • "properties define" 属性键值对,标识设备特性,其值有多种,为空、字符串、u32或u64的数值
  • "child node" 类似于父节点的结构,从而嵌套成“树”型结构

linux3.5/arch/arm/boot/dts/skeleton.dtsi

/ {
	#address-cells = <1>;
	#size-cells = <1>;
	chosen { };
	aliases { };
	memory { device_type = "memory"; reg = <0 0>; };
  • 先讨论这个ske;eton.dtsi 设备树源文件dts,为了提高设备树代码的利用,避免重复,在arm--samsung--chip分别定义对应的.dtsi(‘i’即“include”)文件,这样再一层层的include就行 eg. skeleton.dtsi--s3c24xx.dtsi(samsung定义的属于s3c24xx系列的芯片通用的dtsi)--s3c2416(针对s3c2416芯片使用的dtsi)

  • 前两条是属性定义,'#'含义:number,这两条定义是来约束其子节点的。若当前节点包含了,定义有寻址需求(定义reg property)的子节点,那么必须要定义这两个属性。#address-cells这个属性是用来描述sub node中的reg属性的地址域特性的,也就是说需要用多少个u32的cell来描述该地址域。同理可以推断#size-cells的含义, 关于属性: Linux设备树语法中定义了一些具有规范意义的属性,包括:compatible, address, interrupt等,这些信息能够在内核初始化找到节点的时候,自动解析生成相应的设备信息。此外,还有一些Linux内核定义好的,一类设备通用的有默认意义的属性,这些属性一般不能被内核自动解析生成相应的设备信息,但是内核已经编写的相应的解析提取函数,常见的有 "mac_addr","gpio","clock","power"。"regulator" 等等

  • 后三条都是三个子节点

  • chosen子节点主要用来传递参数,其必须挂接在根节点之下。这样原来通过tag list传递的一些运行时参数就可以通过Device Tree来传递。例如command line可以通过bootargs这个property这个属性传递;initrd的开始地址也可以通过linux,initrd-start这个property这个属性传递。在本例中,chosen节点是空的,在实际中,建议增加一个bootargs的属性。

  • aliases子节点用来定义别名,避免使用很长的绝对路径

  • memory device node是所有设备树文件的必备节点,它定义了系统物理内存的layout。device_type属性定义了该node的设备类型,例如cpu、serial等。对于memory node,其device_type必须等于memory。reg属性定义了访问该device node的地址信息,该属性的值被解析成任意长度的(address,size)数组,具体用多长的数据来表示address和size是在其parent node中定义(#address-cells和#size-cells)。对于device node,reg描述了memory-mapped IO register的offset和length。对于memory node,定义了该memory的起始地址和长度。

linux3.5/arch/arm/boot/dts/exynos4210.dtsi

/ {
	compatible = "samsung,exynos4210";
	interrupt-parent = <&gic>;

	gic:interrupt-controller@10490000 {
		compatible = "arm,cortex-a9-gic";
		#interrupt-cells = <3>;
		interrupt-controller;
		cpu-offset = <0x8000>;
		reg = <0x10490000 0x1000>, <0x10480000 0x100>;
	};

	watchdog@10060000 {
		compatible = "samsung,s3c2410-wdt";
		reg = <0x10060000 0x100>;
		interrupts = <0 43 0>;
	};
	.......
	gpio-controllers {
		#address-cells = <1>;
		#size-cells = <1>;
		gpio-controller;
		ranges;

		gpa0: gpio-controller@11400000 {
			compatible = "samsung,exynos4-gpio";
			reg = <0x11400000 0x20>;
			#gpio-cells = <4>;
		};

		gpa1: gpio-controller@11400020 {
			compatible = "samsung,exynos4-gpio";
			reg = <0x11400020 0x20>;
			#gpio-cells = <4>;
		};
	.......
  • compatible 要将compatible属性就要先说一下,model属性:指明该设备属于哪一个生产厂商的哪一个model。例如:manufacturer,model”。例如model = "samsung,s3c24xx"。samsung是生产商,s3c24xx是model类型,指明了具体的是哪一个系列的SOC。compatible属性对于根节点是匹配machine type的;而对与普通的HW block是来匹配驱动的。例如,compatible = “aaa”,“bbb” 会以“aaa”来匹配合适的驱动,若匹配不到,再使用“bbb”进行驱动查找匹配

  • interrupt-parent 对于各个HW block的中断源是如何连接到对应的interrupt control的呢?通过上面代码可以看出,定义interrupt-parent属性的是root node,难道root node会产生中断到control吗?no,因为如果一个能够产生中断的子节点没有定义interrupt-parent属性的话,那么它继承父节点的。所以,为了避免root node下需要定义此属性的子节点都定义一个,所以了

  • interrupt-parent = <&gic> gic是终端控制器定义的一个label,这里通过&来对其引用

  • 中断树 在设备树中中断也是“树”型结构,在interrupt root node下会挂一些device也会挂interrupt controller(术语叫interrupt nexus:中断关系节点?),在中断控制器下也会挂一些设备、中断控制器。interrupt-parent建立起了这样的中断树结构。而没有定义interrupt-parent的interrupt-controller就是root 参考 http://www.cnblogs.com/xiaojiang1025/p/6131381.html http://www.wowotech.net/device_model/dt_basic_concept.html/comment-page-2#comments

⚠️ **GitHub.com Fallback** ⚠️