Linux 内核——字符设备驱动框架详解
0. Linux 进程控制块与文件对象
在 linux 操作系统中,进程对应的 PCB 中维护一个已打开文件描述符表,如下代码片段这个表本质是一个数组,其下标就是文件句柄(我们都知道 Windows 中是指针、而 linux 是整数)。每打开一个文件,就在该数组中放入这个文件对应的文件对象的指针(struct file*),并且返回数组的下标。在使用 open() 打开设备节点时传入的 flags、mode 等参数都会被记录在对应中。在读写文件时,文件的当前偏移地址则会保存在 f_pos 成员中。
// ~/linux-4.4.232/include/linux/sched.h
struct task_struct {struct fs_struct *fs; /* filesystem information */struct files_struct *files; /* open file information */
};// ~/linux-4.4.232/include/linux/fdtable.h
struct files_struct {struct file* fd_array[32]; // 32 or 64
};// ~/linux-4.4.232/include/linux/fs.h
struct file {struct path f_path;struct inode* f_inode; /* cached value */const struct file_operations* f_op;unsigned int f_flags;fmode_t f_mode;struct mutex f_pos_lock;loff_t f_pos;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
2. cdev object 管理
cdev 其实是对驱动的抽象(struct cdev),它实现了文件接口(struct file_operations)、继承了 struct kobject。char_dev.c 源文件中定义了一个存储 cdev 的名为 cdev_map 的表,当cdev_add() 被调用时, 函数体调用 kobj_map() 把 cdev 对象添加到其中。struct kobj_map 本质是一个哈希表, 它用主设备号作为哈希桶的下标,通过它进行字符设备的管理(增、删、查)。
// ~/linux-4.4.232/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;
};// ~/linux-4.4.232/fs/char_dev.c
static struct kobj_map *cdev_map;// ~/linux-4.4.232/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;
};

2. 在根文件系统创建字符设备节点
有了字符设备驱动后用户层并不能直接使用它们,用户层只有通过字符设备节点才能访问到驱动,所以必须为字符设备生成对应的设备节点。之前基于 Qemu 搭建内核开发环境时使用命令 sudo mknod -m 666 node_name c major minor 创建设备节点,然后将它们写入 ext3 格式的文件系统中,制作出了根文件系统,最后系统启动后可以看到我们创建的设备节点。同样调用 device_create() 函数会触发 kobject_uevent(KOBJ_ADD) 调用,进而生成设备节点。这两种调用都是触发了 mknod() 系统调用。
device_create()device_add() kobject_uevent(KOBJ_ADD) mknod() // 在根文件系统创建 /dev/ledSYSCALL_DEFINE3(mknod)sys_mknodat()do_mknodat()user_path_create() // 路径解析may_mknod() // 权限检查vfs_mknod()may_create() // 创建权限检查security_inode_mknod() // 安全模块检查dir->i_op->mknod() // 文件系统特定实现ext4_mknod() // ext4 实现ext4_new_inode() // 创建新 inodeinit_special_inode() // 初始化设备 inodeencode_dev_to_disk() // 编码设备号到磁盘 inodeext4_add_nondir() // 关联 dentry 和 inodefsnotify_create() // 文件创建通知
4. 调用 open() 函数打开字符设备文件
有了根文件提供的设备节点信息,用户便可以按需读写它们。应用层调用库函数 open() 会触发软中断,进入内核空间调用 sys_open(), 然后调用 vfs_open(),最后驱动层 drv_open() 被调用。当 open 一个字符设备时会发生如下操作:
- VFS 会找到与之对应的节点,并在内存中建立与之对应的 inode 对象(struct inode);
// ~/linux-4.4.232/include/linux/fs.h
struct inode {umode_t i_mode;dev_t i_rdev;union {struct pipe_inode_info *i_pipe;struct block_device *i_bdev;struct cdev *i_cdev;char *i_link;};
};
- 内核还会创建一个 file 对象(struct file),将其保存在 PCB 的已打开文件描述符表中;
- inode 对象中 i_mode 成员会指示它是一个字符设备,然后依据
inode 对象成员 i_rdev 去 cdev_map 里检索对应的 cdev 对象; - inode 对象的 i_cdev成员(struct cdev*) 指向上一步检索到的 cdev 对象;
- 用刚才检索到的 cdev 对象的ops指针去替换第2步创建的 file 对象的 f_op 指针的指向;
如下详细过程:
open("/dev/led", mode, flag) // [0] 库函数调用SYSCALL_DEFINE3(open) // [1] 系统调用 sys_opendo_sys_open()do_sys_open()do_filp_open()path_openat()link_path_walk() // 路径解析do_last()vfs_open() // [2] vfs 调用do_dentry_open()chrdev_open() // 字符设备特殊处理 kobj_lookup() // [注]查找设备驱动replace_fops() // 替换文件操作led_fops->open() // [3] 驱动open方法
5. 字符设备的读(写)、关闭 操作
字符设备文件被打开之后就可以对其进行读、写、关闭等操作。这些操作接口都由 struct file 的 ops 提供(源头是驱动)。
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 (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);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 *, loff_t, loff_t, 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 **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endif
};
6. 参考文献
- Linux驱动最基本的驱动框架 LED驱动
- Linux 字符设备驱动结构(一)——cdev 结构体、设备号相关知识解析
- Linux 字符设备驱动结构(二)—— 自动创建设备节点
