一,从/dev目录说起
从事Linux嵌入式驱动开发的人,都很熟悉下面的一些基础知识, 比如,对于一个char类型的设备,我想对其进行read wirte 和ioctl操作,那么:
1)我们通常会在内核驱动中实现一个file_operations结构体,然后分配主次设备号,调用cdev_add函数进行注册。
2)从/proc/devices下面找到注册的设备的主次设备号,在用mknod /dev/char_dev c major minor命令行创建设备节点。
3)在用户空间open /dev/char_dev这个设备,然后进行各种操作。
OK,字符设备模型就这么简单,很多ABC教程都是一个类似的实现。
然后我们去看内核代码时,突然一脸懵逼。。。怎么内核代码里很多常用的驱动的实现不是这个样子的?没看到有file_operations结构体,我怎么使用这些驱动?看到了/dev目录下有需要的char设备,可是怎么使用呢?
Linux驱动模型的一个重要分界线
linux2.6版本以前,普遍的用法就像我上面说的一样。但是linux2.6版本以后,引用了Linux设备驱动模型,开始使用了基于sysfs的文件系统,出现让我们不是太明白的那些Linux内核驱动了。
也就是说,我们熟悉的那套驱动模式是2.6版本以前的(当然这是基础,肯定要会的)
我们不熟悉的驱动模型是2.6版本以后的。
二,cdev_map对象
//fs/char_dev.c
27 static struct kobj_map *cdev_map;
内核中关于字符设备的操作函数的实现放在"fs/char_dev.c"中,打开这个文件,首先注意到就是这个在内核中不常见的静态全局变量cdev_map(27),我们知道,为了提高软件的内聚性,Linux内核在设计的时候尽量避免使用全局变量作为函数间数据传递的方式,而建议多使用形参列表,而这个结构体变量在这个文件中到处被使用,所以它应该是描述了系统中所有字符设备的某种信息,带着这样的想法,我们可以在"drivers/base/map.c"中找到kobj_map结构的定义:
//drivers/base/map.c
19 struct kobj_map {
20 struct probe {
21 struct probe *next;
22 dev_t dev;
23 unsigned long range;
24 struct module *owner;
25 kobj_probe_t *get;
26 int (*lock)(dev_t, void *);
27 void *data;
28 } *probes[255];
29 struct mutex *lock;
30 };
从中可以看出,kobj_map的核心就是一个struct probe类型、大小为255的数组,而在这个probe结构中,第一个成员next(21)显然是将这些probe结构通过链表的形式连接起来,dev_t类型的成员dev显然是设备号,get(25)和lock(26)分别是两个函数接口,最后的重点来了,void作为C语言中的万金油类型,在这里就是我们cdev结构(通过后面的分析可以看出),所以,这个cdev_map是一个struct kobj_map类型的指针,其中包含着一个struct probe*类型、大小为255的数组,数组的每个元素指向的一个probe结构封装了一个设备号和相应的设备对象(这里就是cdev),下图中体现两种常见的对设备号和cdev管理的方式,其一是一个cdev对象对应这一个/多个设备号的情况, 在cdev_map中, 一个probes对象就对应一个主设备号,多个设备号对应一个cdev时,其实只是次设备号在变,主设备号还是一样的,所以是同一个probes对象;其二是当主设备号超过255时,会进行probe复用,此时probe->next就派上了用场,比如probe[200],可以表示设备号200,455...3895等所有对255取余是200的数字, 参见下文的kobj_map--58--。
三,cdev_add()
1,cdev_add()
了解了cdev_map的功能,我们就可以一探cdev_add()。从中可以看出,其工作显然是交给了kobj_map()
cdev_add()
--460-->就是将我们之前获得设备号和设备号长度填充到cdev结构中,
--468-->kobject_get()将kobject的计数减一,并返回struct kobject*
//fs/char_dev.c
456 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
457 {
458 int error;
459
460 p->dev = dev;
461 p->count = count;
462
463 error = kobj_map(cdev_map, dev, count, NULL,
464 exact_match, exact_lock, p);
465 if (error)
466 return error;
467
468 kobject_get(p->kobj.parent);
469
470 return 0;
471 }
2,kobj_map()
这个函数在内核的设备管理中占有重要的地位,这里我们只从字符设备的角度分析它的功能,这个函数的设计也很单纯,就是封装好一个probe结构并将它的地址放入probes数组进而封装进cdev_map,。
kobj_map()
--48-55-->根据传入的设备号的个数,将设备号和cdev依次封装到kmalloc_array()分配的n个probe结构中
--57-63-->就是遍历probs数组,直到找到一个值为NULL的元素,再将probe的地址存入probes, 将设备号对255取余后与probes的下标对应。至此,我们就将我们的cdev放入的内核的数据结构。
//drivers/base/map.c
32 int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
33 struct module *module, kobj_probe_t *probe,
34 int (*lock)(dev_t, void *), void *data)
35 {
36 unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
37 unsigned index = MAJOR(dev);
38 unsigned i;
39 struct probe *p;
...
44 p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
...
48 for (i = 0; i < n; i++, p++) {
49 p->owner = module;
50 p->get = probe;
51 p->lock = lock;
52 p->dev = dev;
53 p->range = range;
54 p->data = data;
55 }
56 mutex_lock(domain->lock);
57 for (i = 0, p -= n; i < n; i++, p++, index++) {
58 struct probe **s = &domain->probes[index % 255];
59 while (*s && (*s)->range < range)
60 s = &(*s)->next;
61 p->next = *s;
62 *s = p;
63 }
64 mutex_unlock(domain->lock);
65 return 0;
66 }
3,cdev_add函数的实质
kobj_map()函数:用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里 。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
此处只是简单的一个保存过程,并没有将cdev和inode关联起来。
有了这个关联之后,当我们使用mknod 命令,就会创建一个inode节点,并且通过 dev_t将inode和cdev_map里面的cdev联系起来了。
四,cdev的创建
1,手动创建cdev
手动创建设备文件就是使用mknod /dev/xxx 设备类型 主设备号 次设备号的命令创建,
所以首先需要使用cat /proc/devices查看设备的主设备号并通过源码找到设备的次设备号。
2,自动创建cdev
class_create()/device_create() -- 导出相应的设备信息到"/sys"
/* 在/sys中导出设备类信息 */
cls = class_create(THIS_MODULE,DEV_NAME);
/* 在cls指向的类中创建一组(个)设备文件 */
for(i= minor;i<(minor+cnt);i++){
devp = device_create(cls,NULL,MKDEV(major,i),NULL,"%s%d",DEV_NAME,i);
}
3,mknod
device_register
device_add // 其中包含2个关键函数
// 将相关信息添加到/sys文件系统中
device_create_file
// 将相关信息添加到/devtmpfs文件系统中
devtmpfs_create_node
devtmpfs_create_node()函数的核心是调用了内核的 vfs_mknod()函数,这样就在devtmpfs系统中创建了一个设备节点,
当devtmpfs被内核mount到/dev目录下时,该设备节点就存在于/dev目录下,比如/dev/char_dev之类的。
devtmpfs_create_node函数流程:
devtmpfs_create_node(dev);
----devtmpfs_submit_req(&req, tmp);
--------wake_up_process(thread);
------------thread = kthread_run(devtmpfsd, &err, "kdevtmpfs");
----------------devtmpfs_work_loop();
--------------------handle(req->name, req->mode, req->uid, req->gid, req->dev);
------------------------handle_create(name, mode, uid, gid, dev);
----------------------------vfs_mknod(d_inode(path.dentry), dentry, mode, dev->devt);
--------------------------------dir->i_op->mknod(dir, dentry, mode, dev); //kernel\fs\namei.c
//D:\work\source_code\msm-kernel\msm_kernel\fs\hostfs\hostfs_kern.c
static const struct inode_operations hostfs_dir_iops = {
.create = hostfs_create,
.lookup = hostfs_lookup,
.link = hostfs_link,
.unlink = hostfs_unlink,
.symlink = hostfs_symlink,
.mkdir = hostfs_mkdir,
.rmdir = hostfs_rmdir,
.mknod = hostfs_mknod,
.rename = hostfs_rename2,
.permission = hostfs_permission,
.setattr = hostfs_setattr,
};
mknod函数流程:
.mknod
----hostfs_mknod
--------init_special_inode(inode, mode, dev); //kernel\fs\inode.c
------------if (S_ISCHR(mode)) {
----------------inode->i_fop = &def_chr_fops;
--------do_mknod(name, mode, MAJOR(dev), MINOR(dev));
------------mknod(file, mode, os_makedev(major, minor));
----------------sys_mknod(path, mode, dev);
--------------------my_syscall4(__NR_mknodat, AT_FDCWD, path, mode, dev);
------------------------__SYSCALL(__NR_mknodat, sys_mknodat)
----------------------------do_mknodat(dfd, filename, mode, dev);
--------------------------------user_path_create(dfd, filename, &path, lookup_flags);
------------------------------------filename_create(dfd, getname(pathname), path, lookup_flags);
SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode,
unsigned int, dev)
{
return do_mknodat(dfd, filename, mode, dev);
}
SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev)
{
return do_mknodat(AT_FDCWD, filename, mode, dev);
}
init_special_inode函数的实现:
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
} else if (S_ISBLK(mode)) {
inode->i_fop = &def_blk_fops;
inode->i_rdev = rdev;
} else if (S_ISFIFO(mode))
inode->i_fop = &pipefifo_fops;
else if (S_ISSOCK(mode))
; /* leave it no_open_fops */
else
printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"
" inode %s:%lu\n", mode, inode->i_sb->s_id,
inode->i_ino);
}
EXPORT_SYMBOL_NS(init_special_inode, ANDROID_GKI_VFS_EXPORT_ONLY);
/*
* Dummy default file-operations: the only thing this does
* is contain the open that then fills in the correct operations
* depending on the special file...
*/
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
如果node是一个char设备,会给i_fop 赋值一个默认的def_chr_fops,也就是说对该node节点,有一个默认的操作。
在open一个字符设备文件时,最终总会调用chrdev_open,然后调用各个char设备自己的file_operations 定义的open函数。
通过上面的分析,我们知道当device_add()注册device时,会调用devtmpfs_create_node()
但是这个调用是有个约束条件的, 这个约束条件是device中必须定义了devt这个设备号。
所以,对于很多的平台设备platform_devices(也就是那些在dts文件中定义的devices),在进行platform_device_add()时,并不会在/dev下面出现inode节点。
五,chrdev_open()
虽然我们有了字符设备的设备文件,inode也被构造并初始化了,但是在第一次调用chrdev_open()之前,这个inode和具体的chr_dev对象并没有直接关系,
而只是通过设备号建立的"间接"关系。在第一次调用chrdev_open()之后, inode->i_cdev才被根据设备号找到的cdev对象赋值,此后inode才和具体的cdev对象直接联系在了一起。
1,应用层怎么才能精确的调用到底层的驱动程序
-
在Linux文件系统中,每个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的所有信息,例如:文件类型,访问权限等。
-
在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,并且该文件会有对应的主设备号和次设备号。
-
在Linux操作系统中,每个驱动程序都要分配一个主设备号,字符设备的设备号保存在struct cdev结构体中。
-
在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。
注意:
常常我们认为struct inode描述的是文件的静态信息,即这些信息很少会改变。而struct file描述的是动态信息,即在对文件的操作的时候,
struct file里面的信息经常会发生变化。典型的是struct file结构体里面的f_pos(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。
这几个结构体关系如下图所示:
通过上图我们可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。
1)当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备)。还会分配一个struct file结构体。
2)根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备有一个struct cdev结构体。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口。
3)找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中。将struct cdev结构体的中记录的函数操作接口地址记录在struct file结构体的f_op成员中。
4)任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就可以通过fd来找到strut file,然后在由struct file找到操作字符设备的函数接口了。
2,struct inode 与 struct file
struct inode:
/*
* Keep mostly read-only and often accessed (especially for
* the RCU path lookup and 'stat' data) fields at the beginning
* of the 'struct inode'
*/
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
const struct inode_operations *i_op;
dev_t i_rdev;
loff_t i_size;
union {
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
void *i_private; /* fs or device private pointer */
} __randomize_layout;
struct file:
struct file {
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
u64 f_version;
/* needed for tty driver, and maybe others */
void *private_data;
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
3,open()代码流程
user space API:
int open(const char *pathname, int flags, mode_t mode);
open函数在kernel中的流程:
//kernel\fs\open.c
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
----do_sys_open(AT_FDCWD, filename, flags, mode);
--------do_sys_openat2(dfd, filename, &how);
------------struct file *f = do_filp_open(dfd, tmp, &op);
----------------filp = path_openat(&nd, op, flags | LOOKUP_RCU);
--------------------file = alloc_empty_file(op->open_flag, current_cred());
--------------------do_open(nd, file, op);
------------------------vfs_open(&nd->path, file);
----------------------------do_dentry_open(file, d_backing_inode(path->dentry), NULL);
do_dentry_open:
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
f->f_op = fops_get(inode->i_fop);
if (WARN_ON(!f->f_op)) {
error = -ENODEV;
goto cleanup_all;
}
... ...
/* normally all 3 are set; ->open() can clear them if needed */
f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
}
f->f_mode |= FMODE_OPENED;
}
chrdev_open:
/*
* Called every time a character special file is opened
*/
static int chrdev_open(struct inode *inode, struct file *filp)
{
const struct file_operations *fops;
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);
//根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段
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;
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops);
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;
}
实现私有的open函数,filp->f_op->open(inode, filp):
将设备属性封装成结构体后,在编写open函数时,将该结构体作为私有数据添加到设备文件中,如下:
//open函数
static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; //设置私有数据
return 0; //在私有数据设置好后,在write、read、close等函数中直接读取privata_data就可以访问设备结构体
}
六,总结
Linux中几乎所有的"设备"都是"device"的子类,无论是平台设备还是i2c设备还是网络设备,但唯独字符设备不是,cdev并不是继承自device,注册一个cdev对象到内核其实只是将它放到cdev_map中,直到对device_create的分析才知道此时才创建device结构并将kobj挂接到相应的链表,所以,基于历史原因,当下cdev更合适的一种理解是一种接口(使用mknod时可以当作设备),而不是而一个具体的设备,和platform_device,i2c_device有着本质的区别。
参考链接:
https://www.cnblogs.com/xiaojiang1025/p/6196198.html
https://www.cnblogs.com/schips/p/linux_device_model_and_cdev_miscdev.html
一文带你掌握 Linux 字符设备架构-linux中字符设备