>  기사  >  시스템 튜토리얼  >  Linux 커널의 메모리 장벽에 대한 자세한 설명

Linux 커널의 메모리 장벽에 대한 자세한 설명

WBOY
WBOY앞으로
2024-02-10 15:00:24401검색

머리말

순차 일관성과 캐시 일관성에 대한 토론 기사를 읽은 적이 있으며 이 두 개념의 차이점과 연관성을 더 명확하게 이해했습니다. Linux 커널에는 많은 동기화 및 장벽 메커니즘이 있는데 여기서 요약하고 싶습니다.

Linux 커널의 메모리 장벽에 대한 자세한 설명

캐시 일관성

저는 Linux의 많은 메커니즘이 캐시 일관성을 보장하기 위한 것이라고 항상 생각했지만 실제로는 대부분의 캐시 일관성이 하드웨어 메커니즘에 의해 달성됩니다. 잠금 접두어가 있는 명령을 사용할 때만 캐싱과 관련이 있습니다(물론 엄격하지는 않지만 현재 관점에서 볼 때 대부분의 경우에 해당됩니다). 대부분의 경우 순차적 일관성을 보장하려고 합니다.

캐시 일관성은 다중 프로세서 시스템에서 각 CPU에 자체 L1 캐시가 있음을 의미합니다. 동일한 메모리의 내용이 서로 다른 CPU의 L1 캐시에 캐시될 수 있으므로 CPU가 캐시된 내용을 변경할 때 이 데이터를 읽을 때 다른 CPU도 최신 내용을 읽을 수 있도록 해야 합니다. 하지만 걱정하지 마십시오. MESI 프로토콜을 구현하면 하드웨어가 캐시 일관성 작업을 쉽게 완료할 수 있습니다. 여러 CPU가 동시에 쓰기를 해도 문제가 없습니다. 자체 캐시에 있든, 다른 CPU의 캐시에 있든, 메모리에 있든 CPU는 항상 최신 데이터를 읽을 수 있습니다. 이것이 바로 캐시 일관성이 작동하는 방식입니다.

순차적 일관성

순차적 일관성은 캐시 일관성과는 전혀 다른 개념이지만 둘 다 프로세서 개발의 산물입니다. 컴파일러 기술은 계속 발전하기 때문에 코드를 최적화하기 위해 특정 작업의 순서가 변경될 수 있습니다. 다중 문제 및 비순차적 실행의 개념은 오랫동안 프로세서에 존재해 왔습니다. 결과적으로 실행되는 명령어의 실제 순서는 프로그래밍 중 코드 실행 순서와 약간 다를 수 있습니다. 물론 이것은 단일 프로세서에서는 아무 것도 아닙니다. 결국 자신의 코드가 통과하지 않는 한 아무도 신경 쓰지 않을 것입니다. 컴파일러와 프로세서는 자신의 코드가 발견되지 않도록 하면서 실행 순서를 방해합니다. 그러나 다중 프로세서의 경우에는 그렇지 않습니다. 하나의 프로세서에서 명령이 완료되는 순서는 다른 프로세서에서 실행되는 코드에 큰 영향을 미칠 수 있습니다. 따라서 한 프로세서의 스레드 실행 순서가 다른 프로세서의 스레드 관점에서 동일하다는 것을 보장하는 순차적 일관성이라는 개념이 있습니다. 이 문제에 대한 해결책은 프로세서나 컴파일러만으로는 해결할 수 없으며 소프트웨어 개입이 필요합니다.

메모리 장벽

소프트웨어 개입 방법도 매우 간단합니다. 즉, 메모리 장벽을 삽입하는 것입니다. 실제로 메모리 장벽이라는 용어는 프로세서 개발자가 만든 용어이므로 우리가 이해하기 어렵습니다. 메모리 장벽은 쉽게 캐시 일관성을 초래할 수 있으며, 심지어 다른 CPU가 수정된 캐시를 볼 수 있도록 이를 수행할 수 있는지 의심스럽습니다. 프로세서 관점에서 소위 메모리 장벽은 읽기 및 쓰기 작업을 직렬화하는 데 사용되며 소프트웨어 관점에서는 순차적 일관성 문제를 해결하는 데 사용됩니다. 컴파일러는 코드 실행 순서를 방해하려고 하지 않습니까? 프로세서가 코드를 순서대로 실행하기를 원하지 않습니까? 메모리 장벽을 삽입하는 것은 컴파일러에게 명령의 전후 순서를 알려주는 것과 같습니다. 배리어는 되돌릴 수 없습니다. 이는 배리어 뒤에 있는 명령어가 실행되기 전에 명령어를 기다릴 수만 있음을 프로세서에 알려줍니다. 물론 메모리 장벽으로 인해 컴파일러가 문제를 일으키는 것을 막을 수 있지만 프로세서에는 여전히 방법이 있습니다. 프로세서에는 다중 문제, 비순차적 실행, 순차적 완료라는 개념이 없나요? 메모리 장벽 중에는 이전 명령어의 읽기 및 쓰기 작업이 완료되어야 한다는 것만 보장하면 됩니다. 다음 명령어의 읽기 및 쓰기 작업이 완료되었습니다. 따라서 메모리 배리어에는 읽기 배리어, 쓰기 배리어, 읽기/쓰기 배리어의 세 가지 유형이 있습니다. 예를 들어 x86 이전에는 쓰기 작업이 순서대로 완료되는 것이 보장되었으므로 쓰기 장벽이 필요하지 않았습니다. 그러나 이제 일부 ia32 프로세서의 쓰기 작업은 순서대로 완료되므로 쓰기 장벽도 필요합니다.
실제로 특수 읽기-쓰기 배리어 명령어 외에도 잠금 접두어가 있는 명령어와 같이 읽기-쓰기 배리어 기능으로 실행되는 명령어가 많이 있습니다. 특별한 읽기 및 쓰기 장벽 명령이 등장하기 전에 Linux는 생존을 위해 잠금에 의존했습니다.
읽기 및 쓰기 장벽을 삽입할 위치는 소프트웨어의 요구 사항에 따라 다릅니다. 읽기-쓰기 장벽은 순차 일관성을 완전히 달성할 수 없지만 다중 프로세서의 스레드는 검토할 때 순차 일관성을 준수한다고 생각하는 한 항상 실행 순서를 관찰하지는 않습니다. 실행으로 인해 코드에 예상치 못한 상황이 발생하지 않습니다. 예를 들어, 예상치 못한 상황이라고 하면 스레드가 먼저 변수 a에 값을 할당한 다음 변수 b에 값을 할당합니다. 결과적으로 다른 프로세서에서 실행 중인 스레드는 b에 값이 할당되었음을 확인합니다. 그러나 a에는 값이 할당되지 않았습니다. (참고 이 불일치는 캐시 불일치로 인해 발생하는 것이 아니라 프로세서 쓰기 작업이 완료되는 순서의 불일치로 인해 발생합니다.) 이 경우 할당 사이에 쓰기 장벽을 추가해야 합니다. a의 할당과 b의 할당.

여러 프로세서 간의 동기화

SMP를 사용하면 스레드가 여러 프로세서에서 동시에 실행되기 시작합니다. 스레드인 한 통신 및 동기화 요구 사항이 있습니다. 다행스럽게도 SMP 시스템은 공유 메모리를 사용합니다. 즉, 모든 프로세서가 동일한 메모리 내용을 볼 수 있지만 독립적인 L1 캐시가 있지만 캐시 일관성 처리는 여전히 하드웨어에서 처리됩니다. 서로 다른 프로세서의 스레드가 동일한 데이터에 액세스하려면 중요한 섹션과 동기화가 필요합니다. 어떤 동기화가 필요합니까? 이전 UP 시스템에서는 상단의 세마포어에 의존하고 하단의 인터럽트를 끄고 명령어를 읽고 수정하고 쓰기를 했습니다. 이제 SMP 시스템에서는 인터럽트 끄기가 폐지되었습니다. 동일한 프로세서에서 스레드를 동기화하는 것이 여전히 필요하지만 더 이상 그것에만 의존하는 것만으로는 충분하지 않습니다. 수정 쓰기 지침을 읽으시겠습니까? 더 이상은 아닙니다. 명령어의 읽기 작업이 완료되고 쓰기 작업이 수행되지 않으면 다른 프로세서가 읽기 작업 또는 쓰기 작업을 수행할 수 있습니다. 캐시 일관성 프로토콜은 발전했지만 어떤 명령이 이 읽기 작업을 실행했는지 예측할 만큼 아직 충분히 발전하지 않았습니다. 그래서 x86은 잠금 접두사가 있는 명령어를 발명했습니다. 이 명령어가 실행되면 명령어의 읽기 및 쓰기 주소가 포함된 모든 캐시 라인이 무효화되고 메모리 버스가 잠깁니다. 이런 방식으로 다른 프로세서가 동일한 주소 또는 동일한 캐시 라인의 주소를 읽거나 쓰려는 경우 캐시에서(캐시의 관련 라인이 만료됨)나 캐시에서 이를 수행할 수 없습니다. 메모리 버스(전체 메모리 버스가 잠김), 마침내 원자 실행 목표를 달성합니다. 물론 P6 프로세서부터 lock prefix 명령어로 접근할 주소가 이미 캐시에 있다면 메모리 버스를 잠글 필요가 없고 원자적 연산이 완료될 수 있다. L2 캐시로 인해 다중 프로세서 내부 공통이 추가되었습니다.

메모리 버스가 잠겨 있기 때문에 완료되지 않은 읽기 및 쓰기 작업은 메모리 장벽 역할도 하는 잠금 접두어가 있는 명령을 실행하기 전에 완료됩니다.
요즘 멀티 프로세서 간의 스레드 동기화는 상단의 스핀 잠금을 사용하고 하단의 잠금 접두사가 있는 명령을 읽고 수정하고 작성합니다. 물론 실제 동기화에는 프로세서의 작업 스케줄링을 비활성화하고 작업 오프 인터럽트를 추가하고 외부에 세마포어를 추가하는 것도 포함됩니다. Linux에서 이러한 종류의 스핀 잠금 구현은 4세대의 개발을 거쳐 더욱 효율적이고 강력해졌습니다.

内存屏障的实现

\#ifdef CONFIG_SMP  
\#define smp_mb()  mb()  
\#define smp_rmb()  rmb()  
\#define smp_wmb()  wmb()  
\#else  
\#define smp_mb()  barrier()  
\#define smp_rmb()  barrier()  
\#define smp_wmb()  barrier()  
\#endif 

CONFIG_SMP就是用来支持多处理器的。如果是UP(uniprocessor)系统,就会翻译成barrier()。

#define barrier() asm volatile(“”: : :”memory”)
barrier()的作用,就是告诉编译器,内存的变量值都改变了,之前存在寄存器里的变量副本无效,要访问变量还需再访问内存。这样做足以满足UP中所有的内存屏障。

\#ifdef CONFIG_X86_32  
/* 
 \* Some non-Intel clones support out of order store. wmb() ceases to be a 
 \* nop for these. 
 */  
\#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)  
\#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)  
\#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)  
\#else  
\#define mb()  asm volatile("mfence":::"memory")  
\#define rmb()  asm volatile("lfence":::"memory")  
\#define wmb()  asm volatile("sfence" ::: "memory")  
\#endif 

如果是SMP系统,内存屏障就会翻译成对应的mb()、rmb()和wmb()。这里CONFIG_X86_32的意思是说这是一个32位x86系统,否则就是64位的x86系统。现在的linux内核将32位x86和64位x86融合在同一个x86目录,所以需要增加这个配置选项。

可以看到,如果是64位x86,肯定有mfence、lfence和sfence三条指令,而32位的x86系统则不一定,所以需要进一步查看cpu是否支持这三条新的指令,不行则用加锁的方式来增加内存屏障。

SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。 
  SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。 
  LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。 
  MFENCE——串行化发生在MFENCE指令之前的读写操作。 
sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。 
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。 
mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

至于带lock的内存操作,会在锁内存总线之前,就把之前的读写操作结束,功能相当于mfence,当然执行效率上要差一些。

说起来,现在写点底层代码真不容易,既要注意SMP问题,又要注意cpu乱序读写问题,还要注意cache问题,还有设备DMA问题,等等。

多处理器间同步的实现
多处理器间同步所使用的自旋锁实现,已经有专门的文章介绍

위 내용은 Linux 커널의 메모리 장벽에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 lxlinux.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제