Linux Kernel编程 --- 设备驱动中的阻塞与非阻塞IO

本文档参考宋宝华老师的《linux设备驱动开发详解》

1 概述

  • 阻塞:访问设备时,若不能获取资源,则进程挂起,进入睡眠状态;也就是进入等待队列,直到满足的条件被满足为止;
  • 非阻塞:不能获取资源时,不睡眠,要么退出、要么一直查询;直到可以进行操作为止,直接退出且无资源时,返回-EAGAIN

如图所示,

阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源让给其它进程。阻塞的进程会进入休眠状态,所以必须确保有一个地方可以唤醒休眠的进程。一般在中断中唤醒。

非阻塞进程则是通过不断的尝试,直到可以进行IO;

在设备驱动编程中,阻塞IO一般基于wait_queue或基于wait_queue的其它内核对象,wait_queue可以用于同步驱动中事件发生的先后顺序。

使用非阻塞IO 的应用程序可以使用轮询函数来查询设备是否可以立即被访问,用户空间调用select()\poll()接口,设备驱动提供poll()函数

设备驱动的poll()本身不会阻塞,但是与poll()/select()相关的系统调用则会阻塞的等待至少一个文件描述符集合可以访问或超时。

 

2 等待队列

linux驱动中,可以用等待队列wait queue实现阻塞。等待队列与linux进程调度紧密结合,理解等待队列,对理解linux调度很有帮助。

使用等待队列,大体由一下三个方面构成:

  • 初始化init,组织等待队列
  • 睡,schedule,加入等待队列以后,可以主动触发内核调度其他进程,从而进入睡眠
  • 醒,wake up,唤醒等待队列里的进程(第一步本进程已经加入了该等待队列)

2.1 等待队列API

#include <linux/wait.h>

/**** 定义和初始化等待队列 *****/
// head相关
wait_queue_head_t my_queue;        // 1.定义“等待队列head”
init_waitqueue_head( &my_queue );     // 2.初始化该“等待队列head”
DECLARE_WAIT_QUEUE_HEAD(&my_queue );   // 3.相当于1+2的效果,定义+初始化

// 定义等待队列元素
DECLARE_WAITQUEUE( wait,tsk);       // 定义1个等待队列元素,与进程tsk挂钩;tsk是进程,一般用current,表示当前进程,对应task_struct;

// 添加删除等待队列元素,即把1个具体的等待队列添加都head所在的链表里
void add_wait_queue( wait_queue_head_t *q, wait_queue_t *wait );
void remove_wait_queue( wait_queue_head_t *q, wait_queue_t *wait );

/**** 等待事件,不一定需要调用,linux内核有些驱动也调用了 ****/
wait_event( my_queue, condition );            // 等待my_queue的队列被唤醒,同时condition必须满足,否则继续阻塞
wait_event_interruptible( my_queue,condition );     // 可被信号打断
wait_event_timeout( my_queue, condition, timeout );  // timeout到时后,不论condition是否满足,均返回
wait_event_timeout_interruptible( my_queue, condition, timeout );

/**** 唤醒 ****/
void wake_up( wait_queue_head_t * queue );          // 能唤醒TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE两种状态的进程
void wake_up_interruptible( wait_queue_head_t * queue );  // 只能唤醒处于TASK_INTERRUPTIBLE状态的进程
void wake_up_interruptible_timeout( wait_queue_head_t * queue, timeout);  // 只能唤醒处于TASK_INTERRUPTIBLE状态的进程

【注】
wait和wake_up,是否带_interruptible要成对儿使用。

/* 在等待队列上睡眠,老接口,不建议用,会产生race */
sleep_on( wait_queue_head_t *q );            // 干的事挺多,进程状态设置为TASK_UNINTERRUPTIBLE——>定义wait并加到head——>调度出去
interruptible_sleep_on( wait_queue_heat *q );

 如下图,等待队列采用链表形式,先要用 DECLARE_WAIT_QUEUE_HEAD()定义并初始化一个wait_queue_head,然后再定义1个队列元素wait_queue,再通过add/remove函数把这个元素添加到队列或从队列删除。

2.2 等待队列使用模板

#include <linux/wait.h>
#include <linux/sched.h>  // for __set_current_state()/schedule()...

DECLARE_WAIT_QUEUE_HEAD(&xxx_wait); //定义并初始化等待队列头

static ssize_t xxx_write( struct file *file, const char *buffer, size_t count, loff_t * ppos )
{
  ......
  DECLEAR_WAITQUEUE( wait, current );  // 定义等待队列元素,current代表当前进程的task_struct
  add_wait_queue( &xxx_wait,wait );    // 将等待队列元素wait加入到头部为xxx_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 ) ){          // interruptible是浅睡眠,可被信号打断,醒来时要判断是否是被信号打断的
        ret = -ERESTERTSYS;
        goto out;
      }
    }
  }while(avail<0)

  /* 写设备 */
  device_write(...);

out:
  remove_wait_queue( &xxx_wait,wait );
  set_current_state( TASK_RUNNING );
  return ret;
}



#include <asm-generic/current.h>    // current代表当前进程的task_struct

#define get_current() (current_thread_info()->task)
#define current get_current()

 

#include <linux/sched.h>

/*
* set_current_state() includes a barrier so that the write of current->state
* is correctly serialised wrt the caller's subsequent test of whether to
* actually sleep:
*
*	set_current_state(TASK_UNINTERRUPTIBLE);
*	if (do_i_need_to_sleep())
*	schedule();
*
* If the caller does not need such serialisation then use __set_current_state()
*/
#define __set_current_state(state_value)	\
do { current->state = (state_value); } while (0)
#define set_current_state(state_value)	\
set_mb(current->state, (state_value))

2.3 支持阻塞的驱动

把globalmem做成FIFO形式,满了再写就阻塞,空了再读就阻塞,读唤醒写,写唤醒读。

/*
    注意:1.用等待队列实现读写阻塞,把globalmem当成FIFO处理
 */


#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/mutex.h>
#include <linux/sched.h>

#define DEV_NAME    "globalmem"

#define GLOBALMEN_LEN    1024

struct globalmem_dev_t
{
    struct cdev cdev;
    struct class * class;
    dev_t  dev_no;
    wait_queue_head_t r_wait_head; //读队列头
    wait_queue_head_t w_wait_head; //写队列头
    struct mutex mutex;
    unsigned int curr_len;
    char buf[GLOBALMEN_LEN];
}globalmem_dev;
/*
在设备结构体中加入该设备操作时,所用到的内核对象,是Linux 设备驱动编写过程中常用的处理方式
*/


int globalmem_open(struct inode * inode, struct file * filp)
{
    filp->private_data = &globalmem_dev;
#if 0    // 不能放在这,因为每次写,例如echo命令,就会open一次,会重新初始化r_wait_head,导致read等待队列重新初始化,引起等待队列异常。 唉,基本常识都忘记了
    init_waitqueue_head(&globalmem_dev.r_wait_head);
    init_waitqueue_head(&globalmem_dev.w_wait_head);
    mutex_init(&globalmem_dev.mutex);
#endif
    printk("\r\nglobalmem open.");
    printk("\r\nglobalmem open.");
    return 0;
}

ssize_t globalmem_read(struct file *filp, char __user * buf, size_t len, loff_t * pos)
{
    struct globalmem_dev_t * globalmem_devp;
    size_t len_rd;
    int ret;

    globalmem_devp = filp->private_data;

    DECLARE_WAITQUEUE(wait,current);

    mutex_lock(&globalmem_devp->mutex);
    add_wait_queue(&globalmem_devp->r_wait_head,&wait);
    while(globalmem_devp->curr_len==0)
    {
        // non-block 
        if( filp->f_flags&O_NONBLOCK )
        {
            ret = -EAGAIN;
            goto out;
        }

        printk("\r\nglobelmem read before schedule.");
        __set_current_state( TASK_INTERRUPTIBLE );
        mutex_unlock(&globalmem_devp->mutex);    // 睡前一定要解锁,否则可能引起死锁
        schedule();        // schudule    
        printk("\r\nglobelmem read after schedule.");
        if( signal_pending( current ) ){
            ret = -ERESTARTSYS;
            goto out2;        
        }
        printk("\r\nglobelmem after signal_pending.");
        mutex_lock(&globalmem_devp->mutex);    
    }

    if( len>globalmem_devp->curr_len )
        len_rd = globalmem_devp->curr_len;
    else
        len_rd = len;    

    if( copy_to_user(buf,globalmem_devp->buf,len_rd) ){
        ret=-EFAULT;
        goto out;
    }
    else{
        memcpy(globalmem_devp->buf,&globalmem_devp->buf[len_rd],globalmem_devp->curr_len-len_rd);
        globalmem_devp->curr_len-=len_rd; 
        printk(KERN_INFO"read %d bytes,current_len %d bytes.",len_rd,globalmem_devp->curr_len);
        wake_up_interruptible(&globalmem_devp->w_wait_head);    // 唤醒等待队列里的写
        ret = len_rd;
    }

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


ssize_t globalmem_write (struct file *filp, const char __user *buf, size_t len, loff_t *pos)
{
    struct globalmem_dev_t * globalmem_devp;
    size_t len_wr;
    int ret;

    printk("\r\nEnter glocalmem_write.");

    globalmem_devp = filp->private_data;
    DECLARE_WAITQUEUE(wait,current);

    mutex_lock(&globalmem_devp->mutex);
    add_wait_queue(&globalmem_devp->w_wait_head,&wait);
    while(globalmem_devp->curr_len==GLOBALMEN_LEN)
    {
        // non-block 
        if( filp->f_flags&O_NONBLOCK )
        {
            ret = -EAGAIN;
            goto out;
        }

        __set_current_state( TASK_INTERRUPTIBLE );
        mutex_unlock(&globalmem_devp->mutex);    // 睡前一定要解锁,否则可能引起死锁
        schedule();        // schudule    

        if( signal_pending( current ) ){
            ret = -ERESTARTSYS;
            goto out2;        
        }
        
        mutex_lock(&globalmem_devp->mutex);    
    }

    if( len>(GLOBALMEN_LEN-globalmem_devp->curr_len) )
        len_wr = GLOBALMEN_LEN - globalmem_devp->curr_len;
    else
        len_wr = len;    

    if( copy_from_user(globalmem_devp->buf+globalmem_devp->curr_len,buf,len_wr) ){
        ret=-EFAULT;
        goto out;
    }
    else{
        globalmem_devp->curr_len+=len_wr; 
        printk(KERN_INFO"write %d bytes,current_len %d bytes.",len_wr,globalmem_devp->curr_len);
        wake_up_interruptible(&globalmem_devp->r_wait_head);    // 唤醒等待队列里的写
        ret = len_wr;
    }
    

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


loff_t globalmem_llseek(struct file *filp, loff_t offset, int whence )
{
    loff_t ret;    // 注意要有返回值

    switch(whence){
    case SEEK_SET:
        if( offset < 0 )
            return -EINVAL;
        if( offset > GLOBALMEN_LEN )
            return -EINVAL;
        filp->f_pos = offset;
        ret = filp->f_pos;    
        break;
    case SEEK_CUR:
        if((filp->f_pos+offset)< 0 )
            return -EINVAL;
        if((filp->f_pos+offset)> GLOBALMEN_LEN )
            return -EINVAL;
        filp->f_pos += offset;
        ret = filp->f_pos;
        break;
    case SEEK_END:
        if((filp->f_pos+offset)< 0 )
            return -EINVAL;
        if((filp->f_pos+offset) > GLOBALMEN_LEN )
            return -EINVAL;
        filp->f_pos += (offset+GLOBALMEN_LEN);
        ret = filp->f_pos;
        break;
    default:
        return -EINVAL;
        break;
    }
    
    return ret;
}

int globalmem_release(struct inode * inode, struct file * filp)
{
    return 0;
}

struct file_operations globalmem_fops = {
    .owner = THIS_MODULE,
    .open = globalmem_open,
    .read = globalmem_read,
    .write = globalmem_write,
    .llseek = globalmem_llseek,
    .release = globalmem_release,
};

static int __init globalmem_init( void )
{
    int ret;

    printk("enter globalmem_init()\r\n");
    
    cdev_init(&globalmem_dev.cdev,&globalmem_fops);
    globalmem_dev.cdev.owner=THIS_MODULE;

    if( (ret=alloc_chrdev_region(&globalmem_dev.dev_no,0,1,DEV_NAME))<0 )
    {
        printk("alloc_chrdev_region err.\r\n");    
        return ret;
    }
    ret = cdev_add(&globalmem_dev.cdev,globalmem_dev.dev_no,1);
    if( ret )
    {
        printk("cdev_add err.\r\n");    
        return ret;
    }

    /*
         * $ sudo insmod globalmem.ko    如果使用class_create,insmod时会报错,dmesg查看内核log信息发现找不到class相关函数
     *   insmod: ERROR: could not insert module globalmem.ko: Unknown symbol in module
     *   $ dmesg   
     *    [ 5495.606920] globalmem: Unknown symbol __class_create (err 0)
     *    [ 5495.606943] globalmem: Unknown symbol class_destroy (err 0)
     *    [ 5495.607027] globalmem: Unknown symbol device_create (err 0)
     */    

    globalmem_dev.class = class_create( THIS_MODULE, DEV_NAME );
    device_create(globalmem_dev.class,NULL,globalmem_dev.dev_no,NULL,DEV_NAME);

    /* init mem and pos */
    memset(globalmem_dev.buf,0,GLOBALMEN_LEN);

    init_waitqueue_head(&globalmem_dev.r_wait_head);
    init_waitqueue_head(&globalmem_dev.w_wait_head);
    mutex_init(&globalmem_dev.mutex);
    

    return 0;
}


static void __exit globalmem_exit( void )
{
    printk("enter globalmem_exit()\r\n");
    unregister_chrdev_region(globalmem_dev.dev_no, 1);
    cdev_del(&globalmem_dev.cdev);
    device_destroy(globalmem_dev.class,globalmem_dev.dev_no);
    class_destroy(globalmem_dev.class);
}

module_init(globalmem_init);
module_exit(globalmem_exit);

MODULE_LICENSE("GPL");    // 不加此声明,会报上述Unknown symbol问题

3 轮询操作

对于无阻塞访问,应用程序一般要查询是否可以无阻塞的访问。一般用select/poll查询是否可无阻塞访问。

select/poll系统调用最终会使设备驱动中的poll()函数被执行。

  

3.1 用户空间轮询编程

1 应用使用select()编程
#include <sys/select.h>
/* Check the first NFDS descriptors each in READFDS (if not NULL) for read
readiness, in WRITEFDS (if not NULL) for write readiness, and in EXCEPTFDS
(if not NULL) for exceptional conditions. If TIMEOUT is not NULL, time out
after waiting the interval specified therein. Returns the number of ready
descriptors, or -1 for errors.

This function is a cancellation point and therefore not marked with
__THROW. */
extern int select (int __nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

__nfds:最大fd+1

 

#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)

#define	FD_CLR(fd, fdsetp)	__FD_CLR (fd, fdsetp)
#define	FD_ISSET(fd, fdsetp)	__FD_ISSET (fd, fdsetp)
#define	FD_ZERO(fdsetp)	__FD_ZERO (fdsetp)

/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
  __time_t tv_sec;	/* Seconds. */
  __suseconds_t tv_usec;	/* Microseconds. */
};

2 应用使用poll()编程

#include <poll.h>

/* Data structure describing a polling request. */
struct pollfd
{
  int fd;	/* File descriptor to poll. */
  short int events;	/* Types of events poller cares about. */
  short int revents;	/* Types of events that actually occurred. */
};


__BEGIN_DECLS

/* Poll the file descriptors described by the NFDS structures starting at
FDS. If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
an event to occur; if TIMEOUT is -1, block until an event occurs.
Returns the number of file descriptors with events, zero if timed out,
or -1 for errors.

This function is a cancellation point and therefore not marked with
__THROW. */
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

/* 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   // 可读非0优先级波段数据
#ifndef POLLWRNORM       // 与POLLOUT相同
#define POLLWRNORM	0x0100
#endif
#ifndef POLLWRBAND       // 可写非0优先级波段数据
#define POLLWRBAND	0x0200
#endif
#ifndef POLLMSG
#define POLLMSG	0x0400
#endif
#ifndef POLLREMOVE
#define POLLREMOVE	0x1000
#endif
#ifndef POLLRDHUP
#define POLLRDHUP 0x2000
#endif

#define POLLFREE	0x4000	/* currently only for epoll */

#define POLL_BUSY_LOOP	0x8000

struct pollfd {
    int fd;
    short events;
    short revents;
};

3.2 驱动轮询编程

原型:poll( struct file * filp, poll_table * wait )
要干两件事:
  • 对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头部添加到poll_table中。
  • 返回表示涉笔是否可无阻塞读、写访问的掩码

void poll_wait( struct file * filp, wait_queue_head_t * queue, poll_table * wait ); 此函数并不阻塞,只是将当前进程添加到等待列表 poll_table中。实际作用是可以让queue对应的等待队列可以唤醒因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 );    // 把具体驱动的等待队列头部加入到 poll_table中

  if( ... )    // 可读
    mask = POLLIN | POLLRDNORM;

  if( ... )    // 可写
    mask = POLLOUT | POLLWRNORM;

  ...

  return mask;
}

 

3.3 支持轮询的globalmem驱动

unsigned int globalmem_poll( struct file * filp, poll_table * wait )
{
    unsigned int mask=0;
    struct globalmem_dev_t * globalmem_devp;

    globalmem_devp = filp->private_data;

    mutex_lock( &globalmem_devp->mutex );
    poll_wait( filp, &globalmem_devp->r_wait_head, wait );
    poll_wait( filp, &globalmem_devp->w_wait_head, wait );

    if( globalmem_devp->curr_len != 0 )
        mask |= POLLIN | POLLRDNORM;

    if( globalmem_devp->curr_len != GLOBALMEN_LEN )
        mask |= POLLOUT | POLLWRNORM;
    mutex_unlock( &globalmem_devp->mutex );
    return mask;
}

struct file_operations globalmem_fops = {
    .owner = THIS_MODULE,
    .open = globalmem_open,
    .read = globalmem_read,
    .write = globalmem_write,
    .llseek = globalmem_llseek,
    .release = globalmem_release,
    .poll = globalmem_poll,
};

3.4 应用层编程

#include <stdio.h>    // printf
#include <stdlib.h>    // exit
#include <unistd.h>
#include <fcntl.h>    // open
#include <sys/select.h>

#define FILE_NAME    "/dev/globalmem"

int main(int args, char *argv[])
{
    int fd;
    fd_set rd_fd_set,wr_fd_set;    

    printf("\r\nstart.");

    // open file
    fd=open(FILE_NAME,O_RDONLY|O_NONBLOCK);
    if( fd<0 )    
    {
        printf("\r\nopen file err");
        exit(-1);
    }
    
        
    while(1)
    {
        FD_ZERO(&rd_fd_set);
        FD_ZERO(&wr_fd_set);
        FD_SET(fd,&rd_fd_set);
        FD_SET(fd,&wr_fd_set);
        
        select(fd+1,&rd_fd_set,&wr_fd_set,NULL,NULL);
        if( FD_ISSET(fd,&rd_fd_set) )
            printf("\r\nPoll monitor:can be read");
        if( FD_ISSET(fd,&wr_fd_set) )
            printf("\r\nPoll monitor:can be write");        
    }
    close(fd);
    exit(0);
}

 

 

 

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值