元芳你怎么看

本网站主要用于记录我个人学习的内容,希望对你有所帮助

0%

环境配置

DPDK系统要求:

Kernel version >= 4.14
GCC(版本 5.0+)或 Clang(版本 3.6+)
Python 3.6 或更高版本
Meson (version 0.53.2+) and ninja
pyelftools (版本 0.22+)

笔者环境:

内核版本: 6.6.32-1-lts
发行版: ArchLinux
gcc 版本 14.1.1 20240522
Python 3.12.3
Meson 1.4.1
pyelftools 0.31-2

环境配置

  1. 检查当前内核是否支持
1
2
3
4
5
6
7
8
9
10
# 查看当前内核是否支持
zgrep -E "HUGETLBFS|PROC_PAGE_MONITOR" /proc/config.gz
CONFIG_PROC_PAGE_MONITOR=y
CONFIG_HUGETLBFS=y
# 查看高精度事件定时器
zgrep -E "HPET" /proc/config.gz
CONFIG_HPET_TIMER=y
CONFIG_HPET_EMULATE_RTC=y
CONFIG_HPET=y
# CONFIG_HPET_MMAP is not set

如果不支持HUGETLBFS或者PROC_PAGE_MONITOR,重编内核。

  1. 安装上述环境
1
2
sudo pacman -S gcc clang pkgconf python meson ninja python-pyelftools numactl
sudo pacman -S libarchive libelf
  1. 配置大页

最简单的配置,只使用2MB的大页,没有配置1GB的,如果有需要可以官网查教程。

非root用户使用sudo vim操作下面文件填写自己认为适合的数值,不太离谱都行。

1
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

HugePages_Total不为0就代表成功。

1
2
3
4
5
6
7
8
9
10
➜  ~ cat /proc/meminfo | grep Huge
AnonHugePages: 3670016 kB
ShmemHugePages: 0 kB
FileHugePages: 190464 kB
HugePages_Total: 1024
HugePages_Free: 1024
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 2097152 kB

编译安装

下载一个自己喜欢的DPDK版本,懒得话就:

1
git clone http://dpdk.org/git/dpdk --depth=1

进入项目主目录:

1
2
3
4
meson setup build
cd build
ninja
sudo meson install

编译安装完成后,查看路径/usr/local/lib/pkgconfig有无和libdpdk.pc,没有的话全局找一下该文件路径,并且在~/.zshrc添加export PKG_CONFIG_PATH=/path/to/,我本机设置export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig,完成后进行下面命令:

1
sudo ldconfig

Hello World

简单运行一下:

1
2
3
4
cd examples/helloworld
make
cd build
sudo ./helloworld

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
➜  build git:(main) sudo ./helloworld
EAL: Detected CPU lcores: 16
EAL: Detected NUMA nodes: 1
EAL: Detected shared linkage of DPDK
EAL: Multi-process socket /var/run/dpdk/rte/mp_socket
EAL: Selected IOVA mode 'VA'
TELEMETRY: No legacy callbacks, legacy socket not created
hello from core 1
hello from core 2
hello from core 3
hello from core 4
hello from core 5
hello from core 6
hello from core 7
hello from core 8
hello from core 9
hello from core 10
hello from core 11
hello from core 12
hello from core 13
hello from core 14
hello from core 15
hello from core 0

前言

该文章仅用于个人阅读笔记使用,如有错误,敬请指正。

什么是DPDK?

什么是DPDK?对于用户来说,它可能是一个性能出色的包数据处理加速软件库;对于开发者来说,它可能是一个实践包处理新想法的创新工场;对于性能调优者来说,它可能又是一个绝佳的成果分享平台。当下火热的网络功能虚拟化,则将DPDK放在一个重要的基石位置。

DPDK最初的动机很简单,就是证明IA多核处理器能够支撑高性能数据包处理。随着早期目标的达成和更多通用处理器体系的加入,DPDK逐渐成为通用多核处理器高性能数据包处理的业界标杆。

IA不适合进行数据包处理吗?

Linux系统为例,内核栈处理数据包流程:

  1. 数据包到达网卡。
  2. 网卡通过DMA将数据包复制到内存中的接收缓冲区。
  3. 网卡发送中断信号通知CPU有新的数据包到达。
  4. 中断处理程序被触发,处理该中断。
  5. 驱动程序将数据包从网卡的接收缓冲区移到内核的网络栈中,并将其放入适当的队列中。
  6. 内核网络栈对数据包进行处理(例如,协议栈处理,IP、TCP/UDP等)。
  7. 根据数据包的目的地,内核决定下一步操作:
    • 如果数据包需要在内核态进行处理,则直接在内核态处理。
    • 如果数据包需要由用户态应用程序处理,则内核将数据包放入相应的套接字缓冲区中。
  8. 用户态应用程序通过系统调用读取数据包,数据从内核态(CPU)复制到用户态的缓冲区中,然后进行处理。

在网络带宽向万兆发展时,大量的中断操作会严重降低处理性能。当然Linux操作系统对此也提出了NAPI机制,NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后 POLL 的方法来轮询数据。以此来减少中断;问题是数据还需要做一次 CPU copy,复制操作也是很大的性能开销。对此也有处理方案,就是Netmap,使用共享数据包池减少这次CPU copy;还有问题:Linux是分时操作系统,需要进行进/线程进行对应的调度操作,那么它们的上下文切换以及cache misscache write back也会对极致的网络性能带来影响。

DPDK预取

alt text

在DPDK运行的时候,会使用mmap()系统调用把大页映射到用户态的虚拟地址空间,然后就可以正常使用了。

DDIO

DDIO技术之前是如何进行处理网络数据的:当一个网络报文送到服务器的网卡时,网卡通过外部总线(比如PCI总线)把数据和报文描述符送到内存。接着,CPU从内存读取数据到Cache进而到寄存器。进行处理之后,再写回到Cache,并最终送到内存中。最后,网卡读取内存数据,经过外部总线送到网卡内部,最终通过网络接口发送出去。

DDIO技术是如何改进的呢?这种技术使外部网卡和CPU通过LLCCache直接交换数据,绕过了内存这个相对慢速的部件。这样,就增加了CPU处理网络报文的速度(减少了CPU和网卡等待内存的时间),减小了网络报文在服务器端的处理延迟。

  1. 下载 linux 源码,v6.10-rc2
  2. 在项目根目录下执行 make menuconfig

配置

Kernel hacking —>
Compile-time checks and compiler options —>
Debug information (Generate DWARF Version 5 debuginfo) —>
(X) Generate DWARF Version 5 debuginfo

[*] Provide GDB scripts for kernel debugging 

make -j8

busybox 1.36.1

1
2
3

code scripts/kconfig/lxdialog/Makefile
# always := $(hostprogs-y) dochecklxdialog
1
2
make menuconfig

[*] Build static binary (no shared libs)

前言

本文是阅读 kernel 文档中关于 Slab 分配器的文档所写的笔记。

slab分配器背后的基本思想:让常用对象的缓存保持在初始化状态供给内核使用。

slab 分配器的三个主要目标

  1. 分配小内存块有助于消除由伙伴系统引起的内部碎片。slab内部维护了两组小内存缓冲区的缓存,范围从32字节到131072(2的17次方)字节。
  2. 缓存常用的对象,使系统不会浪费时间分配、初始化和销毁对象。当创建一个新的slab时,将很多对象进行打包并且进行初始化;当对象被释放,它保持其初始化状态,以便于下一次的对象分配。
  3. 通过将对象与L1与L2缓存对齐,更好利用硬件缓存。这是slab的最本质的任务,如果一个对象打包进slab后还有剩余空间,slab就将其进行着色,使其与CPU L1缓存对齐。

主体

slabs_full 的所有对象都在使用中。 slabs_partial 中有空闲对象,因此是分配对象的主要候选者。 slabs_free 没有分配的对象,是slab 销毁的主要候选者。

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
// The slab lists for all objects.
struct kmem_cache_node {
spinlock_t list_lock;

#ifdef CONFIG_SLAB
struct list_head slabs_partial; /* partial list first, better asm code */
struct list_head slabs_full;
struct list_head slabs_free;
unsigned long total_slabs; /* length of all slab lists */
unsigned long free_slabs; /* length of free slab list only */
unsigned long free_objects;
unsigned int free_limit;
unsigned int colour_next; /* Per-node cache coloring */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
#endif

#ifdef CONFIG_SLUB
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif

};

写一个简单的包过滤防火墙涉及到使用 netfilter 框架,netfilter 是 Linux 内核中的一个子系统,用于处理和过滤网络数据包。我们可以使用 iptables 来设置规则,但如果我们想要自己编写代码,则需要编写一个内核模块或使用 libnetfilter_queue 来处理用户空间的包过滤。这里我们使用 libnetfilter_queue 库来编写一个简单的包过滤防火墙。

环境设置

首先,确保你已经安装了必要的库和开发包。可以使用以下命令安装它们:

1
sudo apt-get install libnetfilter-queue-dev

防火墙代码

下面是一个简单的 C 代码示例,使用 libnetfilter_queue 来过滤数据包。这个防火墙只允许 ICMP(如 ping)和 TCP 数据包通过,拒绝其他类型的数据包。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/ip.h>
#include <linux/netfilter.h> /* for NF_ACCEPT */
#include <libnetfilter_queue/libnetfilter_queue.h>

/* Callback function to process packets */
static int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
struct nfq_data *nfa, void *data)
{
struct nfqnl_msg_packet_hdr *ph;
unsigned char *payload;
int id = 0;

ph = nfq_get_msg_packet_hdr(nfa);
if (ph) {
id = ntohl(ph->packet_id);
}

int ret = nfq_get_payload(nfa, &payload);
if (ret >= 0) {
struct iphdr *ip_header = (struct iphdr *)payload;
if (ip_header->protocol == IPPROTO_ICMP || ip_header->protocol == IPPROTO_TCP) {
// Allow ICMP and TCP packets
return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
}

// Drop all other packets
return nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
}

int main(int argc, char **argv)
{
struct nfq_handle *h;
struct nfq_q_handle *qh;
struct nfnl_handle *nh;
int fd;
int rv;
char buf[4096] __attribute__ ((aligned));

printf("Opening library handle\n");
h = nfq_open();
if (!h) {
fprintf(stderr, "Error during nfq_open()\n");
exit(1);
}

printf("Unbinding existing nf_queue handler for AF_INET (if any)\n");
if (nfq_unbind_pf(h, AF_INET) < 0) {
fprintf(stderr, "Error during nfq_unbind_pf()\n");
exit(1);
}

printf("Binding nfnetlink_queue as nf_queue handler for AF_INET\n");
if (nfq_bind_pf(h, AF_INET) < 0) {
fprintf(stderr, "Error during nfq_bind_pf()\n");
exit(1);
}

printf("Binding this socket to queue '0'\n");
qh = nfq_create_queue(h, 0, &cb, NULL);
if (!qh) {
fprintf(stderr, "Error during nfq_create_queue()\n");
exit(1);
}

printf("Setting copy_packet mode\n");
if (nfq_set_mode(qh, NFQNL_COPY_PACKET, 0xffff) < 0) {
fprintf(stderr, "Can't set packet_copy mode\n");
exit(1);
}

fd = nfq_fd(h);

while ((rv = recv(fd, buf, sizeof(buf), 0)) && rv >= 0) {
nfq_handle_packet(h, buf, rv);
}

printf("Unbinding from queue 0\n");
nfq_destroy_queue(qh);

printf("Closing library handle\n");
nfq_close(h);

exit(0);
}

编译代码

将上述代码保存为 firewall.c,然后编译它:

1
gcc -o firewall firewall.c -lnetfilter_queue

设置 iptables 规则

为了让 iptables 将数据包传递到 NFQUEUE,使用以下命令:

1
2
sudo iptables -I INPUT -j NFQUEUE --queue-num 0
sudo iptables -I OUTPUT -j NFQUEUE --queue-num 0

运行防火墙

运行编译后的防火墙程序:

1
sudo ./firewall

测试防火墙

  1. 允许 ICMP 包(如 ping)

    打开一个终端,运行 ping 命令:

    1
    ping 8.8.8.8

    你应该看到 ping 包被允许通过。

  2. 允许 TCP 包

    打开一个终端,运行 curl 命令:

    1
    curl http://example.com

    你应该看到 curl 请求被允许通过。

  3. 拒绝其他包

    所有其他类型的数据包将被拒绝。

清理

完成测试后,清理 iptables 规则:

1
2
sudo iptables -D INPUT -j NFQUEUE --queue-num 0
sudo iptables -D OUTPUT -j NFQUEUE --queue-num 0

这就是一个简单的包过滤防火墙的实现,利用 libnetfilter_queue 来处理和过滤数据包。你可以根据需要进一步扩展和修改此代码以实现更复杂的防火墙功能。

要测试 iptables 防火墙规则,确认其是否正确拒绝和允许指定的服务,可以进行以下步骤:

1. 检查允许的服务

允许的服务包括:

  • 本地回环接口流量
  • 已建立和相关的连接
  • SSH (端口 22)
  • ICMP (ping)
  • HTTP (端口 80) 和 HTTPS (端口 443)

测试 SSH 连接

在本地或远程系统上,尝试通过 SSH 连接到配置了防火墙的机器:

1
ssh user@your_server_ip

应能够成功连接。

测试 HTTP 和 HTTPS

在本地或远程系统上,使用 curl 或浏览器访问 HTTP 和 HTTPS 服务:

1
2
curl http://your_server_ip
curl https://your_server_ip

应返回相应的网页内容或响应头。

测试 ICMP (ping)

在本地或远程系统上,尝试 ping 配置了防火墙的机器:

1
ping your_server_ip

应接收到来自目标机器的响应。

2. 检查拒绝的服务

未特别允许的服务应被拒绝。

测试其他端口

尝试访问未允许的端口(例如,端口 23),应该被拒绝:

1
telnet your_server_ip 23

或使用 nc (netcat):

1
nc -zv your_server_ip 23

应显示连接被拒绝或超时。

3. 使用日志检查拒绝的服务

防火墙脚本包含日志记录规则,可以通过系统日志检查被拒绝的连接:

1
sudo tail -f /var/log/syslog

或在某些系统上:

1
sudo journalctl -f

尝试连接未允许的服务,并在日志中查看记录的 IPTABLES-DROP 消息。

总结

以下是一个完整的测试步骤:

  1. 测试允许的服务

    • SSH 连接:
      1
      ssh user@your_server_ip
    • HTTP 和 HTTPS 访问:
      1
      2
      curl http://your_server_ip
      curl https://your_server_ip
    • ICMP (ping):
      1
      ping your_server_ip
  2. 测试拒绝的服务

    • 其他端口访问:
      1
      2
      telnet your_server_ip 23
      nc -zv your_server_ip 23
  3. 查看系统日志

    • 检查日志以确认拒绝的服务被记录:
      1
      sudo tail -f /var/log/syslog
      1
      sudo journalctl -f

通过这些测试,你可以确认防火墙规则是否正确应用,并确保只允许指定的服务。

中断种类

1. 外部中断

外部中断又可以细分为可屏蔽中断和不可屏蔽中断。

2. 内部中断

内部中断可以分为软中断异常
软中断是由软件主动发起的中断,并不是发生了某种错误。
发起软中断的指令:

  • int 8位立即数。常用于系统调用,可表示256种中断。
  • int3。调试断点指令,中断向量号是3。我们常用的gdb或者bochs调试实际上就是使用到了这个指令。运行gdb的进程fork一个子进程用来运行被调试的程序,调试器打断点的本质是:将对应断点的地址的第一个字节备份并更改为0xcc,这样程序运行到断点处就会触发中断处理程序,运行中断处理程序之前要将当前的寄存器等数据压栈保存,查看寄存器或者变量实际上是从栈中获取的。
  • into。中断溢出指令,中断向量号是4。在eflags寄存器的OF位为1的前提下触发中断。
  • bound。数组索引越界检查指令,中断向量号是5。指令格式bound 16/32位 内存/寄存器,如果数据访问越界,则触发该中断。
  • ud2。未定义指令,中断向量号是6。表示该指令CPU无法识别。

除了第一个是软中断外,其余是软中断,也可以认为是异常。因为它们既具备软中断的“主动”行为,又具备异常的“错误”结果。

异常可以分为三类:

  1. Fault(故障)。比如缺页中断,这类异常交给对应的异常处理程序很大概率会被修复,甚至对操作系统有益处。
  2. Trap(陷阱)。比如调试时候的int3,掉入了CPU设下的陷阱,很形象。
  3. Abort(终止)。这是最严重的异常,意味着不可修复,操作系统为了正常运行,会直接将其从进程表中删除。通常是硬件出错或者内核某些数据结构出错。

中断描述符表

中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序入口的表。由于操作系统是中断驱动的,实模式下存储中断的是中断向量表。

中断向量表和中断描述符表的区别?

  • 中断描述符表地址不限制,在哪里都可以。
  • 中断描述符表中的每个描述符用 8 字节描述。

中断处理过程以及保护

一个完整的中断包含CPU内部的CPU外部和CPU内部两个部分以及特权级的检查。

CPU 外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到 CPU。
CPU 内:CPU 执行该中断向量号对应的中断处理程序。

这里只讨论CPU内部的过程:

  1. 处理器根据中断向量号定位中断门描述符。

  2. 处理器进行特权级检查。当前的特权级CPL必须在门描述符DPL和门中代码段描述符DPL之间(防止用户主动调用只为内核服务的例程)。

    1. 如果是由软中断 int nint3into 引发的中断,这些是用户进程中主动发起的中断,由用户代码控制,处理器要检查当前特权级 CPL 和门描述符 DPL,这是检查进门的特权下限,如果 CPL 权限大于等于 DPL,即数值上 CPL≤门描述符 DPL,特权级“门槛”检查通过,进入下一步的“门框”检查。否则,处理器抛出异常。
    2. 这一步检查特权级的上限(门框):处理器要检查当前特权级 CPL 和门描述符中所记录的选择子对应的目标代码段 DPL,如果 CPL 权限小于目标代码段 DPL,即数值上 CPL>目标代码段 DPL,检查通过。否则 CPL 若大于等于目标代码段 DPL,处理器将引发异常,也就是说,除了用返回指令从高特权级返回,特权转移只能发生在由低向高。
    3. 若中断是由外部设备和异常引起的,只直接检查 CPL 和目标代码段的 DPL,和上面的步骤2是一样的,要求 CPL 权限小于目标代码段 DPL,即数值上 CPL >目标代码段 DPL,否则处理器引发异常。
  3. 执行中断处理程序。

中断处理过程

由于IDT中会有中断门,陷阱门,任务门。中断发生时候会将NT位和TF位置为0。

NT位和TF位:
当 NT 标志被设置为 1 时,表示允许任务嵌套。设置为0表示进入了中断处理程序。这里还有个作用,iret指令既可以从新任务返回到旧任务,也可以从中断返回,这里 NT 标志就是区分到底是哪种返回。
当 TF 标志被设置为 1 时,会启用单步调试模式。设置为0也很容易想到,中断程序怎么会让你单步调试捏。

如果执行的是中断门时,会将IF位设置为0,避免中断嵌套,如果是陷阱门或者任务门则不会设置IF位为0,允许CPU响应更高级别的中断。当然也可以在中断处理程序中将IF位置为1,这样就可以处理优先级更高的中断。eflags寄存器有些位可以通过和栈配合来更改位,但是这有内存操作,效率低下且不是原子操作,所以专门给IF位提供了控制指令:sti开中断和cli关中断。IF 位(中断允许位)只能限制外部设备的中断,对那些影响系统正常运行的中断都无效,如异常 exception,软中断,如 int n 等,不可屏蔽中断 NMI 都不受 IF 限制。

中断发生时候的压栈

布拉布拉….

可编程中断控制器 8259A

最理想的方式是每个外设准备一个引脚接收中断,但是这明显不现实,成本高收益低。这时候一个中间层代理就显得智慧,这里使用的是Intel 8259A芯片。传统的PIC(就是可编程中断控制器 Programmable Interrupt Controller 的简称)是两片8259A风格的芯片级联的方式连接在一起。每个芯片可以处理8个不同的IRQ(中断请求)输入线,因为从PIC的INT输出线到主PIC的IRQ2引脚,因此可用的IRQ引脚只有16 - 1个。这种情况仅仅在系统只有一个CPU的情况下,将主芯片的输出引脚连接CPU的INTR引脚,但是系统有多个CPU就不适用了,引入了ACPI的新组件来代替老式的8259A芯片,这里只学习8259A芯片的原理和操作方式,不涉及多处理器的情况。
8259A可以管理和控制可屏蔽中断,它可以屏蔽外设中断,判断中断优先级,向CPU提供中断向量号,这些功能都是可编程的。 外接设备通过主板线路和8259A芯片连接在一起。下面是个人计算机中芯片级联方式:
级联

内部结构

  • INT: 芯片挑选出来优先级最高的中断请求,发信号给CPU。
  • INTA: INT Acknowledge,中断响应信号。用于接收CPU的INTA接口的中断响应信号。
  • IMR(Interrupt Mask Register): 中断屏蔽寄存器,8位,用来屏蔽某个外设的中断信号。
  • IRR(Interrupt Request Register): 中断请求寄存器,8位,用来存储等待处理的中断。
  • PR(Priority Resolver): 优先仲裁器,找出优先级最高的中断。
  • ISR(In-Service Register): 中断服务寄存器,8位,存储正在处理的中断。

级联

工作流程

  1. 外设发出一个中断信号,通过主板线路将信号传递给芯片的某个IRQ接口。
  2. 8259A首先检查IMR寄存器是否屏蔽了该IRQ接口的中断信号,如果为1则表示屏蔽,直接忽略。否则将其送入IRR寄存器,将其对应位置为1。
  3. PR寄存器找出优先级最高的中断(这里优先级判断很简单,IRQ接口接口号越低,优先级越大)。
  4. 芯片在控制电路中通过INT接口向CPU发送INTR信号,信号传入CPU的INTR接口后,CPU就知道有新的中断到来,执行完手里的指令就向自己的INTA接口向8259A回复一个中断响应信号,表示CPU准备就绪。
  5. 8259A受到信号后,将刚才选出来的优先级最高的中断在ISR中置为1,同时将IRR中置为0。
  6. CPU再次发送INTA信号,为了获取中断对应的中断向量号。
  7. 如果8259A的EOI(End of Interrupt)被设置为手动模式,中断处理程序必须有向8259A发送EOI的代码,芯片受到EOI后会自动将ISR对应中断位置为0。

需要注意的是:并不是进入了 ISR 后的中断就高枕无忧了,它还是有可能被后者换下来的。比如,在 8259A 发送中断向量号给 CPU 之前,这时候又来了新的中断,如果它的来源 IRQ 接口号比 ISR 中的低,也就是优先级更高,原来 ISR 中准备上 CPU 处理的旧中断,其对应的 BIT 就得清 0,同时将它所在的 IRR中的相应 BIT 恢复为 1,随后在 ISR 中将此优先级更高的新中断对应的 BIT 置 1,然后将此新中断的中断向量号发给 CPU。

实际操作

说了那么多,我们最终目的很简单:

  1. 构建IDT
  2. 提供中断向量号

8259A内部有两组寄存器:初始化命令寄存器组(ICW1 ~ ICW4),操作命令寄存器组(OCW1 ~ OCW3)。因此我们的操作也分为初始化和操作两部分。

初始化

注意:ICW1 ~ ICW4的初始是有严格顺序的。必须依次写入 ICW1、ICW2、ICW3、ICW4。因为其中某些设置是由关联,依赖的。

ICW1

用来初始化 8259A 的连接方式和中断信号的触发方式。连接方式是指用单片工作,还是用多片级联工作,触发方式是指中断请求信号是电平触发,还是边沿触发。

ICW1

IC4:表示是否写入ICW4,该位为1表示需要在后面写入ICW4,为0则不需要。x86必须设置为1。
SNGL:该位为1,表示是单片,为0则表示级联。
ADI:用来设置8259A的调用时间间隔,x86不需要设置。
LTIM:表示中断检测方式,为0表示边沿触发,为1表示电平触发。
第四位恒定为1。
第5~7位专用于8085处理器,x86不需要,设置为0。

ICW2

用来设置起始中断向量号。需要写入主片的0x21端口和从片的0xA1端口。
我们只需要写高五位的T3~T7,所以任意数字都是8的倍数,这个数字便是设定的起始中断向量号。第三位可以表示8个中断向量号,这是根据8259A芯片自行导入,这样就能表示任意一个IQR接口实际分配的中断向量号。

ICW2

ICW3

在级联的前提下才能使用,用来设置主从片用哪个IRQ接口互联。需要写入主片的0x21端口和从片的0xA1端口。
对于主片,哪个IRQ接口用于连接从片,就将其设置为1(可以设置多个从片),设置为0表示是外接设备。
对于从片,只需要在从片上指定主片用于连接自己的IRQ接口。这样中断发生后,主片会发送与从片做级联的 IRQ 接口号,所有从片用自己的 ICW3 的低 3 位和它对比,若一致则认为是发给自己的。高五位没用到,设置为0。

ICW3主片
ICW3从片

ICW4

ICW4

SFNM 表示特殊全嵌套模式(Special Fully Nested Mode) ,若 SFNM 为 0,则表示全嵌套模式,若 SFNM为 1,则表示特殊全嵌套模式。
BUF 表示本 8259A 芯片是否工作在缓冲模式。BUF 为 0,则工作非缓冲模式,BUF 为 1,则工作在缓冲模式。
当多个 8259A 级联时,如果工作在缓冲模式下,M/S 用来规定本 8259A 是主片,还是从片。若 M/S 为 1 ,则表示则表示是主片,若 M/S 为 0,则表示是从片。若工作在非缓冲模式(BUF 为 0)下,M/S 无效。
AEOI 表示自动结束中断(Auto End Of Interrupt) ,8259A 在收到中断结束信号时才能继续处理下一个中断,此项用来设置是否要让 8259A 自动把中断结束。若 AEOI 为 0,则表示非自动,即手动结束中断,咱们可以在中断处理程序中或主函数中手动向 8259A 的主、从片发送 EOI 信号。这种“操作”类命令,通过下面要介绍的 OCW 进行。若 AEOI 为 1,则表示自动结束中断。
μPM 表示微处理器类型(microprocessor) ,此项是为了兼容老处理器。若 μPM 为 0,则表示 8080 或8085 处理器,若 μPM 为 1,则表示 x86 处理器。

OCW

OCW1

OCW1

需要写入主片的0x21端口和从片的0xA1端口。
OCW1 用来屏蔽连接在 8259A 上的外部设备的中断信号,还记得上面说的IMR(Interrupt Mask Register)吗?实际上就是将OCW1的数据填入该寄存器中。为1表示该位被屏蔽。

OCW2

OCW2

OCW2 要写入到主片的 0x20 及从片的 0xA0 端口。
OCW2 用来设置中断结束方式和优先级模式。

SL:Specific Level,表示是否指定优先等级。等级是用低 3 位来指定的。此处的 SL 只是开启低 3 位的开关,所以 SL 也表示低 3 位的 L2~L0 是否有效。SL 为 1 表示有效,SL 为 0 表示无效。

OCW2 其中的一个作用就是发 EOI 信号结束中断。如果使 SL 为 1,可以用 OCW2 的低 3 位(L2L0)来指定位于 ISR 寄存器中的哪一个中断被终止,也就是结束来自哪个 IRQ 接口的中断信号。如果 SL 位为 0,L2L0 便不起作用了,8259A 会自动将正在处理的中断结束,也就是把 ISR 寄存器中优先级最高的位清 0。

R:用来设置优先级控制方式。为0,则IRQ接口号越低,优先级越高。
R为1:设置为循环优先级方式。

  1. SL为0,初始优先级次序为IR0>IR1>IR2>IR3>IR4>IR5>IR6>IR7。
  2. SL为1,通过 L2L1 指定最低优先级是哪个 IRQ 接口。
    举个例子,在 R 和 SL 都等于 1 的情况下,若想指定 IR5 为最低的优先级,需要将 L2
    L0 置为 101。
    这样,新的初始优先级循环是:IR6>IR7>IR0>IR1>IR2>IR3>IR4>IR5。什么是循环优先级方式?当前 IR6 为最高级别中断请求,处理完成后,IR6 将变成最低级别,IR7 变成最高级别,这一组循环之后的优先级变成了:IR7>IR0>IR1>IR2>IR3>IR4>IR5>IR6。

EOI:End Of Interrupt,为中断结束命令位。令 EOI 为 1,则会令 ISR 寄存器中的相应位清 0,也就是
将当前处理的中断清掉,表示处理结束。向 8259A 主动发送 EOI 是手工结束中断的做法,所以,使用此
命令有个前提,就是 ICW4 中的 AEOI 位为 0,非自动结束中断时才用。

只有EOI位为1时候,表示发送中断结束。

OCW3用于设定特殊的屏蔽方式以及查询方式。

编写中断处理程序

中断向量 019 为处理器内部固定的异常类型,2031 是 Intel 保留的,所以咱们可用的中断向量号最低是 32,将来咱们在设置 8259A 的时候,会把 IR0 的中断向量号设置为 32。

前言

  • 实验环境:Archlinux + Bochs x86 Emulator 2.7
  • 编译器: nasm

—以下均是我的个人笔记,如有错误,敬请指正.—

阅读全文 »

哎!当时懒得记笔记,到头来还是得重新回头翻书看,这次顺带把笔记做上。

CHS 和 LBA

早期的时候计算机是通过CHS寻址模式来定位区块的,但是弊端也很明显,区块必须将指定硬盘的柱面(Cylinder)/磁头(Head)/扇区(Sector)三个参数来定位一个唯一的扇区地址,这套逻辑只有在硬盘操作内部有用,其他设备想要访问的话需要额外知道这些细节数据,太麻烦了,于是LBA应运而生。LBA十分单纯,从0开始定位区块,第一个区块的LBA=0,第二个区块的LBA=1,以此类推,LBA取代了CHS,这样就能以一种相对而言更方便的方式来进行硬盘IO。但是LBA只是在CHS上进行了抽象,实际上的硬件控制器还是需要CHS来进行寻址的,但是我们现在使用LBA给传入的是 0 1 2 3 之类的数字,硬件控制器又该如何通过LBA转换成正确的CHS呢?

阅读全文 »

1
2
CONFIGURE_ARGS="--enable-gdb-stub"
CONFIGURE_ARGS="--enable-debugger --enable-readline"

先改.conf.linux,里面的

1
CONFIGURE_ARGS="--enable-gdb-stub"

再使用:

1
sh .conf.linux 

然后在Makefile里面操作:

我是在vscode中,Ctrl+F,将所有的bochs更改成bochs_gdb,接着将bochs_gdb.h更改为bochs.h,wxbochs_gdb.rc更改为wxbochs.rc,后面这个应该没影响,是和win有关系的,但是保险起见还是改了吧,然后进行make -j16 && sudo make install