本文将介绍一个 linux 下字符设备驱动的简单实现,代码基于 Linux Device Driver
的 scull
驱动,介绍该字符设备的注册、打开、写入数据、读出数据等操作,所有示例代码可以在我的 Github 中找到。
字符设备的定义 Linux对于设备是通过面向对象思想实现的,对于一个字符设备,通过数据结构 struct cdev
来描述。cdev
定义于 <linux/cdev.h>
中,其中最关键的是file_operations
结构,它是实现字符设备的操作集。
1 2 3 4 5 6 7 8 struct cdev { struct kobject kobj ; struct module *owner ; const struct file_operations *ops ; struct list_head list ; dev_t dev; unsigned int count; };
我们实现的scull通过数据结构 struct scull_dev
表示,scull_dev
类是继承 cdev
类。因C语言没有定义面向对象的语法,在这里使用嵌套实现。这样就得到我们的设备模型struct scull_dev
,data
表示我们的硬件设备的起始地址,而 size
则表示这个硬件设备最大的存储空间。
1 2 3 4 5 struct scull_dev { int size ; void *data; struct cdev cdev ; };
下面列出了 cdev
的一些操作方法:
1 2 3 4 5 6 7 8 9 10 void cdev_init (struct cdev * cdev, const struct file_operations * fops) ;struct cdev *cdev_alloc (void ) ;int cdev_add (struct cdev * p, dev_t dev, unsigned count) ;void cdev_del ( struct cdev * p) ;
一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
1 2 3 struct cdev my_cdev ;cdev_init(&my_cdev, &fops); my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
1 2 3 struct cdev *my_cdev = cdev_alloc ();my_cdev->ops = &fops; my_cdev->owner = THIS_MODULE;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的需求而定。
字符设备的操作 scull_dev
的属性已经定义好了,接下来要定义其方法,即如何操作该设备。对于字符设备Linux,Linux 使用 file_operations
结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个系统调用。
linux/fs.h 1 2 3 4 5 6 7 8 9 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 *); int (*open ) (struct inode *, struct file *); int (*release ) (struct inode *, struct file *); ... };
对于我们要实现的scull,需要提供open、write、read、lseek等操作。
1 2 3 4 5 6 7 8 static struct file_operations fops = { .owner = THIS_MODULE, .open = scull_open, .write = scull_write, .read = scull_read, .release = scull_release, .llseek = scull_llseek, };
open open函数有两个参数,一个指向 inode
的指针,一个指向 file
的指针:
1 int (*open ) (struct inode *, struct file *);
inode
表示期望打开的设备节点,在其成员中有个一个指向字符设备 cdev
的指针 i_cdev
,我们可以通过该指针得到指向scull设备的指针(借助 container_of
宏实现)
file
用于表示将要打开的设备描述,包含期望打开文件的标志 f_flags
(读写标志)。我们需要使用指向设备的指针,初始化file成员 private_data
,该成员指向的是将要打开的设备。
1 2 3 4 5 6 7 8 9 10 11 12 13 static int scull_open (struct inode *inode, struct file *fp) { struct scull_dev *sdev = container_of (inode ->i_cdev , struct scull_dev , cdev ); if ((fp->f_flags & O_ACCMODE) == O_WRONLY) { clean_up_scull_data(sdev); } fp->private_data = sdev; printk(KERN_ALERT "scull open!\n" ); return 0 ; }
release release函数参数与open相同:
1 int (*release ) (struct inode *, struct file *);
由于我们要实现的 scull
设备一直存在于内存中,无需任何释放动作,因此只需要返回0即可。
1 2 3 4 5 static int scull_release (struct inode *inode, struct file *fp) { printk(KERN_ALERT "scull release!\n" ); return 0 ; }
write wirte
函数用于向设备写入数据,其参数含义如下:
第一个参数表示打开的文件,即 open 中的 file
第二个参数是指向用户空间,期望写入数据的初始地址
第三个指针是期望写入的数据长度
第四个指针是当前写的位置,write结束时,需要更新该指针指向的数据
write函数的返回值为实际写入数据的长度
1 ssize_t (*write ) (struct file *, const char __user *, size_t , loff_t *);
由于驱动程序处在内核空间,内核空间是不能直接读取用户地址空间的数据,用户空间也不能读取内核地址空间的数据,我们需要借用函数 copy_from_user()
,从用户空间将数据拷贝的内核空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static ssize_t scull_write (struct file *fp, const char __user *buffer , size_t st, loff_t * pos) { struct scull_dev *sdev = fp ->private_data ; int result = -ENOMEM; if (!sdev->data) { sdev->data = kmalloc(SCULL_SIZE, GFP_KERNEL); if (!sdev->data) { goto nomem; } } result = (sdev->size - *pos) > st ? st : (sdev->size - *pos); printk(KERN_ALERT "scull_write : result = %d st = %d pos = %d\n" , result, st, *pos); copy_from_user((sdev->data + *pos), buffer , result); *pos += result; nomem: printk(KERN_ALERT "scull_write char: %d!\n" , result); return result; }
read read
函数,用于读取设备中的数据,参数与write相类似,同样需要借助 copy_to_user()
函数,将数据从内核空间拷贝的用户空间。
1 ssize_t (*read ) (struct file *, char __user *, size_t , loff_t *);
下面是 scull
字符设备实现的 read
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static ssize_t scull_read (struct file *fp, char __user *buffer , size_t st, loff_t *pos) { struct scull_dev *sdev = fp ->private_data ; int result; if (!sdev->data) { result = 0 ; goto nodata; } result = (sdev->size - *pos) > st ? st : (sdev->size - *pos); printk(KERN_ALERT "scull_read : result = %d st = %d pos = %d\n" , result, st, *pos); copy_to_user(buffer , (sdev->data + *pos), result); *pos += result; nodata: printk(KERN_ALERT "scull_read char: %d!\n" , result); return result; }
llseek llseek
函数,用于设置当前读写的位置
1 loff_t (*llseek) (struct file *, loff_t , int );
其函数参数含义如下:
字符设备注册销毁 scull_dev设备的类已经构造完成,接下来就需要使用该类构建设备驱动模块。首先我们需要定义设备驱动模块的初始化函数scull_init()。通过宏控module_init告诉内核指定模块的初始化函数。初始化函数需要完成如下操作:
设备号的申请与释放
一个字符设备或块设备都有一个主设备号和一个次设备号:
主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型 。
次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备 。
内核通过设备号 dev_t
来区分不同的设备,下面列出了操作设备号的一些宏:
1 2 3 4 5 6 #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))
linux
提供了一系列的函数来申请和释放主设备号和次设备号:
1 2 3 4 5 6 7 8 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) ;void unregister_chrdev_region (dev_t from, unsigned count) ;
对应代码如下:
1 2 3 4 5 6 result = alloc_chrdev_region(&dev_num, minor, count, scull_name); if (result < 0 ) { printk(KERN_ALERT "register dev error!\n" ); goto ERR_FINAL; }
字符设备的注册 根据我们自定义的 scull_dev
,对其初始化:动态分配一个scull_dev 对象,初始化其成员 data 与 size
1 2 3 4 5 6 7 8 9 10 scull_device = kmalloc(sizeof (struct scull_dev), GFP_KERNEL); if (!scull_device) { printk(KERN_ALERT "no memory!\n" ); result = -ENOMEM; goto ERR_KMALLOC; } memset (scull_device, 0 , sizeof (struct scull_dev));scull_device->size = SCULL_SIZE;
对于cdev成员,需要初始化其成员 ops
,该成员是指向 struct file_operations
,即包含了操作该设备方法的指针,这里采用的是动态初始化 cdev
方法:
1 2 3 4 cdev_init(&scull_device->cdev, &fops); scull_device->cdev.owner = THIS_MODULE; scull_device->cdev.ops = &fops;
接下来需要通过 cdev_add()
函数将 scull_dev
和它对应的设备号添加到设备列表中
1 2 3 4 5 if (cdev_add(&scull_device->cdev, dev_num, count)) { printk(KERN_ALERT "ERROR: cdev_add %d\n" , count); goto ERR_REGISTER_CDEV; }
自动创建设备节点 基于 udev
机制可以实现设备节点自动创建,在驱动用加入对 udev 的支持需要的工作是:
在驱动初始化的代码里调用 class_create
为该设备创建一个 struct class
结构体
为每个设备调用 device_create
创建对应的设备内核中定义的 struct device
结构体,并在 sysfs 中注册
加载模块的时候,用户空间中的 udev
会自动响应 device_create
函数,去 sysfs
下寻找对应的类从而创建设备节点
1 2 3 4 5 6 7 struct class * class_create (struct module * owner, const char * name) ;void class_destroy (struct class * cls) ;struct device * device_create (struct class * class, struct device * parent, dev_t devt, void * drvdata, const char * fmt, ...) ;void device_destroy ( struct class * class, dev_t devt) ;
对应的示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cls = class_create(THIS_MODULE, scull_name); if (IS_ERR(cls)) { result = PTR_ERR(cls); goto ERR_CREATE_CLASS; } for (i = minor; i < (count + minor); i++) { devp = device_create(cls, NULL , MKDEV(major, i), NULL , "%s%d" , scull_name, i); if (IS_ERR(devp)) { result = PTR_ERR(devp); goto ERR_CREATE_DEVICE; } }
字符设备的销毁 scull_exit()
通过宏控 module_exit
告诉内核模块清除函数,清除函数需要将设备从设备列表中移除,释放动态分配的内存以及注销使用的设备号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static void scull_exit (void ) { int i; for (i = minor; i < (minor + count); i++) { device_destroy(cls, MKDEV(major, i)); } class_destroy(cls); cdev_del(&scull_device->cdev); clean_up_scull_data(scull_device); kfree(scull_device); scull_device = NULL ; unregister_chrdev_region(dev_num, count); printk(KERN_INFO "%s exit!\n" , scull_name); }
这里 clean_up_scull_data
释放 scull
字符设备申请的内存资源:
1 2 3 4 5 static inline void clean_up_scull_data (struct scull_dev *sdev) { kfree(sdev->data); sdev->data = NULL ; }
测试验证 使用以下Makefile
文件编译好驱动程序 scull.ko
1 2 3 4 5 6 7 8 ifneq ($(KERNELRELEASE) ,) obj-m := scull.o else KERNELDIR ?= /lib/modules/$(shell uname -r) /build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif
可以看到,在 /dev
目录下创建了 6 个字符设备,他们 major
相同,minor
不同:
1 2 $ ls /dev/scull* /dev/scull0 /dev/scull1 /dev/scull2 /dev/scull3 /dev/scull4 /dev/scull5
编写一个测试的用户程序来读写字符设备:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main (void ) { int fd; int res; char buf[] = "Houmin says hello to scull!" ; int buflen = sizeof (buf); char buf2[100 ]; fd = open ("/dev/scull0" , O_RDWR); if (fd < 0 ) { printf ("Open scull device failed\n" ); } res = write (fd, buf, buflen); printf ("Write to scull device, content: %s\n" , buf); lseek(fd, 0 , SEEK_SET); res = read (fd, buf2, buflen); buf2[buflen] = '\0' ; printf ("Read from scull device: %s\n" , buf2); close (fd); return 0 ; }
编译运行,显示如下:
1 2 3 4 $ gcc userapp.c -o userapp $ ./userapp Write to scull device, content: Houmin says hello to scull! Read from scull device: Houmin says hello to scull!
查看系统日志信息:
1 2 3 4 5 6 7 8 9 [ 1301.936246 ] scull init, major = 242 [ 1306.650233 ] scull open ! [ 1306.650238 ] scull_write : result = 28 st = 28 pos = 0 [ 1306.650239 ] scull_write char : 28 ! [ 1306.650314 ] scull llseek! [ 1306.650316 ] scull_read : result = 28 st = 28 pos = 0 [ 1306.650317 ] scull_read char : 28 ! [ 1306.650332 ] scull release ! [ 1315.617320 ] scull exit !
参考资料