阻塞操作是指在执行设备操作时, 若不能获得资源, 则挂起进程直到满足可操作的条件后再进行操作。 被挂起的进程进入睡眠状态, 被从调度器的运行队列移走, 直到等待的条件被满足。
非阻塞操作的进程在不能进行设备操作时, 并不挂起, 它要么放弃, 要么不停地查询, 直至可以进行操作为止。
阻塞读取串口一个字符
非阻塞读取串口一个字符
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");
}
}