1.前言
本文将实现一个简单的字符驱动程序,同时在驱动程序的内核模块中注册一个中断处理函数,用来获取键盘的扫描码并存入缓冲区中,最后使用用户态程序访问字符驱动程序,取出键盘扫描码。
中断常用于键盘、鼠标等IO设备,当用户按下键盘时,会产生一个键盘扫描码。例如下面ps/2接口的键盘扫描码,它使用一个数字或数字序列来表示分配到键盘上的每个按键。
2.内核模块初始化
内核模块在初始化的时候不仅仅需要注册一个字符设备,现在还需要在模块初始化的时候就向系统中的中断请求队列挂入我们自己的中断服务程序,也就是对其进行注册,使用的函数是
1 | request_irq(unsigned int irq, |
由于我们要监控键盘的输入中断,当 I/O 设备把中断信号发送给中断控制器时,与之关联的是一个中断号。因此应当使用键盘控制器的中断号。查看proc文件系统下的中断号分配情况:
1 | cat /proc/interrupts |
可以看到键盘控制器intel 8042的中断号为1。因此注册中断的时候中断号应当设置为1。
8042负责读取键盘扫描码并将其存在缓冲器中供程序读取;另外还有一个芯片ECE1077,它负责连接键盘和EC,将键盘动作转换成扫描码。CPU通过两个IO端口与8042通信,这两个IO端口就是0x60,0x64端口。
8042有四个8位的寄存器,它们是输入寄存器(RO)、输出寄存器(WO)、状态寄存器(RO)和命令寄存器(R/W)。
读输出寄存器:inb(0x60);
写输入寄存器:outb(0x60,data);
读状态寄存器:inb(0x64);
b:代表一个字节。
outb() I/O 上写入 8 位数据 ( 1 字节 )。
outw() I/O 上写入 16 位数据 ( 2 字节 );
outl () I/O 上写入 32 位数据 ( 4 字节)。
状态寄存器:
Bit7: PARITY-EVEN(P_E): 从键盘获得的数据奇偶校验错误
Bit6: RCV-TMOUT(R_T): 接收超时,置1
Bit5: TRANS_TMOUT(T_T): 发送超时,置1
Bit4: KYBD_INH(K_I): 为1,键盘没有被禁止。为0,键盘被禁止。
Bit3: CMD_DATA(C_D): 为1,输入缓冲器中的内容为命令,为0,输入缓冲器中的内容为数据。
Bit2: SYS_FLAG(S_F): 系统标志,加电启动置0,自检通过后置1
Bit1: INPUT_BUF_FULL(I_B_F): 输入缓冲器满置1,i8042 取走后置0
Bit0: OUT_BUF_FULL(O_B_F): 输出缓冲器满置1,CPU读取后置0
命令寄存器:
Bit7: 保留,应该为0
Bit6: 将第二套扫描码翻译为第一套
Bit5: 置1,禁止鼠标
Bit4: 置1,禁止键盘
Bit3: 置1,忽略状态寄存器中的 Bit4
Bit2: 设置状态寄存器中的 Bit2
Bit1: 置1,enable 鼠标中断
Bit0: 置1,enable 键盘中断
有了以上知识,再对照键盘的扫描码表,我们就可以编写程序读取并判断键盘输入的数据,并根据不同的按键执行不同的动作。
内核模块处理函数中,还申请了一个kfifo环形缓冲区,用于存储我们获取到的键盘扫描码。
1 |
|
剩余的工作就全都是注册一个字符设备驱动。
1 |
|
3.中断处理函数
上面在注册中断的时候,加入了一个回调函数key_int_handler,但中断发生的时候,就会执行该函数,该函数由我们来编写。使用中断下半部的工作队列机制将处理推迟,或者进行调度。
首先使用DECLARE_WORK声明一个工作队列,被推迟的任务都叫做工作,放到该工作队列中。
描述工作的数据结构是work_struct。
内核里一直运行类似worker thread,它会对工作队列中的work进行处理,大致的工作流程原理可以参考下图所示:
这里的work则是work_struct变量,并且绑定一个执行函数——typedef void (*work_func_t)(struct work_struct *work);。在worker thread中会对非空的工作队列进行工作队列的出队操作,并运行work绑定的函数。
1 |
|
1 | // 创建需要推后完成到工作,名为key_work,待执行函数为key_work_func |
工作创建好以后,我们可以调度它,使用schedule_work提交给工作线程。其中inb(0x60),用于从0x60 IO端口读取扫描码。
下面看工作队列调度处理函数,主要完成的工作就是将前面获取到的扫描码,存入到fifo队列,同时唤醒等待读取的字符驱动数据的进程。
1 |
|
4. 字符处理函数
实现字符驱动程序的读函数,读取fifo队列中的键盘扫描符,拷贝到用户空间。
1 | static ssize_t myread(struct file *filp, char __user *buf, size_t count, loff_t *pos) { // 读设备函数 |
用户空间函数如下:
1 |
|
5. 程序运行
Makefile 文件如下:
1 | #Makefile文件注意:假如前面的.c文件起名为first.c,那么这里的Makefile文件中的.o文件就要起名为first.o 只有root用户才能加载和卸载模块 |
可以看到注册成功并输出了键盘的扫描符与统计的次数。
用户态程序,按照字符串格式输出了键盘的扫描符。
完整程序如下:
1 | upper.c |
1 | user.c |