昨天介紹完 Memory-Mapped I/O (MMIO) 之後,今天我們要繼續探討另一種周邊設備與作業系統的溝通模式——Port-Mapped I/O (PMIO)。
當作業系統使用 MMIO 操作周邊設備時,是透過一般的讀寫記憶體指令,並提供物理記憶體位址 (Physical Address Space) 來進行操作。然而,PMIO 則是採用一種獨立的周邊設備操作方式。每個周邊設備會提供若干個 "Port"(類似暫存器),讓作業系統和周邊設備進行讀寫,資料交換。
// drivers/net/ethernet/8390/ne.c
#define NE_CMD 0x00
#define NE_DATAPORT 0x10 /* NatSemi-defined port window offset. */
#define NE_RESET 0x1f /* Issue a read to reset, a write to clear. */
#define NE_IO_EXTENT 0x20
#define NE1SM_START_PG 0x20 /* First page of TX buffer */
#define NE1SM_STOP_PG 0x40 /* Last page +1 of RX ring */
#define NESM_START_PG 0x40 /* First page of TX buffer */
#define NESM_STOP_PG 0x80 /* Last page +1 of RX ring */
例如,在 Intel 8390 網卡的驅動程式中,定義了 8 個 Port,例如用來傳輸指令 (如 NE_CMD
) 和資料 (如 NE_DATAPORT
)。
由於周邊設備的數量是動態變化的,為了有效管理這些 Port,處理器會將所有 Port 整合,形成一個獨立於實體記憶體空間的地址空間,這個空間稱為 I/O space。
在 I/O space 中,每個設備的 Port 都會佔據特定的連續範圍。透過 /proc/ioports
,我們可以查看所有周邊設備的 Port 在 I/O space 裡的分佈情況:
> cat /proc/ioports
000-0cf7 : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-0060 : keyboard
0062-0062 : PNP0C09:01
0062-0062 : EC data
0064-0064 : keyboard
0066-0066 : PNP0C09:01
0066-0066 : EC cmd
0070-0071 : rtc_cmos
0070-0071 : rtc0
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
0290-029f : pnp 00:00
03f8-03ff : serial
0400-041f : iTCO_wdt
0680-069f : pnp 00:01
這段內容顯示了 PCI Bus 以及其他設備在 I/O space 中的位址範圍。由於 I/O port 是獨立於記憶體空間的,因此讀寫操作需要使用特定的指令。
在 Intel 架構中,作業系統透過 in
和 out
指令來讀寫 I/O port,這些指令專門用於與設備的 Port 進行資料交換。Linux Kernel 對該指令進行了封裝。
// arch/x86/include/asm/shared/io.h
static __always_inline void __out##bwl(type value, u16 port) \
{ \
asm volatile("out" #bwl " %" #bw "0, %w1" \
: : "a"(value), "Nd"(port)); \
} \
\
static __always_inline type __in##bwl(u16 port) \
{ \
type value; \
asm volatile("in" #bwl " %w1, %" #bw "0" \
: "=a"(value) : "Nd"(port)); \
return value; \
}
BUILDIO(b, b, u8)
BUILDIO(w, w, u16)
BUILDIO(l, , u32)
#undef BUILDIO
#define inb __inb
#define inw __inw
#define inl __inl
#define outb __outb
#define outw __outw
#define outl __outl
上述程式碼中,inb
, inw
, inl
, outb
, outw
, outl
這幾個函數分別對應 in
和 out
指令的封裝,並提供 8-bit、16-bit、32-bit 等不同大小的版本,方便驅動程式操作 I/O port。
例如,以下程式碼展示了如何向網卡的 EN0_RCNTLO
Port 寫入資料:
// source/drivers/net/ethernet/8390/ne.c
outb_p(count & 0xff, nic_base + EN0_RCNTLO);
static inline void outb_p(u8 value, unsigned long addr)
{
outb(value, addr);
}
與 MMIO 相似,我們同樣會遇到一個問題:設備的 I/O port 在 I/O Address Space 中的位置是如何決定與分配的呢?這個問題需要進一步探討。在明天的文章中,我們會深入討論這個問題,並解釋設備在 I/O space 中的地址分配邏輯。