评论

收藏

[PHP] Linux驱动开发|阻塞和非阻塞IO实验

开发技术 开发技术 发布于:2022-01-02 11:16 | 阅读数:446 | 评论:0

阻塞和非阻塞IO实验
本文在​​按键中断实验​​的基础上来使用阻塞式I/O和非用阻塞式I/O两种方式进行按键的读取实验,并比较按键中断、阻塞式按键和非阻塞式按键这三个实验的CPU使用率
  • 如何查看CPU占用率(以按键中断实验程序为例)

第一步:加载驱动后,使用后台运行模式打开 imx6uirqApp 测试软件
/imx6uirqApp /dev/imx6uirq &
第二步:使用 “top” 命令查看 CPU 使用率。由于 imx6uirqApp 应用程序是直接在 while 中通过 read 函数读取按键值,因此该软件会一直运行,导致 CPU 使用率高达99.6%
DSC0000.png
1. 阻塞I/O方式的按键检测
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作
1.1 阻塞I/O之等待队列
  • 等待队列头:若要在驱动中使用等待队列,需创建并初始化一个等待队列头
//等待队列头定义
struct __wait_queue_head {
  spinlock_t lock;
  struct list_head task_list;
};
/* 创建并初始化等待队列头 */
typedef struct __wait_queue_head wait_queue_head_t;
init_waitqueue_head(wait_queue_head_t);
  • 等待队列项:等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项
//等待队列项定义
struct __wait_queue {
  unsigned int flags;
  void *private;
  wait_queue_func_t func;
  struct list_head task_list;
};
/* 定义并初始化等待队列项 */
typedef struct __wait_queue wait_queue_t;
DECLARE_WAITQUEUE(name, tsk);
//参数name:等待队列项的名字
//参数tsk:表示该等待队列项属于哪个任务(进程),一般为current,表示当前进程
  • 将队列项添加到等待队列头:设备不可访问时就需要将进程对应的等待队列项添加到创建好的等待队列头中,只有添加到等待队列头中以后,进程才能进入休眠态
void add_wait_queue( wait_queue_head_t *q,
       wait_queue_t *wait)
//参数q:等待队列项要加入的等待队列头
//参数wait:要加入的等待队列项
  • 将队列项移除出等待队列头:设备可以访问后,将进程对应的等待队列项从等待队列头中移除即可
void remove_wait_queue( wait_queue_head_t *q,
      wait_queue_t *wait)
//参数q:要删除的等待队列项所处的等待队列头
//参数wait:要删除的等待队列项
  • 等待唤醒:设备可以使用时就要唤醒进入休眠态的进程
//可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程
void wake_up(wait_queue_head_t *q)
//只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程
void wake_up_interruptible(wait_queue_head_t *q)
  • 等待事件:除主动唤醒以外,也可设置等待队列等待某个事件,当该事件满足以后就自动唤醒等待队列中的进程
//等待以wq为等待队列头的等待队列被唤醒,前提是condition条件必须满足,否则一直阻塞
//函数会将进程设置为TASK_UNINTERRUPTIBLE状态
wait_event(wq, condition) 
wait_event_timeout(wq, condition, timeout)
//等待以wq为等待队列头的等待队列被唤醒,前提是condition条件必须满足,否则一直阻塞
//函数会将进程设置为TASK_INTERRUPTIBLE状态(即可被信号打断)
wait_event_interruptible(wq, condition)
wait_event_interruptible_timeout(wq, condition, timeout)
1.2 阻塞I/O程序编写
在​​按键中断实验​​的代码基础上进行改编,以下为需要改写的部分(下面代码有省略)
  • 驱动程序

定义并初始化等待队列头
......
#define IMX6UIRQ_NAME "blockio" 
......
/* imx6uirq 设备结构体 */
struct imx6uirq_dev{
  ..........
  unsigned char curkeynum;  /* 当前的按键号 */
  wait_queue_head_t r_wait;   /* 读等待队列头 */
};
......
......
static int keyio_init(void) {
  ......
  /* 创建定时器 */
  init_timer(&imx6uirq.timer);
  imx6uirq.timer.function = timer_function;
  /* 初始化等待队列头 */
  init_waitqueue_head(&imx6uirq.r_wait);
  return 0;
}
定义一个等待队列,当按键没有按下时,就要阻塞等待了,然后进行一次任务切换,交出CPU的使用权。等待有按键按下时,会有信号唤醒该等待,并将按键值返回给应用层的程序
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
......
......
#if 0
  /* 加入等待队列,等待被唤醒,也就是有按键按下 */
  ret = wait_event_interruptible(dev->r_wait,atomic_read(&dev->releasekey));
  if (ret) {
  goto wait_error;
  }
#endif

  DECLARE_WAITQUEUE(wait, current);     /* 定义一个等待队列 */
  if(atomic_read(&dev->releasekey) == 0) {  /* 没有按键按下 */
  add_wait_queue(&dev->r_wait, &wait);  /* 添加到等待队列头 */
  __set_current_state(TASK_INTERRUPTIBLE);/* 设置任务状态 */
  schedule();         /* 进行一次任务切换 */
  if(signal_pending(current)) {     /* 判断是否为信号引起的唤醒 */
    ret = -ERESTARTSYS;
    goto wait_error;
  }
  __set_current_state(TASK_RUNNING);  /*设置为运行状态 */
  remove_wait_queue(&dev->r_wait, &wait); /*将等待队列移除 */
  }
  keyvalue = atomic_read(&dev->keyvalue);
  releasekey = atomic_read(&dev->releasekey);
  ......
  return 0;

wait_error:
  set_current_state(TASK_RUNNING);    /* 设置任务为运行态 */
  remove_wait_queue(&dev->r_wait, &wait);   /* 将等待队列移除 */
  return ret;

data_error:
  return -EINVAL;
}
定时器去抖函数中,读取到按键后,触发唤醒
void timer_function(unsigned long arg) {
  ......
  ......
  /* 唤醒进程 */
  if(atomic_read(&dev->releasekey)) {   /* 完成一次按键过程 */
    wake_up_interruptible(&dev->r_wait);
  }
}
  • 应用程序:无需修改
1.3 运行测试
编译驱动程序和应用程序,并进行运行测试,查看CPU使用率
  • 修改Makefile编译目标变量
obj-m := blockio.o
  • 使用“make -j32”编译出驱动模块文件
make -j32
  • 使用“arm-linux-gnueabihf-gcc”命令编译测试APP
arm-linux-gnueabihf-gcc blockioApp.c -o blockioApp
  • 将驱动文件和APP可执行文件拷贝至“rootfs/lib/modules/4.1.15”中
  • 使用“modprobe”命令加载驱动
depmod  #第一次加载驱动时,需使用“depmod”命令
modprobe blockio.ko
  • 使用 “./blockioApp /dev/blockio &” 命令,以后台模式运行应用程序
  • 此时按下按键,应用程序会打印出按键值
DSC0001.png
  • 使用 “top” 命令查看 blockioApp 的CPU使用率:虽然应用程序中仍使用循环读取的方式,但由于无按键值时 read 被阻塞,应用程序也就被阻塞住了,CPU的使用权被让出
DSC0002.png
2. 非阻塞I/O方式的按键检测
按键应用程序以非阻塞的方式读取,按键驱动程序也要以非阻塞的方式返回
2.1 驱动中的poll操作函数
应用程序调用 select、poll 或epoll函数来对驱动程序进行非阻塞访问的时候,驱动程序 file_operations 操作集中的 poll 函数就会执行。所以驱动程序中需要提供对应的 poll 函数, poll 函数原型如下所示:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)
//filp:要打开的设备文件(文件描述符)
//wait:结构体 poll_table_struct 类型指针, 由应用程序传递进来的,
//  一般将此参数传递给poll_wait 函数
//返回值:向应用程序返回设备或者资源状态,可以返回的资源状态如下:
//    POLLIN 有数据可以读取
//    POLLPRI 有紧急的数据需要读取
//    POLLOUT 可以写数据
//    POLLERR 指定的文件描述符发生错误
//    POLLHUP 指定的文件描述符挂起
//    POLLNVAL 无效的请求
//    POLLRDNORM 等同于 POLLIN,普通数据可读
需要在驱动程序的 poll 函数中调用 poll_wait 函数, poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中, poll_wait 函数原型如下:
void poll_wait (struct file * filp, 
    wait_queue_head_t * wait_address, poll_table *p)
//参数wait_address:要添加到poll_table中的等待队列头
//参数p: 就是poll_table,就是file_operations中poll函数的wait参数
2.2 非阻塞I/O程序编写
在​​按键中断实验​​的代码基础上进行改编,以下为需要改写的部分(下面代码有省略)
  • 驱动程序

驱动程序 poll 函数处理部分
......
#define IMX6UIRQ_NAME "noblockio" 
......
unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait) {
  unsigned int mask = 0;
  struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
  /* 将等待队列头添加到poll_table中 */
  poll_wait(filp, &dev->r_wait, wait);  
  /* 按键按下 */
  if(atomic_read(&dev->releasekey)) {
    mask = POLLIN | POLLRDNORM;   /* 返回PLLIN */
  }
  return mask;
}
/* 设备操作函数 */
static struct file_operations imx6uirq_fops = {
  .owner = THIS_MODULE,
  .open = imx6uirq_open,
  .read = imx6uirq_read,
  .poll = imx6uirq_poll,
};
read 函数处理部分
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
  ......
  ......
  /* 非阻塞访问 */
  if (filp->f_flags & O_NONBLOCK) {
    /* 没有按键按下,返回-EAGAIN */
    if(atomic_read(&dev->releasekey) == 0) {
      return -EAGAIN;
    }
  } else {  /* 阻塞访问 */  
    /* 加入等待队列,等待被唤醒,也就是有按键按下 */
    ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); 
    if (ret) {
      goto wait_error;
    }
  }
  ......
  ...... 
wait_error:
  return ret;
data_error:
  return -EINVAL;
}
  • 应用程序
int main(int argc, char *argv[])
  int fd;
  int ret = 0;
  char *filename;
  struct pollfd fds;
  fd_set readfds;
  struct timeval timeout;
  unsigned char data;
  if (argc != 2) {
    printf("Error Usage!\r\n");
    return -1;
  }

  filename = argv[1];
  fd = open(filename, O_RDWR | O_NONBLOCK);  /* 非阻塞访问 */
  if (fd < 0) {
    printf("Can't open file %s\r\n", filename);
    return -1;
  }
/*************************************************/
/*以下代码使用poll函数来实现非阻塞访问,在while循环中
使用poll函数不断的轮询,检查驱动程序是否有数据可以读取,
如果可以读取的话就调用 read 函数读取按键数据*********/
#if 0 
  /* 构造结构体 */
  fds.fd = fd;
  fds.events = POLLIN;
  while(1) {
    ret = poll(&fds, 1, 500);

    if (ret > 0) {  /* 数据有效 */
      ret = read(fd, &data, sizeof(data));
      if(ret < 0) {
        /* 读取错误 */
      } else {
        if(data)
          printf("[APP] key value = %d \r\n", data);
      }   
    } else if (ret == 0) {   /* 超时 */
      /* 用户自定义超时处理 */
    } else if (ret < 0) {  /* 错误 */
      /* 用户自定义错误处理 */
    }
  }
#endif
/*************************************************/
/***** 以下代码使用 select 函数来实现非阻塞访问 *****/
  while(1) {
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
    /* 构造超时时间 */
    timeout.tv_sec = 0;
    timeout.tv_usec = 500000; /* 500ms */
    ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
    switch (ret){      
      case 0:   /* 超时 */
        /* 用户自定义超时处理 */
        break;      
      case -1:  /* 错误 */
        /* 用户自定义错误处理 */
        break;       
      default:  /* 可以读取数据 */
        if(FD_ISSET(fd, &readfds)){
          ret = read(fd, &data, sizeof(data));
          if (ret < 0) {
            /* 读取错误 */
          } else {
            if (data)
              printf("key value=%d\r\n", data);
          }
        }
        break;
    }
  }
  close(fd);
  return ret;
}
2.3 运行测试
编译驱动程序和应用程序,并进行运行测试,查看CPU使用率
  • 修改Makefile编译目标变量
obj-m := noblockio.o
  • 使用“make -j32”编译出驱动模块文件
make -j32
  • 使用“arm-linux-gnueabihf-gcc”命令编译测试APP
arm-linux-gnueabihf-gcc noblockioApp.c -o noblockioApp
  • 将驱动文件和APP可执行文件拷贝至“rootfs/lib/modules/4.1.15”中
  • 使用“modprobe”命令加载驱动
depmod  #第一次加载驱动时,需使用“depmod”命令
modprobe noblockio.ko
  • 使用 “./noblockioApp /dev/noblockio &” 命令,以后台模式运行应用程序
  • 此时按下按键,应用程序会打印出按键值
DSC0003.png
  • 使用 “top” 命令查看 noblockioApp 的CPU使用率:虽然应用程序中仍使用循环读取的方式,但由于无按键值时 read 被阻塞,应用程序也就被阻塞住了,CPU的使用权被让出
DSC0004.png
  • 关闭后台运行的应用程序:先使用"ps"命令查看进程的PID,然后使用"kill -9 PID"关闭指定程序即可
DSC0005.jpg

关注下面的标签,发现更多相似文章