02_阻塞与非阻塞I/O

文章详细阐述了Linux设备驱动中阻塞和非阻塞I/O的操作原理,包括等待队列的使用,如何处理进程的等待和唤醒。同时,介绍了轮询操作,如select、poll和epoll的工作方式,以及在设备驱动中如何实现轮询功能。此外,还讨论了信号在进程状态转换中的角色。
摘要由CSDN通过智能技术生成

        阻塞操作是指在执行设备操作时, 若不能获得资源, 则挂起进程直到满足可操作的条件后再进行操作。 被挂起的进程进入睡眠状态, 被从调度器的运行队列移走, 直到等待的条件被满足。

        非阻塞操作的进程在不能进行设备操作时, 并不挂起, 它要么放弃, 要么不停地查询, 直至可以进行操作为止。

阻塞读取串口一个字符

非阻塞读取串口一个字符

1、等待队列

如下代码为在设备驱动中使用等待队列的模版, 在进行写I/O操作的时候, 判断设备是否可写, 如果不可写且为阻塞I/O, 则进程睡眠并挂起到等待队列。

static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos)
{
	...
	DECLARE_WAITQUEUE(wait, current); /* 定义等待队列元素 */
	add_wait_queue(&xxx_wait, &wait); /* 添加元素到等待队列 */

	/* 等待设备缓冲区可写 */
	do {
		avail = device_writable(...);
		if (avail < 0) {
			if (file->f_flags &O_NONBLOCK) { /* 非阻塞 */
				ret = -EAGAIN;
				goto out;
			}
			__set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态 */
			schedule(); /* 调度其他进程执行 */
			if (signal_pending(current)) { /* 如果是因为信号唤醒 */
				ret = -ERESTARTSYS;
				goto out;
			}
		}
	} while (avail < 0);

	/* 写设备缓冲区 */
	device_write(...)
out:
	remove_wait_queue(&xxx_wait, &wait); /* 将元素移出xxx_wait指引的队列 */
	set_current_state(TASK_RUNNING); /* 设置进程状态为TASK_RUNNING */
	return ret;
}

1) 如果是非阻塞访问(O_NONBLOCK被设置) , 设备忙时, 直接返回“-EAGAIN”。

2) 对于阻塞访问, 会调用__set_current_state(TASK_INTERRUPTIBLE) 进行进程状态切换并显示通过“schedule() ”调度其他进程执行。

3) 醒来的时候要注意, 由于调度出去的时候, 进程状态是TASK_INTERRUPTIBLE, 即浅度睡眠,所以唤醒它的有可能是信号, 因此, 我们首先通过signal_pending(current) 了解是不是信号唤醒的, 如果是, 立即返回“-ERESTARTSYS”。

(ps:TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE 的区别

TASK_INTERRUPTIBLE是可以被信号和wake_up()唤醒的,当信号到来时,进程会被设置为可运行

而TASK_UNINTERRUPTIBLE只能被wake_up()唤醒。

信号本质

信号是在软件层次上对中断机制的一种模拟,软中断

信号来源

信号事件的发生有两个来源:

硬件来源:(比如我们按下了键盘或者其它硬件故障);

软件来源:最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

区分是什么原因唤醒进程,用signal_pending( current );

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

DECLARE_WAITQUEUE、 add_wait_queue这两个动作加起来完成的效果如图。在

wait_queue_head_t指向的链表上, 新定义的wait_queue元素被插入, 而这个新插入的元素绑定了一个task_struct(当前做xxx_write的current, 这也是DECLARE_WAITQUEUE使用“current”作为参数的原因) 。

2、globalfifo设备驱动

把globalmem中的全局内存变成一个FIFO, 只有当FIFO中有数据的时候(即有进程把数据写到这个FIFO而且没有被读进程读空) , 读进程才能把数据读出, 而且读取后的数据会从globalmem的全局内存中被拿掉; 只有当FIFO不是满的时(即还有一些空间未被写, 或写满后被读进程从这个FIFO中读出了数据) , 写进程才能往这个FIFO中写入数据。

修改设备结构体, 在其中增加两个等待队列头部, 分别对应于读和写

struct globalfifo_dev {
	struct cdev cdev;
	unsigned int current_len;
	unsigned char mem[GLOBALFIFO_SIZE];
	struct mutex mutex;
	wait_queue_head_t r_wait;
	wait_queue_head_t w_wait;
};

current_len成员以用于表征目前FIFO中有效数据的长度。 current_len等于0意味着FIFO空, current_len等于GLOBALFIFO_SIZE意味着FIFO满。

两个等待队列头部需在设备驱动模块加载函数中调用init_waitqueue_head() 被初始化, 新的设备驱动模块加载函数如代码如下

static int __init globalfifo_init(void)
{
	int ret;
	dev_t devno = MKDEV(globalfifo_major, 0);

	if (globalfifo_major)
		ret = register_chrdev_region(devno, 1, "globalfifo");
	else {
		ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo");
		globalfifo_major = MAJOR(devno);
	}
	if (ret < 0)
		return ret;

	globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
	if (!globalfifo_devp) {
		ret = -ENOMEM;
		goto fail_malloc;
	}

	globalfifo_setup_cdev(globalfifo_devp, 0);

	mutex_init(&globalfifo_devp->mutex);
	init_waitqueue_head(&globalfifo_devp->r_wait);
	init_waitqueue_head(&globalfifo_devp->w_wait);

	return 0;
fail_malloc:
	unregister_chrdev_region(devno, 1);
	return ret;
}
module_init(globalfifo_init);

设备驱动读写操作需要被修改, 在读函数中需增加唤醒globalfifo_devp->w_wait的语句, 而在写操作中唤醒globalfifo_devp->r_wait, 代码如下:

static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
	int ret;
	struct globalfifo_dev *dev = filp->private_data;
	DECLARE_WAITQUEUE(wait, current);

	mutex_lock(&dev->mutex);
	add_wait_queue(&dev->r_wait, &wait);

	while (dev->current_len == 0) {
		if (filp->f_flags & O_NONBLOCK) {
			ret = -EAGAIN;
			goto out;
		}
		__set_current_state(TASK_INTERRUPTIBLE);
		mutex_unlock(&dev->mutex);

		schedule();
		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			goto out2;
		}

		mutex_lock(&dev->mutex);
	}

	if (count > dev->current_len)
		count = dev->current_len;

	if (copy_to_user(buf, dev->mem, count)) {
		ret = -EFAULT;
		goto out;
	} else {
		memcpy(dev->mem, dev->mem + count, dev->current_len - count);
		dev->current_len -= count;
		printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev->current_len);
		wake_up_interruptible(&dev->w_wait);
		ret = count;
	}
out:
	mutex_unlock(&dev->mutex);;
out2:
	remove_wait_queue(&dev->w_wait, &wait);
	set_current_state(TASK_RUNNING);
	return ret;
}

static ssize_t globalfifo_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
	struct globalfifo_dev *dev = filp->private_data;
	int ret;
	DECLARE_WAITQUEUE(wait, current);

	mutex_lock(&dev->mutex);
	add_wait_queue(&dev->w_wait, &wait);

	while (dev->current_len == GLOBALFIFO_SIZE) {
		if (filp->f_flags & O_NONBLOCK) {
			ret = -EAGAIN;
			goto out;
		}
		__set_current_state(TASK_INTERRUPTIBLE);

		mutex_unlock(&dev->mutex);
		schedule();
		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			goto out2;
		}

		mutex_lock(&dev->mutex);
	}

	if (count > GLOBALFIFO_SIZE - dev->current_len)
		count = GLOBALFIFO_SIZE - dev->current_len;

	if (copy_from_user(dev->mem + dev->current_len, buf, count)) {
		ret = -EFAULT;
		goto out;
	} else {
		dev->current_len += count;
		printk(KERN_INFO "written %d bytes(s),current_len:%d\n", count,
		dev->current_len);

		wake_up_interruptible(&dev->r_wait);
		ret = count;
	}

out:
	mutex_unlock(&dev->mutex);;
out2:
	remove_wait_queue(&dev->w_wait, &wait);
	set_current_state(TASK_RUNNING);
	return ret;
}

3、轮询操作

非阻塞I/O的应用程序通常会使用select() 和poll() 系统调用查询是否可对设备进行无阻塞的访问。 select() 和poll() 系统调用最终会使设备驱动中的poll() 函数被执行,epoll() , 即扩展的poll() 。

1)轮询编程

a.select 原型

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

其中readfds、 writefds、 exceptfds分别是被select() 监视的读、 写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的fd加1。 readfds文件集中的任何一个文件变得可读,select() 返回;同理, writefds文件集中的任何一个文件变得可写, select也返回。

第一次对n个文件进行select() 的时候, 若任何一个文件满足要求, select() 就直接返回; 第2次再进行select() 的时候, 没有文件满足读写要求, select() 的进程阻塞且睡眠。 由于调用select() 的时候, 每个驱动的poll() 接口都会被调用到, 实际上执行select() 的进程被挂到了每个驱动的等待队列上, 可以被任何一个驱动唤醒。 如果FDn变得可读写, select() 返回。

b.poll原型

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

c.epoll原型

①int epoll_create(int size);

创建一个epoll的句柄, size用来告诉内核要监听多少个fd。 需要注意的是, 当创建好epoll句柄后, 它本身也会占用一个fd值, 所以在使用完epoll后, 必须调用close() 关闭。

②int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

告诉内核要监听什么类型的事件。

第1个参数是epoll_create() 的返回值,

第2个参数表示动作, 包含:

EPOLL_CTL_ADD: 注册新的fd到epfd中。

EPOLL_CTL_MOD: 修改已经注册的fd的监听事件。

EPOLL_CTL_DEL: 从epfd中删除一个fd。

第3个参数是需要监听的fd,

第4个参数是告诉内核需要监听的事件类型, struct epoll_event结构如下

events可以是以下几个宏的“或”:

EPOLLIN: 表示对应的文件描述符可以读。

EPOLLOUT: 表示对应的文件描述符可以写。

EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示的是有socket带外数据到来)

EPOLLERR: 表示对应的文件描述符发生错误。

EPOLLHUP: 表示对应的文件描述符被挂断。

EPOLLET: 将epoll设为边缘触发(Edge Triggered) 模式, 这是相对于水平触发(Level Triggered) 来说的。 LT(Level Triggered) 是缺省的工作方式, 在LT情况下, 内核告诉用户一个fd是否就绪了, 之后用户可以对这个就绪的fd进行I/O操作。 但是如果用户不进行任何操作, 该事件并不会丢失, 而ET(EdgeTriggered) 是高速工作方式, 在这种模式下, 当fd从未就绪变为就绪时, 内核通过epoll告诉用户, 然后它会假设用户知道fd已经就绪, 并且不会再为那个fd发送更多的就绪通知。

EPOLLONESHOT: 意味着一次性监听, 当监听完这次事件之后, 如果还需要继续监听这个fd的话,需要再次把这个fd加入到epoll队列里。

③int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件的产生, 其中events参数是输出参数, 用来从内核得到事件的集合, maxevents告诉内核本次最多收多少事件, maxevents的值不能大于创建epoll_create() 时的size, 参数timeout是超时时间(以毫秒为单位, 0意味着立即返回, -1意味着永久等待) 。 该函数的返回值是需要处理的事件数目, 如返回0, 则表示已超时。

4、轮询编程

设备驱动中poll() 函数的原型

unsigned int(*poll)(struct file * filp, struct poll_table* wait);

第1个参数为file结构体指针, 第2个参数为轮询表指针。 这个函数应该进行两项工作。

1) 对可能引起设备文件状态变化的等待队列调用poll_wait() 函数, 将对应的等待队列头部添加到poll_table中。

2) 返回表示是否能对设备进行无阻塞读、 写访问的掩码。

用于向poll_table注册等待队列的关键poll_wait() 函数的原型如下

void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);

把当前进程添加到wait参数指定的等待列表(poll_table) 中, 实际作用是让唤醒参数queue对应的等待队列可以唤醒因select() 而睡眠的进程

驱动程序poll() 函数应该返回设备资源的可获取状态, 即POLLIN、 POLLOUT、 POLLPRI、POLLERR、 POLLNVAL等宏的位“或”结果。 每个宏的含义都表明设备的一种状态, 如POLLIN(定义为0x0001) 意味着设备可以无阻塞地读, POLLOUT(定义为0x0004) 意味着设备可以无阻塞地写。

函数模板

5、轮询操作的globalfifo驱动

globalfifo设备驱动的poll() 函数

static unsigned int globalfifo_poll(struct file *filp, poll_table * wait)
{
	unsigned int mask = 0;
	struct globalfifo_dev *dev = filp->private_data;

	mutex_lock(&dev->mutex);;

	poll_wait(filp, &dev->r_wait, wait);
	poll_wait(filp, &dev->w_wait, wait);

	if (dev->current_len != 0) {
		mask |= POLLIN | POLLRDNORM;
	}

	if (dev->current_len != GLOBALFIFO_SIZE) {
		mask |= POLLOUT | POLLWRNORM;
	}

	mutex_unlock(&dev->mutex);;
	return mask;
}

接口注册

用select() 监控globalfifo的可读写状态

#define FIFO_CLEAR 0x1
#define BUFFER_LEN 20
void main(void)
{
	int fd, num;
	char rd_ch[BUFFER_LEN];
	fd_set rfds, wfds; /* 读/写文件描述符集 */

	/* 以非阻塞方式打开/dev/globalfifo设备文件 */
	fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
	if (fd != -1) {
	/* FIFO清0 */
	if (ioctl(fd, FIFO_CLEAR, 0) < 0)
		printf("ioctl command failed\n");

	while (1) {
		FD_ZERO(&rfds);
		FD_ZERO(&wfds);
		FD_SET(fd, &rfds);
		FD_SET(fd, &wfds);

		select(fd + 1, &rfds, &wfds, NULL, NULL);
		/* 数据可获得 */
		if (FD_ISSET(fd, &rfds))
			printf("Poll monitor:can be read\n");
		/* 数据可写入 */
		if (FD_ISSET(fd, &wfds))
			printf("Poll monitor:can be written\n");
			}
		} else {
			printf("Device open failure\n");
	}
}

用epoll监控globalfifo的可读状态

#define FIFO_CLEAR 0x1
#define BUFFER_LEN 20
void main(void)
{
	int fd;

	fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
	if (fd != -1) {
		struct epoll_event ev_globalfifo;
		int err;
		int epfd;
		if (ioctl(fd, FIFO_CLEAR, 0) < 0)
		printf("ioctl command failed\n");

		epfd = epoll_create(1);
		if (epfd < 0) {
			perror("epoll_create()");
			return;
		}

		bzero(&ev_globalfifo, sizeof(struct epoll_event));
		ev_globalfifo.events = EPOLLIN | EPOLLPRI;

		err = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev_globalfifo);
		if (err < 0) {
			perror("epoll_ctl()");
		return;
		}
		err = epoll_wait(epfd, &ev_globalfifo, 1, 15000);
		if (err < 0) {
			perror("epoll_wait()");
		} else if (err == 0) {
			printf("No data input in FIFO within 15 seconds.\n");
		} else {
			printf("FIFO is not empty\n");
		}
		err = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev_globalfifo);
		if (err < 0)
			perror("epoll_ctl()");
		} else {
			printf("Device open failure\n");
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值