linux内核驱动休眠和唤醒机制(select系统调用的内核驱动poll实现)

王修为
2023-12-01

阻塞与非阻塞的概念:

阻塞IO: 当数据不可读或不可写,进程休眠,直到得到数据可读或可写时才返回。阻塞效率高,实时性比较好。

非阻塞IO:不管数据是否可读可写,都马上返回。

应用程序是否能实现阻塞或非阻塞是取决于驱动程序。实际驱动中应该把阻塞和非阻塞这种选择权交给应用程序来选择。要实现这个效果 ,就必须让驱动程序知道应用程序的选择。这个信息是通过 file 结构来传递的。

struct file 结构中有成员:
    unsigned int         f_flags;
存放的就是 open(path,flag);
中的flag参数。

驱动中每个接口函数都有一个file指针,通过这个参数就能取得应用程序打开方式(阻塞或非阻塞)


等待队列

内核对这种阻塞提供等待队列机制来实现,这样可以改善实时性问题。

等待队列头数据结构

内核使用这个结构来给进程一个休眠的地方。
定义如下:Wait.h (include\linux)
struct __wait_queue_head {
  spinlock_t lock;
  struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

1.等待队列的睡眠

wait_event(wq, condition) ;建立不可以杀进程(信号不能唤醒,效果和msleep相同)。
wait_event_interruptible(wq, condition)    ;它可以被被信号唤醒。休眠过程中,进程可以接收信号,收到后不管条件如何,直接返回。
wait_event_timeout(wq, condition, timeout)    ;休眠期间效果和 wait_event ,但是有一个超时时间 ,时间到,不管条件如何,直接返回。
wait_event_interruptible_timeout(wq, condition, timeout);休眠期间效果和 wait_event_interruptible相同,区别是有超时功能。时间 到,不管条件如何,直接返回。

wq 是变量

2.等待队列的唤醒

通过调用 wake_up* 函数(表面上是宏)唤醒进程

wake_up(wq)  能用于唤醒各种方式进入休眠的进程,只唤醒队列上的一个进程。
wake_up_all(wq)效果和wake_up相同,只是能唤醒队列上所有的进程。
wake_up_interruptible(wq)   只能用于唤醒一个 使用wait_event_interruptible*休眠的进程。
wake_up_interruptible_all(wq)能唤醒队列所有 使用wait_event_interruptible*休眠的进程。
wq 是指针。

以上函数并不是说一调用,就一定可以把进程唤醒,其实它会先去检测进程休眠的条件,是否变成真了,如果为真才把它唤醒。

3. 等待队列 API

1)定义一个等待不队列头变量 ,并且 初始化。有两种方法:

方法1:动态定义:
wait_queue_head_t  wq;   //全局变量

在模块初始化函数中调用:
init_waitqueue_head(&wq);   //安装模块时候执行了初始化

方法2:静态初始化
DECLARE_WAIT_QUEUE_HEAD(wq)   ;    //这句等效于上面两句。

DECLARE_WAIT_QUEUE_HEAD(name) 表示定义一个 名字为name 的等待队列头变量,并且进行初始化。

2)在需要休眠的地方调用 wait_event*类宏让进程休眠

示例:
在按键驱动代码基础上修改read函数的休眠代码

/* 按键驱动read接口 */
static int buttons_read(struct file *filp,
                                     char __user *buff,
                                     size_t count,
                                     loff_t *offp)
{
  unsigned long err;

  /* count ==0 ,则返回0,不是错误 */
  if(!count) {
    return 0;
  }

  /* 没有按键动作 */
  if ( press==0 ) {
    if ( filp->f_flags & O_NONBLOCK) {
      return  -1;
    }

    else { /* 用户以阻塞方式打开 */
      wait_event_interruptible(wq, press);
    }
  }

  /*清按键动作标记*/
  press = 0;

  /* 修正大小 */
  count = min(sizeof(key_values), count);
  /* 复制按键缓冲数据给用户空间 */
  err = copy_to_user((void __user *)buff, (const void *)(&key_values), count);
  if(err) {
    return -EFAULT;
  }

  return count;
}

3)在需要唤醒的地方(一般是要中断程序中)把 休眠条件设置真,然后调用 wake_up* 类宏唤醒进程。

接上一步修改 中断服务程序:
在原来的 press = 1 ;
后面添加  wake_up_interruptible 代码,如下。

press = 1;  //设置按键动作标志位
// wake_up(&wq) ; //唤醒进程
wake_up_interruptible(&wq) ; //唤醒进程


现在要解决的问题是给应用程序提供一个接口,告诉它是否有按键动作发生了。这个接口就是驱动 中文件操作结构中的 poll 接口。

linux 驱动 poll 接口---------- 这个接口是用来告诉应用程序本设备是否可以读,或是否可写。

unsigned int    xxxx_poll(  struct file *file,struct poll_table_struct *wait)
poll 接口模板 :

1) 定义一个等待队列头,并且 初始化
2)按照以下的代码模板实现poll接口。

    //静态定义名字是button_waitq的等待队列头, 并且对它进行初始化
	static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
	
	/* 按键驱动poll接口 */
	static unsigned int buttons_poll(  struct file *file,struct poll_table_struct *wait)
	{
	    unsigned int mask = 0;
	    
	    poll_wait(file, &button_waitq, wait);  //把等待队列加到查询表中,固定方式
	    
	   //press 非0表示有按键动作 
	    if (press)
	        mask |= POLLIN | POLLRDNORM;
	
	  //如存在一个变量wr_flag表示设备可以写,则是mask |=  POLLOUT| POLLWRNORM  
	/*   if(wr_flag)
	       mask |=  POLLOUT| POLLWRNORM;
	  */     
	    return mask;
	}

poll 的系统调用接口 select()函数

参考:

https://mp.csdn.net/postedit/82925127

 类似资料: