嵌入式Linux驱动开发 05:阻塞与非阻塞

文章目录

  • 目的
  • 基础说明
  • 开发准备
    • 驱动程序
    • 应用程序
  • 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 [email protected]:/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 [email protected]:/root/

O_NONBLOCK

应用程序

这个应用程序中使用 openO_NONBLOCK 来处理是否阻塞。

#include 
#include 
#include 
#include 
#include 
#include 

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 \n"); // 阻塞方式
		printf("       test  -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 
#include 
#include 
#include 
#include 
#include 
#include 

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"); // 模块许可

程序演示

嵌入式Linux驱动开发 05:阻塞与非阻塞_第1张图片
上面演示中可以看到应用程序在阻塞模式下资源可用会立即返回,资源不可用会阻塞直至获取到资源再返回。而非阻塞模式下不管资源可不可用都会返回。

poll

应用程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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  \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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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"); // 模块许可

程序演示

嵌入式Linux驱动开发 05:阻塞与非阻塞_第2张图片
上面驱动刷新资源间隔是5秒,所以应用程序设置5秒的超时时间一定可以获取到资源。

异步通知

应用程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 \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 
#include 
#include 
#include 
#include 
#include 

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"); // 模块许可

程序演示

嵌入式Linux驱动开发 05:阻塞与非阻塞_第3张图片
上面驱动程序中每两次定时器触发(10s)会发送一次信号,测试的应用程序在接到信号后会执行对应回调函数。

总结

阻塞和非阻塞是非常基础的内容,形式上通常也就着一些,本身使用上来说并不复杂,更多的是需要和实际的业务功能结合起来处理。

你可能感兴趣的:(嵌入式Linux与设备相关,驱动开发,linux,运维,异步,信号)