4.Linux设备驱动编程之阻塞与非阻塞

来源:百度文库 编辑:神马文学网 时间:2024/04/28 00:34:39

4.Linux设备驱动编程之阻塞与非阻塞

 

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

  在Linux驱动程序中,我们可以使用等待队列(wait queue)来实现阻塞操作。wait queue很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。等待队列可以用来同步对系统资源的访问,上节中所讲述Linux信号量在内核中也是由等待队列来实现的。

  下面我们重新定义设备"globalvar",它可以被多个进程打开,但是每次只有当一个进程写入了一个数据之后本进程或其它进程才可以读取该数据,否则一直阻塞。

#include
#include
#include
#include
#include
#include                        
MODULE_LICENSE("GPL");

#define MAJOR_NUM 254

static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);

struct file_operations globalvar_fops =
{
 read: globalvar_read,

write: globalvar_write,
};

static int global_var = 0;
static struct semaphore sem;
static wait_queue_head_t outq;              //
等待队列头在/include/linux/wait.h
static int flag = 0;                          struct_wait_queue_head{
                                             spinlock_t        lock;
static int __init globalvar_init(void)                 struct list_head    task_list;
{                                           }
 int ret;                               typedef struct_wait_queue_head  wait_queue_head_t;
 ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);  它包含一个自旋锁和一个   
 if (ret)                                           链表,这个链表是一个等待队列入口
 {          
  printk("globalvar register failure");
 }
 else
 {
  printk("globalvar register success");
  init_MUTEX(&sem);
  init_waitqueue_head(&outq);            //初始化等待对列头
 }                                    void init_waitqueue_head (wait_queue_head_t *q){
 return ret;                             spin_lock_init (&q->lock);
}                                      INIT_LIST_HEAD (&q->task_list);
                                        }
static void __exit globalvar_exit(void)    static inline void INIT_LIST_HEAD (struct list_head *list)
{                                      {
 int ret;                                           list->next =list;
 ret = unregister_chrdev(MAJOR_NUM, "globalvar");     list->prev=list
 if (ret)                                        }将整个队列设置为“未上锁的状态”,
 {                                        并将链表指针prevnext指向它自身。
  printk("globalvar unregister failure");
 }
 else
 {
  printk("globalvar unregister success");
 }
}

static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
 //等待数据可获得
 if (wait_event_interruptible(outq, flag != 0))  //wait_event_interruptible()函数调用时,在等待过程
 {                                 中当前进程会被设置为TASK_UNINTERRUPTIBLE
  return - ERESTARTSYS;            状态,在每次唤醒的时候,首先检查condition是否为
 }                                  真,如果为真则返回,否则检查如果进程是被信号唤
                                     醒,会返回- ERESTARTSYS;错误码,如果condition
 if (down_interruptible(&sem))          为真,则返回0
 {                                  源码#define _wait_event_interruptible(outq,codition,ret)
  return - ERESTARTSYS;         do{
 }                                   DEFINE_WAIT(_wait);
                                      for(;;){
 flag = 0;                         prepare_to_wait(&outq,&_wait,TASK_INTERRUPTIBLE);
 if (copy_to_user(buf, &global_var, sizeof(int)))  if(condition)
 {                                         break;
  up(&sem);                              if(!signal_pending(current)){  //检查当前是否
  return - EFAULT;                        schedule();      有信号处理,返回非0表示
 }                                        continue;       有信号处理,返回
 up(&sem);                                 }              - ERESTARTSYS表示信号
 return sizeof(int);                          break;}          函数处理完毕后重新执行信
}                                        finish_wait(&outq,&_wait);   号函数前的某个系
                                     }while(0)                      统调用。
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len,loff_t *off)
{
 if (down_interruptible(&sem))
 {
  return - ERESTARTSYS;
 }
 if (copy_from_user(&global_var, buf, sizeof(int)))
 {
  up(&sem);
  return - EFAULT;
 }
 up(&sem);
 flag = 1;                                  //flag就是为了判断condition用的
 //通知数据可获得
 wake_up_interruptible(&outq);                //只能唤醒TASK_INTERRUPTIBLE状态的
 return sizeof(int);                             进程,与wait_event_interruptible()成对使用
}

module_init(globalvar_init);
module_exit(globalvar_exit);

 


  编写两个用户态的程序来测试,第一个用于阻塞地读/dev/globalvar,另一个用于写/dev/globalvar。只有当后一个对/dev/globalvar进行了输入之后,前者的read才能返回。

  读的程序为:

#include
#include
#include
#include
main()
{
 int fd, num;

 fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
 if (fd != - 1)
 {
  while (1)
  {
   read(fd, &num, sizeof(int)); //程序将阻塞在此语句,除非有针对globalvar的输入
   printf("The globalvar is %d\n", num);

   //如果输入是0,则退出
   if (num == 0)
   {
    close(fd);
    break;
   }
  }
 }
 else
 {
  printf("device open failure\n");
 }
}


  写的程序为:

#include
#include
#include
#include
main()
{
 int fd, num;

 fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
 if (fd != - 1)
 {
  while (1)
  {
   printf("Please input the globalvar:\n");
   scanf("%d", &num);
   write(fd, &num, sizeof(int));

   //如果输入0,退出
   if (num == 0)
   {
    close(fd);
    break;
   }
  }
 }
 else
 {
  printf("device open failure\n");
 }
}


  打开两个终端,分别运行上述两个应用程序,发现当在第二个终端中没有输入数据时,第一个终端没有输出(阻塞),每当我们在第二个终端中给globalvar输入一个值,第一个终端就会输出这个值,如下图:


  关于上述例程,我们补充说一点,如果将驱动程序中的read函数改为:

static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
 //获取信号量:可能阻塞
 if (down_interruptible(&sem))
 {
  return - ERESTARTSYS;
 }

 //等待数据可获得:可能阻塞
 if (wait_event_interruptible(outq, flag != 0))
 {
  return - ERESTARTSYS;
 }
 flag = 0;

 //临界资源访问
 if (copy_to_user(buf, &global_var, sizeof(int)))
 {
  up(&sem);
  return - EFAULT;
 }

 //释放信号量
 up(&sem);

 return sizeof(int);
}


  即交换wait_event_interruptible(outq, flag != 0)down_interruptible(&sem)的顺序,这个驱动程序将变得不可运行。实际上,当两个可能要阻塞的事件同时出现时,即两个wait_eventdown摆在一起的时候,将变得非常危险,死锁的可能性很大,这个时候我们要特别留意它们的出现顺序。当然,我们应该尽可能地避免这种情况的发生!

+还有一个与设备阻塞与非阻塞访问息息相关的论题,即selectpollselectpoll的本质一样,前者在BSD Unix中引入,后者在System V中引入。pollselect用于查询设备的状态,以便用户程序获知是否能对设备进行非阻塞的访问,它们都需要设备驱动程序中的poll函数支持。

  驱动程序中poll函数中最主要用到的一个APIpoll_wait,其原型如下:

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


  poll_wait函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。下面我们给globalvar的驱动添加一个poll函数:

static unsigned int globalvar_poll(struct file *filp, poll_table *wait)
{
 unsigned int mask = 0;

 poll_wait(filp, &outq, wait);

 //数据是否可获得?
 if (flag != 0)
 {
  mask |= POLLIN | POLLRDNORM; //标示数据可获得
 }
 return mask;
}


  需要说明的是,poll_wait函数并不阻塞,程序中poll_wait(filp, &outq, wait)这句话的意思并不是说一直等待outq信号量可获得,真正的阻塞动作是上层的select/poll函数中完成的。select/poll会在一个循环中对每个需要监听的设备调用它们自己的poll支持函数以使得当前进程被加入各个设备的等待列表。若当前没有任何被监听的设备就绪,则内核进行调度(调用schedule)让出cpu进入阻塞状态,schedule返回时将再次循环检测是否有操作可以进行,如此反复;否则,若有任意一个设备就绪,select/poll都立即返回。

  我们编写一个用户态应用程序来测试改写后的驱动。程序中要用到BSD Unix中引入的select函数,其原型为:

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


  其中readfdswritefdsexceptfds分别是被select()监视的读、写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备好则返回。struct timeval数据结构为:

struct timeval
{
  int tv_sec; /* seconds */
  int tv_usec; /* microseconds */
};


  除此之外,我们还将使用下列API

  FD_ZERO(fd_set *set)――清除一个文件描述符集;
  FD_SET(int fd,fd_set *set)――将一个文件描述符加入文件描述符集中;
  FD_CLR(int fd,fd_set *set)――将一个文件描述符从文件描述符集中清除;
  FD_ISSET(int fd,fd_set *set)――判断文件描述符是否被置位。

  下面的用户态测试程序等待/dev/globalvar可读,但是设置了5秒的等待超时,若超过5秒仍然没有数据可读,则输出"No data within 5 seconds"

#include
#include
#include
#include
#include
#include
#include

main()
{
 int fd, num;
 fd_set rfds;
 struct timeval tv;

 fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
 if (fd != - 1)
 {
  while (1)
  {
   //查看globalvar是否有输入
   FD_ZERO(&rfds);
   FD_SET(fd, &rfds);
   //设置超时时间为5s
   tv.tv_sec = 5;
   tv.tv_usec = 0;
   select(fd + 1, &rfds, NULL, NULL, &tv);

   //数据是否可获得?
   if (FD_ISSET(fd, &rfds))
   {
    read(fd, &num, sizeof(int));
    printf("The globalvar is %d\n", num);

    //输入为0,退出
    if (num == 0)
    {
     close(fd);
     break;
    }
   }
   else
    printf("No data within 5 seconds.\n");
  }
 }
 else
 {
  printf("device open failure\n");
 }
}


  开两个终端,分别运行程序:一个对globalvar进行写,一个用上述程序对globalvar进行读。当我们在写终端给globalvar输入一个值后,读终端立即就能输出该值,当我们连续5秒没有输入时,"No data within 5 seconds"在读终端被输出,如下图: