《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记
文章目录
- 阻塞 IO
- 非阻塞 IO
- IO 实验(使用等待队列)
- 等待队列
- 阻塞 IO 实验
- 非阻塞 IO 实验
阻塞 IO
I/O输入/输出(Input/Output),分为IO设备和IO接口两个部分。 在POSIX兼容的系统上,例如Linux系统 ,I/O操作可以有多种方式,比如DIO(Direct I/O),AIO(Asynchronous I/O,异步I/O),Memory-Mapped I/O(内存映射I/O)等,不同的I/O方式有不同的实现方式和性能,在不同的应用中可以按情况选择不同的I/O方式。
——百度百科
阻塞 IO:当用户线程发出 IO 请求后,内核会去查看数据是否就绪,如果没就绪,线程便进入阻塞状态,让出 CPU。当数据就绪后,内核才会将数据拷贝到用户空间,最后返回用户线程,开始处理数据。
非阻塞 IO
非阻塞 IO:当用户进程发出 IO 请求后,不进行等待,立刻返回一个结果,如果返回错误,表示数据没有准备好,这时用户进程可以选择再次发出 IO 请求。如果内核数据已经准备好,那么数据会马上被拷贝到用户线程,然后返回用户线程处理数据。
IO 实验(使用等待队列)
等待队列
等待队列,是指linux系统中进程所组成的队列,就是需要其他事件的发生才会自己本身被唤醒的进程,也就是说这些进程本身是在等待其他某些进程为他们提供进程发生的条件。他们是属于消费者的,但是他们要消耗的东西还没有产生,这些就是处于等待状态的进程,组成了等待队列。等待队列很容易使用, 尽管它的设计很是微妙, 但不需要知道它的内部细节。
——百度百科
等待队列的原型是双循环链表,它有一个队列头,等待队列头结构体定义:
struct _wait_queue_head{
spinlock_t lock; //自旋锁
struct list_head task_list //链表头
};
typefef struct _wait_queue_head wait_queue_head_t;
等待队列除队列头外,其他便是等待队列项,即等待队列的队列元素,它的结构体定义如下:
struct _wait_queue{
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct _wait_queue wait_queue_t;
等待队列 API 函数:
函数 | 描述 |
---|---|
DECLARE_WAIT_QUEUE_HEAD(name) | 定义并初始化一个等待队列头 |
init_waitqueue_head(q) | 初始化等待队列头,q 为队列头指针 |
DECLARE_WAITQUEUE(name, tsk) | 创建并初始化一个等待队列项,tsk 一般设置为 current(一个全局变量)相当于当前进程 |
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) | 添加等待队列项,q 为等待队列头指针,wait 为等待队列项指针 |
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) | 删除添加等待队列项,q 为等待队列头指针,wait 为等待队列项指针 |
wait_event(wq_head, condition) | 不可中断的阻塞等待,让调用进程进入不可中断的睡眠状态,在等待队列 wq_head 里睡眠直到 condition 变为真 |
wait_event_interruptible(wq_head, condition) | 可中断的阻塞等待,让调用进程进入可中断的睡眠状态,在等待队列 wq_head 里睡眠直到 condition 变为真 |
wake_up(wait_queue_head_t *q) | 唤醒等待队列 q 所有休眠进程 |
wake_up_interruptible(wait_queue_head_t *q) | 唤醒等待队列 q 可中断的休眠进程 |
等待队列使用方法:
- 初始化等待队列头,并将条件 condition 设置为假(0)
- 在需要阻塞的地方调用 wait_event(),让进程进入休眠状态
- 想要唤醒线程时,将条件 condition 置为 1,然后调用 wake_up() 唤醒等待队列中的休眠进程
阻塞 IO 实验
驱动核心代码
在驱动的 read() 中,先运行 wait_event_interruptible(),让调用 read() 的线程进入睡眠状态,直到 condition 的值变为 1,这一步骤便是阻塞 IO 的核心(数据还未就绪,线程让出 CPU);当驱动的 write() 被执行时,数据就绪,condition 置为 1,然后调用 wake_up_interruptible() 唤醒第一个线程(不调用唤醒函数,第一个线程并不会自己唤醒)。
// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);
// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = 0;
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, tmp_dev->condition);
// 向应用空间拷贝数据
ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf));
if(ret != 0)
{
printk("copy_to_user error.\r\n");
return -1;
}
printk("chrdev_read.\n");
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
{
printk("copy_from_user error.\r\n");
return -1;
}
tmp_dev->condition = 1; // 将条件置 1
wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
return 0;
}
完整驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/wait.h>
// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);
// 定义一个私有数据结构体
struct my_device
{
dev_t dev_num; // 设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev st_cdev;
struct class *st_class;
struct device *st_device;
char kbuf[32];
int condition; // 条件标志
};
// 定义一个全局私有数据结构体
struct my_device dev1;
// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
file->private_data = &dev1; // 设置私有数据
printk("chrdev_open.\n");
return 0;
}
// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
printk("chrdev_release.\n");
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
{
printk("copy_from_user error.\r\n");
return -1;
}
tmp_dev->condition = 1; // 将条件置 1
wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
return 0;
}
// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = 0;
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, tmp_dev->condition);
// 向应用空间拷贝数据
ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf));
if(ret != 0)
{
printk("copy_to_user error.\r\n");
return -1;
}
printk("chrdev_read.\n");
return 0;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = chrdev_open, //将 open 成员指向 chrdev_open()函数
.read = chrdev_read, //将 read 成员指向 chrdev_read()函数
.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
int ret;
// 自动获取设备号(只申请一个,次设备号从 0 开始)
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
if(ret < 0)
{
goto err_alloc;
}
printk("alloc chrdev region successfully.\n");
dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备
ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
if(ret < 0)
{
goto err_cdev_add;
}
printk("cdev add successfully.\n");
dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
if(IS_ERR(dev1.st_class))
{
ret = PTR_ERR(dev1.st_class); // 返回错误码
goto err_class_create;
}
dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
if(IS_ERR(dev1.st_device))
{
ret = PTR_ERR(dev1.st_device); // 返回错误码
goto err_device_create;
}
return 0;
err_device_create:
class_destroy(dev1.st_class); // 删除类
err_class_create:
cdev_del(&dev1.st_cdev); // 删除 cdev
err_cdev_add:
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
err_alloc:
return ret; // 返回错误号
}
// 驱动出口函数
static void __exit chrdev_exit(void)
{
device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
class_destroy(dev1.st_class); //删除设备类
cdev_del(&dev1.st_cdev); // 删除字符设备
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
printk("chrdev_exit.\n");
}
module_init(chrdev_init); //注册入口函数
module_exit(chrdev_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
“读”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEV_FILE "/dev/chrdev_device"
int main(int argc, char** argv)
{
int fd, tmp;
int ret = 0;
char buf[32] = {0};
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 读数据
printf("app will read data.\n");
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
if(ret == 0)
printf("app read data successfully\ndata: %s\n", buf);
else
printf("app read data failed.\n");
// 关闭设备文件
close(fd);
return 0;
}
“写”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEV_FILE "/dev/chrdev_device"
int main(int argc, char** argv)
{
int fd, tmp;
int ret = 0;
char buf[32] = "Blocking IO";
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 读数据
printf("app will write data.\n");
ret = write(fd, buf, sizeof(buf)); // 向设备文件写数据
if(ret == 0)
printf("app write data successfully\n");
else
printf("app write data failed.\n");
// 关闭设备文件
close(fd);
return 0;
}
Makefile 文件
#目标文件,与驱动源文件同名,编译成模块
obj-m := chrdev_test.o
#架构平台选择
export ARCH=arm64
#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-
#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/
#KDIR := /lib/modules/$(shell uname -r)/build
#编译模块
all:
make -C $(KDIR) M=$(shell pwd) modules
$(CROSS_COMPILE)gcc read.c -o read
$(CROSS_COMPILE)gcc write.c -o write
#清除编译文件
clean:
make -C $(KDIR) M=$(shell pwd) clean
rm read write
实验结果
先运行“读”程序,“读”程序进入阻塞,需要等待“写”程序向设备文件写数据,“写”程序运行后,“读”程序被唤醒,继续运行,
非阻塞 IO 实验
在应用程序打开文件调用 open() 函数时,如 fd = open(DEV_FILE, O_RDWR)
,默认是阻塞式的,如果要采用非阻塞式,需要修改 open() 的参数,例如:fd = open(DEV_FILE, O_RDWR|O_NONBLOCK)
驱动核心代码
在驱动的 read() 中,先判断 file->f_flags 是否带有 O_NONBLOCK,即是否为非阻塞 IO,如果是,则继续判断数据就绪条件 condition 的值,数据没有就绪就直接退出 read(),并返回 -EAGAIN,此时“读”程序所在进程便不会被阻塞,但应用层需要不停循环判断 read 的返回值。(此实验的 wait_event_interruptible() 和 wake_up_interruptible() 没有实质作用,但如果应用层打开设备文件时没有指定 O_NONBLOCK 参数,则本实验依然为阻塞 IO 实验 )
// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = 0;
// 如果 open() 的 flags 参数带 O_NONBLOCK
if(file->f_flags& O_NONBLOCK)
{
// 如果数据没就绪,直接退出 read()
if(tmp_dev->condition == 0)
return -EAGAIN;
}
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, tmp_dev->condition);
// 向应用空间拷贝数据
ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf));
if(ret != 0)
{
printk("copy_to_user error.\r\n");
return -1;
}
printk("chrdev_read.\n");
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
{
printk("copy_from_user error.\r\n");
return -1;
}
tmp_dev->condition = 1; // 将条件置 1
wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
return 0;
}
完整驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/wait.h>
// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);
// 定义一个私有数据结构体
struct my_device
{
dev_t dev_num; // 设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev st_cdev;
struct class *st_class;
struct device *st_device;
char kbuf[32];
int condition; // 条件标志
};
// 定义一个全局私有数据结构体
struct my_device dev1;
// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
file->private_data = &dev1; // 设置私有数据
printk("chrdev_open.\n");
return 0;
}
// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
printk("chrdev_release.\n");
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
{
printk("copy_from_user error.\r\n");
return -1;
}
tmp_dev->condition = 1; // 将条件置 1
wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
return 0;
}
// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = 0;
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, tmp_dev->condition);
// 向应用空间拷贝数据
ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf));
if(ret != 0)
{
printk("copy_to_user error.\r\n");
return -1;
}
printk("chrdev_read.\n");
return 0;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = chrdev_open, //将 open 成员指向 chrdev_open()函数
.read = chrdev_read, //将 read 成员指向 chrdev_read()函数
.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
int ret;
// 自动获取设备号(只申请一个,次设备号从 0 开始)
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
if(ret < 0)
{
goto err_alloc;
}
printk("alloc chrdev region successfully.\n");
dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备
ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
if(ret < 0)
{
goto err_cdev_add;
}
printk("cdev add successfully.\n");
dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
if(IS_ERR(dev1.st_class))
{
ret = PTR_ERR(dev1.st_class); // 返回错误码
goto err_class_create;
}
dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
if(IS_ERR(dev1.st_device))
{
ret = PTR_ERR(dev1.st_device); // 返回错误码
goto err_device_create;
}
return 0;
err_device_create:
class_destroy(dev1.st_class); // 删除类
err_class_create:
cdev_del(&dev1.st_cdev); // 删除 cdev
err_cdev_add:
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
err_alloc:
return ret; // 返回错误号
}
// 驱动出口函数
static void __exit chrdev_exit(void)
{
device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
class_destroy(dev1.st_class); //删除设备类
cdev_del(&dev1.st_cdev); // 删除字符设备
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
printk("chrdev_exit.\n");
}
module_init(chrdev_init); //注册入口函数
module_exit(chrdev_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
“读”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEV_FILE "/dev/chrdev_device"
int main(int argc, char** argv)
{
int fd, tmp;
int ret = 0;
char buf[32] = {0};
// 打开设备文件
fd = open(DEV_FILE, O_RDWR|O_NONBLOCK);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 读数据
printf("app will read data.\n");
while(1)
{
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
sleep(3); // 内核 printk() 和 printf() 同时打印,终端有乱码(可能还是串口调试工具的问题)
if(ret == 0)
printf("app read data successfully\ndata: %s\n", buf);
else
printf("app read data failed.\n");
//sleep(3);
}
// 关闭设备文件
close(fd);
return 0;
}
“写”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEV_FILE "/dev/chrdev_device"
int main(int argc, char** argv)
{
int fd, tmp;
int ret = 0;
char buf[32] = "Non_Blocking IO";
// 打开设备文件
fd = open(DEV_FILE, O_RDWR|O_NONBLOCK);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 读数据
printf("app will write data.\n");
ret = write(fd, buf, sizeof(buf)); // 向设备文件写数据
if(ret == 0)
printf("app write data successfully\n");
else
printf("app write data failed.\n");
// 关闭设备文件
close(fd);
return 0;
}
Makefile 文件
#目标文件,与驱动源文件同名,编译成模块
obj-m := chrdev_test.o
#架构平台选择
export ARCH=arm64
#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-
#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/
#KDIR := /lib/modules/$(shell uname -r)/build
#编译模块
all:
make -C $(KDIR) M=$(shell pwd) modules
$(CROSS_COMPILE)gcc read.c -o read
$(CROSS_COMPILE)gcc write.c -o write
#清除编译文件
clean:
make -C $(KDIR) M=$(shell pwd) clean
rm read write
实验结果
运行“读”程序后,由于数据没有就绪,read 直接返回 -EAGAIN(非 0 的错误号),而不像阻塞 IO 一样挂起等待,“写”程序运行后,内核数据就绪,“读”程序中的 read() 返回 0(读取成功)。