字符设备是Linux系统三大类设备之一(字符设备、块设备、网络设备),作为Linux最简单的一类设备,字符设备常用来传输一些简单的控制命令或者少量的数据。本篇文章分享了如何在Linux内核中创建一个字符设备,并在应用程序中测试该设备的实例。该字符设备通过在内核中创建一段内存空间,并将这段空间作为字符设备读写访问的目标地址,来实现Linux内核字符设备驱动与应用程序的通信。
Linux Ubuntu 18.04, 内核版本:4.18
在内核中创建一个字符设备驱动程序virtual_disk.c,并用模块化的编译方式将其编译成virtual_disk.ko文件并加载到操作系统中。
因为用内核中一段内存空间作为设备虚拟的磁盘访问空间,我们可以假定有一个虚拟的磁盘可以供设备访问。
驱动程序virtual_disk.c源代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#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”写入到内核中,再次读出到用户空间。