ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Facilities & Helper
    Linux/kernel 2023. 9. 2. 03:31

    글의 참고

    - https://classes.engineering.wustl.edu/cse422/code_pointers/05_kernel_code_error_checking.html

    - https://stackoverflow.com/questions/36296130/why-does-is-err-value-cast-negative-max-errno-to-an-unsigned-long

    - https://kernelnewbies.org/FAQ/LikelyUnlikely

    - https://stackoverflow.com/questions/109710/how-do-the-likely-unlikely-macros-in-the-linux-kernel-work-and-what-is-their-ben


    글의 전제

    - 밑줄로 작성된 글은 강조 표시를 의미한다.

    - 그림 출처는 항시 그림 아래에 표시했다.


    글의 내용

    - 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, 8
        test edi, edi
        jle .L2
        mov edi, OFFSET FLAT:.LC0
        call puts
    .L3:
        xor eax, eax
        add rsp, 8
        ret
    .L2:
        mov edi, OFFSET FLAT:.LC1
        call puts
        jmp .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, 8
        test edi, edi
        jg .L6 
        mov edi, OFFSET FLAT:.LC1
        call puts
    .L3:
        xor eax, eax
        add rsp, 8
        ret
    .L6:
        mov edi, OFFSET FLAT:.LC0
        call puts
        jmp .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, 8
        test edi, edi
        jle .L2
        mov edi, OFFSET FLAT:.LC0
        call puts
    .L3:
        xor eax, eax
        add rsp, 8
        ret
    .L2:
        mov edi, OFFSET FLAT:.LC1
        call puts
        jmp .L3

    : 개인적인 경험으로는 `unlikely`가 조금 더 자주 쓰이는 경향이 있다. 왜냐면, 소스 코드를 짜다보면 에러 처리 코드들이 상당히 많이 들어가는데, 이 코드들은 대개 정상적으로 동작하는 코드 흐름보다는 덜 실행된다. 그래서, 에러 처리 조건문은 대개 `unlikely`로 감싸는 경우가 많다.

     

     

    - BUG & BUG_ON & WARN_ON_ONCE [참고1]

Designed by Tistory.