字符设备驱动适合于一些简单的硬件,相对块设备驱动来说易于理解,不过了解内核和字符设备驱动之间的接口对于编写字符设备驱动尤为重要。
主次编号
通过文件系统的字符设备名字来访问字符设备,它们是位于/dev下的设备结点。如果在/dev下执行ls -l,可以看到如下输出:
crw-rw—- 1 root dialout 4, 64 2016-04-10 22:38 ttyS0
‘c’代表字符设备结点;
接下来是读写执行权限,依次是主权限,组权限,其他用户权限;
文件硬链接数或者目录子目录数(如果是目录,该值至少是2,因为空目录包含当前目录.和上级目录..);
所有者;
所属用户组;
主编号;
次编号;
最后修改时间;
名字;
主编号标识设备相连的驱动,次编号标识特定的设备。
在2.6.0内核中,定义在<linux/types.h>中的dev_t是一个32位的数,包含了主次编号,可以调用定义在<linux/kdev_t.h>中的如下宏定义:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
如果要从主次编号得到dev_t的值,使用MKDEV。
如果知道设备的主编号,调用如下函数来获取一个或者多个设备编号:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
from包含主次编号,一般次编号从0开始,count是连续次编号的个数,如果太大,有可能溢出到下一个设备驱动的次编号;name是连接到这个设备编号范围的名字,将出现在/proc/devices和sysfs中。
调用成功返回0;否则返回负的错误码,不能访问请求的区域。
如果不知道设备的主编号,可以调用如下函数,内核会动态分配主编号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
dev是返回的第一个设备的主次编号;baseminor是请求要用的第一个次编号,通常是0;count是连续次编号的个数,如果太大,有可能溢出到下一个设备驱动的次编号;name是连接到这个设备编号范围的名字,将出现在/proc/devices和sysfs中。
在模块的cleanup函数中,要调用如下函数来释放分配的设备编号:
void unregister_chrdev_region(dev_t from, unsigned count);
内核源码树的Documentation/devices.txt给出了静态分配的主设备号;当我们在编写新的设备驱动时,应当使用alloc_chrdev_region来动态获取主设备编号,这样的话,设备结点需要动态创建,通过cat /proc/devices能得到主设备编号,如下:
cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
99 ppdev
108 ppp
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
251 hidraw
252 usbmon
253 bsg
254 rtc
Block devices:
1 ramdisk
2 fd
259 blkext
7 loop
8 sd
9 md
11 sr
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
252 device-mapper
253 pktcdvd
254 mdp
接下来就可以写一个脚本来insmod驱动,创建结点了
#!/bin/sh
module="xxxxx"
device="xxxxx"
mode="664"
# invoke insmod with all arguments we got
# and use a pathname, as newer modutils don't look in . by default
/sbin/insmod ./$module.ko $* || exit 1
# remove stale nodes
rm -f /dev/${device}[0-3]
major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices)
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
# give appropriate group/permissions, and change the group.
# Not all distributions have staff, some have "wheel" instead.
group="staff"
grep -q '^staff:' /etc/group || group="wheel"
chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]
这里最后2行改变了设备结点的组和权限,这个脚本必须由超级用户(root)执行,生成的结点也由超级用户拥有。可以根据自己的要求,改变组和权限。
同时还可以编写一个脚本来rmmod驱动,销毁结点:
#!/bin/sh
module="xxxx"
device="xxxx"
# invoke rmmod with all arguments we got
/sbin/rmmod $module $* || exit 1
# Remove stale nodes
rm -f /dev/${device} /dev/${device}[0-3]
也可以通过把主编号设置为模块参数,默认值为0,这样就可以在编译时指定模块主编号,insmod时指定主编号,或者由内核动态分配(当默认值0没有被改变时)。
int xxxx_major = XXXX_MAJOR;//编译时指定
int xxxx_minor = 0;
int xxxx_nr_devs = 4;
module_param(xxxx_major, int, S_IRUGO);//insmod时指定
module_param(xxxx_minor, int, S_IRUGO);
if (xxxx_major) {
dev = MKDEV(xxxx_major, xxxx_minor);
result = register_chrdev_region(dev, xxxx_nr_devs, "xxxx");
} else {
result = alloc_chrdev_region(&dev, xxxx_minor, xxxx_nr_devs,
"xxxx");//内核动态分配
xxxx_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "xxxx: can't get major %d\n", xxxx_major);
return result;
}
数据结构
大部分的基础性的驱动操作会涉及3个重要的内核数据结构:
file_operations
file
inode
定义在<linux/fs.h>中的file_operations联系了设备驱动编号和设备驱动,它是一个函数指针的集合;每个打开的文件通过包含file_operations成员,与这个函数指针集合相关联;这些函数大部分是用于实现系统调用的;每个函数指针必须指向驱动的函数,或者为NULL,当为NULL时,每个函数的内核确切行为都是不同的。
标记为__user的函数参数指针是用户空间指针,是不能直接被内核使用的,虽然编译时,编译器是忽略这个标记的,但是某些代码检查工具能发现对用户空间指针的错误使用。
file_operations结构如下:
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 *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 *, struct dentry *, 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 **);
};
struct module *owner
一个指向拥有这个结构的模块的指针,该结构可以阻止当模块函数正在被使用时模块被卸载;一般初始化为THIS_MODULE。
loff_t (*llseek) (struct file *, loff_t, int)
用于改变文件当前读写位置,成功执行,新位置作为返回值,否则返回一个负的错误值;如果该函数指针为NULL,seek系统调用会以潜在的未知的方式修改file中的位置计数器。
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)
初始化一个异步读,函数返回可能读操作没完成,如果为NULL,由read代替。
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t)
初始化一个异步写,函数返回可能读操作没完成,如果为NULL,由write代替。
int (*readdir) (struct file *, void *, filldir_t)
提供文件系统的读取目录功能,对于设备文件,应该为NULL。
unsigned int (*poll) (struct file *, struct poll_table_struct *)
poll,epoll,select用于查询一个或多个文件描述符的读或写是否阻塞。poll返回一个位掩码指示是否非阻塞的读或者写是可能的,并且提供信息给内核用来使调用进程睡眠直到I/O变为可能。如果poll为NULL,设备假定为不阻塞的可读可写。
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long)
ioctl提供了发出设备特定命令的方法,如果设备不提供ioctl方法,对于任何事先未定义的请求,系统调用返回一个错误。
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)
不需要BKL(大内核锁),如果设备驱动不提供unlocked_ioctl,则调用ioctl。
long (*compat_ioctl) (struct file *, unsigned int, unsigned long)
需要使能内核配置选项CONFIG_COMPAT=y,不需要BKL(大内核锁)。
int (*mmap) (struct file *, struct vm_area_struct *)
用于将设备内存映射到进程地址空间,如果为NULL,系统调用返回-ENODEV。
int (*open) (struct inode *, struct file *)
对设备文件进行的第一个操作,如果为NULL,设备打开一直成功,驱动不会得到通知。
int (*flush) (struct file *, fl_owner_t id)
当进程关闭设备文件时,调用它将执行并且等待设备的任何未完成操作。
int (*release) (struct inode *, struct file *)
当文件结构被释放时,调用它。
int (*fsync) (struct file *, struct dentry *, int datasync)
当用户调用系统调用fsync时,用于刷新任何挂着的数据。
int (*aio_fsync) (struct kiocb *, int datasync)
fsync的异步版本。
int (*fasync) (int, struct file *, int)
用于通知设备FASYNC标记的改变。如果设备不支持异步通知,设置为NULL。
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)
用于模块检查传递给fcntl(.., F_SETFL, ..)调用的标志。
int (*flock) (struct file *, int, struct file_lock *)
由系统调用flock(2)调用。
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int)
用于虚拟文件系统(VFS)从管道拼接数据到文件。由系统调用splice(2)调用。
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int)
用于虚拟文件系统(VFS)从文件拼接数句到管道。由系统调用splice(2)调用。
int (*setlease)(struct file *, long, struct file_lock **)
设置一个打开文件的租约。需要持有BKL(大内核锁)。
一般驱动只要实现几个重要的方法(llseek/read/write/open/ioctl/release),或者根据自己的需要实现其中的方法。
文件结构
定义在<linux/fs.h>中的struct file用于内核来代表一个打开的文件,每一个打开的文件都对应内核一个struct file结构,内核在open时创建这个结构,然后把它传递给任何需要操作该文件的函数,在文件的所有实例都关闭后,内核释放这个数据结构。
一般使用struct file或file代表数据结构,而使用filp来表示指向该数据结构的指针。
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
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 */
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;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
f_u
一个双向列表和RCU(read-copy update)链表。
f_dentry
关联到文件的目录入口,可以通过filp->f_dentry->d_inode访问目录的inode结构。
f_vfsmnt
虚拟文件系统挂载点结构。
f_op
指向file_operations结构的指针。这里提供了和文件相关的操作,可以在open里依据次编号的不同,赋予不同的文件相关的操作。
f_lock
文件自旋锁。
f_count
文件计数器。
f_flags
文件标志,例如O_RDONLY, O_SYNC, O_NONBLOCK,通过检查是否设置了O_NONBLOCK判断是否请求了非阻塞操作。
f_mode
文件读写模式,只读(FMODE_READ),可读可写(FMODE_READ | FMODE_WRITE)。内核在调用驱动的方法前,会检查操作是否具有读写权限,如果没有相应的权限,操作被拒绝。
f_pos
文件当前读写位置。驱动可以获取它,但是一般不应改变它,作为读写操作的最后的指针参数,读写操作可以修改它,llseek也可以修改它。
f_owner
文件所有者。
f_cred
任务的安全上下文相关。
f_ra
记录单个文件的预读状态。
f_version
文件版本。
f_security
如果配置了,安全相关的。
private_data
当需要在系统调用间保留状态信息时,分配一段内存,保存数据,将地址赋给该成员;但是必须在内核销毁文件结构之前,在release方法中释放分配的内存。
f_ep_links
如果配置了,为EPOLL记录该文件的所有钩子函数。
f_tfile_llink
如果配置了,为EPOLL记录该文件的所有相关文件。
f_mapping
地址空间映射相关。
f_mnt_write_state
如果配置了,挂载点写状态。
inode结构
struct inode {
struct hlist_node i_hash;
struct list_head i_list; /* backing dev IO list */
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
u64 i_version;
loff_t i_size;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
struct timespec i_atime;
struct timespec i_mtime;
struct timespec i_ctime;
blkcnt_t i_blocks;
unsigned int i_blkbits;
unsigned short i_bytes;
umode_t i_mode;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
struct mutex i_mutex;
struct rw_semaphore i_alloc_sem;
const struct inode_operations *i_op;
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct super_block *i_sb;
struct file_lock *i_flock;
struct address_space *i_mapping;
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_mark_entries; /* fsnotify mark entries */
#endif
#ifdef CONFIG_INOTIFY
struct list_head inotify_watches; /* watches on this inode */
struct mutex inotify_mutex; /* protects the watches list */
#endif
unsigned long i_state;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned int i_flags;
atomic_t i_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结构代表打开文件描述符;单个文件的多个打开文件描述符(file)指向同一个inode结构。
inode包含了大量的关于文件的信息:
i_hash
哈希列表。
i_list
用于支持设备IO的列表。
i_sb_list
兄弟列表。
i_dentry
目录列表。
i_ino
inode数目。
i_count
计数器。
i_nlink
链接计数。
i_uid
用户id。
i_gid
组id。
i_rdev
包含了设备文件结点的主次编号,一般使用iminor和imajor通过inode来获取,不直接使用该成员来获取。
i_version
版本信息。
i_size
大小。
i_size_seqcount
大小的顺序计数。
i_atime
文件访问时间。
i_mtime
文件更新时间。
i_ctime
文件创建时间。
i_blocks
文件块数目。一个block为512字节。
i_blkbits
文件块位移数目,一般是4K。
i_bytes
最后一块的字节数。文件大小为i_blocks * 512 + i_bytes。
i_mode
文件模式,块设备文件,字符设备文件,链接文件。
i_lock
文件自旋锁。
i_mutex
互斥量。
i_alloc_sem
读的时候获取i_mutex,写的时候还要获取i_alloc_sem。
i_op
inode操作函数组。包括mknod等。
i_fop
文件操作函数。
i_sb
超级块结构。
i_flock
文件锁。
i_mapping
地址空间映射。
i_data
inode数据地址空间。
i_dquot
引用结构地址数组。
i_devices
设备列表。
i_pipe/i_bdev/i_cdev
管线设备,块设备,字符设备联合体。
i_generation
文件系统的版本信息,一般给NFS使用。
i_fsnotify_mask
如果配置了,inode关心的所有事件。
i_fsnotify_mark_entries
如果配置了,inode关心的所有事件列表。
inotify_watches
如果配置了,观察者列表。
inotify_mutex
如果配置了,保护观察者列表的互斥量。
i_state
inode状态。
dirtied_when
第一次写脏的时间,jiffies。
i_flags
inode标记,例如S_SYNC,S_NOATIME等等。
i_writecount
inode写计数。
i_security
安全相关。
i_acl
posix相关的访问控制列表。
i_default_acl
posix相关的默认访问控制列表。
i_private
文件系统或者设备私有数据。
字符设备注册
通过包含头文件<linux/cdev.h>,可以使用cdev的定义和关联函数,内核在内部使用cdev来代表字符设备。
使用cdev_alloc可以获取一个cdev结构,使用cdev_init可以初始化一个cdev结构,使用cdev_add添加一个cdev结构到内核。
cdev_add有可能失败,如果失败,设备未添加到系统,如果成功,内核就可以调用它的操作,在调用cdev_add之前,要确保驱动完全准备好处理设备上的操作。
使用cdev_del去除cdev结构。
一些老的驱动可能使用register_chrdev注册字符设备,使用unregister_chrdev去除字符设备。
open和release
驱动在open方法中做任何初始化工作,包括:
*检查设备特定的错误;
*如果第一次打开,初始化设备;
*如果需要,更新f_op指针;
*分配并填充filp->private_data。
通常cdev结构会被嵌入到特定的设备结构中,而在open方法中,我们得到的是cdev(通过inode->i_cdev),如果需要得到包含cdev结构的特定结构的指针,使用
container_of(ptr, type, member)即可。
通过查看存储在inode结构中的次编号,可以识别打开的设备,使用iminor可以从inode中获取次编号。
release应当做如下工作:
*释放open分配给filp->private_data的内存;
*在最后关闭设备。
dup和fork系统调用不会调用open来创建打开文件的拷贝,他们只递增文件结构中的计数,不是每一个close系统调用都会调用release方法,它只在文件结构计数为0时调用release方法,这样就保证了一次open只看到一次release。每次调用close都会调用flush,但是很少驱动实现flush,所以除了调用release,close没什么可做。
内核在进程exit时,会调用close关闭所有文件。
使用kmalloc和kfree来分配和释放内核内存,可以传递NULL给kfree,除此之外,所有传递给kfree的指针应当都是指向由kmalloc分配的内存的指针。
读和写
读写方法的输入输出内存是用户空间指针,不能被内核直接解引用。出于如下理由:
*用户空间指针当运行于内核模式可能根本就是无效的,可能没有那个地址的映射,可能指向一些其他的随机数据;
*用户空间内存是分页的,系统调用时,这个内存可能不在RAM中,试图直接引用用户空间内存可能产生一个页面错,结果可能是一个oops,导致进行系统调用的进程死亡;
*用户空间的指针可能是错误的或者恶意的,如果驱动盲目的解引用用户空间提供的指针,那么它提供了用户空间程序存取和覆盖系统任何内存的入口。
内核提供了如下方法来确保用户空间和内核空间数据传输的安全和正确:
copy_to_user
copy_from_user
任何存取用户空间的函数,必须是可重入的,必须能够和其他驱动函数并行执行,必须处于一个能够合法睡眠的位置。
这2个函数除了从用户空间拷贝数据到内核空间以及从内核空间拷贝数据到用户空间,他们还检查用户空间指针是否有效,如果无效,则不拷贝;如果拷贝过程中遇到无效地址,只拷贝部分数据,并返回还要拷贝的数据量。
__copy_to_user和__copy_from_user是不检查用户空间指针的版本。
read方法调用copy_to_user从设备拷贝数据到用户空间,write方法则调用copy_from_user从用户空间拷贝数据到设备。同时,他们应当更新*offp中的文件位置。内核接着在适当的时候更新文件位置到文件结构。pread和pwrite从一个给定的文件偏移开始操作,并不改变其他系统调用看到的文件位置。read和write方法在发生错误时,返回一个负值,当返回大于或者等于0时,代表成功传送的数据量。
read方法的返回值由应用程序解释:
*如果等于传递给read的count参数,系统调用完成;
*如果是正数,但是小于count,那么一般应用程序重复调用read,直至完成;
*如果是0,已达文件末尾;
*如果是一个负值,那么表示出错了,根据<linux/errno.h>可以知道发生了什么错误,典型的错误如:-EINTR(中断)和-EFAULT(坏地址);
*如果没有数据,但是可能后续到达,这时,系统调用被阻塞。
write方法的返回值类似于read。
readv和writev
这2个方法是read和write的矢量版本,一般都不实现,由内核使用read和write模拟,readv循环读取指定的数量到缓存,而writev收集每个缓存的内容,一次写出。
调用的参数跟read和write类似,count指定了iovec的代表的缓存块的个数。
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
这个结构体指定了每一个缓存的起始地址和长度。
使用free可以查看内存使用状态,使用strace可以监视程序发出的系统调用和返回值。