概述
本章节以应用开始访问字符设备为切入点,逐步深入分析字符设备的软件层次、组成框架和交互、如何编写字符设备驱动、设备文件的创建和mdev原理,并对相关接口及涉及到的结构体做一个简要介绍,展示字符设备驱动的全面详情
应用示例
int main(void) {
char buf[10] = {0,};
int fd = open("/dev/led",O_RDWR);
read(fd,buf,10);
memset(buf,1,10);
write(fd,buf,10);
close(fd);
}
/dev目录与文件系统
1./dev目录
/dev是根文件系统下的目录文件,/代表根目录,其挂载的是根文件系统yaffs格式,通过读取根目录的这个文件,就能分析list出其包含的各个设备,其中就包含/dev这个子目录。即/根目录是一个文件,其真实存在与flash介质
ls / 命令即会使用/挂在的yaffs文件系系统来读取根目录的内容,然后list出dev(是一个目录),这时还不需要去读取dev这个目录下的文件内容。
cd /dev命令会分析/dev挂载的文件系统的超级块信息,superblock,而不再理会flash 中dev目录下的数据
Notes:
dev是镜像ramfs对应的flash block mount上来的真实flash数据
2./tmpfs文件系统
/dev在根文件系统构建的时候会挂载为tmpfs。tmpfs是一个基于虚拟内存的文件系统,主要使用RAM和SWAP(RAM只是使用物理内存)。
以后读取dev这个目录的操作都转到tmpfs的操作,确切的讲都是针对RAM的操作,而不再是通过yaffs文件系统的操作函数集去访问flash介质。
tmpfs文件系统是基于RAM,所以掉电后会消失。
Notes:
/dev目录下的设备文件都是每次创建后会消失
/dev目录下的设备文件来源:
在编译过程中制作文件系统时会构建/dev目录,每次在linux系统启动后由设备对应的驱动程序来创建或者mknode手动创建设备文件(/dev/NULL、/dev/console实在制作根文件系统时静态创建)
设备文件的创建
/dev目录下的设备文件基本时通过mdev来动态创建。mdev是一个用户态的应用程序,位于bosybox的工具箱内
1.创建过程:
1>驱动初始化或者总线匹配后会调用驱动的probe接口,上述步骤最终均需调用device_create(设备类、设备号、设备名),在/sys/class/设备类目录下生成唯一的设备属性文件(包括设备号、设备名等信息),并且发送uevent事件(KOBJ_ADD、环境变量、路径等信息)到用户空间(通过socket方式),然后通知mdev在/dev下创建相应的设备节点
2>mdev是一个work_thread线程,收到事件后会分析/sys/class/设备类的对应文件,最终调用mknod动态的创建设备文件,而这个设备文件的主要内容为设备号(这个设备文件对应的inode会记录文件的属性是一个设备文件--其他还包含目录、一般文件、符号链接等)。应用程序open(device_name...)最重要的一步就是通过文件系统接口来获取该设备文件的内容---设备号
3>如果初始化过程没有调用device_create接口来创建设备文件,则需要手动执行mknod命令来创建设备文件(替代mdev的过程),已暴露设备文件供应用访问
2.tmpfs文件系统mount分析
------------------itop4412_kernel_3.0/init/main.c-----------------------
start_kernel(void)
rest_init(void)
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
kernel_init(void * unused)
do_basic_setup(void)
init_tmpfs(void) // 初始化tmpfs文件系统
driver_init(); // 初始化sys下面的子系统
init_irq_proc(); // 初始化/proc/irq目录
do_initcalls(); // 加载所有模块及设备驱动,待tmpfs文件系统挂载完毕,驱动模块将利用uevent事件触发mdev在tmpfs文件系统对应的dev目录下一一创建各设备文件
register_filesystem(&tmpfs_fs_type);
static struct file_system_type tmpfs_fs_type = {
.owner = THIS_MODULE,
.name = "tmpfs",
.mount = shmem_mount,
.kill_sb = kill_litter_super,
};
shm_mnt = vfs_kern_mount(&tmpfs_fs_type, MS_NOUSER,tmpfs_fs_type.name, NULL);
// vfsmount描述得是一个独立的文件系统挂载信息,同一文件系统的所有目录和文件均隶属于同一个vfsmount(详见该结构体描述信息)
struct vfsmount *vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
// 初始化一个新的struct vfsmount结构,并初始化里面的一部分信息(链表指针、mnt_devname等)
struct vfsmount *mnt = alloc_vfsmnt(name);
root =mount_fs(type, flags, name, data);
// // type即对应 register_filesystem(&tmpfs_fs_type)注册的tmpfs_fs_type,调用具体的文件系统挂载操作
type->mount(type, flags, name, data);
mount_nodev(fs_type, flags, data, shmem_fill_super);
sget(fs_type, NULL, set_anon_super, NULL);
// 申请一个超级块(超级块对应一个具体的文件系统)
alloc_super(type)
// 填充超级块fill_super即为shmem_fill_super
fill_super(s, data, flags & MS_SILENT ? 1 : 0);
int shmem_fill_super(struct super_block *sb, void *data, int silent) {
//填充超级块
sb->s_op = &shmem_ops; // 超级块(文件系统操作集合)
// 创建目录i节点,挂载文件系统或者文件系统下创建目录是S_IFDIR,文件系统下创建设备S_IFCHR,详见mknod
inode = shmem_get_inode(sb, NULL, S_IFDIR | sbinfo->mode, 0, VM_NORESERVE);
// 依托inode节点申请目录并由此产生超级快的根目录(生成文件系统根目录)
root = d_alloc_root(inode);
sb->s_root = root;
}
struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,int mode, dev_t dev, unsigned long flags)
mnt->mnt_root = root;
mnt->mnt_sb = root->d_sb;
mnt->mnt_mountpoint = mnt->mnt_root;
mnt->mnt_parent = mnt;
展开:
struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,int mode, dev_t dev, unsigned long flags) {
// 申请inode节点并作相关通用成员初始化(创建时间等)
inode = new_inode(sb);
switch (mode & S_IFMT) {
default:
inode->i_op = &shmem_special_inode_operations;
init_special_inode(inode, mode, dev);
break;
case S_IFREG:
inode->i_mapping->a_ops = &shmem_aops;
inode->i_op = &shmem_inode_operations;
inode->i_fop = &shmem_file_operations;
mpol_shared_policy_init(&info->policy,
shmem_get_sbmpol(sbinfo));
break;
// 文件系统初始化先得由根目录,上面shmem_get_inode参数为S_IFDIR已指明
case S_IFDIR:
inc_nlink(inode);
/* Some things misbehave if size == 0 on a directory */
inode->i_size = 2 * BOGO_DIRENT_SIZE;
// 4.2 dir->i_op来源
inode->i_op = &shmem_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
break;
case S_IFLNK:
/*
* Must not load anything in the rbtree,
* mpol_free_shared_policy will not be called.
*/
mpol_shared_policy_init(&info->policy, NULL);
break;
}
展开:
static const struct inode_operations shmem_dir_inode_operations = {
#ifdef CONFIG_TMPFS
.create = shmem_create,
.lookup = simple_lookup,
.link = shmem_link,
.unlink = shmem_unlink,
.symlink = shmem_symlink,
.mkdir = shmem_mkdir,
.rmdir = shmem_rmdir,
.mknod = shmem_mknod,
.rename = shmem_rename,
#endif
#ifdef CONFIG_TMPFS_XATTR
.setxattr = shmem_setxattr,
.getxattr = shmem_getxattr,
.listxattr = shmem_listxattr,
.removexattr = shmem_removexattr,
#endif
#ifdef CONFIG_TMPFS_POSIX_ACL
.setattr = shmem_setattr,
.check_acl = generic_check_acl,
#endif
};
3.设备的添加
device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
device_register(dev);
device_initialize(dev);
device_add(dev);
// 在/dev下创建对应的/dev/xxx设备节点节点
devtmpfs_create_node(dev);
...依据设备模型创建sysfs下对应的设备相关文件...
// 同时mdev在/dev下创建对应的设备节点------mdev最终调用mknod
kobject_uevent(&dev->kobj, KOBJ_ADD);
// 依据遍历当前总线上的驱动探测属于自己的设备驱动并进行设备必要的初始化并绑定驱动操作集fops
bus_probe_device(dev);
4.mknod接口
见<linux VFS文件系统(二)>,本节不再分析
----itop4412_kernel_3.0/fs/inode.c----
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 = &def_fifo_fops;
else if (S_ISSOCK(mode))
inode->i_fop = &bad_sock_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);
}
file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
-----itop4412_kernel_3.0/fs/char_dev.c------
chrdev_open(struct inode *inode, struct file *filp)
// 以i_rdev(设备号)遍历cdev_map(成员填充时在驱动初始时的cdev_add阶段填充)
kobj_lookup(cdev_map, inode->i_rdev, &idx);
// 获取设备操作集
filp->f_op = fops_get(p->ops);
// 使用设备的操作集的open方法打开设备
filp->f_op->open(inode,filp);
-----itop4412_kernel_3.0/fs/char_dev.c----
cdev_map讲解:
字符设备添加3步骤:
1> 申请cdev: struct cdev *pcdev = cdev_alloc(void)
2> 绑定fops操作集至cdev: void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3> 依据设备号将cdev注入cdev_map对应成员: int cdev_add(struct cdev *p, dev_t dev, unsigned count)
cdev_add
p->dev = dev;
// 遍历cdev_map数组,找到一个未使用的绑定注入的cdev以供open时依据设备号定位
kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
Notes:
cdev_map初始化过程
start_kernel->vfs_caches_init->chrdev_init()
总结:
设备文件的创建是在tmpfs内存文件系统中创建inode,并立即绑定字符设备抽象操作集+设备号+设备名,为后续字符设备的实际操作做准备工作
创建设备文件过程展开:
1>根据父目录绑定的超级块确定文件系统
2>申请一个inode,确定组信息、时间信息
3>设置inode操作集,根据入参的设备类别设置inode对应的文件操作集,设置inode对应的设备号,参数“设备类别及属性、设备号”作用 // 从init_special_inode可知只有字符设备和块设备才具备该属性
4>由设备全路径名产生dentry,参数“设备名”作用
5>dentry与inode绑定,由此形成了设备全路径与inode的绑定,在此基础上用户在open过程中由设备全路径名->dentry->inode直至设备对应的操作集file_opertions即打通
6>文件系统归根结底为目录和文件,基本元素是inode,一切信息都存储于inode,并通过inode形成关联试图
7>inode通过设备号最终定位到字符设备的真实操作集
8>上述过程也揭露了文件系统的核心功能(以sysfs为例)
1>>向用户态展示文件的相关信息(即ls所看到的内容-目录树状图及文件信息,实际上只是将要显示文件集的成员对应的inode成员通过串口显示在终端,供用户可视化参考)
2>>帮助应用定位文件设备驱动注册的操作集,供用户操作设备
9>mknod存储是基于文件系统的行为
接口说明
dev_t
设备号的数据类型,32位,前12位为主设备号,后20位位次设备号
int MAJOR(dev_t dev);
由设备号抽取主设备号
主设备号标志设备对应的驱动程序
int MINOR(devt dev);
由设备号抽取次设备号
此设备号标志设备文件所指的设备
dev_t MKDEV(unsigned int major, unsigned int minor);
由主/次设备号构造dev_t
int register_chrdev_region(dev_t from, unsigned count, const char *name);
作用:指定设备编号来静态注册一个字符设备
from:注册的指定其实设备编号,比如:MKDEV(100,0),表示起始设备对应的主设备号为100,次设备号为0
count:需要连续注册的次设备号的个数,比如上例次设备号的起始为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations的操作方法
*name:字符设备名称,用户态访问的标志
返回值小于0表示注册失败
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
作用:动态分配一个字符设备,注册成功则将主次设备号放入dev
*dev:存放其实设备编号的指针,注册成功即被赋值为设备号,由MAJOR/MINOR抽离主次设备号
baseminor:起始次设备号
count:需要连续注册的次设备号数量
name:字符设备名称
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
作用:初始化cdev结构体,并将fops放入cdev->ops
绑定操作集到对应的设备号
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
作用:将cdev结构体添加到系统中,并将dev(注册号的设备)放入cdev->dev里,count(次设备号个数)放入cdev->count
int cdev_del(struct cdev *cdev, dev_t dev, unsigned count);
作用:将系统中的cdev结构体删除
void unregister_chrdev_region(dev_t from, unsigned count);
作用:注销字符设备
from:注销指定起始设备号
count:需要连续注销的次设备号个数,与注册对应
结构体说明
1.struct cdev
struct cdev {
struct kobject kobj; //融入kobj框架
struct module *owner; //所属模块
const struct file_operations *ops; //设备操作集
struct list_head list; //挂在设备的链表头,用于管理挂在的各子设备集
dev_t dev; //设备编号
unsigned int count; //该种设备类型数量
}
Notes:从中可以看出设备驱动的核心:申请设备结构体资源后填充设备编号和操作集
2.struct file_opertions
struct file_opertions{
......
int (*open) (struct inode *, struct file *);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*release) (struct inode *, struct file *);
......
}
Notes:设备编号的驱动程序操作集,通过cdev_init函数来实现设备号与操作集的绑定文件为一个对象,而操作函数就是它的方法,动作作用于它本身
3.file结构
struct file {
......
const struct file_operations *f_op; // 文件操作集
fmode_t f_mode; // 文件模式,是否可读写
loff_t f_pos; // 当前读/写位置
unsigned int f_flags; // 文件标志,是否阻塞式操作
void *private_data; // 驱动可以用作任意目的或者忽略该字段,可以指向分配的数据,但release时需要释放,一般用于保存硬件资源
......
}
Notes:是一个内核结构,在open是创建,传递给该文件上进行操作的所有函数,知道最后的close,被关闭后会释放该结构体
4.inode结构
struct inode {
......
const struct inode_operations *i_op;
dev_t i_rdev; // 对表示设备文件的inode结构,该字段包含了正在的设备编号
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev; // 字符设备的内核的内部结构,当inode指向一个字符设备文件时,该字段包含指向struct cdev结构的指针
};
......
}
总结:
后三者关系:
file_operations绑定于file
inode与file:
inode为真实设备
file为进程相关的文件描述符,和inode为多为一的关系
设备打开是首先由设备路径名去索引对应文件系统,找到真实的文件,并打开,这样当进程再次打开时则不用在重复该动作,直接在进程对应的file索引表就可以找到设备并返回索引号地址给用户态用
//上述过程详见进程及文件系统细节部分
当打开文件时由inode找到真实设备,在chrdev_open中通过inode找到真实设备的i_cdev,将其指定的文件操作集分享给file用于绑定设备的操作集,并由其调用设备真实的open(用i_cdev 马上open也可以),file与进程相关,file为内核结构,最终映射为进程打开文件集的索引地址给用户态使用即FILE *fp,这样后面在用户态操作read/write等函数时由于之前已经绑定了文件真实的read/write等就可以直接使用了