认识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:)