文章目录
- 目的
- 基础说明
- 开发准备
- 驱动程序
- 应用程序
- O_NONBLOCK
- 应用程序
- 驱动程序
- 程序演示
- poll
- 应用程序
- 驱动程序
- 程序演示
- 异步通知
- 应用程序
- 驱动程序
- 程序演示
- 总结
目的
不管在应用开发还是驱动开发中阻塞和非阻塞都是绕不开的话题。这篇文章将介绍相关的基础内容。
这篇文章中内容均在下面的开发板上进行测试:
《新唐NUC980使用记录:自制开发板(基于NUC980DK61YC)》
这篇文章是在下面文章基础上进行的:
《新唐NUC980使用记录(5.10.y内核):在用户应用中使用GPIO》
基础说明
当应用程序和驱动进行读写交互的时候会有一个问题,你要访问或操作的资源当前是否存在或者是否可用。根据状况和操作逻辑的不同就衍生出了阻塞与非阻塞的概念。
阻塞形式访问的话会直到资源可用才进行下一步操作。非阻塞式操作如果当前资源不可用,要不直接返回错误,要不在后台等到资源可用时候进行通知。
默认情况下应用程序通过 read / write
操作都是阻塞式的,可以通过 open
操作时候传入 O_NONBLOCK
参数设置为非阻塞形式,这样如果资源不可用就会直接返回错误。
除了上面这个操作,还有更多形式的处理这方面问题的机制。比如 select / poll / epoll
,这类方式可以设定一个超时时间,在此时间内会等待资源可用,超时了则会返回。还有比如 异步通知 方式,这个方式有点像是事件或者中断。
开发准备
驱动程序
本文中演示中驱动代码涉及目录与文件结构组织如下:
具体涉及的文件与内容见前面文章 《嵌入式Linux驱动开发 04:基于设备树的驱动开发》 。
驱动程序编译和拷贝到开发板测试操作如下:
# 进入内核目录
cd ~/nuc980-sdk/NUC980-linux-5.10.y
# 编辑驱动文件
gedit ./drivers/user/char_dev/char_drv.c
# 驱动文件内容见下面章节
# 设置编译工具链
export ARCH=arm; export CROSS_COMPILE=arm-buildroot-linux-gnueabi-
export PATH=$PATH:/home/nx/nuc980-sdk/buildroot-2023.02/output/host/bin
# 编译生成内核镜像
make uImage
# 可以根据电脑配置使用make -jx等加快编译速度
# 编译完成后拷贝到电脑上再拷贝到SD卡中
# sudo cp arch/arm/boot/uImage /media/sf_common/
# 我这里开发环境和开发板在同一局域网中,所以可以直接通过网络将uImage文件拷贝到开发板上
# 在开发板中挂载boot分区
# mount /dev/mmcblk0p1 /mnt/
# 在ubuntu中使用scp命令拷贝dtb文件到开发板上
# scp arch/arm/boot/uImage root@192.168.31.142:/mnt/
# 拷贝完成后重启开发板即可测试
# reboot
应用程序
本文中驱动程序需要编写对应的应用程序来测试其功能,应用程序基础准备如下:
# 创建目应用程序录并进入
mkdir -p ~/nuc980-sdk/apps/test
cd ~/nuc980-sdk/apps/test
# 创建应用程序文件
gedit char_dev_test.c
# 应用程序具体内容因不同的驱动程序而异
# 设置编译工具链
export ARCH=arm; export CROSS_COMPILE=arm-buildroot-linux-gnueabi-
export PATH=$PATH:/home/nx/nuc980-sdk/buildroot-2023.02/output/host/bin
# 编译生成目标应用程序
arm-linux-gcc -o char_dev_test char_dev_test.c
# 编译完成后拷贝到电脑上再拷贝到SD卡中
# sudo cp char_dev_test /media/sf_common/
# 我这里开发环境和开发板在同一局域网中,所以可以直接通过网络将文件拷贝到开发板上
# 在ubuntu中使用scp命令拷贝文件到开发板上
# scp char_dev_test root@192.168.31.142:/root/
O_NONBLOCK
应用程序
这个应用程序中使用 open
中 O_NONBLOCK
来处理是否阻塞。
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
/* 该程序用于读写测试本文要编写的驱动程序对应生成的设备 */
int main(int argc, char **argv)
{
int fd;
char buf[4096];
int len;
/* 打开文件 */
if (argc == 2)
{
fd = open(argv[1], O_RDWR); // 阻塞方式
}
else if ((argc == 3) && (strcmp(argv[2], "-nb") == 0))
{
fd = open(argv[1], O_RDWR | O_NONBLOCK); // 非阻塞方式
}
else
{
printf("Usage: test <devpath>\n"); // 阻塞方式
printf(" test <devpath> -nb\n"); // 非阻塞方式
return -1;
}
if (fd < 0)
{
printf("NX applog: can not open file %s\n", argv[1]);
return -1;
}
/* 读数据 */
len = read(fd, buf, 4096);
if(len)
{
buf[len] = '\0';
printf("NX applog: len %d, data %s\n", len, buf);
}
else
{
printf("NX applog: len %d, EAGAIN\n", len);
}
close(fd);
return 0;
}
驱动程序
下面驱动程序中使用定时器来模拟开关资源可用与否。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/delay.h>
static int major = 0;
static const char *char_drv_name = "char_drv";
static struct class *char_drv_class;
static struct device *char_drv_device;
struct timer_list timer;
static bool data_available = false; // 标识当前是否有可用资源
static char *data = "Hello Naisu!";
static void timer_callback(struct timer_list *) // 定时器回调函数
{
// printk("NX modlog: file %s, func %s, line %d, jiffies %lu.\n", __FILE__, __FUNCTION__, __LINE__, jiffies);
data_available = (data_available == false) ? true : false; // 定时器中改变资源可用状态
mod_timer(&timer, jiffies + HZ * 5); // 再次启动定时器
}
static int char_drv_open(struct inode *node, struct file *file)
{
return 0;
}
static int char_drv_close(struct inode *node, struct file *file)
{
return 0;
}
static ssize_t char_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int ret;
if ((file->f_flags & O_NONBLOCK) && (!data_available)) // 如果为非阻塞方式且当前没有资源可用则返回EAGAIN
{
printk("NX modlog: O_NONBLOCK and data_available == false.\n");
return -EAGAIN;
}
while(true) {
if(data_available) {
ret = copy_to_user(buf, data, strlen(data)); // 从内核空间拷贝数据到用户空间
break;
}
printk("NX modlog: wait for data_available.\n");
ssleep(1);
}
return strlen(data);
}
static const struct file_operations char_drv_fops = {
.owner = THIS_MODULE,
.open = char_drv_open,
.release = char_drv_close,
.read = char_drv_read,
};
static int __init char_drv_init(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, char_drv_name, &char_drv_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
char_drv_class = class_create(THIS_MODULE, "char_drv_class");
if (IS_ERR(char_drv_class))
{
unregister_chrdev(major, char_drv_name);
return -1;
}
char_drv_device = device_create(char_drv_class, NULL, MKDEV(major, 0), NULL, char_drv_name); // 创建设备节点创建设备节点,成功后就会出现/dev/char_drv_name的设备文件
if (IS_ERR(char_drv_device))
{
device_destroy(char_drv_class, MKDEV(major, 0));
unregister_chrdev(major, char_drv_name);
return -1;
}
timer_setup(&timer, timer_callback, 0); // 设置定时器和回调函数
timer.expires = jiffies + HZ * 5; // 设置定时周期(5秒)
add_timer(&timer); // 启动定时器
return 0;
}
static void __exit char_drv_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(char_drv_class, MKDEV(major, 0)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
class_destroy(char_drv_class);
unregister_chrdev(major, char_drv_name); // 注销字符设备
}
module_init(char_drv_init); // 模块入口
module_exit(char_drv_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
程序演示
上面演示中可以看到应用程序在阻塞模式下资源可用会立即返回,资源不可用会阻塞直至获取到资源再返回。而非阻塞模式下不管资源可不可用都会返回。
poll
应用程序
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <stdlib.h>
/* 该程序用于读写测试本文要编写的驱动程序对应生成的设备 */
int main(int argc, char **argv)
{
int fd;
char buf[4096];
int len;
int ret;
int timeout;
struct pollfd fds;
nfds_t nfds = 1;
if (argc != 3)
{
printf("Usage: test <devpath> <timeout>\n");
return -1;
}
timeout = atoi(argv[2]);
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd < 0)
{
printf("NX applog: can not open file %s\n", argv[1]);
return -1;
}
/* 等待文件可读 */
fds.fd = fd;
fds.events = POLLIN;
ret = poll(&fds, nfds, timeout); // 等待事件触发, timeout 为 -1 时将不会超时
if ((ret > 0) && (fds.revents & POLLIN))
{
len = read(fds.fd, buf, 4096); // 从文件读取数据
if(len)
{
buf[len] = '\0';
printf("NX applog: len %d, data %s\n", len, buf);
}
}
else
{
printf("NX applog: poll timeout or error\n");
}
close(fd);
return 0;
}
驱动程序
下面驱动中用到的等待队列(WAIT_QUEUE)。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/wait.h>
static int major = 0;
static const char *char_drv_name = "char_drv";
static struct class *char_drv_class;
static struct device *char_drv_device;
struct timer_list timer;
static DECLARE_WAIT_QUEUE_HEAD(data_wait); // 定义并初始化等待队列头 data_wait
static bool data_available = false; // 标识当前是否有可用资源
static char *data = "Hello Naisu!";
static int char_drv_open(struct inode *node, struct file *file)
{
return 0;
}
static int char_drv_close(struct inode *node, struct file *file)
{
return 0;
}
static ssize_t char_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
wait_event_interruptible(data_wait, data_available); // 资源可用时会立即返回,否则会加入等待队列
copy_to_user(buf, data, strlen(data));
return strlen(data);
}
static __poll_t char_drv_poll(struct file *file, poll_table * wait)
{
// printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
poll_wait(file, &data_wait, wait); // 加入等待队列
return data_available ? POLLIN : 0;
}
static void timer_callback(struct timer_list *) // 定时器回调函数
{
// printk("NX modlog: file %s, func %s, line %d, jiffies %lu.\n", __FILE__, __FUNCTION__, __LINE__, jiffies);
data_available = (data_available == false) ? true : false; // 定时器中改变资源可用状态
mod_timer(&timer, jiffies + HZ * 5); // 再次启动定时器
if(data_available)
{
wake_up_interruptible(&data_wait); // 如果资源可用则唤醒
}
}
static const struct file_operations char_drv_fops = {
.owner = THIS_MODULE,
.open = char_drv_open,
.release = char_drv_close,
.read = char_drv_read,
.poll = char_drv_poll,
};
static int __init char_drv_init(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, char_drv_name, &char_drv_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
char_drv_class = class_create(THIS_MODULE, "char_drv_class");
if (IS_ERR(char_drv_class))
{
unregister_chrdev(major, char_drv_name);
return -1;
}
char_drv_device = device_create(char_drv_class, NULL, MKDEV(major, 0), NULL, char_drv_name); // 创建设备节点创建设备节点,成功后就会出现/dev/char_drv_name的设备文件
if (IS_ERR(char_drv_device))
{
device_destroy(char_drv_class, MKDEV(major, 0));
unregister_chrdev(major, char_drv_name);
return -1;
}
timer_setup(&timer, timer_callback, 0); // 设置定时器和回调函数
timer.expires = jiffies + HZ * 5; // 设置定时周期(5秒)
add_timer(&timer); // 启动定时器
return 0;
}
static void __exit char_drv_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(char_drv_class, MKDEV(major, 0)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
class_destroy(char_drv_class);
unregister_chrdev(major, char_drv_name); // 注销字符设备
}
module_init(char_drv_init); // 模块入口
module_exit(char_drv_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
程序演示
上面驱动刷新资源间隔是5秒,所以应用程序设置5秒的超时时间一定可以获取到资源。
异步通知
应用程序
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
static int fd;
static void sig_callback(int sig) // 收到信号时回调函数
{
char buf[4096];
int len;
len = read(fd, buf, 4096); // 从文件读取数据
if(len)
{
buf[len] = '\0';
printf("NX applog: len %d, data %s\n", len, buf);
}
}
int main(int argc, char **argv)
{
int flags;
if (argc != 2)
{
printf("Usage: test <devpath>\n");
return -1;
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd < 0)
{
printf("NX applog: can not open file %s\n", argv[1]);
return -1;
}
signal(SIGIO, sig_callback); // 注册信号和对应回调函数
fcntl(fd, F_SETOWN, getpid());
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC); // 启动异步信号通知
while (1)
{
sleep(2);
printf("NX applog: ......\n");
}
close(fd);
return 0;
}
驱动程序
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
static int major = 0;
static const char *char_drv_name = "char_drv";
static struct class *char_drv_class;
static struct device *char_drv_device;
struct timer_list timer;
static bool data_available = false; // 标识当前是否有可用资源
static char *data = "Hello Naisu!";
struct fasync_struct *data_fasync;
static int char_drv_open(struct inode *node, struct file *file)
{
return 0;
}
static int char_drv_close(struct inode *node, struct file *file)
{
return 0;
}
static ssize_t char_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
copy_to_user(buf, data, strlen(data));
return strlen(data);
}
static int char_drv_fasync(int fd, struct file *file, int on)
{
if (fasync_helper(fd, file, on, &data_fasync) >= 0) // 保存异步通知设置信息
return 0;
else
return -EIO;
}
static void timer_callback(struct timer_list *) // 定时器回调函数
{
// printk("NX modlog: file %s, func %s, line %d, jiffies %lu.\n", __FILE__, __FUNCTION__, __LINE__, jiffies);
data_available = (data_available == false) ? true : false; // 定时器中改变资源可用状态
mod_timer(&timer, jiffies + HZ * 5); // 再次启动定时器
if(data_available)
{
kill_fasync(&data_fasync, SIGIO, POLL_IN); // 如果资源可用则发送信号
}
}
static const struct file_operations char_drv_fops = {
.owner = THIS_MODULE,
.open = char_drv_open,
.release = char_drv_close,
.read = char_drv_read,
.fasync = char_drv_fasync,
};
static int __init char_drv_init(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, char_drv_name, &char_drv_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
char_drv_class = class_create(THIS_MODULE, "char_drv_class");
if (IS_ERR(char_drv_class))
{
unregister_chrdev(major, char_drv_name);
return -1;
}
char_drv_device = device_create(char_drv_class, NULL, MKDEV(major, 0), NULL, char_drv_name); // 创建设备节点创建设备节点,成功后就会出现/dev/char_drv_name的设备文件
if (IS_ERR(char_drv_device))
{
device_destroy(char_drv_class, MKDEV(major, 0));
unregister_chrdev(major, char_drv_name);
return -1;
}
timer_setup(&timer, timer_callback, 0); // 设置定时器和回调函数
timer.expires = jiffies + HZ * 5; // 设置定时周期(5秒)
add_timer(&timer); // 启动定时器
return 0;
}
static void __exit char_drv_exit(void)
{
printk("NX modlog: file %s, func %s, line %d.\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(char_drv_class, MKDEV(major, 0)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
class_destroy(char_drv_class);
unregister_chrdev(major, char_drv_name); // 注销字符设备
}
module_init(char_drv_init); // 模块入口
module_exit(char_drv_exit); // 模块出口
MODULE_LICENSE("GPL"); // 模块许可
程序演示
上面驱动程序中每两次定时器触发(10s)会发送一次信号,测试的应用程序在接到信号后会执行对应回调函数。
总结
阻塞和非阻塞是非常基础的内容,形式上通常也就着一些,本身使用上来说并不复杂,更多的是需要和实际的业务功能结合起来处理。