30.linux内核 阻塞/非阻塞IO

当欲执行的条件不满足的时候,可以休眠进程等待,也可以返回不可操作标志。休眠等待就是阻塞IO模式,直接返回就是非阻塞IO模式。

阻塞IO

在驱动中休眠线程可以使用互斥锁(mutex)、信号量(smaphare)、完成量(complition)。这些都是基于等待队列实现的,很多时候需要直接使用等待队列进行更加复杂的调度操作。
最基本的等待队列的操作是:

  1. 判断条件是否需要休眠,休眠的话吧当前进程放入等待队列,休眠后被唤醒了继续检查条件,是否还需要休眠。对应的函数一般是(wait_event)
  2. 唤醒等待队列中的一个进程
    相关API在wait.h中
//定义等待队列
wait_queue_head_t wait_task;
//初始化等待队列
init_waitqueue_head(&wait_task);
//判断并休眠
wait_event(queue, condition) 								//非中断休眠。
wait_event_interruptible(queue, condition) 					//可以被信号中断,返回非零表示休眠被某个信号中断。
wait_event_timeout(queue, condition, timeout) 				//限时版本,超时返回0。
wait_event_interruptible_timeout(queue, condition, timeout) //限时版本,超时返回0。
//唤醒函数
void wake_up(wait_queue_head_t *queue); 				//唤醒等待在给定queue上的所有进程。
void wake_up_interruptible(wait_queue_head_t *queue); 	//只会唤醒那些执行可中断休眠的进程。

用等待队列简单实现一下字符设备的FIFO,使用wait_event_interruptible可以被ctrl+c打断

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <asm/uaccess.h>

MODULE_LICENSE("GPL");

static int data = 1;
module_param(data, int, 0644);  //声明模块参数
dev_t devid;
struct cdev char_dev;
struct class * char_class;
int buffer_size = 100;
char * char_data;
unsigned char has_data_flag;
static wait_queue_head_t wait_task;

static int open(struct inode * node, struct file * fl){
	return 0;
}

static long ioctl(struct file * fl, unsigned int cmd, unsigned long arg){
	return 0;
}

static ssize_t read(struct file * fl, char __user * buf, size_t len, loff_t * offset){
	int ret = 0,copy_len,data_len;
	wait_event_interruptible(wait_task,has_data_flag != 0);
	has_data_flag = 0;

	data_len = strlen(char_data)+1;
	if(fl->f_pos + len > data_len)
		copy_len = data_len - fl->f_pos; //超过长度,复制剩余部分
	else
		copy_len = len;					 //没超过

	ret = copy_to_user(buf,char_data+fl->f_pos,copy_len);
	ret = copy_len - ret;
	*offset += ret;						 //移动文件指针
	return ret;
}

static ssize_t write(struct file * fl, const char __user * buf, size_t len, loff_t * offset){
	int ret = 0,copy_len,data_len = buffer_size;

	if(fl->f_pos + len > data_len)
		copy_len = data_len - fl->f_pos; //超过长度,复制剩余部分
	else
		copy_len = len;					 //没超过

	ret = copy_from_user(char_data+fl->f_pos,buf,copy_len);
	ret = copy_len - ret;
	*offset += ret;						 //移动文件指针
	has_data_flag = 1;
	wake_up_interruptible(&wait_task);
	return ret;
}

struct file_operations my_opts = {
	.owner = THIS_MODULE,
	.open = open,
	.read = read,
	.write = write,
	.unlocked_ioctl = ioctl
};

static int __init char_init(void){
	int ret = 0;

    devid = MKDEV(241, 1);								//换算设备号
    ret = register_chrdev_region(devid, 1, "char_test");//注册设备,在/proc/drivers下面可以看到
    if (ret < 0)
        goto err0;

    cdev_init(&char_dev,&my_opts);						//绑定opt结构体
    char_dev.owner = THIS_MODULE;
    ret = cdev_add(&char_dev,devid,1);					//注册字符设备驱动
    if (ret < 0)
    	goto err1;

    char_class = class_create(THIS_MODULE,"char_test"); //在/sys/class中创建文件夹
    device_create(char_class,NULL,devid,NULL,"char_test_dev_%d",1);//在上一步文件夹中创建char_test_dev_1

    char_data = kzalloc(buffer_size,GFP_KERNEL);

    has_data_flag = 0;
    init_waitqueue_head(&wait_task);
	printk("char init\n");
    return 0;

	err1:
	    unregister_chrdev_region(devid, 1);
    err0:
        return ret;
}

static void __exit char_exit(void){
	kfree(char_data);
	unregister_chrdev_region(devid, 1);
	cdev_del(&char_dev);
	device_destroy(char_class,devid);
	class_destroy(char_class);
	printk("char exit\n");
}

module_init(char_init);
module_exit(char_exit);

大多是时候等待队列的使用并没有这么简单,配合上锁机制实现更加精细化的控制。精细化使用一般就不用wait_event函数了,而是自己实现wait_event,基本流程是:

  1. 吧当前进程控制块封装成等待队列对象。
  2. 吧等待对象加入等待队列。
  3. 判断条件,不满足的话改变自己的进程状态为休眠(设置状态不会立刻改变线程的行为),调用schedule函数主动放弃CPU,让系统重新调度。
  4. 醒来判断是否是信号唤醒的,比如ctrl+c,可以就此返回
  5. 执行任务
  6. 移出等待队列
  7. 设置自己的状态为运行状态

看一下wait_event源码:

do {
	DEFINE_WAIT(__wait);

	for (;;) {
		prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);
		if (condition)
			break;
		schedule();
	}
	finish_wait(&wq, &__wait);
} while (0)

大致就是这个过程,例子不演示了。

非阻塞IO

非阻塞IO有两种实现,一种是在用户空间打开设备文件后设置是否需要阻塞访问。驱动中直接判断文件描述符的f_flags就可以了。设置了非阻塞访问就直接返回。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <asm/uaccess.h>

MODULE_LICENSE("GPL");

static int data = 1;
module_param(data, int, 0644);  //声明模块参数
dev_t devid;
struct cdev char_dev;
struct class * char_class;
int buffer_size = 100;
char * char_data;
unsigned char has_data_flag;
static wait_queue_head_t wait_task;

static int open(struct inode * node, struct file * fl){
	return 0;
}

static long ioctl(struct file * fl, unsigned int cmd, unsigned long arg){
	return 0;
}

static ssize_t read(struct file * fl, char __user * buf, size_t len, loff_t * offset){
	int ret = 0,copy_len,data_len;

	if (fl->f_flags & O_NONBLOCK && has_data_flag == 0)
		return -EAGAIN;					//直接返回

	wait_event_interruptible(wait_task,has_data_flag != 0);
	has_data_flag = 0;

	data_len = strlen(char_data)+1;
	if(fl->f_pos + len > data_len)
		copy_len = data_len - fl->f_pos; //超过长度,复制剩余部分
	else
		copy_len = len;					 //没超过

	ret = copy_to_user(buf,char_data+fl->f_pos,copy_len);
	ret = copy_len - ret;
	*offset += ret;						 //移动文件指针
	return ret;
}

static ssize_t write(struct file * fl, const char __user * buf, size_t len, loff_t * offset){
	int ret = 0,copy_len,data_len = buffer_size;

	if(fl->f_pos + len > data_len)
		copy_len = data_len - fl->f_pos; //超过长度,复制剩余部分
	else
		copy_len = len;					 //没超过

	ret = copy_from_user(char_data+fl->f_pos,buf,copy_len);
	ret = copy_len - ret;
	*offset += ret;						 //移动文件指针
	has_data_flag = 1;
	wake_up_interruptible(&wait_task);
	return ret;
}

struct file_operations my_opts = {
	.owner = THIS_MODULE,
	.open = open,
	.read = read,
	.write = write,
	.unlocked_ioctl = ioctl
};

static int __init char_init(void){
	int ret = 0;

    devid = MKDEV(241, 1);								//换算设备号
    ret = register_chrdev_region(devid, 1, "char_test");//注册设备,在/proc/drivers下面可以看到
    if (ret < 0)
        goto err0;

    cdev_init(&char_dev,&my_opts);						//绑定opt结构体
    char_dev.owner = THIS_MODULE;
    ret = cdev_add(&char_dev,devid,1);					//注册字符设备驱动
    if (ret < 0)
    	goto err1;

    char_class = class_create(THIS_MODULE,"char_test"); //在/sys/class中创建文件夹
    device_create(char_class,NULL,devid,NULL,"char_test_dev_%d",1);//在上一步文件夹中创建char_test_dev_1

    char_data = kzalloc(buffer_size,GFP_KERNEL);

    has_data_flag = 0;
    init_waitqueue_head(&wait_task);
	printk("char init\n");
    return 0;

	err1:
	    unregister_chrdev_region(devid, 1);
    err0:
        return ret;
}

static void __exit char_exit(void){
	kfree(char_data);
	unregister_chrdev_region(devid, 1);
	cdev_del(&char_dev);
	device_destroy(char_class,devid);
	class_destroy(char_class);
	printk("char exit\n");
}

module_init(char_init);
module_exit(char_exit);

另外就是使用系统调用poll/select/epoll。这些系统调用可以一次性监控多个文件描述符,实现的基本原理是将当前进程加入到所有驱动的阻塞队列然后再阻塞,任何一个文件唤醒进程都会唤醒到自己。
这些系统调用监控文件描述符之后,会调用到驱动中的poll函数,实现对文件的监控。驱动中的poll函数主要完成以下几件事:

  1. 将当前进程等待对象加入到等待队列中去
  2. 判断条件是否满足(目的是触发一开始就满足条件的文件描述符的操作)
  3. 返回条件
    条件定义在poll.h
/* These are specified by iBCS2 */
#define POLLIN		0x0001
#define POLLPRI		0x0002
#define POLLOUT		0x0004
#define POLLERR		0x0008
#define POLLHUP		0x0010
#define POLLNVAL	0x0020

/* The rest seem to be more-or-less nonstandard. Check them! */
#define POLLRDNORM	0x0040
#define POLLRDBAND	0x0080
#ifndef POLLWRNORM
#define POLLWRNORM	0x0100
#endif
#ifndef POLLWRBAND
#define POLLWRBAND	0x0200
#endif
#ifndef POLLMSG
#define POLLMSG		0x0400
#endif
#ifndef POLLREMOVE
#define POLLREMOVE	0x1000
#endif
#ifndef POLLRDHUP
#define POLLRDHUP       0x2000
#endif

POLLIN 如果设备无阻塞的读,就返回该值。
POLLRDNORM 通常的数据已经准备好,可以读了,就返回该值。通常的做法是会返回(POLLLIN|POLLRDNORA)。
POLLRDBAND 如果可以从设备读出带外数据,就返回该值,它只可在linux内核的某些网络代码中使用,通常不用在设备驱动程序中
POLLPRI 如果可以无阻塞的读取高优先级(带外)数据,就返回该值,返回该值会导致select报告文件发生异常,以为select八带外数据当作异常处理。
POLLHUP 当读设备的进程到达文件尾时,驱动程序必须返回该值,依照select的功能描述,调用select的进程被告知进程时可读的。
POLLERR 如果设备发生错误,就返回该值。
POLLOUT 如果设备可以无阻塞地些,就返回该值。
POLLWRNORM 设备已经准备好,可以写了,就返回该值。通常地做法是(POLLOUT|POLLNORM)。
POLLWRBAND 于POLLRDBAND类似。

驱动中poll定义为unsigned int (*poll) (struct file *filep, struct poll_table_struct *wait);跟read,write,open一样添加到file_operations就可以了。简单例子。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/poll.h>
#include <asm/uaccess.h>

MODULE_LICENSE("GPL");

static int data = 1;
module_param(data, int, 0644);  //声明模块参数
dev_t devid;
struct cdev char_dev;
struct class * char_class;
int buffer_size = 100;
char * char_data;
unsigned char has_data_flag;
static wait_queue_head_t wait_task;

static int open(struct inode * node, struct file * fl){
	return 0;
}

static long ioctl(struct file * fl, unsigned int cmd, unsigned long arg){
	return 0;
}

static ssize_t read(struct file * fl, char __user * buf, size_t len, loff_t * offset){
	int ret = 0,copy_len,data_len;
	wait_event_interruptible(wait_task,has_data_flag != 0);
	has_data_flag = 0;

	data_len = strlen(char_data)+1;
	if(fl->f_pos + len > data_len)
		copy_len = data_len - fl->f_pos; //超过长度,复制剩余部分
	else
		copy_len = len;					 //没超过

	ret = copy_to_user(buf,char_data+fl->f_pos,copy_len);
	ret = copy_len - ret;
	*offset += ret;						 //移动文件指针
	return ret;
}

static ssize_t write(struct file * fl, const char __user * buf, size_t len, loff_t * offset){
	int ret = 0,copy_len,data_len = buffer_size;

	if(fl->f_pos + len > data_len)
		copy_len = data_len - fl->f_pos; //超过长度,复制剩余部分
	else
		copy_len = len;					 //没超过

	ret = copy_from_user(char_data+fl->f_pos,buf,copy_len);
	ret = copy_len - ret;
	*offset += ret;						 //移动文件指针
	has_data_flag = 1;
	wake_up_interruptible(&wait_task);
	return ret;
}

unsigned int hello_poll(struct file *fl, struct poll_table_struct *wait)
{
    unsigned int mask = POLLOUT | POLLWRNORM;	//一直可写

    poll_wait(fl, &wait_task,  wait);			//当前进程加入被poll阻塞的等待队列
    if (has_data_flag) {
        mask |= POLLIN | POLLRDNORM;			//可读
    }

    return mask;
}

struct file_operations my_opts = {
	.owner = THIS_MODULE,
	.open = open,
	.read = read,
	.write = write,
	.poll = hello_poll,
	.unlocked_ioctl = ioctl
};

static int __init char_init(void){
	int ret = 0;

    devid = MKDEV(241, 1);								//换算设备号
    ret = register_chrdev_region(devid, 1, "char_test");//注册设备,在/proc/drivers下面可以看到
    if (ret < 0)
        goto err0;

    cdev_init(&char_dev,&my_opts);						//绑定opt结构体
    char_dev.owner = THIS_MODULE;
    ret = cdev_add(&char_dev,devid,1);					//注册字符设备驱动
    if (ret < 0)
    	goto err1;

    char_class = class_create(THIS_MODULE,"char_test"); //在/sys/class中创建文件夹
    device_create(char_class,NULL,devid,NULL,"char_test_dev_%d",1);//在上一步文件夹中创建char_test_dev_1

    char_data = kzalloc(buffer_size,GFP_KERNEL);

    has_data_flag = 0;
    init_waitqueue_head(&wait_task);
	printk("char init\n");
    return 0;

	err1:
	    unregister_chrdev_region(devid, 1);
    err0:
        return ret;
}

static void __exit char_exit(void){
	kfree(char_data);
	unregister_chrdev_region(devid, 1);
	cdev_del(&char_dev);
	device_destroy(char_class,devid);
	class_destroy(char_class);
	printk("char exit\n");
}

module_init(char_init);
module_exit(char_exit);

操作的application:

int main(int argc, char const *argv[])
{
    struct pollfd fds = { 0 };

    fds.fd = open(DEVICE_NAME, O_RDWR);
    if (fds.fd < 0) {
        printf("open failed! error:%s\n", strerror(errno));
        return 0;
    }

    fds.events = POLLIN;
    poll(&fds, 1, 5000); /* 五秒后自动返回 */
    printf("fds.revent: %d\n", fds.revents);

    close(fds.fd);

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值