1. scull(Simple Character Utility for Loading Localities)
1). scull是一个字符驱动,操作一块内存区域好像它是一个设备.
2). scull不依赖于硬件,scull只是操作一些从内核分配的内存.
2. scull的设计
1). 编写驱动的第一步是定义驱动将要提供给用户程序的功能(机制)
3. 主次设备号
1). 字符设备通过文件系统中的名字来存取,这些名字称为文件系统的特殊文件,或者设备文件,或者文件系统的简单节点.通常位于/dev目录下.
2). 主设备号用来标示设备相连的驱动,如/dev/null和/dev/zero都是由驱动1来管理
crw-rw-rw- 1 root root 1, 3 Apr 11 2002 null
crw------- 1 root root 10, 1 Apr 11 2002 psaux
crw------- 1 root root 4, 1 Oct 28 03:04 tty1
crw-rw-rw- 1 root tty 4, 64 Apr 11 2002 ttys0
crw-rw---- 1 root uucp 4, 65 Apr 11 2002 ttyS1
crw--w---- 1 vcsa tty 7, 1 Apr 11 2002 vcs1
crw--w---- 1 vcsa tty 7,129 Apr 11 2002 vcsa1
crw-rw-rw- 1 root root 1, 5 Apr 11 2002 zero
3). 次设备号用来决定引用哪个设备
4). 依据你的驱动是如何编写的,你可以从内核得到一个你的设备的直接指针,或者可以自己使用次设备号作为本地设备数组的索引
4. 设备编号的内部表示
1). 设备号的类型是dev_t类型(包含在头文件<linux/types.h>),32位,高12位用作主设备号,低20位用作次设备号.
2). 在<linux/kdev_t.h>文件中定义了一套获取主次设备号的宏定义
MAJOR(dev_t dev);-------------------->获取主设备号
MINOR(dev_t dev);-------------------->获取次设备号
MKDEV(int major, int minor);-------------->将主次设备号合并成一个dev_t类型
5. 分配和释放字符设备编号
1). 在知道主设备号的情况下,也就是静态的分配了一个主设备号,来给驱动注册分配一个或多个次设备号的函数
int register_chrdev_region(dev_t first_minor, unsigned int minor_count, char *device_name);--------->在头文件<linux/fs.h>中声明
first_minor---------------------------->要分配的起始设备编号,通常是0
minor_count-------------------------->请求连续设备编号的总数
device_name------------------------->设备名,可以在/proc/devices和sysfs中看到.
分配成功: 返回0,否者返回个小于0的数
2). 动态分配一个主设备号的函数
int alloc_chrdev_region(dev_t *dev, unsigned int first_minor, unsigned int minor_count, char *device_name);
dev------------------->一个出参,里面存放了分配的主设备号
3). 释放字符设备编号的函数
void unregister_chrdev_region(dev_t first, unsigned int minor_count); -------------->常用在模块退出时调用
6. 动态分配主设备号的缺点是:无法提前创建设备节点,(说白了,就是无法提前给设备起个名字,如tty0,tty1),因为主设备号是变化的.
在insmod模块后,一旦设备号分配了,就可以读取 /proc/devices来创建 设备文件.
7. 三个重要的内核数据结构--------------------->定义在<linux/fs.h>文件中
1). 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 (*dir_notify)(struct file *filp, unsigned long arg);
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 **);
};
a. struct module *owner
第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载.几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.
b. loff_t (*llseek) (struct file *, loff_t, int);
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.loff_t 参数是一个"long offset", 并且就算在 32 位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器
c. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
d. ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);
初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL,所有的操作会由 read 代替进行(同步地).
e. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
f. ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);
初始化设备上的一个异步写
g. int (*readdir) (struct file *, void *, filldir_t);
对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.
h. unsigned int (*poll) (struct file *, struct poll_table_struct *);
poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.
i. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的ioctl"), 系统调用返回一个错误.
j. int (*mmap) (struct file *, struct vm_area_struct *);
mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
k. int (*open) (struct inode *, struct file *);
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
l. int (*flush) (struct file *);
flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.
定义一个struct 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,
};
########################################################################################################################
2). file
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 path
f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations*f_op;
atomic_long_t
f_count;
unsigned int f_flags;
fmode_t
f_mode;
loff_t
f_pos;
struct fown_structf_owner;
unsigned int
f_uid, f_gid;
struct file_ra_statef_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_headf_ep_links;
spinlock_t
f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space*f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
a. file 和用户空间的FILE指针没有任何关系,FILE定义在C库中,不出现在内核代码中,struct file是一个内核结构,从不出现在用户程序中.
b. struct file代表了一个打开的文件.不是特定给驱动设备的,系统中每个打开的文件都有一个关联的struct file内核空间,当文件的所有实例都关闭后,内核就释放这个结构体.
c. mode_t f_mode;
文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可,但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.
d. loff_t f_pos;
当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ).驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它;读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.
e. unsigned int f_flags;
这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作; 其他标志很少使用. 特别地, 应当检查读/写许可, 使用f_mode 而不是 f_flags. 所有的标志在头文件 <linux/fcntl.h> 中定义.
f.
########################################################################################################################
3). inode
struct inode {
struct hlist_nodei_hash;
struct list_headi_list;
struct list_headi_sb_list;
struct list_headi_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 timespeci_atime;
struct timespeci_mtime;
struct timespeci_ctime;
unsigned int
i_blkbits;
blkcnt_t
i_blocks;
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_semaphorei_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_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;
};
int i_cindex;
__u32 i_generation;
#ifdef CONFIG_DNOTIFY
unsigned long
i_dnotify_mask; /* Directory notify events */
struct dnotify_struct*i_dnotify; /* for directory notifications */
#endif
#ifdef CONFIG_INOTIFY
struct list_headinotify_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
void *i_private; /* fs or device private pointer */
};
a. inode结构由内核在内部用来表示文件,它和代表打开文件描述符的文件结构struct file是不同的,(也可以这么理解,就没有对文件进行任何的操作,就代表了"文件名"),可以对该"文件名"打开多次,但是所得到的"文件描述符"struct file是不一样的,但是都对应同一个"文件"inode.
b. 对于inode结构,对编写驱动用于的成员有两个:
dev_t i_rdev;---------------------------->对于代表设备文件的节点,它包含了设备编号
struct cdev *i_cdev;---------------------->代表字符设备,
c. 通过inode获取主次设备号的函数宏
unsigned int imajor(struct inode *inode);------------------>获得主设备号
unsigned int iminor(struct inode *inode);------------------>获取次设备号
#################################################################################################################
8. 注册字符设备
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
a. 在内核中使用struct cdev结构来代表字符设备---------------------->包含在<linux/cdev.h>头文件中
b. 在内核调用设备前,应该先将给 字符设备分配空间,和将设备添加到内核
c. 分配和初始化字符设备的两种方法
1). 如果你想在运行时获得一个独立的cdev结构,初始化方法是:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;------------------>指向 对字符设备的操作函数指针结构.
2). 将cdev结构嵌入到一个你自己设备特定的结构
void cdev_init(struct cdev *cdev, struct file_operations *fops);------------>初始化已经分配好了的cdev结构
d. cdev建立好后,将它告诉内核
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);------------>将字符设备添加到内核
num------------------>这个设备响应的第一设备号
count---------------->关联到的设备号的设备的个数(常常为1)
e. 从系统中删除一个字符设备的函数
void cdev_del(struct cdev *dev);
##############################################################################################################################
9. 注册字符设备的旧方法
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
major--------------------->主设备号
name--------------------->驱动的名字(在/proc/devices中可以找到)
a. 调用了该函数,会给给定所谓主设备号注册0-255的次设备号,并且为每个设备建立一个缺省的cdev结构.
b. 主次设备号不能大于255
10. 从系统删除设备的旧方法
int unregister_chrdev(unsigned int major, const char *name);
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr);\
(type *)( (char *)__mptr - offsetof(type,member) );})
##################################################################################################################
字符设备模块驱动的编写总结
1. 在模块的入口函数中主要做三件事情
1). 将设备号注册进内核
动态分配设备号的注册
alloc_chrdev_region(&devno, minor, count, name); ------------>minor一般为0
静态分配设备号的注册
cdevno = MKDEV(major, minor);
register_chrdev_region(cdevno, count, chrdev_name);
2). 为设备开辟空间和初始化
cdev_init(&mycdev, &fops);
3). 将设备添加进内核(将设备和设备号进行关联)
cdev_add(&mycdev, cdevno, count);
2. 在模块的退出函数中主要做两件事(当然也可以做些其他事情)
1). 撤销注册的设备号
unregister_chrdev_region(cdevno, count);
2). 删除添加到内核的设备
cdev_del(&mycdev);
###########################################################################################################################
驱动源代码
# include <linux/module.h>
# include <linux/init.h>
# include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
/**
* The descriptions for the module
**/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bruce.Wang");
MODULE_DESCRIPTION("Test the char device");
MODULE_VERSION("1.0");
MODULE_ALIAS("CHRDEV");
//MODULE_DEVICE_TABLE("for device of char ");
/**
*the major device number
**/
static int major = 66;
static int minor = 88;
static int count = 2;
static dev_t cdevno;
static char chrdev_name[] = "Dediprog";
/**
*char device
**/
static struct cdev mycdev;
int my_open(struct inode *inodep, struct file *filep)
{
/**
*Major and minor device numbers obtained by inode
**/
int major = MAJOR(inodep->i_rdev);
int minor = MINOR(inodep->i_rdev);
printk("%s %d\n", __func__, __LINE__);
printk("major------>%d\nminor-------->%d\n",major, minor);
return 0;
}
size_t my_read(struct file *filep, char __user *buf, size_t count, loff_t *offset)
{
printk("%s, %d\n",__func__, __LINE__);
sprintf(buf,"from kernel space,Happy newer");
printk("to user space--------->%s, count: %d\n", buf, count);
return 10;
}
ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *off)
{
printk("%s %d\n", __func__, __LINE__);
printk("from user space--------->%s, count: %d\n", buf, count);
return 20;
}
loff_t my_llseek(struct file *filp, loff_t off, int cmd)
{
printk("%s %d\n", __func__, __LINE__);
printk("off: %#x, cmd: %d\n", off, cmd);
return 30;
}
int my_release(struct inode *inodp, struct file *filp)
{
printk("%s %d\n", __func__, __LINE__);
return 40;
}
/**
*Operations for the character device
**/
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.llseek = my_llseek,
.release = my_release,
};
/**
*The init function of the module
**/
static int __init chrdev_init(void)
{
int ret;
/**
*Combined major and minor device numbers
**/
cdevno = MKDEV(major, minor);
/**
*static register the number of the char device into the kernel
**/
ret = register_chrdev_region(cdevno, count, chrdev_name);
if (ret < 0){
goto ERROR1;
}
/**
*Allocate space for character devices and init it
**/
cdev_init(&mycdev, &fops);
/**
*Add the char device into the kernel(that is,relate with device number)
**/
ret = cdev_add(&mycdev, cdevno, count);
if (ret < 0){
goto ERROR2;
}
return 0;
ERROR1:
printk("register char device number failed!\n");
return ret;
ERROR2:
printk("relate char device with number failed!");
unregister_chrdev_region(cdevno, count);
return ret;
}
/**
*Function executed when the module exits
**/
static void __exit chrdev_exit(void)
{
/**
*Cancellation the char device number from the kernel
**/
unregister_chrdev_region(cdevno, count);
/**
*Remove the char device from the kernel
**/
cdev_del(&mycdev);
printk("See you later,guys!!\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
#############################################################################################################################
驱动Makefile的编写
obj-m := test.o
all:
@make -C /lib/modules/2.6.38-8-generic/build
M=$(shell pwd) modules
clean:
@rm -rf test.ko modules* *mod* test.o Module*
##########################################################################################################################
在用户层编写应用程序来访问自己的设备
从kernel层读取数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
int ret;
char buf[128];
if(argc != 2){
fprintf(stderr, "Usage: %s dev_file\n", argv[0]);
return -1;
}
printf("(1) call open argv[1]=%s\n",argv[1]);
fd = open(argv[1], O_RDWR | O_NDELAY);
if(fd < 0){
perror("open");
return -2;
}
printf("(2) call read\n");
ret = read(fd, buf, 11);
printf("buf-------%s\nret = %d\n", buf,ret);
}
向kernel写数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
int ret;
char buf[128];
if(argc != 2){
fprintf(stderr, "Usage: %s dev_file\n", argv[0]);
return -1;
}
printf("(1) call open argv[1]=%s\n",argv[1]);
fd = open(argv[1], O_RDWR | O_NDELAY);
if(fd < 0){
perror("open");
return -2;
}
sprintf(buf, "Bruce said Hello");
printf("(3) call write\n");
ret = write(fd, buf, 22);
}
###########################################################################################################
我所测试的均是在x86虚拟机的kernel直接将模块插入到PC主机系统了,
测试步骤:
编译驱动
[root@linux ~]# make
向内核中插入驱动模块
.........................# sudo insmod test.ko
查看模块是否插入,在打印的信息里找test
.........................# lsmod
查看内核打印的信息
.........................# dmesg
清空内核打印的信息
........................# sudo dmesg -c
####################################################################
在用户层执行可执行文件
创建一个名为Dediprog的字符设备名,仅仅是名字,方便用户层使用
[root@linux ~]# mknod Dediprog c 66 88
........................# sudo ./app Dediprog
从内核中移除模块的命令:
......................# sudo rmmod test -------------->注意不用加后缀ko