linux 按键驱动代码分析
通过上面的图可以看出来,input有像按键等的输入设备event;触摸屏等的输入设备ts;遥控杆等输入设备js;鼠标等输入设备mouse和键盘等不会在/dev/input下产生节点,而是作为ttyn终端(不包括串口终端)的输入。
通过上面的介绍,结合具体的函数或结构体来解释就是设备驱动层为具体用户设备驱动,输入设备由 input-dev 结构表示,并由e和ice来注册和卸载;input 事件处理层主要和用户空间交互,接收用户空间下发的file 操作命令,生成/dev/input/xx设备节点供用户空间进行file 操作; input core层负责管理系统中的input dev设备 和input 事件处理,并起到承上启下作用,负责输入设备和input 之间信息传输,框架图如下:
(上段来自博客:)
大概介绍到这里,下面来分析代码:
按键的驱动定义在.c(\input\)文件中,老规矩首先看到这个宏:
();
();
这里有个宏,这个宏的跟之前遇见的的功能是一样的,只不过其优先级不同,具体的他们全部定义在Init.h (\linux)中:
# (fn) (fn,early)
# (fn) (fn,0)
# (fn) (fn,1)
# (fn) (fn,1s)
# (fn) (fn,2)
# nc(fn) (fn,2s)
# (fn) (fn,3)
# (fn) (fn,3s)
# (fn) (fn, 4)
# (fn) (fn,4s)
# (fn) (fn,5)
# (fn) (fn,5s)
# (fn) (fn,)
# (fn) (fn, 6)
# (fn) (fn,6s)
# (fn) (fn, 7)
# (fn) (fn,7s)
# (x) (x);
# (fn) (fn)
通过上面的宏可以看出他们的后面的参数不同,系数越小优先级越大,可以看出:
优先级由大到小为:> >
接下来就来看 和:
int (void)
{
(&ver);
}
void (void)
{
(&ver);
}
他们分别调用了平台驱动注册和注销函数,先看平台驱动注册函数的参数结构体:
ver = {
.probe = ,
. = ,
. = {
.name = "gpio-keys",
.owner = ,
.pm = &,
. =(),
}
};
从这个结构体看,跟led的格式就一样的,显示比较name是否都是"gpio-keys"平台设备跟平台驱动的名字比较搭配的话,在注册的时候就会去执行函数,在注销的时候就会执行的函数,往下看就是对应驱动的名字了;然后,这个由宏
(, ,);
生成的,该宏定义在Pm.h (\linux) ,根据该宏的定义,最终会调用和函数控制电源的进入工作状态或者低功耗状态。下一个定义了设备和驱动匹配函数,匹配方法可以用两个字方法:如果中定义了,则通过中的和中的内容进行匹配判断,匹配工作由来完成,该函数会遍历列表,查找是否有成员与相匹配,具体由的name,type和来进行对比,如果找到则返回相应的表项,否则返回null.如果没有定义,或不能找到对应的匹配项,则通过第二种方式来进行对比匹配,通过来完成。
接下来详细说明上面提到的几个函数。
先看探测函数:
int ( *pdev)
{
*dev =&pdev->dev;
const *pdata = (dev);
/*************************************************************************************
获取平台设备信息,也就是得到的数据信息。
*******************************************************************************/
*ddata;
/*************************************************************************************
ata这个是驱动信息的数据结构体,这个结构体的定义是解释按键驱动的关键一步,看一下这个结构体的定义:
ata {
const ata*pdata;
*input;
mutex ;
data[0];
};
ata这个就是在-smdk.c (arch\arm\mach-)定义的平台信息,包括引脚等的信息; *input;这里分配了一个输入设备; mutex 分配一个互斥锁; data[0];分配一个按键数据结构体。
*******************************************************************************/
*input;
/*************************************************************************************
*input;这里定义了一个输入设备,一个输入设备就是使用来表示的,在Input.h (\linux)文件中定义了结构体,其具体含义为(有背景色的为本次按键定义的):
{
*name; //提供给用户的输入设备的名称
*phys; //提供给编程者的设备节点的路径及其名称
const char *uniq; //指定唯一的ID号,就像MAC地址一样
id; //输入设备标识ID,用于和事件处理层进行匹配。
[()]; //位图,设备属性
evbit[()]; //位图,记录设备支持的事件类型
long [()]; //位图,记录设备支持的按键类型
[()]; //位图,记录设备支持的相对坐标
[()]; //位图,记录设备支持的绝对坐标
[()]; //位图,记录设备支持的其他功能
[()]; //位图,记录设备支持的指示灯
[()]; //位图,记录设备支持的声音或警报
ffbit[()]; //位图,记录设备支持的作用力功能
swbit[()]; //位图,记录设备支持的开关功能
et; //每个数据包记录数据量的个数(上报一次输入事件在/之间的数据的个数)
; //设备支持的最大按键值个数
int ; //每个按键的字节大小
void *; //指向按键池,即指向按键值数组首地址
int (*)( *dev, , int ); //修改按键值
int (*)( *dev, int ,int *); //获取按键值
*ff; //假如设备支持力反馈,这里与里反馈结构相关的东西
int ; //支持重复按键
timer; //设置当有连击时的延时定时器
[]; //记录重复按键的参数值比如延时时间和速率
*mt; //指向多触点操作状态
*;//对于绝对坐标而言,该指针指向的是桌表包括当前坐标值,最大值最小值,滤波毛刺,分辨率等,具体的可以查看结构体
key[()];位图,按键的状态
led[()];位图,led的状态
snd[()];位图,声音的状态
sw[()];位图,开关的状态
int (*open)( *dev); //输入设备打开函数
void(*close)( *dev); //输入设备关闭函数
int (*flush)( *dev, file*file); //输入设备断开后清除刷新函数
int (*event)( *dev, , int code, int value); //事件处理
__rcu *grab;
; //当输入核处理输入时间时候的自旋锁
mutex; //用于open、close、flush函数的连续访问互斥
users;// input 打开设备的次数
; //输入设备注销的时候的标志位
dev; //输入设备的类信息
; //链表
node; //链表
;//当前队列的排队数
;//允许最大的排队个数
*vals;//当前队列排队的存储数组
;//表明设备正在受到驱动管理,不需要注销或者释放
};
*******************************************************************************/
int i, error;
int = 0;
if (!pdata) {
pdata =(dev);
if ((pdata))
(pdata);
}
/*************************************************************************************
假如 ata *pdata = (dev)没有获取平台信息成功,就用设备树获取。
*******************************************************************************/
ddata =(( ) +
pdata->* ( ),
);
/*************************************************************************************
这里是为pdata申请一块内存,平且将其内容清零。实际调用的是函数。函数返回的是虚拟地址,这里要注意(释放内存为kfree)和(释放内存为vfree)的区别:最大只能开辟128k-16字节,其分配的内存在屋里地址上是线序的,是申请的可能是非连续的地址,具体的可参考:
*******************************************************************************/
input = e();
/*************************************************************************************
返回值是结构体,到这里是申请为输入设备。
*******************************************************************************/
if (!ddata || !input) {
(dev," to state\n");
error = -;
goto fail1;
}
/*************************************************************************************
判断是否申请成功,假如不成功的话就打印出失败语句,并且goto到fail1,执行(input), kfree(ddata), kfree(pdata)把申请的资源或者得到的数据释放掉。
*******************************************************************************/
ddata->pdata = pdata;
ddata->input = input;
/*************************************************************************************
给ddata相应的成员赋值
**************************************************************************/
(&ddata->);
/*************************************************************************************
这是在为ddata初始化互斥锁(互斥体),mutex的使用场合跟信号量基本相同,一般用户那些进程之间竞争,且占用时间较长的场合,当占用时间较短是,一般使用互旋锁。
对于linux锁机制部分可参考
**************************************************************************/
(pdev,ddata);
/*************************************************************************************
将驱动数据保存到驱动平台数据中,后期将会使用保存的函数。
**************************************************************************/
(input,ddata);
/*************************************************************************************
将驱动数据保存到输入设备中中,后期将会使用保存的函数。
**************************************************************************/
input->name =pdata->name ? : pdev->name;
input->phys ="gpio-keys/";
input->dev. =&pdev->dev;
input->open =;//在挂起或者唤醒的时候会调用open和close函数
input->close =;
input->id. =;
input->id. =;
input->id. =;
input->id. =;
/*************************************************************************************
这里是在给input赋值,这里主要设备了输入的名字为gpio-keys;设备节点及其路径,驱动父类;然后设置了open和close函数;最后设置了id。
**************************************************************************/
/* auto of Linux input */
if (pdata->rep)
(,input->evbit);
/*************************************************************************************
给按键设置可重复多次按下的特性。
**************************************************************************/
for (i = 0; i
; i++) {
const on * = &pdata->[i];
ta *bdata = &ddata->data[i];
error = (pdev,input, bdata, );
if (error)
goto fail2;
if(->)
= 1;
}
/*************************************************************************************
这个循环获取每个按键的信息,并通过函数为每个按键初始化引脚,滤波消抖,申请外部中断,申请定时器中断平且设定中断定时器服务函数。该函数将会在下面单独介绍。假如创建失败会调用(&ddata->data[i]);释放掉申请的引脚,取消申请的队列等等
**************************************************************************/
error = (&pdev->dev.kobj,&);
if (error) {
(dev," to keys/, error: %d\n",
error);
goto fail2;
}
/*************************************************************************************
()在kobj目录下创建一个属性集合,并显示集合中的属性文件。如果文件已存在,会报错。以函数为例,这里将会在gpio-keys/目录下创建一个属性文件,最终会调用:
*[] = {
&.attr,
&.attr,
&ys.attr,
&.attr,
NULL,
};
也就是会生成:
/sys///gpio-keys/[rw]
/sys///gpio-keys/[rw]
/sys///gpio-keys/keys[ro]
/sys///gpio-keys/[ro]
假如创建失败假如创建失败会调用(&ddata->data[i]);释放掉申请的引脚,取消申请的队列等等
**************************************************************************/
error =e(input);
if (error) {
(dev," to input , error: %d\n",
error);
goto fail3;
}
/*************************************************************************************
这是是真正的向内核注册一个输入设备。该函数将结构体注册到输入子系统核心中,由前面介绍的e()函数来分配。e()函数如果注册失败,必须调用()函数释放分配的空间。假如函数注册成功,调用ice()函数来注销输入设备结构体。 e()函数中的设置中的的名字,名字以、、·······等的形式出现在sysfs文件系统中。并且e()函数中()函数将加入链表中,的链表中包含了系统中所有的设备。
假如注册失败会调用(&pdev->dev.kobj,&);来删除之前创建的属性文件。
**************************************************************************/
(&pdev->dev,);
/*************************************************************************************
这函数跟电源管理有关,在.h (\linux)文件中:
int ( *dev, bool val)
{
pable(dev, val);//设置设备能不能被唤醒
able(dev, val); //设置设备使不使用唤醒;
0;
}
**************************************************************************/
0;
fail3:
(&pdev->dev.kobj,&);
fail2:
while (--i >= 0)
(&ddata->data[i]);
fail1:
(input);
kfree(ddata);
/* If we have no , we pdata . */
if(!(&pdev->dev))
kfree(pdata);
error;
}
probe函数做完了整个驱动要做的事情,现在总结一下probe都做了些什么事:
首先获取平台设备信息:(dev)或者(dev)
为定义的设备信息申请一块内存:
申请分配一个输入设备: e
为该输入设备设置属性:input->······
初始化按键相关的引脚、中断、及其有关的定时器信息:
为该设备创建一个属性集合:
正式申请为输入设备:e
刚才有看到在probe函数中,调用了好多自定义的一些函数,接下来逐个分析一下这些函数的实现。
先看一下设置按键的函数:
int ( *pdev,
*input,
ta *bdata,
*)
{
const char *desc =->desc ? ->desc : "";
*dev =&pdev->dev;
;
long ;
int irq, error;
/*************************************************************************************
*desc = ->desc ? ->desc : "";这里是获取按键的描述的名字个设备信息应该为 1、 2、 3,假如获取不到其描述符就是。
并且定义了中断申请需要的变量。
定义在.h (\linux)文件中:
(*)(int, void *);
其中为定义在.h (\linux)文件中
/**
* enum
* @ not from this
* @ by this
* @ to wake the
*/
{
=(0 lock);
/*************************************************************************************
初始化自旋锁,自旋锁适用于临界区不是很大的情况(临界区的执行时间比较短)
总结自旋锁使用流程:
1、首先定义一个自旋锁:
lock
2、初始化自旋锁:
spin_ (lock)
3、获取自旋锁
(1) (lock)//假如获得锁返回真,否则返回假,返回假货不会原地打转的等着获得锁
(2) (lock)//假如获得锁返回真,否则返回假,返回假货将会原地打转的等着获得锁
##############################################################
假如获得会执行临界区(要锁存的区域)的操作
。。。。。。。。。
###############################################################
4、释放掉自旋锁
(lock);
**************************************************************************/
if((->gpio)) {
error =(->gpio, , desc); //申请一个pgio,配置为输入
if (error < 0){
(dev," to GPIO %d, error %d\n",
->gpio,error);
;
}
if(->) {
error =(->gpio,
->* 1000); //按键消抖处理
/* if doesn't */
if (error< 0)
bdata->=
->;
}
irq =(->gpio);//申请GPIO中断,正确返回中断编号,错误返回错误编码(负)
if (irq < 0) {
error =irq;
(dev,
" get irq for GPIO %d, error %d\n",
->gpio,error);
goto fail;
}
bdata->irq =irq;//赋值中断号
(&bdata->work,func);
/*************************************************************************************
将函数func假如工作队列,让中断的第二阶段去执行该函数。实际上&bdata->work是一个描述工作队列的结构体,变量定义为 work,该结构体定义在.h (\linux)文件中:
{
data;
entry;
func;
#
;
#endif
};
结构体中的 func指向要假如中断队列的函数,这样的话函数func就和&bdata->work绑定在一起了,一般情况下,func会在中断下半部执行,所以在中断服务函数中会调用(&bdata->work)来执行该函数。
最后在卸载的时候会调用(&bdata->work);来取消加进去的工作队列。
总结工作队列过程(注:这里是使用内核线程,并不是自定义一个线程,假如需要创建队列可参考:
):
1、首先定义一个工作结构体:
work
2、将定义的工作加入内核工作队列并且绑定放入队列的函数:
(work, );
或者
(work, )
3、当需要执行工作函数的时:
(1)对应的
执行(work)会马上调用函数
(2) 对应的
执行k(time,work)会在time时间后执行
##############################################################
执行函数的操作
。。。。。。。。。
###############################################################
4、取消工作队列中的工作:
(1)对应的
(work)
(2) 对应的
sync//取消延时工作并且等待其完成工作
(work)//取消延时工作
***********************************************************************/
(&bdata->timer,
, ()bdata);
/*************************************************************************************
由于按键消抖需要定时器,这是是初始化定时器,首先看一下&bdata->timer,其类型 定义在Timer.h (\linux)文件中:
{
/*
*All that to the
*same
*/
entry; //双链表,用来将多个定时器连接成一条双向循环队列。
long ; //定时时间
*base; // 这个是动态定时器的主要数据结构,每个cpu上有一个,它包含相应cpu中处理动态定时器需要的所有数据
void (*)( long); //超时时要处理的函数
long data; //超时处理函数参数
int slack; //跟定时器的内核处理方式有关的变量
#ATS
int ;
void *;
char [16];
#endif
#
;
#endif
};
当运行(&bdata->timer,, ( long)bdata);这个宏就是设定的定时器为&bdata->timer,定时器到时间后运行的函数为,定时器函数所需要的数据为( long)bdata。
有关输入子系统的介绍就到这里,下面总结一下定时器的使用方法:
1、定义一个定时器
2、初始化定时器并赋值成员(, , data);
3、增加定时器()
4、该修改定时器的定时时间 (,)
5、取消定时器,有两个可选择
1.()直接删除定时器
2.()等待本次定时器处理完毕再取消(不适用中断上下文)
**************************************************************************/
isr =;
/*************************************************************************************
这是是设置中断服务函数
**************************************************************************/
= | ;
/*************************************************************************************
这是设定中断处理的属性,也就是中断触发方式处理方式等等。这里设置上升沿触发或者下降沿触发。假如设置表明多个设备共享一个中断,此时会用到;当设置时候表明,中断为快速中断。
**************************************************************************/
} else {
if(!->irq) {
(dev,"No IRQ \n");
-;
}
bdata->irq =->irq;
if(->type && ->type != ) {
(dev,"Only for IRQ .\n");
-;
}
bdata->= ->;
(&bdata->timer,
, ( long)bdata);
isr =;
= 0;
}
(input,->type ?: , ->code);
/*************************************************************************************
设定该按键具有什么能力,以本例来说, 具有按键F1 F2 F3的能力
**************************************************************************/
/*
* If has that the be ,
* we don't want it to share the .
*/
if(!->)
|=;
/*************************************************************************************
设置是否共享中断,这里涉及到中断共享机制:
多个中断共享一个中断线必须用做标记,以作为标记的中断假如要向内核申请成功一个中断需要两个条件之一:
该中断还没有被申请
该中断虽然被申请了,但是已经申请的中断也有做标记
当发生共享中断时,所用挂载到此中断的中断服务函数都会得到响应(遍历所有该中断线上的中断),所以说,当该中断设备为共享中断的时候,中断服务函数首先需要判断是否是自己的假如不是自己的那么返回(表明不是本中断),假如检测到时本中断的话就会执行中断里面的函数,最后返回。
**************************************************************************/
error =irq(bdata->irq, isr, , desc, bdata);
/*************************************************************************************
申请一个中断线,其中bdata->irq为要申请的中断线(中断号);isr为中断服务函数;中断的触发方式或者处理方式;要申请中断的描述符;bdata为,区分共享中断线线而设定的。
看一下申请外部中断的流程:
1、申请某个引脚为外部中断 = (->gpio);
2、设备外部中断函数 = ;
3、设置中断发出类型 flags =?
4、描述该中断的一个assic字符串的名字 *name=?
5、设备dev-id是一个空函数指针 void* =?
**************************************************************************/
if (error < 0) {
(dev," to claim irq %d; error %d\n",
bdata->irq,error);
goto fail;
}
0;
fail:
if((->gpio))
(->gpio);
error;
}
现在总结一下设置按键的函数作用:
该函数主要是申请外部中断,设定中断服务函数,由于在消抖的时候会用到定时器,这个函数还初始化了定时器。并且绑定了定时器中断服务函数。
接下来看一下按键服务函数:
(int irq, void *)
{
/*************************************************************************************
首先看中断服务函数的返回值:
,还没有发生中断
中断已经处理完毕
中断需要唤醒处理线程
**************************************************************************/
*bdata = ;
(irq !=bdata->irq);//代码调试用的
if(bdata->->) //根据保存非睡眠状态
(bdata->input->dev.);
if (bdata->)
(&bdata->timer,
+(bdata->));
else
(&bdata->work);
/*************************************************************************************
先看if的条件,条件为消抖的时间,假如需要消抖(消抖的时间非0),那么就执行 (&bdata->timer,+ (bdata->));
用于改变或者设定定时器的定时时间,其中&bdata->timer为要修改的定时器, +(bdata->)为修改后的时间函数是吧毫秒转换为,单单看这个函数就是在过(bdata->)的时间触发定时器,定时器时间到后就会执行定时器服务函数(在按键设置函数中已经设置定时器服务函数为),定时器服务函数的内容为:
void ( )
{
ta *bdata = ( *)_data;
(&bdata->work);
}
可以看到时间一到会执行(&bdata->work);执行中断底部的队列部分;
这里可以看到,假如不需要消抖,那么直接执行中断底部的队列部分(&bdata->work)。
(稍后介绍工作工作队列的工作)
**************************************************************************/
;
}
对于按键中断服务函数可以看出来,中断函数确实是分为中断顶半部(top half)和中断底半部( half),对于按键来说,中断顶半部就是消抖(消抖还用到了定时间,所以说需要的执行时间很短),中断的底半部就是用来执行工作队列里面的工作了,接下来看一下队列的工作都做了什么:
void func( *work)
{
*bdata =
(work, , work);
/*************************************************************************************
是根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针,就本例而言,就是根据->work这个成员的指针来获取整个结构体的指针首地址。
假如想了解的原理,可参考:
**************************************************************************/
(bdata);
/*************************************************************************************
这个是整个输入子系统的关键的一个函数(时间上报函数),稍后将会介绍。
**************************************************************************/
if(bdata->->) //电源管理有关
(bdata->input->dev.);
}
可以看到队列工作就是为了调用时间上报函数,看一下时间上报函数的实现:
void ( *bdata)
{
const on * = bdata->;
*input =bdata->input;
int type =->type ?: ;
int state =(eep(->gpio) ? 1 : 0) ^ ->;
/*************************************************************************************
eep是获取按键的值,其区别是,有些芯片的引脚与cpu是依靠一些总线连接的比如iic总线,那么这些引脚就有可能产生休眠,因此要用eep来获取按键的值,获取后异或->(之前设定的是1),那么当有按键按下时,eep得到的是低电平也就是0,所以0^1=1,这里也是再说名,当按键按下时,获取的state应该是1。
**************************************************************************/
if (type == ) { //这个类型判断是不是(绝对坐标)事件,本次是事件
if (state)
(input,type, ->code, ->value);
} else {
(input,type, ->code, !!state);
}
/*************************************************************************************
这是就是上报事件了,这个为执行(input, type, ->code,!!state);
函数的参数为,input上报的设备(也就是);type为上报事件的类型,这里是(按键事件),->code按键的编码,就本次而言对应的额是F1 F2F3;state为上报值
**************************************************************************/
(input);
/*************************************************************************************
(input)实际定义是:
void ( *dev)
{
(dev, , , 0);
}
用来告诉上层,本次的事件已经完成了.
这里要注意的就是,设置触发方式的时候,设置的是下降沿触发或上升沿触发,也就是当执行一次按键按下松开的动作时,会触发两次中断(一次按下,一次松开),每次中断都会执行本函数,执行一次本函数上报两个输入事件(执行了两次),也就是说,按下松开一共会上报四个数据。
**************************************************************************/
}
到这里上报时间就完工了,回想一下,整个输入子系统的流程可以总结为两部:
首先定义申请注册一个输入设备。
申请按键为外部中断,当按键按下时,在中断服务函数中处理,并上报按键事件。
有关按键的主题部分已经分析完毕,接下来看一下其他的一些函数:
在介绍函数中的时,会生成一些属性文件,看一下这个属性文件是如何使用在驱动程序中的,先看一下生成哪些属性文件
/sys///gpio-keys/[rw]
/sys///gpio-keys/[rw]
/sys///gpio-keys/keys[ro]
/sys///gpio-keys/[ro]
这里就以文件为例介绍如何使用这些属性文件来设置驱动程序。
由上节的led的分析我们知道,对于sysfs下的属性文件,当要读文件的实收会执行*show函数,当要写文件的时候会执行*store程序,接下来分别看下一下这两个函数,首先看show函数:
##name( *dev, \
*attr, \
char *buf) \
{ \
*pdev = (dev); \
/*************************************************************************************
先看下定义:
# (x)((x), , dev)
可以看到就是通过的成员dev来得到结构体的首地址。
**************************************************************************/
*ddata = (pdev); \
/*************************************************************************************
在probe有执行(pdev, ddata);将ddata的保存起来,这里是获取保存的数据。
**************************************************************************/
\
(ddata, buf, \
type, ); \
/*************************************************************************************
这个函数就是控制使能与否的关键函数了。下面介绍本函数
**************************************************************************/
}
(ata *ddata,
char *buf, int type,
bool )
{
int =(type);
/*************************************************************************************
这是是返回事件类型的最大值(因为后面要申请内存,所以这里需要对应本类时间的最大值)。可以查看实际上返回的书是。
**************************************************************************/
long *bits;
ret;
int i;
bits = ((), (*bits), );
/*************************************************************************************
申请一个数组的内存空间,并把申请得到的内存都初始化为零,其参数为要申请的数量,第二个参数为单位数量占有的字节数,第三个参数分配内存时的控制方式。
函数是求一个数是几个long型的长度,(*bits)就是(long)=4,这个函数要是申请的字节数就是()乘以(*bits)得到的字节数。
实际上,可用下面的例子来测试:
#
# (n, d) (((n)+(d)-1) / (d))
# 8
# (nr) (nr, *(long))
void main()
{
intj = 0x300;
*bits;
(" long is %ld\n", (*bits));
(" is%ld\n", (j));
while(1);
}
运行结果为:size of long is is 24
对于第三个参数的标志位,这些标志位定义在Gfp.h (\linux)文件中:
具体的介绍这在里摘录博客的一段话:
GFP的标记有两种:带双下划线前缀的和不带双下划线前缀的;
不带双下划线前缀的GFP标志:
:用于在中断上下文和进程上下文之外的其它代码中分配内存;从不睡眠;
:内核正常分配内存;可能睡眠;
:用于为用户空间页分配内存;可能睡眠;
:如同,但它是从高端内存中申请;
和:功能如同,但是它俩增加限制到内核能做的来满足请求;分配不允许进行任何文件系统调用,而分配根本不允许进行任何IO初始化;它俩主要用于文件系统和虚拟内存代码,那里允许一个分配睡眠,但是递归的文件系统调用会是个坏主意;
带有双下划线前缀的GFP标志:
:这个标志要求分配的内存在能够进行DMA的内存区;平台依赖的;
:这个标志指示分配的内存可以位于高端内存区;平台依赖的;
:正常地,内存分配器尽力返回"缓冲热"的页---可能在处理器缓冲中找到的页;相反,这个标志请求一个"冷"页---在一段时间内没被使用的页;它对分配页做DMA读是很有用的,此时在处理器缓冲中出现是没用的;
:这个标志用于分配内存时阻止内核发出警告,当一个分配请求无法满足时;
:这个标志标识了一个高优先级请求,它被允许来消耗甚至被内核保留给紧急状况的最后的内存页;
:分配器的动作;当分配器有困难满足一个分配请求时,通过重复尝试的方式来"尽力尝试",但是分配操作仍然有可能失败;
:分配器的动作;当分配器有困难满足一个分配请求时,这个标志告诉分配器不要失败,尽最大努力来满足分配请求;
:分配器的动作;当分配器有困难满足一个分配请求时,这个标志告诉分配器立即放弃,不再做任何尝试;
通常,一个或多个带双下划线前缀的标记相或,即可得到对应的不带双下划线前缀的标记;
最常用的标记就是,它的意思就是当前的这个分配代表运行在内核空间的进程而进行的;换句话说,这意味着调用函数是代表一个进程在执行一个系统调用;使用标记,就意味着能够使当前进程在少内存的情况下通过睡眠来等待一个内存页;因此,一个使用的函数必须是可重入的,且不能在原子上下文中运行;当当前进程睡眠,内核采取正确的动作来定位一些空闲的内存页,或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存页;
如果一个内存分配动作发生在中断处理或内核定时器的上下文中时,当前进程就不能被设置为睡眠,也就不能再使用标志了,此时应该使用标志来代替;正常地,内核试图保持一些空闲页以便来满足原子的分配;当使用标志时,标志能够使用甚至最后一个空闲页;如果这最后一个空闲页不存在,那分配就会失败;
**************************************************************************/
if (!bits)
-;
for (i = 0; i pdata->; i++) {
ta *bdata = &ddata->data[i];
if(bdata->->type != type) //假如不是预定类型跳过本循环
;
if (&& !bdata->) //假如不使能跳过本循环
;
(bdata->->code,bits);
/*************************************************************************************
是非原子型的按位操作(函数是原子操作),这里有点难理解,就是将申请内存的是bits相应的按键编码位置1,就以本函数的实际代表意思来解释,就是在判定本按键有效的情况下,就是把刚才申请的bits所指向的内存对应的F1或者F2或者F3相应的位置1。
**************************************************************************/
}
ret =(buf, - 2, bits, );
/*************************************************************************************
将位图转花纹字符串。
**************************************************************************/
buf[ret++] = '\n';
buf[ret] = '\0';
kfree(bits);
ret; //返回值是换成的字符串的长度
}
这个函数的实际含义就是当你对/sys///gpio-keys/ 文件进行读的时候就用调用open函数,举一个直观些的例子就是:
int read(fd,char *buf,&, ); //fd为打开的设备文件
上面的函数是读/sys///gpio-keys/要执行的函数,下面看下写该文件要执行的函数:
re_##name( *dev, \
*attr, \
const char *buf, \
count) \
{ \
*pdev = (dev); \
/*************************************************************************************
先看下定义:
# (x)((x), , dev)
可以看到就是通过的成员dev来得到结构体的首地址。
**************************************************************************/
*ddata = (pdev); \
/*************************************************************************************
在probe有执行(pdev,ddata);将ddata的保存起来,这里是获取保存的数据。
**************************************************************************/
error; \
\
error = (ddata, buf, type); \
/*************************************************************************************
这个函数就是控制使能与否的关键函数了。下面介绍本函数
**************************************************************************/
if (error) \
error; \
\
count; \
}
( *ddata,
const char *buf, int type)
{
int = (type);
/*************************************************************************************
这是是返回事件类型的最大值(因为后面要申请内存,所以这里需要对应本类时间的最大值)。可以查看实际上返回的书是。
**************************************************************************/
long *bits;
error;
int i;
bits = ((), (*bits),);
if (!bits)
-;
/*************************************************************************************
申请内存,跟上个函数一样,不清楚的查看上个函数的解析。
**************************************************************************/
error = (buf, bits, );
/*************************************************************************************
就是吧buf的数据,写入到bits所指向的内存中,是写入的数量。
函数定义在.c (lib)文件中,原型为:
int (const char *bp, *maskp, int )
{
char*nl = (bp, '\n');//
(
功能:查找字符串bp中首次出现字符'\n'的位置
说明:返回首次出现'\n'的位置的指针,返回的地址是被查找字符串指针开始的第一个与'\n'相同字符的指针,如果bp中不存在'\n'则返回NULL。
返回值:成功则返回要查找字符第一次出现的位置,失败返回NULL.
)
;
if(nl)
len= nl - bp; //字符串低一个字符到字符'\n'的长度
else
len= (bp);
list(bp, len, 0, maskp, );//该函数将在下面分析
}
函数定义在.c (fs\)中,现在只列出该参数:
int (const char *buf, int ,int , long *maskp,
)
该函数的作用是将的字符串转换成为位图形似,看一下其参数的意义:
第一个参数const char *buf是要转换的字符串
第二个参数 int 要转化的字符串的长度
第三个参数int ,用户,0代表是内核空间
第四个参数 long *maskp:将要写入的数据地址
第五个参数int :要写入数据的数量
返回0表示成功
**************************************************************************/
if (error)
goto out;
/* First */
for (i = 0; i < ddata->pdata->; i++) {
*bdata = &ddata->data[i];
if (bdata->->type != type)
;
if ((bdata->->code, bits) &&
!bdata->->) {
error = -;
goto out;
}
/*************************************************************************************
是为测试,就是测试bits第bdata->->code位是的值,返回的数据bits第bdata->->code位的值(0or1)。
**************************************************************************/
}
(&ddata->);
/*************************************************************************************
这里是申请互斥体,在前面的probe函数里面有(&ddata->)初始化了互斥体。
互斥锁的使用步骤如下:
1、定义互斥体
2、初始化互斥体 ()
3、尝试获取锁 ()
#########################################################################
临界区操作
#########################################################################
4、释放掉互斥体
**************************************************************************/
for (i = 0; i < ddata->pdata->; i++) {
*bdata = &ddata->data[i];
if (bdata->->type != type)
;
if ((bdata->->code, bits))
tton(bdata);
else
ton(bdata);
}
/*************************************************************************************
根据输入的数据选择是执行tton函数还是执行ton函数,其中tton关掉外部中断和取消定时器(这里的取消是指上次设定的时间没到的那次定时器作用),ton函数就是使能外部按键中断,这里
**************************************************************************/
(&ddata->); //释放掉互斥体
out:
kfree(bits); //释放掉申请的bits的内存
error;
}
有关按键的驱动的驱动初始化及其上报事件读写部分代码已经分析完毕,解析来看一下是卸载函数要执行的代码:
( *pdev)
{
*ddata = (pdev);
*input = ddata->input;
int i;
(&pdev->dev.kobj,&);
/*************************************************************************************
相对于(&pdev->dev.kobj,&);这里是吧初始化时建立的属性文件删除掉
**************************************************************************/
(&pdev->dev, 0);
for (i = 0; i < ddata->pdata->; i++)
(&ddata->data[i]);
/*************************************************************************************
void (ta *bdata)
{
(bdata->irq,bdata);
if(bdata->)
(&bdata->timer);
(&bdata->work);
if((bdata->->gpio))
(bdata->->gpio);
}
该函数主要是删除定时器,取消工作队列的工作,释放申请的引脚
**************************************************************************/
ice(input);//注销输入设备
/* If we have no data, we pdata .*/
if (!(&pdev->dev))
kfree(ddata->pdata);//释放pdata
kfree(ddata);//释放在probe申请的ddata的内存
0;
}
上面的是设备驱动层的核心代码,已经做出详细的介绍,在说明文档里面的\input\input-.txt,文件中有实例代码,有时间,可以去参考分析一下,这里就不在赘述。
有关输入子系统的介绍就到这里,在文章一开始的时候就有介绍过,Input子系统分为三层,从下至上分别是设备驱动层,输入核心层以及事件处理层,上面的介绍是围绕着设备驱动层进行的,接下来看一下事件驱动层。核心层的文件定义在Input.c (\input)文件中,这里仅仅对该文件做大概的介绍,看初始化函数:
int (void)
{
int err;
err =(&);
/*************************************************************************************
注册input类,这里也就是sysfs/class/目录下生成input目录。
这里,注册类和是一样的,实际上调用的是来实现类的注册的,对应的注销函数是,对应的注销函数是
**************************************************************************/
if (err) {
(" class\n");
err;
}
err = ();
/*************************************************************************************
创建proc/bus/input文件路径,并且在该路径下和设备文件及其他们分别对应的结构体。
**************************************************************************/
if (err)
goto fail1;
err =on(MKDEV(, 0), //
ES,"input");
/*************************************************************************************
一个字符驱动获取一个或多个设备编号,
第一个参数MKDEV(, 0):是你要分配的起始设备编号,次设备号一般为0,dev_t是个 32 位量,其中 12位用来表示主设备号,20位用来表示次设备号。
第二个参数ES,:请求的连续设备编号的总数
第三个参数"input":设备的名字;它会出现在 /proc/和 sysfs 中
假如成功返回0
**************************************************************************/
if (err) {
(" char major %d", );
goto fail2;
}
0;
fail2: ();
fail1: (&);
err;
}
这个初始化代码比较简单,就是申请一个输入字符设备,在sysfs/class/目录下生成input目录,在proc/bus/input生成目录并且生成相应的属性文件。主要是该文件还有许多定义的代码,当用户驱动代码的时候会调用这些程序,在这里不做介绍,待用到哪个函数时在去花时间去分析它。
接下来,看一下事件处理层,事件处理层文件主要是用来支持输入设备并与用户空间交互,这部分代码一般不需要我们自己去编写,因为Linux内核已经自带有一些事件处理器,可以支持大部分输入设备,比如Evdev.c(按键等对应的处理函数)、.c(鼠标等对应的处理函数)、.c(摇杆等对应的处理函数)等。对按键来说,用到的是Evdev.c文件(/input)。
首先看该文件对应的初始化函数:
int (void)
{
er(&);
/*************************************************************************************
这里是注册一个新的input ,就是要注册的,接下俩就要看一下这个时间处理(接口)结构体。
**************************************************************************/
}
对于,其为类型的结构体,先看一下(Input.h (\linux))结构体:
{
void *; //指向对应每一个驱动的所特有的数据
void (*event)( *, int type, int code, int value);
/*************************************************************************************
输入事件向内核报告会,内核需要调用的函数
**************************************************************************/
void (*)( *,
const *vals, count);
bool (*)( *, int type, int code, int value);
/*************************************************************************************
类似event,负责从分离出一般时间处理函数
**************************************************************************/
bool (*match)( *, *dev); //比较设备和处理函数的id
int (*)( *, *dev, const *id);
//连接和
void (*)( *);
void (*start)( *);
bool ;
int minor;
const char *name; //时间处理函数的名字
const *;
;///用于链接和此相关的
node;//用于将该链入
}
我们知道对应每一个输入设备,上报事件后最终内核用调用相应的函数去处理,在这里就是每一个都会有对应的(可以是一对多,也可以是多对一)来处理相应的事件。那么要找到与之对应的就需要一个东西来确定两者的对应关系,这个东西就是。其定义在Input.h(\linux\usb)中:
{
void *; //处理者()的特有数据
int open;//记录句柄()是否被打开,这样话就决定是否传递驱动事件
const char *name; //由处理者()给句柄()创建的名字
*dev; //输入设别结构体
*; //指向
; // 用于将此链入所属的链表
;//用于将此链入所属的链表
};
下面来缕缕 v它们三个之间的关系, 拥有 (用于链接和此相关的);也有 ;(用于链接和此相关的);相应的拥有分别连接它们俩的 (用于将此链入所属的链表), ;(用于将此链入所属的链表)。这样的话, 就把和绑定到一块了。现在的疑问是问什么它们俩不直接相互绑定,而需要一个中介来绑定它们俩呢,这里涉及到两个方面,其一是因为一个输入设备可能对应多个,对调用驱动的用户来说,用户可以选择本根据自身的需要调用哪一个;其二,一个输入处理函数可以对应多个输入设备,也就是说多个可共享一个。下面借鉴一张图: