Linux 驱动学习笔记05--字符驱动实例,实现一个共享内存设备的驱动
断断续续学驱动,好不容易有空,做了段字符驱动的例子。主要还是跟书上学习在此记录下来,以后说不定能回过头来温故知新。
首先上驱动源码 gmem.c:
/*************************************************************************
> File Name: gmem.c
> Author: hailin.ma
> Mail: mhl2018@126.com
> Created Time: Fri 18 Dec 2015 05:08:51 PM CST
************************************************************************/
#include <linux/cdev.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#define GMEM_SIZE 0X1000
#define GMEM_CLEAR 0XFF
#define GMEN_MAJOR 250
static int gmem_major = GMEN_MAJOR;
typedef struct gmem_dev{
struct cdev cdev;
unsigned char mem[GMEM_SIZE];
}*GMEM_DEVP;
GMEM_DEVP gmem_devp = NULL;
static ssize_t gmem_read(struct file *filp,char __user *buf,size_t count,loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
GMEM_DEVP dev = filp->private_data;
if(p >= GMEM_SIZE)
return 0;
if(count > GMEM_SIZE - p)
count = GMEM_SIZE - p;
if(copy_to_user(buf,(void*)(dev->mem + p),count))
ret = -EFAULT;
else{
*ppos += count;
ret = count;
printk(KERN_INFO"read %d byte(s) from %d\n",count,p);
}
return ret;
}
static ssize_t gmem_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
GMEM_DEVP dev = filp->private_data;
if(p >= GMEM_SIZE)
return 0;
if(p > GMEM_SIZE - p)
count = GMEM_SIZE - p;
if(copy_from_user(dev->mem + p,buf,count))
ret = -EFAULT;
else{
*ppos += count;
ret = count;
printk(KERN_INFO"written %d byte(s) from %d\n",count,p);
}
}
static loff_t gmem_llseek(struct file *filp,loff_t offset,int orig)
{
loff_t ret;
switch(orig){
case 0:
if(offset < 0){
ret = -EINVAL;
break;
}
if((unsigned int)offset > GMEM_SIZE){
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1:
if((filp->f_pos + offset) > GMEM_SIZE){
ret = -EINVAL;
break;
}
if((filp->f_pos + offset) < 0){
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
}
return ret;
}
static int gmem_ioctl(struct inode *inodep,struct file *filp,unsigned int cmd,unsigned long arg)
{
GMEM_DEVP dev = filp->private_data;
switch(cmd){
case GMEM_CLEAR:
memset(dev->mem,0,GMEM_SIZE);
printk(KERN_INFO"gmem is clear!\n");
break;
default:
return -EINVAL;
}
return 0;
}
int gmem_open(struct inode *inode,struct file *filep)
{
filep->private_data = gmem_devp;
return 0;
}
static const struct file_operations gmem_fops = {
.owner = THIS_MODULE,
.open = gmem_open,
.llseek = gmem_llseek,
.read = gmem_read,
.write = gmem_write,
.ioctl = gmem_ioctl,
};
void gmem_setup_cdev()
{
int err;
dev_t devno = MKDEV(gmem_major,0);
cdev_init(&gmem_devp->cdev,&gmem_fops);
gmem_devp->cdev.owner = THIS_MODULE;
err = cdev_add(&gmem_devp->cdev,devno,1);
if(err){
printk(KERN_NOTICE"Error %d adding gmem",err);
}
}
static int __init gmem_init(void)
{
int result;
printk(KERN_INFO"Init gmem\n");
dev_t devno = MKDEV(gmem_major,0); //通过MKDEV宏生成设备号
if(gmem_major){
result = register_chrdev_region(devno,1,"gmem");
}
else{
result = alloc_chrdev_region(&devno,0,1,"gmem");
gmem_major = MAJOR(devno);
}
if(result < 0){
return result;
}
gmem_devp = kmalloc(sizeof(struct gmem_dev),GFP_KERNEL);
if(!gmem_devp){
result = - ENOMEM;
goto fail_malloc;
}
memset(gmem_devp,0,sizeof(struct gmem_dev));
gmem_setup_cdev();
return 0;
fail_malloc:
unregister_chrdev_region(devno,0);
return result;
}
static void __exit gmem_exit(void)
{
printk(KERN_INFO"exit gmem\n");
}
module_init(gmem_init);
module_exit(gmem_exit);
module_param(gmem_major,int,S_IRUGO);
MODULE_AUTHOR("malth <malth0988@163.com>");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_DESCRIPTION("globle memory module");
MODULE_ALIAS("globle memory");
程序不算长,200行左右,下面进行具体分析。
在 Linux 2.6 内核中,使用 cdev 结构体描述一个字符设备, cdev 结构体的定义:
struct cdev {
struct kobject kobj; /* 内嵌的 kobject 对象 */
struct module *owner; /*所属模块*/
struct file_operations *ops; /*文件操作结构体*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count;
};
cdev 结构体的 dev_t 成员定义了设备号,为 32 位,其中 12 位主设备号, 20 位次设备号。使
用下列宏可以从 dev_t 获得主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
而使用下列宏则可以通过主设备号和次设备号生成 dev_t:
MKDEV(int major, int minor)
cdev 结构体的另一个重要成员 file_operations , 定义了字符设备驱动提供给虚拟文件系统的接口函数。
Linux 2.6 内核提供了一组函数用于操作 cdev 结构体:
void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
cdev_init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接,其源代
码:
void cdev_init(struct cdev *cdev, struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops; /*将传入的文件操作结构体指针赋值给 cdev 的 ops*/
}
cdev_alloc()函数用于动态申请一个 cdev 内存,其源代码:
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
cdev_add()函数和 cdev_del()函数分别向系统添加和删除一个 cdev,完成字符设备的注册和注
销。对 cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对 cdev_del()函数的调用则
通常发生在字符设备驱动模块卸载函数中。
在调用 cdev_add()函数向系统注册字符设备之前,应首先调用 register_chrdev_region()或
alloc_chrdev_region()函数向系统申请设备号,这两个函数的原型为:
int register_chrdev_region(dev_t from, unsigned count, const char *name); //用于已知起始设备的设备号的情况
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name); //用于设备号未知,向系统动态申请未被占用的设备号的情况
函数调用成功之后,会把得到的设备号放入第一个参数 dev 中。 alloc_chrdev_region()与 register_chrdev_region()对比的优点在于它会自动
避开设备号重复的冲突。
相反地,在调用 cdev_del()函数从系统注销字符设备之后, unregister_chrdev_region()应该被调
用以释放原先申请的设备号,这个函数的原型为:
void unregister_chrdev_region(dev_t from, unsigned count);
file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在
应用程序进行 Linux 的 open()、 write()、 read()、 close()等系统调用时最终被调用。 file_operations
结构体目前已经比较庞大,它的定义:
struct file_operations { struct module *owner; /* 拥有该结构的模块的指针,一般为 THIS_MODULES */ 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 *, 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); /* 仅用于读取目录,对于设备文件,该字段为 NULL */ unsigned int(*poll)(struct file *, struct poll_table_struct*); /* 轮询函数,判断目前是否可以进行非阻塞的读取或写入*/ int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); /* 执行设备 I/O 控制命令*/ long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); /* 不使用 BLK 的文件系统,将使用此种函数指针代替 ioctl */ long(*compat_ioctl)(struct file *, unsigned int, unsigned long); /* 在 64 位系统上, 32 位的 ioctl 调用,将使用此函数指针代替*/ 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); /* 异步 fsync */ int(*fasync)(int, struct file *, int); /* 通知设备 FASYNC 标志发生变化*/ int(*lock)(struct file *, int, struct file_lock*); ssize_t(*sendpage)(struct file *, struct page *, int, size_t, loff_t *, int); /* 通常为 NULL */ unsigned long(*get_unmapped_area)(struct file *,unsigned long, nsigned long, unsigned long, unsigned long); /* 在当前进程地址空间找到一个未映射的内存段 */ int(*check_flags)(int); /* 允许模块检查传递给 fcntl(F_SETEL...)调用的标志 */ 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); /* 由 VFS 调用,将管道数据粘接到文件 */ ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /* 由 VFS 调用,将文件数据粘接到管道 */ int (*setlease)(struct file *, long, struct file_lock **); };
下面我们对 file_operations 结构体中的主要成员进行分析。
llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write()函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行 write()系统调用时,将得到-EINVAL 返回值。
readdir()函数仅用于目录,设备节点不需要实现它。
ioctl()提供设备相关控制命令的实现(既不是读操作也不是写操作),当调用成功时,返回给调用程序一个非负值。
mmap()函数将设备内存映射到进程内存中,如果设备驱动未实现此函数,用户进行 mmap()系统调用时将获得-ENODEV 返回值。这个函数对于帧缓冲等设备特别有意义。
当用户空间调用 Linux API 函数 open()打开设备文件时,设备驱动的 open()函数最终被调用。
驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。与 open()函数对应的是 release()函数。
poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行 select()和 poll()系统调用将引起进程的阻塞。
aio_read()和 aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符调用 aio_read()、 aio_write()等系统调用进行读写。
Linux字符设备驱动的组成
1.字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和 cdev 的注册,而在卸载函数中应实现设备号的释放和 cdev 的注销。
/* 设备结构体*/ struct xxx_dev_t { struct cdev cdev; ... } xxx_dev; /* 设备驱动模块加载函数 static int _ _init xxx_init(void) { ... cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化 cdev */ xxx_dev.cdev.owner = THIS_MODULE; /* 获取字符设备号*/ if (xxx_major) { register_chrdev_region(xxx_dev_no, 1, DEV_NAME); } else { alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME); } ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备*/ ... } /*设备驱动模块卸载函数*/ static void _ _exit xxx_exit(void) { unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号*/ cdev_del(&xxx_dev.cdev); /* 注销设备*/ ... }
2.字符设备驱动的 file_operations 结构体中成员函数
file_operations 结构体中成员函数是字符设备驱动与内核的接口,是用户空间对 Linux 进行系
统调用最终的落实者。大多数字符设备驱动会实现 read()、 write()和 ioctl()函数,常见的字符设备
驱动的这 3 个函数的形式如
/* 读设备*/ ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,loff_t*f_pos) { ... copy_to_user(buf, ..., ...); ... } /* 写设备*/ ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count,loff_t *f_pos) { ... copy_from_user(..., buf, ...); ... } /* ioctl 函数 */ int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,unsigned long arg) { ... switch (cmd) { case XXX_CMD1: ... break; case XXX_CMD2: ... break; default: /* 不能支持的命令 */ return - ENOTTY; } return 0; }
设备驱动的读函数中, filp 是文件结构体指针, buf 是用户空间内存的地址,该地址在内核空间不能直接读写, count 是要读的字节数, f_pos 是读的位置相对于文件开头的偏移。
设备驱动的写函数中, filp 是文件结构体指针, buf 是用户空间内存的地址,该地址在内核空间不能直接读写, count 是要写的字节数, f_pos 是写的位置相对于文件开头的偏移。
由于内核空间与用户空间的内存不能直接互访,因此借助了函数 copy_from_user()完成用户空间到内核空间的拷贝,以及 copy_to_user()完成内核空间到用户空间的拷贝。
完成内核空间和用户空间内存拷贝的 copy_from_user()和 copy_to_user()的原型分别为:
unsigned long copy_from_user(void *to, const void _ _user *from, unsigned long count);
unsigned long copy_to_user(void _ _user *to, const void *from, unsigned long count);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为 0。
如果要复制的内存是简单类型,如 char、 int、 long 等,则可以使用简单的 put_user()和 get_user(),如:
int val; // 内核空间整型变量 ... get_user(val, (int *) arg); // 用户→内核, arg 是用户空间的地址 ... put_user(val, (int *) arg); // 内核→用户, arg 是用户空间的地址
读和写函数中的_ _user 是一个宏,表明其后的指针指向用户空间,这个宏定义为:
#ifdef _ _CHECKER_ _ # define _ _user _ _attribute_ _((noderef, address_space(1))) #else # define _ _user #endif
I/O 控制函数的 cmd 参数为事先定义的 I/O 控制命令,而 arg 为对应于该命令的参数。例如对于串行设备,如果 SET_BAUDRATE 是一道设置波特率的命令,那后面的 arg 就应该是
波特率值。
在字符设备驱动中,需要定义一个 file_operations 的实例,并将具体设备驱动的函数赋值给file_operations 的成员,如代码所示。
struct file_operations xxx_fops = { .owner = THIS_MODULE, .read = xxx_read, .write = xxx_write, .ioctl = xxx_ioctl, ... };
xxx_fops 在 cdev_init(&xxx_dev.cdev, &xxx_fops)的语句中被建立与 cdev 的连接。
分析完了基本的函数,下面贴上Makefile文件:
#CFLAGS = -g KVERS = $(shell uname -r) obj-m += gmem.o mem-objs := gmem.o all: make -C /lib/modules/$(KVERS)/build M=$(PWD) modules @rm *.o clean: make -C /lib/modules/$(KVERS)/build M=$(PWD) clean @rm -f *.markers *.order
make之后生成gmem.ko文件,insmod gmem.ko gmem_major=303 加载驱动,303是指定主设备号, mknod /dev/gmem c 303 0 创建字符设备,主设备号303,次设备号0。
echo hello gmem! > /dev/gmem
cat /devgmem
通过上面两条命令可以向gmem写入数据和查看数据,这个方法比较方便,另外就是用open,read来查看了:
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd; char* pathname = "/dev/gmem"; fd = open(pathname,O_RDWR); if(fd == -1) { printf("open error"); } char wrbuf[] = "gmem is good!\n"; write(fd,wrbuf,sizeof(wrbuf)); close(fd); return 0; }
上面代码简单的用文件来打开设备,然后向其中写入数据,程序可以继续拓展 read,ioctl之类的,时间原因,本文就更新到这里,后续可能会补充完善。