本章的目的是编写一个完整字符设备驱动。例子代码来自于LDD3的scull范例。scull字符设备是对
内存的操作,不涉及具体的硬件,所以理论上在任何一台机器上都可以测试,这很方便。
4.1 scull的设计
scull字符设备的驱动将默认对应4个scull设备(文件),scull0-scull3。当然也可以通过在编译时、加载时、运行时3种不同的方式改变scull设备(文件)的数目,后面将逐一说明每种方式的具体做法。
4.2 字符设备的主、次设备号
字符设备的存取是通过其在文件系统中的名字来存取,就如普通的文件通过名字来存取一样。通过
ls -l /dev命令查看/dev目录,可看到类似如下输出:
其中第1列的字母为c的设备为字符类设备,而第5列的10, 175和10, 235分别表示的字符设备的主、次设
备号:其中的10表示主设备号,175和235表示次设备号。
通常来讲,主设备号用来连接设备驱动,如上图中/dev/null和/dev/zero都是由同一驱动来管理。
但也不总是这样,现代的Linux允许多个设备驱动共享同一主设备号,但大部分还是一个主设备号对应一
个驱动。
4.2.1 设备号的内部表示
设备号在内部用定义在中的dev_t类型来表示。设备号的实际类型随着内核版本的变化而变化,不能简单的将设备号认为是某个特定类型,且主次设备号的位分布也是如此。
头文件提供了如下的辅助宏来帮助我们管理设备号:
MAJOR(dev_t dev):获取设备的主设备号;
MINOR(dev_t dev):获取设备的从设备号;
MKDEV(int major, int minor):根据指定的主次设备号构造dev_t。
4.2.2 分配、释放设备号
设备号由系统统一管理,所以驱动使用的设备号需向系统申请。字符驱动申请设备号有静态和动态
两种方式。
静态申请设备号函数:int register_chrdev_region(dev_t first, unsigned int count, char *name);
参数first为驱动想要申请的第一个设备号,count参数指出从设备号first开始想要申请的连续的设备号
个数,name参数代表申请的设备号区间。name参数指定的名称会出现在/proc/devices文件中。函数调用
成功返回0,否则返回负数。
动态申请设备号函数:int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);
参数dev指向申请返回的设备号,参数firstminor指定第一个从设备号,剩余参数的含义同静态申请函
数。函数调用成功返回0,否则返回负数。
申请的设备号最终都必须要被释放。释放字符设备号的函数为:void unregister_chrdev_region(dev_t first, unsigned int count);
释放设备号的时机通常是在设备驱动模块的退出函数中。
4.2.3 动态分配主设备号
部分主设备号已被静态分配给一些通用设备,可以查看源码文档目录下Documentation/devices.txt
查看预分配的主设备号。采用动态分配方式通常是更加的设备号分配方案,但动态分配方式也有它的缺
点:我们事先无法得知分配到的设备号,但这并不成问题,因为一旦设备号被分配后,你可以查看文件
/proc/devices来获得主设备号。如图:
图中第1列是设备的主设备号。
4.2.4 scull模块的加载和卸载、scull设备文件的创建和删除
我们在测试scull驱动的时候需要先创建scull字符设备,我们可以通过下面的脚本来完成scull设备的创建和删除。
scull模块加载、scull设备文件创建脚本:#!/bin/sh
# scull_load
module="scull"
device="scull"
mode="664" # rw-rw-r--
group="bill" # change it to your user name
# invoke insmod with all arguments we got
/sbin/insmod ./$module.ko $* || exit 1
# remove stale nodes
rm -f /dev/${device}[0-3]
majors=($(awk "\$2==\"$module\" {print \$1}" /proc/devices))
major=${majors[0]}
mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3
ln -sf ${device}0 /dev/${device}
# give appropriate group/permissions, and change the group
chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]
scull模块卸载、scull设备文件删除脚本:#!/bin/sh
# scull_unload
module="scull"
device="scull"
# invoke rmmod with all arguments we got
/sbin/rmmod $module $* || exit 1
# Remove stale nodes
rm -f /dev/${device} /dev/${device}[0-3]
另外,除了手动加载scull模块外,Linux系统还提供模块的自动加载机制。
方法一:
(1) 拷贝模块到内核模块目录sudo cp scull.ko /lib/modules/`uname -r`/kernel/drivers/char/scull.ko
(2) 添加模块名到启动时加载模块列表文件/etc/modules:sudo vi /etc/modules
在打开的文件中添加一行:scull
如果是带参数启动模块,在上面行的后面带模块参数即可,类似于insmod加载模块带参数的情况。
(3) 更新内核模块依赖文件sudo depmod
(4) 重启系统,用lsmod命令可查看模块是否成功加载。
方法二:
(1) 拷贝模块到内核模块目录sudo cp scull.ko /lib/modules/`uname -r`/kernel/drivers/char/scull.ko
(2) 编写模块加载脚本/etc/init.d/scull,该脚本会在系统启动时被调用#!/bin/sh -e
modprobe scull
同样,scull模块的参数也可以在添加在modprobe调用行的后面。
创建模块加载脚本的软链接:sudo ln -s /etc/init.d/scull /etc/rcS.d/Sscull
(3) 更新内核模块依赖文件sudo depmod
(4) 重启系统,用lsmod命令可查看模块是否成功加载。
4.3 几个重要数据结构的说明
在正式编写字符设备驱动前,先介绍几个字符设备驱动中会用到重要数据结构。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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
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 *, 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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};
结构体file_operations定义在头文件中,简单来讲,除了第1个成员owner外,其他的成员无
一例外是字符设备所支持的操作,如果这些表示操作的函数指针被置为NULL的话,会在被调用时执行内核缺
省的操作。下面对file_operations中的一些成员作简单说明。
struct
module *owner
表示拥有该结构的模块,用于防止该结构还在使用的情况下模块被卸载。对该成员的赋值,几乎无一例外被
赋成THIS_MODULE。
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
发起文件的异步读写操作。
int (*readdir) (struct file *, void *, filldir_t);
设备文件应该将其置为NULL。该成员主要是用于文件系统。
unsigned int (*poll) (struct file *, struct poll_table_struct *);
系统调用poll,epoll,select最终都对应到该成员,用于查询对一个或多个文件描述符的读写操作是否会被
阻塞。poll可能是进程进入睡眠。如果该成员被赋为NULL,会假定对该设备的读写操作不会被阻塞。
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 *);
设备执行的第一个操作,可以为NULL。
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
当file结构体被释放的时候调用。
int (*fsync) (struct file *, int datasync);
fsync系统调用最终对应到该成员,刷新任何挂起的数据。
int (*aio_fsync) (struct kiocb *, int datasync);
fsync的异步版本。
int (*lock) (struct file *, int, struct file_lock *);
用于实现文件锁。
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
按page发送数据。
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
没有被映射的设备存储空间。
int (*check_flags)(int);
用于检查传送给fcntl(F_SETFL...)的flags。
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_headfu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct pathf_path;
#define f_dentryf_path.dentry
#define f_vfsmntf_path.mnt
const struct file_operations*f_op;
spinlock_tf_lock; /* f_ep_links, f_flags, no IRQ */
#ifdef CONFIG_SMP
intf_sb_list_cpu;
#endif
atomic_long_tf_count;
unsigned int f_flags;
fmode_tf_mode;
loff_tf_pos;
struct fown_structf_owner;
const struct cred*f_cred;
struct file_ra_statef_ra;
u64f_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_headf_ep_links;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space*f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
结构体file定义在头文件中。该结构体用于描述1个打开的文件描述符,和c库的FILE结构体毫
无关系。由内核在open调用时创建,在最后一次close,内核释放该结构体。下面是对结构体的部分成员的
描述。
fmode_tf_mode;
标记文件是可读还是可写等,如FMODE_READ,FMODE_WRITE。
unsigned int f_flags;
文件标记,如O_RDONLY,O_NONBLOCK,O_SYNC等。
const struct file_operations *f_op;
文件绑定的操作。
void *private_data;
文件自定义数据。
struct inode {
/* RCU path lookup touches following: */
umode_ti_mode;
uid_ti_uid;
gid_ti_gid;
const struct inode_operations*i_op;
struct super_block*i_sb;
spinlock_ti_lock;/* i_blocks, i_bytes, maybe i_size */
unsigned inti_flags;
struct mutexi_mutex;
unsigned longi_state;
unsigned longdirtied_when;/* jiffies of first dirtying */
struct hlist_nodei_hash;
struct list_headi_wb_list;/* backing dev IO list */
struct list_headi_lru;/* inode LRU list */
struct list_headi_sb_list;
union {
struct list_headi_dentry;
struct rcu_headi_rcu;
};
unsigned longi_ino;
atomic_ti_count;
unsigned inti_nlink;
dev_ti_rdev;
unsigned inti_blkbits;
u64i_version;
loff_ti_size;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_ti_size_seqcount;
#endif
struct timespeci_atime;
struct timespeci_mtime;
struct timespeci_ctime;
blkcnt_ti_blocks;
unsigned short i_bytes;
struct rw_semaphorei_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_spacei_data;
#ifdef CONFIG_QUOTA
struct dquot*i_dquot[MAXQUOTAS];
#endif
struct list_headi_devices;
union {
struct pipe_inode_info*i_pipe;
struct block_device*i_bdev;
struct cdev*i_cdev;
};
__u32i_generation;
#ifdef CONFIG_FSNOTIFY
__u32i_fsnotify_mask; /* all events this inode cares about */
struct hlist_headi_fsnotify_marks;
#endif
#ifdef CONFIG_IMA
atomic_ti_readcount; /* struct files open RO */
#endif
atomic_ti_writecount;
#ifdef CONFIG_SECURITY
void*i_security;
#endif
#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 */
};
结构体inode定义在头文件中。内核用该结构体描述文件,因此该结构体和用于描述打开文件
描述符的file结构体不同。下面是对该结构体部分成员的描述。
dev_t i_rdev;
描述设备文件,包含设备文件的设备号。
union {
struct pipe_inode_info*i_pipe;
struct block_device*i_bdev;
struct cdev*i_cdev;
};
管道设备、块设备、字符设备在内核内部的表示。
另外,有2个用于从inode结构体获取设备号的辅助函数。unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
4.4 字符设备注册
4.4.1 字符设备的分配和初始化
字符设备用下面的结构体来描述:struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
可以通过静态或动态的方式来分配字符设备结构。动态分配的方式如下:struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
静态分配的字符设备结构可使用cdev_init()来初始化,该接口原型为:void cdev_init(struct cdev *, const struct file_operations *);
4.4.2 挂接字符设备到内核
一旦在定义并初始化好字符设备后,可调用cdev_add()添加字符设备到内核,该接口原型为:int cdev_add(struct cdev *p, dev_t dev, unsigned count);
对该接口需要特别注意的是,一旦调用完成,字符设备就被挂接到内核,立马就有可能投入使用,所以必须
确保在该调用结束前,字符设备的一切准备工作已经就绪。
4.4.3 从内核删除字符设备void cdev_del(struct cdev *p);
4.5 字符设备scull的实现