Linux 设备驱动I/O分析1(基于Linux6.6)---阻塞和非阻塞I/O分析
一、前言
在Unix/Linux下共有五种I/O模型,分别是:
1. 阻塞 I/O (Blocking I/O)
- 概述:在这种模型下,当应用程序执行 I/O 操作(如读取或写入数据)时,进程会被挂起,直到 I/O 操作完成为止。也就是说,调用 I/O 操作的进程会在没有获取数据的情况下进入等待状态,直到 I/O 操作成功返回结果。
- 优点:实现简单,易于理解和使用。
- 缺点:进程会被阻塞,无法执行其他任务,降低了效率。
典型场景:简单的串行化操作,应用程序不需要并发处理。
例子:read()
和 write()
在默认情况下属于阻塞 I/O。
2. 非阻塞 I/O (Non-blocking I/O)
- 概述:在这种模型下,I/O 操作会立即返回,如果没有数据可读或无法写入数据,返回一个错误(通常是
EAGAIN
或EWOULDBLOCK
)。进程可以继续执行其他操作,而不是阻塞等待 I/O 操作完成。 - 优点:进程不需要等待 I/O 操作完成,可以继续做其他事情,适合并发处理。
- 缺点:需要不断检查是否有 I/O 数据可用,这可能会导致轮询,消耗 CPU 资源。
典型场景:需要同时处理多个任务,且每个任务的数据可能在不同时间到达。
例子:通过 fcntl()
设置文件描述符为非阻塞模式。
3. I/O 多路复用 (I/O Multiplexing)
- 概述:这种模型允许单个进程同时监视多个 I/O 流(如多个套接字或文件描述符)。应用程序可以使用系统调用(如
select()
、poll()
或epoll()
)来检查多个文件描述符的状态,知道哪些文件描述符准备好进行读写操作。通过这种方式,应用程序可以避免每个文件描述符的轮询,从而提高效率。 - 优点:通过单个线程或进程处理多个连接,节省了系统资源。
- 缺点:轮询机制可能导致性能问题,尤其是在大量文件描述符时。
典型场景:高并发网络服务器,如 Web 服务器、聊天服务器等。
例子:select()
、poll()
、epoll()
。
4. 信号驱动 I/O (Signal-driven I/O)
- 概述:信号驱动 I/O 允许进程在 I/O 操作完成时收到信号。进程在发起 I/O 操作后可以继续执行其他任务,当 I/O 操作完成时,内核会通过发送一个信号(通常是
SIGIO
)来通知进程,进程随后可以处理数据。 - 优点:进程不需要轮询文件描述符,而是被通知何时可以进行 I/O 操作。
- 缺点:信号的处理有时可能会导致复杂的程序逻辑,因为信号处理程序不能进行复杂的操作。
典型场景:需要响应 I/O 操作完成的事件,适用于处理少量并发 I/O 操作的应用程序。
例子:使用 sigaction()
配置文件描述符的异步通知。
5. 异步 I/O (Asynchronous I/O)
- 概述:在异步 I/O 模型中,应用程序发起 I/O 操作后,立即返回,操作系统会在后台完成 I/O 操作。进程可以在此期间继续执行其他任务。当 I/O 操作完成时,操作系统会通知应用程序(通常通过回调机制或某种形式的通知机制)。进程不需要等待,也不需要轮询,完全解耦了 I/O 操作与主业务逻辑的执行。
- 优点:完全非阻塞,无需轮询,并且没有信号干扰,能充分利用多核 CPU 和系统资源。
- 缺点:实现较为复杂,需要操作系统支持,且通常会涉及到回调函数等异步处理机制。
典型场景:高效的网络服务器或需要大量并发 I/O 的应用程序,适合于大规模数据处理和高吞吐量场景。
例子:在 Linux 中,aio_read()
和 aio_write()
是实现异步 I/O 的系统调用。
总结
模型 | 特点 | 使用场景 |
---|---|---|
阻塞 I/O | 调用会阻塞直到操作完成 | 简单的串行化应用 |
非阻塞 I/O | 调用立即返回,如果没有数据则返回错误代码 | 需要并发处理多个 I/O 流 |
I/O 多路复用 | 通过 select() 、poll() 或 epoll() 监视多个 I/O 流 | 高并发网络服务(如 Web 服务器) |
信号驱动 I/O | 通过信号通知进程何时 I/O 操作完成 | 低延迟通知 I/O 完成的应用程序 |
异步 I/O | 操作系统在后台处理 I/O,进程无需等待 | 大规模并发 I/O,要求高效处理 |
先学习阻塞I/O、非阻塞I/O 、I/O复用(select和poll),先学习一下基础概念
a -- 阻塞
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程知道满足可操作的条件后再进行操作;被挂起的进程进入休眠状态(放弃CPU),被从调度器的运行队列移走,直到等待的条件被满足;
b -- 非阻塞
非阻塞的进程在不能进行设备操作时,并不挂起(继续占用CPU),它或者放弃,或者不停地查询,直到可以操作为止;
驱动程序通常需要提供这样的能力:当应用程序进行 read()、write() 等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的xxx_read()、xxx_write() 等操作中将进程阻塞直到资源可以获取,此后,应用程序的 read()、write() 才返回,整个过程仍然进行了正确的设备 访问,用户并没感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的 xxx_read()、xxx_write() 等操作立刻返回, read()、write() 等系统调用也随即被返回。
因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的挂了。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。阻塞I/O通常由等待队列来实现,而非阻塞I/O由轮询来实现。
二、阻塞I/O实现 —— 等待队列
2.1、基础概念
在Linux 驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。wait queue 很早就作为一个基本的功能单位出现在Linux 内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,上一篇文章所述的信号量在内核中也依赖等待队列来实现。
在Linux内核中使用等待队列的过程很简单,首先定义一个wait_queue_head,然后如果一个task想等待某种事件,那么调用wait_event(等待队列,事件)就可以了。
等待队列应用广泛,但是内核实现却十分简单。其涉及到两个比较重要的数据结构:__wait_queue_head,该结构描述了等待队列的链头,其包含一个链表和一个原子锁,结构定义如下: include/linux/wait.h
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
__wait_queue,该结构是对一个等待任务的抽象。每个等待任务都会抽象成一个wait_queue,并且挂载到wait_queue_head上。该结构定义如下:
struct __wait_queue
{
unsigned int flags;
void *private; /* 通常指向当前任务控制块 */
/* 任务唤醒操作方法,该方法在内核中提供,通常为autoremove_wake_function */
wait_queue_func_t func;
struct list_head task_list; /* 挂入wait_queue_head的挂载点 */
};
Linux中等待队列的实现思想如下图所示,当一个任务需要在某个wait_queue_head上睡眠时,将自己的进程控制块信息封装到wait_queue中,然后挂载到wait_queue的链表中,执行调度睡眠。当某些事件发生后,另一个任务(进程)会唤醒wait_queue_head上的某个或者所有任务,唤醒工作也就是将等待队列中的任务设置为可调度的状态,并且从队列中删除。
使用等待队列时首先需要定义一个wait_queue_head,这可以通过DECLARE_WAIT_QUEUE_HEAD宏来完成,这是静态定义的方法。该宏会定义一个wait_queue_head,并且初始化结构中的锁以及等待队列。当然,动态初始化的方法也很简单,初始化一下锁及队列就可以了。
一个任务需要等待某一事件的发生时,通常调用wait_event,该函数会定义一个wait_queue,描述等待任务,并且用当前的进程描述块初始化wait_queue,然后将wait_queue加入到wait_queue_head中。
#define DECLARE_WAIT_QUEUE_HEAD(name) \
struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
extern void __init_waitqueue_head(struct wait_queue_head *wq_head, const char *name, struct lock_class_key *);
函数实现流程说明如下:
a -- 用当前的进程描述块(PCB)初始化一个wait_queue描述的等待任务。
b -- 在等待队列锁资源的保护下,将等待任务加入等待队列。
c -- 判断等待条件是否满足,如果满足,那么将等待任务从队列中移出,退出函数。
d -- 如果条件不满足,那么任务调度,将CPU资源交与其它任务。
e -- 当睡眠任务被唤醒之后,需要重复b、c 步骤,如果确认条件满足,退出等待事件函数。
2.2、等待队列接口函数
1、定义并初始化
/* 定义“等待队列头” */
wait_queue_head_t my_queue;
/* 初始化“等待队列头”*/
init_waitqueue_head(&my_queue);
直接定义并初始化。init_waitqueue_head()函数会将自旋锁初始化为未锁,等待队列初始化为空的双向循环链表。
DECLARE_WAIT_QUEUE_HEAD(my_queue); 定义并初始化,可以作为定义并初始化等待队列头的快捷方式。
2、定义等待队列:
DECLARE_WAITQUEUE(name,tsk);
定义并初始化一个名为name的等待队列。
2.3、(从等待队列头中)添加/移出等待队列:
/* add_wait_queue()函数,设置等待的进程为非互斥进程,并将其添加进等待队列头(q)的队头中*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
/* 该函数也和add_wait_queue()函数功能基本一样,只不过它是将等待的进程(wait)设置为互斥进程。*/
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait);
2.4、等待事件:
(1)wait_event()宏:
include/linux/wait.h
/**
* wait_event - sleep until a condition gets true
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_UNINTERRUPTIBLE) until the
* @condition evaluates to true. The @condition is checked each time
* the waitqueue @wq is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*/
#define wait_event(wq_head, condition) \
do { \
might_sleep(); \
if (condition) \
break; \
__wait_event(wq_head, condition); \
} while (0)
在等待会列中睡眠直到condition为真。在等待的期间,进程会被置为TASK_UNINTERRUPTIBLE进入睡眠,直到condition变量变为真。每次进程被唤醒的时候都会检查condition的值。
(2)wait_event_interruptible()函数
和wait_event()的区别是调用该宏在等待的过程中当前进程会被设置为TASK_INTERRUPTIBLE状态.在每次被唤醒的时候,首先检查condition是否为真,如果为真则返回,否则检查如果进程是被信号唤醒,会返回-ERESTARTSYS错误码.如果是condition为真,则返回0。
(3)wait_event_timeout()宏
也与wait_event()类似.不过如果所给的睡眠时间为负数则立即返回.如果在睡眠期间被唤醒,且condition为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回0。
(4)wait_event_interruptible_timeout()宏
与wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回ERESTARTSYS错误码。
(5) wait_event_interruptible_exclusive()宏
同样和wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程。
2.5、唤醒队列
(1)wake_up()函数
include/linux/wait.h
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
/**
* __wake_up - wake up threads blocked on a waitqueue.
* @q: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*/
void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(__wake_up);
唤醒等待队列.可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERUPTIBLE状态的进程,和wait_event/wait_event_timeout成对使用. (2)wake_up_interruptible()函数:
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
和wake_up()唯一的区别是它只能唤醒TASK_INTERRUPTIBLE状态的进程.,与wait_event_interruptible/wait_event_interruptible_timeout/ wait_event_interruptible_exclusive成对使用。
下面看一个实例:
static ssize_t hello_read(struct file *filep, char __user *buf, size_t len, loff_t *pos)
{
/*
实现应用进程read的时候,如果没有数据就阻塞
*/
if(len>64)
{
len =64;
}
wait_event_interruptible(wq, have_data == 1);
if(copy_to_user(buf,temp,len))
{
return -EFAULT;
}
have_data = 0;
return len;
}
static ssize_t hello_write(struct file *filep, const char __user *buf, size_t len, loff_t *pos)
{
if(len > 64)
{
len = 64;
}
if(copy_from_user(temp,buf,len))
{
return -EFAULT;
}
printk("write %s\n",temp);
have_data = 1;
wake_up_interruptible(&wq);
return len;
}
注意两个概念:
a -- 疯狂兽群
wake_up的时候,所有阻塞在队列的进程都会被唤醒,但是因为condition的限制,只有一个进程得到资源,其他进程又会再次休眠,如果数量很大,称为 疯狂兽群。
b -- 独占等待
等待队列的入口设置一个WQ_FLAG_EXCLUSIVE标志,就会添加到等待队列的尾部,没有设置设置的添加到头部,wake up的时候遇到第一个具有WQ_FLAG_EXCLUSIVE这个标志的进程就停止唤醒其他进程。
三、非阻塞I/O实现方式 —— 多路复用
3.1、轮询的概念和作用
在用户程序中,select() 和 poll() 也是设备阻塞和非阻塞访问息息相关的论题。使用非阻塞I/O的应用程序通常会使用select() 和 poll() 系统调用查询是否可对设备进行无阻塞的访问。select() 和 poll() 系统调用最终会引发设备驱动中的 poll()函数被执行。
3.2、应用程序中的轮询编程
在用户程序中,select()和poll()本质上是一样的, 不同只是引入的方式不同,前者是在BSD UNIX中引入的,后者是在System V中引入的。用的比较广泛的是select系统调用。原型如下:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptionfds, struct timeval *timeout);
其中readfs,writefds,exceptfds分别是select()监视的读,写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1,timeout则是一个时间上限值,超过该值后,即使仍没有描述符准备好也会返回。
struct timeval
{
int tv_sec; //秒
int tv_usec; //微秒
}
涉及到文件描述符集合的操作主要有以下几种:
1)清除一个文件描述符集 FD_ZERO(fd_set *set);
2)将一个文件描述符加入文件描述符集中 FD_SET(int fd,fd_set *set);
3)将一个文件描述符从文件描述符集中清除 FD_CLR(int fd,fd_set *set);
4)判断文件描述符是否被置位 FD_ISSET(int fd,fd_set *set);
最后我们利用上面的文件描述符集的相关来写个验证添加了设备轮询的驱动,把上边两块联系起来
3.3、设备驱动中的轮询编程
设备驱动中的poll() 函数原型如下
unsigned int(*poll)(struct file *filp, struct poll_table * wait);
第一个参数是file结构体指针,第二个参数是轮询表指针,poll设备方法完成两件事:
a -- 对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table,如果没有文件描述符可用来执行 I/O, 则内核使进程在传递到该系统调用的所有文件描述符对应的等待队列上等待。
b -- 返回表示是否能对设备进行无阻塞读、写访问的掩码。
位掩码:POLLRDNORM, POLLIN,POLLOUT,POLLWRNORM
设备可读,通常返回:(POLLIN | POLLRDNORM)
设备可写,通常返回:(POLLOUT | POLLWRNORM)
poll_wait()函数:用于向 poll_table注册等待队列
void poll_wait(struct file *filp, wait_queue_head_t *queue,poll_table *wait)
poll_wait()函数不会引起阻塞,它所做的工作是把当前进程添加到wait 参数指定的等待列表(poll_table)中。
真正的阻塞动作是上层的select/poll函数中完成的。select/poll会在一个循环中对每个需要监听的设备调用它们自己的poll支持函数以使得当前进程被加入各个设备的等待列表。若当前没有任何被监听的设备就绪,则内核进行调度(调用schedule)让出cpu进入阻塞状态,schedule返回时将再次循环检测是否有操作可以进行,如此反复;否则,若有任意一个设备就绪,select/poll都立即返回。
具体过程如下:
a -- 用户程序第一次调用select或者poll,驱动调用poll_wait并使两条队列都加入poll_table结构中作为下次调用驱动函数poll的条件,一个mask返回值指示设备是否可操作,0为未准备状态,如果文件描述符未准备好可读或可写,用户进程被会加入到写或读等待队列中进入睡眠状态。
b -- 当驱动执行了某些操作,例如,写缓冲或读缓冲,写缓冲使读队列被唤醒,读缓冲使写队列被唤醒,于是select或者poll系统调用在将要返回给用户进程时再次调用驱动函数poll,驱动依然调用poll_wait 并使两条队列都加入poll_table结构中,并判断可写或可读条件是否满足,如果mask返回POLLIN | POLLRDNORM或POLLOUT | POLLWRNORM则指示可读或可写,这时select或poll真正返回给用户进程,如果mask还是返回0,则系统调用select或poll继续不返回。
下面是一个典型模板:
static unsigned int XXX_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0;
struct XXX_dev *dev = filp->private_data; //获得设备结构指针
...
poll_wait(filp, &dev->r_wait, wait); //加读等待对列头
poll_wait(filp ,&dev->w_wait, wait); //加写等待队列头
if(...)//可读
{
mask |= POLLIN | POLLRDNORM; //标识数据可获得
}
if(...)//可写
{
mask |= POLLOUT | POLLWRNORM; //标识数据可写入
}
..
return mask;
}
3.4、调用过程:
Linux下select调用的过程:
1、用户层应用程序调用select(),底层调用poll())
2、核心层调用sys_select() ------> do_select()
最终调用文件描述符fd对应的struct file类型变量的struct file_operations *f_op的poll函数。
poll指向的函数返回当前可否读写的信息。
1)如果当前可读写,返回读写信息。
2)如果当前不可读写,则阻塞进程,并等待驱动程序唤醒,重新调用poll函数,或超时返回。
3、驱动需要实现poll函数
当驱动发现有数据可以读写时,通知核心层,核心层重新调用poll指向的函数查询信息。
poll_wait(filp,&wait_q,wait) // 此处将当前进程加入到等待队列中,但并不阻塞
在中断中使用wake_up_interruptible(&wait_q)唤醒等待队列。
4、实例分析
1、memdev.h
/*mem设备描述结构体*/
struct mem_dev
{
char *data;
unsigned long size;
wait_queue_head_t inq;
};
#endif /* _MEMDEV_H_ */
2、驱动程序 memdev.c
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <linux/poll.h>
#include "memdev.h"
static mem_major = MEMDEV_MAJOR;
bool have_data = false; /*表明设备有足够数据可供读*/
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*设备结构体指针*/
struct cdev cdev;
/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;
return 0;
}
/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
while (!have_data) /* 没有数据可读,考虑为什么不用if,而用while */
{
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
wait_event_interruptible(dev->inq,have_data);
}
/*读数据到用户空间*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}
have_data = false; /* 表明不再有数据可读 */
/* 唤醒写进程 */
return ret;
}
/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
/*从用户空间写入数据*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
have_data = true; /* 有新的数据可读 */
/* 唤醒读进程 */
wake_up(&(dev->inq));
return ret;
}
/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case 0: /* SEEK_SET */
newpos = offset;
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + offset;
break;
case 2: /* SEEK_END */
newpos = MEMDEV_SIZE -1 + offset;
break;
default: /* can't happen */
return -EINVAL;
}
if ((newpos<0) || (newpos>MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
unsigned int mem_poll(struct file *filp, poll_table *wait)
{
struct mem_dev *dev = filp->private_data;
unsigned int mask = 0;
/*将等待队列添加到poll_table */
poll_wait(filp, &dev->inq, wait);
if (have_data) mask |= POLLIN | POLLRDNORM; /* readable */
return mask;
}
/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
.poll = mem_poll,
};
/*设备驱动模块加载函数*/
static int memdev_init(void)
{
int result;
int i;
dev_t devno = MKDEV(mem_major, 0);
/* 静态申请设备号*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 动态分配设备号 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno);
}
if (result < 0)
return result;
/*初始化cdev结构*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &mem_fops;
/* 注册字符设备 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/* 为设备描述结构分配内存*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));
/*为设备分配内存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
/*初始化等待队列*/
init_waitqueue_head(&(mem_devp[i].inq));
//init_waitqueue_head(&(mem_devp[i].outq));
}
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
kfree(mem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}
MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
3、应用程序 app-write.c
#include <stdio.h>
int main()
{
FILE *fp = NULL;
char Buf[128];
/*打开设备文件*/
fp = fopen("/dev/memdev0","r+");
if (fp == NULL)
{
printf("Open Dev memdev Error!\n");
return -1;
}
/*写入设备*/
strcpy(Buf,"memdev is char dev!");
printf("Write BUF: %s\n",Buf);
fwrite(Buf, sizeof(Buf), 1, fp);
sleep(5);
fclose(fp);
return 0;
}
4、应用程序 app-read.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>
int main()
{
int fd;
fd_set rds;
int ret;
char Buf[128];
/*初始化Buf*/
strcpy(Buf,"memdev is char dev!");
printf("BUF: %s\n",Buf);
/*打开设备文件*/
fd = open("/dev/memdev0",O_RDWR);
FD_ZERO(&rds);
FD_SET(fd, &rds);
/*清除Buf*/
strcpy(Buf,"Buf is NULL!");
printf("Read BUF1: %s\n",Buf);
ret = select(fd + 1, &rds, NULL, NULL, NULL);
if (ret < 0)
{
printf("select error!\n");
exit(1);
}
if (FD_ISSET(fd, &rds))
read(fd, Buf, sizeof(Buf));
/*检测结果*/
printf("Read BUF2: %s\n",Buf);
close(fd);
return 0;
}
四、总结
1. 阻塞 I/O (Blocking I/O)
概述:
阻塞 I/O 是最简单的 I/O 模式。在阻塞 I/O 模式下,当进程执行 I/O 操作(如读取或写入数据)时,如果操作无法立即完成(例如设备没有数据可读或者设备忙),进程会被挂起,直到 I/O 操作完成为止。
工作原理:
- 当进程调用
read()
、write()
或类似的系统调用时,如果设备的状态不允许立即完成 I/O 操作,调用会将进程挂起。 - 进程在 I/O 操作完成后被唤醒,接着可以继续执行后续的任务。
优点:
- 实现简单:开发人员只需处理阻塞操作的 I/O 逻辑,代码较为简洁。
- 同步性:代码执行顺序清晰,容易理解。
缺点:
- 效率较低:如果 I/O 操作无法立即完成,进程会一直等待,浪费 CPU 资源,特别是在高并发场景下,多个进程同时阻塞会导致效率低下。
- 响应时间长:进程需要等待 I/O 完成,可能会导致较高的延迟。
典型应用场景:
- 单线程、低并发的设备操作,如简单的串口通讯、文件系统 I/O。
- 对实时性要求不高的应用。
阻塞 I/O 驱动实现的示例:
在设备驱动中,通常会在 file_operations
结构体中的 read
和 write
函数中实现阻塞行为。例如,内核会等待设备准备好数据后再返回。
ssize_t my_device_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
// 阻塞直到设备有数据可以读取
if (device_not_ready()) {
wait_event_interruptible(my_device_wait_queue, device_is_ready());
}
// 读取数据
copy_to_user(buf, device_buffer, count);
return count;
}
2. 非阻塞 I/O (Non-blocking I/O)
概述:
非阻塞 I/O 是一种允许进程立即返回并检查 I/O 操作是否可以继续执行的模式。如果设备准备好进行 I/O 操作,系统调用会立即返回;如果设备未准备好(如没有数据可读或设备忙),则返回一个错误(通常是 EAGAIN
或 EWOULDBLOCK
),进程可以继续执行其他任务。
工作原理:
- 当进程发起 I/O 操作时,设备驱动不会让进程阻塞。如果设备当前无法满足操作(例如,读取时设备没有数据可用),系统调用会立刻返回一个错误代码。
- 进程可以通过其他方式(如轮询、使用
select()
/poll()
/epoll()
等机制)检查设备的状态,知道设备是否准备好后,再执行 I/O 操作。
优点:
- 高效:进程不会被挂起,可以在等待 I/O 操作的同时继续执行其他任务,适合高并发应用。
- 提高吞吐量:在高并发或大量数据处理场景下,非阻塞 I/O 可以避免不必要的等待,增加系统的响应能力。
缺点:
- 复杂性增加:开发者需要处理状态检查、轮询、等待机制等,代码比阻塞模式复杂。
- 需要额外的资源管理:程序需要处理没有数据时的错误返回,可能会消耗更多的 CPU 时间。
典型应用场景:
- 高并发网络服务,特别是需要同时处理多个设备或套接字的服务器应用,如 Web 服务器、数据库服务器等。
- 实时响应的系统,尤其是在高吞吐量的环境下。
非阻塞 I/O 驱动实现的示例:
在设备驱动中,通常通过设置设备文件的标志为非阻塞模式,并在 read
和 write
函数中返回 -EAGAIN
错误代码,来实现非阻塞 I/O。例如,使用 file_operations
中的非阻塞读写:
ssize_t my_device_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
// 如果设备没有准备好数据,返回 EAGAIN 错误
if (!device_has_data()) {
return -EAGAIN;
}
// 读取数据
copy_to_user(buf, device_buffer, count);
return count;
}
3.阻塞与非阻塞 I/O 的区别总结:
特性 | 阻塞 I/O | 非阻塞 I/O |
---|---|---|
执行方式 | 调用会阻塞,直到 I/O 操作完成 | 调用立即返回,若操作不可完成,返回错误码 |
进程行为 | 如果 I/O 无法完成,进程会被挂起 | 进程不会挂起,立即返回并处理其他任务 |
效率 | 在 I/O 操作未完成时,效率较低 | 高效,进程不需要等待,可以做其他事情 |
复杂性 | 实现简单,代码清晰易懂 | 实现较为复杂,需要额外的状态检查和轮询机制 |
适用场景 | 单线程、低并发、对实时性要求不高的应用 | 高并发、需要同时处理多个设备或任务的应用 |
示例 | 串口通信、简单设备读取 | 高并发网络服务器、数据库操作等 |