<kernel v5.0>
Early IO 매핑
커널 부트업 과정에서 paging_init( ) 함수가 수행된 후에 메모리 및 입출력(I/O) 장치들이 가상 주소 공간에 정식으로 매핑되어 사용될 수 있다. 그런데 이러한 동작이 완료되기 전에 반드시 먼저 매핑되어 사용되어야 하는 경우를 위해 먼저(early) 처리할 수 있는 방법을 준비했다. 이러한 API를 사용하는 경우는 특정 시스템의 바이오스 정보 등에 먼저 접근해서 각종 메모리 및 디바이스의 정보를 가져와야 매핑 처리가 가능한 경우다. 주로 이용하는 경우는 다음과 같다.
- ACPI 테이블에 접근해서 각종 디바이스의 설정 정보를 가져와야 하는 경우
- EFI 테이블에 접근해서 각종 디바이스의 설정 정보를 가져와야 하는 경우
- 일부 디바이스에서 특정 설정 정보 등을 읽어와야 하는 경우
이른(early) 시간에 io 매핑이 필요한 경우 256K씩 최대 7개의 fixmap 매핑 공간을 사용하여 매핑을 할 수 있게 한다.
다음 그림은 early_ioremap() 및 early_ioremap_init() 두 함수의 호출 관계를 보여준다.
Early IO 매핑 초기화
early_ioremap_init() – ARM32 & ARM64
ioremap을 사용하기 전에 부트 타임에 early I/O 매핑을 할 수 있도록 준비하는 과정을 자세히 알아본다. 다음 함수는 곧바로 early_ioremap_setup( ) 함수를 호출한다.
- ARM32도 커널 v4.5-rc1 부터 early ioremap을 지원한다.
arch/arm64/mm/ioremap.c
/* * Must be called after early_fixmap_init */ void __init early_ioremap_init(void) { early_ioremap_setup(); }
paging_init()이 완료되지 않아 정규 ioremap() 함수를 사용하지 못하지만 이른(early) 시간에 임시적으로 매핑이 필요한 경우를 위해 준비한다.
- fixmap을 이용하므로 early_fixmap_init() 함수가 먼저 호출되어 초기화가 되어야한다.
early_ioremap_setup()
early I/O 매핑을 위해 fixmap 영역에 7개의 256K 가상 주소 영역을 준비하는 과정을 알아본다.
mm/early_ioremap.c
void __init early_ioremap_setup(void) { int i; for (i = 0; i < FIX_BTMAPS_SLOTS; i++) if (WARN_ON(prev_map[i])) break; for (i = 0; i < FIX_BTMAPS_SLOTS; i++) slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i); }
7개 배열로 이루어진 slot_virt[] 배열에 fixmap에서 early ioremap 용도로 배정된 가상 주소를 배정한다.
- 코드 라인 5~7에서 FIX_BTMAPS_SLOTS(7) 수만큼 루프를 돌며 전역 prev_map[ ] 배열에 값이 설정된 경우 경고 메시지를 출력한다
- 7개 각 주소 공간은 최대 256K를 사용할 수 있다.
- 코드 라인 9~10에서 FIX_BTMAPS_SLOTS(7) 수만큼 루프를 돌며 전역 slot_virt[ ] 배열에 fixmap의 BTMAP 가상 주소를 설정한다. 7개의 fixmap 엔트리가 있고, 각 엔트리들은 fixmap의 BTMAP에 해당하는 가상 주소들을 가리키는데 엔트리 간의 간격은 256K이다.
- slot_virt[0] = FIX_BTMAP_BEGIN
- slot_virt[1] = FIX_BTMAP_BEGIN + 256K
다음 그림은 early_ioremap_setup( ) 함수가 호출되어 early IO 매핑에 사용되는 fixmap의 BTMAPS 영역이 7개의 slot이 초기화되는 것을 보여준다.
주요 API
early_ioremap()
mm/early_ioremap.c
/* Remap an IO device */ void __init __iomem * early_ioremap(resource_size_t phys_addr, unsigned long size) { return __early_ioremap(phys_addr, size, FIXMAP_PAGE_IO); }
물리 주소 @phys_addr부터 @size에 대응하는 io 디바이스들을 early 매핑한 후 매핑된 가상 주소를 반환한다.
- 256K 크기를 최대 7개 까지 io 디바이스를 early 매핑한다.
- FIXMAP_PAGE_IO
- ARM32
- L_PTE_YOUNG | L_PTE_PRESENT | L_PTE_XN | L_PTE_DIRTY | L_PTE_MT_DEV_SHARED | L_PTE_SHARED 송성을 가진다.
- ARM64
- PROT_DEVICE_nGnRE 매핑 속성을 가진다.
- ARM32
early_memremap()
mm/early_ioremap.c
/* Remap memory */ void __init * early_memremap(resource_size_t phys_addr, unsigned long size) { return (__force void *)__early_ioremap(phys_addr, size, FIXMAP_PAGE_NORMAL); }
물리 주소 @phys_addr부터 @size에 대응하는 메모리 영역을 early 매핑한 후 매핑된 가상 주소를 반환한다.
- 256K 크기를 최대 7개 까지 메모리 영역을 early 매핑한다.
- FIXMAP_PAGE_NORMAL
- ARM32
- L_PTE_XN 속성이 추가된 커널 메모리 속성이다.
- ARM64
- 커널 메모리 속성과 동일하다.
- ARM32
__early_ioremap()
mm/early_ioremap.c
static void __init __iomem *
__early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
{
unsigned long offset;
resource_size_t last_addr;
unsigned int nrpages;
enum fixed_addresses idx;
int i, slot;
WARN_ON(system_state != SYSTEM_RUNNING);
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (!prev_map[i]) {
slot = i;
break;
}
}
if (WARN(slot < 0, "%s(%08llx, %08lx) not found slot\n",
__func__, (u64)phys_addr, size))
return NULL;
/* Don't allow wraparound or zero size */
last_addr = phys_addr + size - 1;
if (WARN_ON(!size || last_addr < phys_addr))
return NULL;
prev_size[slot] = size;
/*
* Mappings have to be page-aligned
*/
offset = offset_in_page(phys_addr);
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
/*
* Mappings have to fit in the FIX_BTMAP area.
*/
nrpages = size >> PAGE_SHIFT;
if (WARN_ON(nrpages > NR_FIX_BTMAPS))
return NULL;
/*
* Ok, go for it..
*/
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
if (after_paging_init)
__late_set_fixmap(idx, phys_addr, prot);
else
__early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
WARN(early_ioremap_debug, "%s(%08llx, %08lx) [%d] => %08lx + %08lx\n",
__func__, (u64)phys_addr, size, slot, offset, slot_virt[slot]);
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
}
요청한 물리 주소와 사이즈를 fixmap의 BTMAPS 슬롯에 해당하는 가상주소에 prot 속성으로 매핑한다. 실패하는 경우 null을 반환한다. arm64에서 early 하게 io 매핑할 수 있는 최대 수는 FIX_BTMAPS_SLOTS(7)개이다.
- fixmap용 BTMAPS 슬롯 관리 정보
- slot_virt[]: 부트업 타임에 초기화된 각 슬롯에 해당하는 fixmap 가상 주소 (256K 단위)
- prev_map[]: fixmap에 매핑된 가상 주소
- prev_size[]: 매핑된 사이즈
- 코드 라인 12-18에서 사용할 빈 슬롯을 선택한다. FIX_BTMAPS_SLOTS(7) 수만큼 루프를 돌며 전역 prev_map[ ] 값이 설정되지 않았다면 현재 카운터 i 값을 slot에 설정하여 slot을 선택한다.
- 코드 라인 20~22에서 루프를 도는 동안 빈 슬롯을 못 찾은 경우 경고 메시지를 출력하고 NULL 값을 리턴한다
- 코드 라인 25~27에서 크기가 0이거나 끝 주소가 시스템 주소 범위를 벗어나는 경우 경고 메시지를 출력하고 NULL 값을 리턴한다.
- 코드 라인 29~34에서 prev_size[ ]에 요청 크기를 담아둔다. offset은 요청 물리 주소에서 페이지 단위의 나머지 offset만을 계산하고, 물리 주소는 페이지 단위로 내림 정렬한다
- 코드 라인 35에서 페이지 단위로 정렬된 크기 값을 계산한다. 처음 요청한 물리 시작 주소 + 크기 값을 페이지 단위로 정렬하고, 페이지 단위로 내림 정렬한 물리 주소를 뺀다(최솟값은 페이지 단위가 된다).
- 코드 라인 40~42에서 크기 값으로 페이지 수를 계산하고 한다. 크기가 256K를 초과한다면 NULL을 리턴한다
- 코드 라인 48에서 계산된 페이지 수만큼 루프를 돈다.
- 코드 라인 49~50에서 전역 after_paging_init이 설정된 경우에는 _ _late_clear_fixmap( ) 매크로 함수를 호출한다.
- 예) arm64 및 x86의 경우 별도의 late 처리 함수 없이 그냥 __set_fixmap() 함수를 호출한다.
- 코드 라인 51~52에서 반대로 after_paging_init이 설정되지 않은 경우에는 _ _early_set_fixmap( ) 매크로 함수를 호출한다.
- 예) arm64의 경우 별도의 early 처리 함수 없이 그냥 __set_fixmap() 함수를 호출한다.
- 예) x64의 경우 별도의 early 처리 함수인 __early_set_fixmap() 함수를 호출한다.
- 코드 라인 60에서 prev_map[slot]에 슬롯에 해당하는 fixmap 가상 주소 + offset을 더해 설정한다
__early_set_fixmap()
arm64/include/asm/fixmap.h
#define __early_set_fixmap __set_fixmap
paging_init()이 완료되기 전에는 early_ioremap() 함수가 호출되는 경우에는 fixmap에 매핑한다.
__late_set_fixmap()
mm/early_ioremap.c
#define __late_set_fixmap __set_fixmap
paging_init()이 완료된 후 early_ioremap() 함수가 호출되는 경우에는 fixmap에 매핑한다.
- 커널 v4.1-rc1 부터 부팅 후에도 early_ioremap()을 호출 가능하도록 지원한다. 그 전 까지는 BUG()를 호출하였다.
early_ioremap() API 제공 만료
early_ioremap_reset()
mm/early_ioremap.c
void __init early_ioremap_reset(void) { early_ioremap_shutdown(); after_paging_init = 1; }
정규 페이징이 준비되었으므로 early_ioremap() API를 더 이상 사용하지 말고 정규 ioremap()을 사용해야 하는 시점이다.
- 아키텍처에 따라 이 함수 진행 후에 early_ioremap() 함수가 호출될 때 BUG() 발생토록 한다.
- x86과 arm64의 경우에는 계속 early_ioremap() 함수의 사용을 허용한다.
기타 API
- early_memremap_ro()
- early_memremap()과 동일하나 read only 속성이 추가된다.
- early_memremap_prot()
- early_memremap()과 동일하나 @prot_val 속성을 요청하여 추가할 수 있다.
https://elixir.bootlin.com/linux/v4.11.11/source/arch/arm64/include/asm/fixmap.h#L71 의 코드를 보면
#define FIXADDR_START (FIXADDR_TOP – FIXADDR_SIZE)
enum fixed_addresses {
FIX_HOLE,
…
FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS – 1,
…
}; 입니다.
slot_virt[0]~[6] 레이아웃 순서와 FIX_BTMAP_END, FIX_BTMAP_BEGIN layout의 순서가 뒤집혀야 할것 같은데 확인부탁드립니다
enum값을 인텍스로 FIXADDR_TOP에서 ((x) << PAGE_SHIFT)) 해서 계산하는군요 .. 블로그의 그림이 맞는것 같습니다.
안녕하세요? 빠르게 확인하셨군요. 즐거운 하루 되시기 바랍니다. ^^
안녕하세요
early ioremap에서는 static하게 virt_slot에 io 메모리를 mapping하여서 사용하는 것으로 이해했습니다.
혹시 early mapping해서 사용하던 io 메모리들이 남아있는 경우, paging_init 이후에도 early mapping했던 io 메모리에 계속 접근해서 사용하는 것이 가능 해 보이는 것 같습니다.(early unmap하지 않는 이상)
제 개인적인 생각에는,, early ioremap은 paging_init 이전에만 사용하고, paging_init 이후에는 정규로 매핑하여 사용하는 것으로 통일하면 어떨까 생각해봤는데,, 이렇게 변경하는 것은 어떻게 생각하시나요?
남아있는 early ioremap들을 정규 매핑으로 migration 해주는 부분에 대한 함수를 작성해서 early_ioremap_reset() 함수에서 ‘after_paging_init = 1’ 전에 호출하도록 하면 될 거 같긴한데..
안녕하세요? DA님,
early_ioremap()을 사용하는 드라이버가 해당 IO 주소가 매핑된 A라는 가상주소를 받아서 사용하고 있는데,
갑자기 정규 과정에서 B라는 주소로 바뀌고, 이를 드라이버가 인지해야 하는 방법이 필요합니다.
또한 A와 B의 전환이 RCU 처럼 심리스 하게 처리되어야 하는 문제도 있고,
제 머리로는 이를 자연스럽게 migration하는 방법이 잘 떠오르지 않습니다.
DA님도 고민한번 해보시기 바랍니다.
감사합니다.