认识linux input子系统 (一)
-0-.序
本来只是想写个内核态的键盘记录的,但是发现现在的linux驱动模型已经和以前版本不同,增加了input层,几乎所有的底层驱动都把数据封装在event里上报给input子系统,这样一来,kernel看起来更加模块化,但是没有原来键盘驱动那种一站通的感觉了。
于是研究起input层比起键盘记录更有意思了:)这里只是记录下自己学习后理清的思路,其实自己学习过程挺乱的,最近才有所感悟input层,毕竟硬件的底子我是没有的。
-1-.从用户层看input(event事件)
经常捣鼓linux一定会对/dev,/sys,/proc这几个目录有所印象,这是从内核导出到用户层的接口(从这里几乎可以观览内核)。这下就方便了,kernel为我们导出了input在用户态的接口,就是/dev/input/下的接口,这里我们只关注此目录下的eventX字符设备。
那么这些eventX是干什么用的?简单来说就是我们对计算机的输入(包括敲击键盘,移动鼠标等等操作)经过内核(底层驱动,input)处理最后就上报到这些eventX里面了。
而这里event0,event1,..就是用来区分各个外设的,可以通过命令来查看外设具体和哪个event相关联:
cat /proc/bus/input/devices 这里结果比较多,应为现在PC外设也蛮多的,我们可以看下键盘对应的条目,这里我截取2段:
I: Bus=0011 Vendor=0001 Product=0001 Version=ab54
N: Name="AT Translated Set 2 keyboard"
P: Phys=isa0060/serio0/input0
S: Sysfs=/devices/platform/i8042/serio0/input/input4
U: Uniq=
H: Handlers=kbd event4
B: EV=120013
B: KEY=4 2000000 3803078 f800d001 feffffdf ffefffff ffffffff fffffffe
B: MSC=10
B: LED=7
I: Bus=0003 Vendor=046d Product=c52f Version=0111
N: Name="Logitech USB Receiver"
P: Phys=usb-0000:00:1d.0-1/input1
S: Sysfs=/devices/pci0000:00/0000:00:1d.0/usb6/6-1/6-1:1.1/input/input8
U: Uniq=
H: Handlers=kbd event8
B: EV=1f
B: KEY=837fff 2c3027 bf004444 0 0 1 f84 8a27c000 667bfa d9415fed 8e0000 0 0 0
B: REL=40
B: ABS=1 0
B: MSC=10
从上面我们可以看到H:一行是 Handlers=kbd event4(或Handlers=kbd event8)
kbd(KEYBOARD)代表键盘,而后面eventX就是此键盘在/dev/input/下就对应的eventX字符设备,
我们可以看到,linux为我准备了2个驱动,分别是AT键盘和USB键盘,这里我的笔记本使用的是AT键盘(又叫PS/2键盘,一般笔记本自带的键盘都是AT兼容的)对应于/dev/input/event4,
bash> hexdump /dev/input/event4 [现在我们随意敲击键盘,会发现大量数据涌现]
其实这些数据都是组织好的,在linux/input.h中有这些数据的结构:
struct input_event {
struct timeval time; //事件发生的时间
__u16 type; //事件类类型:按键和移动鼠标就是不同类型
__u16 code;
__s32 value; //事件值:按键a和按键b就对应不同值
};
这里事件指的是我们对外设的操作,比如按键一次a可能就产生数个input_event数据
有了这个结构就好办了,我们可以自己写个测试程序读取/dev/input/event4中的数据:
#include <stdio.h> #include <sys/time.h> #include <linux/input.h> #include <stdlib.h> void usage(char *str) { fprintf(stderr, "<usage> %s /dev/input/eventX/n", str); exit(1); } int main(int argc,char **argv) { FILE *fp; struct input_event ie; if (argc != 2) usage(argv[0]); fp = fopen(argv[1], "r"); if (fp == NULL) { perror(argv[1]); exit(1); } while (1) { fread((void *)&ie, sizeof(ie), 1, fp); if (ferror(fp)) { perror("fread"); exit(1); } printf("[timeval:sec:%d,usec:%d,type:%d,code:%d,value:%d]/n", ie.time.tv_sec, ie.time.tv_usec, ie.type, ie.code, ie.value); } return 0; }
编译成测试test1
bash> test1 /dev/input/event4 [按回车运行程序]
[timeval:sec:1285305058,usec:857706,type:4,code:4,value:28]
[timeval:sec:1285305058,usec:857718,type:1,code:28,value:0]
[timeval:sec:1285305058,usec:857721,type:0,code:0,value:0] [之后按键a]
[timeval:sec:1285305059,usec:978376,type:4,code:4,value:30]
[timeval:sec:1285305059,usec:978401,type:1,code:30,value:1]
[timeval:sec:1285305059,usec:978406,type:0,code:0,value:0]
a[timeval:sec:1285305060,usec:34315,type:4,code:4,value:30] [行首显示我在此终端按下的a]
[timeval:sec:1285305060,usec:34327,type:1,code:30,value:0]
[timeval:sec:1285305060,usec:34329,type:0,code:0,value:0]
[timeval:sec:1285305061,usec:406962,type:4,code:4,value:29]
运行程序后,我们首先看到3组input_event显示出来了,这三组数据其实是我们刚刚运行程序时按的回车键的UP码
之后我们每按一次键都会有6组input_event显示出来,前3组是DOWN码,后3组是UP码(DOWN是按键被按下,UP是按键弹起,键盘有弹性:P)
其实这个程序写完善,配合分析input_event输出便可以做个简单的用户态按键记录了。
可以对照linux/input.h中分析input_event的结果,比如input_event.value值为30 对应于#define KEY_A 30 显然这是按键a被按下的值
既然eventX可以读,那么eventX当然可以写,写对应的结果就是 模拟人敲击键盘,蛮有趣的,这有个完整的写eventX测试代码,
由于我们可能对input_event不是很了解,所以先从eventX读input_event,稍加修改再回写进eventX.
#include <stdio.h> #include <sys/time.h> #include <linux/input.h> #include <stdlib.h> void usage(char *str) { fprintf(stderr, "<usage> %s /dev/input/eventX/n", str); exit(1); } int main(int argc,char **argv) { FILE *fp; struct input_event ie[6]; if (argc != 2) usage(argv[0]); /* 先读取回车UP码对应的3个input_event 并将其丢弃 */ fp = fopen(argv[1], "r"); if (fp == NULL) { perror(argv[1]); exit(1); } fread((void *)ie, sizeof(struct input_event), 3, fp); if (ferror(fp)) perror("fread"); fclose(fp); /* 循环读写eventX 每次读取完整按键码:6组input_event * 稍加修改并写回eventX */ do { fp = fopen(argv[1], "r"); if (fp == NULL) { perror(argv[1]); exit(1); } fread((void *)ie, sizeof(struct input_event), 6, fp); if (ferror(fp)) perror("fread"); fclose(fp); fp = fopen(argv[1], "w"); if (fp == NULL) { perror(argv[1]); exit(1); } ie[1].code += 1; ie[4].code += 1; fwrite((void *)ie, sizeof(struct input_event), 6, fp); if (ferror(fp)) { perror("fwrite"); exit(1); } fclose(fp); // printf("[timeval:sec:%d,usec:%d,type:%d,code:%d,value:%d]/n", // ie.time.tv_sec, ie.time.tv_usec, // ie.type, ie.code, ie.value); } while (1); return 0; }
编译成test2
bash> test2 /dev/input/event4 [按回车]
asbncv [连续敲击abc]
这里KEY_A == 30 KEY_S == 31(即ie[1].code += 1;ie[4].code += 1;)
以上测试代码有几个值得注意的地方:
1.首先要清除我们运行测试程序时 回车的UP码
2.eventX不能同时读写,先读出1次敲击键盘的完整6个input_event,再回写入eventX(若不这样做,测试不会成功)
3.input_event.time不用修改,因为此项被内核忽视([?]从evdev_write()中可以看出内核并没有检测时间,而是直接响应事件)
经过测试,更改第0,3项input_event.value并没有反映,更改第1,4项的code才有反映([?]这里更改此项只是欺骗Xwindows,Xwindows解析/dev/input/eventX向用户回显外设操作,至于详细input_event解析就要到Xwindows代码中去找了,我没看过不敢多说)
这个测试程序只是简单模拟了按键事件
-----------------------------------------------------------------------------
发现语言不简洁,之后是从内核,从驱动看input:)