-
[리눅스 커널] LDM - ueventLinux/kernel 2023. 10. 3. 16:58
글의 참고
- https://linux.die.net/man/8/hotplug
- https://lwn.net/Articles/52621/
- https://www.kernel.org/doc/Documentation/usb/hotplug.txt
- https://chromium.googlesource.com/aosp/platform/system/core/+/refs/heads/master/init/README.ueventd.md
- http://www.makelinux.net/ldd3/chp-14-sect-7.shtml
- https://www.cnblogs.com/schips/p/linux_device_model_4.html
- http://www.wowotech.net/device_model/uevent.html/comment-page-2
- https://blog.csdn.net/m0_74282605/article/details/131642984
- https://www.cnblogs.com/superconvert/p/16901792.html
- https://blog.csdn.net/qq_39257814/article/details/132334684
글의 전제
- 밑줄로 작성된 글은 강조 표시를 의미한다.
- 그림 출처는 항시 그림 아래에 표시했다.
글의 내용
- Overview
: `uevent`는 kobject 를 이루는 구성 요소 중 하나라고 볼 수 있다. uevent 는 kobject 의 상태가 변경/추가/삭제 되었을 때, 유저 스페이스로 알리는 역할을 한다. 쉽게, 리눅스에서 uevent 는 hotplug 메커니즘을 구현하기 위한 implementation 이라고 보면 된다. uevent 는 일반적인 `hot-pluggable` 기능을 지원하는 디바이스에서 사용된다. 예를 들어, 외부에서 USB 디바이스가 하나 새로 장착되면, USB 관련 소프트웨어 드라이버가 동적으로 해당 USB 디바이스를 나타내는 구조체를 만들고, 이 사실을 유저 스페이스에 알린다. 그리고, `/dev/`에 관련 device node 를 하나 생성한다.
- Uevent architecture in kernel
: uevent 의 구조는 생각보다 심플하다. Linux Device Model 에 등록된 디바이스에 이벤트(변경/추가/삭제)가 발생할 경우, uevent 가 user psace 로 전달된다. 초기 리눅스에서는 유저 레벨 프로세스인 `/sbin/hotplug` 를 커널 레벨에서 실행해서 hotplug 메커니즘을 구현했다. 이 때, `/sbin/hotplug` 파일은 커널 레벨의 call_usermodehelper 함수를 기반으로 한다. 기술이 발전함에 따라, `udev` 라는 새로운 hotplug 메커니즘이 등장했고, 이 방식은 기존의 `/sbin/hotplug` 를 대체하기 시작했다. udev 는 unix standard socket 기반으로 만들어졌으며, 커널이 netlink 를 통해서 broadcast 하는 uevent 를 listening 하여 처리한다. 정리하면, 리눅스 커널에서 유저 스페이스로 uevent 를 브로드 캐스트하는 방법은 2가지가 있다.
1. netlink 메커니즘을 통해 유저 스페이스에 uevent 를 알린다(New 메커니즘)[참고1].
2. /sbin/hotplug 를 커널 레벨에서 실행해서, 유저 스페이스에 uevent 를 알린다(Old 메커니즘).: 그런데, usermodehelp 방식은 왜 현재 쓰이지 않게 된 것일까? usermodehelper 는 uevent 가 발생할 때 마다, 유저 프로그램을 실행할 프로세스가 필요하다. 이 말은 커널에 등록되는 디바이스의 수가 증가함에 따라, uevent 가 증가함을 뜻하고, uevent 의 증가는 많은 프로세스 생성을 야기한다. 시스템 start-up 시점에 uevent 가 많이 발생하는데, 이건 시스템 성능이 문제가 아니라 fatal issue 가 될 수 도 있다.
- Data structure
: `kobject_action` 열거형은 어떤 uevent 인지를 나타낸다.
// include/linux/kobject.h - v6.5 enum kobject_action { KOBJ_ADD, KOBJ_REMOVE, KOBJ_CHANGE, KOBJ_MOVE, KOBJ_ONLINE, KOBJ_OFFLINE, KOBJ_BIND, KOBJ_UNBIND, };
- ADD / REMOVE : 디바이스가 커널에 추가 및 제거될 때, 발생한다.
- ONLINE / OFFLINE : CPU가 online / offline 될 때, 발생한다[참고1].
- MOVE : 해당 kobj 의 이름이 바뀌거나, 새로운 부모 노드 밑으로 들어갈 때, 발생한다(kobject_rename, kobject_move).
- CHANGE : 디바이스 상태 및 설정 정보가 변경되었을 때, 발생한다[참고1].
- BIND / UNBIND : uevent 에서 고질적인 문제가 하나있다. udev 가 유저 프로세스 이다보니, udev 가 로드되기 전에 발생한 uevent 들은 모두 의미가 없다는 것이다. 커널 초기화 과정에서 디바이스 트리가 파싱되면서, 디바이스들을 커널에 추가하는 시점은 `device_add` 함수가 호출된다. 이 때, `kobject_uevent` 함수도 호출되면서, uevent 가 발생한다. 그런데, 이 때, udev 가 아직 로드되어 있지가 않다. 이 시점에 uevent 를 어딘가에 넣어놓고 udev 가 로드될 때, 실행하면 안될까? uevent 는 일반적으로 실시간 이벤트다. 즉, 디바이스가 추가/변경/제거 되었을 때, 유저 프로세스 중에서 즉각적으로 응답해야 하는 경우에 사용하는 이벤트다. 이 문제는 ADD가 너무 일찍 발생하기 때문이다. 그렇기 때문에, 디바이스가 커널에 추가되는 시점이 아닌, 디바이스가 드라이버와 매칭되는 시점에 발생시킬 수 있는 BIND 이벤트가 등장한 것이다[참고1].: usermodehelper 메커니즘을 통해 유저 스페이스에 있는 실행 파일(`/sbin/hotplug`)을 직접 실행해서 uevent 를 유저 스페이스로 전송할 수 있다. 그런데, 리눅스에서는 실행 파일을 실행할 때, 환경 변수에 크게 의존한다. 그래서, 환경 변수를 표현할 수 있는 구조체인 `kobj_uevent_env` 를 사용해서 실행 파일을 실행할 때, 같이 전달한다.
// include/linux/kobject.h - v6.5 #define UEVENT_NUM_ENVP 64 /* number of env pointers */ #define UEVENT_BUFFER_SIZE 2048 /* buffer for the variables */ .... struct kobj_uevent_env { char *argv[3]; char *envp[UEVENT_NUM_ENVP]; int envp_idx; char buf[UEVENT_BUFFER_SIZE]; int buflen; };
- arg : usermodehelper 방식으로 uevent 를 생성할 때, 사용한다. 뒤에서 다시 다룬다.
- envp : buf 에 저장된 각 환경 변수의 시작 주소를 가리킨다.
- envp_idx : 각 환경 변수를 구분하는데 사용한다.
- buf : 하나의 uevet 에 대한 모든 환경 변수를 저장한다.
- buflen : 위에 buf 필드에 현재 써진 글자수를 저장한다.: `kset_uevent_ops` 구조체는 kset(서브-시스템) 의 uevent 동작을 정의한다. kset 안에 `kset_uevent_ops`가 정의되어 있다보니, kset 에 포함되지 못한 kobj 는 uevent 를 사용할 수 없다.
// include/linux/kobject.h - v6.5 struct kset_uevent_ops { int (* const filter)(const struct kobject *kobj); const char *(* const name)(const struct kobject *kobj); int (* const uevent)(const struct kobject *kobj, struct kobj_uevent_env *env); };
- filter : 디폴트로 모든 uevent 이벤트가 유저 스페이스로 전송된다. 해당 콜백을 통해서 report 되기 원치않는 이벤트를 filter 할 수 있다.
- name : kset 의 이름을 리턴한다. 즉, 서브-시스템 이름을 리턴한다고 보면 된다. 그런데, 만약 kset 의 이름이 없거나 이상한 이름을 가지고 있다면, uevent 가 report 되지 않는다.
- uevent : uevent 를 만들 때, 각 서브-시스템에서 요구하는 환경 변수를 `key=value` 형식으로 만들어야 한다. 그런데, 서브-시스템마다 `key=value` 쌍이 모두 다르다. 이 콜백을 정의하면, 각 서브-시스템마다 필요한 환경 변수 파싱 코드를 통일한다. 예를 들어, 시스템에 존재하는 모든 디바이스는 `devices_kset`에 포함된다. 이 변수에 `uevent_ops->uevent` 콜백 함수가 정의되어 있다. 이 말은 LDM 에 속하는 모든 디바이스들이 uevent 를 생성할 때, `devices_kset->uevent_ops->uevent` 를 통해서 환경 변수들이 파싱된다는 소리다.: uevent 관련 3 가지 전역 변수들을 알아보자.
1. `uevent_seqnum` 은 말 그대로 uevent 의 번호를 나타낸다. uevent 가 새로 생성될 때 마다 1씩 증가한다. udev 가 uevent 를 순서대로 처리하는데 도움을 준다.
2. `uevent_sock_list`는 모든 network namespace 에 브로드 캐스트하기 위한 리스트다. 뒤에서 다시 다룬다.
3. `uevent_sock_list`는 모든 network namespace 에 브로드 캐스트하기 위한 리스트다. 뒤에서 다시 다룬다.// lib/kobject_uevent.c - v6.5 u64 uevent_seqnum; #ifdef CONFIG_UEVENT_HELPER char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH; #endif struct uevent_sock { struct list_head list; struct sock *sk; }; #ifdef CONFIG_NET static LIST_HEAD(uevent_sock_list); #endif
- What is /sbin/hotplug [참고1 참고2 참고3]?
: usermodehelper 메커니즘의 프로세스는 상당히 직관적이다. `/etc/hotplug/` 폴더에 각 agent 마다 별도의 hotplug script 가 존재한다. 스크립트는 기본적으로 접미사로 `agent`를 사용한다. 만약에, PCI 디바이스가 커널에 등록되면, 커널은 `/etc/hotplug/` 폴더에 pci 관련 hotplug script 파일을 찾는다(여기서는 pci.agent 라는 파일명을 사용하고 있다). 해당 스크립트에는 PCI 관련 uevent 를 받아야 하는 모든 프로그램의 리스트가 작성되어 있다(참고로, `/etc/hotplug/*.agent` 파일들은 사실 쉘 스크립트다).
2) How the original Hotplug scripts works
-----------------------------------------
Conceptually, Hotplug is very simple, and the Hotplug scripts are quite easy to follow.
When something interesting happens, the Linux kernel generates an Hotplug event.
This runs the main Hotplug script, which in turn runs the appropriate script from the /etc/hotplug directory.
There are 3 types of Hotplug events we care about :
1. PCI event : a CardBus device is added or removed from the system. The script /etc/hotplug/pci.agent is run.
2. USB event : a USB device is added or removed from the system. The script /etc/hotplug/usb.agent is run.
3. Network event : a network interface is added or removed from the system. The script /etc/hotplug/net.agent is run.
If we insert a CardBus network card in the system, the following happens :
1) Kernel detects new CardBus device
2) Kernel generates PCI Hotplug event
3) /etc/hotplug/pci.agent runs, finds proper driver module
4) /etc/hotplug/pci.agent loads driver module
5) Driver module initialises, creates new network device
6) Kernel detects new network device
7) Kernel generates Network Hotplug event
8) /etc/hotplug/net.agent runs, configures network device
The sequence of events is similar for USB devices and for removals.
- 참고 : https://hewlettpackard.github.io/wireless-tools/HOTPLUG-UDEV.txt- How to create a uevent ?
: `kset_register` 함수는 새로운 kset 이 만들어 질 때, 유저 스페이스에게 해당 이벤트(새로운 kset 이 시스템에 추가되었음)를 알린다. kboject 는 새로 추가되더라도 uevent 를 생성하지 않았는데, 왜 kset 은 uevent 를 생성할까? 예를 들어, 어떤 시스템에 USB 버스가 있다고 치자. 이 때, USB 버스에 장착되는 디바이스들은 모두 USB 프로토콜에 따라 통신하게 된다. 모든 USB 디바이스는 USB 버스 컨트롤러가 정한 프로토콜 규약을 따라야 한다. 여기서 USB 컨트롤러가 kset 이 되고, USB 디바이스가 kobject 가 된다. 즉, uevent 가 kset 에 속해있는 이유는 각 kobject 의 uevent 동작이 일반적으로 많이 겹치기 때문이다. 특히난, 동일 서브-시스템(kset)에 속하는 디바이스들의 uevent 동작은 겹칠 수 밖에 없다. 그래서, 메모리 낭비 및 중복 코드를 줄이기 위해 uevent 를 kset 에 배치한 것이다.
// lib/kobject.c - v6.5 int kset_register(struct kset *k) { int err; .... kset_init(k); err = kobject_add_internal(&k->kobj); .... kobject_uevent(&k->kobj, KOBJ_ADD); return 0; }
: 그렇다면, kset 에 속해있지 않은 독립적인 kobj 는 uevent 를 발생시킬 수 없다는 것일까? 맞다. uevent 를 생성할 수 없다. 다른 메커니즘을 통해서 유저 스페이스에 알려야 한다.
: `kobject_uevent` 함수는 `kobject_uevent_env` 함수의 래퍼(껍데기) 함수다. 뒤에서 설명하겠지만, 이 함수는 kobj 의 상태 변화(변경/추가/삭제)를 유저 스페이스에 알리는 역할을 한다. LDM 에 새로운 kset 이 추가될 때는, KOBJ_ADD 이벤트를 유저 스페이스에 알리는 것을 볼 수 있다.
// lib/kobject_uevent.c - v6.5 int kobject_uevent(struct kobject *kobj, enum kobject_action action) { return kobject_uevent_env(kobj, action, NULL); }
: `kboject_uevent_env / kobject_uevent` 함수는 uevent 를 생성하고 유저 스페이스로 브로드 캐스트하는 함수다.
// lib/kobject_uevent.c - v6.5 int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[]) { struct kobj_uevent_env *env; const char *action_string = kobject_actions[action]; const char *devpath = NULL; const char *subsystem; struct kobject *top_kobj; struct kset *kset; const struct kset_uevent_ops *uevent_ops; int i = 0; int retval = 0; if (action == KOBJ_REMOVE) // --- 1 kobj->state_remove_uevent_sent = 1; .... /* 2 */ top_kobj = kobj; while (!top_kobj->kset && top_kobj->parent) top_kobj = top_kobj->parent; if (!top_kobj->kset) { return -EINVAL; } /* 2 */ /* 3 */ kset = top_kobj->kset; uevent_ops = kset->uevent_ops; /* 3 */ if (kobj->uevent_suppress) { // --- 4 return 0; } if (uevent_ops && uevent_ops->filter) // --- 5 if (!uevent_ops->filter(kobj)) { pr_debug("kobject: '%s' (%p): %s: filter function " "caused the event to drop!\n", kobject_name(kobj), kobj, __func__); return 0; } /* originating subsystem */ if (uevent_ops && uevent_ops->name) // --- 6 subsystem = uevent_ops->name(kobj); else subsystem = kobject_name(&kset->kobj); if (!subsystem) { pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the " "event to drop!\n", kobject_name(kobj), kobj, __func__); return 0; } /* 7 */ /* environment buffer */ env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL); .... /* complete object path */ devpath = kobject_get_path(kobj, GFP_KERNEL); .... /* 7 */ /* 8 */ /* default keys */ retval = add_uevent_var(env, "ACTION=%s", action_string); if (retval) goto exit; retval = add_uevent_var(env, "DEVPATH=%s", devpath); if (retval) goto exit; retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem); if (retval) goto exit; /* 8 */ /* keys passed in from the caller */ if (envp_ext) { for (i = 0; envp_ext[i]; i++) { retval = add_uevent_var(env, "%s", envp_ext[i]); if (retval) goto exit; } } /* let the kset specific function add its stuff */ if (uevent_ops && uevent_ops->uevent) { // --- 9 retval = uevent_ops->uevent(kobj, env); .... } switch (action) { case KOBJ_ADD: .... kobj->state_add_uevent_sent = 1; break; case KOBJ_UNBIND: zap_modalias_env(env); break; default: break; } mutex_lock(&uevent_sock_mutex); /* we will send an event, so request a new sequence number */ retval = add_uevent_var(env, "SEQNUM=%llu", ++uevent_seqnum); // --- 10 if (retval) { mutex_unlock(&uevent_sock_mutex); goto exit; } retval = kobject_uevent_net_broadcast(kobj, env, action_string, // --- 11 devpath); mutex_unlock(&uevent_sock_mutex); #ifdef CONFIG_UEVENT_HELPER // --- 12 /* call uevent_helper, usually only enabled during early boot */ if (uevent_helper[0] && !kobj_usermode_filter(kobj)) { struct subprocess_info *info; retval = add_uevent_var(env, "HOME=/"); if (retval) goto exit; retval = add_uevent_var(env, "PATH=/sbin:/bin:/usr/sbin:/usr/bin"); if (retval) goto exit; retval = init_uevent_argv(env, subsystem); if (retval) goto exit; retval = -ENOMEM; info = call_usermodehelper_setup(env->argv[0], env->argv, env->envp, GFP_KERNEL, NULL, cleanup_uevent_env, env); if (info) { retval = call_usermodehelper_exec(info, UMH_NO_WAIT); env = NULL; /* freed by cleanup_uevent_env */ } } #endif exit: kfree(devpath); kfree(env); return retval; }
1. uevent 가 `KOBJ_REMOVE` 일 경우, remove uevent 가 전송되었다는 것에 체크한다. 그런데, 너무 앞에 존재하고 있다. 대신 이 위치에 있으면, 실제 uevent 가 유저 스페이스로 전송되었다는 것을 보장하지 못한다. kobj->state_add_uevent_sent 는 뒤쪽에 배치되어 있는데, kobj->state_remove_uevent_sent 왜 이 위치에 놓았을까? 디버깅을 위해서다. 자세한 내용은 이 patch 참고하자.
2. uevent 의 실제적인 동작은 kobj 이 아닌, kset 에서 수행된다(kset 구조체 참고). 즉, 임의의 kset 에 소속되지 않은 kobj 는 uevent 를 전송할 수 없다.
3. 각 kset(서브-시스템) 의 uevent 를 생성하는 공통 동작들이 `kset->uevent_ops` 에 들어간다. 즉, A 라는 kset 에 소속된 모든 kobjects 들은 uevent 를 만들 때, A의 kset->uevent_ops 의 콜백 함수들을 거쳐서 만들어진다.
4. kobj->uevent_suppress 는 해당 kobj 의 uevent 를 enable/disable 하는 기능을 한다.
5. kset 은 `uevent_ops->filter` 콜백을 통해서 report 되기 원치않는 이벤트를 filter 할 수 있다. 즉, 유저 스페이스로 특정 이벤트는 report 되기를 원치않을 때, filter 함수를 이용하면 된다.
6. uevent 에서 반드시 포함되어야 하는 3가지 환경 변수가 있다. kset 즉, 서브-시스템은 반드시 이름을 가지고 있어야 한다.
7. 환경 변수를 저장할 env 를 생성한다. 그리고, devpath 는 sysfs 에서 해당 디바이스가 나타내는 파일의 위치를 의미한다. 대게, devpath는 어떤 디바이스한테 uevent 를 받았는지를 구분하기 위해 사용된다. 예를 들어, env 가 아래와 같이 구성될 경우, `/devices/pci0000:00/0000:00:1d.7/usb2/2-1` 디바이스가 ADD uevent 를 발생시켰다고 볼 수 있다.
`ACTION=add;DEVPATH=/devices/pci0000:00/0000:00:1d.7/usb2/2-1;SUBSYSTEM=usb`
8. uevent 가 발생했을 때, 메타 정보들을 환경 변수라고 하는 이유는 기존 usermodehelper 방식 때문이다. 이 방식이 프로세스를 생성하는 방식으로 구현되어 있기 때문에, 환경 변수가 필요했다. 그래서, 현재는 netlink 를 사용하고 있지만, 기존 방식에 호환성을 위해서 여전히 환경 변수(`kobj_uevent_env`)라고 부르고 있다. 커널에서 uevent 를 만들 때, 반드시 포함되어야 하는 4개의 환경 변수가 있다.
- ACTION : uevent 의 종류
- DEVPATH : uevent 를 발생시킨 주체(디바이스)가 누구인지
- SUBSYSTEM : uevent 를 발생시킨 주체(디바이스)가 포함되어 있는 부모(서브-시스템) 이름
- SEQNUM : uevent 를 순서대로 처리하기 위해 udev 에게 제공되는 정보
9. 각 서브-시스템 종속적인 환경 변수들이 추가된다. USB 같은 경우에 `MAJOR=189 MINOR=149 DEVNAME=bus/usb/002/022 DEVTYPE=usb_device PRODUCT=cf2/6230/100 TYPE=0/0/0 BUSNUM=002 DEVNUM=022` 요런 것들이 추가된다.
10. 새로 생성된 uevent 에 serial number(ID) 를 부여한다. 그리고, 새로운 환경 변수 `SEQNUM` 에 serial number 를 추가한다. `uevent_seqnum` 을 사용하는 이유는 udev 가 uevent 를 받으면, queue에 삽입하는데, 이 때 udev는 uevents 들이 순서가 보장되어서 온다고 가정한다. 그런데, 실제로 그렇지 않은 경우가 존재한다. 그러므로, uevent 에 `SEQNUM`을 추가해서 해당 시퀀스를 보고 순서에 맞게 uevent 를 처리하도록 한다[참고1].
11. netlink 를 통해 uevent 를 유저 스페이스로 브로드 캐스트한다. 뒤에서 다시 다룬다.
12. userhelpermode 방식으로 유저 스페이스에: `add_uevent_var` 함수는 kobj_uevent_env 값을 채워주는 함수다. 예를 들어, `add_uevent_var(env, "ACTION=%s", "add"); add_uevent_var(env, "DEVPATH=%s", "/dev/pci");` 를 실행하면, `kobj_uevent_env.buf`에 아래와 같이 저장된다.
A C T I O N = a d d D E V P A T H = / d e v / p c i - env->envp[env->envp_idx] = env->envp[0] = &"A"
- env->envp[env->envp_idx] = env->envp[1] = &"D": kobj_uevent_env.buf 는 모든 환경 변수들을 저장하는 변수다. 그리고, kobj_uevent_env.evnp 는 buf 에 저장된 각 환경 변수의 시작 주소를 순서대로 저장한다.
// /lib/kobject_uevent.c - v6.5 int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...) { va_list args; int len; if (env->envp_idx >= ARRAY_SIZE(env->envp)) { WARN(1, KERN_ERR "add_uevent_var: too many keys\n"); return -ENOMEM; } va_start(args, format); len = vsnprintf(&env->buf[env->buflen], sizeof(env->buf) - env->buflen, format, args); va_end(args); if (len >= (sizeof(env->buf) - env->buflen)) { WARN(1, KERN_ERR "add_uevent_var: buffer size too small\n"); return -ENOMEM; } env->envp[env->envp_idx++] = &env->buf[env->buflen]; env->buflen += len + 1; return 0; }
: vsnprintf 함수는 예시로 설명하는게 빠르다. 예를 들어, `add_uevent_var(env, "ACTION=%s", "add"); add_uevent_var(env, "DEVPATH=%s", "/dev/pci");` 를 실행하면, vsnprintf 함수의 인자들은 아래와 같이 해석된다.
- vsnprintf(&env->buf[0], 2048 - 0, "ACTION=%s", "add") : &env->buf[0] 에 "ACTION=add" 가 저장된다.
- vsnprintf(&env->buf[10], 2048 - 10, "DEVPATH=%s", "/dev/pci") : &env->buf[10] 에 "DEVPATH=/dev/pci" 가 저장된다.- How to send a uevent via netlink ?
: 현재 리눅스 커널은 uevent 전송 방식으로 netlink 를 주로 사용하고 있다. 이 함수의 핵심은 uevent 가 특정 `network namepsace`에 소속 여부에 따라 달라진다.
- uevent_net_broadcast_untagged : uevent 를 생성한 kobj가 특정 network namespace 에 포함되어 있지 않을 경우 -> 해당 uevent 를 모든 network namespace 로 브로드 캐스트한다.
Untagged kobjects - uevent_net_broadcast_untagged(): Untagged kobjects will be broadcast into all uevent sockets recorded in uevent_sock_list, i.e. into all network namespacs owned by the intial user namespace.
- 참고 : https://github.com/torvalds/linux/commit/a3498436b3a0f8ec289e6847e1de40b4123e1639
- uevent_net_broadcast_tagged : uevent 를 생성한 kobj가 특정 network namespace 에 포함되어 있을 경우 -> 해당 uevent 를 특정 network namespace 로 브로드 캐스트한다.
Tagged kobjects - uevent_net_broadcast_tagged(): Tagged kobjects will only be broadcast into the network namespace they were tagged with.
- 참고 : https://github.com/torvalds/linux/commit/a3498436b3a0f8ec289e6847e1de40b4123e1639: 리눅스 커널 초기에는 netlink 를 통해서 uevent 를 유저 스페이스에 전송할 때, 모든 network namespace 로 전송했다. 그러나, 그러나 이건 오버 헤드가 너무 크다. 그래서, 받고 싶은 유저만 받을 수 있도록 network namespace 를 구분하는 `필터링` 을 적용 하기로 했다[참고1]. namespace 가 존재하면, 해당 namespace 로만 전송된다. 만약, namespace 가 없다면, 해당 uevent 는 모든 namespace 로 브로드 캐스트된다.
// include/linux/kobject_ns.h - v6.5 /* * Namespace types which are used to tag kobjects and sysfs entries. * Network namespace will likely be the first. */ enum kobj_ns_type { KOBJ_NS_TYPE_NONE = 0, KOBJ_NS_TYPE_NET, KOBJ_NS_TYPES }; // lib/kobject_uevent.c - v6.5 static int kobject_uevent_net_broadcast(struct kobject *kobj, struct kobj_uevent_env *env, const char *action_string, const char *devpath) { int ret = 0; #ifdef CONFIG_NET const struct kobj_ns_type_operations *ops; const struct net *net = NULL; ops = kobj_ns_ops(kobj); .... if (ops && ops->netlink_ns && kobj->ktype->namespace) if (ops->type == KOBJ_NS_TYPE_NET) net = kobj->ktype->namespace(kobj); if (!net) ret = uevent_net_broadcast_untagged(env, action_string, devpath); else ret = uevent_net_broadcast_tagged(net->uevent_sock->sk, env, action_string, devpath); #endif return ret; }
: `uevent_net_broadcast_untagged` 함수는 uevent_sock_list 에 있는 모든 socket 들이 속해있는 namespace 에 uevent 를 브로드 캐스트한다. 그런데, 리스너가 없다면 메시지를 보낼 필요가 없다. 그러므로, 리스너가 있는지를 먼저 확인한 후에 존재할 때만 메시지를 보낸다[참고1]. `alloc_uevent_sbk` 함수는 uevent 를 socket 프로토콜에 맞는 구조로 컨버팅한다(`skb`의 약자는 `socket buffer` 이다). 그리고, `netlink_broadcast` 함수를 통해 실제 유저 스페이스로 uevent 를 전송한다.
: `uevent_net_broadcast_tagged` 함수는 딱 하나의 network namespace 에만 uevent 를 브로드 캐스트 할 것이므로, `uevent_sock_list`를 탐색하는 루프문이 필요없다.
// lib/kobject_uevent.c - v6.5 static int uevent_net_broadcast_untagged(struct kobj_uevent_env *env, const char *action_string, const char *devpath) { struct sk_buff *skb = NULL; struct uevent_sock *ue_sk; int retval = 0; list_for_each_entry(ue_sk, &uevent_sock_list, list) { struct sock *uevent_sock = ue_sk->sk; if (!netlink_has_listeners(uevent_sock, 1)) continue; if (!skb) { .... skb = alloc_uevent_skb(env, action_string, devpath); if (!skb) continue; } retval = netlink_broadcast(uevent_sock, skb_get(skb), 0, 1, GFP_KERNEL); .... } .... return retval; } static int uevent_net_broadcast_tagged(struct sock *usk, struct kobj_uevent_env *env, const char *action_string, const char *devpath) { struct user_namespace *owning_user_ns = sock_net(usk)->user_ns; struct sk_buff *skb = NULL; int ret = 0; skb = alloc_uevent_skb(env, action_string, devpath); if (!skb) return -ENOMEM; .... ret = netlink_broadcast(usk, skb, 0, 1, GFP_KERNEL); .... return ret; }
: 결론적으로, netlink_broadcast 함수를 통해서 uevent 가 유저 스페이스로 전송된다. 그런데, `/lib/kobject_uevent.c` 파일에는 위 함수말고도, netlink_broadcast 함수를 사용하는 함수가 있다. 바로, `uevent_net_broadcast` 함수다. 이 함수는 유저 스페이스에서 커널로 [참고1]
- How to send a uevent via usermode helper ?
: uevent 를 usermodehelp 를 통해 전달하기 위해서는 `CONFIG_UEVENT_HELPER`와 `CONFIG_UEVENT_HELPER_PATH` 컨피그가 설정되야 한다. 그리고, 아래 CONFIG_UEVENT_HELPER_PATH 컨피그의 `string` 속성에 hotplug program 의 경로를 입력할 수 있다. 그럴 경우, `/proc/sys/kernel/hotplug` 파일과 `/sys/kernel/uevent_helper` 파일에 hotplug program 의 경로가 써진다(일반적으로, `/sbin/hotplug` 가 입력된다). 유저 레벨에서 /proc/sys/kernel/hotplug 파일에 hotplug program 경로를 직접 입력해도 된다. 참고로, 이 방식은 클래식 방식이기 때문에, 거의 사용하지 않는다는 것을 알아두자.
The hotplug helper /sbin/hotplug is now officially deprecated. The control file /proc/sys/kernel/hotplug has moved to /sys/kernel/uevent_helper, but it is expected to be disabled on most systems in favor of udev and the netlink interface.
- 참고 : https://lwn.net/Articles/166377/// drivers/base/Kconfig - v6.5 config UEVENT_HELPER bool "Support for uevent helper" help The uevent helper program is forked by the kernel for every uevent. Before the switch to the netlink-based uevent source, this was used to hook hotplug scripts into kernel device events. It usually pointed to a shell script at /sbin/hotplug. This should not be used today, because usual systems create many events at bootup or device discovery in a very short time frame. One forked process per event can create so many processes that it creates a high system load, or on smaller systems it is known to create out-of-memory situations during bootup. config UEVENT_HELPER_PATH string "path to uevent helper" depends on UEVENT_HELPER default "" help To disable user space helper program execution at by default specify an empty string here. This setting can still be altered via /proc/sys/kernel/hotplug or via /sys/kernel/uevent_helper later at runtime.
: uevent_helper 전역 변수는 유저 스페이스에 존재하는 hotplug program 의 경로를 저장한다.
// lib/kobject_uevent.c - v6.5 #ifdef CONFIG_UEVENT_HELPER char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH; #endif
: `init_uevent_argv` 함수는 말 그대로 uevent 의 argv 필드를 초기화한다.
- argv[0] : 유저 스페이스 hotplug program 경로(uevent_helper)를 가리킨다.
- argv[1] : sub-system의 이름을 가리킨다.
- argv[2] : NULL// lib/kobject_uevent.c - v6.5 static int init_uevent_argv(struct kobj_uevent_env *env, const char *subsystem) { int buffer_size = sizeof(env->buf) - env->buflen; int len; len = strlcpy(&env->buf[env->buflen], subsystem, buffer_size); .... env->argv[0] = uevent_helper; env->argv[1] = &env->buf[env->buflen]; env->argv[2] = NULL; env->buflen += len + 1; return 0; }
: `call_usermodehelper` 함수는 유저 레벨에서 정의된 hotplug program 을 실행해서 uevent 를 전달한다. 이 함수는 2단계를 통해서 hotplug program 을 실행한다.
1. call_usermodehelper_setup : hotplug program 을 실행할 때, 필요한 메타 데이터를 준비한다.
2. call_usermodehelper_exec : hotplug program 을 실행한다.// kernel/umh.c - v6.5 int call_usermodehelper(const char *path, char **argv, char **envp, int wait) { struct subprocess_info *info; gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL; info = call_usermodehelper_setup(path, argv, envp, gfp_mask, NULL, NULL, NULL); if (info == NULL) return -ENOMEM; return call_usermodehelper_exec(info, wait); }
: `call_usermodehelper_setup` 함수는 즉각적으로 uevent 를 전송하지 않고, 워크-큐(wq)를 통해서 비동기적으로 전송한다. 그리고, CONFIG_STATIC_USERMODEHELPER 컨피그가 설정되어 있으면, 동적으로 hotplug program 경로를 변경할 수 없다. 해당 컨피그에 설정된 string 값이 hotplug program 경로가 된다.
struct subprocess_info *call_usermodehelper_setup(const char *path, char **argv, char **envp, gfp_t gfp_mask, int (*init)(struct subprocess_info *info, struct cred *new), void (*cleanup)(struct subprocess_info *info), void *data) { .... INIT_WORK(&sub_info->work, call_usermodehelper_exec_work); #ifdef CONFIG_STATIC_USERMODEHELPER sub_info->path = CONFIG_STATIC_USERMODEHELPER_PATH; #else sub_info->path = path; #endif .... } int call_usermodehelper_exec(struct subprocess_info *sub_info, int wait) { unsigned int state = TASK_UNINTERRUPTIBLE; DECLARE_COMPLETION_ONSTACK(done); int retval = 0; .... /* * Set the completion pointer only if there is a waiter. * This makes it possible to use umh_complete to free * the data structure in case of UMH_NO_WAIT. */ sub_info->complete = (wait == UMH_NO_WAIT) ? NULL : &done; sub_info->wait = wait; queue_work(system_unbound_wq, &sub_info->work); if (wait == UMH_NO_WAIT) /* task has freed sub_info */ goto unlock; .... wait_for_completion_state(&done, state); .... }
: `call_usermodehelper_exec` 함수는 동기 및 비동기로 유저 hotplug program 을 실행한다. UMH_NO_WAIT 은 비동기를 의미한다. 즉, 완료될 때 까지 기다리지 않고, 다른 일을 수행한다. 그래서, `wait == UMH_NO_WAIT` 이면, call_usermodehelper_exec 함수를 종료한다. `wait != UMH_NO_WAIT` 이면, wait_for_completion_state 함수의 첫 번째 인자에 NULL 이 아닌 값이 들어가므로, 유저 hotplug program 이 완료될 때 까지 대기한다.
: 최종적으로 `kernel_execve` 함수가 호출되는데, 이 함수의 용도가 유저 레벨에서 동작하는 프로세스를 생성하는 역할을 한다. 그런데, 궁금한 부분이 있다. kernel_execve 함수를 통해 실행되는 hotplug program 은 유저 레벨에서 동작하는 스레드일까, 커널 스레드일까?
/* * This is the task which runs the usermode application */ static int call_usermodehelper_exec_async(void *data) { .... retval = kernel_execve(sub_info->path, (const char *const *)sub_info->argv, (const char *const *)sub_info->envp); .... } static void call_usermodehelper_exec_work(struct work_struct *work) { struct subprocess_info *sub_info = container_of(work, struct subprocess_info, work); if (sub_info->wait & UMH_WAIT_PROC) { call_usermodehelper_exec_sync(sub_info); } else { pid_t pid; pid = user_mode_thread(call_usermodehelper_exec_async, sub_info, CLONE_PARENT | SIGCHLD); .... } }
: LWN 에서 2012년에 커널 스레드 관련 간단하게 정리된 글이 있다. `kernel_execve` 함수는 userland process 를 생성하는 프로세스로 정의되고 있다. kernel_execve 함수가 호출된 후에 0이 반환되면, 해당 스레드는 유저 레벨 스레드가 된다고 말하고 있다.
1. Basic rules for process lifetime.
Except for the initial process (init_task, eventual idle thread on the boot CPU) all processes are created by do_fork(). There are three classes of those: kernel threads, userland processes and idle threads to be. There are few low-level operations involved:
* a kernel thread can spawn a new kernel thread; the primitive doing that is kernel_thread().
* a userland process can spawn a new userland process; that's done by sys_fork()/sys_vfork()/sys_clone()/sys_clone2().
* a kernel thread can become a userland process. The primitive is kernel_execve().
* a kernel thread can spawn a future idle thread; that's done by fork_idle().
Result is *not* scheduled until the secondary CPU gets initialized and its state is heavily overwritten in process. Under no circumstances a userland process can become a kernel thread or spawn one. And kernel threads never do fork(2) et.al.
....
That is done after kernel_execve() has returned 0 and then the thread will proceed into userland context created by that execve. Note that some architectures still have kernel_execve() itself switch to userland upon success;
- 참고 : https://lwn.net/Articles/520227/'Linux > kernel' 카테고리의 다른 글
[리눅스 커널] Synchronization - RCU Overview (0) 2023.10.16 [리눅스 커널] Synchronization - sequential locks (0) 2023.10.11 [리눅스 커널] Timer - timekeeping (0) 2023.10.03 [리눅스 커널] Timer - linux clock (0) 2023.09.30 [리눅스 커널] Timer - high resolution timer (0) 2023.09.30