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) 레지스터 값을 읽어온다.

 

참고

 

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 엔트리 하나를 회수한다.

 

참고

 

Swap -2- (Swapin & Swapout)

<kernel v5.0>

Swapin & Swapout

유저 프로세스가 swap되어 언매핑된 빈 가상 주소에 접근하는 경우 swap 영역에서 swap 페이지를 로드하여 복구해야 한다. 이 때 swap 캐시를 거쳐 swap 영역에서 swap된 페이지를 로드하는 과정을 swap-in 이라고 하고, 반대로 저장하는 과정을 swap-out이라고 한다.

 

Swap readahead

swap-in 과정에서 swap되어 빈 영역에 접근하는 경우 fault 에러가 발생한다. 이 때 fault된 가상 주소로 페이지 테이블에 저장된 swap 엔트리를 알아온 후 이를 키로 관련 swap 영역에서 swap 페이지를 로드하는데 주변 페이지들을 미리 로드하여 처리 성능을 올릴 수 있다. 이러한 방법을 readahead라고 하는데 swap 과정에 사용되는 readahead는 다음과 같이 두 가지 방법이 사용되고 있다.

  • vma 기반 swap readahead
  • cluster 기반 swap readahead (for SSD)

 

readahead 기능으로 같이 읽혀온 페이지들에 swap 캐시에 있을때 해당 페이지에 PG_reclaim (플래그 사용을 절약하기 위해 readahead 시에는 reclaim이 아니라 PG_readahead 용도로 사용된다.) 플래그가 붙는다.

 

VMA 기반 swap readahead

유저 프로세스가 swap 된 페이지에 접근하여 fault가 발생하면, swap 캐시에서 페이지를 찾아보고, swap 캐시에서 발견하지 못하면 swap 영역으로 부터 vma 영역내의 fault된 페이지 주변을 조금 더 읽어와서 주변 페이지에 접근하여 fault될 때 이미 읽어온 페이지가 swap 캐시 영역에서 찾을 수 있어 이를 빠르게 anon 페이지로 변환 및 매핑할 수 있다.

 

다음 그림은 swap된 페이지가 swap 영역에서 로드될 때 fault 페이지의 vma내 주변 페이지 일부를 미리 swap 캐시에 로드하는 과정을 보여준다.

 

클러스터 기반 swap readahead

VMA 기반과 다르게 fault된 swap 페이지의 주변이 아니라 swap 영역의 주변 페이지를 더 읽어온다.

 

다음 그림은 swap된 페이지가 swap 영역에서 로드될 때 swap 영역의 주변 페이지 일부를 미리 swap 캐시에 로드하는 과정을 보여준다.

 

Swap 관련 페이지 플래그

  • PG_swapbacked
    • swap 영역을 가졌는지 여부이다.
      • 이 플래그를 가지면 swap이 가능한 일반 anon 페이지이다.
      • 이 플래그가 없으면 swap이 불가능한 clean anon 페이지이다.
  • PG_swapcached
    • swap 되어 swap 캐시 영역에 존재하는 상태이다. 유저가 swap된 가상 주소 페이지에 접근 시 fault 핸들러를 통해 swap 영역보다 먼저 swap 캐시를 찾는다.
      • swap 영역에 기록되었는지 여부는 이 플래그로 알 수 없고 page_swapped() 함수를 통해서 알아낼 수 있다.
    • swap-in이 진행될 때 swap 영역에서 읽을 때 성능 향상을 위해 주변 페이지도 읽어  swap 캐시 영역에 로드한다.
    • swap-out이 진행할 때 이 swap 캐시를 swap 영역에 저장한다.
  • PG_writeback
    • swap 영역에 기록(sync 또는 async) 하는 동안 설정된다.
    • pageout() 에서 swap writeback 후크 함수인 swap_writepage() 함수에서 설정되고, writeback이 완료되면 클리어된다.
  • PG_reclaim (2가지 용도)
    • swap-out 시 PageReclaim()으로 사용된다.
      • reclaim을 위해 swap 영역에 기록하는 중에 설정되고, 이 플래그가 제거될 때 회수할 수 있다.
      • pageout() 에서 writeback 직전에 설정되고, writeback이 완료되면 클리어된다.
    • swap-in 시 PageReadahead()으로 사용된다.
      • readahead로 미리 읽어온 swap 캐시 페이지에 설정된다.
  • PG_dirty
    • swap 영역에 기록하기 위해 설정되며, 이 플래그를 보고 pageout()이 호출된다.
    • add_to_swap_cache() 함수에서 설정되고, pageout() 에서 writeback 직전에 클리어된다.
  • PG_workingset
    • 페이지가 작업중임을 알리기 위한 플래그이다.
      • file 페이지가 inactive lru 리스트에서 refault될 때 그 fault 간격을 메모리 크기와 비교하여 메모리 크기보다 작은 fault 간격인 경우 이를 체크하여 thrashing을 감지하는데 사용하기 위해 workingset 플래그를 사용한다.
      • 자주 사용되는 페이지가 여러 번 fualt되어 캐시를 교체하느라 성능 저하되는 현상을 막는 솔루션이다.
      • 참고: mm: workingset: tell cache transitions from workingset thrashing (2018, v4.20-rc1)
    • file 페이지가 처음 access되면 inactive lru 리스트의 선두에서 출발한다. 그리고 두 번의 access를 감지하면 active lru로 승격한다. 그러나 anon 페이지는 처음 access되면 active lru 리스트의 선두에서 출발하므로 처음부터 workingset으로 설정한다.

 

다음 그림은 swap 관련 페이지 플래그의 변화를 보여준다.

 


Swap 초기화

swap_init_sysfs()

mm/swap_state.c

static int __init swap_init_sysfs(void)
{
        int err;
        struct kobject *swap_kobj;

        swap_kobj = kobject_create_and_add("swap", mm_kobj);
        if (!swap_kobj) {
                pr_err("failed to create swap kobject\n");
                return -ENOMEM;
        }
        err = sysfs_create_group(swap_kobj, &swap_attr_group);
        if (err) {
                pr_err("failed to register swap group\n");
                goto delete_obj;
        }
        return 0;

delete_obj:
        kobject_put(swap_kobj);
        return err;
}
subsys_initcall(swap_init_sysfs);

swap 시스템을 위해 /sys/kernel/mm/swap 디렉토리를 생성하고 관련 속성(vma_ra_enabled) 파일을 생성한다.

 

vma_ra_enabled 속성

mm/swap_state.c

static ssize_t vma_ra_enabled_show(struct kobject *kobj,
                                     struct kobj_attribute *attr, char *buf)
{
        return sprintf(buf, "%s\n", enable_vma_readahead ? "true" : "false");
}
static ssize_t vma_ra_enabled_store(struct kobject *kobj,
                                      struct kobj_attribute *attr,
                                      const char *buf, size_t count)
{
        if (!strncmp(buf, "true", 4) || !strncmp(buf, "1", 1))
                enable_vma_readahead = true;
        else if (!strncmp(buf, "false", 5) || !strncmp(buf, "0", 1))
                enable_vma_readahead = false;
        else
                return -EINVAL;

        return count;
}
static struct kobj_attribute vma_ra_enabled_attr =
        __ATTR(vma_ra_enabled, 0644, vma_ra_enabled_show,
               vma_ra_enabled_store);

static struct attribute *swap_attrs[] = {
        &vma_ra_enabled_attr.attr,
        NULL,
};

static struct attribute_group swap_attr_group = {
        .attrs = swap_attrs,
};

vma 기반 readahead 기능을 enable 하는 속성 파일이다.

  • “/sys/kernel/mm/swap/vma_ra_enabled” 속성의 디폴트 값은 true이다.
    • swap_vma_readahead -> vma_ra_enabled 속성으로 이름이 바뀌었다.
  • swap_use_vma_readahead() 함수를 통해 이 속성의 설정 여부를 알아온다.

 


Swap-out

normal anon 페이지가 swap 영역에 기록된 후 페이지가 free되는 순서는 다음과 같다.

  • normal anon 페이지 → swapcache → unmap → write out → free 페이지
    • add_to_swap()
    • try_to_unmap()
    • pageout()
    • free_unref_page_commit()

 

다음 그림은 swap-out 과정을 보여준다.

 

Swap 영역에 추가

add_to_swap()

mm/swap_state.c

/**
 * add_to_swap - allocate swap space for a page
 * @page: page we want to move to swap
 *
 * Allocate swap space for the page and add the page to the
 * swap cache.  Caller needs to hold the page lock.
 */
int add_to_swap(struct page *page)
{
        swp_entry_t entry;
        int err;

        VM_BUG_ON_PAGE(!PageLocked(page), page);
        VM_BUG_ON_PAGE(!PageUptodate(page), page);

        entry = get_swap_page(page);
        if (!entry.val)
                return 0;

        /*
         * XArray node allocations from PF_MEMALLOC contexts could
         * completely exhaust the page allocator. __GFP_NOMEMALLOC
         * stops emergency reserves from being allocated.
         *
         * TODO: this could cause a theoretical memory reclaim
         * deadlock in the swap out path.
         */
        /*
         * Add it to the swap cache.
         */
        err = add_to_swap_cache(page, entry,
                        __GFP_HIGH|__GFP_NOMEMALLOC|__GFP_NOWARN);
        if (err)
                /*
                 * add_to_swap_cache() doesn't return -EEXIST, so we can safely
                 * clear SWAP_HAS_CACHE flag.
                 */
                goto fail;
        /*
         * Normally the page will be dirtied in unmap because its pte should be
         * dirty. A special case is MADV_FREE page. The page'e pte could have
         * dirty bit cleared but the page's SwapBacked bit is still set because
         * clearing the dirty bit and SwapBacked bit has no lock protected. For
         * such page, unmap will not set dirty bit for it, so page reclaim will
         * not write the page out. This can cause data corruption when the page
         * is swap in later. Always setting the dirty bit for the page solves
         * the problem.
         */
        set_page_dirty(page);

        return 1;

fail:
        put_swap_page(page, entry);
        return 0;
}

swap 엔트리를 할당한 후 이 값을 키로 anon 페이지를 swap 캐시 및 swap 영역에 저장한다. 성공 시 1을 반환한다.

  • 코드 라인 6~7에서 swap-out을 하기 전에 PG_lock과 PG_uptodate가 반드시 설정되어 있어야 한다.
  • 코드 라인 9~11에서 swap할 anon 페이지에 사용할 swap 엔트리를 얻어온다.
  • 코드 라인 24~31에서 swap 엔트리를 키로 swap할 anon 페이지를 swap 캐시에 추가한다.
  • 코드 라인 42~44에서 페이지에 dirty 설정을 한다. 그 후 성공하였으므로 1을 반환한다.
    • address_space에 매핑된 페이지의 경우 드라이버를 통해 dirty 설정을 하고, 페이지에도 PG_dirty 플래그를 설정한다.
    • dirty된 페이지는 reclaim 과정에서 pageout() 함수가 호출되어 swap 영역에 저장되며, 완료된 후에 PG_dirty 플래그가 클리어된다.
  • 코드 라인 46~48에서 fail: 레이블이다. 실패한 경우이므로 0을 반환한다.

 

Swap 캐시에 추가

add_to_swap_cache()

mm/swap_state.c

/*
 * add_to_swap_cache resembles add_to_page_cache_locked on swapper_space,
 * but sets SwapCache flag and private instead of mapping and index.
 */
int add_to_swap_cache(struct page *page, swp_entry_t entry, gfp_t gfp)
{
        struct address_space *address_space = swap_address_space(entry);
        pgoff_t idx = swp_offset(entry);
        XA_STATE_ORDER(xas, &address_space->i_pages, idx, compound_order(page));
        unsigned long i, nr = 1UL << compound_order(page);

        VM_BUG_ON_PAGE(!PageLocked(page), page);
        VM_BUG_ON_PAGE(PageSwapCache(page), page);
        VM_BUG_ON_PAGE(!PageSwapBacked(page), page);

        page_ref_add(page, nr);
        SetPageSwapCache(page);

        do {
                xas_lock_irq(&xas);
                xas_create_range(&xas);
                if (xas_error(&xas))
                        goto unlock;
                for (i = 0; i < nr; i++) {
                        VM_BUG_ON_PAGE(xas.xa_index != idx + i, page);
                        set_page_private(page + i, entry.val + i);
                        xas_store(&xas, page + i);
                        xas_next(&xas);
                }
                address_space->nrpages += nr;
                __mod_node_page_state(page_pgdat(page), NR_FILE_PAGES, nr);
                ADD_CACHE_INFO(add_total, nr);
unlock:
                xas_unlock_irq(&xas);
        } while (xas_nomem(&xas, gfp));

        if (!xas_error(&xas))
                return 0;

        ClearPageSwapCache(page);
        page_ref_sub(page, nr);
        return xas_error(&xas);
}

swap 엔트리 정보를 키로 anon 페이지를 swap 캐시에 추가한다. 성공 시 0을 반환한다.

  • 코드 라인 3에서 swap 엔트리를 사용하여 swap용 address_space 포인터를 알아온다.
  • 코드 라인 4에서 swap 엔트리로 offset 부분만을 읽어 idx에 대입한다.
  • 코드 라인 5에서 xarray operation state를 선언한다.
    • 작업할 xarray는  &address_space->i_pages이고, 초기 인덱스(idx) 및 엔트리의 order를 지정한다.
    • The XArray data structure (2018) | LWN.net
  • 코드 라인 6에서 nr에 compound 페이지의 수 만큼 대입한다.
    • 일반 페이지는 1이 대입되지만, thp의 경우 compound 구성된 페이지들의 수를 대입한다.
  • 코드 라인 8~10에서 xarray로 관리되는 swap 캐시 영역에 추가할 페이지는 PG_locked, PG_swapbacked 플래그 설정이 반드시 있어야 하고, PG_swapcache 플래그는 없어야 한다.
  • 코드 라인 12~13에서  swap 캐시 영역에 추가할 페이지의 참조 카운터를 nr 만큼 증가시키고, PG_swapcache 플래그를 설정한다.
    • thp swap이 지원되는 경우 head 페이지의 참조 카운터를 nr 만큼 증가시킨다.
  • 코드 라인 15~19에서 idx 부터 nr 페이지 수 범위의 xarray가 생성되도록 미리 준비한다.
  • 코드 라인 20~25에서 페이지 수 만큼 순회하며 p->private에 swap 엔트리를 저장하고,  페이지를 xarray에 저장한다.
  • 코드 라인 26~27에서 nr 페이지 수만큼 다음 카운터들을 증가시킨다.
    • address_space가 관리하는 전체 페이지 수
    • NR_FILE_PAGES 카운터
    • swap_cache_info->add_total 카운터
  • 코드 라인 29~31에서 unlock: 레이블이다. xa_node 할당 실패 시 다시 할당하고 반복한다.
  • 코드 라인 33~34에서 할당이 성공한 경우 0을 반환한다.
  • 코드 라인 36~38에서 할당이 실패한 경우 PG_swapcache를 클리어하고, 참조 카운터를 nr 만큼 다시 내린 후 에러 코드를 반환한다.

 


Swap-in

다음과 같은 순서로 swap된 페이지가 복구된다.

  • 새 페이지 할당 → swapcache로 변경 → swap 영역(파일/파티션)에서 읽기 → 매핑

 

다음 그림은 swap-in 과정을 보여준다.

 

swapin_readahead()

mm/swap_state.c

/**
 * swapin_readahead - swap in pages in hope we need them soon
 * @entry: swap entry of this memory
 * @gfp_mask: memory allocation flags
 * @vmf: fault information
 *
 * Returns the struct page for entry and addr, after queueing swapin.
 *
 * It's a main entry function for swap readahead. By the configuration,
 * it will read ahead blocks by cluster-based(ie, physical disk based)
 * or vma-based(ie, virtual address based on faulty address) readahead.
 */
struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
                                struct vm_fault *vmf)
{
        return swap_use_vma_readahead() ?
                        swap_vma_readahead(entry, gfp_mask, vmf) :
                        swap_cluster_readahead(entry, gfp_mask, vmf);
}

swap 엔트리에 대한 swapin을 수행하여 페이지를 읽어들인다.

  • “/sys/kernel/mm/swap/vma_ra_enabled” 속성의 사용 시 vma based readahead를 사용하고, 그렇지 않은 경우 ssd 등에서 사용하는 클러스터 기반의 readahead 방식을 사용한다.

 


VMA 기반 readhead 방식으로 swap-in

vma 내에서 fault된 swap 페이지를 위해 fault된 가상 주소 그 전후로 산출된 readahead 페이지 수 만큼 swap-in을 하는 방식이다. (SSD only)

  • readahead할 페이지들은 vma 경계 또는 pte 테이블 한 개의 범위를 초과할 수 없다.

 

swap_vma_readahead()

mm/swap_state.c

static struct page *swap_vma_readahead(swp_entry_t fentry, gfp_t gfp_mask,
                                       struct vm_fault *vmf)
{
        struct blk_plug plug;
        struct vm_area_struct *vma = vmf->vma;
        struct page *page;
        pte_t *pte, pentry;
        swp_entry_t entry;
        unsigned int i;
        bool page_allocated;
        struct vma_swap_readahead ra_info = {0,};

        swap_ra_info(vmf, &ra_info);
        if (ra_info.win == 1)
                goto skip;

        blk_start_plug(&plug);
        for (i = 0, pte = ra_info.ptes; i < ra_info.nr_pte;
             i++, pte++) {
                pentry = *pte;
                if (pte_none(pentry))
                        continue;
                if (pte_present(pentry))
                        continue;
                entry = pte_to_swp_entry(pentry);
                if (unlikely(non_swap_entry(entry)))
                        continue;
                page = __read_swap_cache_async(entry, gfp_mask, vma,
                                               vmf->address, &page_allocated);
                if (!page)
                        continue;
                if (page_allocated) {
                        swap_readpage(page, false);
                        if (i != ra_info.offset) {
                                SetPageReadahead(page);
                                count_vm_event(SWAP_RA);
                        }
                }
                put_page(page);
        }
        blk_finish_plug(&plug);
        lru_add_drain();
skip:
        return read_swap_cache_async(fentry, gfp_mask, vma, vmf->address,
                                     ra_info.win == 1);
}

swap 엔트리에 대해 vma 기반 swap readahead를 수행한다.

  • 코드 라인 11~15에서 swap용 readahead 정보를 구성한다. 만일 swapin할 페이지가 최소 값 1이면 곧바로 skip 레이블로 이동한다.
  • 코드 라인 17에서 blk_plug를 초기화하고, blk_finish_plug()가 끝나기 전까지 블럭 디바이스에 submit 을 유보하게 한다.
  • 코드 라인 18~27에서 pte 엔트리 수 만큼 순회하며 swap 엔트리 정보가 기록된 pte가 아니면 스킵한다.
  • 코드 라인 28~31에서 swap 캐시 영역에서 페이지를 찾아온다.
  • 코드 라인 32~38에서 새로 할당된 페이지이면 swap 영역으로 부터 비동기로 bio 요청을 하여 페이지를 읽어온다. 그리고 할당한 페이지가 요청한 offset 페이지가 아니면 PG_reclaim(swap-in시 readahead 기능) 플래그를 설정하고, SWAP_RA 카운터를 증가시킨다.
  • 코드 라인 41에서 blk_start_plug() 함수와 짝이되는 함수를 통해 블럭 디바이스의 submit 실행을 지금부터 가능하게 한다.
  • 코드 라인 42에서 per-cpu lru 캐시들을 lru로 되돌린다.
  • 코드 라인 43~45에서 skip: 레이블이다. 다시 한 번 swap 캐시 영역에서 페이지를 찾아온다. 단 한 페이지만(win=1) 처리할 때 swap 캐시에서 싱크 모드로 페이지를 읽어온다.

 


swapin 시 readahed를 위한 페이지 수 산출

swapin_nr_pages()

mm/swap_state.c

static unsigned long swapin_nr_pages(unsigned long offset)
{
        static unsigned long prev_offset;
        unsigned int hits, pages, max_pages;
        static atomic_t last_readahead_pages;

        max_pages = 1 << READ_ONCE(page_cluster);
        if (max_pages <= 1)
                return 1;

        hits = atomic_xchg(&swapin_readahead_hits, 0);
        pages = __swapin_nr_pages(prev_offset, offset, hits, max_pages,
                                  atomic_read(&last_readahead_pages));
        if (!hits)
                prev_offset = offset;
        atomic_set(&last_readahead_pages, pages);

        return pages;
}

@offset 페이지에 대해 swapin 시 readahead할 페이지 수를 산출한다.

  • 코드 라인 3~5에서 최근 readahead 산출 시 사용했던 offset 값이 prev_offset에, 그리고 최근 산출된 readahead 페이지 수가 last_readahead_pages에 저장되어 있다.
  • 코드 라인 7~9에서 최대 페이지 제한으로 1 << page_cluster 값을 지정한다. 만일 그 값이 1이면 추가 산출할 필요 없이 가장 작은 수인 1을 반환한다.
  • 코드 라인 11에서 swap-in시 readahead 히트 페이지 수를 알아온다.
  • 코드 라인 12~13에서 최근 offset(prev_offset), @offset, readahead 히트 페이지(hits), 최대 페이지 수 제한(@max_pages) 및 최근 산출되었었던 readahead 페이지 수(last_readahead_pages) 값을 사용하여 적절한 readahead 페이지 수를 산출한다.
  • 코드 라인 14~15에서 readahead 히트 페이지 수가 0인 경우 offset을 prev_offset에 기억해둔다.
  • 코드 라인 16~18에서 산출한 readahead 페이지 수를 last_readahead_pages에 기억하고 반환한다.

 

__swapin_nr_pages()

mm/swap_state.c

static unsigned int __swapin_nr_pages(unsigned long prev_offset,
                                      unsigned long offset,
                                      int hits,
                                      int max_pages,
                                      int prev_win)
{
        unsigned int pages, last_ra;

        /*
         * This heuristic has been found to work well on both sequential and
         * random loads, swapping to hard disk or to SSD: please don't ask
         * what the "+ 2" means, it just happens to work well, that's all.
         */
        pages = hits + 2;
        if (pages == 2) {
                /*
                 * We can have no readahead hits to judge by: but must not get
                 * stuck here forever, so check for an adjacent offset instead
                 * (and don't even bother to check whether swap type is same).
                 */
                if (offset != prev_offset + 1 && offset != prev_offset - 1)
                        pages = 1;
        } else {
                unsigned int roundup = 4;
                while (roundup < pages)
                        roundup <<= 1;
                pages = roundup;
        }

        if (pages > max_pages)
                pages = max_pages;

        /* Don't shrink readahead too fast */
        last_ra = prev_win / 2;
        if (pages < last_ra)
                pages = last_ra;

        return pages;
}

최근 offset(@prev_offset), @offset, readahead 히트페이지(@hits), 최대 페이지 수 제한(@max_pages) 및 최근 결정된 readahead 페이지 수(@prev_win) 값을 사용하여 적절한 readahead 페이지 수를 산출하여 반환한다.

  • 코드 라인 14~22에서 최근 readahead 히트 페이지가 없는 경우 2개 페이지로 지정한다. 단 offset이 최근 offset과 +- 1 차이를 벗어나면 1개 페이지로 지정한다.
  • 코드 라인 23~28에서 최근 readahead 히트 페이지가 존재하는 경우 4부터 시작하여 두 배씩 증가하는 값(4, 8, 16, …) 중 하나로 결정하는데 증가 값이 (히트 페이지 + 2)를 초과한 수 증 작은 값이어야 한다.
    • 예) hits=10
      • pages = 16
  • 코드 라인 30~31에서 산출한 페이지 수가 최대 페이지 수를 초과하지 않도록 제한한다.
  • 코드 라인 34~36에서 산출한 페이지 수가 급격히 작아지지 않도록, 최근 readahead한 페이지의 절반 이하로 내려가지 않도록 제한한다.
  • 코드 라인 38에서 산출한 readahead 페이지 수를 반환한다.

 


swap 캐시에서 페이지를 읽어오기

read_swap_cache_async()

mm/swap_state.c

/*
 * Locate a page of swap in physical memory, reserving swap cache space
 * and reading the disk if it is not already cached.
 * A failure return means that either the page allocation failed or that
 * the swap entry is no longer in use.
 */
struct page *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
                struct vm_area_struct *vma, unsigned long addr, bool do_poll)
{
        bool page_was_allocated;
        struct page *retpage = __read_swap_cache_async(entry, gfp_mask,
                        vma, addr, &page_was_allocated);

        if (page_was_allocated)
                swap_readpage(retpage, do_poll);

        return retpage;
}

swap 캐시 영역에서 swap 엔트리에 해당하는 페이지를 읽어온다. swap 캐시에서 찾을 수 없으면 swap 캐시를 할당하여 등록한 후 swap 영역에서 블럭 디바이스를 비동기로 읽어오도록 요청한다. 만일 @do_poll을 true로 요청한 경우 swap 캐시로 읽어올 때까지 기다린다.(sync)

  • 코드 라인 5~6에서 swap 캐시 영역에서 페이지를 찾아온다. 만일 swap 캐시에서 발견할 수 없으면 swap 영역에서 읽어올 때 필요한 새 swap 캐시 페이지를 미리 준비해둔다. 이러한 경우 page_was_allocated 값에 true가 담겨온다.
  • 코드 라인 8~9에서 새 swap 캐시 페이지가 할당된 경우 swap 영역에서 읽어오도록 bio 요청을 한다.
  • 코드 라인 11에서 읽어온 페이지를 반환한다.

 

__read_swap_cache_async()

mm/swap_state.c

struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
                        struct vm_area_struct *vma, unsigned long addr,
                        bool *new_page_allocated)
{
        struct page *found_page, *new_page = NULL;
        struct address_space *swapper_space = swap_address_space(entry);
        int err;
        *new_page_allocated = false;

        do {
                /*
                 * First check the swap cache.  Since this is normally
                 * called after lookup_swap_cache() failed, re-calling
                 * that would confuse statistics.
                 */
                found_page = find_get_page(swapper_space, swp_offset(entry));
                if (found_page)
                        break;

                /*
                 * Just skip read ahead for unused swap slot.
                 * During swap_off when swap_slot_cache is disabled,
                 * we have to handle the race between putting
                 * swap entry in swap cache and marking swap slot
                 * as SWAP_HAS_CACHE.  That's done in later part of code or
                 * else swap_off will be aborted if we return NULL.
                 */
                if (!__swp_swapcount(entry) && swap_slot_cache_enabled)
                        break;

                /*
                 * Get a new page to read into from swap.
                 */
                if (!new_page) {
                        new_page = alloc_page_vma(gfp_mask, vma, addr);
                        if (!new_page)
                                break;          /* Out of memory */
                }

                /*
                 * Swap entry may have been freed since our caller observed it.
                 */
                err = swapcache_prepare(entry);
                if (err == -EEXIST) {
                        /*
                         * We might race against get_swap_page() and stumble
                         * across a SWAP_HAS_CACHE swap_map entry whose page
                         * has not been brought into the swapcache yet.
                         */
                        cond_resched();
                        continue;
                } else if (err)         /* swp entry is obsolete ? */
                        break;

                /* May fail (-ENOMEM) if XArray node allocation failed. */
                __SetPageLocked(new_page);
                __SetPageSwapBacked(new_page);
                err = add_to_swap_cache(new_page, entry, gfp_mask & GFP_KERNEL);
                if (likely(!err)) {
                        /* Initiate read into locked page */
                        SetPageWorkingset(new_page);
                        lru_cache_add_anon(new_page);
                        *new_page_allocated = true;
                        return new_page;
                }
                __ClearPageLocked(new_page);
                /*
                 * add_to_swap_cache() doesn't return -EEXIST, so we can safely
                 * clear SWAP_HAS_CACHE flag.
                 */
                put_swap_page(new_page, entry);
        } while (err != -ENOMEM);

        if (new_page)
                put_page(new_page);
        return found_page;
}

swap 캐시 영역에서 페이지를 찾아온다. 만일 swap 캐시에서 발견할 수 없으면 swap 영역에서 읽어오기 위해 새 swap 캐시를 할당하여 준비한 후 반환한다. 새 swap 캐시를 준비한 경우 출력 인자 @new_page_allocated에 true를 저장한다.

  • 코드 라인 16~18에서 swap 캐시 영역에서 swap 엔트리의 offset을 사용하여 swap 캐시 페이지를 찾아온다.
  • 코드 라인 28~29에서 swapoff되어 더 이상 swap 엔트리가 유효하지 않은 경우 null 페이지를 반환하러 루프를 벗어난다.
  • 코드 라인 34~38에서 swap 영역에서 읽어올 페이지 데이터를 저장하기 위해 새 swap 캐시 페이지를 할당한다.
  • 코드 라인 43~65에서 추가할 새 swap 캐시 페이지의 PG_locked, PG_swapbacked 플래그를 먼저 설정하고 swap 캐시에 추가한다. 추가가 완료하면 새 swap 캐시 페이지를 반환한다.
  • 코드 라인 66~72에서 swap 캐시에 추가가 실패하는 경우 새 swap 캐시 페이지의 참조 카운터를 감소시키고 메모리가 부족하지 않는 한 다시 반복한다.
  • 코드 라인 75에서 swap 캐시에서 찾은 페이지를 반환한다.

 

swap 영역에서 읽어 swap 캐시에 저장하기

swap_readpage()

mm/page_io.c

int swap_readpage(struct page *page, bool synchronous)
{
        struct bio *bio;
        int ret = 0;
        struct swap_info_struct *sis = page_swap_info(page);
        blk_qc_t qc;
        struct gendisk *disk;

        VM_BUG_ON_PAGE(!PageSwapCache(page) && !synchronous, page);
        VM_BUG_ON_PAGE(!PageLocked(page), page);
        VM_BUG_ON_PAGE(PageUptodate(page), page);
        if (frontswap_load(page) == 0) {
                SetPageUptodate(page);
                unlock_page(page);
                goto out;
        }

        if (sis->flags & SWP_FS) {
                struct file *swap_file = sis->swap_file;
                struct address_space *mapping = swap_file->f_mapping;

                ret = mapping->a_ops->readpage(swap_file, page);
                if (!ret)
                        count_vm_event(PSWPIN);
                return ret;
        }

        ret = bdev_read_page(sis->bdev, swap_page_sector(page), page);
        if (!ret) {
                if (trylock_page(page)) {
                        swap_slot_free_notify(page);
                        unlock_page(page);
                }

                count_vm_event(PSWPIN);
                return 0;
        }

        ret = 0;
        bio = get_swap_bio(GFP_KERNEL, page, end_swap_bio_read);
        if (bio == NULL) {
                unlock_page(page);
                ret = -ENOMEM;
                goto out;
        }
        disk = bio->bi_disk;
        /*
         * Keep this task valid during swap readpage because the oom killer may
         * attempt to access it in the page fault retry time check.
         */
        get_task_struct(current);
        bio->bi_private = current;
        bio_set_op_attrs(bio, REQ_OP_READ, 0);
        if (synchronous)
                bio->bi_opf |= REQ_HIPRI;
        count_vm_event(PSWPIN);
        bio_get(bio);
        qc = submit_bio(bio);
        while (synchronous) {
                set_current_state(TASK_UNINTERRUPTIBLE);
                if (!READ_ONCE(bio->bi_private))
                        break;

                if (!blk_poll(disk->queue, qc, true))
                        io_schedule();
        }
        __set_current_state(TASK_RUNNING);
        bio_put(bio);

out:
        return ret;
}

swap 영역에서 읽어온 데이터를 swap 캐시 페이지에 저장하도록 bio 요청 한다. 요청 시 @synchronous가 1인 경우 동기 요청한다. 결과는 성공한 경우 0을 반환한다.

  • 코드 라인 5에서 swap 캐시 페이지의 private 멤버에 저장된 swap 엔트리 정보로 swap_info_struct 정보를 알아온다.
  • 코드 라인 9~11에서 페이지에 PG_swapcache, PG_locked 플래그가 반드시 설정되어 있어야 하고, PG_uptodate는 클리어된 상태여야 한다.
  • 코드 라인 12~16에서 front swap을 지원하는 경우 front swap 로드 후 PG_uptodate를 설정하고 성공을 반환한다.
  • 코드 라인 18~26에서 파일 시스템을 통해 사용되는 swap 영역인 경우 해당 드라이버의 (*readpage) 후크 함수를 통해 페이지를 읽어오고 PSWPIN 카운터를 증가시키고 결과를 반환한다.
  • 코드 라인 28~37에서 블럭 디바이스를 통해 사용되는 swap 영역인 경우 블럭 디바이스를 통해 페이지를 읽어오고 PSWPIN을 증가시키고 결과를 반환한다.
  • 코드 라인 40~56에서 bio를 통해 swap 영역을 읽어오도록 요청 준비를 한 PSWPIN 카운터를 증가시킨다.
  • 코드 라인 57~68에서 bio를 통해 요청을 한다. 그런 후 @synchronous가 설정된 경우 완료될 때까지 대기한다.
  • 코드 라인 70~71에서 out: 레이블에서 곧바로 결과를 반환한다.

 


VMA 기반 Swap readahead 정보 구성

 

다음 그림은 vma 기반 swap readahead 과정을 보여준다.

 

vma_swap_readahead 구조체

include/linux/swap.h

struct vma_swap_readahead {
        unsigned short win;
        unsigned short offset;
        unsigned short nr_pte;
#ifdef CONFIG_64BIT
        pte_t *ptes;
#else
        pte_t ptes[SWAP_RA_PTE_CACHE_SIZE];
#endif
};

pmd 사이즈 범위내에서, 즉 1개의 pte 페이지 테이블내에서 pte

  •  win
    • swapin 시 readahead할 산출된 페이지 수
  • offset
    • fault pfn을  기준으로 readahead할 시작 pfn offset
  • nr_pte
    • swapin 시 readahead할 pte 엔트리 수 (vma 및 pmd 단위 경계로 win 값과 다를 수 있다)
  • *ptes
    • fault 페이지 pte + offset에 해당하는 pte 주소

 

swap_ra_info()

mm/swap_state.c

static void swap_ra_info(struct vm_fault *vmf,
                        struct vma_swap_readahead *ra_info)
{
        struct vm_area_struct *vma = vmf->vma;
        unsigned long ra_val;
        swp_entry_t entry;
        unsigned long faddr, pfn, fpfn;
        unsigned long start, end;
        pte_t *pte, *orig_pte;
        unsigned int max_win, hits, prev_win, win, left;
#ifndef CONFIG_64BIT
        pte_t *tpte;
#endif

        max_win = 1 << min_t(unsigned int, READ_ONCE(page_cluster),
                             SWAP_RA_ORDER_CEILING);
        if (max_win == 1) {
                ra_info->win = 1;
                return;
        }

        faddr = vmf->address;
        orig_pte = pte = pte_offset_map(vmf->pmd, faddr);
        entry = pte_to_swp_entry(*pte);
        if ((unlikely(non_swap_entry(entry)))) {
                pte_unmap(orig_pte);
                return;
        }

        fpfn = PFN_DOWN(faddr);
        ra_val = GET_SWAP_RA_VAL(vma);
        pfn = PFN_DOWN(SWAP_RA_ADDR(ra_val));
        prev_win = SWAP_RA_WIN(ra_val);
        hits = SWAP_RA_HITS(ra_val);
        ra_info->win = win = __swapin_nr_pages(pfn, fpfn, hits,
                                               max_win, prev_win);
        atomic_long_set(&vma->swap_readahead_info,
                        SWAP_RA_VAL(faddr, win, 0));

        if (win == 1) {
                pte_unmap(orig_pte);
                return;
        }

        /* Copy the PTEs because the page table may be unmapped */
        if (fpfn == pfn + 1)
                swap_ra_clamp_pfn(vma, faddr, fpfn, fpfn + win, &start, &end);
        else if (pfn == fpfn + 1)
                swap_ra_clamp_pfn(vma, faddr, fpfn - win + 1, fpfn + 1,
                                  &start, &end);
        else {
                left = (win - 1) / 2;
                swap_ra_clamp_pfn(vma, faddr, fpfn - left, fpfn + win - left,
                                  &start, &end);
        }
        ra_info->nr_pte = end - start;
        ra_info->offset = fpfn - start;
        pte -= ra_info->offset;
#ifdef CONFIG_64BIT
        ra_info->ptes = pte;
#else
        tpte = ra_info->ptes;
        for (pfn = start; pfn != end; pfn++)
                *tpte++ = *pte++;
#endif
        pte_unmap(orig_pte);
}

swap용 readahead 정보를 구성한다.

  • 코드 라인 4에서 폴트 핸들러가 전달해준 vmf의 vma를 활용한다.
  • 코드 라인 15~20에서 최대 readahead 페이지 수를 구한 후 max_win에 대입한다. 이 값이 1일 때 추가 산출할 필요 없으므로 ra_info->win에 1을 대입하고 함수를 빠져나간다.
    • 1 << page_cluster와 SWAP_RA_ORDER_CEILING 중 작은 수
      • page_cluster의 초기 값은 2~3(메모리가 16M 이하인 경우 2이고, 그 외의 경우 3)이고, “/proc/sys/vm/page-cluster” 값을 통해 조정된다.
      • SWAP_RA_ORDER_CEILING 값은 64비트 시스템에서 5이고, 32비트 시스템에서 3이다.
      • arm64 디폴트 설정에서 max_win 값은 2^3=8이다.
  • 코드 라인 22~28에서 fault 주소로 페이지 테이블에서 orig_pte 값을 알아오고, 이 값으로 swap 엔트리를 구한다. swap 엔트리가 아닌 경우 pte를 언맵하고 함수를 빠져나간다.
  • 코드 라인 30에서 fault 주소에 해당하는 pfn 값을 구한다.
  • 코드 라인 31~34에서 vma에 지정된 ra_val 값을 알아오고, 이 값에서 pfn, prev_win과 hits 값을 알아온다.
    • ra_val 값에는 세 가지 값이 담겨 있다.
      • PAGE_SHIFT 비트 수를 초과하는 비트들에 pfn  값을 담는다.
      • PAGE_SHIFT 비트 수의 상위 절반에 win 값을 담는다.
      • PAGE_SHIFT 비트 수의 하위 절반에 hits 값을 담는다.
    • 예) ra_val=0b111_101010_100001
      • SWAP_RA_ADDR(ra_val)=0b111_000000_000000
        • PFN_DOWN() 하면 0b111
      • SWAP_RA_WIN(ra_val)=0b101010
      • SWAP_RA_HITS(ra_val)=0b100001
    • 지정되지 않은 경우 prev_win=0, hists=4부터 시작한다.
  • 코드 라인 35~36에서 pfn, 폴트 pfn, hits, max_win, prev_win 값을 사용하여 readahead할 페이지 수를 산출한다.
  • 코드 라인 37~38에서 vma->swap_readahead_info 값에 faddr, win, hits=0 값을 사용하여 ra_val 값을 만들어 저장한다.
  • 코드 라인 40~43에서 만일 win 값이 1인 경우 pte 수와 ptes를 수정할 필요 없으므로 기존 pte 매핑을 언맵하고 함수를 빠져나간다.
  • 코드 라인 46~55에서 다음 세 가지 조건으로 시작과 끝 pfn을 산출한다.
    • 지난번에 사용한 페이지 다음에서 fault가 발생한 경우
    • 지난번에 사용한 페이지 이전에서 fault가 발생한 경우
    • 그 외의 경우
  • 코드 라인 56~65에서 출력 인자인 @ra_info에 pte 정보들을 대입한다.
  • 코드 라인 66에서 오리지널 pte는 매핑 해제한다.

 

다음 그림은 지난 번에 사용했던 페이지와 fault 페이지의 위치에 따라 swap 캐시에 읽어올 readahead 페이지 수를 산출하는 과정을 보여준다.

 

ra_val 값 관련 매크로

mm/swap_state.c

#define SWAP_RA_WIN_SHIFT       (PAGE_SHIFT / 2)
#define SWAP_RA_HITS_MASK       ((1UL << SWAP_RA_WIN_SHIFT) - 1)
#define SWAP_RA_HITS_MAX        SWAP_RA_HITS_MASK
#define SWAP_RA_WIN_MASK        (~PAGE_MASK & ~SWAP_RA_HITS_MASK)

#define SWAP_RA_HITS(v)         ((v) & SWAP_RA_HITS_MASK)
#define SWAP_RA_WIN(v)          (((v) & SWAP_RA_WIN_MASK) >> SWAP_RA_WIN_SHIFT)
#define SWAP_RA_ADDR(v)         ((v) & PAGE_MASK)

#define SWAP_RA_VAL(addr, win, hits)                            \
        (((addr) & PAGE_MASK) |                                 \
         (((win) << SWAP_RA_WIN_SHIFT) & SWAP_RA_WIN_MASK) |    \
         ((hits) & SWAP_RA_HITS_MASK))

 

다음 그림은 swap_ra 값에서 addr, win, hits 값을 가져오는 세 매크로들을 보여준다.

 

다음 그림은 SWAP_RA_VAL() 매크로를 사용하여 addr, win, hits 인자로 swap_ra 값을 만드는 과정을 보여준다.

 

swap_ra_clamp_pfn()

mm/swap_state.c

static inline void swap_ra_clamp_pfn(struct vm_area_struct *vma,
                                     unsigned long faddr,
                                     unsigned long lpfn,
                                     unsigned long rpfn,
                                     unsigned long *start,
                                     unsigned long *end)
{
        *start = max3(lpfn, PFN_DOWN(vma->vm_start),
                      PFN_DOWN(faddr & PMD_MASK));
        *end = min3(rpfn, PFN_DOWN(vma->vm_end),
                    PFN_DOWN((faddr & PMD_MASK) + PMD_SIZE));
}

 

다음 그림은 한 개의 pte 페이지 테이블에서 가져올 pte 엔트리들에 대한 시작 주소와 끝 주소를 알아내는 과정을 보여준다.

 


클러스터 기반 readhead 방식으로 swap-in

swap_cluster_readahead()

mm/swap_state.c

/**
 * swap_cluster_readahead - swap in pages in hope we need them soon
 * @entry: swap entry of this memory
 * @gfp_mask: memory allocation flags
 * @vmf: fault information
 *
 * Returns the struct page for entry and addr, after queueing swapin.
 *
 * Primitive swap readahead code. We simply read an aligned block of
 * (1 << page_cluster) entries in the swap area. This method is chosen
 * because it doesn't cost us any seek time.  We also make sure to queue
 * the 'original' request together with the readahead ones...
 *
 * This has been extended to use the NUMA policies from the mm triggering
 * the readahead.
 *
 * Caller must hold down_read on the vma->vm_mm if vmf->vma is not NULL.
 */
struct page *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
                                struct vm_fault *vmf)
{
        struct page *page;
        unsigned long entry_offset = swp_offset(entry);
        unsigned long offset = entry_offset;
        unsigned long start_offset, end_offset;
        unsigned long mask;
        struct swap_info_struct *si = swp_swap_info(entry);
        struct blk_plug plug;
        bool do_poll = true, page_allocated;
        struct vm_area_struct *vma = vmf->vma;
        unsigned long addr = vmf->address;

        mask = swapin_nr_pages(offset) - 1;
        if (!mask)
                goto skip;

        do_poll = false;
        /* Read a page_cluster sized and aligned cluster around offset. */
        start_offset = offset & ~mask;
        end_offset = offset | mask;
        if (!start_offset)      /* First page is swap header. */
                start_offset++;
        if (end_offset >= si->max)
                end_offset = si->max - 1;

        blk_start_plug(&plug);
        for (offset = start_offset; offset <= end_offset ; offset++) {
                /* Ok, do the async read-ahead now */
                page = __read_swap_cache_async(
                        swp_entry(swp_type(entry), offset),
                        gfp_mask, vma, addr, &page_allocated);
                if (!page)
                        continue;
                if (page_allocated) {
                        swap_readpage(page, false);
                        if (offset != entry_offset) {
                                SetPageReadahead(page);
                                count_vm_event(SWAP_RA);
                        }
                }
                put_page(page);
        }
        blk_finish_plug(&plug);

        lru_add_drain();        /* Push any new pages onto the LRU now */
skip:
        return read_swap_cache_async(entry, gfp_mask, vma, addr, do_poll);
}

swap 엔트리에 대해 클러스터 기반 swap readahead를 수행한다.

  • 코드 라인 9에서 swap 엔트리로 swap 정보를 알아온다.
  • 코드 라인 12에서 폴트 핸들러가 전달해준 vmf의 vma를 활용한다.
  • 코드 라인 15~22에서 fault pnf에 대한 상대 offset pfn이 시작 pfn인데 이 값으로 swapin할 페이지 수 단위로 align한 시작 offset과 끝 offset을 구한다.
    • 예) offset=0x3, mask=0xf
      • start_offset=0x0
      • end_offset=0xf
    • 예) offset=0xffff_ffff_ffff_fffd(-3), mask=0xf
      • start_offset=0xffff_ffff_ffff_fff0(-16)
      • end_offset=0xffff_ffff_ffff_ffff(-1)
  • 코드 라인 23~26에서 시작 offset이 0이면 swap 헤더이므로 그 다음을 사용하도록 증가시키고, 끝 offset이 최대값 미만이 되도록 제한한다.
  • 코드 라인 28에서 blk_plug를 초기화하고, blk_finish_plug()가 끝나기 전까지 블럭 디바이스에 submit 을 유보하게 한다.
  • 코드 라인 29~35에서 pte 엔트리 수 만큼 순회하며 swap 캐시 영역에서 swap 엔트리를 사용하여 페이지를 찾아온다.
  • 코드 라인 36~42에서 새로 할당된 페이지이면 swap 영역으로 부터 비동기로 bio 요청을 하여 페이지를 읽어온다. 그리고 할당한 페이지가 요청한 offset 페이지가 아니면 PG_reclaim(swap-in시 readahead 기능) 플래그를 설정하고, SWAP_RA 카운터를 증가시킨다.
  • 코드 라인 45에서 blk_start_plug() 함수와 짝이되는 함수를 통해 블럭 디바이스의 submit 실행을 지금부터 가능하게 한다.
  • 코드 라인 47에서 per-cpu lru 캐시들을 lru로 되돌린다.
  • 코드 라인 48~49에서 skip: 레이블이다. 다시 한 번 swap 캐시 영역에서 페이지를 async 모드로 찾아온다.

 


Swap 캐시 페이지 찾기

fault 핸들러에서 fault된 pte 엔트리 값에 swap 엔트리가 기록되어 있는 경우 do_swap_page() 함수를 호출하여 swap 캐시 영역에서 찾아 anon 페이지로 매핑해주는데 이 때 swap 캐시 영역을 찾는 함수를 알아본다.

 

lookup_swap_cache()

mm/swap_state.c

/*
 * Lookup a swap entry in the swap cache. A found page will be returned
 * unlocked and with its refcount incremented - we rely on the kernel
 * lock getting page table operations atomic even if we drop the page
 * lock before returning.
 */
struct page *lookup_swap_cache(swp_entry_t entry, struct vm_area_struct *vma,
                               unsigned long addr)
{
        struct page *page;

        page = find_get_page(swap_address_space(entry), swp_offset(entry));

        INC_CACHE_INFO(find_total);
        if (page) {
                bool vma_ra = swap_use_vma_readahead();
                bool readahead;

                INC_CACHE_INFO(find_success);
                /*
                 * At the moment, we don't support PG_readahead for anon THP
                 * so let's bail out rather than confusing the readahead stat.
                 */
                if (unlikely(PageTransCompound(page)))
                        return page;

                readahead = TestClearPageReadahead(page);
                if (vma && vma_ra) {
                        unsigned long ra_val;
                        int win, hits;

                        ra_val = GET_SWAP_RA_VAL(vma);
                        win = SWAP_RA_WIN(ra_val);
                        hits = SWAP_RA_HITS(ra_val);
                        if (readahead)
                                hits = min_t(int, hits + 1, SWAP_RA_HITS_MAX);
                        atomic_long_set(&vma->swap_readahead_info,
                                        SWAP_RA_VAL(addr, win, hits));
                }

                if (readahead) {
                        count_vm_event(SWAP_RA_HIT);
                        if (!vma || !vma_ra)
                                atomic_inc(&swapin_readahead_hits);
                }
        }

        return page;
}

swap 엔트리 값으로 swap 캐시 페이지를 찾아온다.

  • 코드 라인 6에서 swap 엔트리에 매핑된 address_space와 swap 엔트리의 offset 값으로 페이지를 찾아온다.
  • 코드 라인 8에서 swap 캐시 stat의 find_total 카운터를 증가시킨다.
  • 코드 라인 9~10에서 캐시된 페이지를 찾은 경우 vma 기반 readahead가 enable 되었는지 여부를 vam_ra에 대입한다.
  • 코드 라인 13에서 swap 캐시 stat의 find_success 카운터를 증가시킨다.
  • 코드 라인 18~19에서 낮은 확률로 thp 또는 hugetlbfs 페이지인 경우 anon THP에 대한 readahead를 지원하지 않아 그냥 해당 페이지를 반환한다.
  • 코드 라인 21에서 페이지의 PG_reclaim(swap-in시 readahed 플래그로 동작) 플래그 값을 readahead에 대입한 후 클리어한다.
  • 코드 라인 22~33에서 vma 기반 readahead를 사용하는 경우 vma에 저장한 ra_val 값을 갱신한다. 이 때 readahead 플래그가 있었던 페이지인 경우 ra_val내의 hits 값은 증가시킨다.
  • 코드 라인 35~39에서 readahead 플래그가 있었던 페이지인 경우 SWAP_RA_HIT 카운터를 증가시킨다. 만일 vma가 지정되지 않았거나 vma 기반 readahead를 사용하지 않는 경우 swapin_readahead_hits 카운터를 증가시킨다.

 


swap operations

 

address_space_operations 구조체

mm/swap_state.c

/*
 * swapper_space is a fiction, retained to simplify the path through
 * vmscan's shrink_page_list.
 */
static const struct address_space_operations swap_aops = {
        .writepage      = swap_writepage,
        .set_page_dirty = swap_set_page_dirty,
#ifdef CONFIG_MIGRATION
        .migratepage    = migrate_page,
#endif
};

 

swap 영역에 기록

anon 페이지를 swap 캐시에 저장한 후 dirty를 설정하고 빠져나온다음 매핑을 해제한다. 그런 후 pageout()을 통한 swap 캐시를 swap 영역에 기록할 수 있다.

 

swap_writepage()

mm/page_io.c

/*
 * We may have stale swap cache pages in memory: notice
 * them here and get rid of the unnecessary final write.
 */
int swap_writepage(struct page *page, struct writeback_control *wbc)
{
        int ret = 0;

        if (try_to_free_swap(page)) {
                unlock_page(page);
                goto out;
        }
        if (frontswap_store(page) == 0) {
                set_page_writeback(page);
                unlock_page(page);
                end_page_writeback(page);
                goto out;
        }
        ret = __swap_writepage(page, wbc, end_swap_bio_write);
out:
        return ret;
}

dirty 상태의 swap 캐시를 swap 영역에 기록한다.

  • 코드 라인 5~8에서 다음의 경우가 아니면 swap 캐시에서 이 페이지를 제거하고 out: 레이블로 이동한다.
    • 이미 swap 캐시에서 제거되었다. (PG_swapcache 플래그가 없는 상태)
    • writeback이 완료되었다. (PG_writeback이 없는 상태)
    • 이미 swap 영역에 저장한 상태이다. (swap_map[]에 비트가 설정된 상태)
  • 코드 라인 9~14에서 frontswap을 지원하는 경우의 처리이다.
  • 코드 라인 15~17에서 페이지를 swap 영역에 기록 요청하고 결과를 반환한다. sync/async 기록이 완료되면 end_swap_bio_write() 함수를 호출하여 writeback이 완료되었음을 페이지 플래그를 변경한다.

 

swap_set_page_dirty()

mm/page_io.c

int swap_set_page_dirty(struct page *page)
{
        struct swap_info_struct *sis = page_swap_info(page);

        if (sis->flags & SWP_FS) {
                struct address_space *mapping = sis->swap_file->f_mapping;

                VM_BUG_ON_PAGE(!PageSwapCache(page), page);
                return mapping->a_ops->set_page_dirty(page);
        } else {
                return __set_page_dirty_no_writeback(page);
        }
}

swap 페이지에 dirty 표식을 한다. 새롭게 dirty로 변경된 경우 1을 반환한다.

  • 코드 라인 5~9에서 SWP_FS가 설정된 swap 영역인 경우 해당 드라이버가 제공하는 set_page_dirty() 함수를 사용하여 dirty 표식을 한다.
    • SWP_FS는 sunrpc, nfs, xfs, btrfs 등에서 사용된다.
  • 코드 라인 10~12에서 그 외의 일반 swap 영역을 사용하는 경우 swap 캐시 페이지에 dirty 설정을 한다.

 

__set_page_dirty_no_writeback()

mm/page-writeback.c

/*
 * For address_spaces which do not use buffers nor write back.
 */
int __set_page_dirty_no_writeback(struct page *page)
{
        if (!PageDirty(page))
                return !TestSetPageDirty(page);
        return 0;
}

해당 페이지에 dirty 플래그를 설정한다. 새롭게 dirty 설정한 경우 1을 반환한다.

 

참고

 

Swap -1- (Basic, 초기화)

<kernel v5.0>

Swap

유저 프로세스에서 사용한 스택 및 anonymous 메모리 할당(malloc 등) 요청 시 커널은 anon 페이지를 할당하여 관리한다. 메모리 부족 시 swap 영역에 저장할 수 있다.

 

swap 매커니즘은 다음과 같이 3단계 구성을 통해 swap 된다.

  • Swap 캐시(swapcache)
  • Frontswap
  • Swap 영역(Swap Backing Store)

 

다음 그림은 3단계 swap 컴포넌트들을 보여준다.

 

clean anon 페이지

swap 가능한 anon 페이지와 다르게 swap 영역을 가지지 않아 swap 할 수 없는 특별한 anon 페이지가 clean anon 페이지이다. 이러한 clean anon 페이지는 swap 할 필요없다.

  • PG_swapbacked 가 없는 anon 페이지가 clean anon 페이지 상태이다.
  • lazy free 페이지로 매핑을 바로 해제 하지 않기 위해 사용되는데, 이들은 madvise API를 통해 사용된다.

 

FRONTSWAP

Back-End에 존재하는 swap 영역과 달리 Front-End에 만든 swap 시스템이다.

  • 여기에 사용되는 swap 장치는 swap할 페이지에 대해 동기 저장할 수 있는 고속이어야 한다.
    • back-end swap 장치는 보통 비동기 저장한다.
  • tmem(Transcendent Memory)을 사용하며 현재 Xen에서 제공된다.
    • DRAM 메모리 또는 persistent 메모리
  • 참고: Frontswap (2012) | Kernel.org

 

 

Transcendent Memory

부족한 RAM 개선을 위한 새로운 메모리 관리 기술중 하나이다.  리눅스 커널에서 동기적으로 접근할 수 있을정도로 빠른 메모리로 직접 주소 지정되지 않고, 간접 주소 지정 방식으로 호출되며, 영역은 가변 사이즈로 구성되는 영구적 또는 일시적 메모리를 의미하는 메모리 클래스이다. 이 메모리에 저장된 데이터는 경고없이 사라질 수 있다.

 

Cleancache

page 캐시 중 clean page 캐시만을 저장할 수 있는 캐시이다. swap 캐시도 swap 영역에 저장된 경우 이 또한 clean page 캐시이므로 이 cleancache를 사용할 수 있다.

  • 현재 리눅스 커널은 xen에서 제공하는 tmem을 사용한다.

 

 

swap 영역(Swap Backing Store)

swap 영역은 다음과 같이 swap 파일 및 swap 디스크(블럭 디바이스)에 지정하여 사용할 수 있다.

  • swap 파일 
  • swap 디스크

 

다음과 같은 방법으로도 swap 장치를 구성할 수 있다.

  • Swap over NFS, sunrpc 등 네트워크를 통해 마운트된 파일 시스템에 위치한 swap 파일
  • 기타 로컬 RAM의 일부를 사용하여 압축하여 저장하는 zram을 사용하여 IO 요청을 줄인다.
    • 디폴트로 cpu를 사용하여 압축하고, 서버 등에서는 HW를 사용한 압축도 가능하다.

 

다음 그림과 같이 swap 영역을 지정하는 다양한 방법이 있다.

 

Swap Priority

다음 그림과 같이 swap 영역은 여러 개를 지정할 수 있다. priority를 부여할 때 높은 번호 priority가 높은 우선 순위로 먼저 사용되고, priority를 부여하지 않은 경우 디폴트로 swapon을 통해 활성화한 순서대로 사용된다.

  • default priority 값은 -2부터 순서대로 낮아진다.
  • swap 영역 1개의 크기는 아키텍처마다 조금씩 다르다.
    • ARM32
      • 실제 가상 주소 공간(최대 3G)이내에서 사용 가능하나 보통 2G까지 사용한다.
    • ARM64
      • 실제 가상 주소 공간(커널 설정마다 다름)이내에서 사용 가능하다.

 

swap 캐시

anon 페이지가 swap 영역으로 나가고(swap-out) 들어올(swap-in) 때 일시적으로 swap 영역의 내용이 RAM에 존재하는 경우이다. 예를 들어 유저 프로세스가 사용하던 anon 페이지가 메모리 부족으로 인해 swap 영역에 저장이 되었다고 가정한다. 이 때 유저 프로세스가 swap된 페이지에 접근을 시도할 때 fault 가 발생하고, fault 핸들러가 swap 된 페이지임을 확인하면 swap-in을 시도한다. 그런 후 swap 캐시가 발견되면 swap 영역에서 로드를 하지 않고 곧바로 swap 캐시를 찾아 anon 페이지로 변경하여 사용한다.

  • swap 캐시 상태는 PG_swapcache 플래그로 표현된다.

 

swap 캐시별로 사용하는 address_space는 xarray(기존 radix tree)로 관리되며 이에 접근하기 위해 사용되는 rock 경합에 대한 개선을 위해 각각의 address_space의 크기를 64M로 제한하였다.

 

XArray 자료 구조 사용

swap 캐시는 기존 커널에서 radix tree를 사용하여 관리하였지만, 커널 v4.20-rc1부터 XArray 자료 구조로 관리하고 있다.

  • address_space->i_pages가 radix tree를 가리켰었는데 지금은 xarray를 가리킨다.

 

다음 그림은 여러 개의 swap 영역을 사용할 때 64M 단위로 관리되는 swap 캐시의 운용되는 모습을 보여준다.

 

Swap 슬롯 캐시

swap 영역의 swap_map[]을 통해 swap 엔트리를 할당하는데, 이 때마다 swap 영역의 락이 필요하다. swap 영역을 빈번하게 접근할 때 이의 성능이 저하되는 것을 개선하기 위해 swap 엔트리 할당을 위해 앞단에 per-cpu swap 슬롯 캐시를 사용하여 swap 영역의 잠금 없이 빠르게 할당/해제할 수 있게 하였다.

 

다음 그림은 anon  페이지를 swap 영역에 저장할 때 swap 엔트리의 빠른 할당 및 회수에  사용되는 각각 64개로 구성된 swap 슬롯 캐시를 보여준다.

 

Swap 엔트리

swap 엔트리는 swap 페이지를 검색할 수 있는 최소한의 정보를 담고 있다.

  • VMA 영역내의 anonymous 페이지가 swap이 되면 swap 엔트리 정보를 구성하여 해당 pte 엔트리에 기록해둔다.
  • ARM에서 swap 엔트리의 구성은 다음과 같다.
    • offset
      • swap 페이지 offset
    • type
      • swap 영역 구분
    • 하위 두 비트=0b00
      • ARM 및 ARM64 아키텍처에서 언매핑 상태로, 이 페이지에 접근하는 경우 fault 에러가 발생한다.
  • 참고: Swap 엔트리 | 문c

 

다음 그림은 유저 프로세스의 가상 주소 공간에 swap 상태의 페이지와 사용 중인 상태의 anonymous 페이지들의 관리 상태를 swap 공정 관점으로 보여준다.

  • 1개의 swap 영역(type=0)의 첫 번째 헤더 페이지를 제외한 그 다음 페이지(offset=1)를 swap 엔트리로 할당한 경우
    • ARM64용 swap 엔트리 값 0x100이 페이지 테이블에 매핑되고, swap 캐시의 xarray에는 엔트리 값을 키로 페이지가 저장된다.

 

Swap 영역 관리

swap 영역은 swap_info_struct의 멤버인 swap_map이 바이트 단위로 관리되어 사용된다. 이 때 각 바이트 값은 1 페이지의 swap 여부에 대응된다. 이 값에는 페이지의 참조 카운터가 기록된다. 이 swap_map은 다음과 같이 두 가지 관리 방식이 사용된다.

  • Legacy swap 맵 관리
    • Legacy HDDswap 파일에서 사용되는 일반적인 방식이다.  swap 페이지의 할당/해제 관리에 사용되는 swap_map을 SMP 시스템에서 사용하기 위해서 swap을 시도하는 cpu가 swap 영역(swap_info_struct)에 대한 spin-lock을 잡고 swap_map의 선두부터  빈 자리를 검색하여 사용하는 방식이다. 이 방식은  swap할 페이지마다 spin-lock을 획득하고 사용하므로 lock contention이 매우 큰 단점이 있다.
  • per-cpu 클러스터 단위 swap 맵 관리

 

다음 그림과 같이 swap 페이지의 swap 여부는 swap_map[] 배열에 각 swap 페이지가 1바이트 정보로 기록되어 관리된다.

 

다음 그림은 per-cpu 클러스터 단위로 swap_map을 관리하는 모습을 보여준다.

 

swap 유틸리티

mkswap

swap 영역을 파일 또는 파티션에 할당하여 생성한다.

 

$ mkswap -h

Usage:
 mkswap [options] device [size]

Set up a Linux swap area.

Options:
 -c, --check               check bad blocks before creating the swap area
 -f, --force               allow swap size area be larger than device
 -p, --pagesize SIZE       specify page size in bytes
 -L, --label LABEL         specify label
 -v, --swapversion NUM     specify swap-space version number
 -U, --uuid UUID           specify the uuid to use
 -V, --version             output version information and exit
 -h, --help                display this help and exit

 

예) 10M(10240K) 사이즈의 /abc 파일명을 생성한 후 swap 파일로 지정한다.

  • $ fallocate –length 10485760 /abc
    • 또는 dd if=/dev/zero of=/abc bs=1M count=10
  • $ mkswap /abc

 

예) hdc3 파티션을 swap 영역으로 생성한다.

  • $ mkswap /dev/hdc3

 

swapon

swap 영역을 파일 또는 파티션에 지정하여 enable 한다.

 

$ swapon -h

Usage:
 swapon [options] [<spec>]

Enable devices and files for paging and swapping.

Options:
 -a, --all                enable all swaps from /etc/fstab
 -d, --discard[=<policy>] enable swap discards, if supported by device
 -e, --ifexists           silently skip devices that do not exist
 -f, --fixpgsz            reinitialize the swap space if necessary
 -o, --options <list>     comma-separated list of swap options
 -p, --priority <prio>    specify the priority of the swap device
 -s, --summary            display summary about used swap devices (DEPRECATED)
     --show[=<columns>]   display summary in definable table
     --noheadings         don't print table heading (with --show)
     --raw                use the raw output format (with --show)
     --bytes              display swap size in bytes in --show output
 -v, --verbose            verbose mode

 -h, --help     display this help and exit
 -V, --version  output version information and exit

The <spec> parameter:
 -L <label>             synonym for LABEL=<label>
 -U <uuid>              synonym for UUID=<uuid>
 LABEL=<label>          specifies device by swap area label
 UUID=<uuid>            specifies device by swap area UUID
 PARTLABEL=<label>      specifies device by partition label
 PARTUUID=<uuid>        specifies device by partition UUID
 <device>               name of device to be used
 <file>                 name of file to be used

Available discard policy types (for --discard):
 once    : only single-time area discards are issued
 pages   : freed pages are discarded before they are reused
If no policy is selected, both discard types are enabled (default).

Available columns (for --show):
 NAME   device file or partition path
 TYPE   type of the device
 SIZE   size of the swap area
 USED   bytes in use
 PRIO   swap priority
 UUID   swap uuid
 LABEL  swap label

For more details see swapon(8).

 

예) /abc 파일명의 swap 파일에서 swap을 활성화한다.

  • $ swapon /abc

 

예) hdc3 파티션의 swap을 활성화한다.

  • $ swapon /dev/hdc3

 

swapoff

swap 영역으로 지정된 파일 또는 파티션의 swap 기능을 disable 한다.

 

$ swapoff -h

Usage:
 swapoff [options] [<spec>]

Disable devices and files for paging and swapping.

Options:
 -a, --all              disable all swaps from /proc/swaps
 -v, --verbose          verbose mode

 -h, --help     display this help and exit
 -V, --version  output version information and exit

The <spec> parameter:
 -L <label>             LABEL of device to be used
 -U <uuid>              UUID of device to be used
 LABEL=<label>          LABEL of device to be used
 UUID=<uuid>            UUID of device to be used
 <device>               name of device to be used
 <file>                 name of file to be used

For more details see swapoff(8).

 

예) /abc 파일명의 swap 파일에서 swap을 비활성화한다.

  • $ swapoff /abc

 

예) hdc3 파티션의 swap을 비활성화한다.

  • $ swapoff /dev/hdc3

 

Swap Extent

swap 영역의 페이지 범위를 swap 장치의 블럭에 매핑할 때 사용하는 자료 구조이다. swap 영역이 블럭 디바이스에 직접 사용된 경우에는 swap 영역의 각 페이지가 블럭 디바이스의 각 블럭에 연속적으로 1:1로 동일하게 사용되므로 하나의 매핑만 필요하다. 그러나 swap 영역으로 swap 파일을 이용하는 경우에는 swap 파일의 각 페이지와 블럭 디바이스의 실제 사용된 블럭이 동일하게 연속하지 않으므로 여러 개의 매핑이 필요하다. 그렇다고 하나의 페이지와 하나의 블럭을 각각 매핑하면 이러한 매핑으로 너무 많이 소모된다. 따라서 범위로 묶어서 매핑을 하는데 이 때 사용하는 구조체가 swap_extent이다.

  • 하나의 swap_extent에는 시작 페이지 번호와 페이지 수 그리고 매핑할 시작 블럭 번호가 포함된다.
  • 리스트로 관리하던 swap extent를 커널 v5.3-rc1에서 rbtree로 변경하였다.

 

다음 그림은 swap 파일이 위치한 블럭 디바이스에 3개의 연속된 블럭들로 나뉘어 배치된 경우에 필요한 3개의 swap_extent를 보여준다.

 


Swapon

swap 영역을 파일 또는 블럭 디바이스에 지정하여 활성화한다.

 

sys_swapon()

mm/swapfile.c -1/3-

SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
{
        struct swap_info_struct *p;
        struct filename *name;
        struct file *swap_file = NULL;
        struct address_space *mapping;
        int prio;
        int error;
        union swap_header *swap_header;
        int nr_extents;
        sector_t span;
        unsigned long maxpages;
        unsigned char *swap_map = NULL;
        struct swap_cluster_info *cluster_info = NULL;
        unsigned long *frontswap_map = NULL;
        struct page *page = NULL;
        struct inode *inode = NULL;
        bool inced_nr_rotate_swap = false;

        if (swap_flags & ~SWAP_FLAGS_VALID)
                return -EINVAL;

        if (!capable(CAP_SYS_ADMIN))
                return -EPERM;

        if (!swap_avail_heads)
                return -ENOMEM;

        p = alloc_swap_info();
        if (IS_ERR(p))
                return PTR_ERR(p);

        INIT_WORK(&p->discard_work, swap_discard_work);

        name = getname(specialfile);
        if (IS_ERR(name)) {
                error = PTR_ERR(name);
                name = NULL;
                goto bad_swap;
        }
        swap_file = file_open_name(name, O_RDWR|O_LARGEFILE, 0);
        if (IS_ERR(swap_file)) {
                error = PTR_ERR(swap_file);
                swap_file = NULL;
                goto bad_swap;
        }

        p->swap_file = swap_file;
        mapping = swap_file->f_mapping;
        inode = mapping->host;

        /* If S_ISREG(inode->i_mode) will do inode_lock(inode); */
        error = claim_swapfile(p, inode);
        if (unlikely(error))
                goto bad_swap;

        /*
         * Read the swap header.
         */
        if (!mapping->a_ops->readpage) {
                error = -EINVAL;
                goto bad_swap;
        }
        page = read_mapping_page(mapping, 0, swap_file);
        if (IS_ERR(page)) {
                error = PTR_ERR(page);
                goto bad_swap;
        }
        swap_header = kmap(page);

        maxpages = read_swap_header(p, swap_header, inode);
        if (unlikely(!maxpages)) {
                error = -EINVAL;
                goto bad_swap;
        }

        /* OK, set up the swap map and apply the bad block list */
        swap_map = vzalloc(maxpages);
        if (!swap_map) {
                error = -ENOMEM;
                goto bad_swap;
        }        

swap용 블럭 디바이스 또는 파일을 @type 번호의 swap 영역에 활성화한다. 성공 시 0을 반환한다.

  • 코드 라인 20~21에서 잘못된 swap 플래그가 지정된 경우 -EINVAL 에러를 반환한다. 허용되는 플래그들은 다음과 같다.
    • SWAP_FLAG_PREFER (0x8000)
    • SWAP_FLAG_PRIO_MASK (0x7fff)
    • SWAP_FLAG_DISCARD (0x10000)
    • SWAP_FLAG_DISCARD_ONCE (0x20000)
    • SWAP_FLAG_DISCARD_PAGES (0x40000
  • 코드 라인 23~24에서 CAP_SYS_ADMIN 권한이 없는 경우 -EPERM 에러를 반환한다.
  • 코드 라인 26~27에서 swap_avail_heads 리스트가 초기화되지 않은 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 29~31에서 swap  영역을 구성하기 위해 swap_info_struct를 할당하고 초기화한다.
  • 코드 라인 33에서 워커 스레드에서 swap_discard_work 함수를 호출할 수 있도록 워크를 초기화한다.
    • SSD를 사용하는 디스크에서 discard 기능을 사용할 수 있다.
  • 코드 라인 35~48에서 swapon할 디바이스 또는 파일을 오픈한 후 swap 정보에 지정한다.
  • 코드 라인 49~55에서 swap파일의 address_space와 inode 정보로 다음과 같이 수행한다.
    • 블럭 디바이스인 경우 swap 정보의 멤버 bdev에 inode를 포함한 블럭 디바이스를 지정한다. 그리고 멤버 flag에 SWP_BLKDEV 플래그를 추가한다.
    • 파일인 경우 swap 정보의 멤버 bdev에 inode->i_sb->s_bdev를 지정한다. 그리고 이 파일이 이미 swapfile로 사용중이면 -EBUSY 에러를 반환한다.
  • 코드 라인 60~63에서 swap 파일 시스템의 (*readpage) 후크가 지정되지 않은 경우 -INVAL 에러를 반환한다.
  • 코드 라인 64~68에서 swap 파일의 헤더를 읽기 위해 인덱스 0에 대한  대한 페이지 캐시를 읽어온다.
  • 코드 라인 69에서 읽어온 페이지를 swap_header로 사용하기 위해 kmap을 사용하여 잠시 매핑해둔다.
    • arm64에서는 highmem을 사용하지 않기 때문에 이미 매핑되어 있다.
  • 코드 라인 71~75에서 swap 헤더를 파싱하여 swap 정보에 그 시작과 끝 위치를 기록하고, 실제 swap 영역에 해당하는 페이지 수를 알아온다.
  • 코드 라인 78~82에서 실제 swap 영역에 해당하는 페이지 수에 해당하는 바이트를 할당하여 swap_map에 할당한다.

 

mm/swapfile.c -2/3-

.       if (bdi_cap_stable_pages_required(inode_to_bdi(inode)))
                p->flags |= SWP_STABLE_WRITES;

        if (bdi_cap_synchronous_io(inode_to_bdi(inode)))
                p->flags |= SWP_SYNCHRONOUS_IO;

        if (p->bdev && blk_queue_nonrot(bdev_get_queue(p->bdev))) {
                int cpu;
                unsigned long ci, nr_cluster;

                p->flags |= SWP_SOLIDSTATE;
                /*
                 * select a random position to start with to help wear leveling
                 * SSD
                 */
                p->cluster_next = 1 + (prandom_u32() % p->highest_bit);
                nr_cluster = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);

                cluster_info = kvcalloc(nr_cluster, sizeof(*cluster_info),
                                        GFP_KERNEL);
                if (!cluster_info) {
                        error = -ENOMEM;
                        goto bad_swap;
                }

                for (ci = 0; ci < nr_cluster; ci++)
                        spin_lock_init(&((cluster_info + ci)->lock));

                p->percpu_cluster = alloc_percpu(struct percpu_cluster);
                if (!p->percpu_cluster) {
                        error = -ENOMEM;
                        goto bad_swap;
                }
                for_each_possible_cpu(cpu) {
                        struct percpu_cluster *cluster;
                        cluster = per_cpu_ptr(p->percpu_cluster, cpu);
                        cluster_set_null(&cluster->index);
                }
        } else {
                atomic_inc(&nr_rotate_swap);
                inced_nr_rotate_swap = true;
        }

        error = swap_cgroup_swapon(p->type, maxpages);
        if (error)
                goto bad_swap;

        nr_extents = setup_swap_map_and_extents(p, swap_header, swap_map,
                cluster_info, maxpages, &span);
        if (unlikely(nr_extents < 0)) {
                error = nr_extents;
                goto bad_swap;
        }
        /* frontswap enabled? set up bit-per-page map for frontswap */
        if (IS_ENABLED(CONFIG_FRONTSWAP))
                frontswap_map = kvcalloc(BITS_TO_LONGS(maxpages),
                                         sizeof(long),
                                         GFP_KERNEL);

        if (p->bdev &&(swap_flags & SWAP_FLAG_DISCARD) && swap_discardable(p)) {
                /*
                 * When discard is enabled for swap with no particular
                 * policy flagged, we set all swap discard flags here in
                 * order to sustain backward compatibility with older
                 * swapon(8) releases.
                 */
                p->flags |= (SWP_DISCARDABLE | SWP_AREA_DISCARD |
                             SWP_PAGE_DISCARD);

                /*
                 * By flagging sys_swapon, a sysadmin can tell us to
                 * either do single-time area discards only, or to just
                 * perform discards for released swap page-clusters.
                 * Now it's time to adjust the p->flags accordingly.
                 */
                if (swap_flags & SWAP_FLAG_DISCARD_ONCE)
                        p->flags &= ~SWP_PAGE_DISCARD;
                else if (swap_flags & SWAP_FLAG_DISCARD_PAGES)
                        p->flags &= ~SWP_AREA_DISCARD;

                /* issue a swapon-time discard if it's still required */
                if (p->flags & SWP_AREA_DISCARD) {
                        int err = discard_swap(p);
                        if (unlikely(err))
                                pr_err("swapon: discard_swap(%p): %d\n",
                                        p, err);
                }
        }
  • 코드 라인 1~2에서 swap 기록을 안정적으로 할 수 있는 장치인 경우 SWP_STABLE_WRITES 플래그를 추가한다.
  • 코드 라인 4~5에서 swap 기록이 빠른 장치(zram, pmem 등)인 경우 비동기로 처리할 필요 없다. 이 때 SWP_SYNCHRONOUS_IO 플래그를 추가한다.
  • 코드 라인 7~11에서 SSD 처럼 non-rotational 블럭 장치인 경우 SWP_SOLIDSTATE 플래그를 추가한다.
  • 코드 라인 16에서 다음 사용할 클러스터 위치를 swap 영역내에서 랜덤하게 선택한다.
  • 코드 라인 17에서 swap 가용 페이지 수를 사용하여 클러스터의 수를 결정한다.
    • 1개의 클러스터는 SWAPFILE_CLUSTER(256) 수 만큼 페이지를 관리한다.
    • 현재 x86_64 아키텍처만 THP_SWAP을 지원하고 이 때 256 페이지 대신 HPAGE_PMD_NR 수 만큼 페이지를 관리한다.
  • 코드 라인 19~27에서 결정된 클러스터 수만큼 cluster_info를  할당하고 초기화한다.
  • 코드 라인 29~38에서 swap 정보의 멤버 percpu_cluster에 per-cpu percpu_cluster 구조체를 할당하여 지정하고 초기화한다.
  • 코드 라인 39~42에서 SSD가 아닌 장치인 경우 nr_rotate_swap을 증가시키고 inced_nr_rotate_swap을 true로 지정한다.
    • nr_rotate_swap 값이 0이 아니면 vma 기반 readahead를 사용하지 않는다.
  • 코드 라인 44~46에서 cgroup용 swapon을 위해 swap_cgroup 배열들을 할당하고 준비한다.
  • 코드 라인 48~53에서 swap 맵과 swap_extent를 할당하고 준비한다.
  • 코드 라인 55~58에서 frontswap을 지원하는 커널인 경우 frontswap용 맵을 할당한다.
    • 맵의 각 비트는 1 페이지에 대응한다.
  • 코드 라인 60~88에서 SWAP_FLAG_DISCARD 요청을 처리한다.

 

mm/swapfile.c -3/3-

        error = init_swap_address_space(p->type, maxpages);
        if (error)
                goto bad_swap;

        mutex_lock(&swapon_mutex);
        prio = -1;
        if (swap_flags & SWAP_FLAG_PREFER)
                prio =
                  (swap_flags & SWAP_FLAG_PRIO_MASK) >> SWAP_FLAG_PRIO_SHIFT;
        enable_swap_info(p, prio, swap_map, cluster_info, frontswap_map);

        pr_info("Adding %uk swap on %s.  Priority:%d extents:%d across:%lluk %s%s%s%s%s\n",
                p->pages<<(PAGE_SHIFT-10), name->name, p->prio,
                nr_extents, (unsigned long long)span<<(PAGE_SHIFT-10),
                (p->flags & SWP_SOLIDSTATE) ? "SS" : "",
                (p->flags & SWP_DISCARDABLE) ? "D" : "",
                (p->flags & SWP_AREA_DISCARD) ? "s" : "",
                (p->flags & SWP_PAGE_DISCARD) ? "c" : "",
                (frontswap_map) ? "FS" : "");

        mutex_unlock(&swapon_mutex);
        atomic_inc(&proc_poll_event);
        wake_up_interruptible(&proc_poll_wait);

        if (S_ISREG(inode->i_mode))
                inode->i_flags |= S_SWAPFILE;
        error = 0;
        goto out;
bad_swap:
        free_percpu(p->percpu_cluster);
        p->percpu_cluster = NULL;
        if (inode && S_ISBLK(inode->i_mode) && p->bdev) {
                set_blocksize(p->bdev, p->old_block_size);
                blkdev_put(p->bdev, FMODE_READ | FMODE_WRITE | FMODE_EXCL);
        }
        destroy_swap_extents(p);
        swap_cgroup_swapoff(p->type);
        spin_lock(&swap_lock);
        p->swap_file = NULL;
        p->flags = 0;
        spin_unlock(&swap_lock);
        vfree(swap_map);
        kvfree(cluster_info);
        kvfree(frontswap_map);
        if (inced_nr_rotate_swap)
                atomic_dec(&nr_rotate_swap);
        if (swap_file) {
                if (inode && S_ISREG(inode->i_mode)) {
                        inode_unlock(inode);
                        inode = NULL;
                }
                filp_close(swap_file, NULL);
        }
out:
        if (page && !IS_ERR(page)) {
                kunmap(page);
                put_page(page);
        }
        if (name)
                putname(name);
        if (inode && S_ISREG(inode->i_mode))
                inode_unlock(inode);
        if (!error)
                enable_swap_slots_cache();
        return error;
}
  • 코드 라인 1~3에서 @type에 대한 swap 영역을 초기화한다.
    • swapper_spaces[type]에 swap 영역 크기를 SWAP_ADDRESS_SPACE_PAGES(2^14=16K pages=64M) 단위 수로 나누어 address_space 배열을 할당하여 준비한다.
  • 코드라인 6~9에서 SWAP_FLAG_PREFER 플래그가 요청된 경우 플래그에 priority 값이 추가되어 있다. 이 경우 priority 값만 분리하여 prio에 대입한다. 그 외의 경우 -1이다.
  • 코드 라인 10~19에서 swap 영역을 활성화하고, 메시지를 춮력한다.
    • “Adding <페이지수> swap on <파일/블럭 디바이스명>. Priority:<prio> extents:<extent 매핑수> across:<span 크기> [SS][D][s][c][FS]”
      • SS: SSD
      • D: discardable
      • s: swap 영역 discard
      • c: swap 페이지 discard
      • FS: FrontSwap 맵
  • 코드 라인 25~26에서 swap 파일을 사용하는 swap 영역인 경우 inode에 S_SWAPFILE 플래그를 추가한다.
  • 코드 라인 27~28에서 에러 없이 성공적으로 처리하려 out 레이블로 이동한다.
  • 코드 라인 29~53에서 bad_swap: 레이블이다. swap 영역을 활성화하지 못하는 경우 할당했었던 메모리를 회수한다.
  • 코드 라인 54~65에서 out: 레이블이다. swap 영역의 초기화가 성공한 경우 swap 슬롯 캐시를 활성화한다.

 

swap_info_struct 할당후 초기화

alloc_swap_info()

mm/swapfile.c

static struct swap_info_struct *alloc_swap_info(void)
{
        struct swap_info_struct *p;
        unsigned int type;
        int i;
        int size = sizeof(*p) + nr_node_ids * sizeof(struct plist_node);

        p = kvzalloc(size, GFP_KERNEL);
        if (!p)
                return ERR_PTR(-ENOMEM);

        spin_lock(&swap_lock);
        for (type = 0; type < nr_swapfiles; type++) {
                if (!(swap_info[type]->flags & SWP_USED))
                        break;
        }
        if (type >= MAX_SWAPFILES) {
                spin_unlock(&swap_lock);
                kvfree(p);
                return ERR_PTR(-EPERM);
        }
        if (type >= nr_swapfiles) {
                p->type = type;
                swap_info[type] = p;
                /*
                 * Write swap_info[type] before nr_swapfiles, in case a
                 * racing procfs swap_start() or swap_next() is reading them.
                 * (We never shrink nr_swapfiles, we never free this entry.)
                 */
                smp_wmb();
                nr_swapfiles++;
        } else {
                kvfree(p);
                p = swap_info[type];
                /*
                 * Do not memset this entry: a racing procfs swap_next()
                 * would be relying on p->type to remain valid.
                 */
        }
        INIT_LIST_HEAD(&p->first_swap_extent.list);
        plist_node_init(&p->list, 0);
        for_each_node(i)
                plist_node_init(&p->avail_lists[i], 0);
        p->flags = SWP_USED;
        spin_unlock(&swap_lock);
        spin_lock_init(&p->lock);
        spin_lock_init(&p->cont_lock);

        return p;
}

swap 영역 정보를 할당한다. (할당한 swap_info_struct 포인터를 반환한다.)

  • 코드 라인 6~10에서 swap_info_struct 구조체와 연결하여 노드 수 만큼의 plist_node 구조체 배열을 할당한다.
  • 코드 라인 13~16에서 swap 파일 수만큼 swap_info[] 배열에 빈 자리가 있는지 찾아본다.
  • 코드 라인 17~21에서 생성한 swap 파일 수가 이미 MAX_SWAPFILES 수 이상인 경우 할당을 취소하고 -EPERM 에러를 반환한다.
  • 코드 라인 22~31에서 swap_info[] 배열에 빈 자리가 없으면 마지막에 할당한 메모리를 지정하고, swap 파일 수를 증가시킨다.
  • 코드 라인 32~39에서 swap_info[] 배열에 빈 자리가 있으면 이미 할당한 메모리는 취소하고, 기존 할당한 메모리를 사용한다.
  • 코드 라인 40~47에서 할당한 swap_info_struct 배열의 관련 멤버들을 초기화하고, SWP_USED 플래그를 설정한다.

 


Swap 헤더

swap_header 구조체

include/linux/swap.h

/*
 * Magic header for a swap area. The first part of the union is
 * what the swap magic looks like for the old (limited to 128MB)
 * swap area format, the second part of the union adds - in the
 * old reserved area - some extra information. Note that the first
 * kilobyte is reserved for boot loader or disk label stuff...
 *
 * Having the magic at the end of the PAGE_SIZE makes detecting swap
 * areas somewhat tricky on machines that support multiple page sizes.
 * For 2.5 we'll probably want to move the magic to just beyond the
 * bootbits...
 */
union swap_header {
        struct {
                char reserved[PAGE_SIZE - 10];
                char magic[10];                 /* SWAP-SPACE or SWAPSPACE2 */
        } magic;
        struct {
                char            bootbits[1024]; /* Space for disklabel etc. */
                __u32           version;
                __u32           last_page;
                __u32           nr_badpages;
                unsigned char   sws_uuid[16];
                unsigned char   sws_volume[16];
                __u32           padding[117];
                __u32           badpages[1];
        } info;
};

 

다음 그림은 swap 파일의 헤더 구성을 보여준다.

read_swap_header()

mm/swapfile.c

static unsigned long read_swap_header(struct swap_info_struct *p,
                                        union swap_header *swap_header,
                                        struct inode *inode)
{
        int i;
        unsigned long maxpages;
        unsigned long swapfilepages;
        unsigned long last_page;

        if (memcmp("SWAPSPACE2", swap_header->magic.magic, 10)) {
                pr_err("Unable to find swap-space signature\n");
                return 0;
        }

        /* swap partition endianess hack... */
        if (swab32(swap_header->info.version) == 1) {
                swab32s(&swap_header->info.version);
                swab32s(&swap_header->info.last_page);
                swab32s(&swap_header->info.nr_badpages);
                if (swap_header->info.nr_badpages > MAX_SWAP_BADPAGES)
                        return 0;
                for (i = 0; i < swap_header->info.nr_badpages; i++)
                        swab32s(&swap_header->info.badpages[i]);
        }
        /* Check the swap header's sub-version */
        if (swap_header->info.version != 1) {
                pr_warn("Unable to handle swap header version %d\n",
                        swap_header->info.version);
                return 0;
        }

        p->lowest_bit  = 1;
        p->cluster_next = 1;
        p->cluster_nr = 0;

        maxpages = max_swapfile_size();
        last_page = swap_header->info.last_page;
        if (!last_page) {
                pr_warn("Empty swap-file\n");
                return 0;
        }
        if (last_page > maxpages) {
                pr_warn("Truncating oversized swap area, only using %luk out of %luk\n",
                        maxpages << (PAGE_SHIFT - 10),
                        last_page << (PAGE_SHIFT - 10));
        }
        if (maxpages > last_page) {
                maxpages = last_page + 1;
                /* p->max is an unsigned int: don't overflow it */
                if ((unsigned int)maxpages == 0)
                        maxpages = UINT_MAX;
        }
        p->highest_bit = maxpages - 1;

        if (!maxpages)
                return 0;
        swapfilepages = i_size_read(inode) >> PAGE_SHIFT;
        if (swapfilepages && maxpages > swapfilepages) {
                pr_warn("Swap area shorter than signature indicates\n");
                return 0;
        }
        if (swap_header->info.nr_badpages && S_ISREG(inode->i_mode))
                return 0;
        if (swap_header->info.nr_badpages > MAX_SWAP_BADPAGES)
                return 0;

        return maxpages;
}

swap 헤더를 파싱하여 swap 정보에 그 시작과 끝 위치를 알아온다.

  • 코드 라인 10~13에서 페이지의 마지막 10바이트에 “SWAPSPACE2” 라는 매직 문자열을 확인하고, 없는 경우 에러 메시지를 출력하고 0을 반환한다.
    • “Unable to find swap-space signature\n”
  • 코드 라인 16~30에서 버전이 1로 확인되는 경우 모든 unsigned int 값들을 바이트 swap 하여 읽는다. 만일 버전이 1이 아닌 경우 다음과 같은 경고 메시지를 출력하고 0을 반환한다.
    • “Unable to handle swap header version %d\n”
  • 코드 라인 32~34에서 클러스터 수를 0으로하고, 다음 클러스터 번호는 1로 지정하여 초기화한다. 그리고 swap 시작(lowestbit) 페이지로 첫 페이지인 1을 지정한다.
  • 코드 라인 36에서 아키텍처가 지원하는 swap offset 페이지 한계를 알아와서 maxpages에 대입한다.
  • 코드 라인 37~41에서 swap 헤더에 기록된 last_page 수가 0인 경우 “Empty swap-file” 메시지를 출력하고 0을 반환한다.
  • 코드 라인 42~46에서 last_page가 maxpages를 초과하는 경우 경고 메시지를 출력한다.
  • 코드 라인 47~52에서 maxpages가 last_page보다 큰 경우 maxpages는 last_page+1을 대입한다.
  • 코드 라인 53에서 swap 끝(highestbit) 페이지로 maxpages-1을 지정한다.
  • 코드 라인 57~61에서 swap 파일 또는 블럭 디바이스의 페이지 수를 알아와서 maxpages보다 작은 경우 다음과 같은 경고 메시지를 출력하고 0을 반환한다.
    • “Swap area shorter than signature indicates\n”
  • 코드 라인 62~63에서 swap 파일의 경우 배드 페이지가 존재하는 경우 0을 반환한다.
  • 코드 라인 64~65에서 배드 페이지 수가 MAX_SWAP_BADPAGES를 초과하는 경우 0을 반환한다.
  • 코드 라인 67에서 maxpages를 반환한다.

 

다음 그림은 swap 파일 또는 블럭 디바이스에서 swap 헤더를 읽고 swap 정보에 시작(lowest_bit)과 끝(highest_bit) 위치를 처음 지정하는 모습을 보여준다.

 


Cgroup용 swap

swap_cgroup_swapon()

mm/swap_cgroup.c

int swap_cgroup_swapon(int type, unsigned long max_pages)
{
        void *array;
        unsigned long array_size;
        unsigned long length;
        struct swap_cgroup_ctrl *ctrl;

        if (!do_swap_account)
                return 0;

        length = DIV_ROUND_UP(max_pages, SC_PER_PAGE);
        array_size = length * sizeof(void *);

        array = vzalloc(array_size);
        if (!array)
                goto nomem;

        ctrl = &swap_cgroup_ctrl[type];
        mutex_lock(&swap_cgroup_mutex);
        ctrl->length = length;
        ctrl->map = array;
        spin_lock_init(&ctrl->lock);
        if (swap_cgroup_prepare(type)) {
                /* memory shortage */
                ctrl->map = NULL;
                ctrl->length = 0;
                mutex_unlock(&swap_cgroup_mutex);
                vfree(array);
                goto nomem;
        }
        mutex_unlock(&swap_cgroup_mutex);

        return 0;
nomem:
        pr_info("couldn't allocate enough memory for swap_cgroup\n");
        pr_info("swap_cgroup can be disabled by swapaccount=0 boot option\n");
        return -ENOMEM;
}

swap cgroup을 할당하고 준비한다. 성공 시 0을 반환한다.

  • 코드 라인 8~9에서 memcg swap을 지원하는 커널이 아니면 함수를 빠져나간다.
    • CONFIG_MEMCG_SWAP & CONFIG_MEMCG_SWAP_ENABLED 커널 옵션을 사용해야 한다.
  • 코드 라인 11~16에서 swap 영역에 필요한 페이지 수만큼 필요한 swap_cgroup 구조체를 수를 산출하고 그 수 만큼 페이지 포인터 배열을 할당한다.
  • 코드 라인 18~22에서 전역 swap_cgroup_ctrl[] 배열에서 @type에 해당하는 swap_cgroup_ctrl에 할당한 페이지 포인터 배열을 연결하고, length에 산출한 swap_cgroup 구조체 수를 담는다.
  • 코드 라인 23~30에서 swap_cgroup_ctrl에 할당하여 준비한 페이지 포인터 배열에 swap_cgroup 구조체 배열용으로 사용할 페이지들을 할당하고 연결한다.
  • 코드 라인 33에서 정상 할당이 완료되면 0을 반환한다.
  • 코드 라인 34~37에서 nomem: 레이블이다. swap cgroup을 할당하기에 메모리가 부족하다고 메시지 출력을 한다. 그리고 메모리 부족 시 커널 옵션으로 “swapaccount=0″을 사용하면 swap cgroup을 disable 할 수 있다고 메시지 출력을 한다.

 

다음 그림은 swap cgroup을 할당하고 준비하는 과정을 보여준다.

 

swap_cgroup_prepare()

mm/swap_cgroup.c

/*
 * allocate buffer for swap_cgroup.
 */
static int swap_cgroup_prepare(int type)
{
        struct page *page;
        struct swap_cgroup_ctrl *ctrl;
        unsigned long idx, max;

        ctrl = &swap_cgroup_ctrl[type];

        for (idx = 0; idx < ctrl->length; idx++) {
                page = alloc_page(GFP_KERNEL | __GFP_ZERO);
                if (!page)
                        goto not_enough_page;
                ctrl->map[idx] = page;

                if (!(idx % SWAP_CLUSTER_MAX))
                        cond_resched();
        }
        return 0;
not_enough_page:
        max = idx;
        for (idx = 0; idx < max; idx++)
                __free_page(ctrl->map[idx]);

        return -ENOMEM;
}

swap_cgroup_ctrl에 할당하여 준비한 각 페이지 포인터 배열에 swap_cgroup 구조체 배열용으로 사용할 페이지들을 할당하고 연결한다.

  • 코드 라인 7에서 @type에 해당하는 swap_cgroup_ctrl을 지정한다.
  • 코드 라인 9~17에서 ctrl->length 만큼 순회하며 swap_cgroup 구조체 배열용 페이지를 할당하여 ctrl->map[idx]에 연결한다.
  • 코드 라인 18에서 성공시 0을 반환한다.
  • 코드 라인 19~24에서 not_enough_page: 레이블이다. 메모리 부족 시 할당한 페이지들을 할당 해제하고 -ENOMEM 에러를 반환한다.

 


swap 맵 초기화

setup_swap_map_and_extents()

mm/swapfile.c

static int setup_swap_map_and_extents(struct swap_info_struct *p,
                                        union swap_header *swap_header,
                                        unsigned char *swap_map,
                                        struct swap_cluster_info *cluster_info,
                                        unsigned long maxpages,
                                        sector_t *span)
{
        unsigned int j, k;
        unsigned int nr_good_pages;
        int nr_extents;
        unsigned long nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
        unsigned long col = p->cluster_next / SWAPFILE_CLUSTER % SWAP_CLUSTER_COLS;
        unsigned long i, idx;

        nr_good_pages = maxpages - 1;   /* omit header page */

        cluster_list_init(&p->free_clusters);
        cluster_list_init(&p->discard_clusters);

        for (i = 0; i < swap_header->info.nr_badpages; i++) {
                unsigned int page_nr = swap_header->info.badpages[i];
                if (page_nr == 0 || page_nr > swap_header->info.last_page)
                        return -EINVAL;
                if (page_nr < maxpages) {
                        swap_map[page_nr] = SWAP_MAP_BAD;
                        nr_good_pages--;
                        /*
                         * Haven't marked the cluster free yet, no list
                         * operation involved
                         */
                        inc_cluster_info_page(p, cluster_info, page_nr);
                }
        }

        /* Haven't marked the cluster free yet, no list operation involved */
        for (i = maxpages; i < round_up(maxpages, SWAPFILE_CLUSTER); i++)
                inc_cluster_info_page(p, cluster_info, i);

        if (nr_good_pages) {
                swap_map[0] = SWAP_MAP_BAD;
                /*
                 * Not mark the cluster free yet, no list
                 * operation involved
                 */
                inc_cluster_info_page(p, cluster_info, 0);
                p->max = maxpages;
                p->pages = nr_good_pages;
                nr_extents = setup_swap_extents(p, span);
                if (nr_extents < 0)
                        return nr_extents;
                nr_good_pages = p->pages;
        }
        if (!nr_good_pages) {
                pr_warn("Empty swap-file\n");
                return -EINVAL;
        }

        if (!cluster_info)
                return nr_extents;

        /*
         * Reduce false cache line sharing between cluster_info and
         * sharing same address space.
         */
        for (k = 0; k < SWAP_CLUSTER_COLS; k++) {
                j = (k + col) % SWAP_CLUSTER_COLS;
                for (i = 0; i < DIV_ROUND_UP(nr_clusters, SWAP_CLUSTER_COLS); i++) {
                        idx = i * SWAP_CLUSTER_COLS + j;
                        if (idx >= nr_clusters)
                                continue;
                        if (cluster_count(&cluster_info[idx]))
                                continue;
                        cluster_set_flag(&cluster_info[idx], CLUSTER_FLAG_FREE);
                        cluster_list_add_tail(&p->free_clusters, cluster_info,
                                              idx);
                }
        }
        return nr_extents;
}

swap 맵과 swap_extent를 할당하고 준비하며 할당한 swap_extent 수를 반환한다.

  • 코드 라인 11에서 swap 최대 페이지 수로 필요한 클러스터 수를 산출한다.
  • 코드 라인 12에서 다음에 진행할 클러스터 번호를 알아온다.
    • swap 파일은 최대 64M 단위로 최대 클러스터 번호는
  • 코드 라인 15에서 good 페이지 수를 산출할 때 배드 페이지들을 빼기 전에 먼저 헤더 페이지로 1 페이지를 사용하므로 swap 최대 페이지 수에서 1을 뺀다.
  • 코드 라인 17~18에서 free_clusters와 discard_clusters 리스트들을 클리어한다.
  • 코드 라인 20~33에서 헤더 페이지에 기록된 배드 페이지 수만큼 순회하며 배드 페이지 번호를 알아와서 그에 해당하는 swap_map[]에 SWAP_MAP_BAD 마킹을 하고, good 페이지 수를 감소시킨다. 마킹한 배드 페이지가 있는 클러스터도 사용중으로 설정한다.
  • 코드 라인 36~37에서 swap 영역을 클러스터 단위로 관리하는데 끝 부분이 정렬되지 않고 남는 영역의 페이지들도 모두 사용중인 클러스터로 설정한다.
  • 코드 라인 39~52에서 good 페이지가 존재하는 경우 swap 영역의 첫 페이지는 SWAP_MAP_BAD로 설정하고, 첫 클러스터를 사용중으로 설정한다. 그리고 swap extent를 구성한다.
  • 코드 라인 53~56에서 good 페이지가 하나도 없으면 빈 swap 파일이라고 경고 메시지를 출력하고 -EINVAL 에러를 반환한다.
  • 코드 라인 58~59에서 @cluster_info가 지정되지 않은 경우 swap extent 수를 반환한다.
  • 코드 라인 65~77에서 false cache line sharing을 줄이기 위해 cluster_info와 address_space를 각 cpu들이 따로 접근하도록 떨어뜨렸다. 사용 가능한 free 클러스터들을 fre_clusters 리스트에 추가하고, cluster_info 정보의 플래그에 CLUSTER_FLAG_FREE를 추가한다.
  • 코드 라인 78에서 swap extent 수를 반환한다.

 

다음 그림은 swap 영역의 헤더 페이지를 분석한 정보로 swap_map을 구성하는 모습을 보여준다.

 

다음 그림은 SSD를 사용 시 클러스터 구성을 위해 free 클러스터 리스트를 준비하는 과정을 보여준다.

  • 헤더 페이지, BAD 페이지가 속한 클러스터는 사용 중으로 만들어 free 클러스터 리스트에서 제거한다.
  • free 클러스터를 추가할 때 클러스터를 순서대로 넣지 않고 64개씩 분리하여 추가하는 것을 보여준다.

 

 


Swap Extents

swap 영역을 블럭 디바이스에 범위를 매핑할 때 사용한다. swap 영역의 종류에 따라 다음 3가지 방법으로 swap extent를 준비한다.

  • 블럭 디바이스를 사용하는 경우 swap 영역과 블럭 디바이스는 한 번에 전부 1개의 swap extent를 사용하여 매핑한다.
  • 마운트된 파일 시스템에서 (*swap_activate)가 지원되는 swap 파일이 위치한 swap 영역도 한 번에 전부를 매핑하므로  1개의 swap extent만 필요하다.
    • nfs, xfs, btrfs, sunrpc, …
  • generic swap 파일
    • swap 영역으로 swap 파일을 사용하는 경우이다. 이 때에는 블럭 디바이스의 빈 공간이 여러 군데에 fragment된 경우 이므로 여러 개의 swap_extent가 필요하게 된다.

 

setup_swap_extents()

mm/swapfile.c

/*
 * A `swap extent' is a simple thing which maps a contiguous range of pages
 * onto a contiguous range of disk blocks.  An ordered list of swap extents
 * is built at swapon time and is then used at swap_writepage/swap_readpage
 * time for locating where on disk a page belongs.
 *
 * If the swapfile is an S_ISBLK block device, a single extent is installed.
 * This is done so that the main operating code can treat S_ISBLK and S_ISREG
 * swap files identically.
 *
 * Whether the swapdev is an S_ISREG file or an S_ISBLK blockdev, the swap
 * extent list operates in PAGE_SIZE disk blocks.  Both S_ISREG and S_ISBLK
 * swapfiles are handled *identically* after swapon time.
 *
 * For S_ISREG swapfiles, setup_swap_extents() will walk all the file's blocks
 * and will parse them into an ordered extent list, in PAGE_SIZE chunks.  If
 * some stray blocks are found which do not fall within the PAGE_SIZE alignment
 * requirements, they are simply tossed out - we will never use those blocks
 * for swapping.
 *
 * For S_ISREG swapfiles we set S_SWAPFILE across the life of the swapon.  This
 * prevents root from shooting her foot off by ftruncating an in-use swapfile,
 * which will scribble on the fs.
 *
 * The amount of disk space which a single swap extent represents varies.
 * Typically it is in the 1-4 megabyte range.  So we can have hundreds of
 * extents in the list.  To avoid much list walking, we cache the previous
 * search location in `curr_swap_extent', and start new searches from there.
 * This is extremely effective.  The average number of iterations in
 * map_swap_page() has been measured at about 0.3 per page.  - akpm.
 */
static int setup_swap_extents(struct swap_info_struct *sis, sector_t *span)
{
        struct file *swap_file = sis->swap_file;
        struct address_space *mapping = swap_file->f_mapping;
        struct inode *inode = mapping->host;
        int ret;

        if (S_ISBLK(inode->i_mode)) {
                ret = add_swap_extent(sis, 0, sis->max, 0);
                *span = sis->pages;
                return ret;
        }

        if (mapping->a_ops->swap_activate) {
                ret = mapping->a_ops->swap_activate(sis, swap_file, span);
                if (ret >= 0)
                        sis->flags |= SWP_ACTIVATED;
                if (!ret) {
                        sis->flags |= SWP_FS;
                        ret = add_swap_extent(sis, 0, sis->max, 0);
                        *span = sis->pages;
                }
                return ret;
        }

        return generic_swapfile_activate(sis, swap_file, span);
}

swap extents를 준비한다. 출력 인자 @span에 페이지 수를 지정한다. 결과가 0인 경우 새로 활성화된 경우이다.

  • 코드 라인 8~12에서 swap 영역이 블럭 디바이스인 경우 swap 영역 전체(0 ~ sis->max 페이지)를 지정한 블럭 디바이스에 한 번에 매핑할 수 있다. 그렇게 하기 위해 0번 블럭부터 전체를 매핑하도록 1개의 swap_extent를 추가한다.
  • 코드 라인 14~24에서 매핑된 오퍼레이션의 (*swap_activate) 후크가 지원되는 경우 호출한 후 이미 활성화된 경우 SWP_ACTIVATED 플래그를 추가한다. 또한 새로 활성화된 경우 SWP_FS 플래그를 설정하고, swap 영역 전체(0 ~ sis->max 페이지)를 한 번에 매핑하도록 1개의 swap_extent를 구성하여 추가한다.
  • 코드 라인 26에서 swap 영역이 generic한 swap 파일인 경우 1개 이상의 매핑을 위해 swap_extent 들을 추가하고 활성화한다.

 

generic_swapfile_activate()

mm/page_io.c

int generic_swapfile_activate(struct swap_info_struct *sis,
                                struct file *swap_file,
                                sector_t *span)
{
        struct address_space *mapping = swap_file->f_mapping;
        struct inode *inode = mapping->host;
        unsigned blocks_per_page;
        unsigned long page_no;
        unsigned blkbits;
        sector_t probe_block;
        sector_t last_block;
        sector_t lowest_block = -1;
        sector_t highest_block = 0;
        int nr_extents = 0;
        int ret;

        blkbits = inode->i_blkbits;
        blocks_per_page = PAGE_SIZE >> blkbits;

        /*
         * Map all the blocks into the extent list.  This code doesn't try
         * to be very smart.
         */
        probe_block = 0;
        page_no = 0;
        last_block = i_size_read(inode) >> blkbits;
        while ((probe_block + blocks_per_page) <= last_block &&
                        page_no < sis->max) {
                unsigned block_in_page;
                sector_t first_block;

                cond_resched();

                first_block = bmap(inode, probe_block);
                if (first_block == 0)
                        goto bad_bmap;

                /*
                 * It must be PAGE_SIZE aligned on-disk
                 */
                if (first_block & (blocks_per_page - 1)) {
                        probe_block++;
                        goto reprobe;
                }

                for (block_in_page = 1; block_in_page < blocks_per_page;
                                        block_in_page++) {
                        sector_t block;

                        block = bmap(inode, probe_block + block_in_page);
                        if (block == 0)
                                goto bad_bmap;
                        if (block != first_block + block_in_page) {
                                /* Discontiguity */
                                probe_block++;
                                goto reprobe;
                        }
                }

                first_block >>= (PAGE_SHIFT - blkbits);
                if (page_no) {  /* exclude the header page */
                        if (first_block < lowest_block)
                                lowest_block = first_block;
                        if (first_block > highest_block)
                                highest_block = first_block;
                }

                /*
                 * We found a PAGE_SIZE-length, PAGE_SIZE-aligned run of blocks
                 */
                ret = add_swap_extent(sis, page_no, 1, first_block);
                if (ret < 0)
                        goto out;
                nr_extents += ret;
                page_no++;
                probe_block += blocks_per_page;
reprobe:
                continue;
        }
        ret = nr_extents;
        *span = 1 + highest_block - lowest_block;
        if (page_no == 0)
                page_no = 1;    /* force Empty message */
        sis->max = page_no;
        sis->pages = page_no - 1;
        sis->highest_bit = page_no - 1;
out:
        return ret;
bad_bmap:
        pr_err("swapon: swapfile has holes\n");
        ret = -EINVAL;
        goto out;
}

swap 영역이 generic한 swap 파일에서 이를 활성화한다. 성공 시 추가한 extent 수를 반환한다.

  • 코드 라인 17~18에서 한 개의 페이지에 들어갈 수 있는 블럭 비트 수를 구해 blocks_per_page에 대입한다.
    • 블럭 크기는 512byte 이다. 그러나 여기서 말하는 블럭은 IO 단위로 처리 가능한 가상 블럭을 의미한다. swap 파일이 ext2, ext3, ext4 파일시스템에서 운영되는 경우 가상 블럭 사이즈는 1K, 2K, 4K, 8K를 지원하고, 디폴트로 4K를 사용한다.
      • 예) PAGE_SIZE(4096) >> inode->iblkbits(12) = 1 블럭
  • 코드 라인 24~26에서 파일의 시작 페이지(page_no)를 0부터 끝 페이지(sis->max) 까지 순회를 위해 준비한다. 이 때 probe_block도 0부터 시작하고, last_block에는 파일의 끝 블럭 번호를 대입한다.
  • 코드 라인 27~28에서 probe_block 부터 blocks_per_page 단위로 증가하며 last_block까지 순회한다.
    • swap 파일이 ext2, ext3, ext4 파일시스템에서 운영되는 경우 블럭 사이즈로 디폴트 설정을 사용하면 1페이지가 1블럭과 동일하다. 따라서 blocks_per_page의 경우 1이다.
  • 코드 라인 34~36에서 swap 파일의 probe_block 페이지에 대한 디스크 블럭 번호를 알아와서 first_block에 대입한다.
  • 코드 라인 41~44에서 알아온 디스크 블럭 번호(first_block)가 blocks_per_page 단위로 정렬되지 않은 경우 정렬될 때까지 swap 파일에 대한 블럭 번호(probe_block)를 증가시키고 reprobe 레이블로 이동한다.
  • 코드 라인 46~58에서 페이지 내에 2개 이상의 블럭이 있는 경우 블럭내 두 번째 페이지부터 블럭내 끝 페이지까지 순회한다. probe_block + 순회중인 페이지 순번을 더한 번호로 이에 해당하는 블럭 디바이스의 블럭 번호와 동일하게 연동되는지 확인한다. 만일 일치하지 않으면 reprobe 레이블로 이동한다.
  • 코드 라인 60~66에서 헤더 페이지를 제외하고 알아온 블럭 디바이스의 번호(first_block)로 가장 작은 lowest_block과 가장 큰 highest_block을 갱신한다.
  • 코드 라인 71~74에서 swap 파일의 page_no에 해당하는 1개 페이지를 알아온 블럭 디바이스 번호(first_block)에 매핑한다. 매핑 시 실제 swap extent가 추가되면 1이 반환된다. 추가되지 않고 기존 swap extent에 merge되면 0이 반환된다. 이렇게 반환된 수를 nr_extents에 합산한다.
  • 코드 라인 75~76에서 다음 페이지를 처리하러 계속 진행한다. probe_block은 페이지 단위로 정렬되어야 하므로 blocks_per_page 만큼 증가시킨다.
  • 코드 라인 77~79에서 reprobe: 레이블이다. while 루프를 계속 진행한다.
  • 코드 라인 80에서 반환할 값으로 추가한 extent 수를 대입한다.
  • 코드 라인 81에서 출력 인자 @span에는 가장 작은 블록부터 가장 큰 블록까지의 수를 대입한다.
  • 코드 라인 82~83에서 page_no가 한 번도 증가되지 않은 경우 빈 페이지인 경우이다. page_no에 1을 대입한다.
  • 코드 라인 84~86에서 max, pages, highest_bit등을 갱신한다.
  • 코드 라인 87~88에서 out: 레이블이다. 추가한 extent 수를 반환한다.
  • 코드 라인 89~92에서 bad_bmap: 레이블이다. 다음 에러 메시지를 출력하고 -EINVAL 에러를 반환한다.
    • “swapon: swapfile has holes\n”

 

swap_extent 구조체

include/linux/swap.h

/*
 * A swap extent maps a range of a swapfile's PAGE_SIZE pages onto a range of
 * disk blocks.  A list of swap extents maps the entire swapfile.  (Where the
 * term `swapfile' refers to either a blockdevice or an IS_REG file.  Apart
 * from setup, they're handled identically.
 *
 * We always assume that blocks are of size PAGE_SIZE.
 */
struct swap_extent {
        struct list_head list;
        pgoff_t start_page;
        pgoff_t nr_pages;
        sector_t start_block;
};

swap 영역을 블럭 디바이스에 범위를 매핑할 때 사용한다.

  •  list
    • 다음과 같이 두 가지 사용 방법으로 나뉜다.
      • swap_info_struct에 내장된 swap_extent의 list인 경우 리스트 헤드로 사용된다.
      • 헤드에 추가되는 swap_extent의 list는 추가할 때 사용하는 노드로 사용된다.
  • start_page
    • swap 영역에서 매핑할 시작 페이지이다.
  • nr_pages
    • swap 영역에서 위의 start_page부터 연속 매핑할 페이지 수이다.
  • start_block
    • swap 영역의 start_page 부터 nr_pages 만큼 블럭 디바이스의 start_block 부터 매핑된다.

 

add_swap_extent()

mm/swapfile.c

/*
 * Add a block range (and the corresponding page range) into this swapdev's
 * extent list.  The extent list is kept sorted in page order.
 *
 * This function rather assumes that it is called in ascending page order.
 */
int
add_swap_extent(struct swap_info_struct *sis, unsigned long start_page,
                unsigned long nr_pages, sector_t start_block)
{
        struct swap_extent *se;
        struct swap_extent *new_se;
        struct list_head *lh;

        if (start_page == 0) {
                se = &sis->first_swap_extent;
                sis->curr_swap_extent = se;
                se->start_page = 0;
                se->nr_pages = nr_pages;
                se->start_block = start_block;
                return 1;
        } else {
                lh = sis->first_swap_extent.list.prev;  /* Highest extent */
                se = list_entry(lh, struct swap_extent, list);
                BUG_ON(se->start_page + se->nr_pages != start_page);
                if (se->start_block + se->nr_pages == start_block) {
                        /* Merge it */
                        se->nr_pages += nr_pages;
                        return 0;
                }
        }

        /*
         * No merge.  Insert a new extent, preserving ordering.
         */
        new_se = kmalloc(sizeof(*se), GFP_KERNEL);
        if (new_se == NULL)
                return -ENOMEM;
        new_se->start_page = start_page;
        new_se->nr_pages = nr_pages;
        new_se->start_block = start_block;

        list_add_tail(&new_se->list, &sis->first_swap_extent.list);
        return 1;
}
EXPORT_SYMBOL_GPL(add_swap_extent);

swap 영역의 @start_page부터 @nr_pages를 블럭 디바이스의 @start_block에 매핑한다. 만일 새 swap extent가 할당된 경우 1을 반환한다.

  • 코드 라인 9~15에서 swap 영역을 처음 매핑하러 시도할 때 0번 페이지부터 시작하는데 이 때 @nr_pages 만큼 @start_block에 매핑한다. 매핑할 때 사용되는 swap_extent는 swap_info_struct 구조체 내부에 기본으로 사용되는 first_swap_extent를 사용한다. 내장된 swap extent를 사용했어도 추가되었다는 의미로 1을 반환한다.
    • 블럭 디바이스를 swap 영역으로 사용 시 swap_extent는 하나만 사용되므로, 한 번의 매핑을 위해 이 조건 한 번만 호출된다.
  • 코드 라인 16~25에서 연속된 블럭을 사용할 수 있는 경우 기존 매핑을 merge하여 사용되는 케이스이다. merge 되었으므로 swap extent가 추가되지 않아 0을 반환한다.
  • 코드 라인 30~38에서 방금 전에 매핑한 블럭 디바이스 번호가 연속되지 않는 경우 새로운 swap extent를 할당하고 새로운 매핑 정보를 기록한다. 그리고 sis->first_swap_extent.list에 추가한 후 1을 반환한다.

 

다음 그림은 swap 파일이 마운트된 블럭 디바이스에 흩어지지(fragment) 않고 연속되어 사용되는 경우 swap extent를 1개만 사용하는 모습을 보여준다.

 

다음 그림은 swap 파일이 마운트된 블럭 디바이스에 3번 흩어져서(fragment) 사용되는 경우 swap extent를 3개 사용한 모습을 보여준다.

 

destroy_swap_extents()

mm/swapfile.c

/*
 * Free all of a swapdev's extent information
 */
static void destroy_swap_extents(struct swap_info_struct *sis)
{
        while (!list_empty(&sis->first_swap_extent.list)) {
                struct swap_extent *se;

                se = list_first_entry(&sis->first_swap_extent.list,
                                struct swap_extent, list);
                list_del(&se->list);
                kfree(se);
        }

        if (sis->flags & SWP_ACTIVATED) {
                struct file *swap_file = sis->swap_file;
                struct address_space *mapping = swap_file->f_mapping;

                sis->flags &= ~SWP_ACTIVATED;
                if (mapping->a_ops->swap_deactivate)
                        mapping->a_ops->swap_deactivate(swap_file);
        }
}

모든 swap extent들을 리스트에서 제거하고 할당 해제한다.

 


swap 엔트리용 address_space 관리

swap용 address_space 생성과 소멸

address_space 구조체는 swapon/swapoff 명령에 의해 생성과 소멸된다.

  • swapon -> sys_swapon() -> init_swap_address_space()
  • swapoff -> sys_swapoff() -> exit_swap_address_space()

 

init_swap_address_space()

mm/swap_state.c

int init_swap_address_space(unsigned int type, unsigned long nr_pages)
{
        struct address_space *spaces, *space;
        unsigned int i, nr;

        nr = DIV_ROUND_UP(nr_pages, SWAP_ADDRESS_SPACE_PAGES);
        spaces = kvcalloc(nr, sizeof(struct address_space), GFP_KERNEL);
        if (!spaces)
                return -ENOMEM;
        for (i = 0; i < nr; i++) {
                space = spaces + i;
                xa_init_flags(&space->i_pages, XA_FLAGS_LOCK_IRQ);
                atomic_set(&space->i_mmap_writable, 0);
                space->a_ops = &swap_aops;
                /* swap cache doesn't use writeback related tags */
                mapping_set_no_writeback_tags(space);
        }
        nr_swapper_spaces[type] = nr;
        rcu_assign_pointer(swapper_spaces[type], spaces);

        return 0;
}

swap용 address_space 구조체를 할당하여 준비한다.

  • swapon에 의해 호출되어 nr_pages를 SWAP_ADDRESS_SPACE_PAGES(2^14) 단위로 절상한 수만큼 address_space 구조체를 할당하여 초기화한 후 swapper_space[@type]에 지정한다.
  • swap 영역에 사용되는 swap 캐시를 하나의 radix tree로 관리하였었는데, rock 사용 빈도를 줄여 성능을 올리기 위해 swap 캐시를 관리하는 address_space마다 최대 64M만을 관리하도록 나누어 배치하였다. 따라서 swapper_space[] 배열을 사용하는 것이 아니라 swapper_space[][] 이중 배열로 사용하는 것으로 변경되었다.

 

다음 그림은 swapon 시 swap용 address_space 구조체 배열이 생성되는 과정을 보여준다.

 

exit_swap_address_space()

mm/swap_state.c

void exit_swap_address_space(unsigned int type)
{
        struct address_space *spaces;

        spaces = swapper_spaces[type];
        nr_swapper_spaces[type] = 0;
        rcu_assign_pointer(swapper_spaces[type], NULL);
        synchronize_rcu();
        kvfree(spaces);
}

swapoff에 의해 호출되어 swapper_space[@type]에 저장된 address_space 배열을 할당 해제한다.

 

swap 엔트리로 address_space 찾기

swap_address_space()

include/linux/swap.h

#define swap_address_space(entry)                           \
        (&swapper_spaces[swp_type(entry)][swp_offset(entry) \
                >> SWAP_ADDRESS_SPACE_SHIFT])

swap 엔트리에 해당하는 address_space 포인터를 반환한다.

  • swap 엔트리의 type과 offset으로 지정된 address_space 포인터를 반환한다.

 

/* linux/mm/swap_state.c */
/* One swap address space for each 64M swap space */
#define SWAP_ADDRESS_SPACE_SHIFT        14
#define SWAP_ADDRESS_SPACE_PAGES        (1 << SWAP_ADDRESS_SPACE_SHIFT)

1 개의 address_space가 관리하는 swap 크기는 64M이다.

 

다음 그림은 swap_address_space() 함수에서 swap 엔트리로 address_space를 알아오는 과정을 보여준다.

 

swapper_spaces[]

mm/swap_state.c

struct address_space *swapper_spaces[MAX_SWAPFILES] __read_mostly;

MAX_SWAPFILES(커널 옵션에 따라 27~32, ARM64 디폴트=29) 수 만큼의 파일과 연결되며 각각은 address_space 배열이 지정된다.

  • 이 배열은 static하게 1차원 배열로 생성되지만, 실제 운영은 다음과 같이 2차원 배열로 사용한다.
    • address_space[type][offset]

 

다음 그림은 swap 캐시의 address_space가 관리하는 xarray와 swap opearations를 보여준다.

 


구조체

swap_info_struct 구조체

/*
 * The in-memory structure used to track swap areas.
 */
struct swap_info_struct {
        unsigned long   flags;          /* SWP_USED etc: see above */
        signed short    prio;           /* swap priority of this type */
        struct plist_node list;         /* entry in swap_active_head */
        signed char     type;           /* strange name for an index */
        unsigned int    max;            /* extent of the swap_map */
        unsigned char *swap_map;        /* vmalloc'ed array of usage counts */
        struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
        struct swap_cluster_list free_clusters; /* free clusters list */
        unsigned int lowest_bit;        /* index of first free in swap_map */
        unsigned int highest_bit;       /* index of last free in swap_map */
        unsigned int pages;             /* total of usable pages of swap */
        unsigned int inuse_pages;       /* number of those currently in use */
        unsigned int cluster_next;      /* likely index for next allocation */
        unsigned int cluster_nr;        /* countdown to next cluster search */
        struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
        struct swap_extent *curr_swap_extent;
        struct swap_extent first_swap_extent;
        struct block_device *bdev;      /* swap device or bdev of swap file */
        struct file *swap_file;         /* seldom referenced */
        unsigned int old_block_size;    /* seldom referenced */
#ifdef CONFIG_FRONTSWAP
        unsigned long *frontswap_map;   /* frontswap in-use, one bit per page */
        atomic_t frontswap_pages;       /* frontswap pages in-use counter */
#endif
        spinlock_t lock;                /*
                                         * protect map scan related fields like
                                         * swap_map, lowest_bit, highest_bit,
                                         * inuse_pages, cluster_next,
                                         * cluster_nr, lowest_alloc,
                                         * highest_alloc, free/discard cluster
                                         * list. other fields are only changed
                                         * at swapon/swapoff, so are protected
                                         * by swap_lock. changing flags need
                                         * hold this lock and swap_lock. If
                                         * both locks need hold, hold swap_lock
                                         * first.
                                         */
        spinlock_t cont_lock;           /*
                                         * protect swap count continuation page
                                         * list.
                                         */
        struct work_struct discard_work; /* discard worker */
        struct swap_cluster_list discard_clusters; /* discard clusters list */
        struct plist_node avail_lists[0]; /*
                                           * entries in swap_avail_heads, one
                                           * entry per node.
                                           * Must be last as the number of the
                                           * array is nr_node_ids, which is not
                                           * a fixed value so have to allocate
                                           * dynamically.
                                           * And it has to be an array so that
                                           * plist_for_each_* can work.
                                           */
};

swap 영역마다 하나씩 사용된다.

  • flags
    • swap 영역에 사용되는 플래그 값들이다. (아래 참조)
  • prio
    • 영역의 사용에 대한 순서를 정하기 위한 우선 순위 값이다.
    • default 값은 -2부터 생성되는 순서대로 감소된다.
    • 사용자가 지정하는 경우 양수를 사용할 수 있다.
  • list
    • swap_active_head 리스트에 사용되는 노드이다.
  • type
    • swap 영역에 대한 인덱스 번호이다. (0~)
  • max
    • swap 영역의 전체 페이지 수 (bad 페이지 포함)
  • *swam_map
    • swap 영역에 대한 swap 맵으로 1페이지당 1바이트의 값을 사용된다.
    • 예) 0=free 상태, 1~0x3e: 사용 상태(usage counter), 0x3f: bad 상태
  • *cluster_info
    • swap 영역에서 사용하는 모든 클러스터들을 가리킨다.
  • free_clusters
    • 사용 가능한  free 클러스터들을 담는 리스트이다.
  • lowest_bit
    • swap 영역에서 가장 낮은 free 페이지의 offset 값이다.
  • highest_bit
    • swap 영역에서 가장 높은 free 페이지의 offset 값이다.
  • pages
    • swap 영역의 전체 사용가능한 페이지 수 (bad 페이지 제외)
  • inuse_pages
    • swap 영역에서 할당되어 사용중인 페이지 수
  • cluster_next
    • 높은 확률로 다음 할당 시 사용할 페이지 offset를 가리킨다.
  • cluster_nr
    • 다음 클러스터를 검색하기 위한 countdown 값
  • *percpu_cluster
    • cpu별 현재 지정된 클러스터를 가리킨다.
    • 해당 cpu에 지정된 클러스터가 없는 경우 그 cpu의 값은 null을 가진다.
  • *curr_swap_extent
    • 현재 사용중인 swap extent를 가리킨다.
  • first_swap_extent
    • swap 영역에 내장된 첫 swap extent이다.
  • *bdev
    • swap 영역에 지정된 블럭 디바이스 또는 swap 파일의 블럭 디바이스
  • *swap_file
    • swap 파일을 가리킨다.
  • old_block_size
    • swap 블럭 디바이스의 사이즈
  • *frontswap_map
    • frontswap의 in-use 상태를 비트로 표기하고, 1 비트는 1 페이지에 해당한다.
  • frontswap_pages
    • fronswap의 in-use 카운터
  • lock
    • swap 영역에 대한 lock이다.
  • cont_lock
    • swap_map[]의 usage 카운터가 0x3e를 초과하는 경우에 swap 영역의 맵 관리는 swap count continuation 모드로 관리되는데 이 때 사용되는 swap count continuation 페이지 리스트에 접근할 때 사용하는 lock 이다.
  • discard_work
    • discard를 지원하는 SSD에서 사용할 워크이다.
  • discard_clusters
    • discard할 클러스터들이 담긴 리스트이다.
    • discard 처리 후 free 클러스터 리스트로 옮긴다.
  • avail_lists[0]
    • 노드별로 관리되는 swap_avail_heads[] 리스트의 노드로 사용된다.

 

플래그에 사용되는 값들이다.

enum {
        SWP_USED        = (1 << 0),     /* is slot in swap_info[] used? */
        SWP_WRITEOK     = (1 << 1),     /* ok to write to this swap?    */
        SWP_DISCARDABLE = (1 << 2),     /* blkdev support discard */
        SWP_DISCARDING  = (1 << 3),     /* now discarding a free cluster */
        SWP_SOLIDSTATE  = (1 << 4),     /* blkdev seeks are cheap */
        SWP_CONTINUED   = (1 << 5),     /* swap_map has count continuation */
        SWP_BLKDEV      = (1 << 6),     /* its a block device */
        SWP_ACTIVATED   = (1 << 7),     /* set after swap_activate success */
        SWP_FS          = (1 << 8),     /* swap file goes through fs */
        SWP_AREA_DISCARD = (1 << 9),    /* single-time swap area discards */
        SWP_PAGE_DISCARD = (1 << 10),   /* freed swap page-cluster discards */
        SWP_STABLE_WRITES = (1 << 11),  /* no overwrite PG_writeback pages */
        SWP_SYNCHRONOUS_IO = (1 << 12), /* synchronous IO is efficient */
                                        /* add others here before... */
        SWP_SCANNING    = (1 << 13),    /* refcount in scan_swap_map */
};
  • SWP_USED
    • swapon되어 사용 중인 swap 영역이다.
  • SWP_WRITEOK
    • swap 영역에 write 가능한 상태이다.
  • SWP_DISCARDABLE
    • swap 영역이 discard를 지원한다. (SSD)
  • SWP_DISCARDING
    • swap 영역에 discard 작업을 수행 중이다.
  • SWP_SOLIDSTATE
    • swap 영역이 SSD이다.
  • SWP_CONTINUED
    • swap 영역에서 usage 카운터가 0x3e를 초과하여 사용 중이다.
  • SWP_BLKDEV
    • swap 영역이 블럭 디바이스이다.
  • SWP_ACTIVATED
    • swap 영역이 swap_activate 후크를 성공한 경우에 설정된다.
  • SWP_FS
    • 파일 시스템을 통한 swap 파일이다.
  • SWP_AREA_DISCARD
    • 한 번만 discard를 지원한다.
  • SWP_PAGE_DISCARD
    • 클러스터 단위로 자유롭게 discard를 사용할 수 있다.
  • SWP_STABLE_WRITES
    • 체크섬등을 사용한 무결성 저장 장치에 부여한다.
    • SCSI Data Integrity Block Device
  • SWP_SYNCHRONOUS_IO
    • 동기 저장이 가능한 swap 영역이다. (RAM 등)
  • SWP_SCANNING
    • swap 영역을 검색중일 때 설정된다.

 

참고