首页 > 代码库 > 《linux设备驱动开发详解》笔记——8阻塞与非阻塞IO

《linux设备驱动开发详解》笔记——8阻塞与非阻塞IO

 

8.1 阻塞与非阻塞IO

8.1.0 概述

  • 阻塞:访问设备时,若不能获取资源,则进程挂起,进入睡眠状态;也就是进入等待队列

技术分享技术分享

 

 

  • 非阻塞:不能获取资源时,不睡眠,要么退出、要么一直查询;直接退出且无资源时,返回-EAGAIN

技术分享

技术分享

  • 阻塞进程的唤醒:必须有地方能够唤醒处于睡眠状态的阻塞进程,否则就真睡不醒了。一般是在中断中。
  • 阻塞与非阻塞可以在open时设置,也可以通过fcntl和ioctl重新设置

8.1.1 等待队列

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

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

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

【注】
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函数把这个元素添加到队列或从队列删除。

技术分享

 

重要的等待队列模板(假设head_t已经定义好了):

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

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))

8.1.2 支持阻塞等待的驱动

 把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;


int globalmem_open(struct inode * inode, struct file * filp)
{
    filp->private_data = http://www.mamicode.com/&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问题

8.1.3 在用户空间验证阻塞驱动

运行结果:打开两个终端窗口,在一个里面用echo写入FIFO,两一个cat实时显示。

终端1:

:~$ sudo cat /dev/globalmem &
:[1] 8881
~$ 
success
nb

终端2:
sudo echo "success" > /dev/globalmem 
bash: /dev/globalmem: 权限不够
:~$ sudo su                 // 必须切换到root,否则没有权限写
[sudo] 密码: 
root@***# echo success > /dev/globalmem 
root@***# echo nb > /dev/globalmem 

 

8.2 轮询操作

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

  技术分享

 

8.2.1 用户空间轮询编程

#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. */
};

 

#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;
};

 

! 8.2.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;
}

 

8.2.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,
};

 

8.2.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);
}

 

终端1执行应用程序:
1
.初始状态,只显示可写 Poll monitor:can be write ..... 2.执行命令1后,可读可写 Poll monitor:can be write Poll monitor:can be read .... 3.执行命令2后,又变回可写,不可读了 Poll monitor:can be write .....
终端2读写globalmem # echo
"test" > /dev/globalmem   // 命令1 # cat /dev/globalmem        // 命令2 test

8.3 总结

《linux设备驱动开发详解》笔记——8阻塞与非阻塞IO