ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] PM - Wakeup source
    Linux/kernel 2023. 8. 30. 15:21

    글의 참고

    - http://www.wowotech.net/pm_subsystem/wakelocks.html

    - https://elinux.org/Android_Power_Management

    - https://www.linaro.org/blog/converting-code-implementing-suspend-blockers/

    - https://static.lwn.net/images/pdf/suspend_blockers.pdf

    - https://blog.csdn.net/hunan4222/article/details/100170965

    - https://www.kernel.org/doc/Documentation/devicetree/bindings/power/wakeup-source.txt


    글의 전제

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

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


    글의 내용

    - Wake lock

    : `wakeup source`를 언급할 때, wake lock이 빠질 수가 없다. 왜냐면, wakeup source는 wake lock을 기반으로 만들어졌기 때문이다. `wake lock`은 안드로이드에서 만든 개념이다. `wake lock`은 시스템 전체가 suspend 모드로 진입하는 것을 막는다. 즉, 시스템이 wake lock이 잡혀있으면, suspend 모드로 진입하지 못하게 된다. 그런데, wake lock은 리눅스 진영과 안드로이드 진영에서 심하게 대립했었다. 이러한 대립은 기본적으로 리눅스 진영에서 x86 기반의 디바이스만 지원했을 때 나왔던 얘기다.

    In the past, the Android and Linux development communities had different (and sometimes opposing) viewpoints on power management. The fact that Linux community worked most of the time with devices that were connected to a power source while the Android community worked with battery-powered devices resulted in two strategies in the field of power management:

    - Non-aggressive suspend strategy, where a hardware block can be put into low-power mode or completely powered off if it is not used, and
    - Aggressive suspend strategy, where a hardware block should be powered on only when needed.

    These different viewpoints were best summarized in the white paper by Rafael J. Wysocki “Technical Background of the Android Suspend Blockers Controversy”.

    Most of the issues between communities have now been laid to rest with the Linux community adopting functionality similar to what was used on Android devices, making it possible to support both strategies in the upstream kernel.

    - 참고 : https://www.linaro.org/blog/converting-code-implementing-suspend-blockers/

    : 현재는 리눅스에서 ARM과 같은 소형 임베디드 디바이스도 지원하면서, 안드로이드 wake lock과 같은 기능을 채택했다.

     

    : 안드로이드 wake lock은 `driver/android/power.c`에 작성되어 있고, 리눅스 진영의 wake lock은 `kernel/power/wakelock.c`에 있다. 안드로이드 wake lock은 기능 동작이 심플하다. `/sys/power/wake_lock`에 임의의 문자열을 쓰면, 해당 문자열에 매핑되는 wake lock이 생성된다. 이 wake lock은 시스템이 저전력 상태가 되는 것을 막는다. `/sys/power/wake_unlock`에 앞에서 만든 wake lock의 이름을 쓰면, 시스템을 잡고 있던 wake lock이 해제된다. 시스템에 wake lock이 존재하지 않으면 자동으로 suspend 모드로 진입한다. 안드로이드 wake lock은 커널 인터페이스도 제공한다. 커널에서는 아래의 2개 인터페이스를 사용해서 커널 레벨의 wake lock을 생성할 수 있다.

    void wake_lock(struct wake_lock *lock);
    void wake_unlock(struct wake_lock *lock);

    : 자세한 `wake lock`의 동작을 알아보기 전에 리눅스 커널의 `wake lock 및 wakeup source` 관련 몇 가지 전제를 미리 알아두고 가자.

     

    1. `wake lock 및 wakeup source`는 시스템 절전 모드와 관련된 객체다.
    2. `wake lock & wakeup source`는 능동적인 객체가 아닌, 수동적 객체다. 즉, 시스템 suspend 과정에서 단지 저 놈들이 활성화 되어있었기 때문에 suspend 상태로 진입하지 못하는 것이다.

     

     

    : PM 코어는 적극적으로 시스템을 절전 모드 상태로 보내려고 하는 객체다. 그런데, 그 과정에서 `wake lock & wakeup source`가 존재해서 시스템을 suspend로 집어넣지 못하게 될 뿐이다. 그래서, 리눅스 커널 소스에서 suspend 진입 코드는 많지만, 시스템을 직접적으로 wakeup 시키는 코드는 찾을 수 없을 것이다(단지, `wake lock & wakeup source`를 active 시키는 코드만 찾을 수 있을 것이다).

     

     

    - Wake lock 동작

    : `wake lock` 기능은 리눅스에서 필수적인 기능이 아니다. 그러므로, CONFIG_PM_WAKELOCKS을 통해서 `활성화/비활성화`가 가능하다. `wake_lock_XXXXX`과 `wake_unlock_XXXXX` 함수들은 sysfs에 export 되서 유저 스페이스에서 접근가능한 함수들이다. wake lock의 실제 `쓰기` 인터페이스는 `pm_wake_lock`과 `pm_wake_unlock`이 사용되는 것을 볼 수 있다. `읽기` 인터페이스는 `pm_show_wakelocks`가 사용되는 것을 알 수 있다. 이 모든 인터페이스는 `/kernel/power/wakelock.c` 에 구현되어 있다.

    //kernel/power/main.c -v6.5
    #ifdef CONFIG_PM_WAKELOCKS
    static ssize_t wake_lock_show(struct kobject *kobj,
    			      struct kobj_attribute *attr,
    			      char *buf)
    {
    	return pm_show_wakelocks(buf, true);
    }
    
    static ssize_t wake_lock_store(struct kobject *kobj,
    			       struct kobj_attribute *attr,
    			       const char *buf, size_t n)
    {
    	int error = pm_wake_lock(buf);
    	return error ? error : n;
    }
    
    power_attr(wake_lock);
    
    static ssize_t wake_unlock_show(struct kobject *kobj,
    				struct kobj_attribute *attr,
    				char *buf)
    {
    	return pm_show_wakelocks(buf, false);
    }
    
    static ssize_t wake_unlock_store(struct kobject *kobj,
    				 struct kobj_attribute *attr,
    				 const char *buf, size_t n)
    {
    	int error = pm_wake_unlock(buf);
    	return error ? error : n;
    }
    
    power_attr(wake_unlock);
    
    #endif /* CONFIG_PM_WAKELOCKS */

     

     

    : `pm_wake_lock` 함수는 새로운 wake lock을 만드는 함수다. 유저 스페이스든 커널 스페이스든 wake lock을 만들기 위해서는 결국 이 함수를 호출해야 한다.

    //kernel/power/wakelock.c - v6.5
    int pm_wake_lock(const char *buf)
    {
    	const char *str = buf;
    	struct wakelock *wl;
    	u64 timeout_ns = 0;
    	size_t len;
    	int ret = 0;
    
    	if (!capable(CAP_BLOCK_SUSPEND))
    		return -EPERM;
    
    	while (*str && !isspace(*str))
    		str++;
    
    	len = str - buf;
    	if (!len)
    		return -EINVAL;
    
    	if (*str && *str != '\n') {
    		/* Find out if there's a valid timeout string appended. */
    		ret = kstrtou64(skip_spaces(str), 10, &timeout_ns);
    		if (ret)
    			return -EINVAL;
    	}
    
    	mutex_lock(&wakelocks_lock);
    
    	wl = wakelock_lookup_add(buf, len, true);
    	if (IS_ERR(wl)) {
    		ret = PTR_ERR(wl);
    		goto out;
    	}
    	if (timeout_ns) {
    		u64 timeout_ms = timeout_ns + NSEC_PER_MSEC - 1; // #define NSEC_PER_MSEC	1000000L
    
    		do_div(timeout_ms, NSEC_PER_MSEC);
    		__pm_wakeup_event(wl->ws, timeout_ms);
    	} else {
    		__pm_stay_awake(wl->ws);
    	}
    
    	wakelocks_lru_most_recent(wl);
    
     out:
    	mutex_unlock(&wakelocks_lock);
    	return ret;
    }

     

     

    : 제일 먼저 현재 프로세스가 `CAP_BLOCK_SUSPEND`의 권한을 갖고 있는지 체크한다. 만약, 권한이 없다면 당연히 웨이크-락프로세스는 종료된다. 사용자가 직접 시스템 전력 관리 개입하는 것은 굉장히 좋지 못한 행위다. 악위적으로 시스템을 계속 awake 상태로 놓을 수 도 있기 때문이다. 그래서, 리눅스에서는 권한 검사를 위해 `capabilities`라는 개념을 도입했다. 모든 프로세스들은 커널의 리소스를 사용하고 싶다면, 대응하는 `capabilities`를 가지고 있어야 한다.

     

     

    : 두 번째는 문자열 검사를 한다. 예를 들어, 유저 스페이스에서 `echo yohda 1000 > /sys/power/wake_lock` 을 입력하면, 커널에서는 `yohda 1000`을 받게된다. 여기서 `while(*str && !isspace(*str))`는 띄어쓰기를 만날 때 까지 str의 포인터를 증가시키는 거다. 즉, str이 `yohda 1000`라면, 5번째 인덱스에서 루프를 종료한다. 그래서 `len`은 wake lock name의 길이가 된다. 그 다음 str에서 스페이스를 제거하고 해당 값을 타임 아웃 딜레이로 사용한다(`timeout_ns`).

     

     

    : `wakelock_lookup_add` 함수는 동일한 이름의 웨이크-락이 존재하는지를 검사한다. 이미 존재하면, 해당 웨이크-락을 반환한다. 만약 존재하지 않는다면, 새로운 wake lock을 만들고 거기에 wakeup soure를 할당해서 반환한다. 이 함수는 아래에서 자세하게 설명한다.

     

     

    : `timeout_ns` 값이 존재하면 `__pm_wakeup_event` 함수에서 사용 가능한 ms 단위로 바꿔야 한다. `timeout_ns + NSEC_PER_MSEC - 1` 은 올림 계산을 위해 적용하는 것이다. 예를 들어, 4095B를 1KB 단위로 바꾸고 싶다고 하자. 그렇면, 4095 / 1024 = 3.99902... 가 된다. 여기서 (4095 +1024−1) / 1024 = 4.9980... 가 된다. 즉, 올림시킨 것이다. 그렇면, 4096B면 어떨가? 4096은 1024로 딱 나누어 떨어지기 때문에 올림을 해서는 안된다. 그래서 `-1`을 하는것이다. (4096 +1024−1) / 1024 = 4.9990... 이 된다. 즉, 딱 나누어 떨어지면 조작하지 않고 그대로 사용하고, 딱 나누어 떨어지지 않으면 올림해서 사용하겠다는 소리다. `do_div` 함수는 나눗셈 함수다. ns 값을 ms 단위로 나눠져야 ms 값이 되기 때문이다. 그런데, 왜 `/` 연산자를 안쓰고 `do_div` 매크로를 사용할까? `do_div` 매크로는 몫과 나머지를 2개를 반환받고 싶을 때 사용한다. 심지어, `do_div`는 단일 나머지 연산자로 몫과 나머지를 모두 구한다. 그렇기 때문에 포퍼먼스 측면에서도 `/`과 `%` 연산자를 사용하는 것보다 효율이 좋다[링크].

     

     

    : `waklock_lookup_add` 함수는 제일 먼저 동일한 이름의 웨이크-락이 있는지를 검사한다. 동일한 이름의 웨이크-락이 없다면, 새로운 웨이크락을 만든다. wake lock을 관리하는데, Red-Black(RB) 트리를 사용한다.

    //kernel/power/Kconfig - v6.5
    config PM_WAKELOCKS
    	bool "User space wakeup sources interface"
    	depends on PM_SLEEP
    	help
    	Allow user space to create, activate and deactivate wakeup source
    	objects with the help of a sysfs-based interface.
    
    config PM_WAKELOCKS_LIMIT
    	int "Maximum number of user space wakeup sources (0 = no limit)"
    	range 0 100000
    	default 100
    	depends on PM_WAKELOCKS
    
    config PM_WAKELOCKS_GC
    	bool "Garbage collector for user space wakeup sources"
    	depends on PM_WAKELOCKS
    	default y
    ...
    ...
    
    //kernel/power/wakelock.c - v6.5
    static struct rb_root wakelocks_tree = RB_ROOT; // #define RB_ROOT	(struct rb_root) { NULL, }
    ...
    
    static struct wakelock *wakelock_lookup_add(const char *name, size_t len,
    					    bool add_if_not_found)
    {
    	struct rb_node **node = &wakelocks_tree.rb_node;
    	struct rb_node *parent = *node;
    	struct wakelock *wl;
    
    	while (*node) {
    		int diff;
    
    		parent = *node;
    		wl = rb_entry(*node, struct wakelock, node); // #define	rb_entry(ptr, type, member) container_of(ptr, type, member)
    		diff = strncmp(name, wl->name, len);
    		if (diff == 0) {
    			if (wl->name[len])
    				diff = -1;
    			else
    				return wl;
    		}
    		if (diff < 0)
    			node = &(*node)->rb_left;
    		else
    			node = &(*node)->rb_right;
    	}
    	if (!add_if_not_found)
    		return ERR_PTR(-EINVAL);
    
    	if (wakelocks_limit_exceeded())
    		return ERR_PTR(-ENOSPC);
    
    	/* Not found, we have to add a new one. */
    	wl = kzalloc(sizeof(*wl), GFP_KERNEL);
    	if (!wl)
    		return ERR_PTR(-ENOMEM);
    
    	wl->name = kstrndup(name, len, GFP_KERNEL);
    	if (!wl->name) {
    		kfree(wl);
    		return ERR_PTR(-ENOMEM);
    	}
    
    	wl->ws = wakeup_source_register(NULL, wl->name);
    	if (!wl->ws) {
    		kfree(wl->name);
    		kfree(wl);
    		return ERR_PTR(-ENOMEM);
    	}
    	wl->ws->last_time = ktime_get();
    
    	rb_link_node(&wl->node, parent, node);
    	rb_insert_color(&wl->node, &wakelocks_tree);
    	wakelocks_lru_add(wl);
    	increment_wakelocks_number();
    	return wl;
    }

     

     

    : 탐색시에 조금 특별한 경우가 있을 수 있다. 예를 들어, `yohda123`과 `yohda` 웨이크-락이 존재하면 어떻게 될까? `strncmp` 함수는 이러한 부분을 체크해주지 못하기 때문에, `if (wl->name[len])` 에서 값이 NULL이 아니면, 뒤에 문자열이 더 존재한다는게 된다. 그래서 `yohda`이 아닌, `yohda123`가 된다. 그리고 문자열 비교가 strncmp 함수를 사용하므로, 아스키 코드상 값이 작으면 왼쪽, 값이 크면 오른쪽으로 순회를 한다. 

     

     

    : 순회를 했는데, 동일한 이름이 없다면 이제 wake lock을 새로 만들어야 한다. 만드는 과정 자체는 단순하기 때문에 설명은 생략한다. 그런데, 새로 만든 wake lock을 추가할 때, RB-tree에 추가되는 것은 알겠는데 `wakelocks_lru_add` 함수는 뭘까? 해당 내용은 뒤에서 다시 설명한다.

     

     

    : `pm_wake_unlock` 함수는 현재 active 되어있는 wake lock을 de-active 만들기 위해 사용된다. 앞 부분은 `pm_wake_lock` 함수와 동일하므로, 자세한 설명은 생략한다. `__pm_relax` 함수를 통해 PM Core에게 wake lock의 wakeup source를 이제 de-active 시켜도 된다고 알린다. 그 이후에 2가지 함수가 호출된다. 짧게 설명하면, wake lock을 해제하는 시점에 가비지 컬렉션을 동작시킨다고 보면 된다. 가비지 컬렉션 내용은 뒤에서 다시 설명한다.

    //kernel/power/wakelock.c - v6.5
    int pm_wake_unlock(const char *buf)
    {
    	struct wakelock *wl;
    	size_t len;
    	int ret = 0;
    
    	if (!capable(CAP_BLOCK_SUSPEND))
    		return -EPERM;
    
    	len = strlen(buf);
    	if (!len)
    		return -EINVAL;
    
    	if (buf[len-1] == '\n')
    		len--;
    
    	if (!len)
    		return -EINVAL;
    
    	mutex_lock(&wakelocks_lock);
    
    	wl = wakelock_lookup_add(buf, len, false);
    	if (IS_ERR(wl)) {
    		ret = PTR_ERR(wl);
    		goto out;
    	}
    	__pm_relax(wl->ws);
    
    	wakelocks_lru_most_recent(wl);
    	wakelocks_gc();
    
     out:
    	mutex_unlock(&wakelocks_lock);
    	return ret;
    }

     

     

    : wake lock 구조체는 안보고 갈 수가 없다. 상당히 심플한 구조다. 실제 wakeup 기능은 모두 wakeup source가 담당한다. 여기서 반드시 구분해야 하는 내용이 있다. `struct wakelock` 구조체를 보면 2개의 자료 구조를 사용한다. wake lock에서 실제 데이터를 조회, 삭제, 추가 등 작업을 하는 자료 구조는 레드 블랙다. 리스트는 단순히 LRU 알고리즘용으로 존재하는 자료 구조다.

    //kernel/power/wakelock.c - v6.5
    struct wakelock {
    	char			*name;
    	struct rb_node		node;
    	struct wakeup_source	*ws;
    #ifdef CONFIG_PM_WAKELOCKS_GC
    	struct list_head	lru;
    #endif
    };

     

     

     

     

    - Wake lock GC

    : `웨이크-락 GC`는 `가비지 컬렉션`을 의미한다. 웨이크-락의 라이프 사이클은 wakeup source가 active 되어 있는 동안만 존재한다. wakeup source가 deactive되면, wake lock은 다시 제거된다. 그런데, 동일한 wakeup event가 빈번하게 발생하면 `생성->제거->생성->제거-> ... ` 주기가 반복되면서 레이턴시가 발생하게 된다.

     

    : 이러한 이유로 wakeup source가 deactive 시점에 wake lock을 해제하지 않고, 별도의 자료 구조에서 관리하는 것이 좋아보인다. 근데, wake lock이 너무 많아지는 것도 문제가 될 수 있다. 왜냐면, 현재 시점에 사용되지도 wake lock에 쓸데없이 메모리를 많이 소모할 우려가 있기 때문이다. 그래서 GC 메커니즘이 등장했고, GC는 `LRU` 알고리즘을 사용한다. 이 기능은 필수가 아니기 때문에 `CONFIG_PM_WAKELOCKS_GC`로 활성 및 비활성화가 가능하다. 만약, `CONFIG_PM_WAKELOCKS_GC`를 사용하지 않으면, wake locks들을 모두 유지한다. 즉, GC 기능을 사용하지 않기 때문에 wake lock들을 계속 메모리에 유지시킨다. `CONFIG_PM_WAKELOCKS_GC`가 사용되면 특정 조건이 만족했을 때, 최근에 자주 사용되지 않던 wake lock을 제거한다.

     

    : `worklocks_lru_list`이 wake locks을 관리하는 방법은 HEAD에 가장 가까운 wake lock이 가장 최근에 액세스한 wake lock을 의미하게 된다. 이 말은 HEAD에서 멀어질 수 록 가장 오랫동안 사용하지 않는 wake lock 이라는 소리다. `pm_wake_lock` 및 `pm_wake_unlock` 에서 `wakelocks_lru_most_recent` 함수를 호출해서 현재 사용된 wake lock을 제일 앞으로 이동시킨다(`list_move`). 근데, `pm_wake_lock` 함수에서 `wakelocks_lru_most_recent`을 호출하는 것은 이해가 가는데, `pm_wake_unlock`에서 `wakelocks_lru_most_recent`를 호출하는 것은 조금 이상해보인다.

     

    : LRU란 `Least Recently Used`의 약자다. 즉, 최근에 가장 사용하지 않는 것을 제거하는 메커니즘이다. LRU는 LFU와 헷갈리면 안된다. LFU는 `Least Frequently Used`의 약자다. 즉, 최근에 가장 적게 참조된 것을 제거하는 메커니즘이다. 즉, LRU는 `시간의 양`과 관련이 있고, LFU는 `참조된 횟수`와 관련이 있다.

     

    : wake lock의 특성상 한 번 active 되면, de-active가 되기전까지 계속 active 를 유지한다. 즉, 단발성이 아닌 지속성이다. 이 말은 wake lock이 할당된 지점부터 해제되기 바로직전까지 계속 사용되었다고 보는 것이 옳다. 그렇기 때문에, `pm_wake_unlock` 에서도 `wakelocks_lru_most_recent` 함수를 호출하는 것이다.

     

    : 왜 GC 동작을 리소스가 해제 되는 시점에 발동시킬까? 이걸 역으로 생각해보면, wake lock 같은 작은 리소스를 관리하기 위해 GC 관련된 작업을 담당하는 스레드를 만드는 것이 좋은 방법일까? wake lock 같은 작은 리소스를 관리하기 위해 별도의 스레드를 할당하는 것은 코드의 복잡성만 증가시키고 동기화 문제까지 발생할 수 있다. 심플한게 좋다. wake lock GC의 핵심은 자신만의 알고리즘을 통해 메모리에서(RB-tree) 해제될 가장 적합한 리소스를 찾는 것이다. 그렇다면, 일단 해제 리소스가 있어야 한다. 그 중에서 어떤 리소스를 아예 해제 시킬지를 정해야 한다. 그런데 해제할 리소스도 없는데, GC가 실행되는 것은 옳지 못하다. 즉, GC는 해제할 리소스가 있을 때 불려야 한다. wake lock 리소스의 해제는 `pm_wake_unlock` 함수에서 담당한다. 그러므로, `wakelocks_gc` 함수가 호출되기 가장 적절한 위치는 `pm_wake_unlock`이다.

     

    : wake lock GC 리스트는 `wakelocks_lru_list` 전역 변수다. `wakelocks_gc_count`는 현재 GC 리스트에 추가된 wake locks의 개수를 나타낸다. 

    //kernel/power/wakelock.c - v6.5
    #ifdef CONFIG_PM_WAKELOCKS_GC
    #define WL_GC_COUNT_MAX	100
    #define WL_GC_TIME_SEC	300
    
    static void __wakelocks_gc(struct work_struct *work);
    static LIST_HEAD(wakelocks_lru_list);
    static DECLARE_WORK(wakelock_work, __wakelocks_gc);
    static unsigned int wakelocks_gc_count;
    
    static inline void wakelocks_lru_add(struct wakelock *wl)
    {
    	list_add(&wl->lru, &wakelocks_lru_list);
    }
    
    static inline void wakelocks_lru_most_recent(struct wakelock *wl)
    {
    	list_move(&wl->lru, &wakelocks_lru_list);
    }
    
    static void __wakelocks_gc(struct work_struct *work)
    {
    	struct wakelock *wl, *aux;
    	ktime_t now;
    
    	mutex_lock(&wakelocks_lock);
    
    	now = ktime_get();
    	list_for_each_entry_safe_reverse(wl, aux, &wakelocks_lru_list, lru) {
    		u64 idle_time_ns;
    		bool active;
    
    		spin_lock_irq(&wl->ws->lock);
    		idle_time_ns = ktime_to_ns(ktime_sub(now, wl->ws->last_time));
    		active = wl->ws->active;
    		spin_unlock_irq(&wl->ws->lock);
    
    		if (idle_time_ns < ((u64)WL_GC_TIME_SEC * NSEC_PER_SEC))
    			break;
    
    		if (!active) {
    			wakeup_source_unregister(wl->ws);
    			rb_erase(&wl->node, &wakelocks_tree);
    			list_del(&wl->lru);
    			kfree(wl->name);
    			kfree(wl);
    			decrement_wakelocks_number();
    		}
    	}
    	wakelocks_gc_count = 0;
    
    	mutex_unlock(&wakelocks_lock);
    }
    
    static void wakelocks_gc(void)
    {
    	if (++wakelocks_gc_count <= WL_GC_COUNT_MAX)
    		return;
    
    	schedule_work(&wakelock_work);
    }
    #else /* !CONFIG_PM_WAKELOCKS_GC */

     

     

    : `pm_wake_unlock` 함수에서 wake lock이 해제되면 `wakelocks_gc` 함수를 호출한다. `wakelocks_gc` 함수는 `GC 카운트`가 100이 넘으면 실제 wake lock GC가 동작한다. wake lock GC는 앞에서도 말했지만, `wakelocks_lru_list` 리스트 뒤쪽에 해제할 wake lock들이 놓인다.

     

     

    : 리스트 루프는 `list_for_each_entry_safe_reverse`를 통해서 실행된다. 이 매크로는 리스트를 뒤에서부터 탐색한다. 그리고, 루프 중간에 리스트를 제거하면 루프에 영향을 줄 수 있다. `list_for_each_entry_safe_reverse`는 중간에 노드를 삭제하더라도 안전하게 루프를 순회한다. 그렇면, 맨 뒤 부터 맨 앞 까지 전부 제거해야 할까? 아니다. 이것도 기준이 있다.

     

    : `wakelocks_lru_list` 리스트에는 최근 가장 오랫동안 사용되지 않는 wake lock이 맨 뒤에 존재한다. 즉, 리스트를 뒤에서부터 보면 가장 오랫동안 사용되지 노드들이 정렬되어 있는 형태다. wake lock GC는 이 부분을 루프 탈출 트리거로 잡는다. 


    idle_time_ns = ktime_to_ns(ktime_sub(now, wl->ws->last_time)) " 현재 시간을 기준으로 wake lock이 마지막으로 활성화됬던 시점을 뺀다. 이 값의 의미는 `현재 시간을 기준으로 해당 wake lock이 얼마나 오랫동안 idle 상태에 있었는지` 에 대한 시간의 양을 나타낸다.

    if (idle_time_ns < ((u64)WL_GC_TIME_SEC * NSEC_PER_SEC)) " 해당 wake lock이 현재 시간을 기준으로 300초 동안 한 번도 사용된 적이 없다면, 그 때 해제한다. 그런데, 만약 300초 동안 idle 상태가 아니었다면 제거하지 않는다.

     

     

    : 사실 300초는 많이 길다. 만약, GC 발동 조건이 되었는데  `wakelocks_lru_list` 리스트의 맨 뒤쪽 노드가 300초 동안 idle 상태가 아니었다면, 앞에 친구들은 당연히 300초가 안될 것이니, 루프를 종료한다. 즉, 하나도 제거하지 않게 된다. 하나 말하지 않은 부분이 있다. GC에 선정이 되었더라도 해당 wake lock이 그 시점에 active 상태라면, 당연히 제거 대상에서 제외된다.

     

     

     

     

    - Wakeup source registration

    : 드라이버 코드를 작성하다 보면, probe 함수 마지막에 `device_init_wakeup` 함수를 볼 수 있다. `device_init_wakeup` 함수는 해당 디바이스를 wakeup source로 등록해주는 함수다. 이 과정이 어떻게 진행되는지 알아보자.

    // drivers/rtc/rtc-at91rm9200.c - v6.5
    /*
     * Initialize and install RTC driver
     */
    static int __init at91_rtc_probe(struct platform_device *pdev)
    {
    	....
    
    	/* cpu init code should really have flagged this device as
    	 * being wake-capable; if it didn't, do that here.
    	 */
    	if (!device_can_wakeup(&pdev->dev))
    		device_init_wakeup(&pdev->dev, 1);
    
            ....
    	return ret;
    }

     

     

    : `device_set_wakeup_capable` 함수에 두 번째 인자로 `true`가 전달되면, 해당 디바이스의 `dev->power.can_wakeup` 플래그 SET하고 `/sys/devices/*/power`에 파워 관련 디렉토리를 생성한다. 만약, `false`가 전달되면 `device_wakeup_enable` 함수를 통해 생되었던 `wakeup_source`가 `device_wakeup_disable` 함수를 호출함으로써 제거한다. 그리고, `device_set_wakeup_capable` 함수에 false를 줌으로써, 남아있던 sysfs 관련 자료들도 모두 제거한다. 

    //include/linux/pm_wakeup.h - v6.5
    /**
     * device_init_wakeup - Device wakeup initialization.
     * @dev: Device to handle.
     * @enable: Whether or not to enable @dev as a wakeup device.
     *
     * By default, most devices should leave wakeup disabled.  The exceptions are
     * devices that everyone expects to be wakeup sources: keyboards, power buttons,
     * possibly network interfaces, etc.  Also, devices that don't generate their
     * own wakeup requests but merely forward requests from one bus to another
     * (like PCI bridges) should have wakeup enabled by default.
     */
    static inline int device_init_wakeup(struct device *dev, bool enable)
    {
    	if (enable) {
    		device_set_wakeup_capable(dev, true);
    		return device_wakeup_enable(dev);
    	} else {
    		device_wakeup_disable(dev);
    		device_set_wakeup_capable(dev, false);
    		return 0;
    	}
    }

     

     

    : `device_wakeup_enable` 함수는 인자로 전달 된 디바이스의 새로 만든 wakeup source(`wakeup_source_register`)를 연결(`device_wakeup_attach`)시킨다. 예외 처리 인자로 꾀나 중요한 내용이 있다. 만약, 이 함수가 호출될 때 시스템이 suspending 중이라면 `wakeup source`를 생성할 수 없다. 중요한 내용이다. 시스템 suspending 중에 기존에 이미 존재했던 wakeup source가 발생시킨 wakeup event는 받을 수 있지만, wakeup source를 새로 생성하는 것은 허락하지 않는다.

    // drivers/base/power/wakeup.c - v6.5 
    /**
     * device_wakeup_enable - Enable given device to be a wakeup source.
     * @dev: Device to handle.
     *
     * Create a wakeup source object, register it and attach it to @dev.
     */
    int device_wakeup_enable(struct device *dev)
    {
    	struct wakeup_source *ws;
    	int ret;
    
    	if (!dev || !dev->power.can_wakeup)
    		return -EINVAL;
    
    	if (pm_suspend_target_state != PM_SUSPEND_ON)
    		dev_dbg(dev, "Suspicious %s() during system transition!\n", __func__);
    
    	ws = wakeup_source_register(dev, dev_name(dev));
    	if (!ws)
    		return -ENOMEM;
    
    	ret = device_wakeup_attach(dev, ws);
    	if (ret)
    		wakeup_source_unregister(ws);
    
    	return ret;
    }
    EXPORT_SYMBOL_GPL(device_wakeup_enable);

     

     

    : `wakeup_source_register` 함수는 인자로 전달된 디바이스에 대응하는 새로운 `ws`를 생성한다. 이 함수는 3가지 업무를 담당한다.

    //drivers/base/power/wakeup.c - v6.5
    /**
     * wakeup_source_register - Create wakeup source and add it to the list.
     * @dev: Device this wakeup source is associated with (or NULL if virtual).
     * @name: Name of the wakeup source to register.
     */
    struct wakeup_source *wakeup_source_register(struct device *dev,
    					     const char *name)
    {
    	struct wakeup_source *ws;
    	int ret;
    
    	ws = wakeup_source_create(name);
    	if (ws) {
    		if (!dev || device_is_registered(dev)) {
    			ret = wakeup_source_sysfs_add(dev, ws);
    			if (ret) {
    				wakeup_source_free(ws);
    				return NULL;
    			}
    		}
    		wakeup_source_add(ws);
    	}
    	return ws;
    }
    EXPORT_SYMBOL_GPL(wakeup_source_register);

     

     

    : 먼저 빈 껍데기 `ws`를 만든다(`이름`과 `ID`만 추가된 상태). 그리고, `wakeup_source_sysfs_add` 함수를 통해 `/sys/class/wakeup*`를 생성한다. 그리고, `wakeup_source_add` 함수는 `ws`에 필수적인 정보들을 더 추가한다.

     

     

    : `wakeup_source_create` 함수는 실제 `ws`를 만드는 작업을 한다. 인자로 받은 `ws`의 이름과 `ws`를 구분할 id를 할당받는다.

    //drivers/base/power/wakeup.c - v6.5
    /**
     * wakeup_source_create - Create a struct wakeup_source object.
     * @name: Name of the new wakeup source.
     */
    struct wakeup_source *wakeup_source_create(const char *name)
    {
    	struct wakeup_source *ws;
    	const char *ws_name;
    	int id;
    
    	ws = kzalloc(sizeof(*ws), GFP_KERNEL);
    	if (!ws)
    		goto err_ws;
    
    	ws_name = kstrdup_const(name, GFP_KERNEL);
    	if (!ws_name)
    		goto err_name;
    	ws->name = ws_name;
    
    	id = ida_alloc(&wakeup_ida, GFP_KERNEL);
    	if (id < 0)
    		goto err_id;
    	ws->id = id;
    
    	return ws;
    
    err_id:
    	kfree_const(ws->name);
    err_name:
    	kfree(ws);
    err_ws:
    	return NULL;
    }
    EXPORT_SYMBOL_GPL(wakeup_source_create);

     

     

    : `wakeup_source_create` 함수를 통해 정상적으로 `ws`가 만들어지면, 몇 가지 초기화 작업을 한 다음에 디바이스에 연결해야 한다. 이 때, `ws`에 초기화 작업을 진행하는 함수가 `wakeup_source_add` 함수다. 

    // drivers/base/power/wakeup.c - v6.5
    /**
     * wakeup_source_add - Add given object to the list of wakeup sources.
     * @ws: Wakeup source object to add to the list.
     */
    void wakeup_source_add(struct wakeup_source *ws)
    {
    	unsigned long flags;
    
    	if (WARN_ON(!ws))
    		return;
    
    	spin_lock_init(&ws->lock);
    	timer_setup(&ws->timer, pm_wakeup_timer_fn, 0);
    	ws->active = false;
    
    	raw_spin_lock_irqsave(&events_lock, flags);
    	list_add_rcu(&ws->entry, &wakeup_sources);
    	raw_spin_unlock_irqrestore(&events_lock, flags);
    }
    EXPORT_SYMBOL_GPL(wakeup_source_add);

     

     

    : `wakeup_source_add` 함수의 역할은 다음과 같다.

    1" wakeup source default timer 타이머 세팅
    2" default로 `active`는 false로 설정. 즉, `ws`가 생성되는 시점에는 비활성화 상태로 만들어진다.
    3" 새로 생성된 `ws`를 글로벌 리스트인 `wakeup_sources` 에 추가한다. 

     

     

    : `ws` 타이머는 웨이크-업을 언제까지 유지할 지를 결정한다. 즉, `pm_wakeup_timer_fn`은 일정 시간동안 시스템을 awake 시키고, 시간이 지나면 해당 `ws`를 비활성화 시킨다. `pm_wakeup_timer_fn` 함수에서 시간이 경과하면, `wakeup_source_deactivate` 함수를 통해 비활성화 시키는 것을 확인할 수 있다.

    //drivers/base/power/wakeup.c - v6.5
    /**
     * pm_wakeup_timer_fn - Delayed finalization of a wakeup event.
     * @t: timer list
     *
     * Call wakeup_source_deactivate() for the wakeup source whose address is stored
     * in @data if it is currently active and its timer has not been canceled and
     * the expiration time of the timer is not in future.
     */
    static void pm_wakeup_timer_fn(struct timer_list *t)
    {
    	struct wakeup_source *ws = from_timer(ws, t, timer);
    	unsigned long flags;
    
    	spin_lock_irqsave(&ws->lock, flags);
    
    	if (ws->active && ws->timer_expires
    	    && time_after_eq(jiffies, ws->timer_expires)) {
    		wakeup_source_deactivate(ws);
    		ws->expire_count++;
    	}
    
    	spin_unlock_irqrestore(&ws->lock, flags);
    }

     

     

    : `ws` 가 성공적으로 생성되었으면, 디바이스에 연결해야 한다. 디바이스와 `ws`의 연결 작업은 `dev->power.wakeup = ws;` 코드를 통해 이루어진다. 그리고, `wakeup interrupt` 관련 내용이 나오는데 이 내용은 이 글에서 다루도록 한다.

    // drivers/base/power/wakeup.c - v6.5
    /**
     * device_wakeup_attach - Attach a wakeup source object to a device object.
     * @dev: Device to handle.
     * @ws: Wakeup source object to attach to @dev.
     *
     * This causes @dev to be treated as a wakeup device.
     */
    static int device_wakeup_attach(struct device *dev, struct wakeup_source *ws)
    {
    	spin_lock_irq(&dev->power.lock);
    	if (dev->power.wakeup) {
    		spin_unlock_irq(&dev->power.lock);
    		return -EEXIST;
    	}
    	dev->power.wakeup = ws;
    	if (dev->power.wakeirq)
    		device_wakeup_attach_irq(dev, dev->power.wakeirq);
    	spin_unlock_irq(&dev->power.lock);
    	return 0;
    }

     

     

     

     

    - Physical wakeup sources

    : 이상적으로 리눅스는 모든 디바이스들을 wakeup source로 설정할 수 있다. 그러나, 실제로 `wakeup source` 로 지정되는 디바이스들은 다음과 같다.

    1. GPIO wakeup
    2. USB wakeup
    3. Touchscreen wakeup
    4. MMC/SD wakeup
    5. RTC wakeup
    6, DM Timer wakeup

     

     

    - Wakeup source sysfs

    : `ws` 관련해서 sysfs는 3개의 폴더 및 파일이 존재한다.

    개별 디바이스용 wakeup source - wakeup_sysfs_add(`/sys/devices/*/power`) [ 참조1 ]
    시스템 전체 wakeup sources - wakeup_source_sysfs_add(`/sys/class/wakeup*`) [ 참조1 참조2 ]
    디버깅용 wakeup sources - wakeup_sources_debugfs_init(`/sys/kernel/debug/wakeup_sources`) [ 참조1 ]

     

     

    : 위에서 `/sys/class/wakeup*`은 `/sys/kernel/debug/wakeup_sources`를 대체하기 위해 등장했다. `참조` 글들을 읽어보면 알 수 있겠지만, 안드로이드의 많은 유저 애플리케이션이 `/sys/kernel/debug/wakeup_source` 파일에 의존하고 있다고 한다. 그런데, 문제는 저 위치가 `debugfs` 라는 점이다. `debugfs` 파일시스템은 `개발 단계`에서 테스트 및 디버깅용으로 사용하는 파일 시스템이다. `debugfs` 파일시스템은 본질적으로 `제품 단계`에서는 사용하면 안되는 파일 시스템이다. 그러므로, 인터페이스를 `/sys/class`로 옮기고 여러 가지 속성들을 추가했다. 속성을 추가하는 코드는 기능적으로 크게 의미가 있어보이지는 않는다. 그러므로, 각자 분석해보도록 하자.

     

    : `Physical wakeup sources`에서도 보다시피, 리눅스 시스템을 wakeup 시킬 모든 리소스들은 `wakeup source`로 등록이 된다. 즉, 특정 디바이스가 시스템을 wakeup 시킬 기능을 가지고 있다면, sysfs의 각 디바이스의 파일의 `/sys/devices/**/power/wakeup` 파일이 존재하게 된다. 이 말은 굉장히 중요하다. 시스템 절전 모드 상황에서 시스템을 wakeup 시킬 수 있는 디바이스들은 `/sys/devices/**/power/wakeup` 여기안에서 존재한다는 뜻이다. `power/wakeup` 파일은 sysfs를 통해서 터미널에서 On/Off 할 수 있기 때문에, 디버깅도 수월하다. 

Designed by Tistory.