《【北京迅为】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 信号的进程 IDint 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_NONBLOCKif(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; // 将条件置 1wake_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); // 删除 cdeverr_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);elseprintf("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 的进程 IDfcntl(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");elseprintf("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) cleanrm read write
实验结果
先运行“读”程序,read 运行后开启信号驱动 IO,当运行 write “写”程序时,驱动中数据准备就绪,驱动向应用层发送 SIGIO 信号,读程序中信号处理函数被执行,将读到的数据打印到终端。