简单的字符设备驱动
实验:写一个简单的字符设备驱动
本实验我们编写一个简单的字符设备驱动,实现基本的 open()、read() 和 write() 方法。接着编写用户空间的测试程序,测试 read() 方法。
代码清单 1 是实验字符设备驱动的完整代码。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #define DEMO_NAME "my_demo_dev"
- static dev_t dev;
- static struct cdev* demo_cdev;
- static signed count = 1;
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- int major = MAJOR(inode->i_rdev);
- int minor = MINOR(inode->i_rdev);
- printk("%s: major=%d, minor=%d\n", __func__, major, minor);
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t lbuf, loff_t* ppos)
- {
- printk("%s: enter\n", __func__);
- return 0;
- }
- static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* f_pos)
- {
- printk("%s: enter\n", __func__);
- return 0;
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write
- };
- static int __init simple_char_init(void)
- {
- int ret;
- ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME);
- if (ret)
- {
- printk("failed to allocate char device region");
- return ret;
- }
- demo_cdev = cdev_alloc();
- if (!demo_cdev)
- {
- printk("cdev_alloc failed\n");
- goto unregister_chrdev;
- }
- cdev_init(demo_cdev, &demodrv_fops);
- ret = cdev_add(demo_cdev, dev, count);
- if (ret)
- {
- printk("cdev_add failed\n");
- goto cdev_fail;
- }
- printk("succeeded register char device: %s\n", DEMO_NAME);
- printk("major number = %d, minor number = %d\n", MAJOR(dev), MINOR(dev));
- return 0;
- cdev_fail:
- cdev_del(demo_cdev);
- unregister_chrdev:
- unregister_chrdev_region(dev, count);
- return ret;
- }
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
- if (demo_cdev)
- cdev_del(demo_cdev);
- unregister_chrdev_region(dev, count);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
我们首先看到字符设备驱动的初始化函数 simple_char_init。其中最先调用 alloc_chrdev_region() 函数为字符设备驱动动态分配一个未被使用的设备号。其函数原型如下:
- int alloc_chrdev_region(dev_t *dev,
- unsigned int firstminor,
- unsigned int count,
- char *name);
其中,dev 会返回最终分配的设备号。firstminor 是请求的第一个次设备号的基数,count 是需要分配的此设备好的数量。name 是与设备关联的名字,这个名字将出现在 /proc/devices 中。
设备号分配成功时会返回 0,否则返回负值。最终分配的设备号需要使用 unregister_chrdev_region() 函数释放。
举个例子:如果 firstminor 为 0, count 为 10,那么就会得到 10 个设备号,它们的次设备号分别为 0、1、2、……、10。
接着我们使用 cdev_alloc() 函数分配并初始化一个 cdev 结构。目前没有过多了解 cdev 结构,可以把它看作是字符设备的抽象。最终我们需要使用 cdev_del() 函数来释放分配的结构资源。
再接着调用 cdev_init() 函数来初始化字符设备结构,主要是初始化它的 file_operations 成员,其中定义了设备如何读、写等,后续我们会实现这些操作函数。
最后我们通过 cdev_add() 函数将以上分配的设备结构添加到系统中。
可能会有多个设备由同一个驱动程序管理。
接着我们看实现的操作函数,实现比较简单:open() 函数打印设备的主、次号;read() 和 write() 函数只是打印一下日志。
字符设备驱动的退出函数内容,在上面已经介绍到了。要通过 cdev_del() 函数释放分配的字符设备结构空间;还要调用 unregister_chrdev_region() 函数释放字符设备号资源。
编写如下 Makefile:
- BASEINCLUDE ?= /lib/modules/`uname -r`/build
- mydemo-objs := simple_char.o
- obj-m := mydemo.o
- all:
- $(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;
- clean:
- $(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean;
- rm -f *.ko
编译成功后加载内核模块:
- tim@tim:~$ sudo insmod mydemo.ko
- tim@tim:~$ sudo dmesg
- [ 7849.308204] succeeded register char device: my_demo_dev
- [ 7849.308206] major number = 238, minor number = 0
可以通过查看 /proc/devices 文件核对:
- tim@tim:~$ cat /proc/devices
- Character devices:
- 238 my_demo_dev
'proc' 是 'process' 的缩写。
最后我们手动创建一个 dev 节点,这样就能在用户层使用这个字符设备了:
- tim@tim:~$ sudo mknod /dev/demo_drv c 238 0
使用 device_create 等系列函数也能自动创建设备节点。
创建好 dev 节点之后,我们就可以写一个测试程序验证写的驱动程序。如代码清单 2 所示,调用用户态的 open() 和 read(),最终会调用到驱动中的 open() 和 read()。
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
- #define DEMO_DEV_NAME "/dev/demo_drv"
- int main()
- {
- char buffer[64];
- int fd;
- fd = open(DEMO_DEV_NAME, O_RDONLY);
- if (fd < 0)
- {
- printf("open device %s failed\n", DEMO_DEV_NAME);
- return -1;
- }
- read(fd, buffer, 64);
- close(fd);
- return 0;
- }
编译测试程序后运行,并查看内核日志,可以看到符合预期。
- tim@tim:~$ gcc test.c -o test
- tim@tim:~$ ./test
- tim@tim:~$ sudo dmesg
- [ 926.311786] demodrv_open: major=238, minor=0
- [ 926.311790] demodrv_read: enter
实验:使用 misc 机制来创建设备驱动
杂项设备(Miscellaneous Device)是Linux内核中一种特殊类型的设备,用于表示一些无法被常规设备类型(如字符设备或块设备)所归类的设备。它提供了一种通用的机制,可以将各种类型的设备注册到内核中,以便用户空间程序可以通过文件系统接口与这些设备进行交互。
代码清单 3 中把之前的字符设备驱动代码改写成了杂项设备驱动。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/miscdevice.h>
- #define DEMO_NAME "my_demo_dev"
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- int major = MAJOR(inode->i_rdev);
- int minor = MINOR(inode->i_rdev);
- printk("%s: major=%d, minor=%d\n", __func__, major, minor);
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t lbuf, loff_t* ppos)
- {
- printk("%s: enter\n", __func__);
- return 0;
- }
- static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* f_pos)
- {
- printk("%s: enter\n", __func__);
- return 0;
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write
- };
- static struct device* mydemodrv_device;
- static struct miscdevice mydemodrv_misc_device =
- {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &demodrv_fops,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- ret = misc_register(&mydemodrv_misc_device);
- if (ret)
- {
- printk("failed register misc device\n");
- return ret;
- }
- mydemodrv_device = mydemodrv_misc_device.this_device;
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- }
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
- misc_deregister(&mydemodrv_misc_device);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
和字符设备主要的差异是,杂项设备的注册很简单,一个 misc_register() 函数即可完成。而且能够自动在 /dev 目录下分配设备文件,无需我们手动创建。它的原型为:
- int misc_register(struct miscdevice* misc);
针对传入的 miscdevice 结构体,我们一般需要赋值以下成员:
minor:次设备号。可以手动指定,一般使用 MISC_DYNAMIC_MINOR 进行动态分配。
杂项设备的主设备号是 10。
name:设备名称。就是出现在 /dev 目录下的设备文件名。
fops:文件操作函数集合。这块和上一节字符设备驱动中提及的文件操作函数集合是同一个内容。
与 misc_register 对应,misc_deregister 用于注销杂项设备。
注意引入头文件 linux/miscdevice.h。
实验:为虚拟设备编写驱动
本实验为 read() 和 write() 方法添加实现。为此我们创建一个虚拟设备,通过它来进行读写操作。
代码清单 4 是它的实现,大部分内容和上节内容是一样的。我们关注 device_buffer 指针,它在模块加载时分配空间,同时注意释放。这段分配的内存就是模拟我们读写操作作用的内存。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/miscdevice.h>
- #define DEMO_NAME "my_demo_dev"
- /* 虚拟 FIFO 设备的缓冲区 */
- static char* device_buffer;
- #define MAX_DEVICE_BUFFER_SIZE 64
- static struct device* mydemodrv_device;
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- int major = MAJOR(inode->i_rdev);
- int minor = MINOR(inode->i_rdev);
- printk("%s: major=%d, minor=%d\n", __func__, major, minor);
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t lbuf, loff_t* ppos)
- {
- int actual_readed;
- int max_free;
- int need_read;
- int ret;
- printk("%s enter\n", __func__);
- max_free = MAX_DEVICE_BUFFER_SIZE - *ppos;
- need_read = max_free > lbuf ? lbuf : max_free;
- if (need_read == 0)
- dev_warn(mydemodrv_device, "no space for read");
- ret = copy_to_user(buf, device_buffer + *ppos, need_read);
- if (ret == need_read)
- return -EFAULT;
- actual_readed = need_read - ret;
- *ppos += actual_readed;
- printk("%s, actual_read=%d, pos=%lld\n", __func__, actual_readed, *ppos);
- return actual_readed;
- }
- static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- int actual_write;
- int free;
- int need_write;
- int ret;
- printk("%s: enter\n", __func__);
- free = MAX_DEVICE_BUFFER_SIZE - *ppos;
- need_write = free > count ? count : free;
- if (need_write == 0)
- dev_warn(mydemodrv_device, "no space for write");
- ret = copy_from_user(device_buffer + *ppos, buf, need_write);
- if (ret == need_write)
- return -EFAULT;
- actual_write = need_write - ret;
- *ppos += actual_write;
- printk("%s: actual_write=%d, ppos=%lld\n", __func__, actual_write, *ppos);
- return actual_write;
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write
- };
- static struct miscdevice mydemodrv_misc_device =
- {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &demodrv_fops,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- device_buffer = kmalloc(MAX_DEVICE_BUFFER_SIZE, GFP_KERNEL);
- if (!device_buffer)
- return -ENOMEM;
- ret = misc_register(&mydemodrv_misc_device);
- if (ret)
- {
- printk("failed register misc device\n");
- kfree(device_buffer);
- return ret;
- }
- mydemodrv_device = mydemodrv_misc_device.this_device;
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- }
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
- kfree(device_buffer);
- misc_deregister(&mydemodrv_misc_device);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
关注实现的 read() 函数,代码里对应为 demodrv_read,其定义为:
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t lbuf, loff_t* ppos);
file:设备文件指针,包含文件描述符等设备文件信息。
buf:指向用户空间(特别用 __user 宏加以指示)缓冲区的指针。之后读取的数据就会传输到这里。
lbuf:要读取的最大字节数。
ppos:文件位置偏移量的指针。它用于跟踪在文件中读取的位置,以便下一次读取可以从适当的位置开始。
demodrv_read() 函数的操作:根据 ppos 确定要读取的数据位置,将内核空间缓冲区内容复制到用户空间缓冲区。更新 ppos 的值,以便下次从适当的位置开始。返回读取的字节数或错误码。
将数据从内核空间复制到用户空间,使用 copy_to_user() 函数。它的原型如下:
- unsigned long copy_to_user(void __user* to, const void* from, unsigned long n);
to:指向用户空间目标地址的指针。数据将从内核空间复制到这个目标地址。
from:指向内核空间源地址的指针。数据将从这个源地址复制到用户空间。
n:要复制的字节数。
copy_to_user 函数的返回值是复制失败的字节数。如果返回值为0,则表示复制成功。
注意,copy_to_user 函数的返回值是复制失败的字节数。
demodrv_write() 是实现的写操作函数,参数定义和写操作是类似,这边就不一一讲解说明了。copy_from_user() 用于将数据从用户空间复制到内核空间。
接着,如代码清单 5 所示,我们写一个测试程序,来进行读写测试。
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <string.h>
- #define DEMO_DEV_NAME "/dev/my_demo_dev"
- int main()
- {
- char buffer[64];
- int fd;
- int ret;
- size_t len;
- char message[] = "Testing the virtual FIFO device";
- char* read_buffer;
- len = sizeof(message);
- fd = open(DEMO_DEV_NAME, O_RDWR);
- if (fd < 0)
- {
- printf("open device %s failed\n", DEMO_DEV_NAME);
- return -1;
- }
- /* 1. write the message to device */
- ret = write(fd, message, len);
- if (ret != len)
- {
- printf("cannot write on device %d, ret=%d", fd, ret);
- return -1;
- }
- /* close the fd, and reopen it */
- close(fd);
- fd = open(DEMO_DEV_NAME, O_RDWR);
- if (fd < 0)
- {
- printf("open device %s failed\n", DEMO_DEV_NAME);
- return -1;
- }
- read_buffer = (char*)malloc(2 * len);
- memset(read_buffer, 0, 2 * len);
- ret = read(fd, read_buffer, 2*len);
- printf("read %d bytes\n", ret);
- printf("read buffer=%s\n", read_buffer);
- close(fd);
- free(read_buffer);
- return 0;
- }
测试程序首先打开设备,写入 message 数组,大小是 32 字节(包含 null 终止字符)。接着关闭设备再重新打开,可以读取刚才写入的内容。
需要打开两次设备的原因是我们没有实现 llseek 函数。驱动程序中,每次读写操作都会维护增加文件偏移量。
我们可以从 log 中核实具体的操作:
- tim@tim:~$ sudo ./test
- read 64 bytes
- read buffer=Testing the virtual FIFO device
- tim@tim:~$ sudo dmesg
- [23093.876971] demodrv_open: major=10, minor=120
- [23093.876975] demodrv_write: enter
- [23093.876975] demodrv_write: actual_write=32, ppos=32
- [23093.876999] demodrv_open: major=10, minor=120
- [23093.877010] demodrv_read enter
- [23093.877010] demodrv_read, actual_read=64, pos=64
实验:使用 KFIFO 环形缓冲区改进设备驱动
上节的实验,缓冲区的写入和读取都是基于文件偏移值的,这不利于“生产者和消费者”的情况。针对这种情况,我们这节介绍一种 Linux 内核实现的称为 KFIFO 的机制。
书中介绍 KFIFO 无需额外加锁就能保证数据的安全。猜测内部实现是无锁队列。
代码清单 6 是 KFIFO 的例子,主要是 DEFINE_KFIFO、kfifo_to_user 和 kfifo_from_user 的使用。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/miscdevice.h>
- #include <linux/kfifo.h>
- #define DEMO_NAME "my_demo_dev"
- static struct device* mydemodrv_device;
- DEFINE_KFIFO(mydemo_fifo, char, 64);
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- int major = MAJOR(inode->i_rdev);
- int minor = MINOR(inode->i_rdev);
- printk("%s: major=%d, minor=%d\n", __func__, major, minor);
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t count, loff_t* ppos)
- {
- int actual_readed;
- int ret;
- printk("%s: count=%lu\n", __func__, count);
- ret = kfifo_to_user(&mydemo_fifo, buf, count, &actual_readed);
- if (ret)
- return -EIO;
- printk("%s, actual_readed=%d, pos=%lld\n", __func__, actual_readed, *ppos);
- return actual_readed;
- }
- static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- int actual_write;
- int ret;
- printk("%s: count=%lu\n", __func__, count);
- //if (kfifo_avail(&mydemo_fifo) < count)
- // return -EAGAIN;
- ret = kfifo_from_user(&mydemo_fifo, buf, count, &actual_write);
- if (ret)
- return -EIO;
- printk("%s: actual_write=%d, ppos=%lld\n", __func__, actual_write, *ppos);
- return actual_write;
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write
- };
- static struct miscdevice mydemodrv_misc_device =
- {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &demodrv_fops,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- ret = misc_register(&mydemodrv_misc_device);
- if (ret)
- {
- printk("failed register misc device\n");
- return ret;
- }
- mydemodrv_device = mydemodrv_misc_device.this_device;
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- }
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
- misc_deregister(&mydemodrv_misc_device);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
DEFINE_KFIFO 的定义如下,用于申明一个 kfifo 变量:
- DEFINE_KFIFO(name, type, size)
name 是为 kfifo 分配的变量名;type 是存储在 kfifo 中的数据类型;size 是 kfifo 的大小,单位是 type 类型元素的数量。
kfifo_to_user 的定义如下,用于将 kfifo 中的数据复制到用户空间。
- unsigned int kfifo_to_user(
- struct kfifo* fifo,
- void __user* to,
- unsigned int len,
- unsigned int* copied);
fifo 是指向 kfifo 结构的指针;to 是指向用户空间的指针,数据将被复制到这个地址;len 是请求复制的字节数;copied 是一个指向无符号整数的指针,将返回实际复制的字节数。
kfifo_to_user 如果操作成功,将返回 0;否则返回错误码。
kfifo_from_user 用于将用户空间的数据复制到 kfifo 中,其定义和 kfifo_to_user 的类似,不再赘述。
与上一节的测试程序不同,使用 kfifo 就不用打开两次设备文件了,因为 kfifo 内部有维护 “队列指针”。这边我们再介绍另一种验证方法,使用 cat 命令和重定向命令写入:
- tim@tim:~$ sudo su
- root@tim:~$ echo "Hello World!" > /dev/my_demo_dev
- root@tim:~$ cat /dev/my_demo_dev
- Hello World!
我们可以从日志里核对读写操作。可以推测,cat 的逻辑是一直读取到没有内容读为止,即 read 的返回值为 0。
- [13235.125900] demodrv_open: major=10, minor=120
- [13235.125914] demodrv_write: count=13
- [13235.125916] demodrv_write: actual_write=13, ppos=0
- [13244.102314] demodrv_open: major=10, minor=120
- [13244.102324] demodrv_read: count=131072
- [13244.102326] demodrv_read, actual_readed=13, pos=0
- [13244.102341] demodrv_read: count=131072
- [13244.102342] demodrv_read, actual_readed=0, pos=0
重定向写入的逻辑也是类似,直到 write 的返回值累计达到需要写入的值为止。所以目前的程序,当需要写入的内容大于 kfifo 的容量时,会一直尝试写入,导致控制台一直不会返回。
目前想到一种简单的方法,写入的字节数大于 kfifo 容量时,直接返回错误:
if (kfifo_avail(&mydemo_fifo) < count) return -EAGAIN;
实验:把虚拟设备驱动改为非阻塞模式
open() 函数的定义如下,其中 flags 可以设置为 O_NONBLOCK,指示以非阻塞模式打开文件。
- int open(const char* pathname, int flags);
我们修改之前的驱动代码,如代码清单 7 所示,以适配非阻塞模式。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/miscdevice.h>
- #include <linux/kfifo.h>
- #define DEMO_NAME "my_demo_dev"
- static struct device* mydemodrv_device;
- DEFINE_KFIFO(mydemo_fifo, char, 64);
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- int major = MAJOR(inode->i_rdev);
- int minor = MINOR(inode->i_rdev);
- printk("%s: major=%d, minor=%d\n", __func__, major, minor);
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t count, loff_t* ppos)
- {
- int actual_readed;
- int ret;
- printk("%s: count=%lu\n", __func__, count);
- if (kfifo_is_empty(&mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- }
- ret = kfifo_to_user(&mydemo_fifo, buf, count, &actual_readed);
- if (ret)
- return -EIO;
- printk("%s, actual_readed=%d, pos=%lld\n", __func__, actual_readed, *ppos);
- return actual_readed;
- }
- static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- int actual_write;
- int ret;
- printk("%s: count=%lu\n", __func__, count);
- if (kfifo_is_full(&mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- }
- ret = kfifo_from_user(&mydemo_fifo, buf, count, &actual_write);
- if (ret)
- return -EIO;
- printk("%s: actual_write=%d, ppos=%lld\n", __func__, actual_write, *ppos);
- return actual_write;
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write
- };
- static struct miscdevice mydemodrv_misc_device =
- {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &demodrv_fops,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- ret = misc_register(&mydemodrv_misc_device);
- if (ret)
- {
- printk("failed register misc device\n");
- return ret;
- }
- mydemodrv_device = mydemodrv_misc_device.this_device;
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- }
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
- misc_deregister(&mydemodrv_misc_device);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
较上一节的代码,增加非阻塞操作逻辑:如果读取时 kfifo 为空,则直接返回错误值;如果写入时 kfifo 已经满了,则直接返回错误值。
EAGAIN 可以记忆成 "Error, Try AGAIN"。
这也是上一节一直写入问题的一种解决方式。
从这个例子可以看出,标志在驱动层是否起作用,取决于具体的设备驱动程序是否适配此标志。
实验:把虚拟设备驱动改为阻塞模式
上一节实验的设备驱动是非阻塞的,只要驱动当时无法满足请求的资源,就会立即返回 -EAGAIN。应用程序在驱动非阻塞模式下,为了能成功读写数据,需要采用轮询的方式。
而应用程序在驱动阻塞模式下,就不用这么麻烦:当请求的数据无法立刻满足时,驱动会让进程睡眠,直到数据准备好为止。
在内核中,我们可以使用等待队列来维护进程的阻塞模式相关操作。等待队列的定义如下:
书上说可使用等待队列机制来实现进程的阻塞,我认为用“维护”更合适一点。
- typedef struct __wait_queue_head {
- spinlock_t lock;
- struct list_head task_list;
- } wait_queue_head_t;
我们可以把等待队列简单理解成一个带锁的链表,以满足同步需求。等待队列上的相关操作有睡眠等待和唤醒,这边先仅介绍实验代码中用到的。
睡眠等待相关的操作函数是 wait_event_interruptible,其定义如下:
- wait_event_interruptible(q, condition)
其中,q 是 wait_queue_head_t 类型的等待队列头部,condition 是等待条件。wait_event_interruptible 会让进程在满足等待条件时进入睡眠状态。
这边说明一下 wait_event_interruptible 的大致流程:首先判断传入的等待条件是否满足,如果不满足则直接返回,以防止进程进行无故的睡眠等待。如果满足等待条件,会把当前进程信息放入等待队列中,并主动让出执行时间,进入睡眠状态,这部分是内核中的进程调度机制。当满足唤醒条件时,调度机制会重新恢复睡眠前保存的上下文,继续运行。后续会把唤醒的进程从等待队列中删除。
驱动在内核态也是进程上下文相关的,驱动部分也同样进入阻塞。
唤醒相关的操作函数是 wake_up_interruptible,其定义如下:
- void wake_up_interruptible(wait_queue_head_t *q);
wake_up_interruptible 会唤醒传入等待队列中记录的所有进程。函数中进行的“唤醒”操作仅仅是标记进程的状态为就绪,即不保证进程马上就能唤醒,还是依赖于调度策略。
以上就是睡眠等待和唤醒的介绍,可以看到机制都是基于本身的调度机制,所以我觉得等待队列用“维护”一词更加妥当。
等待队列的维护者是本驱动。单从等待队列上来看就是带锁的链表,就是进程信息的插入、删除和遍历,并不是高深的知识,不用“害怕”。
如代码清单 8 所示,我们看阻塞版本的 kfifo 实验代码。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/slab.h>
- #include <linux/miscdevice.h>
- #include <linux/kfifo.h>
- #define DEMO_NAME "my_demo_dev"
- DEFINE_KFIFO(mydemo_fifo, char, 64);
- struct mydemo_device
- {
- const char* name;
- struct device* dev;
- struct miscdevice* miscdev;
- wait_queue_head_t read_queue;
- wait_queue_head_t write_queue;
- };
- struct mydemo_private_data
- {
- struct mydemo_device* device;
- };
- static struct mydemo_device* mydemo_device;
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- struct mydemo_private_data* data;
- struct mydemo_device* device = mydemo_device;
- printk("%s: major=%d, minor=%d\n", __func__, MAJOR(inode->i_rdev), MINOR(inode->i_rdev));
- data = kmalloc(sizeof(struct mydemo_private_data), GFP_KERNEL);
- if (!data)
- return -ENOMEM;
- data->device = device;
- file->private_data = data;
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- struct mydemo_private_data* data = file->private_data;
- kfree(data);
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t count, loff_t* ppos)
- {
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- int actual_readed;
- int ret;
- printk("%s: count=%lu\n", __func__, count);
- if (kfifo_is_empty(&mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- printk("%s: pid=%d, going to sleep\n", __func__, current->pid);
- ret = wait_event_interruptible(device->read_queue, !kfifo_is_empty(&mydemo_fifo));
- if (ret)
- return ret;
- }
- ret = kfifo_to_user(&mydemo_fifo, buf, count, &actual_readed);
- if (ret)
- return -EIO;
- if (!kfifo_is_full(&mydemo_fifo))
- wake_up_interruptible(&device->write_queue);
- printk("%s, pid=%d, actual_readed=%d, pos=%lld\n", __func__, current->pid, actual_readed, *ppos);
- return actual_readed;
- }
- static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- int actual_write;
- int ret;
- printk("%s: count=%lu\n", __func__, count);
- if (kfifo_is_full(&mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- printk("%s: pid=%d, going to sleep\n", __func__, current->pid);
- ret = wait_event_interruptible(device->write_queue, !kfifo_is_full(&mydemo_fifo));
- if (ret)
- return ret;
- }
- ret = kfifo_from_user(&mydemo_fifo, buf, count, &actual_write);
- if (ret)
- return -EIO;
- if (!kfifo_is_empty(&mydemo_fifo))
- wake_up_interruptible(&device->read_queue);
- printk("%s: pid=%d, actual_write=%d, ppos=%lld\n", __func__, current->pid, actual_write, *ppos);
- return actual_write;
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write
- };
- static struct miscdevice mydemodrv_misc_device =
- {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &demodrv_fops,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- struct mydemo_device* device = kmalloc(sizeof(struct mydemo_device), GFP_KERNEL);
- if (!device)
- return -ENOMEM;
- ret = misc_register(&mydemodrv_misc_device);
- if (ret)
- {
- printk("failed register misc device\n");
- goto free_device;
- }
- device->dev = mydemodrv_misc_device.this_device;
- device->miscdev = &mydemodrv_misc_device;
- init_waitqueue_head(&device->read_queue);
- init_waitqueue_head(&device->write_queue);
- mydemo_device = device;
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- free_device:
- kfree(device);
- return ret;
- }
- static void __exit simple_char_exit(void)
- {
- struct mydemo_device* device = mydemo_device;
- printk("removing device\n");
- misc_deregister(&mydemodrv_misc_device);
- kfree(device);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
在驱动设备加载时,初始化读和写的等待队列。设备进行读取时,如果 kfifo 为空,则进入睡眠等待。读取内容后,如果 kfifo 没满,则唤醒写等待队列中的进程。设备进行写操作时,如果 kfifo 已满,则进入睡眠等待。写入内容后,如果 kfifo 非空,则唤醒读等待队列中的进程。
我们使用 cat 和 echo 来验证驱动。
- tim@tim:~$ sudo su
- root@tim:~$ cat /dev/my_demo_dev &
- root@tim:~$ echo "Hello World" > /dev/my_demo_dev
- root@tim:~$ Hello World
在命令后加 &,表示让其在后台运行。因为一开始驱动内部的 kfifo 是空的,所以此时 cat 读取进程会进入睡眠等待。接着我们使用 echo 写入,则会唤醒读进程,控制台上就打印了写入的内容。
实验:向虚拟设备中添加 I/O 多路复用支持
多路复用在应用层上对应的函数是 poll() 函数。因为之前接触它不多,所以这个实验我们先从测试应用程序开始。先了解应用层想得到什么功能,再去实现驱动对应的内容。
poll 函数用于监控多个文件描述符以查看它们是否已经准备好进行 I/O 操作。函数原型如下:
- int poll(struct pollfd fds[], nfds_t nfds, int timeout);
参数 fds 是一个指向 pollfd 结构数组的指针。每个 pollfd 结构表示一个文件描述符以及想要监视的事件类型。
参数 nfds 指定 fds 数组中的元素数目。
参数 timeout 指定返回前等待的最大毫秒数。设置为 -1,表示会一直阻塞,直到想要的事件发生。
参数 fds 对应的 pollfd 结构定义如下:
- struct pollfd {
- int fd; /* 文件描述符 */
- short events; /* 请求的事件 */
- short revents; /* 返回的事件 */
- };
成员 fd 为要被监控的文件描述符。
成员 events 指定感兴趣的事件的位掩码。常见的值包括 POLLIN(数据可读),POLLOUT(数据可写)。
成员 revents 的内容是,当 poll 返回时,包含的实际发生的事件的位掩码。
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <sys/ioctl.h>
- #include <fcntl.h>
- #include <errno.h>
- #include <poll.h>
- #include <linux/input.h>
- #include <unistd.h>
- int main()
- {
- int ret;
- struct pollfd fds[2];
- char buffer0[64];
- char buffer1[64];
- fds[0].fd = open("/dev/mydemo0", O_RDWR);
- if (fds[0].fd == -1)
- goto fail;
- fds[0].events = POLLIN;
- fds[1].fd = open("/dev/mydemo1", O_RDWR);
- if (fds[1].fd == -1)
- goto fail;
- fds[1].events = POLLIN;
- while (1)
- {
- ret = poll(fds, 2, -1);
- if (ret == -1)
- goto fail;
- if (fds[0].revents & POLLIN)
- {
- ret = read(fds[0].fd, buffer0, sizeof(buffer0));
- if (ret < 0)
- goto fail;
- printf("%s\n", buffer0);
- }
- if (fds[1].revents & POLLIN)
- {
- ret = read(fds[1].fd, buffer1, sizeof(buffer1));
- if (ret < 0)
- goto fail;
- printf("%s\n", buffer1);
- }
- }
- fail:
- perror("poll test failed");
- exit(EXIT_FAILURE);
- }
代码清单 9 是 poll 函数的测试程序。它一直监控两个设备是否数据可读,一旦数据可读,就从中读取数据。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/miscdevice.h>
- #include <linux/device.h>
- #include <linux/slab.h>
- #include <linux/kfifo.h>
- #include <linux/wait.h>
- #include <linux/sched.h>
- #include <linux/cdev.h>
- #include <linux/poll.h>
- #define DEMO_NAME "mydemo_dev"
- #define MYDEMO_FIFO_SIZE 64
- static dev_t dev;
- static struct cdev* demo_cdev;
- struct mydemo_device
- {
- char name[64];
- struct device* dev;
- wait_queue_head_t read_queue;
- wait_queue_head_t write_queue;
- struct kfifo mydemo_fifo;
- };
- struct mydemo_private_data
- {
- struct mydemo_device* device;
- char name[64];
- };
- #define MYDEMO_MAX_DEVICES 8
- static struct mydemo_device* mydemo_device[MYDEMO_MAX_DEVICES];
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- unsigned int minor = iminor(inode);
- struct mydemo_private_data* data;
- struct mydemo_device* device = mydemo_device[minor];
- printk("%s: major=%d. minor=%d, device=%s\n",
- __func__, MAJOR(inode->i_rdev), MINOR(inode->i_rdev), device->name);
- data = kmalloc(sizeof(struct mydemo_private_data), GFP_KERNEL);
- if (!data)
- return -ENOMEM;
- sprintf(data->name, "private_data_%d", minor);
- data->device = device;
- file->private_data = data;
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- struct mydemo_priavte_data* data = file->private_data;
- kfree(data);
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char* __user buf, size_t count, loff_t* ppos)
- {
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- int actual_readed;
- int ret;
- if (kfifo_is_empty(&device->mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- printk("%s:%s pid=%d, going to sleep, %s\n",
- __func__, device->name, current->pid, data->name);
- ret = wait_event_interruptible(device->read_queue, !kfifo_is_empty(&device->mydemo_fifo));
- if (ret)
- return ret;
- }
- ret = kfifo_to_user(&device->mydemo_fifo, buf, count, &actual_readed);
- if (ret)
- return -EIO;
- if (!kfifo_is_full(&device->mydemo_fifo))
- wake_up_interruptible(&device->write_queue);
- printk("%s:%s, pid=%d, actual_readed=%d, pos=%lld\n",
- __func__, device->name, current->pid, actual_readed, *ppos);
- return actual_readed;
- }
- static ssize_t demodrv_write(struct file* file, const char* __user buf, size_t count, loff_t* ppos)
- {
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- unsigned int actual_write;
- int ret;
- if (kfifo_is_full(&device->mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- printk("%s:%s pid=%d, going to sleep, %s\n",
- __func__, device->name, current->pid, data->name);
- ret = wait_event_interruptible(device->write_queue, !kfifo_is_full(&device->mydemo_fifo));
- if (ret)
- return ret;
- }
- ret = kfifo_from_user(&device->mydemo_fifo, buf, count, &actual_write);
- if (ret)
- return -EIO;
- if (!kfifo_is_empty(&device->mydemo_fifo))
- wake_up_interruptible(&device->read_queue);
- printk("%s:%s pid=%d, actual_write=%d, ppos=%lld, ret=%d\n",
- __func__, device->name, current->pid, actual_write, *ppos, ret);
- return actual_write;
- }
- static unsigned int demodrv_poll(struct file* file, poll_table* wait)
- {
- int mask = 0;
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- poll_wait(file, &device->read_queue, wait);
- poll_wait(file, &device->write_queue, wait);
- if (!kfifo_is_empty(&device->mydemo_fifo))
- mask |= POLLIN | POLLRDNORM;
- if (!kfifo_is_full(&device->mydemo_fifo))
- mask |= POLLOUT | POLLWRNORM;
- return mask;
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write,
- .poll = demodrv_poll,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- int i;
- struct mydemo_device* device;
- ret = alloc_chrdev_region(&dev, 0, MYDEMO_MAX_DEVICES, DEMO_NAME);
- if (ret)
- {
- printk("failed to allocate char device region\n");
- return ret;
- }
- demo_cdev = cdev_alloc();
- if (!demo_cdev)
- {
- printk("cdev_alloc failed\n");
- goto unregister_chrdev;
- }
- cdev_init(demo_cdev, &demodrv_fops);
- ret = cdev_add(demo_cdev, dev, MYDEMO_MAX_DEVICES);
- if (ret)
- {
- printk("cdev_add failed\n");
- goto cdev_fail;
- }
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- device = kmalloc(sizeof(struct mydemo_device), GFP_KERNEL);
- if (!device)
- {
- ret = -ENOMEM;
- goto free_device;
- }
- sprintf(device->name, "%s%d", DEMO_NAME, i);
- mydemo_device[i] = device;
- init_waitqueue_head(&device->read_queue);
- init_waitqueue_head(&device->write_queue);
- ret = kfifo_alloc(&device->mydemo_fifo, MYDEMO_FIFO_SIZE, GFP_KERNEL);
- if (ret)
- {
- ret = -ENOMEM;
- goto free_kfifo;
- }
- printk("mydemo_fifo=%px\n", &device->mydemo_fifo);
- }
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- free_kfifo:
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- if (&device->mydemo_fifo)
- kfifo_free(&device->mydemo_fifo);
- }
- free_device:
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- if (mydemo_device[i])
- kfree(mydemo_device[i]);
- }
- cdev_fail:
- cdev_del(demo_cdev);
- unregister_chrdev:
- unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);
- return ret;
- }
- static void __exit simple_char_exit(void)
- {
- int i;
- printk("removing device\n");
- if (demo_cdev)
- cdev_del(demo_cdev);
- unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- if (mydemo_device[i])
- kfree(mydemo_device[i]);
- }
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
因为 poll 是监控多个文件描述符的,为了方便,我们让此驱动程序支持多设备。如代码清单 10 的第 36 行所示,我们定义了长度为 8 的设备信息数组,里面包含各自的 kfifo 和读写等待队列。
各个设备信息的初始化可以看设备初始化函数 simple_char_init,其中为 8 个设备分配设备号并初始化和添加注册。然后初始化各个设备信息,包括初始化 kfifo 和读写等待队列。
因为副设备号是从 0 开始的,所以在 open 的时候,可以直接用副设备号对设备信息进行索引。我们将索引到的设备信息指针保存到 private_data 中,以供后续的 read、write、poll 操作函数使用。
read 和 write 操作函数和之前的逻辑是一样的,使用 private_data 里指定的 kfifo 和读写等待队列。
不同的设备使用不同的 kfifo。这在设计上是容易理解的。
接下来就开始介绍重点了:我们需要实现 file_operations 中的 poll 接口,在 129 至 144 行。
- void poll_wait(struct file* filp, wait_queue_head_t* wait_address, poll_table* p);
首先调用 poll_wait 函数,它将当前进程添加到等待队列中,然后将文件设备指针 filp 以及等待队列 wait_address 关联记录到 poll_table 中。
然后会根据 kfifo 的实际情况,设置设备状态掩码。
poll_wait 只是起到添加、关联的作用。不会引起阻塞。
最后需要说明很重要的点:驱动 poll 接口中不引起阻塞,那是在哪里进行阻塞的,才能达到性能控制?用户层 poll 函数最终会调用到内核中的 do_sys_poll 函数。在 do_sys_poll 中,它会根据文件设备指针调用各个设备驱动层的 poll 接口。
do_sys_poll 中会根据驱动层 poll 接口掩码返回值判断各个设备的读写状态,如果有设备满足需求,那最好,do_sys_poll 可以直接返回;如果各个设备都不满足,那就只能把当前进程进入睡眠状态。
因为之前我们已经在 poll_wait 中把当前进程加入到读写队列里了,睡眠的进程可以通过 wake_up_interruptible 被唤醒。唤醒的 do_sys_poll 会再次调用驱动层的 poll 函数进行检查,然后返回最新的设备状态。
还遗留一个问题,在驱动代码中即使设备可读写,也通过 poll_wait 把当前进程加入到等待队列中,会不会引起性能问题?其实不用担心,do_sys_poll 在返回时会移除 poll_table 中记录的进程项。
以上其实已经讲清了 poll 的基本原理。书上没讲这么详细。
驱动程序和测试程序都有了,现在我们开始测试功能。首先我们需要手动创建设备节点:
- tim@tim:~$ cat /proc/devices
- 238 mydemo_dev
- tim@tim:~$ sudo mknod /dev/mydemo0 c 238 0
- tim@tim:~$ sudo mknod /dev/mydemo1 c 238 1
接着后台运行测试程序。因为一开始两个设备都是不可读的,所以此时程序会进入阻塞状态。
我们使用 echo 命令先往 /dev/mydemo0 设备里写入数据,此时程序会被唤醒,poll 返回 /dev/mydemo0 可读,然后读取其中的内容进行打印;接着继续调用 poll,程序又进入阻塞状态;再往 /dev/mydemo1 设备里写入数据,也是如预期打印了写入的数据。
- tim@tim:~$ sudo ./test &
- tim@tim:~$ sudo su
- root@tim:~$ echo "Hello0" > /dev/mydemo0
- Hello0
- root@tim:~$ echo "Hello1" > /dev/mydemo1
- Hello1
实验:为什么不能唤醒读写进程
这个实验是上一个实验的延续,是作者故意制造的错误。
代码清单 11 是错误代码的一个片段,需要注意它和上一节实验的差异:上一节实验的等待队列是每个不同设备有一个,而此处是每个 open “会话”一个。
- static dev_t dev;
- static struct cdev *demo_cdev;
- struct mydemo_device {
- char name[64];
- struct device *dev;
- };
- struct mydemo_private_data {
- struct mydemo_device *device;
- char name[64];
- struct kfifo mydemo_fifo;
- wait_queue_head_t read_queue;
- wait_queue_head_t write_queue;
- };
- #define MYDEMO_MAX_DEVICES 8
- static struct mydemo_device *mydemo_device[MYDEMO_MAX_DEVICES];
- static int demodrv_open(struct inode *inode, struct file *file)
- {
- unsigned int minor = iminor(inode);
- struct mydemo_private_data *data;
- struct mydemo_device *device = mydemo_device[minor];
- int ret;
- printk("%s: major=%d, minor=%d, device=%s\n", __func__,
- MAJOR(inode->i_rdev), MINOR(inode->i_rdev), device->name);
- data = kmalloc(sizeof(struct mydemo_private_data), GFP_KERNEL);
- if (!data)
- return -ENOMEM;
- sprintf(data->name, "private_data_%d", minor);
- ret = kfifo_alloc(&data->mydemo_fifo,
- MYDEMO_FIFO_SIZE,
- GFP_KERNEL);
- if (ret) {
- kfree(data);
- return -ENOMEM;
- }
- init_waitqueue_head(&data->read_queue);
- init_waitqueue_head(&data->write_queue);
- data->device = device;
- file->private_data = data;
- return 0;
- }
上一节我们已经知道了 poll 的原理,poll 中关联维护的等待队列就是驱动中的等待队列。而上述错误代码中,一个 open “会话”就单独使用一个等待队列,如果在一个“会话”内,那是没有问题的。但是往往“生产者/消费者”对驱动的操作都是不同的进程,即使用上述驱动,open 后都对应自己的等待队列,那添加、唤醒的操作肯定是彼此影响不到。它们根本没有关联。
实验:向虚拟设备添加异步通知
除了阻塞之外,异步通知机制也是一种节约资源的手段,它允许应用程序在事件发生时接收通知。类似中断或者回调函数的概念。
一般在驱动中实现异步通知的步骤如下:
1. 初始化 fasync 结构:驱动中需要包含一个指向 fasync_struct 结构的指针。这个结构用于跟踪注册了异步通知的进程。其定义如下:
- struct fasync_struct {
- rwlock_t fa_lock;
- int magic;
- int fa_fd;
- struct fasync_struct* fa_next; /* singly linked list */
- struct file* fa_file;
- struct rcu_head fa_rcu;
- };
根据定义我们可以简单的把 fasync_struct 看成是带锁的、带有通知信息的链表。
2. 实现驱动文件操作函数中的 .fasync 接口:应用程序会使用 fcntl 注册异步通知,最终会调用到 .fasync 接口。我们需要在 .fasync 接口中更新维护 fasync_struct 链表,可使用现有的 fasync_helper 函数来进行维护。
我们先看 .fasync 接口的定义:
- int my_fasync(int fd, struct file* filp, int on);
其中,fd 参数就是用户态的文件描述符,用作信号标识;filp 参数和其他文件操作函数中的一样,标识内核驱动的文件实例;on 参数表示是添加还是删除异步通知。
fasync 接口相比于 read、write 函数有一个 fd 参数。这边可以这么理解:
filp 是驱动设备的文件抽象,所以各个接口都需要。但是异步通知信号是按进程通知的,所以需要额外区分打开设备的进程,这边用用户态的 fd 进行表示。
再看到 fasync_helper 函数:
- int fasync_helper(int fd, struct file* filp, int on, struct fasync_struct** fapp);
其中,fd、filp、on 的含义和 .fasync 接口中定义的是一致的。特别关注 fapp 参数,它是一个二级指针,可以知道链表的维护都是在 fasync_helper 内部进行。在内部维护的意思是说,链表节点空间的分配都是在函数内部,外部只需要记录一个链表头就可以了。
fasync_helper 函数的主要作用也是很容易“猜测”的:将 fd 和 filp 参数作为链表的关键字进行索引,再根据 on 参数来觉得是添加节点还是删除节点。
3. 向用户空间发送通知:当驱动满足条件时,可以通过 kill_fasync 函数来发送信号,通知到用户。
注意仅是发送信号。用户接收到信号,调用对应的信号处理函数是另一套机制。
kill_fasync 函数的原型为:
- void kill_fasync(struct fasync_struct** fa, int sig, int band);
其中,fa 参数就是上文中维护的 fasync_struct 结构指针;sig 参数指定要发送的信号,通常是 SIGIO 信号;band 参数指定事件的类型,一般为 POLL_IN(数据可读)或 POLL_OUT(数据可写)。
kill_fasync 函数中会遍历链表,为每个进程发送通知信号。
这边的 'kill' 应该是做发送信号含义。可能是因为我们熟知杀死进程会发送终止信号。
驱动中实现异步同步的步骤已经介绍完毕。如代码清单 12 所示,我们结合实验代码再整体梳理一遍。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/miscdevice.h>
- #include <linux/device.h>
- #include <linux/slab.h>
- #include <linux/kfifo.h>
- #include <linux/wait.h>
- #include <linux/sched.h>
- #include <linux/cdev.h>
- #include <linux/poll.h>
- #define DEMO_NAME "mydemo_dev"
- #define MYDEMO_FIFO_SIZE 64
- static dev_t dev;
- static struct cdev* demo_cdev;
- struct mydemo_device
- {
- char name[64];
- struct device* dev;
- wait_queue_head_t read_queue;
- wait_queue_head_t write_queue;
- struct kfifo mydemo_fifo;
- struct fasync_struct* fasync;
- };
- struct mydemo_private_data
- {
- struct mydemo_device* device;
- char name[64];
- };
- #define MYDEMO_MAX_DEVICES 8
- static struct mydemo_device* mydemo_device[MYDEMO_MAX_DEVICES];
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- unsigned int minor = iminor(inode);
- struct mydemo_private_data* data;
- struct mydemo_device* device = mydemo_device[minor];
- printk("%s: major=%d. minor=%d, device=%s\n",
- __func__, MAJOR(inode->i_rdev), MINOR(inode->i_rdev), device->name);
- data = kmalloc(sizeof(struct mydemo_private_data), GFP_KERNEL);
- if (!data)
- return -ENOMEM;
- sprintf(data->name, "private_data_%d", minor);
- data->device = device;
- file->private_data = data;
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- struct mydemo_priavte_data* data = file->private_data;
- kfree(data);
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char* __user buf, size_t count, loff_t* ppos)
- {
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- int actual_readed;
- int ret;
- if (kfifo_is_empty(&device->mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- printk("%s:%s pid=%d, going to sleep, %s\n",
- __func__, device->name, current->pid, data->name);
- ret = wait_event_interruptible(device->read_queue, !kfifo_is_empty(&device->mydemo_fifo));
- if (ret)
- return ret;
- }
- ret = kfifo_to_user(&device->mydemo_fifo, buf, count, &actual_readed);
- if (ret)
- return -EIO;
- if (!kfifo_is_full(&device->mydemo_fifo))
- {
- wake_up_interruptible(&device->write_queue);
- kill_fasync(&device->fasync, SIGIO, POLL_OUT);
- }
- printk("%s:%s, pid=%d, actual_readed=%d, pos=%lld\n",
- __func__, device->name, current->pid, actual_readed, *ppos);
- return actual_readed;
- }
- static ssize_t demodrv_write(struct file* file, const char* __user buf, size_t count, loff_t* ppos)
- {
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- unsigned int actual_write;
- int ret;
- if (kfifo_is_full(&device->mydemo_fifo))
- {
- if (file->f_flags & O_NONBLOCK)
- return -EAGAIN;
- printk("%s:%s pid=%d, going to sleep, %s\n",
- __func__, device->name, current->pid, data->name);
- ret = wait_event_interruptible(device->write_queue, !kfifo_is_full(&device->mydemo_fifo));
- if (ret)
- return ret;
- }
- ret = kfifo_from_user(&device->mydemo_fifo, buf, count, &actual_write);
- if (ret)
- return -EIO;
- if (!kfifo_is_empty(&device->mydemo_fifo))
- {
- wake_up_interruptible(&device->read_queue);
- kill_fasync(&device->fasync, SIGIO, POLL_IN);
- printk("%s kill fasync\n", __func__);
- }
- printk("%s:%s pid=%d, actual_write=%d, ppos=%lld, ret=%d\n",
- __func__, device->name, current->pid, actual_write, *ppos, ret);
- return actual_write;
- }
- static unsigned int demodrv_poll(struct file* file, poll_table* wait)
- {
- int mask = 0;
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- poll_wait(file, &device->read_queue, wait);
- poll_wait(file, &device->write_queue, wait);
- if (!kfifo_is_empty(&device->mydemo_fifo))
- mask |= POLLIN | POLLRDNORM;
- if (!kfifo_is_full(&device->mydemo_fifo))
- mask |= POLLOUT | POLLWRNORM;
- return mask;
- }
- static int demodrv_fasync(int fd, struct file* file, int on)
- {
- struct mydemo_private_data* data = file->private_data;
- struct mydemo_device* device = data->device;
- printk("%s send SIGIO\n", __func__);
- return fasync_helper(fd, file, on, &device->fasync);
- }
- static const struct file_operations demodrv_fops =
- {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write,
- .poll = demodrv_poll,
- .fasync = demodrv_fasync,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- int i;
- struct mydemo_device* device;
- ret = alloc_chrdev_region(&dev, 0, MYDEMO_MAX_DEVICES, DEMO_NAME);
- if (ret)
- {
- printk("failed to allocate char device region\n");
- return ret;
- }
- demo_cdev = cdev_alloc();
- if (!demo_cdev)
- {
- printk("cdev_alloc failed\n");
- goto unregister_chrdev;
- }
- cdev_init(demo_cdev, &demodrv_fops);
- ret = cdev_add(demo_cdev, dev, MYDEMO_MAX_DEVICES);
- if (ret)
- {
- printk("cdev_add failed\n");
- goto cdev_fail;
- }
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- device = kmalloc(sizeof(struct mydemo_device), GFP_KERNEL);
- if (!device)
- {
- ret = -ENOMEM;
- goto free_device;
- }
- device->fasync = NULL;
- sprintf(device->name, "%s%d", DEMO_NAME, i);
- mydemo_device[i] = device;
- init_waitqueue_head(&device->read_queue);
- init_waitqueue_head(&device->write_queue);
- ret = kfifo_alloc(&device->mydemo_fifo, MYDEMO_FIFO_SIZE, GFP_KERNEL);
- if (ret)
- {
- ret = -ENOMEM;
- goto free_kfifo;
- }
- printk("mydemo_fifo=%px\n", &device->mydemo_fifo);
- }
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- free_kfifo:
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- if (&device->mydemo_fifo)
- kfifo_free(&device->mydemo_fifo);
- }
- free_device:
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- if (mydemo_device[i])
- kfree(mydemo_device[i]);
- }
- cdev_fail:
- cdev_del(demo_cdev);
- unregister_chrdev:
- unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);
- return ret;
- }
- static void __exit simple_char_exit(void)
- {
- int i;
- printk("removing device\n");
- if (demo_cdev)
- cdev_del(demo_cdev);
- unregister_chrdev_region(dev, MYDEMO_MAX_DEVICES);
- for (i = 0; i < MYDEMO_MAX_DEVICES; i++)
- {
- if (mydemo_device[i])
- kfree(mydemo_device[i]);
- }
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
- MODULE_DESCRIPTION("simple character device");
如代码第 27 行所示,我们在设备信息结构体中添加 fasync_struct 指针。并在驱动加载时(simple_char_init)将 fasync_struct 指针设置为空,即头节点为空。实现的 .fasync 接口为 demodrv_fasync 函数,可以看到其中调用了 fasync_helper 函数对异步通知链表进行维护。在原本的读写接口中(demodrv_read 和 demodrv_write)添加信号发送功能。以写接口为例,当 kfifo 中写入了数据,则调用 kill_fasync 发送设备可读信号。
我们再看到实现的测试程序。
- #define _GNU_SOURCE
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <sys/ioctl.h>
- #include <fcntl.h>
- #include <errno.h>
- #include <poll.h>
- #include <signal.h>
- static int fd;
- void my_signal_fun(int signum, siginfo_t* siginfo, void* act)
- {
- int ret;
- char buf[64];
- if (signum == SIGIO)
- {
- if (siginfo->si_band & POLLIN)
- {
- printf("FIFO is not empty\n");
- if ((ret = read(fd, buf, sizeof(buf))) != -1)
- {
- buf[ret] = '\0';
- puts(buf);
- }
- }
- if (siginfo->si_band & POLLOUT)
- printf("FIFO is not full\n");
- }
- }
- int main()
- {
- int ret;
- int flag;
- struct sigaction act, oldact;
- sigemptyset(&act.sa_mask);
- sigaddset(&act.sa_mask, SIGIO);
- act.sa_flags = SA_SIGINFO;
- act.sa_sigaction = my_signal_fun;
- if (sigaction(SIGIO, &act, &oldact) == -1)
- goto fail;
- fd = open("/dev/mydemo0", O_RDWR);
- if (fd < 0)
- goto fail;
- /* 设置异步I/O信号的所有者为当前进程 */
- if (fcntl(fd, F_SETOWN, getpid()) == -1)
- goto fail;
- /* 设置文件描述符异步I/O通知时要发送的信号类型 */
- if (fcntl(fd, F_SETSIG, SIGIO) == -1)
- goto fail;
- /* 获取文件 flags */
- if ((flag = fcntl(fd, F_GETFL)) == -1)
- goto fail;
- /* 设置文件 flags,设置 FASYNC,支持异步通知 */
- if (fcntl(fd, F_SETFL, flag | FASYNC) == -1)
- goto fail;
- while (1)
- sleep(1);
- fail:
- perror("fasync test");
- exit(EXIT_FAILURE);
- }
如代码清单 13 所示,首先会调用 sigaction 函数来为特定信号绑定处理函数。其函数原型为:
- int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
其中,signum 参数指定要设置的信息;act 参数是一个 sigaction 指针,该结构体描述如何处理信号;可利用 oldact 参数存储原本的处理设置信息。
sigaction 结构体的定义如下:
- struct sigaction
- {
- void (*sa_handler)(int);
- void (*sa_sigaction)(int, siginfo_t*, void*);
- sigset_t sa_mask;
- int sa_flags;
- void (*sa_restorer)(void);
- };
其中,sa_handler 和 sa_sigaction 都是信号处理函数,从定义可以看到 sa_sigaction 接收的函数参数更多,可以实现更复杂的处理逻辑。
sa_flags 参数用于控制信号处理的各种选项。比如 SA_SIGINFO,指示信号处理函数使用 sa_sigaction 字段,而不使用 sa_handler。
sa_mask 参数用于指定一个信号集,表示在处理当前信号的过程中需要阻塞的其他信号。
当前的信号处理可能会被新来的信号打断。或者当前信号可能会和其他信号同时处理。为了确保信号处理的同步,可以使用 sa_mask 来阻塞其他信号。
紧接着程序会调用 fcntl 函数进行各种操作,我们逐一来看。
F_SETOWN:设置异步 I/O 通知的所有者。可以看到这边将 fd 和进程进行了关联。
F_SETOWN 应该是要显示指定一下,因为信号的接收者可能是一个单独的进程,也可能是一个进程组。
F_SETSIG:用于设置文件描述符异步 I/O 通知时要发送的信号类型。
理论上来说,驱动代码需要显式支持和尊重 F_SETSIG 设置。但是我们目前的驱动程序没有对此进行处理,可以使用 ioctl 接口进行设置。
F_GETFL 和 F_SETFL:用于获取和设置文件状态标志。
会根据 F_SETFL 的值调用 .fasync 接口。
不要忘记添加 _GNU_SOURCE 宏启用 GNU 扩展。以上命令参数不全是 POSIX 标准参数。
最后,我们来进行测试。我们首先将测试程序置于后台运行,此时程序没有接收到 SIGIO 信号,一直在循环睡眠中。再使用 echo 命令往设备写数据,会触发驱动发送 SIGIO 信号。从而触发测试程序调用信号处理函数,读取设备内容。
- tim@tim:~$ sudo su
- tim@tim:~$ ./test &
- root@tim:~$ echo "Hello World" > /dev/mydemo0
- FIFO is not empty
- Hello World
- FIFO is not full
实验:解决驱动的宕机难题
注意代码清单 12 中第 212 行代码(device->fasync = NULL),它对异步通知链表指针赋零。上一节我们已经介绍过,这个指针是充当头指针作用的,如果没对它赋零初始化,会引起异常地址访问的问题。
原书中特意暴露了这个问题,没有对头指针进行初始化。一运行测试程序直接系统卡死,错误日志塞满了原本空间就不多的硬盘😮。
我一开始还没注意到是程序的问题,还以为是虚拟机的问题。
这是第一次感受到驱动开发需要特别严谨,一出错误都是“致命”的。
书中介绍了在 qemu 环境下的错误排查。我们故意引入错误,然后在 qemu 环境下运行测试程序,可以看到控制台打印了错误现场:
- [ 618.588872] Unable to handle kernel paging request at virtual address 00000000886c3734
- [ 618.609669] Mem abort info:
- [ 618.610209] ESR = 0x96000004
- [ 618.610851] Exception class = DABT (current EL), IL = 32 bits
- [ 618.611503] SET = 0, FnV = 0
- [ 618.611921] EA = 0, S1PTW = 0
- [ 618.617500] Data abort info:
- [ 618.617670] ISV = 0, ISS = 0x00000004
- [ 618.618433] CM = 0, WnR = 0
- [ 618.619630] user pgtable: 4k pages, 48-bit VAs, pgdp = 000000003cdaeb78
- [ 618.620233] [00000000886c3734] pgd=0000000000000000
- [ 618.621310] Internal error: Oops: 96000004 [#1] SMP
- [ 618.622046] Modules linked in: mydemo(OE) binfmt_misc(E)
- [ 618.623390] CPU: 2 PID: 6293 Comm: test Kdump: loaded Tainted: G OE 5.0.0+ #3
- [ 618.624249] Hardware name: linux,dummy-virt (DT)
- [ 618.627801] pstate: 20400005 (nzCv daif +PAN -UAO)
- [ 618.639836] pc : fasync_insert_entry+0x508/0x908
- [ 618.640489] lr : (null)
- [ 618.640721] sp : ffff80002314f540
- [ 618.641071] x29: ffff80002314f540 x28: ffff800023113800
- [ 618.643366] x27: 0000000000000000 x26: 0000000000000000
- [ 618.643856] x25: 0000000056000000 x24: 0000000000000015
- [ 618.644246] x23: 0000000080001000 x22: 0000ffffa1c7ed04
- [ 618.644910] x21: 0000000000000000 x20: ffff8000298a9c00
- [ 618.647638] x19: 0000000000000000 x18: 0000000000000000
- [ 618.647911] x17: 0000000000000000 x16: 0000000000000000
- [ 618.648149] x15: 0000000000000000 x14: 0000000000000000
- [ 618.648334] x13: 0000000000000000 x12: 0000000000000000
- [ 618.648519] x11: 0000000000000000 x10: 0000000000000000
- [ 618.648735] x9 : ffff000010825900 x8 : ffff80002fdc6d40
- [ 618.648972] x7 : ffff80002fdc6d40 x6 : 0000000000000032
- [ 618.649257] x5 : ffff8000288d2300 x4 : ffff0000121e8530
- [ 618.649748] x3 : ffff0000121e8530 x2 : 0000000000000001
- [ 618.650029] x1 : 0000000000000000 x0 : 00000000886c371c
- [ 618.650330] Process test (pid: 6293, stack limit = 0x0000000012179b77)
- [ 618.650804] Call trace:
- [ 618.651138] fasync_insert_entry+0x508/0x908
- [ 618.653995] fasync_add_entry+0x4c/0x70
- [ 618.654380] fasync_helper+0x4c/0x54
- [ 618.655582] demodrv_fasync+0x64/0x6c [mydemo]
- [ 618.656047] setfl+0x1d8/0x50c
- [ 618.656333] do_fcntl+0x2e8/0xb48
- [ 618.656650] __se_sys_fcntl+0x110/0x16c
- [ 618.656987] __arm64_sys_fcntl+0x40/0x48
- [ 618.657348] __invoke_syscall+0x24/0x2c
- [ 618.657491] invoke_syscall+0xa4/0xd8
- [ 618.657625] el0_svc_common+0x100/0x1e4
- [ 618.657764] el0_svc_handler+0x414/0x440
- [ 618.658037] el0_svc+0x8/0xc
- [ 618.665971] Code: f9400fe0 f9004fe0 140000ac f94053e0 (f9400c00)
错误日志里提供了很丰富的信息:
'Unable to handle kernel paging request at virtual address 00000000886c3734':访问此地址发生错误。
'Mem abort info' 和 'Data abort info':提供了内存异常的详细信息。
'Modules linked in: mydemo(OE) binfmt_misc(E)':奔溃发生在我们加载的 mydemo 模块。
'CPU: 2 PID: 6293 Comm: test':显示奔溃发生在 CPU2 上,进程 ID 是 6293,进程名是 test。
'pc : fasync_insert_entry+0x508/0x908':发生奔溃时的 pc 指针值。同时还有各个奔溃现场的寄存器值。
'Call trace':奔溃时的函数调用堆栈。比如 fasync_insert_entry+0x508/0x908,其中,fasync_insert_entry 是函数名,0x508 是此函数地址偏移值,0x908 表示此函数的大小(单位:字节)。
虽然我们已经知道错误是怎么引起的,但是还是假装不知道,结合堆栈排查一下。
- tim@tim:~$ gdb-multiarch vmlinux
- (gdb) list *(fasync_insert_entry+0x508)
我们使用 gdb 调试 vmlinux 文件,并使用 list 命令定位源码行数。


比如 fasync_insert_entry+0x508,定位的结果如图 1 所示:fa 变量解引用发生异常了,即 fa 地址内容有问题。我们再看看这个地址内容是从哪里来的,打开 fs/fcntl.c 源码,可以看到 fa 是从函数参数 fapp 中来的。
结合后续堆栈信息,同样的操作,我们看 fapp 参数是从哪里来的。最后追踪下来会发现 fapp 参数就是 fasync_helper 传进去的链表地址。我们需要把它初始化成空,非空就表示有结点,就引发了问题。
书中没显式初始化。而是直接使用了 kzalloc 来分配内存,它会将分配的内存清零。
书中没讲解实机环境下的 crash 信息查看。此问题先做标记。