OS - 字符驱动
本文档是总结编写一个完整的字符驱动需要的所有知识与技能。
字符设备通常用来与用户程序之间传输数据。
概念一、主次编号
在文件中 Documentation/admin-guide/devices.txt 查看所有静态分配的版本号
[root@localhost proc]# ls -l /dev/ total 0 crw-rw----. 1 root video 10, 175 Jan 6 23:19 agpgart crw-r--r--. 1 root root 10, 235 Jan 6 23:19 autofs crw-------. 1 root root 10, 234 Jan 6 23:19 btrfs-control brw-rw----. 1 root disk 253, 0 Jan 6 23:19 dm-0 brw-rw----. 1 root disk 253, 1 Jan 6 23:19 dm-1 crw-rw----. 1 root audio 14, 9 Jan 6 23:19 dmmidi
主编号标识设备相连的驱动,例如/dev/null 和 /dev/zero 都由驱动1来管理,而虚拟控制台和串口终端都由驱动4来管理。
次编号被内核用来决定引用哪个设备。
// include/linux/types.h typedef __u32 __kernel_dev_t; typedef __kernel_dev_t dev_t;
dev_t 是一个32位的量,12位用作主编号,20位用作次编号。
在<linux/kdev_t.h> 中定义一套宏,获得一个dev_t的主或次编号
// include/linux/kdev_t.h #define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 将dev_t 类型转换成主编号 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 将dev_t 类型转换成次编号
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) // 将主次编号转换成dev_t类型
在建立一个字符驱动时,第一次事是获取一个或多个设备编号来使用。注册函数为 register_chardev_region <linux/fs.h>
// include/linux/fs.h
extern int register_chrdev_region(dev_t, unsigned, const char *);
此函数在fs/char_dev.c文件中实现
// fs/char_dev.c
int register_chrdev_region(dev_t from, unsigned count, const char *name)
成功返回0, 出错返回负数
第一个参数 是dev_t from类型的起始设备编号
第二个参数是 无符号整形count表示请求连续设备编号的总数。
第三个参数name是连接到这个编号范围的设备的名字,它会出现在/proc/devices 和 sysfs 中
如果事先不知道需要分配的版本编号,使用alloc_chrdev_region动态分配版本编号
// include/linux/fs.h extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
此函数在fs/char_dev.c文件中实现
// fs/char_dev.c
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
dev 是一个只输出的参数,函数成功完成后,会持有分配范围的第一个数。
firstminor 是请求的第一个要用的次编号,通常情况下是0
count 与 name 与上面静态分配相同
一旦动态分配好版本号,可以在/proc/devices中读取
在不使用设备编号时,需要释放,一般在cleanup函数中调用
extern void unregister_chrdev_region(dev_t, unsigned);
获得版本号的主要代码例子
/* * Get a range of minor numbers to work with, asking for a dynamic * major unless directed otherwise at load time.
* dev 是 dev_t 结构的局部变量, 初始值为0
* scull_major 是全局变量,初始值为0
* scull_minor 是全局变量,初始值为0
* scull_nr_devs 是全局变量,计数器 */ if (scull_major) { dev = MKDEV(scull_major, scull_minor); result = register_chrdev_region(dev, scull_nr_devs, "scull"); } else { result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); // 使用动态分配获得版本号 scull_major = MAJOR(dev); // 使用宏获得主版本号 } // 对操作进行检查 if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n", scull_major); return result; }
概念二、file_operations 数据结构
file_operations 结构将 对象 与 操作 进行了连接。 <linux/fs.h> 头文件中
// include/linux/fs.h struct file_operations { /* * @ owner 指向一个拥有这个结构模块的指针 * 用来在它的操作还在被使用时阻止模块被卸载 * 一般情况下, 值为 THIS_MODULE * 定义在 <linux/module.h>的宏 */ struct module *owner; /* * @ llseek 方法用作改变文件中的当前读/写位置 * 新位置作为(正的)返回值 * 错误返回负值 * 如果这个函数指针为NULL, seek 调用会以潜在地 * 无法预知的方式修改file结构中的位置计数器 */ loff_t (*llseek) (struct file *, loff_t, int); /* * @ read 方法用来从设备中获取数据 * 如果NULL, EINVAL("Invalid argument") * 成功返回读取的字节数(signed size类型) * 如果失败,返回负数 */ ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); /* * @write 发送数据给设备 * 如果是NULL, -INVAL返回给调用write系统调用的程序 * 如果成功,返回成功写的字节数 * 如果失败,返回负数 */ 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 *); int (*iterate_shared) (struct file *, struct dir_context *); /* * @poll 是3个系统调用的后端 poll,epoll,select 查询对一个或多个 * 文件描述符的读或写是否会阻塞。 * 如果返回一个位掩码表示阻塞的读或写是可能的 * 则提供给内核信息用来使用进程睡眠直到I/O变为可能 * 如果驱动的poll方法为NULL,设备假定为不阻塞地可读可写 */ 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); /* * @mmap 请求将设备内存映射到进程的地址空间 * 如果这个方法是NULL, * 则mmap系统调用返回 -ENODEV */ int (*mmap) (struct file *, struct vm_area_struct *); /* * @open 打开一个驱动文件 * 如果为NULL * 则设备打开一直成功,但驱动不会得到通知 */ int (*open) (struct inode *, struct file *); /* * @flush 执行设备任何未完成的操作 * 如果flush为NULL * 则内核简单地忽略用户应用程序的请求 */ int (*flush) (struct file *, fl_owner_t id); /* * @release 文件结构被释放时引用这个操作 * 如果为NULL, 结构与open操作相同 */ int (*release) (struct inode *, struct file *); /* * @fsync 用户调用来刷新任何挂着的数据 * 如果这个指针是NULL * 则系统调用返回 -EINVAL */ int (*fsync) (struct file *, loff_t, loff_t, int datasync); /* * @fasync 通知设备它的FASYNC标志的改变 * 如果是NULL, 说明驱动不支持异步通知 */ int (*fasync) (int, struct file *, int); /* * @lock 方法用来实现文件加锁 */ int (*lock) (struct file *, int, struct file_lock *); /* * @sendpage 由内核调用来发送数据,一次一页,到对应的文件 */ ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /* * @get_unmapped_area 在进程的地址空间中找一个合适的位置来映射在底层设备上的内存段 */ unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /* * @ check_flags 允许模块检查传递给fnctl(F_SETFL...)调用的标志 */ 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_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64); ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64); } __randomize_layout;
一个实现file_operations的例子
struct file_operations scull_fops = { .owner = THIS_MODULE, .llseek = scull_llseek, .read = scull_read, .write = scull_write, .ioctl = scull_ioctl, .open = scull_open, // 将scull_open 方法关联 .release = scull_release, // 将scull_release 方法关联 };
概念三、file 文件结构
struct file 与 用户空间FILE无任何关系,是一个内核结构,不会出现在用户程序中,定义在<linux/fs.h>
文件结构代表一个打开的文件,由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。
文件所有实例都关闭后,内核释放这个数据结构。
// include/linux/fs.h struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ /* * @f_op 和文件关联的操作 */ 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; /* * @f_flags 文件标志,定义在<linux/fcntl.h>中 * O_RDONLY * O_NONBLOCK 请求是否是非阻塞操作 * O_SYNC */ unsigned int f_flags; /* * @ f_mode 确定文件模式(可读/可写/可读可写) * */ fmode_t f_mode; struct mutex f_pos_lock; /* * @f_pos 当前读写位置 * loff_t 在所有平台都是64位 */ 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 */ /* @ private_data 在系统调用间保留状态信息 */ 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; struct list_head f_tfile_llink; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; } __randomize_layout __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
概念四、inode 结构
inode 结构由内核在内部用来表示文件,可能有代表单个文件的多个打开描述符的许多文件结构
但是都指向一个单个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; #ifdef CONFIG_FS_POSIX_ACL struct posix_acl *i_acl; struct posix_acl *i_default_acl; #endif const struct inode_operations *i_op; struct super_block *i_sb; struct address_space *i_mapping; #ifdef CONFIG_SECURITY void *i_security; #endif /* Stat data, not accessed from path walking */ unsigned long i_ino; /* * Filesystems may only read i_nlink directly. They shall use the * following functions for modification: * * (set|clear|inc|drop)_nlink * inode_(inc|dec)_link_count */ union { const unsigned int i_nlink; unsigned int __i_nlink; }; /* * @ i_rdev 代表设备文件的节点 * 包含实际的设备编号 */ dev_t i_rdev; loff_t i_size; struct timespec i_atime; struct timespec i_mtime; struct timespec i_ctime; spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */ unsigned short i_bytes; unsigned int i_blkbits; enum rw_hint i_write_hint; blkcnt_t i_blocks; #ifdef __NEED_I_SIZE_ORDERED seqcount_t i_size_seqcount; #endif /* Misc */ unsigned long i_state; struct rw_semaphore i_rwsem; unsigned long dirtied_when; /* jiffies of first dirtying */ unsigned long dirtied_time_when; struct hlist_node i_hash; struct list_head i_io_list; /* backing dev IO list */ #ifdef CONFIG_CGROUP_WRITEBACK struct bdi_writeback *i_wb; /* the associated cgroup wb */ /* foreign inode detection, see wbc_detach_inode() */ int i_wb_frn_winner; u16 i_wb_frn_avg_time; u16 i_wb_frn_history; #endif struct list_head i_lru; /* inode LRU list */ struct list_head i_sb_list; struct list_head i_wb_list; /* backing dev writeback list */ union { struct hlist_head i_dentry; struct rcu_head i_rcu; }; u64 i_version; atomic_t i_count; atomic_t i_dio_count; atomic_t i_writecount; #ifdef CONFIG_IMA atomic_t i_readcount; /* struct files open RO */ #endif const struct file_operations *i_fop; /* former ->i_op->default_file_ops */ struct file_lock_context *i_flctx; struct address_space i_data; struct list_head i_devices; union { struct pipe_inode_info *i_pipe; struct block_device *i_bdev; /* * @i_cdev 是内核内部结构,代表字符设备 * 当节点指的是一个字符设备文件时 * 指向这个结构 */ struct cdev *i_cdev; char *i_link; unsigned i_dir_seq; }; __u32 i_generation; #ifdef CONFIG_FSNOTIFY __u32 i_fsnotify_mask; /* all events this inode cares about */ struct fsnotify_mark_connector __rcu *i_fsnotify_marks; #endif #if IS_ENABLED(CONFIG_FS_ENCRYPTION) struct fscrypt_info *i_crypt_info; #endif void *i_private; /* fs or device private pointer */ } __randomize_layout;
可以使用函数从inode结构中获得主次编号
// include/linux/fs.h
static inline unsigned iminor(const struct inode *inode); static inline unsigned imajor(const struct inode *inode);
概念五、注册字符设备
内核在内部使用类型 struct cdev 的结构代表字符设备,在内核调用你的设备操作前,必须先分配并注册一个或几个这些结构。
其头文件 <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; } __randomize_layout;
两种方法来分配和初始化这些结构
第一种,在运行时获得一个独立的cdev结构
struct cdev *my_cdev = cdev_alloc(); my_cdev -> ops = &my_fops;
第二种,初始化已经分配的结构
// include/linux/cdev.h void cdev_init(struct cdev *, const struct file_operations *);
scull 示例代码设备注册
第一步, 使用struct scull_dev 类型表示每个设备。
struct scull_dev { struct scull_qset *data; /* Pointer to first quantum set */ int quantum; /* the current quantum size */ int qset; /* the current array size */ unsigned long size; /* amount of data stored here */ unsigned int access_key; /* used by sculluid and scullpriv */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure */ };
第二步:关注cdev的初始化流程
/* * Set up the char_dev structure for this device. */ static void scull_setup_cdev(struct scull_dev *dev, int index) { int err, devno = MKDEV(scull_major, scull_minor + index); cdev_init(&dev->cdev, &scull_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &scull_fops; err = cdev_add (&dev->cdev, devno, 1); /* Fail gracefully if need be */ if (err) printk(KERN_NOTICE "Error %d adding scull%d", err, index); }
概念六、open 和 release
/* * Open and close */ int scull_open(struct inode *inode, struct file *filp) { struct scull_dev *dev; /* device information */ dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev; /* for other methods */ /* now trim to 0 the length of the device if open was write-only */ if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) { if (down_interruptible(&dev->sem)) return -ERESTARTSYS; scull_trim(dev); /* ignore errors */ up(&dev->sem); } return 0; /* success */ } int scull_release(struct inode *inode, struct file *filp) { return 0; }

浙公网安备 33010602011771号