《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记
文章目录
- 应用层信号机制
- 应用层开启异步通知
- 驱动层异步通知接口
- 实验代码
信号驱动 IO 不需要像 poll 一样查询设备的状态,一旦设备有目标事件发生,就会触发 SIGIO 信号,然后处理信号函数,完成相应数据处理。
上面这一操作又叫异步通知,该机制与中断相似,事件发生时不立刻处理,而是发送一个信号,然后系统自动运行信号处理函数。
应用层信号机制
在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。
——百度百科
信号的相关系统调用包括 signal()、kill()、pause()、alarm() 和 setitimer(),这里只讲 signal() 函数。
signal() 函数原型为:
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
signum 为需要处理的信号(Linux 的所有信号可以通过 kill -l 查看)
handler 处理信号的回调函数
信号处理函数原型为:
typedef void (*sighandler_t)(int);
应用层开启异步通知
设备必须被开启异步通知后,才可以使用信号驱动 IO,具体操作为:
fcntl(fd, SETOWN, getpid()); // 设置接收 SIGIO 和 SIGURG 信号的进程 ID
int tmp = fcntl(fd, F_GETFD); // 获取文件描述符标志
fcntl(fd, F_SETFL, tmp | FASYNC); // 设置文件描述符标志
fcntl 函数可以改变已经打开的文件描述符的属性,其原型如下:
#include
int fcntl(int fd,int cmd, ...)
fd 为要操作的文件描述符,cmd 为操作文件描述符的命令,… 表示该函数的参数是可变长参数。
成功返回的值会根据 cmd 不同而不同,错误时返回 -1,同时设置 errno。
cmd 可取值包括:
命令 | 描述 |
---|---|
F_DUPFD | 复制文件描述符 |
F_GETFD | 获取文件描述符标志 |
F_SETFD | 设置文件描述符标志 |
F_GETFL | 获取文件状态标志 |
F_SETFL | 设置文件状态标志 |
F_GETLK | 获取文件锁 |
F_SETLK | 设置文件锁 |
F_SETLKW | 与 F_SETLK 类似,但会等待返回 |
F_GETOWN | 获取当前接收 SIGIO 和 SIGURG 信号的进程 ID 和 进程组 ID |
F_SETOWN | 设置当前接收 SIGIO 和 SIGURG 信号的进程 ID 和进程组 ID |
驱动层异步通知接口
内核中,异步通知需要用到 fasync 方法,fasync 和 poll、read 等函数类似,都是文件操作集合(file_operations 结构体)的成员函数,fasync 原型如下:
int (*fasync) (int fd,struct file *filp,int on);
在驱动中,fasync 函数调用 fasync_helper() 来操作 fasync_struct 结构体,fasync_helper() 函数原型如下:
int fasync_helper(int fd,struct file *filp,int on,struct fasync_struct **fapp)
该函数的前三个参数都使用 fasnc() 的形参,只有 struct fasync_struct 类型变量是新的参数,该结构体定义如下:
struct fasync_struct {
int magic;
int fa_fd;
struct fasync_struct *fa_next; /* singly linked list */
struct file *fa_file;
};
该结构体就是异步队列的基本元素,当收到信号,内核就会在这个异步队列里寻找相应文件描述符(fa_fd),然后根据 fa_file->owner 找到对应线程 PID,最后调用 sig_handler 完成异步 IO 操作。
上面提到了信号的接收,但是这个信号从哪里发出呢?这时就要用到驱动层的 kill_fasync() 函数了,该函数发送信号,通知应用程序,原型如下:
void kill_fasync(struct fasync_struct **fp,int sig,int band);
第一个参数是上面提到的 fasync_struct 结构体变量,sig 代表要发送的信号,band 一般填 POLL_IN 或 POLL_OUT,表示驱动端有数据读或写。
实验代码
驱动核心代码
在上一份驱动代码的基础上添加 chrdev_fasync() 函数,当驱动运行该函数时,调用 fasync_helper() 函数,将异步通知的信号添加到异步处理队列中。
// fasync
static int chrdev_fasync(int fd, struct file *file, int on)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
// 将异步通知的信号添加到异步处理列表
return fasync_helper(fd, file, on, &tmp_dev->fasync);
}
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()函数
.poll = chrdev_poll, //将 poll 字段指向 chrdev_poll()函数
.fasync = chrdev_fasync, //将 fasync 字段指向 chrdev_fasync()函数
};
完整驱动代码
#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>
#include <linux/poll.h>
#include <linux/signal.h>
#include <linux/fcntl.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 fasync_struct *fasync;
};
// 定义一个全局私有数据结构体
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)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
// 卸载异步通知
fasync_helper(0, file, 0, &tmp_dev->fasync);
printk("chrdev_release.\n");
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;
// 如果 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);
tmp_dev->condition = 0; // 条件标志复位
// 向应用空间拷贝数据
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); // 唤醒等待队列中的休眠进程
kill_fasync(&tmp_dev->fasync, SIGIO, POLL_IN); // 发送 SIGIO 信号
return 0;
}
// poll
static unsigned int chrdev_poll(struct file *file, struct poll_table_struct *wait)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
poll_wait(file, &my_wait_queue, wait); // 阻塞
if(tmp_dev->condition == 1)
{
return POLLIN; // 返回事件类型
}
return 0;
}
// fasync
static int chrdev_fasync(int fd, struct file *file, int on)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
// 将异步通知的信号添加到异步处理列表
return fasync_helper(fd, file, on, &tmp_dev->fasync);
}
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()函数
.poll = chrdev_poll, //将 poll 字段指向 chrdev_poll()函数
.fasync = chrdev_fasync, //将 fasync 字段指向 chrdev_fasync()函数
};
// 驱动入口函数
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>
#include <signal.h>
#define DEV_FILE "/dev/chrdev_device"
int fd;
char buf[32] = {0};
// SIGIO 信号处理函数
static void sig_handler(int signum)
{
int ret = 0;
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");
}
int main(int argc, char** argv)
{
int tmp;
int ret = 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);
// 注册 SIGIO 信号的信号处理函数
signal(SIGIO, sig_handler);
// 操作文件描述符,设置接收 SIGIO 的进程 ID
fcntl(fd, F_SETOWN, getpid());
// 获取文件描述符标志
tmp = fcntl(fd, F_GETFD);
// 设置文件描述符状态标志,增加 FASYNC 标志
fcntl(fd, F_SETFL, tmp | FASYNC);
// 死循环
while(1);
// 关闭设备文件
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] = "fasync test";
// 打开设备文件
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 文件
由于我打算在 X86 平台测试,所我屏蔽了交叉编译和平台的平台的环境变量,内核目录改为本机内核地址,
#目标文件,与驱动源文件同名,编译成模块
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 运行后开启信号驱动 IO,当运行 write “写”程序时,驱动中数据准备就绪,驱动向应用层发送 SIGIO 信号,读程序中信号处理函数被执行,将读到的数据打印到终端。