在正文开始前,我们先考虑一个场景。假如有一个任务是要你来设计一个Android手机的马达驱动程序,你应该怎么去设计呢?
我想至少应该保证,在很短的一段时间内有几个程序通过系统多次调用马达震动的函数,驱动程序在设计上是不可能拒绝和丢弃任意一个请求的,那么在执行震动的时候,难道要每一个程序都等待马达执行到自己所要求的震动结束么?
答案显然不是。从保证每一个请求都会被执行的角度考虑,马达的驱动中应该设计一个专门的缓冲区来保存每一个震动请求。从程序效率角度考虑,对于程序来说,每一个马达震动请求应该在加入马达震动工作的缓冲区中后,程序应该继续执行,而不是等待自己的请求完全被马达完成后再继续执行。
其实与这个场景相似的情况也出现在很多其他驱动程序中,我们应该怎样去解决这一类问题呢?这就是我们现在要讨论的Linux中的WorkQueue机制。
Linux中的WorkQueue是为了简化创建内核线程而建立的机制,Linux设备驱动程序中非常常见。
在Linux设备驱动中,我们经常要启动一些内核线程去完成一些我们想要驱动去做的工作。但是,我们又不希望因为一项工作的执行而影响驱动对于其他工作调用驱动程序的相应,这时候我们就需要一个能够缓冲工作的缓冲区,能够替我们将工作一个一个按照顺序不遗漏地执行,并且在执行过程中还能够不停地将新工作加到这个工作缓冲区中。
这就是我们使用WorkQueue机制的原因。
好,我们再回到最开始的那个场景中,对于一个马达的驱动,我们究竟应该将什么东西加入到WorkQueue中呢?如果你的想法是将一个震动工作加入到队列中,那你的想法和我最初的想法是一样的。这样好像是对的,WorkQueue顾名思义是工作队列,我要马达做的就是按照队列中一个又一个的work来震动,那将震动加进去貌似就对了。但是如果真的是这样的话,那我们的手机震动起来可就不正常了。
为什么呢?假设有一天,我们不停地快速点击手机虚拟按键的返回键,每点击一次就会产生一个震动,震动事件假设是300ms,而我们点击返回键的频率是每秒钟4次,那么按照上面的策略,我们给系统的命令就是每秒钟震动1.2s,这样点击5秒钟,在我们手指最后一次点击离开返回键之后,手机的马达还会再震动1s。这样的结果显然就不会是我们想要的结果。
除了上面这个关于体验的问题之外,这个策略还有一个比较重要的缺点就是占用了大量的CPU时间。因为WorkQueue都是工作在CPU上的,每一个work的处理都要占用CPU时间,所以按照上面的策略,我们一个小小的马达就会占用很多很多CPU时间,这显然也不是我们想要的。
那么我们应该如何使用WorkQueue来完成我们最开始的那个马达驱动的设计呢?这里我们可以参考isa1200的源代码来分析一下。
static int __devinit isa1200_probe(struct i2c_client *client,const struct i2c_device_id *id)
{
……
①INIT_WORK(&haptic->work, isa1200_chip_work);
②hrtimer_init(&haptic->timer, CLOCK_MONOTONIC,HRTIMER_MODE_REL);
③haptic->timer.function = isa1200_vib_timer_func;
……
④haptic->enable = 0;
......
⑤haptic->dev.enable = isa1200_chip_enable;
……
⑥haptic->hap_wq = create_singlethread_workqueue("haptic_wq");
……
}
上面摘取了一部分isa1200马达驱动程序probe函数的关键代码。从⑥这句可以看到程序注册了一个名为haptic_wq 的WorkQueue,而这个WorkQueue的work是什么呢?看①,这个work是isa1200_chip_work。这个isa1200_chip_work就是我们关注的焦点。从上面的②③④⑤这几句来看,程序还为设备注册了一个定时器,一个开关标记enable和一个enable接口。这里可以想到,这个定时器一定是控制马达震动时间的定时器。而这个enable接口就是马达的开关函数了。好,接下来我们看看到底程序注册了一个什么work到WorkQueue中。
static void isa1200_chip_work(struct work_struct *work)
{
struct isa1200_chip*haptic;
haptic =container_of(work, struct isa1200_chip, work);
if(haptic->pat_mode)
isa1200_vib_set_pattern(haptic);
else
isa1200_vib_set(haptic, haptic->enable);
}
从上面的代码看到,work很简单,看起来只是一个set,究竟set了什么呢?跟踪进函数isa1200_vib_set()
static void isa1200_vib_set(struct isa1200_chip *haptic, int enable)
{
pr_debug("isa1200vib_set:%d\n",enable);
if (enable) {
isa1200_pm_enable(haptic->client);
if(haptic->pdata->mode_ctrl == PWM_INPUT_MODE) {
if(!gpio_get_value(haptic->pdata->hap_hen_gpio))
gpio_direction_output(haptic->pdata->hap_hen_gpio,1);
pwm_config(haptic->pwm, (haptic->period_ns * 95) /100, haptic->period_ns);
pwm_enable(haptic->pwm);
/* shiftfreq 50000/(128+15)/2 = 175 */
isa1200_write_reg(haptic->client,ISA1200_HCTRL4, 15);
} else if(haptic->pdata->mode_ctrl == PWM_GEN_MODE) {
isa1200_write_reg(haptic->client,ISA1200_HCTRL5,
ISA1200_HCTRL5_VIB_STRT);
}
} else {
if(haptic->pdata->mode_ctrl == PWM_INPUT_MODE){
#if 0
/* 50%duty */
pm8058_pwm_haptic_set(haptic->pwm,32);
isa1200_write_reg(haptic->client,ISA1200_HCTRL4, 0);
#endif
gpio_direction_output(haptic->pdata->hap_hen_gpio, 0);
}
else if(haptic->pdata->mode_ctrl == PWM_GEN_MODE) {
isa1200_write_reg(haptic->client,ISA1200_HCTRL5,
ISA1200_HCTRL5_VIB_STOP);
}
isa1200_pm_disable(haptic->client);
}
}
看到上面代码,就豁然开朗了,这里work只是在enable这个参数不为0的条件下配置了一个pwm波的占空比,通过改变pwm波的占空比来控制马达震动。也就是说,在马达这个设备上,只是用WorkQueue来完成一个修改pwm波占空比的任务,说到底就是修改了一个配置。这就避免了大量占用CPU时间的问题了!
那紧接着又有一个问题,上面的enable标记在probe里面是0啊,怎么打开的马达呢?我们再来看马达另外一个很重要的函数isa1200_chip_enable,这个就是在probe里注册的那个enable开关接口
static void isa1200_chip_enable(struct timed_output_dev *dev, intvalue)
{
struct isa1200_chip*haptic = container_of(dev, struct isa1200_chip,
dev);
mutex_lock(&haptic->lock);
pr_debug("isa1200chip_enable:%d\n",value);
printk(KERN_INFO"%s %d\n", __func__,value);
if(value == 0) {
if((haptic->enable == 0)||(haptic->pat_mode)) {
/* do notexec stop in stop state or pattern mode */
pr_debug("isa1200useless set\n");
mutex_unlock(&haptic->lock);
return;
}
}
/* interrupt anotherruning vib */
if(hrtimer_cancel(&haptic->timer)){
if(haptic->pat_mode)
isa1200_pm_disable(haptic->client);
else if(value !=0)
isa1200_pm_disable(haptic->client);
}
if(value == 0)
haptic->enable= 0;
else{
haptic->enable= 1;
haptic->pat_mode= 0;
}
if(queue_work(haptic->hap_wq,&haptic->work)) {
if(value != 0) {
value =(value > haptic->pdata->max_timeout ?
haptic->pdata->max_timeout: value);
hrtimer_start(&haptic->timer,
ktime_set(value / 1000,(value % 1000) * 1000000),
HRTIMER_MODE_REL);
dump_isa1200_reg("new:",haptic->client);
}
} else{
pr_debug("isa1200queue_work fail on %d\n",haptic->enable);
haptic->enable= 0;
}
mutex_unlock(&haptic->lock);
}
isa1200_chip_enable这个函数是马达的开关函数,这个函数定位在timed_out这个class的设备的enable接口,系统通过这个接口向马达发送开关命令。
第一小段的橙色代码中可以看到一个if的判断,这里就是对enable这个值进行配置。如果value的值为0,则会把enable这个标记改为0。如果这个enable为0,从上面的分析知道,就不会将马达打开。如果enable为1,则会将马达打开。那么,驱动程序又是怎么控制马达震动时间的呢?
第二小段的橙色代码是启动了一个haptic的定时器,这个定时器在前面probe分析中看到,这个定时器在时间到达时,会调用isa1200_vib_timer_func这个函数。这个函数的功能需要分析一下这个函数的代码。
static enum hrtimer_restartisa1200_vib_timer_func(struct hrtimer *timer)
{
struct isa1200_chip *haptic = container_of(timer, structisa1200_chip,
timer);
①haptic->enable= 0;
②queue_work(haptic->hap_wq,&haptic->work);
return HRTIMER_NORESTART;
}
isa1200_vib_timer_func这个函数最主要的功能就是上面橙色代码的部分,只是将又一个work又加入到WorkQueue中,那定时器到时时是将什么样的work加入到工作队列中呢?看橙色①,这个定时器到时时,会将enable标记配置为0,然后将work加入到工作队列中。这里enable为0的work就会将马达关闭。
到此,我们就大概明白了马达的工作流程和WorkQueue在其中的作用了。总结一下,就是驱动程序会不停地将从enable接口接收到的信息配置一个work,然后将这个work加入到WorkQueue中,这个work的作用就是去修改马达的状态。而马达的震动只是work配置后一个持续的效果。当有新的work到来的时候,就会用新的持续效果来替换旧的持续效果。这样既节省了CPU时间,又能保证震动的稳定性,WorkQueue功不可没。