免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 10756 | 回复: 13
打印 上一主题 下一主题

[驱动] 深入浅出Linux设备驱动之阻塞与非阻塞 [复制链接]

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-08-05 11:26 |只看该作者 |倒序浏览
深入浅出Linux设备驱动之阻塞与非阻塞
阻塞与非阻塞



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

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

wait的API:


  1. wait_event(queue, condition)

  2. wait_event_interruptible(queue, condition)

  3. wait_event_timeout(queue, condition, timeout)

  4. wait_event_interruptible_timeout(queue, condition, timeout)
复制代码

等待condition变为true,否则一直睡眠

condition可以在任何地方被改变,但改变时须wake_up 等待队列wq里面的process。

唤醒函数:

void wake_up(wait_queue_head_t *queue); //唤醒所有

void wake_up_interruptible(wait_queue_head_t *queue); //唤醒interruptible

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


  1. #include <linux/module.h>
  2. #include <linux/init.h>
  3. #include <linux/fs.h>
  4. #include <asm/uaccess.h>
  5. #include <linux/wait.h>
  6. #include <asm/semaphore.h>


  7. MODULE_LICENSE("GPL");
  8. #define MAJOR_NUM 254


  9. static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);


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

  11. struct file_operations globalvar_fops =
  12. {
  13. read: globalvar_read,
  14. write: globalvar_write,
  15. };


  16. static int global_var = 0;
  17. static struct semaphore sem;
  18. static wait_queue_head_t outq;
  19. static int flag = 0;


  20. static int __init globalvar_init(void)
  21. {
  22. int ret;
  23. ret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops);
  24. if (ret)
  25. {
  26. printk("globalvar register failure");
  27. }
  28. else
  29. {
  30. printk("globalvar register success");
  31. init_MUTEX(&sem);
  32. init_waitqueue_head(&outq);
  33. }
  34. return ret;
  35. }

  36. static void __exit globalvar_exit(void)
  37. {
  38. int ret;
  39. ret = unregister_chrdev(MAJOR_NUM, "globalvar");
  40. if (ret)
  41. {
  42. printk("globalvar unregister failure");
  43. }
  44. else
  45. {
  46. printk("globalvar unregister success");
  47. }
  48. }

  49. static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
  50. {

  51. if (wait_event_interruptible(outq, flag != 0))
  52. {
  53. return - ERESTARTSYS;
  54. }

  55. if (down_interruptible(&sem))
  56. {
  57. return - ERESTARTSYS;
  58. }

  59. flag = 0;
  60. if (copy_to_user(buf, &global_var, sizeof(int)))
  61. {
  62. up(&sem);
  63. return - EFAULT;
  64. }
  65. up(&sem);
  66. return sizeof(int);
  67. }

  68. static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len,loff_t *off)
  69. {
  70. if (down_interruptible(&sem))
  71. {
  72. return - ERESTARTSYS;
  73. }
  74. if (copy_from_user(&global_var, buf, sizeof(int)))
  75. {
  76. up(&sem);
  77. return - EFAULT;
  78. }
  79. up(&sem);
  80. flag = 1;

  81. wake_up_interruptible(&outq);
  82. return sizeof(int);
  83. }

  84. module_init(globalvar_init);
  85. module_exit(globalvar_exit);
复制代码


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


读的程序为:


  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <stdio.h>
  4. #include <fcntl.h>



  5. main()
  6. {
  7.  int fd, num;

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

  15.    //如果输入是0,则退出
  16.    if (num == 0)
  17.    {
  18.     close(fd);
  19.     break;
  20.    }
  21.   }
  22.  }
  23.  else
  24.  {
  25.   printf("device open failure\n");
  26.  }
  27. }

复制代码
 
写的程序为:


  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <stdio.h>
  4. #include <fcntl.h>
  5. main()
  6. {
  7.  int fd, num;

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

  16.    //如果输入0,退出
  17.    if (num == 0)
  18.    {
  19.     close(fd);
  20.     break;
  21.    }
  22.   }
  23.  }
  24.  else
  25.  {
  26.   printf("device open failure\n");
  27.  }
  28. }

复制代码
  

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

没有办法,我们还是要借助fork函数,完成验证,呵呵,fork大练兵了。
程序如下:

  1. #include <sys/types.h>

  2. #include <sys/stat.h>

  3. #include <stdio.h>

  4. #include <fcntl.h>

  5. #include <unistd.h>

  6. int main(void)
  7. {

  8. pid_t pid;

  9. pid=fork();

  10. if(pid < 0)printf("error in fork!");
  11. else if (pid == 0)
  12. { int fd, num;
  13. printf("I am parent\n");

  14. fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
  15. if (fd != - 1)
  16. {
  17. while (1)
  18. {
  19. read(fd, &num, sizeof(int));
  20. printf("The globalvar is %d\n", num);


  21. if (num == 0)
  22. {
  23. close(fd);
  24. break;
  25. }
  26. }
  27. }
  28. else
  29. {
  30. printf("device open failure\n");
  31. }
  32. }

  33. else
  34. {int fd, num;
  35. printf( "I am child\n");

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

  44. if (num == 0)
  45. {
  46. close(fd);
  47. break;
  48. }

  49. }
  50. }
  51. else
  52. {
  53. printf("device open failure\n");
  54. }




  55. }
  56. return 0;
  57. }
  58. 运行结果如下:
  59. [root@(none) study]$./gtest1
  60. I am child
  61. Please input the globalvar:
  62. I am parent
  63. 1
  64. Please input the globalvar:
  65. The globalvar is 1
  66. 2
  67. Please input the globalvar:
  68. The globalvar is 2
  69. 0
  70. The globalvar is 0

  71. [root@(none) study]$./gtest1
  72. I am child
  73. Please input the globalvar:
  74. I am parent
  75. 0
  76. The globalvar is 0

  77. [root@(none) study]$./gtest1
  78. I am child
  79. Please input the globalvar:
  80. I am parent
  81. 0
  82. The globalvar is 0
  83. [root@(none) study]$./gtest1
  84. I am parent
  85. I am child
  86. Please input the globalvar:
  87. 0
  88. The globalvar is 0
  89. [root@(none) study]$./gtest1
  90. I am child
  91. Please input the globalvar:
  92. I am parent
  93. 1
  94. Please input the globalvar:
  95. The globalvar is 1
  96. 2
  97. Please input the globalvar:
  98. The globalvar is 2
  99. 3
  100. Please input the globalvar:
  101. The globalvar is 3
  102. 0
  103. The globalvar is 0
  104. [root@(none) study]$./gtest1
  105. I am child
  106. I am parent
  107. Please input the globalvar:
  108. 1
  109. Please input the globalvar:
  110. The globalvar is 1
  111. 2
  112. Please input the globalvar:
  113. The globalvar is 2
  114. 0
  115. The globalvar is 0
复制代码


分析一下不难得出结论,我们的阻塞得到了验证。



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

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

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

  14.  //临界资源访问
  15.  if (copy_to_user(buf, &global_var, sizeof(int)))
  16.  {
  17.   up(&sem);
  18.   return - EFAULT;
  19.  }

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

  22.  return sizeof(int);
  23. }
复制代码


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

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

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

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

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


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

  4.  poll_wait(filp, &outq, wait);

  5.  //数据是否可获得?
  6.  if (flag != 0)
  7.  {
  8.   mask |= POLLIN | POLLRDNORM; //标示数据可获得
  9.  }
  10.  return mask;
  11. }
复制代码


需要说明的是,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);

其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1。timeout参数是一个指向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":

  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <stdio.h>
  4. #include <fcntl.h>
  5. #include <sys/time.h>
  6. #include <sys/types.h>
  7. #include <unistd.h>
  8. int main(void)
  9. {
  10.  int fd, num;
  11.  fd_set rfds;
  12.  struct timeval tv;

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

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

  30.     //输入为0,退出
  31.     if (num == 0)
  32.     {
  33.      close(fd);
  34.      break;
  35.     }
  36.    }
  37.    else
  38.     printf("No data within 5 seconds.\n");
  39.   }
  40.  }
  41.  else
  42.  {
  43.   printf("device open failure\n");
  44.  }
  45. }
复制代码


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

[ 本帖最后由 dreamice 于 2008-8-5 14:12 编辑 ]

论坛徽章:
0
2 [报告]
发表于 2008-08-05 11:28 |只看该作者
mark,学习了

论坛徽章:
5
摩羯座
日期:2014-07-22 09:03:552015元宵节徽章
日期:2015-03-06 15:50:392015亚冠之大阪钢巴
日期:2015-06-12 16:01:352015年中国系统架构师大会
日期:2015-06-29 16:11:2815-16赛季CBA联赛之四川
日期:2018-12-17 14:10:21
3 [报告]
发表于 2008-08-05 12:13 |只看该作者
代码最好用code标签括起来
看着也舒服

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
4 [报告]
发表于 2008-08-05 12:25 |只看该作者
原帖由 T-bagwell 于 2008-8-5 12:13 发表
代码最好用code标签括起来
看着也舒服


不知道怎么弄

论坛徽章:
0
5 [报告]
发表于 2008-08-05 13:01 |只看该作者
不錯,感謝分享

论坛徽章:
0
6 [报告]
发表于 2008-08-05 16:26 |只看该作者
不错~

论坛徽章:
0
7 [报告]
发表于 2008-08-08 15:43 |只看该作者
使用信号量也可以实现阻塞吧。
waiter queue跟信号量相比的好处是什么

论坛徽章:
0
8 [报告]
发表于 2010-06-05 10:17 |只看该作者
问一下,原文代码中阻塞例程里提到“交换wait_event_interruptible(outq, flag != 0)和down_interruptible(&sem)的顺序,这个
驱动程序将变得不可运行”
这一句没有看懂,哪位兄弟能帮忙分析一下。。。感谢!

论坛徽章:
0
9 [报告]
发表于 2010-06-28 14:54 |只看该作者
mark

论坛徽章:
0
10 [报告]
发表于 2010-06-28 17:40 |只看该作者
學習一下。。。。。。。。。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP