字符设备是Linux系统三大类设备之一(字符设备、块设备、网络设备),作为Linux最简单的一类设备,字符设备常用来传输一些简单的控制命令或者少量的数据。本篇文章分享了如何在Linux内核中创建一个字符设备,并在应用程序中测试该设备的实例。该字符设备通过在内核中创建一段内存空间,并将这段空间作为字符设备读写访问的目标地址,来实现Linux内核字符设备驱动与应用程序的通信。
Linux Ubuntu 18.04, 内核版本:4.18
在内核中创建一个字符设备驱动程序virtual_disk.c,并用模块化的编译方式将其编译成virtual_disk.ko文件并加载到操作系统中。
因为用内核中一段内存空间作为设备虚拟的磁盘访问空间,我们可以假定有一个虚拟的磁盘可以供设备访问。
驱动程序virtual_disk.c源代码如下:
#include <linux/module.h> #include <linux/types.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/mm.h> #include <linux/sched.h> #include <linux/init.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include <linux/slab.h> #include <asm/io.h> #define VIRTUALDISK_SIZE 0x2000 #define MEM_CLEAR 0x1 #define PORT1_SET 0x2 #define PORT2_SET 0x3 #define VIRTUALDISK_MAJOR 200 static int VirtualDisk_major = VIRTUALDISK_MAJOR; static void *VirtualDisk_devp = NULL; struct VirtualDisk { struct cdev cdev; unsigned char mem[VIRTUALDISK_SIZE]; int port1; long port2; long count; }; static void VirtualDisk_setup_cdev(struct VirtualDisk *dev, int minor); static int VirtualDisk_open(struct inode *inode, struct file *filp); static int VirtualDisk_release(struct inode *inode, struct file *filp); static ssize_t VirtualDisk_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos); static ssize_t VirtualDisk_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos); static loff_t VirtualDisk_llseek(struct file *filp, loff_t offset, int orig); static const struct file_operations VirtualDisk_fops = { .owner = THIS_MODULE, .llseek = VirtualDisk_llseek, .read = VirtualDisk_read, .write = VirtualDisk_write, .open = VirtualDisk_open, .release = VirtualDisk_release, }; static void VirtualDisk_setup_cdev(struct VirtualDisk *dev, int minor) { int err; dev_t devno = MKDEV(VirtualDisk_major, 0); cdev_init(&dev->cdev, &VirtualDisk_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &VirtualDisk_fops; err = cdev_add(&dev->cdev, devno, 1); if (err) { printk(KERN_NOTICE "Error in cdev_add() \n"); } } static int VirtualDisk_open(struct inode *inode, struct file *filp) { struct VirtualDisk *devp = NULL; filp->private_data = VirtualDisk_devp; devp = filp->private_data; devp->count++; return 0; } static int VirtualDisk_release(struct inode *inode, struct file *filp) { struct VirtualDisk *devp = filp->private_data; devp->count--; return 0; } static ssize_t VirtualDisk_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) { unsigned long p = *ppos; unsigned int count = size; int ret = 0; struct VirtualDisk *devp = filp->private_data; if (p >= VIRTUALDISK_SIZE) { return count ? -ENXIO : 0; } if (count > VIRTUALDISK_SIZE - p) { count = VIRTUALDISK_SIZE - p; } if (copy_to_user(buf, (void *)(devp->mem + p), count)) { ret = -EFAULT; } else { *ppos += count; ret = count; printk(KERN_INFO "read %d bytes from %lx\n", count, p); } return ret; } static ssize_t VirtualDisk_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos) { unsigned long p = *ppos; unsigned int count = size; int ret = 0; struct VirtualDisk *devp = filp->private_data; if (p >= VIRTUALDISK_SIZE) { return count ? -ENXIO : 0; } if (count > VIRTUALDISK_SIZE - p) { count = VIRTUALDISK_SIZE - p; } if (copy_from_user((void *)(devp->mem + p), buf, count)) { ret = -EFAULT; } else { *ppos += count; ret = count; printk(KERN_INFO "written %d bytes to %lx\n", count, p); } return ret; } static loff_t VirtualDisk_llseek(struct file *filp, loff_t offset, int orig) { loff_t ret = 0; switch (orig) { case SEEK_SET: if (offset < 0) { ret = -EINVAL; break; } if ((unsigned int)offset > VIRTUALDISK_SIZE) { ret = -EINVAL; break; } filp->f_pos = offset; ret = filp->f_pos; break; case SEEK_CUR: if ((filp->f_pos + offset) > VIRTUALDISK_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; break; } return ret; } /* Device driver module init function */ static int __init VirtualDisk_init(void) { int ret; dev_t devno = MKDEV(VirtualDisk_major, 0); if (VirtualDisk_major) { ret = register_chrdev_region(devno, 1, "VirtualDisk"); } else { ret = alloc_chrdev_region(&devno, 0, 1, "VirtualDisk"); VirtualDisk_major = MAJOR(devno); } if (ret < 0) { return ret; } VirtualDisk_devp = kmalloc(sizeof(struct VirtualDisk), GFP_KERNEL); if (!VirtualDisk_devp) { ret = -ENOMEM; goto fail_kmalloc; } memset(VirtualDisk_devp, 0, sizeof(struct VirtualDisk)); VirtualDisk_setup_cdev(VirtualDisk_devp, 0); return 0; fail_kmalloc: unregister_chrdev_region(devno, 1); return ret; } /* Driver module exit function */ static void __exit VirtualDisk_exit(void) { struct VirtualDisk *devp = NULL; devp = VirtualDisk_devp; cdev_del(&devp->cdev); kfree(VirtualDisk_devp); unregister_chrdev_region(MKDEV(VirtualDisk_major, 0), 1); } module_init(VirtualDisk_init); module_exit(VirtualDisk_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("VirtualDisk character device driver"); MODULE_AUTHOR("Jack"); MODULE_VERSION("V1.0");上面的驱动代码实现了在内核中创建一个名称为“VirtualDisk”的字符设备驱动,该字符设备的主设备号为200,次设备号为0。设备可访问的内存空间大小为8K。
有了驱动程序,还需要创建一个Makefile文件才能进行编译。Makefile文件内容如下:
ifneq ($(KERNELRELEASE),) obj-m := virtual_disk.o else CC := gcc KERNEL_DIR = /usr/src/linux-headers-$(uname -r) PWD := $(shell pwd) modules: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules modules_install: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install clean: rm -f *.o rm -f *.o.* rm -f *.symvers rm -f *.order rm -f *.ko rm -f *.ko.* rm -f *.mod.c rm -f *.mod.* rm -f .counter.* rm -rf .tmp_versions创建好Makefile后,给它增加执行权限"chmod a+x Makefile"。然后执行“make”进行编译,编译后会生成virtual_disk.ko的模块文件。
在命令行执行“insmod virtual_disk.ko”加载模块文件,并执行"lsmod”查看模块文件。
[root@stage4 kernel]# insmod virtual_disk.ko insmod virtual_disk.ko [ 100.418030] virtual_disk: loading out-of-tree module taints kernel. [ 100.419660] virtual_disk: module verification failed: signature and/or required key missing - tainting kernel [root@stage4 app]# lsmod lsmod Module Size Used by virtual_disk 11884 0 [root@stage4 app]#执行命令“cat /proc/devices”可以查看到设备名称和主设备号。
[root@stage4 app]# 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 7 vcs 10 misc 13 input 21 sg 128 ptm 136 pts 162 raw 200 VirtualDisk 229 hvc 245 hidraw该设备需要通过手动添加设备文件,使用mknod命令添加设备文件,如下。
[root@stage4 kernel]# mknod /dev/virtual_disk c 200 0 mknod /dev/virtual_disk c 200 0再查看/dev/目录,就可以看到生成了名字为“virtual_disk”的字符设备文件。
[root@stage4 app]# ls -l /dev ls -l /dev total 0 crw-r--r-- 1 root root 10, 235 Jan 11 12:15 autofs drwxr-xr-x 2 root root 60 Jan 11 12:15 block drwxr-xr-x 2 root root 2940 Jan 11 12:15 char ... crw-r--r-- 1 root root 200, 0 Jan 11 12:16 virtual_disk crw-rw-rw- 1 root root 1, 5 Jan 11 12:15 zero编写测试程序virtual_disk_test.c,代码如下。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <string.h> #define MAX_LEN (100) int main() { int fd; int str_len = 0; char write_buf[255] = { 0 }; char read_buf[255] = { 0 }; printf("please input a string:\n"); scanf("%s", write_buf); str_len = strlen(write_buf); if (str_len > MAX_LEN) { printf("Error, the input string length is beyond the maximum str length.\n"); return -1; } fd = open("/dev/virtual_disk", O_RDWR); if(fd < 0) { printf("virtual_disk device open fail\n"); return -1; } lseek(fd, 0, SEEK_SET); printf("Write %d bytes data to /dev/virtual_disk \n", str_len); printf("%s\n", write_buf); write(fd, write_buf, str_len); lseek(fd, 0, SEEK_SET); printf("Read %d bytes data from /dev/virtual_disk \n", str_len); read(fd, read_buf, str_len); printf("%s\n", read_buf); close(fd); return 0; }将virtual_disk_test.c源程序编译生成可执行程序virtual_disk.elf:
gcc -o virtual_disk.elf virtual_disk_test.c执行virtual_disk.elf程序:
[root@stage4 app]# ./virtual_disk.elf ./virtual_disk.elf please input a string: HelloWorld HelloWorld Write 10 bytes data to /dev/virtual_disk HelloWorld [ 2002.463060] written 10 bytes to 0 Read 10 bytes data from /dev/virtual_disk [ 2002.464765] read 10 bytes from 0 HelloWorld [root@stage4 app]#可以看到通过创建的字符设备,成功地将字符串“HelloWorld”写入到内核中,再次读出到用户空间。