ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - Thermal framework : Overview & devicetree
    Linux/kernel 2023. 8. 7. 19:18

    글의 참고

    - https://elinux.org/images/f/f7/ELC-2020-Thara-Ram-Linux-Kernel-Thermal-Warming.pdf

    - https://static.linaro.org/connect/san19/presentations/san19-101.pdf

    - https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal.txt

    - https://en.wikipedia.org/wiki/Computer_cooling

    - https://usermanual.wiki/Document/RockchipDeveloperGuideLinux44ThermalEN.146232978.pdf

    - https://betheme.net/dashuju/95669.html?action=onClick

    - https://www.kernel.org/doc/html/next/driver-api/thermal/cpu-idle-cooling.html

    - https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal.txt

    - https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal-cooling-devices.yaml

    - https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal-zones.yaml

    - https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal

    - https://www.kernel.org/doc/Documentation/driver-api/thermal/power_allocator.rst

    - https://www.cnblogs.com/hellokitty2/p/15600099.html


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다.


    글의 내용

    - Thermal framework

    : 리눅스 커널의 `Thermal framework`는 커널 레벨에서 과열로 인해 디바이스에 손상이 가는 것을 방지하고, 디바이스를 더 오래 그리고 안전하게 사용할 수 있도록 하는데 의의가 있다.

     

     

     

    - Thermal framework architecture

    : 아래 그림에서 보는 것과 같이 `Thermal framework`는 core, governor, zone, cooling device, sensor driver로 구성되어 있다.




    https://betheme.net/dashuju/95669.html?action=onClick

     

     

    : `Thermal Framework`를 2개의 시스템 레벨로 나누면 아래와 같다.


    https://betheme.net/dashuju/95669.html?action=onClick

     

    - Thermal zones

    : 온도 센서가 온도를 측정하는 영역 및 쿨링 동작이 적용되는 영역을 의미한다. 

     

     

    - Thermal sensors

    : 실제 물리적인 온도 측정 센서를 나타낸다. 예를 들어, NTC thermisor 같은 것들이 여기에 속한다.

     

     

    - Thremal core

    : `Thermal framework`에 존재하는 다른 요소들에게 Thermal 관련 핵심적인 기능들을 제공한다. 그리고, sysfs를 제공해서 user space와 통신할 수 있는 인터페이스를 제공한다.

     

     

    - Thermal governor

    : 온도가 특정 임계값(`throttle`)을 넘었을 때, 어떤 `cooling state`를 적용할 지를 결정하는 역할을 한다. 리눅스 커널은 다양한 thermal governor가 존재한다.

     

    1. step_wise
    2. power_allocator
    3. user_space
    4. fair_share
    5. bangbang

     

     

    - Cooling devices

    : thermal governor에서 어떤 정책이 사용될지가 정해지고, 앞에서 정해진 전략에서 어떤 쿨링 상태를 적용할지가 정해지면, 이제 해당 쿨링 상태에 따른 실제 알고리즘이 구현되어야 한다. 예를 들어, `영상 100도에서는 CPU의 주파수와 전압을 70% 낮춘다` 등과 같은 실제 알고리즘(쿨링 동작)이 적용되어야 한다. 이 알고리즘을 `cooling device`가 맡고 있다. 그런데, 이런 알고리즘은 디바이스마다 다르게 적용될 수 있어야 한다. 

     

    : 결국, 쿨링 디바이스란 쿨링 동작을 수행하는 논리적인 디바이스라고 볼 수 있다. 주의할 점은 `논리적인 디바이스`라고 했다. 물리적인 디바이스가 아니다. 물론, `fan`과 같은 경우는 물리적인 쿨링 디바이스지만, 그외에는 모드 논리적인 디바이스일 확률이 높다.

    예를 들어, `cooling device`는 특정 `trip point`에 트리거되어 동작하게 된다. `영상 100도에서는 CPU의 주파수와 전압을 70% 낮춘다` 와 같은 문장이 있을 때, `trip point` == 100 , `CPU의 주파수와 전압을 70% 낮춘다`는 쿨링 디바이스가 되는 것이다. `CPU의 주파수와 전압을 70%` 낮춘다는 것이 쿨링 디바이스인데, 이게 특별한 물리적인 디바이스일까? 아니다. CPU의 주파수와 전압은 CPU 스스로가 낮출 것이기 때문에, 일반적으로 `CPU cooling device`라고 부르게 된다. `GPU의 주파수와 전압을 70% 낮춘다` 라고 하면, `GPU cooling device`라고 한다. 그리고, 위에서 볼 수 있다시피 디바이스들은 `heating device`면서, 그와 동시에 자기 스스로의 온도를 낮출 수 있는 `cooling device` 이기도 하다.

     

     

     

    - Thermal device-tree [ 참고1 참고2 ]

    : 리눅스 커널 버전 6.5를 기준으로 `Thermal framework` 관련 device-tree 문서는 총 4개가 존재한다.

    1. thermal-zone.yaml
    2. thermal-sensor.yaml
    3. thermal-cooling-devices.yaml
    4. thermal-idle.yaml

     

     

    : `Thermal framework`를 본격적으로 분석하기 전에 디바이스 트리에 작성된 예시 코드를 통해 `Thermal framework`의 전체적인 구조를 살펴보자. `Thermal framework`에서 지원하는 노드는 크게 5개 존재한다.

    1. thermal sensors : 온도 측정을 담당하는 노드
    2. cooling devices : 실제 온도를 낮추는 동작을 수행하는 노드
    3. trip points : `cooling device`가 언제 혹은 어느 시점에 온도를 낮춰야 하는지를 알려주는 노드. 즉, 온도 임계값을 지정하는 노드
    4. cooling maps : `trip point`와 `cooling device`를 연결해주는 노드 
    5. thermal zones : `thermal seonsor`가 관리하는 범위를 나타낸다. 구체적으로 어떤 온도에서 어떤 cooling 전략이 사용될지, 몇 초 간격으로 온도 측정 할 것인지 등을 작성한다.

     

     

    : 예제를 통해서 알아보자. 아래에서 `#cooling-cells` 속성은 자신이 `cooling device` 노드라는 것을 나타낸다. 즉, `cpu0`과 `fan0`은 `cooling-device`가 된다. 그리고, 인자값은 대개는 `2`를 사용한다. 이 말의 의미는 ` ... = <&{cooling_device} 최소값 최대값>` 형태로 사용하겠다는 뜻이다. 이 내용은 뒤에서 다시 설명한다. `cooling device`는 기본적으로 여러 개의 `cooling state`를 가지고 있다. 기본적으로, 숫자가 커질수 록 강한 cooling 동작을 하게 된다. 아래 `cpu0`을 예로 들어보자.

     

    : 아래 `cpu0` 노드를 보면 OPP 값들이 작성되어 있는 것을 확인할 수 있다. 즉, DVFS를 통해서 온도를 낮추겠다는 뜻이고, 이 말은 `cpu0`은 `passive cooling device`가 된다는 소리다. 그리고, 4개의 OPP를 가지고 있는게 확인한다. 결국, 각 OPP가 `cooling state`에 대응하게 된다.

     

    1. OPP[0] == cooling state[0] : 970Mhz 1.2V
    2. OPP[1] == cooling state[1] : 792Mhz 1.1V
    3. OPP[2] == cooling state[2] : 396Mhz 0.95V
    4. OPP[3] == cooling state[3] : 198Mhz 0.85V

     

     

    : `bandgap0` 노드는 `#thermal-sensor-cells` 속성을 하고 있다. 즉, 이 노드가 실제 물리적인 온도 센서를 의미한다. 인자로 `0`을 넣으면, 해당 노드만 전달하면 된다. 즉, ` ... = <&bandgap0>` 처럼 사용하면 된다는 뜻이다.

    #include <dt-bindings/thermal/thermal.h>
    
    cpus {
    	/*
    	 * Here is an example of describing a cooling device for a DVFS
    	 * capable CPU. The CPU node describes its four OPPs.
    	 * The cooling states possible are 0..3, and they are
    	 * used as OPP indexes. The minimum cooling state is 0, which means
    	 * all four OPPs can be available to the system. The maximum
    	 * cooling state is 3, which means only the lowest OPPs (198MHz@0.85V)
    	 * can be available in the system.
    	 */
    	cpu0: cpu@0 {
    		...
    		operating-points = <
    			/* kHz    uV */
    			970000  1200000
    			792000  1100000
    			396000  950000
    			198000  850000
    		>;
    		#cooling-cells = <2>; /* min followed by max */
    	};
    	...
    };
    
    &i2c1 {
    	...
    	/*
    	 * A simple fan controller which supports 10 speeds of operation
    	 * (represented as 0-9).
    	 */
    	fan0: fan@48 {
    		...
    		#cooling-cells = <2>; /* min followed by max */
    	};
    };
    
    ocp {
    	...
    	/*
    	 * A simple IC with a single bandgap temperature sensor.
    	 */
    	bandgap0: bandgap@0000ed00 {
    		...
    		#thermal-sensor-cells = <0>;
    	};
    };
    
    thermal-zones {
    	cpu_thermal: cpu-thermal {
    		polling-delay-passive = <250>; /* milliseconds */
    		polling-delay = <1000>; /* milliseconds */
    
    		thermal-sensors = <&bandgap0>;
    
    		trips {
    			cpu_alert0: cpu-alert0 {
    				temperature = <90000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "active";
    			};
    			cpu_alert1: cpu-alert1 {
    				temperature = <100000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "passive";
    			};
    			cpu_crit: cpu-crit {
    				temperature = <125000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "critical";
    			};
    		};
    
    		cooling-maps {
    			map0 {
    				trip = <&cpu_alert0>;
    				cooling-device = <&fan0 THERMAL_NO_LIMIT 4>;
    			};
    			map1 {
    				trip = <&cpu_alert1>;
    				cooling-device = <&fan0 5 THERMAL_NO_LIMIT>, <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>;
    			};
    		};
    	};
    };

     

     

    : 위에서 설명한 `cooling device`와 `thermal sensor`는 개별적으로 존재하고 있다. 이 둘을 연결하기 위해 사용하는 노드가 `cooling-maps` 노드다. 그런데, `cooling-maps` 노드는 `thermal-zone` 노드안에서만 존재할 수 있다. 그리고, `thermal-zone` 노드는 `thermal-zones` 노드안에만 존재할 수 있다. `thermal-zones` 노드는 컨테이너와 같다. 즉, 루트 노드가 된다. 그래서 특별한 기능을 하는 것은 아니다. 각 서브-시스템들이 자신들만의 루트 노드를 만드는 이유는 `파싱`을 편하게 하기 위해서다.

     

     

    : 실제 `thermal-zone`의 의미와 동일한 노드는 `thermal-zone` 노드다. `thermal-zone`은 각 온도 센서가 장착되어 있는 영역을 소프트웨어적으로 추상화했다. 이 영역에는 다음의 속성들이 올 수 있다[참고1].

     

    - Required
    1. polling-delay : `thermal framework`는 기본적으로 등록된 thermal-sensor의 값을 일정 주기로 polling 하는 방식을 온도를 측정한다. 그 때, polling 간격(period)을 얼만큼 할지를 명시한다. 이 값이 `0`일 경우, `polling timer`를 비활성화하고, `thermal sensor`의 인터럽트를 온도 센서 읽는 타이밍을 파악한다.
      
    2. polling-delay-passive : passive action이 동작중인 상황에서 polling period는 이 값으로 결정된다. 예를 들어, 위에 `cpu0`이 100`C 에서 passive action을 수행할 것이다. 그렇면, 설정된 OPP 중에 하나가 동작하게 될 것 이다. 이 시점부터는 `polling-delay`를 적용하는게 아니라, `polling-delay-passive`를 적용한다. 이 값이 `0`일 경우, `polling timer`를 비활성화하고, `thermal sensor`의 인터럽트를 온도 센서 읽는 타이밍을 파악한다. 

    3. thermal-sensors : 모니터링할 온도 센서를 명시한다. 여러 온도 센서를 명시할 수 있다. 

    4. trips : `trip node`의 컨테이너 역할만 한다. 실제 trip 에 대한 명시는 이 노드안에 작성된다.
    - temperature : trip point 온도를 나타낸다.
    - hysteresis : 채터링 방지를 위해 사용한다. 예를 들어, 90`C를 기준으로 쿨링 동작이 동작하면 온도가 90`C 에서 아주 빠르게 흔들리면 오동작을 일으킬 수 있다. 그래서 구간 개념으로 임계값을 설정해야 한다. 일반적으로 위에 `temperature`가 `high threshodl`, `temperature - hysteresis`가 `low threshold`가 된다.
    - type : 해당 trip point에서 어떤 행동을 할지를 나타낸다.

    - Optional 
    5. cooling-maps : `cooling device`와 `trip point`을 연결시키는 노드.

    6. coefficient : 뒤에서 다시 다룬다.

     

     

    : 위 예시에서는 한 개의 `thermal-zone(cpu-thermal)`만 존재한다. 해당, 영역은 일반적으로는 1000ms로 폴링하다가, passive action 동작이 수행되면, 250ms로 폴링을 하게된다. 그리고, 온도 센서로 `bandgap0` 을 사용한다. 해당 영역에 트립 포인트는 다음과 같다.

     

    1. cpu_alert0 : 90`C 도에서 active action 동작.  
    2. cpu_alert1 : 100`C 도에서 passive action 동작.
    3. cpu_crit : 125`C 도에서 셧다운 동작.

     

     

    : 실제 위에 각각 트립 포인터에 어떤 쿨링 동작들이 수행될까? `cooling-maps` 노드를 확인하면 된다.

     

    1. map0 : `cpu_alert0` 트립 포인트에서 `fan0`의 쿨링 상태(동작) 0 ~ 4를 적용
    2. map1 : `cpu_alert1` 트립 포인트에서는 2가지 쿨링 동작을 동시에 수행한다.
    - `fan0`의 쿨링 상태(동작) 5 ~ 9 적용
    - `cpu0`의 쿨링 상태(동작) 0 ~ 3 적용

     

    : `THERMAL_NO_LIMIT` 값은 실제로는 `-1`이다. 그런데, 이 값은 최소값에 들어가면 최소값으로 전달되고, 최대값에 들어가면 최대값으로 전달된다. 즉, 아래와 같아진다[참고1].

    <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT> = <&cpu0 0 3>

     

     

    : 또 다른 예시를 살펴보자. 아래 예시는 SoC 안에 한 개의 온도 센서 IC가 존재하는데, 3개의 `thermal-zone`이 존재한다. 온도 센서 IC 하나인데, 어떻게 해야 할까? 온도 센서 IC 내부에는 기본적으로 다수의 온도 센서들이 존재한다. 그렇다면, 이제 문제는 어떻게 각 `thermal-zone`에 온도 센서 한 개를 대응시키냐가 문제다. 온도 센서 IC 노드(`bandgap0`)에 `thermal-sensor-cells = <1>`을 작성하면, 두 번째 인자로 온도 센서 ID를 보낼 수 있다.

     

    #include <dt-bindings/thermal/thermal.h>
    
    ocp {
    	...
    	/*
    	 * A simple IC with several bandgap temperature sensors.
    	 */
    	bandgap0: bandgap@0000ed00 {
    		...
    		#thermal-sensor-cells = <1>;
    	};
    };
    
    thermal-zones {
    	cpu_thermal: cpu-thermal {
    		polling-delay-passive = <250>; /* milliseconds */
    		polling-delay = <1000>; /* milliseconds */
    
    				/* sensor       ID */
    		thermal-sensors = <&bandgap0     0>;
    
    		trips {
    			/* each zone within the SoC may have its own trips */
    			cpu_alert: cpu-alert {
    				temperature = <100000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "passive";
    			};
    			cpu_crit: cpu-crit {
    				temperature = <125000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "critical";
    			};
    		};
    
    		cooling-maps {
    			/* each zone within the SoC may have its own cooling */
    			...
    		};
    	};
    
    	gpu_thermal: gpu-thermal {
    		polling-delay-passive = <120>; /* milliseconds */
    		polling-delay = <1000>; /* milliseconds */
    
    				/* sensor       ID */
    		thermal-sensors = <&bandgap0     1>;
    
    		trips {
    			/* each zone within the SoC may have its own trips */
    			gpu_alert: gpu-alert {
    				temperature = <90000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "passive";
    			};
    			gpu_crit: gpu-crit {
    				temperature = <105000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "critical";
    			};
    		};
    
    		cooling-maps {
    			/* each zone within the SoC may have its own cooling */
    			...
    		};
    	};
    
    	dsp_thermal: dsp-thermal {
    		polling-delay-passive = <50>; /* milliseconds */
    		polling-delay = <1000>; /* milliseconds */
    
    				/* sensor       ID */
    		thermal-sensors = <&bandgap0     2>;
    
    		trips {
    			/* each zone within the SoC may have its own trips */
    			dsp_alert: dsp-alert {
    				temperature = <90000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "passive";
    			};
    			dsp_crit: gpu-crit {
    				temperature = <135000>; /* millicelsius */
    				hysteresis = <2000>; /* millicelsius */
    				type = "critical";
    			};
    		};
    
    		cooling-maps {
    			/* each zone within the SoC may have its own cooling */
    			...
    		};
    	};
    };

     

    : 3개의 온도 센서가 다음과 같이 대응되고 있다.

    1. bandgap0[0] - cpu_thermal
    2. bandgap0[1] - gpu_thermal
    3. bandgap0[2] - dsp_thermal

     

     

    : 또 다른 케이스를 살펴보자. 이번에는 다수의 온도 센서가 한 개의 `thermal-zone`에 등록되는 케이스다. 여기서 눈여겨 볼 부분은 `coeffcients` 속성이다. 이 속성은 여러 온도 센서를 통해서 하나의 `thermal-zone`를 보다 정확하게 측정하기 사용한다. 이 때, `coeffcients` 속성이 측정된 다수의 온도 센서들의 값들을 어떻게 계산될 지를 알려준다.

    #include <dt-bindings/thermal/thermal.h>
    
    &i2c1 {
    	...
    	/*
    	 * A simple IC with a single temperature sensor.
    	 */
    	adc: sensor@49 {
    		...
    		#thermal-sensor-cells = <0>;
    	};
    };
    
    ocp {
    	...
    	/*
    	 * A simple IC with a single bandgap temperature sensor.
    	 */
    	bandgap0: bandgap@0000ed00 {
    		...
    		#thermal-sensor-cells = <0>;
    	};
    };
    
    thermal-zones {
    	cpu_thermal: cpu-thermal {
    		polling-delay-passive = <250>; /* milliseconds */
    		polling-delay = <1000>; /* milliseconds */
    
    		thermal-sensors = <&bandgap0>,	/* cpu */
    				  <&adc>;	/* pcb north */
    
    		/* hotspot = 100 * bandgap - 120 * adc + 484 */
    		coefficients =		<100	-120	484>;
    
    		trips {
    			...
    		};
    
    		cooling-maps {
    			...
    		};
    	};
    };

     

    : `coefficients` 속성은 `thermal-sensors` 속성과 함께 적용된다. 위에 내용을 토대로 하면 다음과 같은식이 나온다.

    hotspot = 100 * bandgap - 120 * adc + 484

     

     

    : 위와 같은 식이 나오는 이유는 기본적으로 `coefficients` 속성이 만들어 내는 공식때문이다. 위에 디바이스 트리를 기준으로 보면, `c0 = 100, x0 = bandgap, c1 = -120, x1 = adc, c2 = 484` 가 된다. 위에서 보면, `484`는 대응되는 센서가 없다. 이럴 경우, 그냥 더해진다.

    Z = c0 * x0 + c1 * x1 + ... + c(n-1) * x(n-1) + cn.

    The coefficients are ordered and they match with sensors by means of sensor ID. Additional coefficients are interpreted as constant offset.

    - 참고 : https://www.kernel.org/doc/Documentation/devicetree/bindings/thermal/thermal.txt

     

     

    : 그런데, `Thermal framework`는 매번 `thermal sensor`를 polling 방식으로 읽어야 하는 걸까? 인터럽트 방식은 없을까? 아래와 같이 `polling-delay` / `polling-delay-passiv` 속성은 `0` 값을 가질 수 있다. 이렇게, `0`을 설정하면 인터럽트가 트리거되는 시점에 온도 센서를 읽겠다는 뜻이다.

    // https://www.cnblogs.com/hellokitty2/p/15600099.html
    thermal_zones: thermal-zones {
        ...
        soc_max {
            polling-delay = <0>; 
            polling-delay-passive = <0>; 
            thermal-governor = "step_wise"; 
            thermal-sensors = <&lvts 0>; /* 
    
            trips { 
                soc_max_crit: soc_max_crit@0 {
                    temperature = <116500>;
                    hysteresis = <2000>;
                    type = "critical"; 
                };
            };
        };
        ...
    }

     

     

    : 인터럽트를 이용해서 온도 센서를 읽는 타이밍을 잡는 구조는 대개 온도 센서 디바이스에 특정 threshold 값을 설정해놓고, 그 값을 넘으면 Host쪽으로 인터럽트를 발생시키는 구조로 되어있다. 그래서 온도 센서 노드에 대개 인터럽트 관련 내용이 작성되어 있다. 참고로, 위에 `thermal-governor` 속성이 보이는데, 커널 버전 6.5를 기준으로는 사라진 속성이다.

     

    : 퀄컴 `TSENS` 드라이버에서 위에서 말한 부분을 살펴볼 수 있다.

    // https://elixir.bootlin.com/linux/v6.5/source/Documentation/devicetree/bindings/thermal/thermal-sensor.yaml
    #include <dt-bindings/interrupt-controller/arm-gic.h>
    
        // Example 1: SDM845 TSENS
        soc: soc {
                #address-cells = <2>;
                #size-cells = <2>;
    
                /* ... */
    
                tsens0: thermal-sensor@c263000 {
                        compatible = "qcom,sdm845-tsens", "qcom,tsens-v2";
                        reg = <0 0x0c263000 0 0x1ff>, /* TM */
                              <0 0x0c222000 0 0x1ff>; /* SROT */
                        #qcom,sensors = <13>;
                        interrupts = <GIC_SPI 506 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 508 IRQ_TYPE_LEVEL_HIGH>;
                        interrupt-names = "uplow", "critical";
                        #thermal-sensor-cells = <1>;
                };
    
                tsens1: thermal-sensor@c265000 {
                        compatible = "qcom,sdm845-tsens", "qcom,tsens-v2";
                        reg = <0 0x0c265000 0 0x1ff>, /* TM */
                              <0 0x0c223000 0 0x1ff>; /* SROT */
                        #qcom,sensors = <8>;
                        interrupts = <GIC_SPI 507 IRQ_TYPE_LEVEL_HIGH>,
                                     <GIC_SPI 509 IRQ_TYPE_LEVEL_HIGH>;
                        interrupt-names = "uplow", "critical";
                        #thermal-sensor-cells = <1>;
                };
        };

     

     

     

    - Thermal sysfs [ 참고1 참고2 ]

    : 기본적으로 `Thermal framework`는 `/sys/class/thermal/**/ 에 exported 된다.

    1. /sys/class/thermal/thermal_zone[0-*] : thermal zone sysfs
    2. /sys/class/thermal/cooling_device[0-*] : cooling device sysfs

     

     

    : `sysfs`는 `power allocator` 관련 부분을 제외하고는 크게 어려운 부분이 없기 때문에, 각 속성들에 대한 자세한 설명은 위 `참조1` 링크를 참고하자.

Designed by Tistory.