《1》在内核中, dev_t 类型(<linux/types.h> 中定义)用来保存设备编号-----包括主设备号和次设备号。在内核2.6.0版本中,dev_t是一个32位的数,其中的12位用来表示主设备号,而其余20位用来表示次设备号。当然我们的代码不应该对设备编号的组织做任何假定。而应该始终使用<linux/kdev_t.h> 中定义的宏。比如要获得dev_t 的主设备号或次设备号,应使用: MAJOR(dev_t dev) ; MINOR(dev_t dev) ; 相反, 如果需要将主设备号和次设备号转换成dev_t 类型,则使用: MKDEV(int major , int minor) ;
《2》在建立一个字符设备之前,我们首先需要获得一个或者多个设备编号。静态分配设备号需要的函数是:
int register_chrdev_region(dev_t first , unsigned int count , char * name);
first : 是要分配的设备号范围的起始值。first的次设备号经常被设置为0, 但对该函数来讲并不是必需的。
count :所请求的连续设备编号的个数。如果count非常大,则所请求的范围可能和下一个主设备号重叠, 但只要我们所请求的编号范围是可用的,则不会带来任何问题。
name : 是和该编号范围关联的设备名称, 它将出现在 /proc/devices 和 sysfs中。
成功分配时返回0, 错误情况返回一个错误码,并且不能请求所请求的区域。
当我们不知道设备号的时候,我们可以通过动态分配来实现,使用函数是:
int alloc_chrdev_region(dev_t *dev , unsigned int firstminor , unsigned int count , char *name) ;
dev : 是仅用于输出的参数, 在成功完成调用后将保存已分配范围的第一个编号。
firstminor 应该是要使用的被请求的第一个次设备号, 它通常是0 。count 和 name 参数与register_chrdev_region 函数是一样的。
《3》无论采用什么样的方法分配设备号,都应该在不再使用它们时释放这些设备编号。使用下面的函数:
void unregister_chrdev_region(dev_t first , unsigned int count ) ;
《4》在书本中例子主要是讲解scull, 下面这个scull_load 脚本是发布scull的一部分, 使用模块形式发行的驱动程序的用户可以在系统的rc.local 文件中调用这个脚本,或是在需要模块时手工调用。
#!/bin/sh
module = “scull”
device=“scull”
mode=“664”
#使用传入到该脚本的所有参数调用insmod , 同时使用路径名来指定模块位置,
#这是因为新的modutils 默认不会在当前目录中查找模块。
/sbin/insmod ./$module.ko $* || exit 1
#删除原有节点
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
#给定适当的组属性及许可,并修改属组。
#并非所有的发行版都具有staff组, 有些有wheel 组。
group =“staff”
grep -q ' ^staff : ' / etc / group || group = “ wheel ”
chgrp $group /dev/${device}[ 0 -3 ]
chmod $mode /dev/ ${device}[ 0 -3]
这个脚本同样可以适用于其他的驱动程序,只要重新定义变量并调整mknod 那几行语句就可以了。
这个脚本必须由超级用户运行,所以新创建的设备文件自然属于root。默认权限位只属于root对其有写访问权,而其他用户只有读访问权,所以我们需要做相应的修改。
《5》大部分基本驱动程序操作涉及到三个重要的内核数据结构,分别是file_operations , file 和 inode 。迄今为止,我们已经为自己保留了一些设备编号,但未将任何驱动程序操作连接到这些编号。file_operations 结构就是用来建立这种连接的。这种结构定义在<linux / fs.h>, 其中包含了一组函数指针。每个打开的文件(在内部由一个file 结构表示) 和一组函数关联(通过包含指向一个file_operations 结构的f_op字段) 。
这三个结构体主要如下:
struct file {
union {
struct list_head fu_list; 文件对象链表指针linux/include/linux/list.h
struct rcu_head fu_rcuhead; RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
} f_u;
struct path f_path; 包含dentry和mnt两个成员,用于确定文件路径
#define f_dentry f_path.dentry f_path的成员之一,当前文件的dentry结构
#define f_vfsmnt f_path.mnt 表示当前文件所在文件系统的挂载根目录
const struct file_operations *f_op; 与该文件相关联的操作函数
atomic_t f_count; 文件的引用计数(有多少进程打开该文件)
unsigned int f_flags; 对应于open时指定的flag
mode_t f_mode; 读写模式:open的mod_t mode参数
off_t f_pos; 该文件在当前进程中的文件偏移量
struct fown_struct f_owner; 该结构的作用是通过信号进行I/O时间通知的数据。
unsigned int f_uid, f_gid; 文件所有者id,所有者组id
struct file_ra_state f_ra; 在linux/include/linux/fs.h中定义,文件预读相关
unsigned long 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;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
};
struct inode {
struct hlist_node i_hash; 哈希表
struct list_head i_list; 索引节点链表
struct list_head i_dentry; 目录项链表
unsigned long i_ino; 节点号
atomic_t i_count; 引用记数
umode_t i_mode; 访问权限控制
unsigned int i_nlink; 硬链接数
uid_t i_uid; 使用者id
gid_t i_gid; 使用者id组
kdev_t i_rdev; 实设备标识符
loff_t i_size; 以字节为单位的文件大小
struct timespec i_atime; 最后访问时间
struct timespec i_mtime; 最后修改(modify)时间
struct timespec i_ctime; 最后改变(change)时间
unsigned int i_blkbits; 以位为单位的块大小
unsigned long i_blksize; 以字节为单位的块大小
unsigned long i_version; 版本号
unsigned long i_blocks; 文件的块数
unsigned short i_bytes; 使用的字节数
spinlock_t i_lock; 自旋锁
struct rw_semaphore i_alloc_sem; 索引节点信号量
struct inode_operations *i_op; 索引节点操作表
struct file_operations *i_fop; 默认的索引节点操作
struct super_block *i_sb; 相关的超级块
struct file_lock *i_flock; 文件锁链表
struct address_space *i_mapping; 相关的地址映射
struct address_space i_data; 设备地址映射
struct dquot *i_dquot[MAXQUOTAS];节点的磁盘限额
struct list_head i_devices; 块设备链表
struct pipe_inode_info *i_pipe; 管道信息
struct block_device *i_bdev; 块设备驱动
unsigned long i_dnotify_mask;目录通知掩码
struct dnotify_struct *i_dnotify; 目录通知
unsigned long i_state; 状态标志
unsigned long dirtied_when;首次修改时间
unsigned int i_flags; 文件系统标志
unsigned char i_sock; 套接字
atomic_t i_writecount; 写者记数
void *i_security; 安全模块
__u32 i_generation; 索引节点版本号
union {
void *generic_ip;文件特殊信息
} u;
};
inode 译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。
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(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, 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);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
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(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
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);
};
《6》下面对于一些操作的函数给予更为详细的介绍, file_operations结构体中的函数:stuct module *owner
第一个file_operations字段并不是一个操作;相反,它是指向“拥有”该结构的模块的指针。内核使用这个字段以避免在模块的操作正在被使用时卸载该模块。几乎在所有的情况下,该成员都会被初始化为THIS_MODULE, 它是定义在<linux/module.h>中的一个宏。
loff_t ( * llseek)(struct file * , loff_t , int );
方法llseek用来修改文件的当前读写位置,并将新位置作为(正的)返回值返回。参数loff_t一个“长偏移量” , 即使在32位平台上也至少占用64位的数据宽度。出错时返回一个负的返回值。如果这个函数指针是NULL,对seek的调用将会以魔种不可预期的方式修改file结构中的位置计数器。
ssize_t (*read) (struct file * ,char __user * , size_t , loff_t * ) ;
用来从设备中读取数据。该函数指针被赋为NULL值时,将导致read系统调用出错并返回 -EINVAL (“invalid argument ” , 非法参数 “ )。
ssize_t (* aio_read)( struct kiocb * , char __user * , size_t ,loff_t );
初始化一个异步的读取操作----即在函数返回之前可能不会完成读取操作。如果该方法为NULL , 所有的操作将通过read(同步)处理。
ssize_t (* write) (struct file * , const char __user * ,size_t ,loff_t *);
向设备发送数据。如果没有这个函数, write 系统调用会向程序返回一个 -EINVAL 。如果返回值非负, 则表示成功写入的字节数。
ssize_t (* aio_write ) (struct kiocb * , const char __user * , size_t ,loff_t *) ;
初始化设备上的异步写入操作。
int (*readdir) (struct file * , void * , filldir_t );
对于设备文件来说 , 这个字段应该为NULL 。它仅是用于读取目录 , 只对文件系统有用 。
unsigned int (* poll ) ( struct file * , struct poll_table_struct * ) ;
poll 方法是poll , epoll 和 select 这三个系统调用的后端实现。 这三个系统调用可用来查询某个或多个文件描述符上的读取或写入是否会被阻塞。 poll 方法应该返回一个位掩码 , 用来指出非阻塞的读取或写入是否可能 , 并且也会向内核提供将调用进程置于休眠状态直到I/O变为可能时的信息。如果驱动程序将poll的方法定义为NULL, 则设备会被认为即可读也可写, 并且不会被阻塞。
int (* ioctl ) (struct inode * , struct file * , unsigned int , unsigned long ) ;
系统调用ioctl提供了一致执行设备特定命令的方法(如格式化软盘的某个磁道,这既不是读操作也不是写操作)。另外, 内核还能识别一部分ioctl 命令, 而不必调用 fops 表中的ioctl 。如果设备不提供ioctl入口点, 则对于任何内核未预先定义的请求, ioctl 系统调用将返回错误(-ENOTTY ,“No such ioctl for device , 该设备无此ioctl 命令”)。
int( * mmap) (struct file * , struct vm_area_struct * ) ;
mmap 用于请求将设备内存隐射到进程地址空间。如果设备没有实现这个方法, 那么mmap 系统调用将返回 -ENODEV。
int (*open) (struct inode * , struct file * ) ;
尽管这个始终是对设备文件执行的第一个操作,然后却并不要求驱动程序一定要声明一个相应的方法。如果这个入口为NULL , 设备的打开操作永远成功, 但系统不会通知驱动程序。
int (*flush )(struct file *) ;
对flush 操作的调用发生在进程关闭设备文件描述符副本的时候, 它应该执行(并等待) 设备上尚未完结的操作。请不要将它同用户使用的fsync操作相混淆。目前, flush 仅仅用于少数几个驱动程序, 比如, SCSI磁带驱动程序用它来确保设备被关闭之前所有数据都被写到磁带中。如果flush被设置为NULL , 内核将简单地忽略用户应用程序的请求。
int (* release) (struct inode * , struct file *)
当file结构被释放时, 将调用这个操作。与open相仿, 也可以将release 设置为NULL。
int (* fsync ) (struct file * , struct dentry * , int) ;
该方法是fsync系统调用的后端实现, 用户调用它来刷新待处理的数据。如果驱动程序没有实现这一方法fsync 系统调用返回-EINVAL。
int (* aio_fsync)(struct kiocb * , int );
这个是fsync方法的异步版本。
int (* fasync )(int , struct file * , int ) ;
这个操作用来通知设备其FASYNC标志发生了变化。异步通知是比较高级的问题, 将在第六章介绍。如果设备不支持异步通知, 该字段可以是NULL 。
int (* lock) (struct file * , int , struct file_lock * );
lock 方法用于实现文件锁定, 锁定是常规文件不可缺少的特性, 但设备驱动几乎从来不会实现这个方法。
ssize_t (* readv) (struct file * , const struct iovec * , unsigned long , loff_t * ) ;
ssize_t (* writev) (struct file * , const struct iovec * , unsigned long , loff_t * );
这些方法用来实现分散 / 聚集型的读写操作 。应用程序有时需要进行涉及多个内存区域的单次读或写操作, 利用上面这些系统调用可完成这类工作, 而不必强加额外的数据拷贝操作。如果这些函数指针被设置成NULL ,就会调用read 和 write 方法(可能是多次)。
ssize_t (*sendfile ) (struct file * , loff_t * , size_t , read_actor_t , void * ) ;
这个方法实现sendfile 系统调用的读取部分。sendfile 系统调用以最小的复制操作将数据从一个文件描述符移动到另一个。例如:Web服务器可以利用这个方法将某个设备的内容发送到网络连接。设备驱动程序通常将sendfile设置为NULL。
ssize_t (* sendpage) (struct file * , struct page * , int , size_t , loff_t *, int );
sendpage 是sendfile 系统调用的另外一半, 它由内核调用以将数据发送到对应的文件, 每次一个数据页。设备驱动程序通常也不需要实现sendpage。
unsigned long (* get_unmapped_area ) (struct file * , unsigned long , unsigned long , unsigned long , unsigned long ) ;
该方法的目的是在进程的地址空间中找到一个合适的位置,以便将底层设备中的内存段映射到该位置。该任务通常由内存管理代码完成, 但该方法的存在可允许驱动程序强制满足特定设备需要的任何对齐要求。大部分驱动程序可设置该方法为NULL。
int (* check_flags )(int )
该方法允许模块检查传递给fcntl(F_SETFL...)调用的标志。
int (* dir_notify )(struct file * , unsigned long ) ;
当应用程序使用fcntl来请求目录改变通知时, 该方法将被调用。该方法仅对文件系统有用,驱动程序不必实现dir_notify。
《7》通常scull设备驱动的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 ,
.release =scull_release ,
} ;
《8》struct file 是一个内核结构,