ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] Kernel command-line parameters
    Linux/kernel 2023. 11. 6. 17:08

    글의 참고

    - https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html

    - https://stackoverflow.com/questions/68307458/how-to-set-linux-kernel-command-line-on-arm

    - https://stackoverflow.com/questions/48801998/passing-bootargs-via-chosen-node-in-device-tree-not-working-for-beaglebone-black

    - https://www.kernel.org/doc/Documentation/devicetree/usage-model.txt

    - https://stackoverflow.com/questions/64877292/how-does-the-bootloader-pass-the-kernel-command-line-to-the-kernel

    - https://man7.org/linux/man-pages/man8/sysctl.8.html


    글의 전제

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

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


    글의 내용

    - Overview

    : `커널 커맨드 라인 파라미터` 는 임베디드 리눅스 개발을 하면 사실 크게 신경쓸 필요가 없다고 생각된다. 그러나, 서버 시장에서는 필수적인 개념이고, Devops 및 리눅스 시스템 엔지니어가 되고 싶다면 이 내용을 반드시 알고 있어야 한다. 대개 시스템 포퍼먼스와 관련이 깊은 내용이기 때문에, 알아두면 좋은 점이 많다. 이 글에서는 Kernel`s command-line parameters 를 KCP 라고 부르도록 하겠다.

     

     

    - CONFIG_CMDLINE_*

    : KCP 에는 여러 개의 컨피그가 존재한다. 어떤 컨피그를 설정하냐에 따라 커널에 KCP 를 전달하는 방법이 달라진다. 아래의 내용은 arm64 를 기준으로 설명한다.

    1. CONFIG_CMDLINE : default kernel command string 을 사용한다. 즉, `cmdline = default kernel command string`.
    config CMDLINE
    string "Default kernel command string"
    default ""
    help
      Provide a set of default command-line options at build time by
      entering them here. As a minimum, you should specify the the
      root device (e.g. root=/dev/nfs).

    1. CONFIG_CMDLINE_FROM_BOOTLODAER : 부트 로더에서 전달하는 KCP 를 사용한다. 부트 로더의 KCP로 default kernel command string 을 오버라이트 한다는 뜻이다. 즉, `cmdline = 부트 로더 KCP`.
    config CMDLINE_FROM_BOOTLOADER
    bool "Use bootloader kernel arguments if available"
    help
      Uses the command-line options passed by the boot loader. If
      the boot loader doesn't provide any, the default kernel command
      string provided in CMDLINE will be used.

    2. CONFIG_CMDLINE_EXTEND : default kernel command string 에 부트 로더의 kernel command parameter 를 추가한다. 즉, `cmdline = default kernel command string + 부트 로더 KCP`.
    config CMDLINE_EXTEND
    bool "Extend bootloader kernel arguments"
    help
      The command-line arguments provided by the boot loader will be
      appended to the default kernel command string.

    3. CONFIG_CMDLINE_FORCE : 항상 default kernel command string 만 사용하도록 강제한다. 부트 로더의 kernel command parameter 는 무시한다. 즉, `cmdline = default kernel command string`.
    config CMDLINE_FORCE
    bool "Always use the default kernel command string"
    help
      Always use the default kernel command string, even if the boot
      loader passes other arguments to the kernel.
      This is useful if you cannot or don't want to change the
      command-line options your boot loader passes to the kernel.

     

    : CONFIG_CMDLINE 컨피그에는 default kernel command string 을 명시할 수 있다. 예를 들어, `/arch/arm/configs/imx_v6_v7_defconfig` 에는 아래와 같이 명시되어 있다. 그런데, CONFIG_CMDLINE 은 코드에 작성된 데이터가 아니다. 이걸 코드에서 사용할 수 있어야 한다. 이 데이터가 언제 코드에 적용될까?

    // https://github.com/torvalds/linux/blob/master/arch/arm/configs/imx_v6_v7_defconfig
    CONFIG_CMDLINE="noinitrd console=ttymxc0,115200"

     

    : 리눅스 커널이 시스템을 초기화하는 시점에 부트 로더에서 전달받은 디바이스 트리를 파싱하는 코드가 있다. 거기서 `/chosen` 노드를 파싱하는 `early_init_dt_scan_chosen` 함수가 있다. 이 함수에서 모든 kernel`s command line parameter 를 파싱해서 하나로 만든다. 위에 CONFIG_CMDLINE 도 이 함수에서 사용된다.

    // drivers/of/fdt.c - v6.5
    int __init early_init_dt_scan_chosen(char *cmdline)
    {
    	int l, node;
    	const char *p;
    	const void *rng_seed;
    	const void *fdt = initial_boot_params;
    
    	node = fdt_path_offset(fdt, "/chosen"); // --- 1
    	....
    	if (node < 0)
    		/* Handle the cmdline config options even if no /chosen node */
    		goto handle_cmdline;
    
    	....
    
    	/* Retrieve command line */
    	p = of_get_flat_dt_prop(node, "bootargs", &l); // --- 1
    	if (p != NULL && l > 0)
    		strscpy(cmdline, p, min(l, COMMAND_LINE_SIZE));
    
    handle_cmdline:
    	/*
    	 * CONFIG_CMDLINE is meant to be a default in case nothing else
    	 * managed to set the command line, unless CONFIG_CMDLINE_FORCE
    	 * is set in which case we override whatever was found earlier.
    	 */
    #ifdef CONFIG_CMDLINE // --- 2
    #if defined(CONFIG_CMDLINE_EXTEND) // --- 3
    	strlcat(cmdline, " ", COMMAND_LINE_SIZE);
    	strlcat(cmdline, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
    #elif defined(CONFIG_CMDLINE_FORCE) // --- 4
    	strscpy(cmdline, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
    #else // --- 5
    	/* No arguments from boot loader, use kernel's  cmdl*/
    	if (!((char *)cmdline)[0]) // --- 5
    		strscpy(cmdline, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
    #endif
    #endif /* CONFIG_CMDLINE */
    
    	pr_debug("Command line is: %s\n", (char *)cmdline);
    
    	return 0;
    }
    1. 제일 먼저 부트 로더에게 받은 디바이스 트리에서 `/chosen` 노드의 존재 여부를 검사한다. 그리고, 존재하면 `bootargs` 프로퍼티가 있는지 검사한다. 만약에 있다면, bootargs 의 내용을 변수 cmdline 에 복사한다. 이 bootargs 의 내용이 부트 로더로 부터 전달받은 command line parameter 가 된다. 아래 코드는 커널의 dts 파일이다. 부트 로더는 아래 작성된 `bootargs` 를 그대로 전달하거나 혹은 변형해서 전달한다(이 과정은 뒤에서 다시 설명). 

    // https://stackoverflow.com/questions/48801998/passing-bootargs-via-chosen-node-in-device-tree-not-working-for-beaglebone-black
    chosen {
            bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2 rootfstype=ext3 rw rootwait";
            stdout-path = &uart0;
    };
    그런데, 여기서 `strscpy` 를 함수를 사용하는 것에 주목할 필요가 있다. 즉, early_init_dt_scan_chosen 함수가 호출되기 전까지는 cmdline 에 아무 데이터도 없었다는 뜻이다.

    2. 이전 버전은 모르겠지만, 커널 v6.5 는 부트 로더로 부터 전달받은 command line parameter 를 default kernel command line parameter 로 사용한다. 즉, `CONFIG_CMDLINE_*` 섹션에서 언급된 컨피그가 하나도 설정되어 있지 않더라도, 기본적으로 부트 로더로 부터 전달받은 command line parameter 를 default kernel command line parameter 로 사용된다는 뜻이다.

    3. 부트 로더로 부터 전달받은 command line parameter 뒤에 default kernel command string 을 append 한다. 즉, `kernel parameter = 부트 로더 command line + default kernel command string` 가 된다.

    4. default kernel command string 만 사용하겠다는 뜻이다.

    5. cmdline[0] 에는 디바이스 트리의 bootargs 속성의 내용이 들어간다. 즉, 부트 로더에게 받은 command line parameter 의 존재 여부를 검사하는 코드를 의미한다.

     

    : 그런데, 부트 로더가 커널에게 데이터를 전달하는 방법이 디바이스 트리말고 뭐가 더 있을까? 

    2.3 Runtime configuration
    ----------------------------------------------------------------------------------------------------
    In most cases, a DT will be the sole method of communicating data from firmware to the kernel, so also gets used to pass in runtime and configuration data like the kernel parameters string and the location of an initrd image.

    - 참고 : https://www.kernel.org/doc/Documentation/devicetree/usage-model.txt

     

    : 커널 문서를 보면, 대부분의 경우에 부트 로더가 커널에게 command line 를 전달하는 유일한 방법(sole method)이 디바이스 트리라고 명시되어있다. 그렇다면, 부트 로더에서 bootargs 를 수정하는 경우도 있을텐데, u-boot 는 이 수정한 내용을 dtb 에 업데이트 한다는 것일까? 디바이스 트리는 리눅스에만 존재하는 개념이 아니었나? 디바이스 트리는 `Open Firmware` 제단에서 만든 메커니즘이다. 디바이스 트리는 시스템에 존재하는 하드웨어 정보들을 추상화시키기 위한 하나의 방법이다. 디바이스 드라이버 개발자들은 이 디바이스 트리에 디바이스에 대한 하드웨어 정보를 작성하고 커널이 부팅하는 시점에 디바이스 트리에서 필요한 하드웨어 정보들을 가지고 와서 실제 디바이스를 초기화 및 설정하게 된다. 그런데, 앞에서 언급했듯이, 디바이스 트리는 Open Firmware 에서 만든 표준화된 방법이다. 이 말은, 디바이스 트리가 리눅스의 부속물이 아니라는 뜻이다. 즉, 하드웨어 정보를 소프트웨어적으로 추상화 시킬려는 모든 소프트웨어에서 디바이스 트리를 도입할 수 있다. u-boot 도 그중 하나다.

     

    : u-boot 에서도 u-boot 만의 디바이스 트리가 존재한다. 엔비디아 테그라124 디바이스 트리는 아래와 같이 u-boot & linux kernel 모두에 존재한다. 그렇다면, 리눅스 커널의 dts 와 어떤 차이가 있을까?

    1. u-boot : arch/arm/dts/tegra124-jetson-tk1.dts
    2. linux kernel : arch/arm/boot/dts/tegra124-jetson-tk1.dts

     

    : 위 2개의 파일을 열어보면 거의 동일하다는 것을 알 수 있다. 차이가 있다면, u-boot 쪽의 dts 파일이 리눅스 커널보다는 `stripped down version` 이라고 보는 것이 맞을 것 같다. 즉, 사이즈가 작다. 왜냐면, u-boot 는 말 그대로 부트 로더이기 때문에, 커널 로딩에 필요한 디바이스만 dts 에 작성되어 있으면 된다.

     

    : u-boot 가 커널에게 kernel parameters 을 전달할 때, kernel parameter 를 수정 및 override 해서 전달할 수 있다. 흔히 `setenv` 와 `printenv` 를 통해서 kernel parameters 를 수정해서 전달하면, 기존 리눅스 커널의 bootargs 는 overridden 된다.

     Boot Linux:
    ------------------------------------
    The "bootm" command is used to boot an application that is stored in memory (RAM or Flash). In case of a Linux kernel image, the contents of the "bootargs" environment variable is passed to the kernel as parameters. You can check and modify this variable using the "printenv" and "setenv" commands:

    - 참고 : https://github.com/u-boot/u-boot

     

    : 그런데, u-boot 에도 `/chosen` 노드가 있고, 리눅스 커널에도 `/chosen` 노드가 있다. 그리고, 그 안에 bootargs 도 작성되어 있는 경우도 있는데, 어느 dtb 의 bootargs 가 리눅스 커널에게 전달될까? u-boot 코드를 분석해보면 알 수 있지만, 결국 리눅스 커널의 bootargs 가 전달된다. 대신, u-boot 에서 setenv 혹은 printenv 를 사용하지 않았다는 전제가 있어야 한다. 해당 내용은 `fdt_chosen` 함수를 살펴보면 알 수 있다(아래 블락 다이러그램은 u-boot v2022.10 버전으로 작성했다).

     

    : 위에 질문에 연장선으로, 리눅스 커널의 bootargs 가 적용됬다고 가정하자. 이 때, u-boot 에서 setenv 로 bootargs 를 변경했다면, 커널 dtb 에 존재하는 bootargs 프로퍼티를 수정하는가? 맞다. 위에 fdt_setprop 함수는 인자로 리눅스 커널의 dtb 를 받는다. 여기서 "bootargs" 프로퍼티를 찾아서 u-boot 환경 변수에 `bootargs` 가 존재한다면, override 한다. 그런데, override 는 좀 빡쌘거 아닐까? _do_env_set 함수는 `setenv` 명령어를 호출하면 최종적으로 호출되는 함수인데, 해당 함수에 대한 주석이 다음과 같다.

    //cmd/nvedit.c - v2022.10
    /*
     * Set a new environment variable,
     * or replace or delete an existing one.
     */
    static int _do_env_set(int flag, int argc, char *const argv[], int env_flag)
    ....

     

    : 새로운 command lie 을 기존 bootargs 에 append 하는 방법은 없을까? 2가지 방법이 있다.

    1. `setenv bootargs $(bootargs) root=/dev/mmcblk0p1` 와 같이 $(bootargs) 를 앞쪽에 추가해서 새로운 내용(root=/dev/mmcblk0p1) 를 뒤쪽에 append 하기[참고1]

    2. `otherbootargs` 변수 사용하기. 이 변수는 애초에 bootargs 에 append 용으로 나온 변수다[참고1].
    setenv bootargs memtest=17 ${consoleparam} $mtdparts ubi.mtd=boot-config ubi.mtd=root root=ubi1:rootfs rw rootfstype=ubifs threadsirqs=1 kthreadd_pri=25 ksoftirqd_pri=8 irqthread_pri=15 ${usb_gadget_args} $othbootargs

     

    : 참고로, ARM u-boot 에서 kernel parameter 를 생성할 때, 2가지 방법이 존재한다.

    // arch/arm/lib/bootm.c -v2023.01
    /* Subcommand: PREP */
    static void boot_prep_linux(struct bootm_headers *images)
    {
    	....
    
    	if (CONFIG_IS_ENABLED(OF_LIBFDT) && CONFIG_IS_ENABLED(LMB) && images->ft_len) {
    		debug("using: FDT\n");
    		....
    	} else if (BOOTM_ENABLE_TAGS) {
    		debug("using: ATAGS\n");
    		....
    	} else {
    		panic("FDT and ATAGS support not compiled in\n");
    	}
    
    	board_prep_linux(images);
    }

     

    : 현재 디바이스 트리를 이용하는 (1) 번을 사용하고 있다. (2) 번은 `tagged list` 를 이용하는 방식인데, 레거시 방식이다. arm64 에서는 사용하지 않기 때문에, 궁금하다면 해당 링크를 참고하자.

    1. FDT (new) - CONFIG_OF_LIBFDT
    2. Tags (old) - CONFIG_SUPPORT_PASSING_ATAGS

     

     

    - Three way to pass arguments to kernel

    : 위에서는 u-boot 가 리눅스 커널에게 어떻게 kernel parameter 를 던지는 대해서만 알아봤다. 그러나, KCP 를 커널에게 전달하는 방법은 이 뿐만이 아니다. 기본적으로, 3 가지 방법이 존재한다.

    1. 커널을 빌드할 때 : CONFIG_CMDLINE 컨피그를 SET 하고, CONFIG_CMDLINE = ${default kernel command string} 을 작성하면, COMFIG_CMDLINE 에 작성된 string 이 kernel parameter 에 추가된다. 위에서도 봤던 내용이다.

    2. 커널이 시작될 때 : 부트 로더를 통해서 커널에 전달하는 것이다. 부트 로더는 커널보다도 이미지 사이즈가 작아서 빌드 속도가 빠르다. 위에서 봤겠지만, early_init_dt_scan_chosen 함수는 부트 로더에서 받은 kernel parameter 를 default 로 사용한다. 즉, kernel parameter 관련 어떠한 컨피그를 설정하지 않아도 부트 로더에게 받은 kernel parameter 는 default 로 적용이 된다. 위에서 봤던 내용이다.

    3. 커널이 동작 중 일 때 : 말 그대로 동적으로 KCP 를 적용하는 것이다. sysctl(`/proc/sys/` 와 `/sys/`)을 통해서 가능하다. 그런데, sysctl 이 모든 KCP 를 지원하는 것이 아니다. 즉, 동적으로 변경이 가능한 KCP list 가 정해져있다. 이 내용은 새로운 내용이다.

     

    : sysctl 로 변경가능한 kernel parameter 는 `/proc/sys/` 에 모여있다. 이 내용은 별도의 글에서 다루도록 한다.

    /proc/sys$ ls
    abi  debug  dev  fs  kernel  net  user  vm

     

Designed by Tistory.