Timer -6- (Clocksource & Timer Driver)

<kernel v5.4>

Timer -6- (Clock Source & Timer Driver)

 

리눅스는 clock sources subsystem에 등록된 클럭 소스들 중 가장 정확도가 높고 안정적인 클럭 소스를 찾아 제공한다. 이 클럭 소스를 읽어 리눅스 시간 관리를 수행하는 timekeeping  subsystem에 제공된다.

  • 주요 연관 관계
    • clk(common clock foundation) -> clock sources subsystem -> timekeeping subsystem

 

또한 타이머를 사용하기 위해 커널은 timer 및 hrtimer API를 제공하고, 이의 타이머 hw 제어를 위해 최대한 아키텍처 독립적인 코드를 사용하여 clock events subsystem에 제공된다.

  • 주요 연관 관계
    • 타이머 프로그램: hrtimer API -> clock events susbsystem -> tick device -> timer hw
    • 타이머 만료: clk(common clock foundation) ->timer hw -> interrupt controller hw -> irq subsystem -> hrtimer API로 등록한 콜백 함수

 

clock events subsystem은 다음 두 가지 운용 모드를 사용하여 커널에 타이머 이벤트를 전달할 수 있다.

  • periodic
    • 규칙적으로 타이머 인터럽트를 발생시킨다.
  • oneshot
    • 단발 타이머 인터럽트가 발생할 수 있도록 만료시간을 프로그램할 수 있다.

 

다음 그림과 같이 1Mhz(1초에 100만번) 클럭 주파수를 사용하는 타이머 hw의 다운카운터에 10000 값을 설정하면, 10000 값이 0이 되기까지 10ms가 소요되며 그 후 타이머 인터럽트에가 발생되고, 이에 해당하는 타이머 ISR이 동작하는 것을 알 수 있다.

 

다음 그림은 각각의 cpu에 포함된 타이머에 기준 클럭을 공급하는 모습을 보여준다.

  • cpu마다 사용되는 타이머 인터럽트를 통해 타이머가 깨어나고 다음 타이머를 프로그램하는 과정이다.

 

타이머 클럭 주파수

타이머에 공급되는 클럭 주파수는 높으면 높을 수록 타이머의 해상도는 높아진다. 커널에 구현된 hrtimer는 1나노초 단위부터 제어 가능하다. 따라서 정밀도를 높이기 위해 이상적인 클럭 주파수는 1Ghz 이상을 사용하면 좋다. 그러나 외부 클럭 주파수가 높으면 높을 수록 전력 소모가 크므로 적절한 선으로 제한하여 사용한다. 현재 수 Mhz ~ 100Mhz의 클럭을 사용하며, 언젠가 필요에 의해 1Ghz 클럭도 사용되리라 판단된다. 저전력 시스템을 위해 500~20Khz를 사용하는 경우도 있다.

 

타이머 하드웨어 종류

타이머 하드웨어는 매우 다양하다. 이들을 크게 두 그룹으로 나눈다.

  • 아키텍처 내장 타이머 (architected timer)
    • arm per-core 아키텍처 내장 타이머 (rpi2, rpi3, rpi4)
    • arm 메모리 맵드 아키텍처 내장 타이머
  • 아키텍처 비내장 타이머 (SoC에 포함)
    • arm 글로벌 타이머
    • bcm2835 타이머 (rpi)

 

arm 아키텍처 내장형 generic 타이머 hw 타입

arm 아키텍처에 따라 generic timer extension이 적용되어 아키텍처에 내장된 generic 타이머들이다. 이 타이머는 SoC 제조 업체에서 설계에 따라 다음 2 가지 타입으로 나뉘어 사용된다. 스마트 폰등의 절전(deep sleep)을 위해 아키텍처 내장형 타이머는 주 타이머로 사용하고, 절전을 위해 메모리 매핑형 타이머를 브로드 캐스트 타이머로 나누어 동시에 동작시키기도 한다.

  • 코어별 아키텍처 내장 타이머(per-core architected timer)
    • per-cpu 보조프로세서 cp15를 통해 타이머 레지스터를 사용한다.
    • GIC의 PPIs를 사용하여 인터럽트를 전달한다.
    • armv7 및 armv8 아키텍처에서 사용된다.
  • 메모리 매핑형 아키텍처 내장 타이머(memory mapped architected timer)
    • 타이머 레지스터들을 주소 공간에 매핑하여 사용한다.
      • 장점으로 user 영역에서도 타이머에 접근할 수 있다.
    • 타이머당 8개까지의 프레임(타이머)을 사용할 수 있다.
    • GIC의 SPIs를 사용하여 인터럽트를 전달한다.
      • 특정 1개의 cpu만 인터럽트를 받을 수 있으므로 수신을 원하는 cpu로 인터럽트를 미리 라우팅해두어야 한다.
    • 틱 브로드 캐스트용 타이머로 사용한다.

 

코어별 아키텍처 내장 타이머 디바이스

rpi3용 “arm,armv7-timer” 타이머

arch/arm/boot/dts/bcm2837.dtsi

	timer {
		compatible = "arm,armv7-timer";
		interrupt-parent = <&local_intc>;
		interrupts = <0 IRQ_TYPE_LEVEL_HIGH>, // PHYS_SECURE_PPI
			     <1 IRQ_TYPE_LEVEL_HIGH>, // PHYS_NONSECURE_PPI
			     <3 IRQ_TYPE_LEVEL_HIGH>, // VIRT_PPI
			     <2 IRQ_TYPE_LEVEL_HIGH>; // HYP_PPI
		always-on;
	};

 

rpi4용 “arm,armv8-timer” 타이머

https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/arm/boot/dts/bcm2838.dtsi

  • 아래 GIC_PPI 14은 rpi4가 사용하는 non-secure timer로 SGI에서 사용하는 irq 수 16을 더해 PPI ID30(hwirq=30)이다.
	timer {
		compatible = "arm,armv7-timer";
		interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>,
			     <GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>,
			     <GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>,
			     <GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) |
					  IRQ_TYPE_LEVEL_LOW)>;
		arm,cpu-registers-not-fw-configured;
		always-on;
	};

arm,armv7-timer” 또는 “arm,armv8-timer” 타이머에 대한 속성의 용도는 다음과 같다.

  •  compatible
    • 타이머 디바이스 드라이버명 “arm,armv7-timer” 또는 “arm,armv8-timer”
  • interrupt-parent
    • 타이머 만료 시각에 local_intc 인터럽트 컨트롤러에 인터럽트가 발생된다.
  • interrupts
    • cpu core마다 최대 4개의 타이머가 사용될 수 있으며 각각에 대해 인터럽트가 발생된다. 각 타이머 인터럽트에 연결된 PPI 번호를 지정한다.
    • 4개 타이머 용도는 각각 다르다.
      • 시큐어 펌웨어용
      • non 시큐어용으로 커널이 사용
      • Guest 커널용
      • 하이퍼바이저용
  • always-on
    • 타이머에 c3 절전 기능이 없다. 즉 별도의 파워 블럭으로 관리되지 않고 항상 켜져있는 타이머이다.
    • suspend시에도 절대 타이머 전원이 다운되지 않는 시스템에서 이 속성을 사용한다.

다음은 옵션 파라미터이다.

  • clock-frequency
    • 타이머를 이용하기 위해 펌웨어가 CNTFREQ 레지스터에 클럭 주파수를 먼저 설정한다. 따라서 커널은 부팅 시 CNTFREQ 레지스터를 설정할 필요 없이 해당 레지스터의 값을 읽어오는 것으로 클럭 주파수를 알아올 수 있다. 만일 펌웨어가 CNTFREQ 레지스터에 클럭 주파수를 설정하지 않는 시스템을 사용하는 경우 이 속성 항목을 읽어 타이머를 초기화하여야 한다.가능하면 이 속성은 사용하지 않는 것을 권장한다.
  •  arm,cpu-registers-not-fw-configured
    • 펌웨어(주로 시큐어 펌웨어)가 어떠한 타이머 레지스터도 사용하지 않는 시스템이다. 이러한 경우 현재 커널이 시큐어용 타이머를 사용하도록 한다.
  •  arm,no-tick-in-suspend
    • 시스템 suspend(깊은 절전) 시 타이머가 stop 되는 기능이 있는 경우 사용된다.

 

메모리 매핑형 아키텍처 내장 타이머 디바이스

타이머 레지스터를 별도의 주소 공간에 매핑하므로 추가로 reg 속성을 사용하고, 최대 8개까지 frame을 사용한다.

arch/arm/boot/dts/qcom-apq8084.dtsi

soc: soc {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;
        compatible = "simple-bus";

        timer@f9020000 {
                #address-cells = <1>;
                #size-cells = <1>;
                ranges;
                compatible = "arm,armv7-timer-mem";
                reg = <0xf9020000 0x1000>;
                clock-frequency = <19200000>;

                frame@f9021000 {
                        frame-number = <0>;
                        interrupts = <0 8 0x4>,
                                     <0 7 0x4>;
                        reg = <0xf9021000 0x1000>,
                              <0xf9022000 0x1000>;
                };

               (...생략...)

 


ARM per-core 아키텍처 내장 Generic Timer

다음 그림은 ARM SMP 각 core에 내장된 generic 타이머가 각 모드별로 사용하는 타이머 종류를 보여준다.

각 core 마다 최대 7개(4+3)개의 타이머가 인터럽트와 연결되어 있다.

  • EL1 physical timer
    • non 시큐어용으로 커널이 사용
      • 예) rpi4에서 부팅되는 메인 커널이 사용
  • EL1 virtual timer
    • Guest 커널용
      • 예) kvm용 guest 타이머
  • Non-secure EL2 physical timer
    • 하이퍼바이저용
      • 예) xen 하이퍼바이저
  • Non-secure EL2 virtual timer
    • ARMv8.1-VHE 구현시 사용 가능
  • Secure EL2 virtual timer
    • ARMv8.4-SecEL2 구현시 사용 가능
  • Secure EL2 physical timer
    • ARMv8.4-SecEL2 구현시 사용 가능
  • EL3 physical timer
    • 시큐어 펌웨어용
    • 시큐어 펌웨어를 사용하지 않는 경우 커널이 사용할 수도 있다.

 

다음 그림은 arm64 시스템에서 core별로 최대 7개의 타이머를 사용할 수 있음을 보여준다.

 

arm32 시스템에서는 core마다 최대 4개의 타이머를 사용할 수 있다.

  • Physical 타이머는 뱅크되어 3개의 모드에서 각각 사용된다.
    • PL1 Secure 모드
    • PL1 Non-Secure 모드
    • PL2 하이퍼 모드
  • Virtual 타이머는 guest OS에 사용되며 하이퍼 바이저가 설정하는 voffset을 더해 사용된다.

 

다음은 rpi4 시스템에서 사용 중인 타이머를 보여준다. 4개 각 cpu에 내장된 타이머이며 PPI ID30(hwirq=30)에 non-secure physical timer가 동작하고 있다.

  • PPI#27은 KVM을 사용하여 두 개의 cpu에 배정하여 동작시키고 있다.
$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
  1:          0          0          0          0     GICv2  25 Level     vgic
  3:   18098867    2706362   12943752    3421084     GICv2  30 Level     arch_timer
  4:    4109642     550709          0          0     GICv2  27 Level     kvm guest timer
  8:        282          0          0          0     GICv2 114 Level     DMA IRQ
 16:    1645412          0          0          0     GICv2  65 Level     fe00b880.mailbox
 19:       6648          0          0          0     GICv2 153 Level     uart-pl011
 21:          0          0          0          0     GICv2 169 Level     brcmstb_thermal
 22:    3790111          0          0          0     GICv2 158 Level     mmc1, mmc0
 24:          0          0          0          0     GICv2  48 Level     arm-pmu
 25:          0          0          0          0     GICv2  49 Level     arm-pmu
 26:          0          0          0          0     GICv2  50 Level     arm-pmu
 27:          0          0          0          0     GICv2  51 Level     arm-pmu
 28:          0          0          0          0     GICv2 106 Level     v3d
 30:   27727409          0          0          0     GICv2 189 Level     eth0
 31:    3055759          0          0          0     GICv2 190 Level     eth0
 37:         32          0          0          0     GICv2  66 Level     VCHIQ doorbell
 39:    1921424          0          0          0  Brcm_MSI 524288 Edge      xhci_hcd
IPI0:   1796626    2697423    5510661    2638902       Rescheduling interrupts
IPI1:      8372     356892     286341     302455       Function call interrupts
IPI2:         0          0          0          0       CPU stop interrupts
IPI3:         0          0          0          0       CPU stop (for crash dump) interrupts
IPI4:         0          0          0          0       Timer broadcast interrupts
IPI5:   5243413     274725    4761277     692912       IRQ work interrupts
IPI6:         0          0          0          0       CPU wake-up interrupts

 

클럭 소스와 클럭 이벤트용 레지스터

클럭 소스에 사용되는 카운터 레지스터

  • 7개의 CNTxxCT_ELx 레지스터는 64비트 up-counter 레지스터로 클럭 주파수에 맞춰 계속 증가한다. 이 사이클 값을 읽어 리눅스 커널의 timekeeping 및 sched_clock 등에 제공하여 리눅스 시각과 태스크 소요 시간등을 갱신한다.

 

클럭 이벤트 프로그래밍에 사용되는 2 가지 타이머 레지스터

  • 첫 번째 방법
    • Timer value(7개의 CNTxxTVAL_ELx )레지스터는 32비트 down-counter로 클럭 주파수에 맞춰 감소하다 0이되면 타이머 인터럽트가 발생한다.
    • 인터럽트 조건 = timer value == 0
    • arm32, arm64 리눅스 커널은 이 레지스터를 사용하여 타이머를 동작시킨다.
  • 두 번째 방법
    • Compare value(7개의 CNTxxCVAL_ELx) 64비트 레지스터 값을 Counter(CNTxxCT_ELx) 레지스터 값이 같거나 초과하는 순간 타이머 인터럽트가 발생한다.
      • 인터럽트 조건 = counter – offset – compare value
      • Virtual 타이머의 경우 offset을 추가해야 한다.

 

다음 그림은 arm32에서 generic 타이머들의 구성을 보여준다.

  • 하이퍼 바이저용 Physical 카운터 관련 레지스터들(CNTHP_CTL, CNTHPCT, CNTHP_TVAL, CNTHP_CVAL) 생략
  • 이벤트 스트림 기능은 ARM 전용으로 atomic하게 busy wait 하는 delay 류의 API를 위해 ARM에서 절전을 위해 사용하는 wfe를 사용하는 중에 지속적으로 깨워 loop를 벗어나는 조건을 체크하기 위해 사용된다.

 


타이머 하드웨어

1) ARM 코어별 아키텍처 내장형 generic 타이머

대부분의 arm 및 arm64 시스템에서 사용하는 방법이다.

특징

  • 아키텍처 코어마다 2~4개의 타이머를 제공한다. (32bit down 카운터 및 64bit up 카운터를 제공)
    • no Security Extension & no Virutalization Extension
      • Physical 타이머 (커널용)
      • Virtual 타이머 (Guest OS용)
    • Security Extension only
      • Non-secure physical 타이머 (커널용)
      • Secure physical 타이머 (Secure 펌웨어용)
      • Virtual 타이머 (Guest OS용)
    • Security Extension & Virtualization Extension
      • Non-secure PL1 타이머 (커널용)
      • Secure PL1 physical 타이머 (Secure 펌웨어용)
      • Non-secure PL2 physical 타이머 (하이퍼바이저용)
      • Virtual 타이머 (Guest OS용)
  • Generic 타이머는 ARM 아키텍처가 Generic Timer Extension을 지원하는 경우에 사용할 수 있다.
    • ARMv7 및 ARMv8에 채용되었다.
  • 연속(continuous) 및 one shot event 지원
    • GIC의 PPI를 통해 만료 타임 시 인터럽트 시그널을 보낼 수 있다.
  • 고해상도 타이머(hrtimer)로 동작한다.
  • 타이머 레지스터에 접근하기 위해 코프로세서를 통해 사용할 수 있다.(CP14 & cp15)

 

다음 그림은 core별로 타이머가 내장되었고, GIC(Global Interrupt Controller)의 PPI를 통해 연결된 것을 확인할 수 있다.

 

Generic 타이머 레지스터

ARM64용 generic 타이머 레지스터

 

다음은 ARM64 generic 타이머 레지스터들 중 리눅스 커널과 관련된 레지스터들만 표현하였다.

  • CNTFRQ_EL0, Counter-timer Frequency register
    • bit[31..0]에 클럭 주파수가 설정된다.
    • 이 레지스터 값에 접근하려면 다음과 같이 코프로세서 명령을 사용한다.
      • MSR or MRC p15, 0, <Register>, c14, c0, 0
    • 예) 19.2Mhz의 클럭이 공급되는 경우 19200000 값이 설정된다.
  • CNTKCTL_EL1, Counter-timer Kernel Control register
    • EL0PTEN
      • EL0에서 physical 카운터 관련 레지스터들의 접근 설정. 0=disable, 1=enable
    • EL0VTEN
      • EL0에서 virtual 카운터 관련 레지스터들의 접근 설정. 0=disable, 1=eable
    • EVNTI
      • virtual 카운터로 트리거되는 이벤트 스트림 값
    • EVNTDIR
      • EVNTI 값으로 트리거될 때 방향. 0=0→1로 전환, 1→0 전환
    • EVNTEN
      • virtual 카운터로부터 event 스트림 생성. 0=disable, 1=enable
    • EL0VCTEN
      • PL0에서 virtual 카운터 및 frequency 레지스터의 접근 설정. 0=disable, 1=enable
    • EL0PCTEN
      • PL0에서 physical 카운터 및 frequency 레지스터의 접근 설정. 0=disable, 1=enable
  • CNTP_CTL_EL0, Counter-timer Physical Timer Control register
  • CNTV_CTL_EL0, Counter-timer Virtual Timer Control register
    • ISTATUS
      • 타이머 조건 만족 여부. 1=meet condition, 0=not meet condition
    • IMASK
      • 타이머 조건을 만족시키는 경우 인터럽트 발생 여부를 제어한다. 0=assert(타이머 설정 시 사용), 1=none
    • ENABLE
      • 타이머 사용 여부. 1=enable, 0=disable(shutdown)
  • CNTPCT_EL0, Counter-timer Physical Count register
  • CNTVCT_EL0, Counter-timer Virtual Count register
    • bit[61..0]에 카운터 값이 있다.
  • CNTP_TVAL_EL0, Counter-timer Physical Timer TimerValue register
  • CNTV_TVAL_EL0, Counter-timer Virtual Timer TimerValue register
    • bit[31..0]에 타이머 값이 설정된다.
  • CNTHCTL_EL2, Counter-timer Hypervisor Control register
  • CNTHP_CTL_EL2, Counter-timer Hypervisor Physical Timer Control register
  • CNTHP_CVAL_EL2, Counter-timer Physical Timer CompareValue register (EL2)
  • CNTHP_TVAL_EL2, Counter-timer Physical Timer TimerValue register (EL2)
  • CNTHPS_CTL_EL2, Counter-timer Secure Physical Timer Control register (EL2)
  • CNTHPS_CVAL_EL2, Counter-timer Secure Physical Timer CompareValue register (EL2)
  • CNTHPS_TVAL_EL2, Counter-timer Secure Physical Timer TimerValue register (EL2)
  • CNTHV_CTL_EL2, Counter-timer Virtual Timer Control register (EL2)
  • CNTHV_CVAL_EL2, Counter-timer Virtual Timer CompareValue register (EL2)
  • CNTHV_TVAL_EL2, Counter-timer Virtual Timer TimerValue Register (EL2)
  • CNTHVS_CTL_EL2, Counter-timer Secure Virtual Timer Control register (EL2)
  • CNTHVS_CVAL_EL2, Counter-timer Secure Virtual Timer CompareValue register (EL2)
  • CNTHVS_TVAL_EL2, Counter-timer Secure Virtual Timer TimerValue register (EL2)
  • CNTP_CVAL_EL0, Counter-timer Physical Timer CompareValue register
  • CNTPS_CTL_EL1, Counter-timer Physical Secure Timer Control register
  • CNTPS_CVAL_EL1, Counter-timer Physical Secure Timer CompareValue register
  • CNTPS_TVAL_EL1, Counter-timer Physical Secure Timer TimerValue register
  • CNTV_CVAL_EL0, Counter-timer Virtual Timer CompareValue register
  • CNTVOFF_EL2, Counter-timer Virtual Offset register

 

다음 그림은 10ms 타이머를 설정하고 만료 시 타이머 인터럽트가 발생하는 과정을 타이머 레지스터로 보여준다.

 

ARM32용 generic 타이머 레지스터

 

다음은 ARMv7의 타이머 관련 레지스터들을 보여준다.

 

2) BCM2708(rpi) 타이머

32비트 raspberry pi에서 사용된 글로벌 타이머이다. 이외에 몇몇 시스템에서 한정하여 사용된다.

특징

  • 1개의 글로벌 타이머 제공
  • 연속(continuous) 모드만 지원
  • extra 클럭 분주기 레지스터
  • extra stop 디버그 모드 컨트롤 비트
  • 32bit 프리 러닝 카운터
  • ARM 시스템 타이머를 일부 개량하였다.
  • rpi: 1Mhz 클럭을 사용

 

시스템 타이머 레지스터

  • Timer Load
  • Timer Value (RO)
  • Timer Control (RW)
    • 아래 그림 참고
  • Timer IRQ Clear/Ack (WO)
    • 1을 기록 시 인터럽트 pending bit를 클리어한다.
    • 리셋 값: 0x003e_0020
  • Timer RAW IRQ (RO)
    • 인터럽트 pending bit 상태를 알아온다.
      • 0=clear, 1=set
  • Timer Masked IRQ (RO)
    • 인터럽트 마스크 상태를 알아온다.
      • 0=인터럽트 disable(mask), 1=인터럽트 enabled(unmask)
  • Timer Reload (RW)
    • Load 레지스터의 사본과 동일하다 다만 다른 점은 값을 설정해도 리로드가 즉시 발생하지 않는다.
  • Timer Pre-divider (RW)
    • APB 클럭을 설정된 lsb 10비트 값 + 1로 분주한다.
      • timer_clock = apb_clock / (pre_divider + 1)
    • 리셋 시 0x7d가 설정된다.
  • Timer Free Running Counter

 

Timer Control Register

ARM의 SP804는 8비트만 사용하는데 Boradcomm이 이를 확장하여 24bit를 운용한다.

  • 8bit pre-scaler를 사용하여 시스템 클럭 / (prescaler 값 + 1)이 사용된다.
  • 리셋 값: 0x003e_0020
    • pre-scaler: 62 (0x3e)
    • timer interrupt enable (bit5)

 


Device Tree 기반 타이머 초기화

다음 그림은 arm 및 arm64 아키텍처에 내장된 generic 타이머를 사용하는 드라이버의 초기화 함수를 호출하는 예를 보여준다.

 

timer_probe()

drivers/clocksource/timer-probe.c

void __init timer_probe(void)
{
        struct device_node *np;
        const struct of_device_id *match;
        of_init_fn_1_ret init_func_ret;
        unsigned timers = 0;
        int ret;

        for_each_matching_node_and_match(np, __timer_of_table, &match) {
                if (!of_device_is_available(np))
                        continue;

                init_func_ret = match->data;

                ret = init_func_ret(np);
                if (ret) {
                        if (ret != -EPROBE_DEFER)
                                pr_err("Failed to initialize '%pOF': %d\n", np,
                                       ret);
                        continue;
                }

                timers++;
        }

        timers += acpi_probe_device_table(timer);

        if (!timers)
                pr_crit("%s: no matching timers found\n", __func__);
}

커널에 등록된 디바이스 드라이버들 중 디바이스 트리 또는 ACPI 정보가 요청한 타이머를 찾아 초기화(probe) 함수를 호출한다.

  • _timer_of_table 섹션에 등록된 디바이스 드라이버들의 이름과 Device Tree 또는 ACPI 테이블로 부터 읽은 노드의 디바이스명이 매치되는 드라이버들의 초기화 함수들을 호출한다.

 

per core 아키텍처 내장 타이머 초기화 (armv7, armv8)

다음 그림은 arm 및 arm64 아키텍처 내장형 generic 타이머 초기화 함수의 함수 호출 관계를 보여준다.

 

arch_timer_of_init()

drivers/clocksource/arm_arch_timer.c

static int __init arch_timer_of_init(struct device_node *np)
{
        int i, ret;
        u32 rate;

        if (arch_timers_present & ARCH_TIMER_TYPE_CP15) {
                pr_warn("multiple nodes in dt, skipping\n");
                return 0;
        }

        arch_timers_present |= ARCH_TIMER_TYPE_CP15;
        for (i = ARCH_TIMER_PHYS_SECURE_PPI; i < ARCH_TIMER_MAX_TIMER_PPI; i++)
                arch_timer_ppi[i] = irq_of_parse_and_map(np, i);

        arch_timer_populate_kvm_info();

        rate = arch_timer_get_cntfrq();
        arch_timer_of_configure_rate(rate, np);

        arch_timer_c3stop = !of_property_read_bool(np, "always-on");

        /* Check for globally applicable workarounds */
        arch_timer_check_ool_workaround(ate_match_dt, np);

        /*
         * If we cannot rely on firmware initializing the timer registers then
         * we should use the physical timers instead.
         */
        if (IS_ENABLED(CONFIG_ARM) &&
            of_property_read_bool(np, "arm,cpu-registers-not-fw-configured"))
                arch_timer_uses_ppi = ARCH_TIMER_PHYS_SECURE_PPI;
        else
                arch_timer_uses_ppi = arch_timer_select_ppi();

        if (!arch_timer_ppi[arch_timer_uses_ppi]) {
                pr_err("No interrupt available, giving up\n");
                return -EINVAL;
        }

        /* On some systems, the counter stops ticking when in suspend. */
        arch_counter_suspend_stop = of_property_read_bool(np,
                                                         "arm,no-tick-in-suspend");

        ret = arch_timer_register();
        if (ret)
                return ret;

        if (arch_timer_needs_of_probing())
                return 0;

        return arch_timer_common_init();
}
TIMER_OF_DECLARE(armv7_arch_timer, "arm,armv7-timer", arch_timer_of_init);
TIMER_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);

armv7 & armv8 아키텍처에 내장된 Generic 타이머를 사용하는 디바이스 트리용 디바이스 드라이버의 초기화 함수이다.

  • 코드 라인 6~11에서 디바이스 트리에서 cp15를 사용하는 방식의 per-core 아키텍처 내장형 타이머가 이 드라이버를 사용하여 초기화 함수로 진입하는 경우 한 번만 초기화하고 그 이후 호출되는 경우 skip 한다.
    • 참고로 cp15를 사용하는 arm per-core 아키텍처 내장형 타이머와 arm 메모리 맵드 아키텍처 내장 타이머 두 개 다 같이 운영될 수 있다.
  • 코드 라인 12~13에서 아키텍처의 각 코어가 지원하는 최대 4개 타이머 수만큼 순회하며 타이머 노드의 “intrrupts” 속성 값을 읽어 각 타이머가 연결된 PPI에 해당하는 인터럽트를 요청하고 그 결과인 virq를 저장한다.
  • 코드 라인 15에서 커널의 타이머 virq와 Guest 커널용 타이머 virq를 각각 kvm 타이머 정보에 저장한다.
  • 코드 라인 17~18에서 다음 두 가지 방법으로 클럭 주파수 값을 알아와서 arch_timer_rate에 저장한다.
    • 디바이스 트리를 사용하는 경우
      • “clock-frequency” 속성 값에서 클럭 주파수 값을 알아온다. 만일 속성이 지정되지 않은 경우 CNTFRQ(Counter Frequency Register) 레지스터에서 읽은 값을 클럭 주파수 값으로 사용한다.
    • ACPI를 사용하는 경우
      • CNTFRQ 레지스터에서 읽은 값을 클럭 주파수 값으로 사용한다.
  • 코드 라인 20에서 절전(c3stop) 기능 여부를 알아온다.
    • 타이머 노드의 “always-on” 속성이 없는 경우 절전 기능이 사용되도록 arch_timer_c3stop을 true로 설정한다.
    • deep-sleep 상태의 코어에 대해 타이머 장치나 인터럽트 장치도 절전을 위해 파워가 off될 수 있는데 이러한 절전(c3stop) 기능이 사용되면 안되는 시스템에서는 “always-on”을 설정한다.
  • 코드 라인 23에서 Cortex-A72 타이머 errta 루틴을 동작시킨다.
    • unstable한 코어를 찾으면 유저 스페이스 인터페이스(vdso-direct 포함)에서 직접 타이머 레지스터의 액세스를 사용하지 않게 한다.
  • 코드 라인 29~38에서 시스템이 지원하는 최대 4개의 타이머들 중 현재 커널이 사용할 적절한 타이머를 선택한다.
    • 타이머 노드의 “arm,cpu-registers-not-fw-configured” 속성이 있는 경우 시큐어 펌웨어를 사용하지 않는 경우이다. 이러한 경우 시큐어용 타이머를 커널에서 사용하도록 선택한다. 이 옵션을 사용하는 장점으로 남는 두 타이머(non-secure phys 타이머 및 virt 타이머)들을 guest os를 위해 더 남겨둘 수 있다.
  • 코드 라인 41~42에서 “arm,no-tick-in-suspend” 속성이 존재하는 경우 타이머가 suspend 기능이 있다고 판단하도록 arch_counter_suspend_stop을 true로 설정한다.
  • 코드 라인 44~46에서 현재 커널이 사용하도록 지정된 Generic 타이머를 per-cpu 인터럽트에 등록하고 boot cpu용은 즉시 enable하고 클럭 이벤트 디바이스에 등록한다
  • 코드 라인 48~51에서 cp15를 사용하는 arm per-core 아키텍처 내장형 타이머와 arm 메모리 맵드 아키텍처 내장 타이머 두 개 다 같이 운영해야 하는 경우 둘 다 probe 된 경우에만 마지막에 arch_timer_common_init() 함수가 호출되도록 한다. 이 루틴에서는 가장 높은 rate의 Generic 타이머를 클럭 소스 및 스케줄러 클럭으로 등록한다.

 

다음 그림은 디바이스 트리의 타이머 노드 정보를 읽어오는 모습을 보여준다. (예: rpi3 & rpi4)

 

arch_timer_populate_kvm_info()

drivers/clocksource/arm_arch_timer.c

static void __init arch_timer_populate_kvm_info(void)
{
        arch_timer_kvm_info.virtual_irq = arch_timer_ppi[ARCH_TIMER_VIRT_PPI];
        if (is_kernel_in_hyp_mode())
                arch_timer_kvm_info.physical_irq = arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI];
}

kvm을 위해 사용할 타이머 정보를 저장한다.

  • 코드 라인 3에서 guest os용 타이머 irq를 kvm용 가상 타이머 정보에 저장한다.
  • 코드 라인 4~5에서 커널이 하이퍼모드에서 동작한 경우 non-secure 물리 타이머용 irq를 kvm용 물리 타이머 정보에 저장한다.

 

타이머 Rate 읽기

arch_timer_get_cntfrq() – ARM64

arch/arm64/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntfrq(void)
{
        return read_sysreg(cntfrq_el0);
}

CNTFRQ_EL0(Counter Frequency Register)로부터 rate 값을 읽어 반환한다.

 

read_sysreg() – ARM64

arch/arm64/include/asm/sysreg.h

/*
 * Unlike read_cpuid, calls to read_sysreg are never expected to be
 * optimized away or replaced with synthetic values.
 */
#define read_sysreg(r) ({                                       \
        u64 __val;                                              \
        asm volatile("mrs %0, " __stringify(r) : "=r" (__val)); \
        __val;                                                  \
})

@r 레지스터 값을 읽어온다. (u64 값 반환)

 

arch_timer_get_cntfrq() – ARM32

arch/arm/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntfrq(void)
{
        u32 val;
        asm volatile("mrc p15, 0, %0, c14, c0, 0" : "=r" (val));
        return val;
}

CNTFRQ(Counter Frequency Register)로부터 rate 값을 읽어 반환한다.

  • CNTFRQ(Counter Frequency Register)
    • 시스템 카운터의 주파수가 담겨있다. 32bit 값을 사용한다.
    • rpi2: 19,200,000 (19.2Mhz)

 

arch_timer_of_configure_rate()

drivers/clocksource/arm_arch_timer.c

/*
 * For historical reasons, when probing with DT we use whichever (non-zero)
 * rate was probed first, and don't verify that others match. If the first node
 * probed has a clock-frequency property, this overrides the HW register.
 */
static void arch_timer_of_configure_rate(u32 rate, struct device_node *np)
{
        /* Who has more than one independent system counter? */
        if (arch_timer_rate)
                return;

        if (of_property_read_u32(np, "clock-frequency", &arch_timer_rate))
                arch_timer_rate = rate;

        /* Check the timer frequency. */
        if (arch_timer_rate == 0)
                pr_warn("frequency not available\n");
}

디바이스 트리의 @np 타이머 노드에서 “clock-frequency” 속성을 읽어 아키텍처 내장형 타이머의 rate 값을 저장한다.

  • 코드 라인 4~5에서 아키텍처 내장형 타이머의 rate 값이 이미 설정된 경우 함수를 빠져나간다.
  • 코드 라인 7~8에서 디바이스 트리의 @np 타이머 노드에서 “clock-frequency” 속성을 읽어 아키텍처 내장형 타이머의 rate 값을 저장한다.
  • 코드 라인 11~12에서 rate 값이 여전히 0이면 경고 메시지를 출력한다.

 

적절한 타이머 PPI 선택

arch_timer_select_ppi()

drivers/clocksource/arm_arch_timer.c

/**
 * arch_timer_select_ppi() - Select suitable PPI for the current system.
 *
 * If HYP mode is available, we know that the physical timer
 * has been configured to be accessible from PL1. Use it, so
 * that a guest can use the virtual timer instead.
 *
 * On ARMv8.1 with VH extensions, the kernel runs in HYP. VHE
 * accesses to CNTP_*_EL1 registers are silently redirected to
 * their CNTHP_*_EL2 counterparts, and use a different PPI
 * number.
 *
 * If no interrupt provided for virtual timer, we'll have to
 * stick to the physical timer. It'd better be accessible...
 * For arm64 we never use the secure interrupt.
 *
 * Return: a suitable PPI type for the current system.
 */
static enum arch_timer_ppi_nr __init arch_timer_select_ppi(void)
{
        if (is_kernel_in_hyp_mode())
                return ARCH_TIMER_HYP_PPI;

        if (!is_hyp_mode_available() && arch_timer_ppi[ARCH_TIMER_VIRT_PPI])
                return ARCH_TIMER_VIRT_PPI;

        if (IS_ENABLED(CONFIG_ARM64))
                return ARCH_TIMER_PHYS_NONSECURE_PPI;

        return ARCH_TIMER_PHYS_SECURE_PPI;
}

현재 커널 시스템이 사용할 적절한 타이머 PPI를 선택한다.

  • 코드 라인 3~4에서 커널이 하이퍼 모드에서 동작하는 경우 하이퍼 바이저용 타이머 PPI를 선택한다.
  • 코드 라인 6~7에서 시스템이 제공하지 않거나 다른 상위에서 하이퍼 바이저가 동작 중이라, 하이퍼 모드를 사용할 수 없는 경우 이 커널은 guest os용 타이머 PPI를 선택한다.
  • 코드 라인 9~12에서 arm64 커널인 경우 non-secure 커널 타이머 PPI를 선택하고, arm32 커널인 경우 secure 커널 타이머를 선택한다.

 

arch_timer_needs_of_probing()

drivers/clocksource/arm_arch_timer.c

static bool __init arch_timer_needs_of_probing(void)
{
        struct device_node *dn;
        bool needs_probing = false;
        unsigned int mask = ARCH_TIMER_TYPE_CP15 | ARCH_TIMER_TYPE_MEM;

        /* We have two timers, and both device-tree nodes are probed. */
        if ((arch_timers_present & mask) == mask)
                return false;

        /*
         * Only one type of timer is probed,
         * check if we have another type of timer node in device-tree.
         */
        if (arch_timers_present & ARCH_TIMER_TYPE_CP15)
                dn = of_find_matching_node(NULL, arch_timer_mem_of_match);
        else
                dn = of_find_matching_node(NULL, arch_timer_of_match);

        if (dn && of_device_is_available(dn))
                needs_probing = true;

        of_node_put(dn);

        return needs_probing;
}

cp15 및 mmio 두 타입의 아키텍처 내장형 타이머가 둘 다 이미 probe 되었는지 여부를 반환한다.

  • 코드 라인 5~9에서 cp15 및 mmio 타입 둘 다 probing 완료되었으면 더 이상 probing이 필요 없으므로 false를 반환한다.
  • 코드 라인 15~16에서 cp15 타이머가 이미 probing된 경우 mmio 타입 타이머가 매치되는지 확인한다.
  • 코드 라인 17~18에서 mmio 타이머가 이미 probing된 경우 cp15 타입 타이머가 매치되는지 확인한다.
  • 코드 라인 20~25에서 디바이스가 사용 가능한 상태라면 probing을 하기 위해 true를 반환한다. 그렇지 않은 경우 false를 반환한다.

 

arch_timer_common_init()

drivers/clocksource/arm_arch_timer.c

static int __init arch_timer_common_init(void)
{
        arch_timer_banner(arch_timers_present);
        arch_counter_register(arch_timers_present);
        return arch_timer_arch_init();
}

generic 타이머를 클럭 소스로 등록하고 스케줄러 클럭과 딜레이 타이머로 등록한다.

  • 코드 3에서 generic 타이머 정보를 출력한다. cp15 타이머인지 mmio 타이머인지 여부를 가려낼 수 있다.
    • arm64-rpi4: “arch_timer: cp15 timer(s) running at 54.00MHz (phys).”를 출력한다.
    • arm32 rpi2: “Architected cp15 timer(s) running at 19.20MHz (virt).”를 출력한다.
    • arm32 rpi3: “Architected cp15 timer(s) running at 19.20MHz (phys).”를 출력한다.
  • 코드 4에서 generic 타이머를 클럭 소스, timercounter 및 스케쥴 클럭으로 등록한다.
  • 코드 5에서 아키텍처별 타이머 초기화 루틴을 수행한다.
    • arm32에서 generic 타이머를 딜레이 루프 타이머로 등록한다.
    • arm64에서 별도의 초기화 코드가 없다.

 


타이머 제어

타이머 프로그램

4가지 타입 타이머를 프로그램하는 함수이다.

  • arch_timer_set_next_event_virt()
    • 이 함수만 소스를 아래에 보여준다. 나머지는 유사 동등.
    • ARCH_TIMER_VIRT_ACCESS 레지스터를 사용한다.
  • arch_timer_set_next_event_phys()
    • ARCH_TIMER_PHYS_ACCESS 레지스터를 사용한다.
  • arch_timer_set_next_event_virt_mem()
    • ARCH_TIMER_MEM_VIRT_ACCESS 레지스터를 사용한다.
  • arch_timer_set_next_event_phys_mem()
    • ARCH_TIMER_MEM_PHYS_ACCESS 레지스터를 사용한다.

 

arch_timer_set_next_event_virt()

drivers/clocksource/arm_arch_timer.c

static int arch_timer_set_next_event_virt(unsigned long evt,
                                          struct clock_event_device *clk)
{
        set_next_event(ARCH_TIMER_VIRT_ACCESS, evt, clk);
        return 0;
}

virt 타이머를 프로그램한다.

 

set_next_event()

drivers/clocksource/arm_arch_timer.c

static __always_inline void set_next_event(const int access, unsigned long evt,
                                           struct clock_event_device *clk)
{
        unsigned long ctrl;
        ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, clk);
        ctrl |= ARCH_TIMER_CTRL_ENABLE;
        ctrl &= ~ARCH_TIMER_CTRL_IT_MASK;
        arch_timer_reg_write(access, ARCH_TIMER_REG_TVAL, evt, clk);
        arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, clk);
}

@access 타입 타이머 액세스 컨트롤 레지스터를 읽어 타이머를 프로그램한다. 이 때 타이머 레지스터도 @evt 값을 기록한다.

 

타이머 shutdown

4가지 타입 타이머를 shutdown 하는 함수이다.

  • arch_timer_shutdown_virt()
    • 이 함수만 소스를 아래에 보여준다. 나머지는 유사 동등.
    • ARCH_TIMER_VIRT_ACCESS  레지스터를 사용한다.
  • arch_timer_shutdown_phys()
    • ARCH_TIMER_PHYS_ACCESS 레지스터를 사용한다.
  • arch_timer_shutdown_virt_mem()
    • ARCH_TIMER_MEM_VIRT_ACCESS 레지스터를 사용한다.
  • arch_timer_shutdown_phys_mem()
    • ARCH_TIMER_MEM_PHYS_ACCESS 레지스터를 사용한다.

 

arch_timer_shutdown_virt()

drivers/clocksource/arm_arch_timer.c

static int arch_timer_shutdown_virt(struct clock_event_device *clk)
{
        return timer_shutdown(ARCH_TIMER_VIRT_ACCESS, clk);
}

virt 타이머 기능을 정지시킨다.

 

timer_shutdown()

drivers/clocksource/arm_arch_timer.c

static __always_inline int timer_shutdown(const int access,
                                          struct clock_event_device *clk)
{
        unsigned long ctrl;

        ctrl = arch_timer_reg_read(access, ARCH_TIMER_REG_CTRL, clk);
        ctrl &= ~ARCH_TIMER_CTRL_ENABLE;
        arch_timer_reg_write(access, ARCH_TIMER_REG_CTRL, ctrl, clk);

        return 0;
}

@access 타입 타이머 컨트롤 레지스터를 읽어 bit0만 클리어 한 후 다시 기록하여 타이머 기능을 정지시킨다.

 


클럭 소스, Timecounter 및 스케줄 클럭으로 등록

arch_counter_register()

drivers/clocksource/arm_arch_timer.c

static void __init arch_counter_register(unsigned type)
{
        u64 start_count;

        /* Register the CP15 based counter if we have one */
        if (type & ARCH_TIMER_TYPE_CP15) {
                u64 (*rd)(void);

                if ((IS_ENABLED(CONFIG_ARM64) && !is_hyp_mode_available()) ||
                    arch_timer_uses_ppi == ARCH_TIMER_VIRT_PPI) {
                        if (arch_timer_counter_has_wa())
                                rd = arch_counter_get_cntvct_stable;
                        else
                                rd = arch_counter_get_cntvct;
                } else {
                        if (arch_timer_counter_has_wa())
                                rd = arch_counter_get_cntpct_stable;
                        else
                                rd = arch_counter_get_cntpct;
                }

                arch_timer_read_counter = rd;
                clocksource_counter.archdata.vdso_direct = vdso_default;
        } else {
                arch_timer_read_counter = arch_counter_get_cntvct_mem;
        }

        if (!arch_counter_suspend_stop)
                clocksource_counter.flags |= CLOCK_SOURCE_SUSPEND_NONSTOP;
        start_count = arch_timer_read_counter();
        clocksource_register_hz(&clocksource_counter, arch_timer_rate);
        cyclecounter.mult = clocksource_counter.mult;
        cyclecounter.shift = clocksource_counter.shift;
        timecounter_init(&arch_timer_kvm_info.timecounter,
                         &cyclecounter, start_count);

        /* 56 bits minimum, so we assume worst case rollover */
        sched_clock_register(arch_timer_read_counter, 56, arch_timer_rate);
}

generic 타이머를 클럭 소스로 등록하고 스케줄러 클럭으로도 등록한다. 단 cp15 및 mmio generic 두 타이머가 사용가능하면 cp15를 우선 사용한다.

  • 코드 라인 6~23에서 가능하면 cp15 기반의 타이머를 우선 사용하도록 전역 arch_timer_read_counter 함수에 virtual 또는 physical 타이머용 함수를 대입한다.
    • 커널에 CONFIG_ARM_ARCH_TIMER_OOL_WORKAROUND 적용된 경우 stable용 함수를 사용한다.
  • 코드 라인 24~26에서 메모리 mapped(mmio) 방식의 타이머를 사용하는 경우 전역 arch_timer_read_counter 함수에 메모리 mapped 방식의 virtual 타이머를 읽어내는 함수를 대입한다.
  • 코드 라인 28~29에서 “arm,no-tick-in-suspend” 속성이 없는 일반적인 시스템인 경우 CLOCK_SOURCE_SUSPEND_NONSTOP 플래그를 추가한다.
  • 코드 라인 30에서 타이머 값을 읽어 시작 카운터 값으로 사용한다.
  • 코드 라인 31에서 “arch_sys_counter”라는 이름의 56비트 클럭소스카운터를 클럭 소스로 등록한다.
  • 코드 라인 32~35에서 56비트 타임카운터/사이클 카운터를 초기화한다.
    • rpi2: 아키텍처 Generic 타이머를 사용하여 타임카운터를 초기화한다.
      • mult=0x3415_5555, shift=24로 설정하여 19.2Mhz 클럭 카운터로 1 cycle 당 52ns가 소요되는 것을 알 수 있다.
      • cyclecounter_cyc2ns() 함수를 호출하여 변동된 cycle 카운터 값으로 소요 시간 ns를 산출한다.
  • 코드 라인 38에서 56비트 카운터를 사용하여 스케줄러 클럭으로 등록한다.

 

다음 그림은 arch_counter_register() 함수의 처리 과정을  보여준다.

 

사이클 카운터 값 읽기

arch_counter_read_cc()

drivers/clocksource/arm_arch_timer.c

static cycle_t arch_counter_read_cc(const struct cyclecounter *cc)
{
        return arch_timer_read_counter();
}

커널에서 사용하는 사이클 카운터 레지스터 값을 읽어온다.

 

/*                    
 * Default to cp15 based access because arm64 uses this function for
 * sched_clock() before DT is probed and the cp15 method is guaranteed
 * to exist on arm64. arm doesn't use this before DT is probed so even
 * if we don't have the cp15 accessors we won't have a problem.
 */     
u64 (*arch_timer_read_counter)(void) = arch_counter_get_cntvct;

사이클 카운터 레지스터 값을 읽어오는 함수가 대입되며, 초기 값은 virtual 카운터를 읽어오는 함수가 지정된다.

 

arch_counter_get_cntvct()

arch/arm/include/asm/arch_timer.h

static inline u64 arch_counter_get_cntvct(void)
{
        u64 cval;

        isb();
        asm volatile("mrrc p15, 1, %Q0, %R0, c14" : "=r" (cval));
        return cval;
}

CNTVCT(64bit Virtual Counter Register)을 읽어 반환한다.

 


Delay 타이머로 등록(for arm32)

arch_timer_arch_init()

arch/arm/kernel/arch_timer.c

int __init arch_timer_arch_init(void)
{                       
        u32 arch_timer_rate = arch_timer_get_rate();

        if (arch_timer_rate == 0)
                return -ENXIO;
                
        arch_timer_delay_timer_register();

        return 0;
}

아키텍처 generic 타이머를 딜레이 루프 타이머로 등록한다.

 

arch_timer_delay_timer_register()

arch/arm/kernel/arch_timer.c

static void __init arch_timer_delay_timer_register(void)
{                       
        /* Use the architected timer for the delay loop. */
        arch_delay_timer.read_current_timer = arch_timer_read_counter_long;
        arch_delay_timer.freq = arch_timer_get_rate();
        register_current_timer_delay(&arch_delay_timer);
}

아키텍처 generic 타이머를 딜레이 루프 타이머로 등록하고 초기화한다.

 


타이머(clock events) 등록

arch_timer_register()

drivers/clocksource/arm_arch_timer.c

static int __init arch_timer_register(void)
{
        int err;
        int ppi;

        arch_timer_evt = alloc_percpu(struct clock_event_device);
        if (!arch_timer_evt) {
                err = -ENOMEM;
                goto out;
        }

        ppi = arch_timer_ppi[arch_timer_uses_ppi];
        switch (arch_timer_uses_ppi) {
        case ARCH_TIMER_VIRT_PPI:
                err = request_percpu_irq(ppi, arch_timer_handler_virt,
                                         "arch_timer", arch_timer_evt);
                break;
        case ARCH_TIMER_PHYS_SECURE_PPI:
        case ARCH_TIMER_PHYS_NONSECURE_PPI:
                err = request_percpu_irq(ppi, arch_timer_handler_phys,
                                         "arch_timer", arch_timer_evt);
                if (!err && arch_timer_has_nonsecure_ppi()) {
                        ppi = arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI];
                        err = request_percpu_irq(ppi, arch_timer_handler_phys,
                                                 "arch_timer", arch_timer_evt);
                        if (err)
                                free_percpu_irq(arch_timer_ppi[ARCH_TIMER_PHYS_SECURE_PPI],
                                                arch_timer_evt);
                }
                break;
        case ARCH_TIMER_HYP_PPI:
                err = request_percpu_irq(ppi, arch_timer_handler_phys,
                                         "arch_timer", arch_timer_evt);
                break;
        default:
                BUG();
        }

        if (err) {
                pr_err("can't register interrupt %d (%d)\n", ppi, err);
                goto out_free;
        }

        err = arch_timer_cpu_pm_init();
        if (err)
                goto out_unreg_notify;

        /* Register and immediately configure the timer on the boot CPU */
        err = cpuhp_setup_state(CPUHP_AP_ARM_ARCH_TIMER_STARTING,
                                "clockevents/arm/arch_timer:starting",
                                arch_timer_starting_cpu, arch_timer_dying_cpu);
        if (err)
                goto out_unreg_cpupm;
        return 0;

out_unreg_cpupm:
        arch_timer_cpu_pm_deinit();

out_unreg_notify:
        free_percpu_irq(arch_timer_ppi[arch_timer_uses_ppi], arch_timer_evt);
        if (arch_timer_has_nonsecure_ppi())
                free_percpu_irq(arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI],
                                arch_timer_evt);

out_free:
        free_percpu(arch_timer_evt);
out:
        return err;
}

ARMv7 & ARMv8 아키텍처에 내장된 Generic 타이머를 클럭 이벤트 디바이스로 per-cpu 인터럽트에 등록하고 boot cpu용은 즉시 enable한다.

  • 코드 라인 6~10에서 clock_event_device 구조체를 할당받아 온다. 메모리 할당이 실패하는 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 12에서 아키텍처 내장 generic 타이머에 사용될 인터럽트 ppi를 알아온다.
  • 코드 라인 13~42에서 ppi에 해당하는 인터럽트를 요청하고, 핸들러를 설정한다.
  • 코드 라인 44~46에서 CONFIG_CPU_PM 커널 옵션을 사용하는 경우 cpu 절전 상태 변화에 따라 호출되도록 cpu pm notify chain에 arch_timer_cpu_pm_notify() 함수를 등록한다.
  • 코드 라인 49~54에서 cpu 상태 변화에 따라 호출되도록 cpu notify chain에 arch_timer_cpu_notify() 함수를 등록하고, boot cpu에 대한 cp15용 generic 타이머 설정 및 per-cpu 인터럽트를 enable 한 후 정상 값 0을 반환한다.
    • secondary cpu가 on될 때마다 타이머가 초기화되고 off될 때마다 migration된다.

 

다음 그림은 클럭 이벤트 디바이스를 통해 per-cpu 인터럽트 핸들러가 등록되는 것을 보여준다.

 

arch_timer_starting_cpu()

drivers/clocksource/arm_arch_timer.c

static int arch_timer_starting_cpu(unsigned int cpu)
{
        struct clock_event_device *clk = this_cpu_ptr(arch_timer_evt);
        u32 flags;

        __arch_timer_setup(ARCH_TIMER_TYPE_CP15, clk);

        flags = check_ppi_trigger(arch_timer_ppi[arch_timer_uses_ppi]);
        enable_percpu_irq(arch_timer_ppi[arch_timer_uses_ppi], flags);

        if (arch_timer_has_nonsecure_ppi()) {
                flags = check_ppi_trigger(arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI]);
                enable_percpu_irq(arch_timer_ppi[ARCH_TIMER_PHYS_NONSECURE_PPI],
                                  flags);
        }

        arch_counter_set_user_access();
        if (evtstrm_enable)
                arch_timer_configure_evtstream();

        return 0;
}

요청한 cpu의 per-core 아키텍처 내장형 generic 타이머를 가동한다.

  • 코드 라인 6에서 per-cpu 클럭 이벤트 디바이스에 대한 cp15용 generic 타이머 설정을 한다. (초기 shutdown)
  • 코드 라인 8~9에서 사용하는 타이머의 per-cpu 인터럽트를 enable한다.
  • 코드 라인 11~15에서 nonsecure ppi인 경우 nonsecure용 물리 타이머의 per-cpu 인터럽트를 enable한다.
  • 코드 라인 17에서 타이머의 user access를 허용한다
    • CNTKCTL(Timer PL1 Control Register).PL0VCTEN 비트를 설정하여 Virtual 타이머의 PL0 access를 허용하게 한다.
  • 코드 라인 18~19에서 시스템에 따라 이벤트 스트림(10khz)도 구성한다.

 

__arch_timer_setup()

drivers/clocksource/arm_arch_timer.c

static void __arch_timer_setup(unsigned type,
                               struct clock_event_device *clk)
{
        clk->features = CLOCK_EVT_FEAT_ONESHOT;

        if (type == ARCH_TIMER_TYPE_CP15) {
                typeof(clk->set_next_event) sne;

                arch_timer_check_ool_workaround(ate_match_local_cap_id, NULL);

                if (arch_timer_c3stop)
                        clk->features |= CLOCK_EVT_FEAT_C3STOP;
                clk->name = "arch_sys_timer";
                clk->rating = 450;
                clk->cpumask = cpumask_of(smp_processor_id());
                clk->irq = arch_timer_ppi[arch_timer_uses_ppi];
                switch (arch_timer_uses_ppi) {
                case ARCH_TIMER_VIRT_PPI:
                        clk->set_state_shutdown = arch_timer_shutdown_virt;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_virt;
                        sne = erratum_handler(set_next_event_virt);
                        break;
                case ARCH_TIMER_PHYS_SECURE_PPI:
                case ARCH_TIMER_PHYS_NONSECURE_PPI:
                case ARCH_TIMER_HYP_PPI:
                        clk->set_state_shutdown = arch_timer_shutdown_phys;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_phys;
                        sne = erratum_handler(set_next_event_phys);
                        break;
                default:
                        BUG();
                }

                clk->set_next_event = sne;
        } else {
                clk->features |= CLOCK_EVT_FEAT_DYNIRQ;
                clk->name = "arch_mem_timer";
                clk->rating = 400;
                clk->cpumask = cpu_possible_mask;
                if (arch_timer_mem_use_virtual) {
                        clk->set_state_shutdown = arch_timer_shutdown_virt_mem;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_virt_mem;
                        clk->set_next_event =
                                arch_timer_set_next_event_virt_mem;
                } else {
                        clk->set_state_shutdown = arch_timer_shutdown_phys_mem;
                        clk->set_state_oneshot_stopped = arch_timer_shutdown_phys_mem;
                        clk->set_next_event =
                                arch_timer_set_next_event_phys_mem;
                }
        }

        clk->set_state_shutdown(clk);

        clockevents_config_and_register(clk, arch_timer_rate, 0xf, 0x7fffffff);
}

아키텍처에 내장된 generic 타이머를 클럭 이벤트 디바이스로 설정하여 per-cpu 인터럽트와 핸들러를 설정하고 타이머는 shutdown 한다.

  • 코드 라인 4에서 generic 타이머에 oneshot 기능을 부여한다.
  • 코드 라인 6~12에서 보조프로세서 cp15로 제어되는 타이머인 경우이면서 c3stop 기능이 있는 경우 features에 C3STOP 플래그를 추가한다.
    • rpi2: “Always-on” 속성을 사용하여 c3stop을 사용하지 않는다.
  • 코드 라인 13~34에서 “arch_sys_timer”라는 이름으로 현재 cpu에 대해서만 cpumask를 설정하고 rating을 450으로 설정한다. 그리고 ppi 타입에 해당하는 타이머 인터럽트 및 핸들러를 설정한다.
  • 코드 라인 35~51에서 메모리 mapped 타이머를 지원하는 경우 feauters에 DYNIRQ 플래그를 추가하고 “arch_mem_timer”라는 이름으로 전체 cpu에 대해서 cpumask를 설정하고 rating을 400으로 설정한다.그리고 virtual 또는 physical 두 타입 중 하나의 타이머 인터럽트 및 핸들러를 설정한다.
  • 코드 라인 53에서 현재 cpu의 타이머 출력을 정지시킨다.
  • 코드 라인 55에서 클럭 이벤트 디바이스 설정 및 등록을 진행한다.

 

다음 그림은 __arch_timer_setup() 함수의 처리 과정을 보여준다.

 


이벤트 스트림 (for ARM)

ARM 아키텍처에서 delay() API의 내부 polling을 최소화하고, 저전력으로 구동하기 위해 wfe 명령을 사용한 구현에서 일정 스트림 간격으로 cpu를 깨우기 위해 사용된다.

 

이벤트 스트림 설정 및 가동

arch_timer_configure_evtstream()

drivers/clocksource/arm_arch_timer.c

static void arch_timer_configure_evtstream(void)
{
        int evt_stream_div, pos;

        /* Find the closest power of two to the divisor */
        evt_stream_div = arch_timer_rate / ARCH_TIMER_EVT_STREAM_FREQ;
        pos = fls(evt_stream_div);
        if (pos > 1 && !(evt_stream_div & (1 << (pos - 2))))
                pos--;
        /* enable event stream */
        arch_timer_evtstrm_enable(min(pos, 15));
}

아키텍처 generic 타이머 중 virtual 타이머에 이벤트 스트림을 위해 약 10Khz에 해당하는 트리거를 설정하고 동작시킨다.

  • 코드 라인 6에서 divider 값으로 사용할 값을 구한다.
    • 19.2Mhz / 10Khz = 1920
  • 코드 라인 7~9에서 divider 값을 2의 차수 단위로 정렬한 값에 해당하는 비트 수 pos를 구한다.
    • pos = 11 (2 ^ 11 = 2048 분주)
  • 코드 라인 11에서 virtual 타이머에 이벤트 스트림의 분주율에 사용할 pos 값으로 설정하고 이벤트 스트림 기능을 enable한다.

 

다음 그림은 54Mhz의 클럭 소스를 4096 분주하여 목표치 10Khz에 가장 근접한 13.183Khz 주기의 이벤트 스트림용 트리거를 enable하는 것을 보여준다.

 

다음 그림은 19.2Mhz의 클럭 소스를 2048 분주하여 목표치 10Khz에 가장 근접한 9.375Khz 주기의 이벤트 스트림용 트리거를 enable하는 것을 보여준다.

 

arch_timer_evtstrm_enable()

drivers/clocksource/arm_arch_timer.c

static void arch_timer_evtstrm_enable(int divider)
{
        u32 cntkctl = arch_timer_get_cntkctl();

        cntkctl &= ~ARCH_TIMER_EVT_TRIGGER_MASK;
        /* Set the divider and enable virtual event stream */
        cntkctl |= (divider << ARCH_TIMER_EVT_TRIGGER_SHIFT)
                        | ARCH_TIMER_VIRT_EVT_EN;
        arch_timer_set_cntkctl(cntkctl);
        arch_timer_set_evtstrm_feature();
        cpumask_set_cpu(smp_processor_id(), &evtstrm_available);
}

Generic 타이머의 이벤트 스트림 기능을 enable하기 위해 CNTKCTL 레지스터에 divider(0~15) 값과 enable 비트를 설정한다.

  • 코드 라인 3에서 CNTKCTL 레지스터 값을 읽어온다.
  • 코드 라인 5에서 CNTKCTL.EVNTI(bit7:4)에 해당하는 비트를 클리어한다.
  • 코드 라인 7~9에서 divider 값과 enable 비트를 추가하고 CNTKCTL 레지스터에 저장한다.
  • 코드 라인 10에서 전역 elf_hwcap에 이벤트 스트림에 대한 플래그들을 추가한다.
  • 코드 라인 11에서 evestrm_available 비트마스크에 현재 cpu를 설정한다.

 

arch_timer_get_cntkctl() – ARM64

arch/arm64/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntkctl(void)
{
        return read_sysreg(cntkctl_el1);
}

CNTKCTL(Timer Control for EL0) 레지스터 값을 읽어온다.

 

arch_timer_get_cntkctl() – ARM32

arch/arm/include/asm/arch_timer.h

static inline u32 arch_timer_get_cntkctl(void)
{
        u32 cntkctl;
        asm volatile("mrc p15, 0, %0, c14, c1, 0" : "=r" (cntkctl));
        return cntkctl;
}

CNTKCTL(Timer PL1 Control) 레지스터 값을 읽어온다.

 

참고

 

방문자 통계 (2019년 11/21~12/5)

안녕하세요? 문c 블로그입니다.

 

방문자 수가 작년보다 70~80% 이상 증가하였습니다.

현재까지 리눅스 커널에 대한 분석글을 올린 글 수는 341개이고, 그림은 1800개 정도입니다.

 

해마다 더 많은 분들이 리눅스 커널 분석을 도전하시길 바랍니다.

 

방문자 통계 참고) 2019년 11월 21~12월 5일

감사합니다.

커널 v5.4 release (LTS version)

안녕하세요?

그 동안 커널 v4.x  분석(주로 커널 메모리와  매핑 시스템)내용을 커널 v5.0으로 migration하고 있었습니다.

얼마전에 커널 v5.4가 release되었고, 이 버전이 LTS 버전이므로,

이후 분석(클럭, 타이머, 스케줄러 등)은 LTS 버전인 커널 v5.4로 진행합니다.

감사합니다.

Compound 페이지

<kernel v5.0>

Compound 페이지

  • 버디 시스템에서 order 단위로 free 페이지를 관리하고, 할당 받아 사용하는 페이지들은 order를 제거한 상태에서 사용한다. 그런데 order 단위로 묶어 관리하는 페이지가 있는데 이들을 compound 페이지라고 한다.
  • 용도
    • 슬랩 캐시
    • Huge 페이지
      • 일반적인 high order 페이지와 다른 점은 TLB 성능을 극대화하기 위해 설계되었다.
      • 성능을 향상시키기 위해 리눅스는 대량의 페이지(high order 페이지)를 할당 받는 경우 PMD 레벨에서 huge 페이지를 사용하여 더 빠른 access를 사용할 수 있게 매핑을 한다.
      • HugeTLBFS 및 THP(Transparent Huge Page)에서 사용한다.
  • 2015년 12월 kernel v.4.6-rc1 에서 CONFIG_PAGEFLAGS_EXTENDED 옵션과 PG_compound, PG_tail 이 없어지고 PG_head 만 남겨지게 되었다.

 

prep_compound_page()

/mm/page_alloc.c

void prep_compound_page(struct page *page, unsigned long order)
{
        int i;
        int nr_pages = 1 << order;

        set_compound_page_dtor(page, COMPOUND_PAGE_DTOR);
        set_compound_order(page, order);
        __SetPageHead(page);
        for (i = 1; i < nr_pages; i++) {
                struct page *p = page + i;
                set_page_count(p, 0);
                p->mapping = TAIL_MAPPING;
                set_compound_head(p, page);
        }
}

compound 페이지를 준비한다.

  • 코드 라인 6에서 compound 파괴자 id에 COMPOUND_PAGE_DTOR를 대입한다.
    • 파괴자 id는 다음과 같다.
      • NULL_COMPOUND_DTOR
      • COMPOUND_PAGE_DTOR
      • HUGETLB_PAGE_DTOR
      • TRANSHUGE_PAGE_DTOR
  • 코드 라인 7에서 두 번째 페이지에 order를 설정한다.
  • 코드 라인 8에서 헤더 페이지의 플래그에 PG_Head 비트를 설정한다.
  • 코드 라인 9~14에서 나머지 페이지의 참조 카운터에 0을 대입하고, 나머지 페이지들이 헤드 페이지를 가리키게 한 후 플래그에 PG_Tail 비트를 설정한다.

 

다음 그림은 prep_compound_page() 함수에 의해 compound 페이지가 준비되는 모습을 보여준다.

  • 두 번째 페이지에 compound 정보가 존재한다.

 

page_count()

include/linux/mm.h

static inline int page_count(struct page *page)
{
        return atomic_read(&compound_head(page)->_count);
}

요청 페이지의 _count 값을 알아온다. 만일 compound page인 경우 선두 페이지에서 _count 값을 알아온다.

 

compound_order()

include/linux/mm.h

static inline int compound_order(struct page *page)
{
        if (!PageHead(page))
                return 0;
        return page[1].compound_order;
}

compound 페이지이면 compound_order를 알아오고 compound 페이지가 아닌 경우 0 order를 반환한다

  • 첫 번째 페이지의 Head 플래그가 설정된 경우 compound 페이지를 의미한다.
  • 두 번째 페이지 구조체에서 compound_order를 알아온다.

 

다음 그림은 compound 페이지로부터 order 값을 알아오는 과정을 보여준다.

compound_order-1a

 

 

 

Swap -3- (Swap 영역 할당/해제)

<kernel v5.0>

swap 엔트리 할당/해제

swapon으로 지정된 swap 파일이나 swap 블록 디바이스가 swap 영역으로 지정되면 swap_map[] 이라는 1바이트 배열을 사용하여 swap 엔트리들의 할당을 관리한다.

  • swap_map[offset]에 사용되는 offset 인덱스는 swap 영역의 offset 페이지를 의미한다.
    • swap_map[0]은 swap 영역의 0번 페이지를 의미한다.
  • swap_map[]에 사용되는 값
    • 0
      • free 상태에서 사용되는 값
    • 1~SWAP_MAP_MAX(0x3e)
      • in-use 상태에서 사용되는 값으로 swap 엔트리의 참조 카운터가 저장된다.
      • 초과시 SWAP_MAP_CONTINUED(0x80) 플래그가 추가되고 이의 관리를 위해 별도의 swap_map[] 페이지가 생성된다.
    • SWAP_MAP_BAD (0x3f)
      • bad 페이지로 사용되는 값
    • SWAP_HAS_CACHE (0x40)
      • cache 페이지 (추가 플래그)
    • SWAP_MAP_SHMEM (0xbf)
  • swap 엔트리들은 중간에 per-cpu swap 슬롯 캐시에 충전되어 사용된다.

 

swap 엔트리들 할당

get_swap_pages()

mm/swapfile.c

int get_swap_pages(int n_goal, swp_entry_t swp_entries[], int entry_size)
{
        unsigned long size = swap_entry_size(entry_size);
        struct swap_info_struct *si, *next;
        long avail_pgs;
        int n_ret = 0;
        int node;

        /* Only single cluster request supported */
        WARN_ON_ONCE(n_goal > 1 && size == SWAPFILE_CLUSTER);

        avail_pgs = atomic_long_read(&nr_swap_pages) / size;
        if (avail_pgs <= 0)
                goto noswap;

        if (n_goal > SWAP_BATCH)
                n_goal = SWAP_BATCH;

        if (n_goal > avail_pgs)
                n_goal = avail_pgs;

        atomic_long_sub(n_goal * size, &nr_swap_pages);

        spin_lock(&swap_avail_lock);

start_over:
        node = numa_node_id();
        plist_for_each_entry_safe(si, next, &swap_avail_heads[node], avail_lists[node]) {
                /* requeue si to after same-priority siblings */
                plist_requeue(&si->avail_lists[node], &swap_avail_heads[node]);
                spin_unlock(&swap_avail_lock);
                spin_lock(&si->lock);
                if (!si->highest_bit || !(si->flags & SWP_WRITEOK)) {
                        spin_lock(&swap_avail_lock);
                        if (plist_node_empty(&si->avail_lists[node])) {
                                spin_unlock(&si->lock);
                                goto nextsi;
                        }
                        WARN(!si->highest_bit,
                             "swap_info %d in list but !highest_bit\n",
                             si->type);
                        WARN(!(si->flags & SWP_WRITEOK),
                             "swap_info %d in list but !SWP_WRITEOK\n",
                             si->type);
                        __del_from_avail_list(si);
                        spin_unlock(&si->lock);
                        goto nextsi;
                }
                if (size == SWAPFILE_CLUSTER) {
                        if (!(si->flags & SWP_FS))
                                n_ret = swap_alloc_cluster(si, swp_entries);
                } else
                        n_ret = scan_swap_map_slots(si, SWAP_HAS_CACHE,
                                                    n_goal, swp_entries);
                spin_unlock(&si->lock);
                if (n_ret || size == SWAPFILE_CLUSTER)
                        goto check_out;
                pr_debug("scan_swap_map of si %d failed to find offset\n",
                        si->type);

                spin_lock(&swap_avail_lock);
nextsi:
                /*
                 * if we got here, it's likely that si was almost full before,
                 * and since scan_swap_map() can drop the si->lock, multiple
                 * callers probably all tried to get a page from the same si
                 * and it filled up before we could get one; or, the si filled
                 * up between us dropping swap_avail_lock and taking si->lock.
                 * Since we dropped the swap_avail_lock, the swap_avail_head
                 * list may have been modified; so if next is still in the
                 * swap_avail_head list then try it, otherwise start over
                 * if we have not gotten any slots.
                 */
                if (plist_node_empty(&next->avail_lists[node]))
                        goto start_over;
        }

        spin_unlock(&swap_avail_lock);

check_out:
        if (n_ret < n_goal)
                atomic_long_add((long)(n_goal - n_ret) * size,
                                &nr_swap_pages);
noswap:
        return n_ret;
}

swap 엔트리들을 준비하고 그 수를 반환한다. (THP swap 엔트리도 1건으로 반환한다)

  • 코드 라인 3에서 THP swap을 지원하는 커널인 경우 @entry_size를 size에 대입한다. 지원하지 않는 경우 size는 항상 1이다.
    • THP swap을 지원하는 경우 엔트리 크기로 HPAGE_PMD_NR을 사용한다.
      • 예) 4K 페이지를 사용하는 경우 pmd 사이즈가 2M이고 HPAGE_PMD_NR=512이다.
  • 코드 라인 10에서 클러스터 방식에서는 @n_goal에서 1개만 요청가능하다.
  • 코드 라인 12~14에서 남은 swap 페이지를 size로 나눈 수를 avail_pgs에 대입하고 그 수가 0 이하이면 noswap 레이블로 이동한다.
  • 코드 라인 16~20에서 @n_goal이 SWAP_BATCH(64) 또는 avail_pgs를 초과하지 않도록 제한한다.
  • 코드 라인 22에서 남은 swap 페이지에서 @n_goal * size 만큼 뺀다.
  • 코드 라인 26~28에서 start_over: 레이블이다. 전역 swap_avail_heads[node] priority 리스트에 등록된 각 swap 영역을 순회한다.
    • swap 영역: swap_info_struct 노드
  • 코드 라인 30에서 si->avail_lists[node] 노드들을 전역 swap_avail_heads[node] priority 리스트의 끝에 다시 추가한다.
  • 코드 라인 33~48에서 si->highest_bit가 설정되지 않았거나 write 불가능한 swap 영역인 경우 경고 메시지를 출력하고 si->avail_lists[node] 노드들을 swap_avail_heads[node[ priority 리스트에서 제거하고 nextsi: 레이블로 이동한다.
  • 코드 라인 49~51에서 THP swap 요청 시 파일 시스템을 사용하지 않은 swap 영역인 경우 THP 클러스터용 swap 엔트리들을 준비하고 swp_entries에 대입한다.
  • 코드 라인 52~54에서 THP swap 요청이 아닌 경우 @n_goal 만큼 swap 엔트리들을 준비하고 swp_entries에 대입한다.
  • 코드 라인 56~57에서 swap 엔트리가 준비되었거나, THP 클러스터 요청인 경우 check_out: 레이블로 이동한다.
  • 코드 라인 58~59에서 swap 엔트리가 준비되지 못한 경우 “scan_swap_map of si %d failed to find offset\n” 디버그 메시지를 출력한다.
  • 코드 라인 62~76에서 nextsi: 레이블이다. 다음 swap 영역을 계속 진행한다.
  • 코드 라인 80~83에서 check_out: 레이블이다. 처리가 다 완료되었다. 목표(@n_goal) 이하의 swap 엔트리가 준비된 경우 남은 swap 페이지 수를 그 차이만큼 추가하여 갱신한다.
  • 코드 라인 84~85에서 noswap: 레이블이다. 준비된 swap 엔트리 수를 반환한다.

 

swap_map을 스캔하여 1 개의 swap 엔트리 할당

scan_swap_map()

mm/swapfile.c

static unsigned long scan_swap_map(struct swap_info_struct *si,
                                   unsigned char usage)
{
        swp_entry_t entry;
        int n_ret;

        n_ret = scan_swap_map_slots(si, usage, 1, &entry);

        if (n_ret)
                return swp_offset(entry);
        else
                return 0;

}

swap 영역에서 1개의 free swap 엔트리를 스캔한 후 offset 값을 반환한다. 실패 시 0을 반환한다.

 

swap_map을 스캔하여 swap 엔트리들 할당

scan_swap_map_slots()

mm/swapfile.c -1/3-

static int scan_swap_map_slots(struct swap_info_struct *si,
                               unsigned char usage, int nr,
                               swp_entry_t slots[])
{
        struct swap_cluster_info *ci;
        unsigned long offset;
        unsigned long scan_base;
        unsigned long last_in_cluster = 0;
        int latency_ration = LATENCY_LIMIT;
        int n_ret = 0;

        if (nr > SWAP_BATCH)
                nr = SWAP_BATCH;

        /*
         * We try to cluster swap pages by allocating them sequentially
         * in swap.  Once we've allocated SWAPFILE_CLUSTER pages this
         * way, however, we resort to first-free allocation, starting
         * a new cluster.  This prevents us from scattering swap pages
         * all over the entire swap partition, so that we reduce
         * overall disk seek times between swap pages.  -- sct
         * But we do now try to find an empty cluster.  -Andrea
         * And we let swap pages go all over an SSD partition.  Hugh
         */

        si->flags += SWP_SCANNING;
        scan_base = offset = si->cluster_next;

        /* SSD algorithm */
        if (si->cluster_info) {
                if (scan_swap_map_try_ssd_cluster(si, &offset, &scan_base))
                        goto checks;
                else
                        goto scan;
        }

        if (unlikely(!si->cluster_nr--)) {
                if (si->pages - si->inuse_pages < SWAPFILE_CLUSTER) {
                        si->cluster_nr = SWAPFILE_CLUSTER - 1;
                        goto checks;
                }

                spin_unlock(&si->lock);

                /*
                 * If seek is expensive, start searching for new cluster from
                 * start of partition, to minimize the span of allocated swap.
                 * If seek is cheap, that is the SWP_SOLIDSTATE si->cluster_info
                 * case, just handled by scan_swap_map_try_ssd_cluster() above.
                 */
                scan_base = offset = si->lowest_bit;
                last_in_cluster = offset + SWAPFILE_CLUSTER - 1;

                /* Locate the first empty (unaligned) cluster */
                for (; last_in_cluster <= si->highest_bit; offset++) {
                        if (si->swap_map[offset])
                                last_in_cluster = offset + SWAPFILE_CLUSTER;
                        else if (offset == last_in_cluster) {
                                spin_lock(&si->lock);
                                offset -= SWAPFILE_CLUSTER - 1;
                                si->cluster_next = offset;
                                si->cluster_nr = SWAPFILE_CLUSTER - 1;
                                goto checks;
                        }
                        if (unlikely(--latency_ration < 0)) {
                                cond_resched();
                                latency_ration = LATENCY_LIMIT;
                        }
                }

                offset = scan_base;
                spin_lock(&si->lock);
                si->cluster_nr = SWAPFILE_CLUSTER - 1;
        }

swap 영역에서 @nr 수 만큼 free swap 엔트리를 스캔하여 @slots[] 배열에 저장해온다. 이 때 스캔해온 free swap 엔트리 수를 반환한다.

  • 코드 라인 12~13에서 한 번에 스캔할 최대 수를 SWAP_BATCH(64)개로 제한한다.
  • 코드 라인 26에서 swap 영역에 스캐닝이 완료될 때 까지 스캐닝 중이라고 SWP_SCANNING 플래그를 추가한다.
  • 코드 라인 27에서 스캔 시작점은 si->cluster_next 페이지부터이다.
  • 코드 라인 30~35에서 SSD 클러스터 방식을 사용하는 경우이다.

 

2) 빈 클러스터(256개 free swap 페이지) 스캔 (non-SSD)
  • 코드 라인 37에서 non-SSD 클러스터 방식을 사용하는 경우이다. 클러스터 번호를 1 감소시키는데 이미 0인 경우의 처리이다.
  • 코드 라인 38~41에서 swap 영역의 남은 free swap 페이지가 SWAPFILE_CLUSTER(256)보다 적은 경우 클러스터 번호를 255로 변경하고 checks 레이블로 이동한다.
  • 코드 라인 43~55에서 swap 영역에서 락을 잡은채로 첫 번째 empty 클러스터를 찾는다. 즉 lowest_bit ~ highest_bit까지 256개의 연속된 free 페이지가 발견되면 checks: 레이블로 이동한다.
    • non-ssd 방식이므로 256개의 free swap 페이지의 시작이 클러스터 단위로 정렬되어 있지 않아도 된다.
  • 코드 라인 56~57에서 offset에 해당하는 swap 페이지가 이미 swap되어 사용 중인 경우 다음 클러스터의 페이지를 진행하기 위해 진행중인 offset 페이지 += 256으로 증가시킨다.
  • 코드 라인 58~64에서 순회 중인 클러스터의 마지막 페이지에 도달한 경우 offset을 다시 해당 클러스터의 가장 첫 페이지로 되돌리고, cluster_next에 순회중인 offset을 지정하고, 클러스터 번호를 255로 변경하고 checks 레이블로 이동한다.
  • 코드 라인 65~68에서 순회중에 LATENCY_LIMIT(256) 페이지 마다 preemption point를 수행한다.
  • 코드 라인 71~73에서 첫 빈 클러스터를 찾지 못한 경우이다. offset을 시작점으로 다시 되돌리고, 클러스터 번호를 255로 변경한다.

 

mm/swapfile.c -2/3-

checks:
        if (si->cluster_info) {
                while (scan_swap_map_ssd_cluster_conflict(si, offset)) {
                /* take a break if we already got some slots */
                        if (n_ret)
                                goto done;
                        if (!scan_swap_map_try_ssd_cluster(si, &offset,
                                                        &scan_base))
                                goto scan;
                }
        }
        if (!(si->flags & SWP_WRITEOK))
                goto no_page;
        if (!si->highest_bit)
                goto no_page;
        if (offset > si->highest_bit)
                scan_base = offset = si->lowest_bit;

        ci = lock_cluster(si, offset);
        /* reuse swap entry of cache-only swap if not busy. */
        if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
                int swap_was_freed;
                unlock_cluster(ci);
                spin_unlock(&si->lock);
                swap_was_freed = __try_to_reclaim_swap(si, offset, TTRS_ANYWAY);
                spin_lock(&si->lock);
                /* entry was freed successfully, try to use this again */
                if (swap_was_freed)
                        goto checks;
                goto scan; /* check next one */
        }

        if (si->swap_map[offset]) {
                unlock_cluster(ci);
                if (!n_ret)
                        goto scan;
                else
                        goto done;
        }
        si->swap_map[offset] = usage;
        inc_cluster_info_page(si, si->cluster_info, offset);
        unlock_cluster(ci);

        swap_range_alloc(si, offset, 1);
        si->cluster_next = offset + 1;
        slots[n_ret++] = swp_entry(si->type, offset);

        /* got enough slots or reach max slots? */
        if ((n_ret == nr) || (offset >= si->highest_bit))
                goto done;

        /* search for next available slot */

        /* time to take a break? */
        if (unlikely(--latency_ration < 0)) {
                if (n_ret)
                        goto done;
                spin_unlock(&si->lock);
                cond_resched();
                spin_lock(&si->lock);
                latency_ration = LATENCY_LIMIT;
        }

        /* try to get more slots in cluster */
        if (si->cluster_info) {
                if (scan_swap_map_try_ssd_cluster(si, &offset, &scan_base))
                        goto checks;
                else
                        goto done;
        }
        /* non-ssd case */
        ++offset;

        /* non-ssd case, still more slots in cluster? */
        if (si->cluster_nr && !si->swap_map[offset]) {
                --si->cluster_nr;
                goto checks;
        }

done:
        si->flags -= SWP_SCANNING;
        return n_ret;
case 1) 현재 페이지(si->cluster_next) 할당 체크 (non-SSD)
  • 코드 라인 1에서 checks: 레이블이다.
  • 코드 라인 2~11에서 SSD 클러스터 방식을 사용하는 경우이다.
  • 코드 라인 12~13에서 swap 영역에 SWP_WRITEOK 플래그가 없으면 no_page 레이블로 이동한다.
  • 코드 라인 14~15에서 swap 영역에 가장 큰 페이지 번호인 highest_bit가 설정되지 않은 경우에도 no_page 레이블로 이동한다.
  • 코드 라인 16~17에서 offset 페이지가 가장 큰 페이지 번호를 초과한 경우에는 다시 scan_base와 offset을 가장 낮은 페이지 번호인 lowest_bit로 변경한다.
  • 코드 라인 19에서 offset에 해당하는 클러스터를 lock 하고 가져온다.
  • 코드 라인 21~31에서 전체 swap 페이지가 사용 가능한 free swap 영역의 절반을 초과한 경우이면서 offset에 SWAP_HAS_CACHE 값으로 설정된 경우 해당 swap 캐시를 제거한다. 제거가 성공한 경우 checks 레이블로 이동하고, 그렇지 않은 경우 다음을 위해 scan 레이블로 이동한다.
  • 코드 라인 33~39에서 offset 페이지에 대한 swap_map[]이 이미 사용 중인 경우이다. n_ret 값이 있으면 done 레이블로 이동하고, 없으면 다음을 위해 scan 레이블로 이동한다.
  • 코드 라인 40~42에서 offset 페이지에 대한 swap_map[]이 빈 경우이다. free swap 상태이므로 할당을 위해 참조 카운터 @usage 값을 대입하고, 클러스터를 사용 중으로 표기한다. 그런 후 클러스터 lock을 푼다.
  • 코드 라인 44에서 swap 영역(lowest_bit, highest_bit)을 1 페이지만큼 추가 갱신한다. 만일 swap 영역내의 모든 페이지가 할당 완료된 경우 swap_avail_heads에서 swap 영역을 제거한다.
  • 코드 라인 45~46에서 다음 클러스터를 위해 현재 offset 페이지 + 1을 한다. 출력 인자 @slots[]에 offset에 해당하는 swap 엔트리를 저장한다.
  • 코드 라인 49~50에서 요청한 수만큼 슬롯을 채웠거나 영역의 끝까지 진행을 한 경우 done 레이블로 이동한다.
  • 코드 라인 55~62에서 인터럽트 레이튼시를 줄이는 방법이다. swap 영역에 대해 오랫동안 락을 잡고 있지 않기 위해 LATENCY_LIMIT(256) 페이지 마다 잠시 lock을 풀었다가 다시 획득한다.
  • 코드 라인 65~70에서 SSD 클러스터 방식을 사용하는 경우이다.
  • 코드 라인 72~78에서 SSD 클러스터 방식이 아닌 경우의 동작이다. 다음 페이지를 위해 offset을 증가시키고 슬롯이 더 필요한 경우 클러스터 번호를 감소시키고 checks 레이블로 이동한다.
  • 코드 라인 80~82에서 done: 레이블이다. 스캐닝일 수행하는 동안 설정한 SWP_SCANNING 플래그를 swap 영역에서 제거하고 슬롯에 준비한 swap 엔트리 수를 반환한다.

 

mm/swapfile.c -3/3-

scan:
        spin_unlock(&si->lock);
        while (++offset <= si->highest_bit) {
                if (!si->swap_map[offset]) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (unlikely(--latency_ration < 0)) {
                        cond_resched();
                        latency_ration = LATENCY_LIMIT;
                }
        }
        offset = si->lowest_bit;
        while (offset < scan_base) {
                if (!si->swap_map[offset]) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (unlikely(--latency_ration < 0)) {
                        cond_resched();
                        latency_ration = LATENCY_LIMIT;
                }
                offset++;
        }
        spin_lock(&si->lock);

no_page:
        si->flags -= SWP_SCANNING;
        return n_ret;
}
3) 현재 페이지 ~ 영역 끝까지 스캔 (non-SSD)
  • 코드 라인 1~16에서 scan: 레이블이다. swap 영역의 free 할당 가능한 마지막 페이지까지 순회하며 offset을 증가하며 다음 두 경우에 한하여 checks 레이블로 이동한다. 또한 순회중에 LATENCY_LIMIT(256) 페이지 마다 preemption point를 수행한다.
    • swap_map[offset]이 free 상태로 할당 가능한 상태이다.
    • swap 영역이 50% 이상 가득 차고 offset에 해당하는 swap 영역은 free 상태고 swap 캐시만 존재하는 경우이다.

 

4) 시작 페이지 ~ 현재 페이지까지 스캔 (non-SSD)
  • 코드 라인 17~32에서 offset을 swap 영역의 free 할당 가능한 첫 페이지부터 scan_base까지 순회하며 다음 두 경우에 한하여 checks 레이블로 이동한다. 또한 순회중에 LATENCY_LIMIT(256) 페이지 마다 preemption point를 수행한다.
    • swap_map[offset]이 free 상태로 할당 가능한 상태이다.
    • swap 영역이 50% 이상 가득 차고 offset에 해당하는 swap 영역은 free 상태고 swap 캐시만 존재하는 경우이다.

 

  • 코드 라인 35~37에서 no_page: 레이블이다. 스캐닝일 수행하는 동안 설정한 SWP_SCANNING 플래그를 swap 영역에서 제거하고 슬롯에 준비한 swap 엔트리 수를 반환한다.

 

다음 4개의 그림은 1개의 free swap 엔트리를 찾는 순서이다.

 

첫 번째, si->cluster_next가 현재 위치의 페이지로 할당 가능한 상태인지 체크한다.

 

두 번째, 현재 위치의 페이지로 할당 불가능한 경우 다음으로 swap 영역 전체에서 256개의 빈 swap 페이지가 있는지 확인한다.

 

세 번째, 빈 클러스터가 없는 경우 현재 위치에서 swap 영역 끝까지 스캔한다.

 

네 번째, 마지막으로 swap 영역 처음부터 현재 위치까지 스캔한다.

 

swap 엔트리들을 swap 영역으로 반환

swapcache_free_entries()

mm/swapfile.c

void swapcache_free_entries(swp_entry_t *entries, int n)
{
        struct swap_info_struct *p, *prev;
        int i;

        if (n <= 0)
                return;

        prev = NULL;
        p = NULL;

        /*
         * Sort swap entries by swap device, so each lock is only taken once.
         * nr_swapfiles isn't absolutely correct, but the overhead of sort() is
         * so low that it isn't necessary to optimize further.
         */
        if (nr_swapfiles > 1)
                sort(entries, n, sizeof(entries[0]), swp_entry_cmp, NULL);
        for (i = 0; i < n; ++i) {
                p = swap_info_get_cont(entries[i], prev);
                if (p)
                        swap_entry_free(p, entries[i]);
                prev = p;
        }
        if (p)
                spin_unlock(&p->lock);
}

@n개의 swap 엔트리 @entries를 free 상태로 변경한다.

  • 코드 라인 6~7에서 @n이 0개 이하이면 함수를 빠져나간다.
  • 코드 라인 17~18에서 요청한 swap 엔트리들의 swap 영역이 섞여 있으면 이를 접근하기 위해 lock을 잡아야 하는데 이의 빈번함을 피하기 위해 먼저 swap 엔트리의 소팅을 수행 한다.
  • 코드 라인 19~24에서 @n개를 순회하며 swap 엔트리를 free 한다. 만일 swap 영역이 바뀌는 경우 unlock하고 다시 새로운 swap 영역을 lock 한다.

 

swap_info_get_cont()

mm/swapfile.c

static struct swap_info_struct *swap_info_get_cont(swp_entry_t entry,
                                        struct swap_info_struct *q)
{
        struct swap_info_struct *p;

        p = _swap_info_get(entry);

        if (p != q) {
                if (q != NULL)
                        spin_unlock(&q->lock);
                if (p != NULL)
                        spin_lock(&p->lock);
        }
        return p;
}

swap 영역이 바뀌면 기존 swap 영역 @q를 unlock하고 새로운 swap 영역 p의 lock을 획득한다.

 

swap 엔트리 할당 해제

swap_entry_free()

mm/swapfile.c

static void swap_entry_free(struct swap_info_struct *p, swp_entry_t entry)
{
        struct swap_cluster_info *ci;
        unsigned long offset = swp_offset(entry);
        unsigned char count;

        ci = lock_cluster(p, offset);
        count = p->swap_map[offset];
        VM_BUG_ON(count != SWAP_HAS_CACHE);
        p->swap_map[offset] = 0;
        dec_cluster_info_page(p, p->cluster_info, offset);
        unlock_cluster(ci);

        mem_cgroup_uncharge_swap(entry, 1);
        swap_range_free(p, offset, 1);
}

swap 엔트리를 할당 해제한다.

  • 코드 라인 7에서 offset 페이지가 소속된 클러스터의 락을 획득한다.
  • 코드 라인 7~10에서 offset 페이지에 대한 swap_map[]의 값을 count로 알아온 후 0으로 클리어하여 free 상태로 변경 한다.
  • 코드 라인 11에서 offset 페이지가 소속된 클러스터의 사용 카운터를 1 감소시킨다.
  • 코드 라인 12에서 클러스터 락을 푼다.
  • 코드 라인 14에서 memcg를 통해 swap 엔트리의 회수를 보고한다.
  • 코드 라인 15에서 swap 엔트리의 할당 해제로 인해 할당 가능한 범위를 조정한다.

 

swap 영역 할당/해제 후 사용 가능 영역 조정

swap_range_alloc()

mm/swapfile.c

static void swap_range_alloc(struct swap_info_struct *si, unsigned long offset,
                             unsigned int nr_entries)
{
        unsigned int end = offset + nr_entries - 1;

        if (offset == si->lowest_bit)
                si->lowest_bit += nr_entries;
        if (end == si->highest_bit)
                si->highest_bit -= nr_entries;
        si->inuse_pages += nr_entries;
        if (si->inuse_pages == si->pages) {
                si->lowest_bit = si->max;
                si->highest_bit = 0;
                del_from_avail_list(si);
        }
}

swap 영역의 @offset 부터 @nr_entries 만큼 할당을 통한 할당 가능 범위를 갱신한다.

  • 코드 라인 6~9에서 offset ~ nr_entries -1 범위를 할당할 때 최소 페이지 또는 최대 페이지를 필요 시 갱신한다.
  • 코드 라인 10에서 si->inuse_pages를 할당 한 페이지 수 만큼 추가한다.
  • 코드 라인 11~15에서 swap 영역을 다 사용한 경우 이 swap 영역을 swap_avail_heads 리스트에서 제거한다.

 

swap_range_free()

mm/swapfile.c

static void swap_range_free(struct swap_info_struct *si, unsigned long offset,
                            unsigned int nr_entries)
{
        unsigned long end = offset + nr_entries - 1;
        void (*swap_slot_free_notify)(struct block_device *, unsigned long);

        if (offset < si->lowest_bit)
                si->lowest_bit = offset;
        if (end > si->highest_bit) {
                bool was_full = !si->highest_bit;

                si->highest_bit = end;
                if (was_full && (si->flags & SWP_WRITEOK))
                        add_to_avail_list(si);
        }
        atomic_long_add(nr_entries, &nr_swap_pages);
        si->inuse_pages -= nr_entries;
        if (si->flags & SWP_BLKDEV)
                swap_slot_free_notify =
                        si->bdev->bd_disk->fops->swap_slot_free_notify;
        else
                swap_slot_free_notify = NULL;
        while (offset <= end) {
                frontswap_invalidate_page(si->type, offset);
                if (swap_slot_free_notify)
                        swap_slot_free_notify(si->bdev, offset);
                offset++;
        }
}

swap 영역의 @offset 부터 @nr_entries 만큼 할당 해제를 통한 할당 가능 범위를 갱신한다.

  • 코드 라인 4~12에서 offset ~ nr_entries -1 범위를 할당 해제할 때 최소 페이지 또는 최대 페이지를 필요 시 갱신한다.
  • 코드 라인 13~14에서 새로 사용 가능한 영역이 생긴 경우이므로 이 swap 영역을 &swap_avail_heads 리스트에 추가한다.
  • 코드 라인 17에서 si->inuse_pages를 할당 해제한 페이지 수 만큼 감소시킨다.
  • 코드 라인 18~28에서 offset ~ nr_entries -1 범위에 대해 frontswap에서 페이지를 invalidate 한다. 그리고 블럭 디바이스를 사용한 swap 영역인 경우 (*swap_slot_free_notify) 후크 함수를 호출하여 swap 영역이 free 되었음을 통지한다.

 

swap_avail_heads[] 리스트

swap_avail_heads[] 리스트는 노드별로 운영되며, 사용 가능한 swap 영역이 등록되어 있다.

 

swapfile_init()

mm/swapfile.c”

static int __init swapfile_init(void)
{
        int nid;

        swap_avail_heads = kmalloc_array(nr_node_ids, sizeof(struct plist_head),
                                         GFP_KERNEL);
        if (!swap_avail_heads) {
                pr_emerg("Not enough memory for swap heads, swap is disabled\n");
                return -ENOMEM;
        }

        for_each_node(nid)
                plist_head_init(&swap_avail_heads[nid]);

        return 0;
}
subsys_initcall(swapfile_init);

swap_avail_heads[] 리스트를 노드 수 만큼 할당한 후 초기화한다.

  • 이 리스트에는 swapon시 사용될 swap 영역이 등록된다.

 

add_to_avail_list()

mm/swapfile.c

static void add_to_avail_list(struct swap_info_struct *p)
{
        int nid;

        spin_lock(&swap_avail_lock);
        for_each_node(nid) {
                WARN_ON(!plist_node_empty(&p->avail_lists[nid]));
                plist_add(&p->avail_lists[nid], &swap_avail_heads[nid]);
        }
        spin_unlock(&swap_avail_lock);
}

요청한 swap 영역을 swap_avail_heads[]을 모든 노드에 추가한다.

 

del_from_avail_list()

mm/swapfile.c

static void del_from_avail_list(struct swap_info_struct *p)
{
        spin_lock(&swap_avail_lock);
        __del_from_avail_list(p);
        spin_unlock(&swap_avail_lock);
}

요청한 swap 영역을 swap_avail_heads[] 리스트에서 제거한다.

 

__del_from_avail_list()

mm/swapfile.c

static void __del_from_avail_list(struct swap_info_struct *p)
{
        int nid;

        for_each_node(nid)
                plist_del(&p->avail_lists[nid], &swap_avail_heads[nid]);
}

요청한 swap 영역을 모든 노드의 swap_avail_heads[] 리스트에서 제거한다.

 


클러스터를 사용한 swap 페이지 할당

SSD 및 persistent 메모리를 사용한 블럭 디바이스를 swap 영역으로 사용할 때 가능한 방법이다. 메모리 부족 시 여러 cpu에서 동시에 swap을 시도하게 되는데 각 cpu들이 클러스터별로 동작을 하게 설계되어 있다. 그 외에도 각 cpu가 각자의 cpu에 해당하는 swap_cluster_info 구조체에 접근할 때 false cache line share 현상으로 성능이 저하되는 것을 최대한 줄이기 위해 free 클러스터 리스트에 각 클러스터들을 그냥 순번대로 등록하지 않고 최소한 캐시 라인 크기 이상으로 떨어뜨리면 이러한 문제를 해결할 수 있다.

 

구조체

swap_cluster_info 구조체

include/linux/swap.h

/*
 * We use this to track usage of a cluster. A cluster is a block of swap disk
 * space with SWAPFILE_CLUSTER pages long and naturally aligns in disk. All
 * free clusters are organized into a list. We fetch an entry from the list to
 * get a free cluster.
 *
 * The data field stores next cluster if the cluster is free or cluster usage
 * counter otherwise. The flags field determines if a cluster is free. This is
 * protected by swap_info_struct.lock.
 */
struct swap_cluster_info {
        spinlock_t lock;        /*
                                 * Protect swap_cluster_info fields
                                 * and swap_info_struct->swap_map
                                 * elements correspond to the swap
                                 * cluster
                                 */
        unsigned int data:24;
        unsigned int flags:8;
};

하나의 클러스터 상태 및 다음 free 클러스터 번호를 가리키기 위해 사용되는 자료 구조이다.

  • lock
    • 클러스터 락
  • data:24
    • free 클러스터 상태
      • 다음 free 클러스터 번호
  • flags:8
    • CLUSTER_FLAG_FREE (1)
      • free 클러스터
    • CLUSTER_FLAG_NEXT_NULL (2)
      • 마지막 free 클러스터
    • CLUSTER_FLAG_HUGE (4)
      • thp용 클러스터

 

percpu_cluster 구조체

include/linux/swap.h

/*
 * We assign a cluster to each CPU, so each CPU can allocate swap entry from
 * its own cluster and swapout sequentially. The purpose is to optimize swapout
 * throughput.
 */
struct percpu_cluster {
        struct swap_cluster_info index; /* Current cluster index */
        unsigned int next; /* Likely next allocation offset */
};

per-cpu에 사용되는 클러스터 자료 구조이다.

  • index
    • 현재 클러스터를 담고 있다.
  • next
    • 높은 확률로 다음 할당 페이지 offset을 담고 있다.

 

swap_cluster_list 구조체

include/linux/swap.h

struct swap_cluster_list {
        struct swap_cluster_info head;
        struct swap_cluster_info tail;
};

다음 swap 클러스터 리스트에 사용된다.

  • si->free_clusters 리스트
  • si->discard_clusters 리스트

 

다음 그림은 각 cpu가 클러스터를 하나씩 지정하여 사용하는 모습을 보여준다.

  • 지정되지 않는 경우 해당 cpu의 si->cluster 값에 null을 사용한다.

 

다음 그림은 free swap 클러스터들을 연결한 상태의 free swap 클러스터 리스트를 보여준다.

 

SWAP_CLUSTER_COLS

mm/swapfile.c

#define SWAP_CLUSTER_COLS                                               \
        max_t(unsigned int, SWAP_CLUSTER_INFO_COLS, SWAP_CLUSTER_SPACE_COLS)

free 클러스터 리스트를 처음 구성할 때 false cache line share 현상을 감소시키기 위해 swap_info_cluster 구조체를 건너뛰어야 할 간격을 산출한다.

 

mm/swapfile.c

#define SWAP_CLUSTER_INFO_COLS                                          \
        DIV_ROUND_UP(L1_CACHE_BYTES, sizeof(struct swap_cluster_info))
#define SWAP_CLUSTER_SPACE_COLS                                         \
        DIV_ROUND_UP(SWAP_ADDRESS_SPACE_PAGES, SWAPFILE_CLUSTER)

 

다음과 같이 swap_cluster_info 구조체들이 free_clusters 리스트에 구성될 때 SWAP_CLUSTER_COLS 번호 간격으로 등록되는 모습을 보여준다.

  • 모든 클러스터를 다음과 같은 순서대로 등록한다.
  • 0, 64, 128, 192, ….
  • 1, 65, 129, 193, ….
  • 2, 66, 130, 194, ….

 

클러스터 하나 통째 할당 (SSD 아니더라도 사용 가능)

swap_alloc_cluster()

mm/swapfile.c

static int swap_alloc_cluster(struct swap_info_struct *si, swp_entry_t *slot)
{
        unsigned long idx;
        struct swap_cluster_info *ci;
        unsigned long offset, i;
        unsigned char *map;

        /*
         * Should not even be attempting cluster allocations when huge
         * page swap is disabled.  Warn and fail the allocation.
         */
        if (!IS_ENABLED(CONFIG_THP_SWAP)) {
                VM_WARN_ON_ONCE(1);
                return 0;
        }

        if (cluster_list_empty(&si->free_clusters))
                return 0;

        idx = cluster_list_first(&si->free_clusters);
        offset = idx * SWAPFILE_CLUSTER;
        ci = lock_cluster(si, offset);
        alloc_cluster(si, idx);
        cluster_set_count_flag(ci, SWAPFILE_CLUSTER, CLUSTER_FLAG_HUGE);

        map = si->swap_map + offset;
        for (i = 0; i < SWAPFILE_CLUSTER; i++)
                map[i] = SWAP_HAS_CACHE;
        unlock_cluster(ci);
        swap_range_alloc(si, offset, SWAPFILE_CLUSTER);
        *slot = swp_entry(si->type, offset);

        return 1;
}

thp swap을 위해 사용될 free swap 클러스터(256 페이지)를 통째로 할당할 swap 엔트리를 출력 인자 @slot에 저장한다. 성공하는 경우 1을 반환한다.

  • 코드 라인 12~15에서 thp swap을 지원하지 않는 커널의 경우 실패로 0을 반환한다.
  • 코드 라인 17~18에서 free 클러스터 리스트에 남은 free 클러스터가 없는 경우 실패로 0을 반환한다.
  • 코드 라인 20~24에서 free 클러스터 리스트의 첫 클러스터 번호(idx)에 해당하는 클러스터를 huge 할당 상태로 변경한다.
  • 코드 라인 26~28에서 해당 클러스터에 포함된 페이지들에 대한 할당 상태를 SWAP_HAS_CACHE로 변경한다.
  • 코드 라인 30에서 해당 클러스터를 할당 상태로 바꿈에 따라 lowest_bit 및 highest_bit 범위의 변경 시 갱신한다.
  • 코드 라인 31~33에서 출력 인자 @slot에 swap 엔트리를 저장하고, 성공 1을 반환한다.

 

scan_swap_map_ssd_cluster_conflict()

mm/swapfile.c

/*
 * It's possible scan_swap_map() uses a free cluster in the middle of free
 * cluster list. Avoiding such abuse to avoid list corruption.
 */
static bool
scan_swap_map_ssd_cluster_conflict(struct swap_info_struct *si,
        unsigned long offset)
{
        struct percpu_cluster *percpu_cluster;
        bool conflict;

        offset /= SWAPFILE_CLUSTER;
        conflict = !cluster_list_empty(&si->free_clusters) &&
                offset != cluster_list_first(&si->free_clusters) &&
                cluster_is_free(&si->cluster_info[offset]);

        if (!conflict)
                return false;

        percpu_cluster = this_cpu_ptr(si->percpu_cluster);
        cluster_set_null(&percpu_cluster->index);
        return true;
}

요청한 @offset 페이지가 free 클러스터의 첫 free 클러스터인지 확인한다. 만일 그렇지 않은 경우 conflict 상황이므로 true를 반환한다.

  • 코드 라인 8~11에서 scan_swap_map()은 사용 가능한 클러스터 리스트 중간에 있는 free 클러스터를 사용할 수 있다. 이러한 목록 손상을 피하기는 방법이다. 만일 요청한 @offset 페이지가 free 클러스터의 처음이 아닌 경우 conflict 상황이다.
  • 코드 라인 13~14에서 conflict 상황이 아니면 정상적으로 false를 반환한다.
  • 코드 라인 16~18에서 conflict 상황인 경우 현재 cpu의 percpu_cluster를 null로 설정하고 true를 반환한다.

 

SSD 클러스터 단위 swap 맵 할당

scan_swap_map_try_ssd_cluster()

mm/swapfile.c

/*
 * Try to get a swap entry from current cpu's swap entry pool (a cluster). This
 * might involve allocating a new cluster for current CPU too.
 */
static bool scan_swap_map_try_ssd_cluster(struct swap_info_struct *si,
        unsigned long *offset, unsigned long *scan_base)
{
        struct percpu_cluster *cluster;
        struct swap_cluster_info *ci;
        bool found_free;
        unsigned long tmp, max;

new_cluster:
        cluster = this_cpu_ptr(si->percpu_cluster);
        if (cluster_is_null(&cluster->index)) {
                if (!cluster_list_empty(&si->free_clusters)) {
                        cluster->index = si->free_clusters.head;
                        cluster->next = cluster_next(&cluster->index) *
                                        SWAPFILE_CLUSTER;
                } else if (!cluster_list_empty(&si->discard_clusters)) {
                        /*
                         * we don't have free cluster but have some clusters in
                         * discarding, do discard now and reclaim them
                         */
                        swap_do_scheduled_discard(si);
                        *scan_base = *offset = si->cluster_next;
                        goto new_cluster;
                } else
                        return false;
        }

        found_free = false;

        /*
         * Other CPUs can use our cluster if they can't find a free cluster,
         * check if there is still free entry in the cluster
         */
        tmp = cluster->next;
        max = min_t(unsigned long, si->max,
                    (cluster_next(&cluster->index) + 1) * SWAPFILE_CLUSTER);
        if (tmp >= max) {
                cluster_set_null(&cluster->index);
                goto new_cluster;
        }
        ci = lock_cluster(si, tmp);
        while (tmp < max) {
                if (!si->swap_map[tmp]) {
                        found_free = true;
                        break;
                }
                tmp++;
        }
        unlock_cluster(ci);
        if (!found_free) {
                cluster_set_null(&cluster->index);
                goto new_cluster;
        }
        cluster->next = tmp + 1;
        *offset = tmp;
        *scan_base = tmp;
        return found_free;
}

현재 cpu에 지정된 클러스터에서 free swap 엔트리를 찾아 출력 인자 @offset과 @scan_base에 저장한다. 실패하는 경우 false를 반환한다.

  • 코드 라인 9에서 새 클러스터를 다시 찾아야 할 때 이동될 new_cluster: 레이블이다.
  • 코드 라인 10~26에서 현재 cpu에 지정된 클러스터가 없는 경우 다음 중 하나를 수행한다.
    • free 클러스터 리스트에서 준비한다.
    • free 클러스터 리스트에 등록된 free 클러스터가 없으면 discard 클러스터 리스트에 있는 클러스터들을 블럭 디바이스에 discard 요청하고 free 클러스터에 옮긴다. 그런 후 다시 new_cluster: 레이블로 이동하여 다시 시도한다.
    • free 클러스터 리스트 및 discard 클러스터 리스트에 등록된 클러스터가 없는 경우 사용할 수 있는 클러스터가 없어 false를 반환한다.
  • 코드 라인 34~40에서 클러스터 내 찾을 next 페이지를 tmp에 대입하고, 다음 클러스터의 마지막 페이지를 벗어난 경우 현재 cpu가 이 클러스터에 null을 대입하여 사용을 포기하게 한다. 그리고 다시 새로운 클러스터를 찾으러 new_cluster 레이블로 이동한다. 클러스터의 락을 획득한 상태가 아니므로 경쟁 상황에서 이미 다른 cpu가 이 클러스터를 사용했을 수 있으므로 이 클러스터를 포기한다.
  • 코드 라인 41~53에서 tmp 페이지가 소속한 클러스터 락을 획득하 상태에서 tmp ~ max 페이지까지 순회하며 swap_map[]에 free swap 페이지가 있는지 찾아 발견되면 found_free에 true를 대입하고 루프를 탈출한다. 만일 이 클러스터에 free swap 페이지가 하나도 발견되지 않으면 이 클러스터를 포기하고, 다시 새로운 클러스터를 찾으러 new_cluster: 레이블로 이동한다.
  • 코드 라인 54~57에서 찾은 페이지를 @offset과 @scan_base에 저장하고 성공 결과인 true를 반환한다.

 

클러스터 사용 증가

inc_cluster_info_page()

mm/swapfile.c

/*
 * The cluster corresponding to page_nr will be used. The cluster will be
 * removed from free cluster list and its usage counter will be increased.
 */
static void inc_cluster_info_page(struct swap_info_struct *p,
        struct swap_cluster_info *cluster_info, unsigned long page_nr)
{
        unsigned long idx = page_nr / SWAPFILE_CLUSTER;

        if (!cluster_info)
                return;
        if (cluster_is_free(&cluster_info[idx]))
                alloc_cluster(p, idx);

        VM_BUG_ON(cluster_count(&cluster_info[idx]) >= SWAPFILE_CLUSTER);
        cluster_set_count(&cluster_info[idx],
                cluster_count(&cluster_info[idx]) + 1);
}

@page_nr에 해당하는 클러스터의 사용 카운터를 증가시킨다. 처음 사용되는 경우 free 클러스터를 할당 상태로 변경한다.

  • 코드 라인 6~7에서 SSD 클러스터 방식을 사용하지 않는 경우 아무것도 하지 않고 함수를 빠져나간다.
  • 코드 라인 8~9에서 @idx 클러스터가 free 상태이면 free 클러스터에서 제거하고 할당 상태로 변경한다.
  • 코드 라인 11에서 @idx 클러스터의 사용 카운터가 256 이상이 되면 버그다.
  • 코드 라인 12~13에서 @idx 클러스터의 사용 카운터를 1 증가 시킨다.

 

 

alloc_cluster()

mm/swapfile.c

static void alloc_cluster(struct swap_info_struct *si, unsigned long idx)
{
        struct swap_cluster_info *ci = si->cluster_info;

        VM_BUG_ON(cluster_list_first(&si->free_clusters) != idx);
        cluster_list_del_first(&si->free_clusters, ci);
        cluster_set_count_flag(ci + idx, 0, 0);
}

@idx 클러스터를 free 클러스터에서 제거하고, 사용 카운터를 0으로 설정한다.

 

 

클러스터 사용 감소

dec_cluster_info_page()

mm/swapfile.c

/*
 * The cluster corresponding to page_nr decreases one usage. If the usage
 * counter becomes 0, which means no page in the cluster is in using, we can
 * optionally discard the cluster and add it to free cluster list.
 */
static void dec_cluster_info_page(struct swap_info_struct *p,
        struct swap_cluster_info *cluster_info, unsigned long page_nr)
{
        unsigned long idx = page_nr / SWAPFILE_CLUSTER;

        if (!cluster_info)
                return;

        VM_BUG_ON(cluster_count(&cluster_info[idx]) == 0);
        cluster_set_count(&cluster_info[idx],
                cluster_count(&cluster_info[idx]) - 1);

        if (cluster_count(&cluster_info[idx]) == 0)
                free_cluster(p, idx);
}

@page_nr에 해당하는 클러스터의 사용 카운터를 감소시킨다. 사용 카운터가 0이 되면 free 상태로 변경하고 free 클러스터 리스트에 추가한다.

  • 코드 라인 6~7에서 SSD 클러스터 방식을 사용하지 않는 경우 아무것도 하지 않고 함수를 빠져나간다.
  • 코드 라인 9에서 @idx 클러스터의 사용 카운터가 0인 경우 버그다.
  • 코드 라인 10~11에서 @idx 클러스터의 사용 카운터를 1 감소 시킨다.
  • 코드 라인 13~14에서 @idx 클러스터의 사용 카운터가 0이되면 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.

 

free_cluster()

mm/swapfile.c

static void free_cluster(struct swap_info_struct *si, unsigned long idx)
{
        struct swap_cluster_info *ci = si->cluster_info + idx;

        VM_BUG_ON(cluster_count(ci) != 0);
        /*
         * If the swap is discardable, prepare discard the cluster
         * instead of free it immediately. The cluster will be freed
         * after discard.
         */
        if ((si->flags & (SWP_WRITEOK | SWP_PAGE_DISCARD)) ==
            (SWP_WRITEOK | SWP_PAGE_DISCARD)) {
                swap_cluster_schedule_discard(si, idx);
                return;
        }

        __free_cluster(si, idx);
}

@idx 클러스터를 free 상태로 변경하고 free 클러스터 리스트에 추가한다.

  • 코드 라인 11~15에서 discard가 허용된 기록 가능한 swap 영역인 경우 @idx 클러스트에 대해 워커 스레드를 통해  discard 후 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.
  • 코드 라인 17에서 그 외의 경우 @idx 클러스터를 곧바로 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.

 

 

discard 정책

SSD 장치가 discard 옵션을 사용하여 마운트되었거나 트림(trim) 작업을 지원하는 경우 swapon을 사용하여 swap 영역을 지정할 때 discard 옵션을 추가하여 swap discard 기능을 사용할 수 있다. 이러한 경우 일부 SSD 장치의 성능을 향상시킬 수도 있다. swapon의 discard 옵션은 다음 두 가지를 지정하거나, 지정하지 않으면 두 가지 정책을 다 사용한다.

  • –discard=once
    • 전체 swap 영역에 대해 한 번만 discard 작업을 수행한다.
  • –discard=pages
    • 사용 가능한 swap 페이지를 재사용하기 전에 비동기적으로 discard 한다.

 

__free_cluster()

mm/swapfile.c

static void __free_cluster(struct swap_info_struct *si, unsigned long idx)
{
        struct swap_cluster_info *ci = si->cluster_info;

        cluster_set_flag(ci + idx, CLUSTER_FLAG_FREE);
        cluster_list_add_tail(&si->free_clusters, ci, idx);
}

@idx 클러스터를 free 상태로 변경하고 free 클러스터 리스트에 추가한다.

 

스케줄드 discard 클러스터

swap_cluster_schedule_discard()

mm/swapfile.c

/* Add a cluster to discard list and schedule it to do discard */
static void swap_cluster_schedule_discard(struct swap_info_struct *si,
                unsigned int idx)
{
        /*
         * If scan_swap_map() can't find a free cluster, it will check
         * si->swap_map directly. To make sure the discarding cluster isn't
         * taken by scan_swap_map(), mark the swap entries bad (occupied). It
         * will be cleared after discard
         */
        memset(si->swap_map + idx * SWAPFILE_CLUSTER,
                        SWAP_MAP_BAD, SWAPFILE_CLUSTER);

        cluster_list_add_tail(&si->discard_clusters, si->cluster_info, idx);

        schedule_work(&si->discard_work);
}

스케줄 워크를 동작시켜 @idx 클러스터를 discard 요청 후 free 클러스터에 추가하게 한다.

  • 코드 라인 11~12에서 @idx 클러스터의 모든 페이지에 해당하는 swap_map[]을 bad 페이지로 마크한다.
  • 코드 라인 14에서 이 클러스터를 discard 클러스터에 등록한다.
  • 코드 라인 16에서 스케줄 워크를 동작시켜 swap_discard_work() 함수를 호출한다. 이 함수에서는 블럭 디바이스에 discard를 요청 후 free 상태로 변경하고 free 클러스터 리스트에 추가하는 작업을 수행한다.

 

swap_discard_work()

mm/swapfile.c

static void swap_discard_work(struct work_struct *work)
{
        struct swap_info_struct *si;

        si = container_of(work, struct swap_info_struct, discard_work);

        spin_lock(&si->lock);
        swap_do_scheduled_discard(si);
        spin_unlock(&si->lock);
}

discard 클러스터 리스트의 클러스터들을 discard 요청 후 free 클러스터로 옮긴다.

 

swap_do_scheduled_discard()

mm/swapfile.c

/*
 * Doing discard actually. After a cluster discard is finished, the cluster
 * will be added to free cluster list. caller should hold si->lock.
*/
static void swap_do_scheduled_discard(struct swap_info_struct *si)
{
        struct swap_cluster_info *info, *ci;
        unsigned int idx;

        info = si->cluster_info;

        while (!cluster_list_empty(&si->discard_clusters)) {
                idx = cluster_list_del_first(&si->discard_clusters, info);
                spin_unlock(&si->lock);

                discard_swap_cluster(si, idx * SWAPFILE_CLUSTER,
                                SWAPFILE_CLUSTER);

                spin_lock(&si->lock);
                ci = lock_cluster(si, idx * SWAPFILE_CLUSTER);
                __free_cluster(si, idx);
                memset(si->swap_map + idx * SWAPFILE_CLUSTER,
                                0, SWAPFILE_CLUSTER);
                unlock_cluster(ci);
        }
}

discard 클러스터 리스트의 클러스터들을 discard 요청 후 free 클러스터로 옮긴다.

  • 코드 라인 8~9에서 discard 클러스터 리스트의 모든 클러스터를 순회하며 discard 클러스터 리스트에서 제거한다.
  • 코드 라인 12~13에서 블록 디바이스에 각 클러스터에 포함된 페이지들을 모두 discard 요청한다.
  • 코드 라인 15~20에서 cluster 락을 획득한 채로 순회 중인 클러스터를 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.

 

discard_swap_cluster()

mm/swapfile.c

/*
 * swap allocation tell device that a cluster of swap can now be discarded,
 * to allow the swap device to optimize its wear-levelling.
 */
static void discard_swap_cluster(struct swap_info_struct *si,
                                 pgoff_t start_page, pgoff_t nr_pages)
{
        struct swap_extent *se = si->curr_swap_extent;
        int found_extent = 0;

        while (nr_pages) {
                if (se->start_page <= start_page &&
                    start_page < se->start_page + se->nr_pages) {
                        pgoff_t offset = start_page - se->start_page;
                        sector_t start_block = se->start_block + offset;
                        sector_t nr_blocks = se->nr_pages - offset;

                        if (nr_blocks > nr_pages)
                                nr_blocks = nr_pages;
                        start_page += nr_blocks;
                        nr_pages -= nr_blocks;

                        if (!found_extent++)
                                si->curr_swap_extent = se;

                        start_block <<= PAGE_SHIFT - 9;
                        nr_blocks <<= PAGE_SHIFT - 9;
                        if (blkdev_issue_discard(si->bdev, start_block,
                                    nr_blocks, GFP_NOIO, 0))
                                break;
                }

                se = list_next_entry(se, list);
        }
}

discard를 지원하는 SSD 스타일의 swap 장치에 @start_page 부터 @nr_pages만큼 discard 요청한다.

 


Per-cpu Swap 슬롯 캐시

swap 영역의 swap_map[]을 통해 swap 엔트리를 할당한다. 이 때마다 swap 영역의 락이 필요하다. 이의 성능 향상을 위해 swap 엔트리 할당을 위해 앞단에 per-cpu swap 슬롯 캐시를 사용하여 swap 영역의 잠금 없이 빠르게 할당/해제할 수 있게 하였다.

 

swap_slots_cache 구조체

include/linux/swap_slots.h

struct swap_slots_cache {
        bool            lock_initialized;
        struct mutex    alloc_lock; /* protects slots, nr, cur */
        swp_entry_t     *slots;
        int             nr;
        int             cur;
        spinlock_t      free_lock;  /* protects slots_ret, n_ret */
        swp_entry_t     *slots_ret;
        int             n_ret;
};

swap 슬롯 캐시 하나에는 할당용과 회수용 swap 엔트리 배열을 가진다.

  • lock_initialized
    • 락 초기화 여부를 표시한다.
  • alloc_lock
    • swap 엔트리 할당용 swap 슬롯 캐시 락
  • *slots
    • swap 엔트리 할당용 swap 엔트리 배열
  • nr
    • swap 엔트리 할당용 swap 엔트리 수
  • cur
    • swap 엔트리 할당용 현재 swap 엔트리 번호
  • free_lock
    • swap 엔트리 회수용 swap 슬롯 캐시 락
  • *slots_ret
    • swap 엔트리 회수용 swap 엔트리 배열
  • n_ret
    • swap 엔트리 회수용 swap 엔트리 수

 

다음 그림은 swap 슬롯 캐시의 두 swap 엔트리 배열의 용도를 보여준다.

 

swap 슬롯 캐시에서 swap 엔트리 할당

get_swap_page()

mm/swap_slots.c

swp_entry_t get_swap_page(struct page *page)
{
        swp_entry_t entry, *pentry;
        struct swap_slots_cache *cache;

        entry.val = 0;

        if (PageTransHuge(page)) {
                if (IS_ENABLED(CONFIG_THP_SWAP))
                        get_swap_pages(1, &entry, HPAGE_PMD_NR);
                goto out;
        }

        /*
         * Preemption is allowed here, because we may sleep
         * in refill_swap_slots_cache().  But it is safe, because
         * accesses to the per-CPU data structure are protected by the
         * mutex cache->alloc_lock.
         *
         * The alloc path here does not touch cache->slots_ret
         * so cache->free_lock is not taken.
         */
        cache = raw_cpu_ptr(&swp_slots);

        if (likely(check_cache_active() && cache->slots)) {
                mutex_lock(&cache->alloc_lock);
                if (cache->slots) {
repeat:
                        if (cache->nr) {
                                pentry = &cache->slots[cache->cur++];
                                entry = *pentry;
                                pentry->val = 0;
                                cache->nr--;
                        } else {
                                if (refill_swap_slots_cache(cache))
                                        goto repeat;
                        }
                }
                mutex_unlock(&cache->alloc_lock);
                if (entry.val)
                        goto out;
        }

        get_swap_pages(1, &entry, 1);
out:
        if (mem_cgroup_try_charge_swap(page, entry)) {
                put_swap_page(page, entry);
                entry.val = 0;
        }
        return entry;
}

swap 슬롯 캐시를 통해 요청 페이지에 사용할 swap 엔트리를 얻어온다. 실패 시 null을 반환한다.

  • 코드 라인 8~12에서 thp의 경우 thp swap이 지원될 때에만 HPAGE_PMD_NR 단위로 swap 엔트리를 확보한다.
    • 현재 x86_64 아키텍처가 thp  swap이 지원되고 있다.
  • 코드 라인 23~42에서 thp가 아닌 경우이다. swap 슬롯 캐시가 활성화된 경우 swap 슬롯 캐시를 통해 swap 엔트리를 얻어온다. swap 슬롯 캐시가 부족하면 리필한다.
  • 코드 라인 44에서 swap 슬롯 캐시가 활성화되지 않은 경우에는 직접 swap 엔트리를 얻어온다.
  • 코드 라인 45~49에서 out: 레이블이다. memcg를 통해 메모리 + swap 용량이 메모리 한계를 초과하는 경우 swap 엔트리를 drop 한다.
    • memory.memsw.limit_in_bytes
  • 코드 라인 50에서 swap 엔트리를 반환한다.

 

swap 슬롯 캐시 충전

refill_swap_slots_cache()

mm/swap_slots.c

/* called with swap slot cache's alloc lock held */
static int refill_swap_slots_cache(struct swap_slots_cache *cache)
{
        if (!use_swap_slot_cache || cache->nr)
                return 0;

        cache->cur = 0;
        if (swap_slot_cache_active)
                cache->nr = get_swap_pages(SWAP_SLOTS_CACHE_SIZE,
                                           cache->slots, 1);

        return cache->nr;
}

슬롯 캐시에 swap 영역에서 검색해온 swap 엔트리들을 충전(refill)하고 그 수를 반환한다.

  • 코드 라인 4~5에서 swap 슬롯 캐시를 사용하지 않거나 캐시에 swap 엔트리들이 이미 있는 경우 0을 반환한다.
  • 코드 라인 7~10에서 슬롯 캐시가 활성화된 경우 swap_map을 검색하여 swap 엔트리들을 SWAP_SLOTS_CACHE_SIZE(64) 만큼 가져온다.
  • 코드 라인 12에서 가져온 swap 엔트리 수를 반환한다.

 

swap 슬롯 캐시로 swap 엔트리 회수

free_swap_slot()

mm/swap_slots.c

int free_swap_slot(swp_entry_t entry)
{
        struct swap_slots_cache *cache;

        cache = raw_cpu_ptr(&swp_slots);
        if (likely(use_swap_slot_cache && cache->slots_ret)) {
                spin_lock_irq(&cache->free_lock);
                /* Swap slots cache may be deactivated before acquiring lock */
                if (!use_swap_slot_cache || !cache->slots_ret) {
                        spin_unlock_irq(&cache->free_lock);
                        goto direct_free;
                }
                if (cache->n_ret >= SWAP_SLOTS_CACHE_SIZE) {
                        /*
                         * Return slots to global pool.
                         * The current swap_map value is SWAP_HAS_CACHE.
                         * Set it to 0 to indicate it is available for
                         * allocation in global pool
                         */
                        swapcache_free_entries(cache->slots_ret, cache->n_ret);
                        cache->n_ret = 0;
                }
                cache->slots_ret[cache->n_ret++] = entry;
                spin_unlock_irq(&cache->free_lock);
        } else {
direct_free:
                swapcache_free_entries(&entry, 1);
        }

        return 0;
}

swap 슬롯 캐시를 통해 swap 엔트리 하나를 회수한다. (put_swap_page() 함수에서 호출된다)

  • 코드 라인 5에서 현재 cpu에 대한 swap 슬롯 캐시를 알아온다.
  • 코드 라인 6~12에서 swap 슬롯 캐시를 사용할 수 있는 경우이다. swap 슬롯 캐시의 lock을 획득한 채로 다시 한 번 swap 슬롯 캐시를 사용 가능한지 체크한다. 사용 불가능한 경우 락을 풀고 direct_free 레이블로 이동하여 slot 캐시로 반환하지 않고 직접 swap 영역에 반환한다.
  • 코드 라인 13~22에서 슬롯 캐시가 관리하는 최대 수를 초과하는 경우 기존 swap 슬롯 캐시에 존재하는 swap 엔트리들을 한꺼번에 각 글로벌 swap 영역으로 회수한다.
  • 코드 라인 23~24에서 swap 슬롯 캐시로 swap 엔트리 하나를 회수한다.
  • 코드 라인 25~28에서 direct_free: 레이블이다. swap 슬롯 캐시가 아닌 글로벌 swap 영역으로 직접 swap 엔트리 하나를 회수한다.

 

참고