-
[리눅스 커널] Facilities & HelperLinux/kernel 2023. 9. 2. 03:31
글의 참고
- https://classes.engineering.wustl.edu/cse422/code_pointers/05_kernel_code_error_checking.html
- https://kernelnewbies.org/FAQ/LikelyUnlikely
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
- IS_ERR & ERR_PTR & PTR_ERR
: 리눅스 커널 프로그래밍은 반환값으로 포인터와 정수를 같이 사용한다. 이게 근데 생각보다 큰 문제가 있다. 반환값이 포인터 일 때는 관습적으로 `0(NULL)`을 에러로 판단하는데, 정수는 `0`을 성공이라고 판단하기 때문이다. 이렇게 반환값이 통일되지 못하면, 개발이 복잡해질 뿐만 아니라, 나중에 에러가 발생했을 때 디버깅을 하기도 어려워진다.
: 그래서 리눅스 커널은 이 문제를 해결하기 위해 2가지 기발한 아이디어를 생각해냈다.
1" 가상 주소에서 상위 한 페이지(4KB)는 사용하지 않는다. 즉, `0xFFFF_FFFF ~ 0xFFFF_F000`는 유효하지만 다른 곳에서 사용할 수 없도록 한다.
2. 에러는 음수로 사용한다. 음수는 `unsigned`로 표현할 경우, 상위 주소를 표현할 수 있다. 아래에서 다시 다룬다.: 위의 아이디어를 바탕으로 `/include/linux.err.h` 파일에 여러 함수 및 변수들을 정의해 놓았다.
: `IS_ERR_VALUE` 매크로는 에러 포인터를 음수 정수로 바꿔서 리눅스 에러 코드와 비교한다. 이 테크닉은 리눅스 커널에서 에러 코드 넘버로 `-1 ~ -4095`를 사용한다는 것을 이용한다. 음수를 `unsigned` 로 컨버팅하면 어떻게 될까?
(unsigned int)-1 : 0xFFFF_FFFF
(unsigned int)-2 : 0xFFFF_FFFE
...
...
(unsigned int)-0xFFFFFFFE : 0x0000_0002
(unsigned int)-FFFFFFFF : 0x0000_0001: `(unsigned long)-MAX_ERRNO` = 0xFFFF_F001 이다. 이 말은, 아래 `x`가 0xFFFF_FFFF(-1) ~ 0xFFFF_F001(-4095) 범위안에 들어오면 조건은 `true`가 되고, 결론적으로는 에러라는 뜻이다.
// include/linux/err.h - v6.5 /* * Kernel pointers have redundant information, so we can use a * scheme where we can return either an error code or a normal * pointer with the same return value. * * This should be a per-architecture thing, to allow different * error and pointer decisions. */ #define MAX_ERRNO 4095 #ifndef __ASSEMBLY__ /** * IS_ERR_VALUE - Detect an error pointer. * @x: The pointer to check. * * Like IS_ERR(), but does not generate a compiler warning if result is unused. */ #define IS_ERR_VALUE(x) unlikely((unsigned long)(void *)(x) >= (unsigned long)-MAX_ERRNO)
: `IS_ERR_VALUE` 매크로는 `IS_ERR` 함수에서 내부적으로 사용된다. 그리고, `IS_ERR` 함수는 포인터를 인자로 받고 있다. 그렇면, 리눅스 커널에서는 포인터 에러는 모두 0xFFFF_FFFF(-1) ~ 0xFFFF_F001 범위안에 오도록 하는 것일까?
// include/linux/err.h - v6.5 /** * IS_ERR - Detect an error pointer. * @ptr: The pointer to check. * Return: true if @ptr is an error pointer, false otherwise. */ static inline bool __must_check IS_ERR(__force const void *ptr) { return IS_ERR_VALUE((unsigned long)ptr); }
: 리눅스 커널에서는 `ERR_PTR` 함수의 반환값을 `IS_ERR` 함수로 넘기는 구조를 자주 사용한다.
// include/linux/err.h - v6.5 /** * ERR_PTR - Create an error pointer. * @error: A negative error code. * * Encodes @error into a pointer value. Users should consider the result * opaque and not assume anything about how the error is encoded. * * Return: A pointer with @error encoded within its value. */ static inline void * __must_check ERR_PTR(long error) { return (void *) error; } /** * PTR_ERR - Extract the error code from an error pointer. * @ptr: An error pointer. * Return: The error code within @ptr. */ static inline long __must_check PTR_ERR(__force const void *ptr) { return (long) ptr; }
: `_devm_regulator_get_enable` 함수는 내부적으로 `_devm_regulator_get` 함수를 호출하고 있다. `_devm_regulator_get` 함수는 에러 반환시에 `ERR_PTR(-ENOMEM)`을 사용하고 있다. 즉, 음수를 포인터형으로 변경하고 있다.
static struct regulator *_devm_regulator_get(struct device *dev, const char *id, int get_type) { struct regulator **ptr, *regulator; ptr = devres_alloc(devm_regulator_release, sizeof(*ptr), GFP_KERNEL); if (!ptr) return ERR_PTR(-ENOMEM); ... return regulator; } ... ... static int _devm_regulator_get_enable(struct device *dev, const char *id, int get_type) { struct regulator *r; int ret; r = _devm_regulator_get(dev, id, get_type); if (IS_ERR(r)) return PTR_ERR(r); ... }
: 당연한 얘기지만, 에러 코드를 반환할 때는 반드시 `음수`로 만들어줘야 하고 `-4095` 보다 작으면 안된다. 그러므로, 리눅스 커널에서 공식적으로 제공하는 에러 코드를 사용하는 것이 좋다.
https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/errno-base.h https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/errno.h https://elixir.bootlin.com/linux/latest/source/include/linux/errno.h
- likely & unlikely [ 참고1 참고2 ]
: 커널 소스를 분석하다 보면 정말 많이 마주치게 되는 매크로가 바로 `likely`와 `unlikely`다. 이 매크로들은 컴파일러가 코드를 최적화 할 수 있도록 `힌트`를 제공한다. 예를 들어, 아래와 같은 코드는 컴파일러가 최적화를 하는데 있어서 어떠한 힌트도 제공하지 못한다. 컴파일러는 `argc` 값에 대한 힌트가 없기 때문에, 코드를 컴파일 할 때 `re-ordering`을 하지 못한다.
#include <cstdio>
int main(int argc, char *argv[]){if (argc > 0) {puts ("LOVE\n");} else {puts ("SOJU\n");}return 0;}.LC0:.string "LOVE\n".LC1:.string "SOJU\n"main:sub rsp, 8test edi, edijle .L2mov edi, OFFSET FLAT:.LC0call puts.L3:xor eax, eaxadd rsp, 8ret.L2:mov edi, OFFSET FLAT:.LC1call putsjmp .L3: 그냥 쭈욱 해석하면 된다. `sub rsp, 8`은 파라미터 2개를 생성하는 코드다. `test edi, edi`에서 edi 레지스터의 값이 `0`인지와 음수인지 양수인지를 판단한다.`jle`는 위에서 실행한 명령어의 결과가 0과 같거나 더 작으면, `.L2`(`SOJU`)로 분기한다. 만약, 결과가 양수라면 브랜치하지 않고 계속 실행을 이어나간다.
: 여기서 `unlikely`를 이용하면 어떻게 바뀌는지 알아보자. 컴파일러가 C 파일을 어떻게 컴파일 하는지가 핵심이다. 즉, 어셈블리 코드 흐름이 어떻게 바뀌었을까? 일반적인 흐름이라면, 위와 같이 비교값이 양수면 `LOVE`, 음수면 `SOJU` 여야 한다. 그런데, `unlikely` 매크로를 사용하면 일반적인 흐름에 `SOJU`가 있고, 분기로 `LOVE`로 점프한다. 즉, `unlikely` 매크로는 조건문이 발생할 확률이 적다는 것을 컴파일러에게 알려준다.
#include <stdio.h>
//#define likely(x) __builtin_expect(!!(x), 1)#define unlikely(x) __builtin_expect(!!(x), 0)
int main(int argc, char *argv[]){if (unlikely(argc > 0)) {puts ("LOVE\n");} else {puts ("SOJU\n");}return 0;}.LC0:.string "LOVE\n".LC1:.string "SOJU\n"main:sub rsp, 8test edi, edijg .L6mov edi, OFFSET FLAT:.LC1call puts.L3:xor eax, eaxadd rsp, 8ret.L6:mov edi, OFFSET FLAT:.LC0call putsjmp .L3: 그래서 `LOVE`를 출력하는 문장을 뒤로 옮기고, 발생할 확률이 더 높은 `SOJU`를 일반적인 코드 흐름에서 수행되도록 한다. 그렇면, `likely` 매크로도 추측하기가 쉬워진다. 조건문이 발생활 확률이 높다는 것을 컴파일러에게 알려준다. 아래 코드를 보면 일반적인 `argc > 0`과 다를바가 없다. 왜냐면, 인자로 주어진 조건문이 발생할 확률이 높기 때문에, 리-오더링을 하지 않기 때문이다.
#include <cstdio>
#define likely(x) __builtin_expect(!!(x), 1)//#define unlikely(x) __builtin_expect(!!(x), 0)
int main(int argc, char *argv[]){
if (likely(argc > 0)) {puts ("LOVE\n");} else {puts ("SOJU\n");}return 0;}.LC0:.string "LOVE\n".LC1:.string "SOJU\n"main:sub rsp, 8test edi, edijle .L2mov edi, OFFSET FLAT:.LC0call puts.L3:xor eax, eaxadd rsp, 8ret.L2:mov edi, OFFSET FLAT:.LC1call putsjmp .L3: 개인적인 경험으로는 `unlikely`가 조금 더 자주 쓰이는 경향이 있다. 왜냐면, 소스 코드를 짜다보면 에러 처리 코드들이 상당히 많이 들어가는데, 이 코드들은 대개 정상적으로 동작하는 코드 흐름보다는 덜 실행된다. 그래서, 에러 처리 조건문은 대개 `unlikely`로 감싸는 경우가 많다.
- BUG & BUG_ON & WARN_ON_ONCE [참고1]
'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] PM - Wakeup interrupt (0) 2023.09.05 [리눅스 커널] Cpuidle - overview & data structure (0) 2023.09.04 [리눅스 커널] PM - Syscore (0) 2023.09.01 [리눅스 커널] PM - Freezing of task (0) 2023.09.01 [리눅스 커널] PM - Driver Power Management (0) 2023.08.30