0%

Linux-Lab4-IO access and Interrupts

IO access and Interrupts

实验目的:

  • 与外围设备通信
  • 实现中断处理程序
  • 将中断与进程上下文同步

外围设备通过 Read/Write 其寄存器进行控制:

  • 通常,设备具有多个寄存器,可以在内存地址空间或 I/O 地址空间中的连续地址访问这些寄存器
  • 连接到 I/O 总线的每个设备都有一组 I/O 地址,称为 I/O 端口
  • I/O 端口可以映射到物理内存地址,以便处理器可以通过直接与内存配合使用的指令与设备通信
  • 为简单起见,我们将直接使用 I/O 端口(不映射到物理内存地址)与物理设备进行通信

每个器件的 I/O 端口被结构化为一组专用寄存器,以提供统一的编程接口,大多数设备将具有以下类型的寄存器:

  • Control registers:接收设备命令
  • Status registers:包含有关设备内部状态的信息
  • Input registers:从设备中获取数据 - Read
  • Output registers:在其中写入数据并传输给设备 - Write

Accessing the hardware

在 Linux 中,I/O 端口访问在所有体系结构上实现,并且可以使用多个 API

在访问 I/O 端口之前,首先必须请求访问它们,以确保只有一个用户在使用:

1
2
3
4
#include <linux/ioport.h>

struct resource *request_region(unsigned long first, unsigned long n,
const char *name);
  • first:IO 端口的基地址
  • n:IO 端口占用的范围
  • name:使用这段 IO 地址的设备名

要释放保留区域 resource,必须使用以下函数:

1
void release_region(unsigned long start, unsigned long n);

使用案例如下:

1
2
3
4
5
6
7
8
9
10
11
#include <linux/ioport.h>

#define MY_BASEPORT 0x3F8
#define MY_NR_PORTS 8

if (!request_region(MY_BASEPORT, MY_NR_PORTS, "com1")) {
/* handle error */
return -ENODEV;
}

release_region(MY_BASEPORT, MY_NR_PORTS);

所有端口请求都可以通过文件从用户空间看到:/proc/ioports

1
2
3
4
5
6
7
8
9
10
11
12
root@qemux86:~# cat /proc/ioports                                               
0000-0cf7 : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-0060 : keyboard
0064-0064 : keyboard
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu

驱动程序获得所需的 I/O 端口范围后,可以在这些端口上执行读取或写入操作:

1
2
3
4
5
6
7
unsigned inb(int port); /* reads one byte (8 bits) from port */
unsigned inw(int port); /* reads two bytes (16-bit) from ports */
unsigned inl (int port); /* reads four bytes (32-bits) from port */

void outb(unsigned char byte, int port); /* writes one byte (8 bits) to port */
void outw(unsigned short word, int port); /* writes two bytes (16-bits) to port */
void outl(unsigned long word, int port); /* writes four bytes (32-bits) to port */
  • 读取出来的字符并不是 ASCII,而是注册表值 scancode
  • 我们只需要在按下时选择代码,然后解码 ASCII 字符
  • PS:键盘 “按下时” 和 “松开时” 是两个不同的 scancode,后面的 is_key_press 用于展示这个特点

Interrupt handling

与其他资源一样,驱动程序必须先访问 Interrupt handling 中断处理程序,然后才能使用它,并在执行结束时释放它:

1
2
3
4
5
6
7
8
#include <linux/interrupt.h>

typedef irqreturn_t (*irq_handler_t)(int, void *);

int request_irq(unsigned int irq_no, irq_handler_t handler,
unsigned long flags, const char *dev_name, void *dev_id);

void free_irq(unsigned int irq_no, void *dev_id);
  • 中断处理程序函数在中断上下文中执行,这意味着无法调用阻塞 API
  • 必须避免在中断处理程序中执行大量工作,而是在需要时使用延迟工作

中断处理程序函数的签名:

1
irqreturn_t (*handler)(int irq_no, void *dev_id);
  • irq_no:中断编号
  • irqerturn_t:标识返回信息
    • IRQ_NONE:中断不适用于此设备(共享中断)
    • IRQ_HANDLED:中断可以直接在中断上下文中处理
    • IRQ_WAKE_THREAD:计划进程上下文处理函数的运行

实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <linux/interrupt.h>

#define MY_BASEPORT 0x3F8
#define MY_IRQ 4

irqreturn_t my_handler(int irq_no, void *dev_id)
{
struct my_device_data *my_data = (struct my_device_data *) dev_id;
/* if interrupt is not for this device (shared interrupts) */
/* return IRQ_NONE;*/

/* clear interrupt-pending bit */
/* read from device or write to device*/
return IRQ_HANDLED;
}

static my_init(void)
{
[...]
struct my_device_data *my_data;
int err;

err = request_irq(MY_IRQ, my_handler, IRQF_SHARED,
"com1", my_data);
if (err < 0) {
/* handle error*/
return err;
}
[...]
}

有关系统中断的信息和统计信息可以在 /proc/interrupt/proc/stat 中找到

1
2
3
4
5
6
7
8
root@qemux86:~# cat /proc/interrupts                                            
CPU0
0: 71 IO-APIC 2-edge timer
1: 9 IO-APIC 1-edge i8042
9: 0 IO-APIC 9-fasteoi acpi
10: 403 IO-APIC 10-fasteoi virtio1, virtio2, virtio5
11: 22 IO-APIC 11-fasteoi virtio3, virtio4, virtio0
12: 125 IO-APIC 12-edge i8042

Locking

由于中断处理程序在中断上下文中运行,因此可以执行的操作受到限制:

  • 无法访问用户空间内存
  • 无法调用阻塞函数,因此不能使用互斥锁(中断发生时,程序会把当前进程的上下文保存到内核栈上,称为中断帧,如果在中断中发生阻塞,schedule 新调用的进程很可能会破坏中断帧),其实这是为了实现中断嵌套所付出的代价
  • 使用自旋锁进行同步也很棘手(如果所使用的自旋锁,已被正在运行的处理程序中断的进程获取,则可能导致死锁)

在某些情况下,设备驱动程序必须使用中断进行同步(例如,当数据在中断处理程序和进程上下文或下半部分处理程序之间共享时),在这些情况下,有必要停用中断并使用自旋锁:

1
2
3
4
5
void spin_lock_irqsave (spinlock_t * lock, unsigned long flags); /* 保存中断的当前状态,禁止本地中断,获取指定的锁 */
void spin_unlock_irqrestore (spinlock_t * lock, unsigned long flags); /* 对指定的锁进行解锁,并恢复到加锁之前的状态 */

void spin_lock_irq (spinlock_t * lock); /* 禁止本地中断,获取指定的锁 */
void spin_unlock_irq (spinlock_t * lock); /* 对指定的锁进行解锁,恢复本地中断 */

为了使用在进程上下文和中断处理例程之间共享的资源,将按如下方式使用上述功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static spinlock_t lock;

/* IRQ handling routine: interrupt context */
irqreturn_t kbd_interrupt_handle(int irq_no, void * dev_id)
{
...
spin_lock(&lock);
/* Critical region - access shared resource */
spin_unlock (&lock);
...
}

/* Process context: Disable interrupts when locking */
static void my_access(void)
{
unsigned long flags;

spin_lock_irqsave(&lock, flags);
/* Critical region - access shared resource */
spin_unlock_irqrestore(&lock, flags);

...
}

void my_init (void)
{
...
spin_lock_init (&lock);
...
}
  • 因为系统硬中断 kbd_interrupt_handle 在任何时候都可以发生(内核会直接抢占原来的进程,从而执行硬中断)
  • 如果在 my_access 中拿了自旋锁之后被 kbd_interrupt_handle 抢占,就会发生死锁
  • 如果不在 kbd_interrupt_handle 中加锁,又可能会破坏共享数据(例如:在引用指针之前置空了指针)
  • 因此需要使用 spin_lock_irqsave 在加锁的同时禁止硬中断

Exercises

要解决练习,您需要执行以下步骤:

  • 从模板准备 skeletons
  • 构建模块
  • 将模块复制到虚拟机
  • 启动 VM 并在 VM 中测试模块
1
2
3
make clean
LABS=interrupts make skels
make build

直接看完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/uaccess.h>
#include <linux/ioport.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>

MODULE_DESCRIPTION("KBD");
MODULE_AUTHOR("Kernel Hacker");
MODULE_LICENSE("GPL");

#define MODULE_NAME "kbd"

#define KBD_MAJOR 42
#define KBD_MINOR 0
#define KBD_NR_MINORS 1

#define I8042_KBD_IRQ 1
#define I8042_STATUS_REG 0x64
#define I8042_DATA_REG 0x60

#define BUFFER_SIZE 1024
#define SCANCODE_RELEASED_MASK 0x80

struct kbd {
struct cdev cdev;
/* TODO 3: add spinlock */
spinlock_t lock;
char buf[BUFFER_SIZE];
size_t put_idx, get_idx, count;
} devs[1];

/*
* Checks if scancode corresponds to key press or release.
*/
static int is_key_press(unsigned int scancode)
{
return !(scancode & SCANCODE_RELEASED_MASK);
}

/*
* Return the character of the given scancode.
* Only works for alphanumeric/space/enter; returns '?' for other
* characters.
*/
static int get_ascii(unsigned int scancode)
{
static char *row1 = "1234567890";
static char *row2 = "qwertyuiop";
static char *row3 = "asdfghjkl";
static char *row4 = "zxcvbnm";

scancode &= ~SCANCODE_RELEASED_MASK;
if (scancode >= 0x02 && scancode <= 0x0b)
return *(row1 + scancode - 0x02);
if (scancode >= 0x10 && scancode <= 0x19)
return *(row2 + scancode - 0x10);
if (scancode >= 0x1e && scancode <= 0x26)
return *(row3 + scancode - 0x1e);
if (scancode >= 0x2c && scancode <= 0x32)
return *(row4 + scancode - 0x2c);
if (scancode == 0x39)
return ' ';
if (scancode == 0x1c)
return '\n';
return '?';
}

static void put_char(struct kbd *data, char c)
{
if (data->count >= BUFFER_SIZE)
return;

data->buf[data->put_idx] = c;
data->put_idx = (data->put_idx + 1) % BUFFER_SIZE;
data->count++;
}

static bool get_char(char *c, struct kbd *data)
{
/* TODO 4: get char from buffer; update count and get_idx */
if (data->count > 0){
*c = data->buf[data->get_idx];
data->get_idx = (data->get_idx + 1) % BUFFER_SIZE;
data->count--;
return true;
}

return false;
}

static void reset_buffer(struct kbd *data)
{
/* TODO 5: reset count, put_idx, get_idx */
printk("reset");
while(data->count != 0){
data->buf[data->put_idx] = 0;
data->put_idx = (data->put_idx - 1) % BUFFER_SIZE;
data->count --;
}
}

/*
* Return the value of the DATA register.
*/
static inline u8 i8042_read_data(void)
{
u8 val;
/* TODO 3: Read DATA register (8 bits). */
val = inb(I8042_DATA_REG);
return val;
}

/* TODO 2: implement interrupt handler */
/* TODO 3: read the scancode */
/* TODO 3: interpret the scancode */
/* TODO 3: display information about the keystrokes */
/* TODO 3: store ASCII key to buffer */
irqreturn_t kbd_interrupt_handler(int irq_no,void* dev_id)
{
struct kbd* data = (struct kbd*)dev_id;
u8 scancode = i8042_read_data();
int pressed = is_key_press(scancode);
int key = get_ascii(scancode);

pr_info("IRQ: %d, scancode = 0x%x (%u), pressed = %d, ch = %c\n",
irq_no, scancode, scancode, pressed, key);
if(!pressed){
return IRQ_NONE;
}

spin_lock(&data->lock);
put_char(data,key);
spin_unlock(&data->lock);
return IRQ_NONE;
}

static int kbd_open(struct inode *inode, struct file *file)
{
struct kbd *data = container_of(inode->i_cdev, struct kbd, cdev);

file->private_data = data;
pr_info("%s opened\n", MODULE_NAME);
return 0;
}

static int kbd_release(struct inode *inode, struct file *file)
{
pr_info("%s closed\n", MODULE_NAME);
return 0;
}

/* TODO 5: add write operation and reset the buffer */
static ssize_t kbd_write(struct file *file, const char __user *user_buffer,
size_t size, loff_t *offset)
{
struct kbd *data = (struct kbd*) file->private_data;
unsigned long flag;

spin_lock_irqsave(&data->lock,flag);
reset_buffer(data);
spin_unlock_irqrestore(&data->lock,flag);

return size;
}

static ssize_t kbd_read(struct file *file, char __user *user_buffer,
size_t size, loff_t *offset)
{
struct kbd *data = (struct kbd *) file->private_data;
size_t read = 0;
unsigned long flag;
char ch;
bool more = true;

/* TODO 4: read data from buffer */

while(size--){
spin_lock_irqsave(&data->lock,flag);
more = get_char(&ch,data);
spin_unlock_irqrestore(&data->lock,flag);

if(!more)
break;
if(put_user(ch,user_buffer++))
return -EFAULT;
read++;
}

return read;
}

static const struct file_operations kbd_fops = {
.owner = THIS_MODULE,
.open = kbd_open,
.release = kbd_release,
.read = kbd_read,
/* TODO 5: add write operation */
.write = kbd_write,
};

static int kbd_init(void)
{
int err;
err = register_chrdev_region(MKDEV(KBD_MAJOR, KBD_MINOR),
KBD_NR_MINORS, MODULE_NAME);
if (err != 0) {
pr_err("register_region failed: %d\n", err);
goto out;
}

/* TODO 1: request the keyboard I/O ports */
if(!request_region(I8042_DATA_REG+1,KBD_NR_MINORS, MODULE_NAME)) {
err = -EBUSY;
goto out_unregister;
}
if (!request_region(I8042_STATUS_REG+1, KBD_NR_MINORS, MODULE_NAME)) {
err = -EBUSY;
goto out_unregister;
}

/* TODO 3: initialize spinlock */
spin_lock_init(&devs[0].lock);
/* TODO 2: Register IRQ handler for keyboard IRQ (IRQ 1). */
err = request_irq(I8042_KBD_IRQ,kbd_interrupt_handler,
IRQF_SHARED,MODULE_NAME,&devs[0]);
if(err){
goto out_release_regions;
}

cdev_init(&devs[0].cdev, &kbd_fops);
cdev_add(&devs[0].cdev, MKDEV(KBD_MAJOR, KBD_MINOR), 1);

pr_notice("Driver %s loaded\n", MODULE_NAME);
return 0;

/*TODO 2: release regions in case of error */
out_release_regions:
release_region(I8042_STATUS_REG+1, KBD_NR_MINORS);
release_region(I8042_DATA_REG+1, KBD_NR_MINORS);
out_unregister:
unregister_chrdev_region(MKDEV(KBD_MAJOR, KBD_MINOR),
KBD_NR_MINORS);
out:
return err;
}

static void kbd_exit(void)
{
cdev_del(&devs[0].cdev);

/* TODO 2: Free IRQ. */
free_irq(I8042_KBD_IRQ,&devs[0]);
/* TODO 1: release keyboard I/O ports */
release_region(I8042_STATUS_REG+1,KBD_NR_MINORS);
release_region(I8042_DATA_REG+1,KBD_NR_MINORS);
unregister_chrdev_region(MKDEV(KBD_MAJOR, KBD_MINOR),
KBD_NR_MINORS);
pr_notice("Driver %s unloaded\n", MODULE_NAME);
}

module_init(kbd_init);
module_exit(kbd_exit);
  • 注意:当 insmod 这个驱动程序时可能会报错
1
2
3
root@qemux86:~# insmod skels/interrupts/kbd.ko
kbd: loading out-of-tree module taints kernel.
insmod: can't insert 'skels/interrupts/kbd.ko': Device or resource busy
  • 这是因为键盘 IO 已经有对应的驱动了:
1
2
3
root@qemux86:~# cat /proc/ioports | egrep "(0060|0064)"
0060-0060 : keyboard
0064-0064 : keyboard
  • 键盘 I/O 端口是在引导期间由内核注册的,我们将无法删除关联的模块
  • 因此,我们需要欺骗内核并注册 0x61 和 0x65 端口
1
2
3
4
5
6
7
root@qemux86:~/skels/interrupts# insmod kbd.ko                                  
kbd: loading out-of-tree module taints kernel.
Driver kbd loaded
root@qemux86:~/skels/interrupts# cat /proc/ioports | grep kbd
0061-0061 : kbd
root@qemux86:~/skels/interrupts# rmmod kbd
Driver kbd unloaded
  • 当中断注册完成以后,可以在 /proc/interrupts 中进行查看:
1
2
3
4
5
6
7
8
root@qemux86:~/skels/interrupts# cat /proc/interrupts                           
CPU0
0: 68 IO-APIC 2-edge timer
1: 9 IO-APIC 1-edge i8042, kbd
9: 0 IO-APIC 9-fasteoi acpi
10: 452 IO-APIC 10-fasteoi virtio1, virtio2, virtio5
11: 13 IO-APIC 11-fasteoi virtio3, virtio4, virtio0
12: 125 IO-APIC 12-edge i8042
  • 通过如下命令可以在虚拟机中打开键盘:(用来查看 kbd_interrupt_handler 是否正确)
1
QEMU_DISPLAY=gtk make boot 
  • 被这个多线程和锁搞得头痛,有时候莫名其妙陷入死锁,尝试使用 printk 打印时还报错,看了答案以后进行了大刀阔斧的修改才解决了问题
  • 另外 i8042_read_data 实际上是使用了系统自带的键盘 IO 端口(程序中自己申请的 IO 端口其实就是花架子,不影响程序流程)
  • 在程序输入输出的地方都需要用锁,但是如果在程序拿到锁的情况下发生中断,中断中也要拿同一个锁的情况下就会发生问题(硬件中断在任何时候都会发生)
1
2
3
spin_lock_irqsave(&data->lock,flag);
more = get_char(&ch,data);
spin_unlock_irqrestore(&data->lock,flag);
  • 所以使用 spin_lock_irqsave 在加锁的同时禁止中断

仔细对比了答案以后,又分析了一下之前死锁的原因,感觉问题应该出在 cat 中:

  • kbd_open 正常执行
  • kbd_read 有返回但是无限循环
  • kbd_release 根本不会执行,程序卡死在 kbd_read

我之前的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static ssize_t kbd_read(struct file *file,  char __user *user_buffer,
size_t size, loff_t *offset)
{
struct kbd *data = (struct kbd *) file->private_data;
size_t read = min(BUFFER_SIZE - *offset, size);
unsigned long flag;
char ch;
bool more = true;
int i = 0;

if(read == 0){
printk("read full");
return 0;
}

/* TODO 4: read data from buffer */
for(i=0;i<read;i++){
spin_lock_irqsave(&data->lock,flag);
more = get_char(&ch,data);
spin_unlock_irqrestore(&data->lock,flag);

if(!more)
break;
if(put_user(ch,user_buffer++))
return -EFAULT;
}

return read;
}
  • 这个函数有个很明显的问题,就是 Read 的返回值不对(刚开始也没考虑这些问题)
  • 把它修改为以下代码后就没有问题了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static ssize_t kbd_read(struct file *file,  char __user *user_buffer,
size_t size, loff_t *offset)
{
struct kbd *data = (struct kbd *) file->private_data;
size_t read = 0;
unsigned long flag;
char ch;
bool more = true;
int i = 0;

if(size == 0){
printk("read full");
return 0;
}

/* TODO 4: read data from buffer */
for(i=0;i<size;i++){
spin_lock_irqsave(&data->lock,flag);
more = get_char(&ch,data);
spin_unlock_irqrestore(&data->lock,flag);

if(!more)
break;
if(put_user(ch,user_buffer++))
return -EFAULT;
read ++;
}

return read;
}