当前位置:   article > 正文

【Linux高级 I/O(1)】如何使用阻塞 I/O 与非阻塞 I/O?_linux io阻塞如何解决

linux io阻塞如何解决

        本系列再次回到文件 I/O 相关话题的讨论,将会介绍文件 I/O 当中的一些高级用法,以应对不同应用场合的需求,主要包括:非阻塞 I/O、I/O 多路复用、异步 I/O、存储映射 I/O 以及文件锁。

非阻塞 I/O

        关于“阻塞”一词前面已经给大家多次提到,阻塞其实就是进入了休眠状态,交出了 CPU 控制权。前面所学习过的函数,譬如 wait()、pause()、sleep()等函数都会进入阻塞,本文来聊一聊关于阻塞式 I/O 与 非阻塞式 I/O。

        阻塞式 I/O 顾名思义就是对文件的 I/O 操作(读写操作)是阻塞式的,非阻塞式 I/O 同理就是对文件的 I/O 操作是非阻塞的。这样说大家可能不太明白,这里举个例子,譬如对于某些文件类型(读管道文件、网络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式 I/O 常见的一种表现;如果是非阻塞式 I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!

        普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,read()或 write()一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O 操作,也可以使用非阻塞式 I/O 进行操作。

阻塞 I/O 与非阻塞 I/O 读文件

        本小节我们将分别演示使用阻塞式 I/O 和非阻塞式 I/O 对文件进行读操作,在调用 open()函数打开文件时,为参数 flags 指定 O_NONBLOCK 标志,open()调用成功后,后续的 I/O 操作将以非阻塞式方式进行; 这就是非阻塞 I/O 的打开方式,如果未指定 O_NONBLOCK 标志,则默认使用阻塞式 I/O 进行操作。

        对于普通文件来说,指定与未指定 O_NONBLOCK 标志对其是没有影响,普通文件的读写操作是不会阻塞的,它总是以非阻塞的方式进行 I/O 操作,这是普通文件本质上决定的,前面已经给大家进行了说明。

        本小节我们将以读取鼠标为例,使用两种 I/O 方式进行读取,来进行对比,鼠标是一种输入设备,其对应的设备文件在/dev/input/目录下,如下所示:

        通常情况下是 mouseX(X 表示序号 0、1、2),但也不一定,也有可能是 eventX,如何确定到底是哪个设备文件,可以通过对设备文件进行读取来判断,譬如使用 od 命令: 

sudo od -x /dev/input/event3

 Tips:需要添加 sudo,在 Ubuntu 系统下,普通用户是无法对设备文件进行读取或写入操作。

 当执行命令之后,移动鼠标或按下鼠标、松开鼠标都会在终端打印出相应的数据,如下所示:

         如果没有打印信息,那么这个设备文件就不是鼠标对应的设备文件,那么就换一个设备文件再次测试, 这样就会帮助你找到鼠标设备文件。笔者使用的 Ubuntu 系统,对应的鼠标设备文件是/dev/input/event3。接 下来我们编写一个测试程序,使用阻塞式 I/O 读取鼠标。

        示例代码演示了以阻塞方式读取鼠标,调用 open()函数打开鼠标设备文件"/dev/input/ event3",以只读方式打开,没有指定 O_NONBLOCK 标志,说明使用的是阻塞式 I/O;程序中只调用了一次 read()读取鼠标。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <string.h>
  8. int main(void){
  9. char buf[100];
  10. int fd, ret;
  11. /* 打开文件 */
  12. fd = open("/dev/input/event3", O_RDONLY);
  13. if (-1 == fd) {
  14. perror("open error");
  15. exit(-1);
  16. }
  17. /* 读文件 */
  18. memset(buf, 0, sizeof(buf));
  19. ret = read(fd, buf, sizeof(buf));
  20. if (0 > ret) {
  21. perror("read error");
  22. close(fd);
  23. exit(-1);
  24. }
  25. printf("成功读取<%d>个字节数据\n", ret);
  26. /* 关闭文件 */
  27. close(fd);
  28. exit(0);
  29. }

        编译上述示例代码进行测试:

        执行程序之后,发现程序没有立即结束,而是一直占用了终端,没有输出信息,原因在于调用 read()之后进入了阻塞状态,因为当前鼠标没有数据可读;如果此时我们移动鼠标、或者按下鼠标上的任何一个按键,阻塞会结束,read()会成功读取到数据并返回,如下所示:

 

        打印信息提示,此次 read 成功读取了 48 个字节,程序当中我们明明要求读取的是 100 个字节,为什么 这里只读取到了 48 个字节?这里暂时先不去理会这个问题。 接下来,我们将示例代码修改成非阻塞式 I/O,如下所示:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <string.h>
  8. int main(void){
  9. char buf[100];
  10. int fd, ret;
  11. /* 打开文件 */
  12. fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
  13. if (-1 == fd) {
  14. perror("open error");
  15. exit(-1);
  16. }
  17. /* 读文件 */
  18. memset(buf, 0, sizeof(buf));
  19. ret = read(fd, buf, sizeof(buf));
  20. if (0 > ret) {
  21. perror("read error");
  22. close(fd);
  23. exit(-1);
  24. }
  25. printf("成功读取<%d>个字节数据\n", ret);
  26. /* 关闭文件 */
  27. close(fd);
  28. exit(0);
  29. }

        修改方法很简单,只需在调用 open()函数时指定 O_NONBLOCK 标志即可,对上述示例代码进行编译测试:

        执行程序之后,程序立马就结束了,并且调用 read()返回错误,提示信息为"Resource temporarily unavailable",意思就是说资源暂时不可用;原因在于调用 read()时,如果鼠标并没有移动或者被按下(没有 发生输入事件),没有数据可读,故而导致失败返回,这就是非阻塞 I/O。

        可以对代码进行修改,使用轮训方式不断地去读取,直到鼠标有数据可读,read()将会成功 返回:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <string.h>
  8. int main(void){
  9. char buf[100];
  10. int fd, ret;
  11. /* 打开文件 */
  12. fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);
  13. if (-1 == fd) {
  14. perror("open error");
  15. exit(-1);
  16. }
  17. /* 读文件 */
  18. memset(buf, 0, sizeof(buf));
  19. for(;;){
  20. ret = read(fd, buf, sizeof(buf));
  21. if (0 < ret) {
  22. printf("成功读取<%d>个字节数据\n", ret);
  23. /* 关闭文件 */
  24. close(fd);
  25. exit(0);
  26. }
  27. }
  28. }

 阻塞 I/O 的优点与缺点

        当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式 I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;而对于非阻塞 I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮训等待,直到数据可读,要么直接放弃!

        所以阻塞式 I/O 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU 资源,将 CPU 资源让给别人使用;而非阻塞式则是抓紧利用 CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU 使用率!

        执行非阻塞示例代码对应的程序时,通过 top 命令可以发现该程序的占用了非常高的 CPU 使用率,如下所示:

        其 CPU 占用率达到了 100%!在一个系统当中,一个进程的 CPU 占用率这么高是一件非常危险的事情。而阻塞式方式的代码中,其 CPU 占用率几乎为 0,所以就本文所举例子来说,阻塞式 I/O 绝地要优于非阻塞式 I/O,那既然如此,我们为何还要介绍非阻塞式 I/O 呢?下一节我们将通过一个例子给大家介绍,阻塞式 I/O 的困境!

使用非阻塞 I/O 实现并发读取

        上一小节给大家所举的例子当中,只读取了鼠标的数据,如果要在程序当中同时读取鼠标和键盘,那又该如何呢?本小节我们将分别演示使用阻塞式 I/O 和非阻塞式 I/O 同时读取鼠标和键盘;同理键盘也是一种输入类设备,但是键盘是标准输入设备 stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错误,标准输入设备对应的文件描述符为 0,所以在程序当中直接使用即可,不需要再调用 open 打开。

        首先我们使用阻塞式方式同时读取鼠标和键盘,示例代码如下所示:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <string.h>
  8. #define MOUSE "/dev/input/event3"
  9. int main(void)
  10. {
  11. char buf[100];
  12. int fd, ret;
  13. /* 打开鼠标设备文件 */
  14. fd = open(MOUSE, O_RDONLY);
  15. if (-1 == fd) {
  16. perror("open error");
  17. exit(-1);
  18. }
  19. /* 读鼠标 */
  20. memset(buf, 0, sizeof(buf));
  21. ret = read(fd, buf, sizeof(buf));
  22. printf("鼠标: 成功读取<%d>个字节数据\n", ret);
  23. /* 读键盘 */
  24. memset(buf, 0, sizeof(buf));
  25. ret = read(0, buf, sizeof(buf));
  26. printf("键盘: 成功读取<%d>个字节数据\n", ret);
  27. /* 关闭文件 */
  28. close(fd);
  29. exit(0);
  30. }

        上述程序中先读了鼠标,再接着读键盘,所以由此可知,在实际测试当中,需要先动鼠标在按键盘(按下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘,程序才能够顺利运行结束。因为 read 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得不到执行。

        这就是阻塞式 I/O 的一个困境,无法实现并发读取(同时读取),主要原因在于阻塞,那如何解决这个问题呢?当然大家可能会想到使用多线程,一个线程读取鼠标、另一个线程读取键盘,亦或者创建一个子进程,父进程读取鼠标、子进程读取键盘等方法,当然这些方法自然可以解决,但不是我们要学习的重点。

        既然阻塞 I/O 存在这样一个困境,那我们可以使用非阻塞式 I/O 解决它,将代码修改为非 阻塞式方式同时读取鼠标和键盘。使用 open()打开得到的文件描述符,调用 open()时指定 O_NONBLOCK 标志将其设置为非阻塞式 I/O;因为标准输入文件描述符(键盘)是从其父进程进程而来,并不是在我们的程序中调用 open()打开得到的,那如何将标准输入设置为非阻塞 I/O,可以使用fcntl()函数。通过如下代码将标准输入(键盘) 设置为非阻塞方式:

  1. int flag;
  2. flag = fcntl(0, F_GETFL); //先获取原来的 flag
  3. flag |= O_NONBLOCK; //将 O_NONBLOCK 标志添加到 flag
  4. fcntl(0, F_SETFL, flag); //重新设置 flag

        则代码修改为:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #define MOUSE "/dev/input/event3"
  8. int main(void)
  9. {
  10. char buf[100];
  11. int fd, ret,flag;
  12. /* 打开鼠标设备文件 */
  13. fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
  14. if (-1 == fd) {
  15. perror("open error");
  16. exit(-1);
  17. }
  18. /* 将键盘设置为非阻塞方式 */
  19. flag = fcntl(0, F_GETFL); //先获取原来的 flag
  20. flag |= O_NONBLOCK; //将 O_NONBLOCK 标准添加到 flag
  21. fcntl(0, F_SETFL, flag); //重新设置 flag
  22. for(;;){
  23. /* 读鼠标 */
  24. ret = read(fd, buf, sizeof(buf));
  25. if(0 < ret)
  26. printf("鼠标: 成功读取<%d>个字节数据\n", ret);
  27. /* 读键盘 */
  28. ret = read(0, buf, sizeof(buf));
  29. if(0 < ret)
  30. printf("键盘: 成功读取<%d>个字节数据\n", ret);
  31. }
  32. /* 关闭文件 */
  33. close(fd);
  34. exit(0);
  35. }

         将读取鼠标和读取键盘操作放入到一个循环中,通过轮训方式来实现并发读取鼠标和键盘,对上述代码 进行编译,测试结果:

        

        这样就解决了阻塞所出现的问题,不管是先动鼠标还是先按键盘都可以成功读取到相应数 据。 虽然使用非阻塞 I/O 方式解决了问题,但由于程序当中使用轮训方式,故而会使得该程序的 CPU 占用率特别高,终归还是不太安全,会对整个系统产生很大的副作用,如何解决这样的问题呢?我们将在下一篇文章向大家介绍。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/343372
推荐阅读
相关标签
  

闽ICP备14008679号