안녕하세요 스터디를 하다가 코드 (kernel version: v5.9) 가 꼭 필요한지를 정확히 몰라서 질문 올립니다.
$(kernel_root)/arch/arm64/kernel/head.S 에서
SYM_FUNC_START_LOCAL(__create_page_tables) 함수안에
adr_l x6, vabits_actual
str x5, [x6]
dmb sy
dc ivac, x6
동작하는 platform에서 지원하는 가상주소 비트 수를 읽어서 vabits_actual 이라는 곳에 저장하는 코드입니다.
이 코드가 동작할 때에 D-cache 는 disable되어있는 상태이지만 나중에 d-cache를 enable 했을 때 stale한 data가 있으면 예상치 못한 data를 읽을 수도 있기에 dcache invalidate해주는 부분은 이해를 했습니다.
이 때, dmb sy를 통해서 vabits_actual에 값을 value를 저장하는 코드와 d-cache invalidate 해주는 코드의 순서를 보장해주고 있습니다. 근데 d-cache가 disable되어있는 상태에서 이 순서가 왜 보장되어야 하는지 잘 모르겠습니다.
선배님들에게 질문해봅니다.
댓글 7
-
문c(문영일)
2020.11.16 22:38
-
문c(문영일)
2020.11.16 22:49
정작 중요한 답변을 못했네요.
위의 dmb 명령은 배리어로 동작하는 것보다, 메모리에 대한 명확한 기록을 보장하게 하기 위한 용도로 사용된 코드입니다.-> (수정) 위의 dmb 명령은 store 명령과 dc 명령과의 배리어로 동작하는 것입니다. dc 명령도 메모리 관련 명령이라 베리어를 사용할 때 isb가 아니라 dmb 명령을 사용합니다. 그 후 dc 명령으로 데이터 캐시를 flush 하여 다른 cpu에서 잘못된 데이터를 읽지 않도록 방지합니다. 많이 틀렸군요. ^^;
참고로 뒤의 캐시를 비우는 명령은 MMU를 disable하여 캐시를 사용하지 않고 있는 상황인데, 나중에 캐시를 clean & invalidate 할 때 잘못된 기록을 막기 위해 invalidate 합니다.또 한 가지, 메모리 베리어로 동작할 때 dmb는 str과 dc가 바뀌지 않도록 방지하는 것이 아니라 str과 dmb 이후에 나올 load/store 명령과의 순서가 바뀌지 않도록 합니다. 명령어 배리어로는 isb를 사용하는 것을 기억해두시면 좋겠습니다. -
DEWH
2020.11.18 11:33
안녕하세요. 16기 양원혁입니다.
저도 해당 사항에 궁금증이 생겨서, 이곳 저곳에서 찾아보았습니다.
먼저 몇가지 알아야 할 사항이 있습니다.
1) MMU off인 상황에서 메모리 타입은 Device_nGnRnE으로 간주 됩니다. 이 말은 간단하게 data cache를 lookup 하지 않고, 메모리에 직접적으로 read, write합니다.
2) dmb는 load, store 뿐만 아니라, cache operation에 대한 순서 또한 보장해줍니다.
[ARMv8-A_Programmers_Guide_for_ARMv8-A.13.2]
> It also ensures that any explicit preceding data or unified cache maintenance operations
> have completed before any subsequent data accesses are executed.
>
> DC CSW, x5 // Data clean by Set/way
> LDR x0, [x1] // Effect of data cache clean might not be seen by this
> // instruction
> DMB ISH
> LDR x2, [x3] // Effect of data cache clean will be seen by this
이제 이 둘을 조합해서 타켓 코드를 살펴보면,
str x5, [x6]
dmb sys // 시스템 도메인에서, 위 store가 완료되었음을 보장합니다.
dc ivac, x6 // store가 완료 후, stale cache를 invalid해야 합니다.
이제 본 질문은 아래와 같은 다른 질문으로 바꿔봅시다.
만약 dc가 store 보다 먼저 실행된다면, 어떤 문제가 발생하는가?
결론적으로는 store가 commit되기 전에, 다른 cpu로 cache line fetch될 수 있습니다.
아래 그림은 dc ivac, A가 완료된 모습을 보여줍니다.
┌───────┐ ┌───────┐
│CPU1(mmu off)│ │CPU2(mmu on)│
└───────┘ └───────┘
│ │
┌────────┐ ┌────────┐
│cache (A: invalid) │ │cache (A: invalid) │
└────────┘ └────────┘
│ |
┌───────────────────┐
│ memory (A: old_val) │
└───────────────────┘
이때, store가 완료되기전, cpu2로 부터 A에 대한 cacheline fetch가 발생합니다.
그런 다음 store가 완료된다면 아래 그림과 같습니다.
┌───────┐ ┌───────┐
│CPU1(mmu off)│ │CPU2(mmu on)│
└───────┘ └───────┘
│ │
┌────────┐ ┌────────┐
│cache (A: invalid) │ │cache (A: old val)│
└────────┘ └────────┘
│ |
┌───────────────────┐
│ memory (A: new_val) │
└───────────────────┘
이러한 잠재적인 stale cache를 피하기 위해서는,
store가 먼저 실행되고 dc를 수행해야 합니다.
이를 보장하기 위해 dmb가 사용됩니다.
Ref) https://lore.kernel.org/kernelnewbies/CAEcHRToEjECSNpdocLwtd6M1vC2qEhBa2pziCV5g75d6h86kFQ@mail.gmail.com/T/#m390fd945500591ec444028da325b0d1481175be3
-
문c(문영일)
2020.11.18 21:31
안녕하세요? 양원혁님!
mmu disable 상태에선 buffer도 사용못하는 것을 확인했고, dmb 명령이 베리어로서 다음 캐시명령과의 구분에도 적용되는 것을 확인하여 제가 답변을 단 잘못된 부분들은 수정하였습니다.
원 질문에서 아래의 베리어 코드를 사용한 정확한 이유는 양원혁님이 주고 받은 메일을 보니 잘 알 수 있었습니다.
dmb sy
dc ivac, x6dmb는 정말 아래 dc 명령과의 구분을 위해 사용했었군요.
확실히 커널 코드의 분석엔 많은 주의가 필요합니다. ^^
감사합니다.
-
콤퓨타
2020.11.19 09:05
정확히 이해되었고 납득이 되었습니다.
감사합니다.
-
콤퓨타
2020.11.29 04:56
안녕하세요.
다시 한번 댓글을 읽어보니 전번에는 이해가 다되었다고 생각했는데, 설명해주신 부분에서 아직도 이해가 안 가는 부분이 생겨서 다시 한번 질문 드립니다.
아래 그림은 dc ivac, A가 완료된 모습을 보여줍니다.
┌───────┐ ┌───────┐
│CPU1(mmu off)│ │CPU2(mmu on)│
└───────┘ └───────┘
│ │
┌────────┐ ┌────────┐
│cache (A: invalid) │ │cache (A: invalid) │
└────────┘ └────────┘
│ |
┌───────────────────┐
│ memory (A: old_val) │
└───────────────────┘
이때, store가 완료되기전, cpu2로 부터 A에 대한 cacheline fetch가 발생합니다.
그런 다음 store가 완료된다면 아래 그림과 같습니다.
┌───────┐ ┌───────┐
│CPU1(mmu off)│ │CPU2(mmu on)│
└───────┘ └───────┘
│ │
┌────────┐ ┌────────┐
│cache (A: invalid) │ │cache (A: old val)│
└────────┘ └────────┘
│ |
┌───────────────────┐
│ memory (A: new_val) │
└───────────────────┘
위 설명해서, dc ivac, A 가 완료하고 난 후에 store 연산 전에 CPU2의 cache에서 fetch하게 되면 결국 CPU2의 cache에 stale한 cache line이 생겨서 문제가 되는 부분은 이해가 되었으나, 사실 CPU2의 cache가 old val을 fetch하는 것은 CPU1에서 일어나는 store 명령어 전에 일어나게 되면 어쩔수 없이 일어나는 상황이 아닌가요?
즉, CPU2의 cache가 invalid상태에서 memory로 부터 data를 fetch하는 상황은 CPU1의 DC 그리고 store 명령어와는 별개로 독립적으로 일어날 수 있는 상황이라는 점에서는 DMB barrier가 상황을 도와주고있는 모습은 아닌듯 해 보입니다.
오히려 CPU2의 cache에 stale한 cache line이 걱정이라면, 어떤 방식으로라든 CPU2에서 dc ivac, A 명령어를 수행해주는게 본질적인 문제해결인듯 합니다.
혹시 제가 잘못 이해하고있는 부분이 있을까요?
-
DEWH
2020.11.30 11:14
> 위 설명해서, dc ivac, A 가 완료하고 난 후에 store 연산 전에 CPU2의 cache에서 fetch하게 되면 결국 CPU2의 cache에 stale한 cache line이 생겨서 문제가 되는 부분은 이해가 되었으나, 사실 CPU2의 cache가 old val을 fetch하는 것은 CPU1에서 일어나는 store 명령어 전에 일어나게 되면 어쩔수 없이 일어나는 상황이 아닌가요?
네, Race condition의 예시로 살펴보았습니다. 따라서, 이런 상황을 피해야 하고, 그렇기 위해서 DMB를 사용하게 되었습니다.
> 즉, CPU2의 cache가 invalid상태에서 memory로 부터 data를 fetch하는 상황은 CPU1의 DC 그리고 store 명령어와는 별개로 독립적으로 일어날 수 있는 상황이라는 점에서는 DMB barrier가 상황을 도와주고있는 모습은 아닌듯 해 보입니다.
DMB의 sy 속성으로 CPU2에서 store이 먼저 완료되고 dc가 됨을 볼 수 있습니다. 이 순서를 보장한다면, CPU2에서 stale cache를 읽는 상황은 발생하지 않습니다.
ㅡㅡㅡㅡ ㅡㅡㅡㅡ
| CPU1 | | CPU2 |
ㅡㅡㅡㅡ ㅡㅡㅡㅡ
Cacheline fetch
store
dc // Fetch된 cacheline을 invalidate
...
load <- New val
> 오히려 CPU2의 cache에 stale한 cache line이 걱정이라면, 어떤 방식으로라든 CPU2에서 dc ivac, A 명령어를 수행해주는 게 본질적인 문제해결인듯 합니다.
CPU2에서 dc만 한다해서 Race condition이 사라지는 것은 아닙니다.(또한 어느 시점에 dc를 사용할지도 확실하지 않습니다.)
DMB없이 cpu2에서 dc2를 사용한다면 아래와 같은 상황이 발생할 수 있을 것 같습니다.
ㅡㅡㅡㅡ ㅡㅡㅡㅡ
| CPU1 | | CPU2 |
ㅡㅡㅡㅡ ㅡㅡㅡㅡ
dc
dc
cache line fetch
store
...
load <- stale cache!
.
안녕하세요? 문c 블로그(http://jake.dothome.co.kr)의 문영일입니다.
dmb는 캐시가 아니라 메모리 버퍼를 flush하는 동작을 수행합니다. 이 명령이 수행되면 물리 메모리에 대한 읽기 요청 및 쓰기 요청이 완료될 때까지 더 이상 요청을 받지 않고 대기합니다. 이러한 버퍼는 cpu -> L1 cache -> L2 cache -> buffer -> AXI bus -> DRAM controller -> DRAM 장치 순서를 따라 동작합니다. 캐시는 disable 해도 버퍼는 별도로 동작합니다. (캐시를 invalidate 하는 명령은 여러 가지 준비되어 있습니다)-> (수정) dmb는 버퍼를 flush하는 동작을 수행합니다. 이 명령을 완료하기 전까지 물리 메모리에 대한 읽기/쓰기 요청을 지연시킵니다. cpu -> write buffer -> L1 cache -> L2 cache -> outgoing buffer -> AXI bus -> DRAM controller -> DRAM 장치 순서를 따라 동작하고, mmu disable 상태에선 cpu -> AXI bus -> DRAM controller -> DRAM 순서로 동작합니다. mmu가 enable 상태에서 캐시 disable 상태에서도 버퍼를 별개로 동작시킬 수 있지만, mmu disable 상태에선 캐시와 버퍼 모두 disable 됩니다.
dmb 명령의 효과는 다음과 같습니다.
첫 번쨰,
out-of-order memory 장치를 가진 아키텍처(ARMv7, ARMv8 포함)에서 cpu가 내린 메모리 관련 로드/저장 및 캐시 조작 명령이 버퍼내에서 순서가 변경될 수 있는데 이를 방지하기 위해 메모리 배리어로 사용합니다.
두 번쨰,
cpu가 수행한 load/store 명령이 명확히(explict) 메모리에 기록되는 것을 보장하게 합니다. load/store 명령은 성능 향상을 위해 버퍼에서 대기하여 비동기로 메모리에 읽고 씁니다. cpu는 요청만하고 실제 메모리에 기록 여부를 체크하지 않습니다. 그러므로 버퍼를 사용한 물리메모리에 액세스할 때 중요한 기록을 수행한 후에는 dmb 명령을 내려야 합니다. 예를 들어 페이지 테이블 엔트리등을 기록하라고 하고 dmb 명령을 수행하지 않으면 버퍼에 있는 동안 지연되므로 다른 agent(다른 cpu, gpu, dma hw, pci 장치, MMU 장치의 페이지 테이블 Walk 등)에서 해당 주소의 값을 읽을 때 기록 전의 값을 읽어 오동작 할 수 있습니다. 물리 메모리 및 I/O 장치에 사용하는 물리 주소에 대한 접근을 위해 가상 주소 매핑을 하는데 이 때 캐시 및 버퍼의 사용 여부를 지정할 수 있습니다. 일반 메모리의 경우는 최대 성능을 위해 캐시 및 버퍼를 사용하는 매핑을 사용합니다. 가장 느린 디바이스의 경우 캐시 및 버퍼 모두 사용하지 않는 디바이스 매핑을 사용합니다.(예: ARMv7=MT_DEVICE(strong), ARMv8=MT_DEVICE_nGnRnE)-> (수정) cpu가 수행한 load/store 명령이 명확히(explict) 메모리에 기록하도록 버퍼를 flush 합니다. 성능을 위해 load/store 명령은 캐시 및 버퍼를 사용하고 DRAM에 기록하는 것을 확인하지 않습니다. 또한 mmu를 disable한 상태에서도 cpu는 요청만하고 DRAM에 기록하는 것을 확인하지 않습니다. 그러므로 물리메모리에 액세스할 때 중요한 기록을 수행한 후 이 데이터에 다른 다른 cpu들이 동기화하여 접근을 하기 위해서는 dmb 명령을 사용해야 합니다. 예를 들어 페이지 테이블 엔트리등을 기록하라고 하고 dmb(페이지 테이블은 dsb도 필요함) 명령을 수행하지 않으면 다른 agent(다른 cpu, gpu, dma hw, pci 장치, MMU 장치의 페이지 테이블 Walk 등)에서 해당 주소의 값을 읽을 때 기록 전의 값을 읽어 오동작 할 수 있습니다. 물리 메모리 및 I/O 장치에 사용하는 물리 주소에 대한 접근을 위해 가상 주소 매핑을 하는데 이 때 캐시 및 버퍼의 사용 여부를 지정할 수 있습니다. 일반 메모리의 경우는 최대 성능을 위해 캐시 및 버퍼를 사용하는 매핑을 사용합니다. 가장 느린 디바이스의 경우 캐시 및 버퍼 모두 사용하지 않는 디바이스 매핑을 사용합니다.(예: ARMv7=MT_DEVICE(strong), ARMv8=MT_DEVICE_nGnRnE). 최근 아키텍처에서 dmb는 버퍼 flush도 보장하지 않고, 그냥 베리어로서만 동작을 하는 것 같습니다. 즉 buffer flush 및 메모리에 대해 완전한 기록이 완료될 때까지 보장을 하려면 dmb 보다 느린 dsb를 사용해야 합니다.
감사합니다.