17-阻塞型IO

从内核中最简单的驱动程序入手,描述Linux驱动开发,主要文章目录如下(持续更新中):
01 - 第一个内核模块程序
02 - 注册字符设备驱动
03 - open & close 函数的应用
04 - read & write 函数的应用
05 - ioctl 的应用
06 - ioctl LED灯硬件分析
07 - ioctl 控制LED软件实现(寄存器操作)
08 - ioctl 控制LED软件实现(库函数操作)
09 - 注册字符设备的另一种方法(常用)
10 - 一个cdev实现对多个设备的支持
11 - 四个cdev控制四个LED设备
12 - 虚拟串口驱动
13 - I2C驱动
14 - SPI协议及驱动讲解
15 - SPI Linux驱动代码实现
16 - 非阻塞型I/O
17 - 阻塞型I/O
18 - I/O多路复用之 select
19 - I/O多路复用之 poll
20 - I/O多路复用之 epoll
21 - 异步通知

1. 阻塞型IO简介

 上一篇讲解了非阻塞型的IO,当以非阻塞的方式打开设备时如果资源不可用,则会立即返回 EAGAIN 并尝试重新获取资源,非阻塞性IO会定期尝试查看资源是否可以获取,这种方式效率较低。本文来讲述另外一种IO操作——阻塞性IO,阻塞型IO的执行过程如下所示:
在这里插入图片描述
 当进程以阻塞的方式打开设备时,如果进程发现资源不可用会主动将自己的状态设置为 TASK_UNINTERRUPTIBLE 或 TASK_INTERRUPTIBLE 然后将自己加入一个驱动所维护的等待队列中,最后调用 sehedule 主动放弃CPU,操作系统将其从运行队列上移除,并调度其他的进程执行

2. 阻塞型IO的定义及相关函数

2.1 结构体定义

 阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。阻塞性IO也有其缺点即进程在休眠的时间就不能做其他的事情。进入休眠状态后当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,要实现等待队列,内核中定义了如下的结构体来实现(include\linux\wait.h)

struct __wait_queue_head {
	spinlock_t lock;
	struct list_head task_list;
};
typedef 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;	// 等待队列节点

 其中 wait_queue_head_t 表示等待队列头即一个等待队列的头部,每个访问设备的进程都是一个队列项用 wait_queue_t 表示,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。

2.2 初始化函数

 定义初始化等待队列头

DECLARE_WAIT_QUEUE_HEAD(name)	// 静态定义一个等待队列头
init_waitqueue_head(q)			// 初始化一个等待队列头

 定义初始化等待队列节点(项)

@原   型: DECLARE_WAITQUEUE(name, tsk)
@功   能: 给当前正在运行的进程创建并初始化了一个等待队列项
@param1: 等待队列项的名字
@param2: 这个等待队列项属于哪个任务(进程),一般设置为current,在Linux内核中current相当于一个全局变量,表示当前进程 

2.3 添加/删除队列节点函数

 添加队列节点

@原   型: void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
@功   能: 将队列项添加至等待队列头的队列中 
@param1: 要加入的等待队列的等待队列头
@param2: 要加入的等待队列项
@return: 无返回值

 删除队列节点

@原   型: void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
@功   能: 从等待队列中移除等待队列项
@param1: 要操作的等待队列头
@param2: 要移除的等待队列项
@return: 无返回值

2.4 唤醒操作

@原   型: wake_up(wait_queue_head_t *q)
@功   能: 将这个等待队列头中的所有进程都唤醒,可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进程
@param1: 要唤醒的等待队列头
@return: 无返回值


@原   型: wake_up_interruptible(wait_queue_head_t *q)
@功   能: 将这个等待队列头中的所有进程都唤醒,只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程
@param1: 要唤醒的等待队列头
@return: 无返回值


@原   型: wake_up_locked(wait_queue_head_t *q)
@功   能: 将这个等待队列头中的所有进程都唤醒
@param1: 要唤醒的等待队列头
@return: 无返回值

2.5 等待事件

 除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程,相关函数定义如下

扫描二维码关注公众号,回复: 8613585 查看本文章
@原   型: wait_event(wq, condition)
@功   能: 条件 condition 不成立,将当前进程放入到等待队列并休眠,此函数会将进程设置为TASK_UNINTERRUPTIBLE 状态
@param1: 等待队列头
@param2: 触发条件

@原   型: wait_event_timeout(wq, condition, timeout)
@功   能: 功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位
@param1: 等待队列头
@param2: 触发条件
@param3: 超时时间
@return: 0表示超时时间到,且condition为假。1表示condition为真,也就是条件满足了

wait_event_interruptible(wq, condition)						// 进程休眠时可以通过信号来唤醒
wait_event_interruptible_timeout(wq, condition, timeout)	// 
wait_event_interruptible_exclusive(wq, condition)			// 排他性,默认情况下唤醒操作会把队列中所有进程都唤醒,如果一个进程以排他方式休眠则唤醒时不会唤醒其他进程
wait_event_interruptible_locked(wq, condition)				// 调用前先获得等待队列内部的锁
wait_event_interruptible_lock_irq(wq, condition, lock)		// 上锁的同时禁止中断

返回值: 不带 timeout 返回0表示被成功唤醒,返回 -ERESTARTSYS 表示被信号唤醒
	    带 timeout 返回0表示超时,返回大于0表示被成功唤醒,这个值表示离超时还剩余的时间

等待事件和唤醒操作对应的关系为:
   带 locked 的用 wake_up_locked 唤醒
   不带 locked 带 interruptible 的用 wake_up_interruptible 唤醒
   否则用 wake_up 唤醒

2.6 其他函数

@原   型: int signal_pending(struct task_struct *p)
		  int signal_pending(current)
@功   能: 检查当前进程是否有信号处理,返回不为0表示有信号需要处理。
@return:  返回 -ERESTARTSYS 表示信号函数处理完毕后重新执行信号函数前的某个系统调用。也就是说,如果信号函数前有发生系统调用,在调度信号处理函数之前,内核会检查系统调用的返回值,看看是不是因为这个信号而中断了系统调用。如果返回值-ERESTARTSYS,并且当前调度的信号具备-ERESTARTSYS属性,系统就会在用户信号函数返回之后再执行该系统调用。


@原   型: __set_current_state(state_value)
@功   能:  改变进程状态
@param1: TASK_RUNNING			// 可运行状态
		 TASK_INTERRUPTIBLE		// 可中断的等待状态
		 TASK_UNINTERRUPTIBLE 	// 不可中断的等待状态


@原   型: void schedule(void)		 
@功   能: 进行一次任务切换,调度其他进程执行
@return: 无返回值

3. 示例代码

 本例中继续使用虚拟串口驱动来实现阻塞型I/O,上一节当fifo中的数据为空时,直接return,本例中实现的功能是读操作时如果fifo中的数据为空,则读进程休眠,此时FIFO不为满唤醒写进程;写操作时FIFO不为满,将用户空间数据拷贝到内核空间,唤醒读进程。
 具体实现代码如下:

3.1 demo.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/kfifo.h>
#include <linux/wait.h>

#define VSER_CHRDEV_ANME ("vser_chrdev")
#define VSER_CLASS_ANME  ("vser_cls")
#define VSER_DEVICE_ANME ("vser_dev")
#define KFIFO_SIZE (16)
#define FLAG (0)

struct vser_dev{
	dev_t dev_no;
	int major;
	int minor;
	struct cdev cdev;
	struct class *cls;
	struct device *dev;
	wait_queue_head_t rwqh;		// 定义读的等待队列头 
	wait_queue_head_t wwqh;		// 定义写的等待队列头
};
struct vser_dev test_vser_dev;

DEFINE_KFIFO(vser_fifo, char, KFIFO_SIZE);	// 声明定义一个虚拟串口

static int vser_open(struct inode *inode, struct file *filp)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	filp->private_data = &test_vser_dev;
	
	return 0;
}

static int vser_release(struct inode *indoe, struct file *filp)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	return 0;
}

static ssize_t vser_read(struct file *filp, char __user *userbuf, size_t size, loff_t *offset)
{
	unsigned int copied_num, ret;
	struct vser_dev *test_vser_dev = filp->private_data;

#if FLAG
	DECLARE_WAITQUEUE(r_wait, current);		// 定义读的等待队列节点
#endif

	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	if ( kfifo_is_empty(&vser_fifo) )		// kfifo为空返回真
	{
		printk("kfifo_is_empty.\n");
		if ( filp->f_flags & O_NONBLOCK )	// 如果非阻塞方式打开直接返回
		{
			printk("O_NONBLOCK.\n");
			ret = -EAGAIN;	// try again
		}
		
#if FLAG	
		// 此段代码与83行代码意义相同,是76行代码的具体实现过程
		// 如果使用此段代码,在本函数中err0中的代码段也需一起使用
		add_wait_queue(&test_vser_dev->rwqh, &r_wait);	// 将读的等待队列节点 添加到 读的等待队列头中
		__set_current_state(TASK_INTERRUPTIBLE);		// 改变进程状态为休眠
		schedule();										// 调度其他进程执行

		if ( signal_pending(current) )					// 被信号唤醒
		{
			ret = -ERESTARTSYS;
			goto err0;
		}
#endif

#if (~FLAG)
		// condition 条件不成立的时候进程休眠,即kfifo为空时进程休眠
		if ( wait_event_interruptible(test_vser_dev->rwqh, !kfifo_is_empty(&vser_fifo)) < 0 )
		{
			ret = -ERESTARTSYS;
		}
#endif	
		goto err0;
	}

	printk("kfifo is not empty.\n");
	if (size > KFIFO_SIZE)
	{
		size = KFIFO_SIZE;	// 判断拷贝内容的大小
	}
		
	ret = kfifo_to_user(&vser_fifo, userbuf, size, &copied_num);	// kfifo不为空将数据拷贝到用户空间
	if (ret < 0)
	{
		printk("kfifo_to_user failed.\n");
		ret = -EFAULT;	// Bad Address
		goto err0;
	}
	printk("%s copied_num = %d.\n", __FUNCTION__, copied_num);

	if ( !kfifo_is_full(&vser_fifo) )					// kfifo不为满
	{
		wake_up_interruptible(&test_vser_dev->wwqh);		// 唤醒写的等待队列头
	}

	return copied_num;

err0:
#if FLAG
	set_current_state(TASK_RUNNING);	// 设置当前进程为运行态
	remove_wait_queue(&test_vser_dev->rwqh, &r_wait);	// 将等待队列清除
#endif
	return ret;
}

static ssize_t vser_write(struct file *filp, const char __user *userbuf, size_t size, loff_t *offset)
{
	unsigned int copied_num = 0;
	unsigned int ret = 0;	
	struct vser_dev *test_vser_dev = filp->private_data;

	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	if ( kfifo_is_full(&vser_fifo) )		// kfifo为满返回真
	{
		printk("kfifo_is_full.\n");
		if ( filp->f_flags & O_NONBLOCK )	// 判断是否以非阻塞方式打开
		{
		    printk("%s -- O_NONBLOCK.\n", __FUNCTION__);
			ret = -EAGAIN;
			goto err0;
		}

		if (wait_event_interruptible(test_vser_dev->wwqh, !kfifo_is_full(&vser_fifo)) < 0)
		{
			ret = -ERESTARTSYS;
		}
		goto err0;
	}

	if (size > KFIFO_SIZE)
	{
		size = KFIFO_SIZE;
	}
		
	ret = kfifo_from_user(&vser_fifo, userbuf, size, &copied_num);	// kfifo不为满,则将用户空间数据拷贝到内核空间
	if (ret == -EFAULT)
	{
		printk("kfifo_from_user failed.\n");
		goto err0;
	}
	printk("%s -- copied_num = %d.\n", __FUNCTION__, copied_num);

	if ( !kfifo_is_empty(&vser_fifo) )
	{
		wake_up_interruptible(&test_vser_dev->rwqh);		// 唤醒读的等待队列
	}

	return copied_num;

err0:
	return ret;
}

struct file_operations vser_fops = 
{
	.open = vser_open,
	.release = vser_release,
	.read = vser_read,
	.write = vser_write,
};

static int __init vser_init(void)
{
	int ret;
	
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	if (test_vser_dev.major)
	{
		test_vser_dev.dev_no = MKDEV(test_vser_dev.major, 0);
		ret = register_chrdev_region(test_vser_dev.dev_no, 1, VSER_CHRDEV_ANME);
		if (ret < 0)
		{
			printk("register_chrdev_region failed.\n");
			goto register_chrdev_region_err;
		}
	}
	else
	{
		ret = alloc_chrdev_region(&test_vser_dev.dev_no, 0, 1, VSER_CHRDEV_ANME);
		if (ret < 0)
		{
			printk("alloc_chrdev_region failed.\n");
			goto alloc_chrdev_region_err;
		}
	}

	cdev_init(&test_vser_dev.cdev, &vser_fops);
	ret = cdev_add(&test_vser_dev.cdev, test_vser_dev.dev_no, 1);
	if (ret < 0)
	{
		printk("cdev_add failed.\n");
		goto cdev_add_err;
	}

	test_vser_dev.cls = class_create(THIS_MODULE, VSER_CLASS_ANME);
	if ( IS_ERR(test_vser_dev.cls) )
	{
		printk("class_create failed.\n");
		ret = PTR_ERR(test_vser_dev.cls);
		goto class_create_err;
	}
	
	test_vser_dev.dev = device_create(test_vser_dev.cls, NULL, test_vser_dev.dev_no, NULL, VSER_DEVICE_ANME);
	if ( IS_ERR(test_vser_dev.dev) )
	{
		printk("device_create failed.\n");
		ret = PTR_ERR(test_vser_dev.dev);
		goto device_create_err;
	}

	init_waitqueue_head(&test_vser_dev.rwqh);	// 初始化读的等待队列头
	init_waitqueue_head(&test_vser_dev.wwqh);	// 初始化写的等待队列头
	
	return 0;

device_create_err:
	class_destroy(test_vser_dev.cls);
class_create_err:
	cdev_del(&test_vser_dev.cdev);
cdev_add_err:
	unregister_chrdev(test_vser_dev.major, VSER_CHRDEV_ANME);
alloc_chrdev_region_err:
register_chrdev_region_err:
	return ret;
}

static void __exit vser_exit(void)
{
	printk("%s -- %d.\n", __FUNCTION__, __LINE__);

	device_destroy(test_vser_dev.cls, test_vser_dev.dev_no);
	class_destroy(test_vser_dev.cls);
	cdev_del(&test_vser_dev.cdev);
	unregister_chrdev(test_vser_dev.major, VSER_CHRDEV_ANME);
}

module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");

3.2 test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[])
{
	int fd, ret;
	char cmd;
	const char *dev_pathname = "/dev/vser_dev";
	char read_buf[8]  = "\0";
	char write_buf[8] = "abc"; 

	if (argc < 2)
	{
		printf("please input param 'w' or 'r'\n");
		return -1;
	}
	
	fd = open(dev_pathname, O_RDWR , 0666);	// 默认以阻塞方式打开设备文件
	if (fd < 0)
	{
		perror("open");
		return -1;
	}

	cmd = argv[1][0];
	switch(cmd)
	{
	case 'r':
		ret = read(fd, read_buf, sizeof(read_buf));
		if (ret == -1)	// 读失败
		{
			perror("read");
		}
		else if (ret == 0)
		{
			printf("read end of file\n");
		}
		else
		{
			printf("read_buf = %s\n", read_buf);
		}
		break;
	case 'w':
		ret = write(fd, write_buf, strlen(write_buf));
		if (ret == -1)
		{
			perror("write");
		}
		break;
	default:
		printf("cmd error\n");
	}

	close(fd);
	
	return 0;
}

3.3 Makefile

KERNELDIR ?= /home/linux/ti-processor-sdk-linux-am335x-evm-04.00.00.04/board-support/linux-4.9.28/
PWD := $(shell pwd)

EXEC = app
OBJS = test.o
CC   = arm-linux-gnueabihf-gcc

$(EXEC):$(OBJS)
	make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) modules
	$(CC) $^ -o $@
.o:.c
	$(CC) -c $<

install:
	sudo cp *.ko  app /tftpboot
	sudo cp *.ko app /media/linux/rootfs1/home/root/
	make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) clean
	rm app
	sudo ls -l /media/linux/rootfs1/home/root/
clean:
	make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) clean
	rm app

obj-m += demo.o

3.4 测试结果

root@am335x-evm:~# insmod demo.ko 
[  445.778808] vser_init -- 182.
root@am335x-evm:~# ./app r &
[1] 990
[  449.686864] vser_open -- 33.
[  449.689856] vser_read -- 56.
[  449.698447] kfifo_is_empty.
root@am335x-evm:~# ./app w
[  452.273161] vser_open -- 33.
[  452.276157] vser_write -- 127.
[  452.279248] vser_write -- copied_num = 3.
read_buf = abc
[  456.627227] vser_release -- 41.
[1]+  Done                    ./app r
[  456.841265] vser_release -- 41.
root@am335x-evm:~# rmmod demo.ko 
[  508.718326] vser_exit -- 246.
发布了57 篇原创文章 · 获赞 64 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_36310253/article/details/103065634