parse_early_param()

<kernel v5.10>

커멘드 라인 파라미터 파싱

부트 커멘드라인 문자열을 파싱하여 해당 파라미터가 early 파라미터인 경우 해당 설정 함수를 호출한다. 호환을 위해 “console” 파라미터는 earlycon으로 동작한다.

boot_command_line(이하 cmdline 또는 커멘드 라인) 파라미터로 요청받은 문자열을 토큰으로 parsing 하여 이에 대응하는 설정 함수를 setup_param 테이블에서 찾고 해당 항목이 early로 설정되어 있는 경우 이를 호출하여 실행한다. 또한 요청 토큰이 “console”인 경우 setup_param 테이블에서 “earlycon”을 찾아 해당 설정 함수를 호출한다. DTB 내부에서도 chosen 노드의  stdout-path 속성값에 해당하는 console  디바이스를 earlycon_of_table 에서 찾은 경우 해당 설정 함수를 동작시킨다.

  • setup_param 테이블에서 early_console 함수를 찾아 등록된 초기화 함수 수행
    • __setup_start 부터 __setup_end 영역
    • obs_kernel_param 구조체 엔트리들
  • 다음 조건에 해당되는 커널 파라미터를 발견하면 해당 커널 파라미터에 등록된 함수를 호출한다.
    • early 파라미터 호출
      • 요청한 파라미터가 등록된 커널 파라미터 문자열과 같으면서 early 설정이 된 경우
    • earlycon 호출
      • 요청한 파라미터가 “console”이고 등록된 커널 파라미터는 “earlycon”인 경우
  • cmdline 문자열 중 “console” 을 발견 시 콘솔 디바이스명으로 모든 earlycon으로 등록된 모든 초기화 함수를 수행
  • 예) rpi2: earlycon=xxx를 설정한 경우:
    • pl011_setup_earlycon(“xxx”) → setup_earlycon() → pl011_early_console_setup(“xxx”)
    • uart_setup_earlycon(“xxx”) → setup_earlycon() → early_serial8250_setup(“xxx”)
    • uart8250_setup_earlycon(“xxx”) → 상동
    • setup_of_earlycon(“xxx”) → early_init_dt_scan_chosen_serial()
      • 디바이스 트리에서 /chosen의 stdout-path를 찾아냄
  • 예) rpi2는 두 개의 드라이버를 console로 지정한다.
    • dwc_otg.lpm_enable=0 console=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p6 rootfstype=ext4 elevator=deadline rootwait
      • ttyAMA는 ARM에서 제공하는 pl011 UART 드라이버이다.
      • tty는 리눅스 기본 tty 드라이버이다.
    • earlycon이 필요 시
      • “earlycon=pl011,0x3f201000,115200n8”
    • earlyprintk가 필요 시
      • “earlyprintk”
    • earlycon과 earlyprintk와 동시 사용 시
      • “earlycon=pl011,0x3f201000,115200n8 earlyprintk”

 

다음 그림은 매치되는 early 파라미터들의 셋업 함수를 호출한다.

  • 예) rpi2에 등록된 earlycon 관련 항목들
    • EARLYCON_DECLARE(pl011, pl011_early_console_setup);
      • 연결함수: pl011_setup_earlycon() -> setup_earlycon() -> pl011_early_console_setup()
    • EARLYCON_DECLARE(uart, early_serial8250_setup);
      • 연결함수: uart_setup_earlycon() -> setup_earlycon() -> early_serial8250_setup()
    • EARLYCON_DECLARE(uart8250, early_serial8250_setup);
      • 연결함수: uart8250_setup_earlycon() -> setup_earlycon() -> early_serial8250_setup()
    • early_param(“earlycon”, setup_of_earlycon);
      • 연결함수: setup_of_earlycon() -> early_init_dt_scan_chosen_serial() -> of_setup_earlycon() -> 예) pl011_early_console_setup()
  • 예) rpi2에 등록된 earlyprintk 관련 항목
    • early_param(“earlyprintk”, setup_early_printk);

 

parse_early_param()

init/main.c

/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
        static int done __initdata;
        static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;

        if (done)
                return;

        /* All fall through to do_early_param. */
        strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
        parse_early_options(tmp_cmdline);
        done = 1; 
}

커멘트 라인 파라미터들 중 early 파라미터에 해당하는 설정 함수를 호출한다.

  • 코드 라인 10에서 전역 변수 boot_command_line의 내용을 tmp_cmdline에 복사한다.
    • boot_command_line
      • A) 부트로더가 다음 중 하나를 전달해 준다.
        • ATAG _CMDLINE 문자열 – ARM32 only
        • DTB의 “/chosen” 노드의 “bootargs” 속성 값
      • B) 커널에서도 준비한 문자열
        • 커널 옵션으로 입력한 커맨드라인 문자열 CONFIG_CMDLINE이 준비된다.
      • 위의 A) 및 B)를 아래 옵션에 따라 조합하여 사용한다.
        • 1) CONFIG_CMDLINE_EXTEND
          • A) DTB(or ATAG)와 B) 커널 cmdline을 합쳐서 사용한다.
        • 2) CONFIG_CLDLINE_FORCE
          • B) 커널 cmdline을 사용한다.
        • 3) no option (default)
          • A) DTB(or ATAG)를 사용한다.
  • 코드 라인 12에서 early 파라미터에 해당하는 설정 함수를 호출한다.

 

parse_early_options()

init/main.c

void __init parse_early_options(char *cmdline)
{
        parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
                   do_early_param);
}

do_early_param() 함수 주소를 인수로 parse_args() 함수를 호출한다.

  • 파라미터 블록, 개수, 범위가 지정되는 경우 그 파라미터 범위에 해당하는 토큰과 매치되는 경우 해당 파라미터에 값을 대입한다.
  • 그러나 파라미터 블록, 개수 및 범위가 0으로 전달되는 경우 각 토큰을 파싱하게 되면 param과 val 값을 가지고 항상 unknown handler인 do_early_param() 함수가 호출된다.

 

parse_args()

kernel/params.c

/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
char *parse_args(const char *doing,
                 char *args,
                 const struct kernel_param *params,
                 unsigned num,
                 s16 min_level,
                 s16 max_level,
                 void *arg,
                 int (*unknown)(char *param, char *val,
                                const char *doing, void *arg))
{
        char *param, *val, *err = NULL;

        /* Chew leading spaces */
        args = skip_spaces(args);

        if (*args)
                pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);

        while (*args) {
                int ret;
                int irq_was_disabled;

                args = next_arg(args, &param, &val);
                /* Stop at -- */
                if (!val && strcmp(param, "--") == 0)
                        return err ?: args;
                irq_was_disabled = irqs_disabled();
                ret = parse_one(param, val, doing, params, num,
                                min_level, max_level, arg, unknown);
                if (irq_was_disabled && !irqs_disabled())
                        pr_warn("%s: option '%s' enabled irq's!\n",
                                doing, param);

                switch (ret) {
                case 0:
                        continue;
                case -ENOENT:
                        pr_err("%s: Unknown parameter `%s'\n", doing, param);
                        break;
                case -ENOSPC:
                        pr_err("%s: `%s' too large for parameter `%s'\n",
                               doing, val ?: "", param);
                        break;
                default:
                        pr_err("%s: `%s' invalid for parameter `%s'\n",
                               doing, val ?: "", param);
                        break;
                }

                err = ERR_PTR(ret);
        }

        return err;
}

커멘드 라인 파라미터를 파싱한 토큰과 값으로 파라미터 블럭 @params에서 @min_level ~ @max_level 범위의 파라미터 테이블을 검색하여 일치하는 토큰이 있는 경우 해당 파라미터에 값을 대입한다. 만일 매치되는 조건이 없으면 @unknown 함수를 호출한다.

  • 코드 라인 14~17에서 args 인수에는 cmdline의 주소를 담고 있는데 space(0x20, 탭, 라인피드 등) 인 경우 skip 한다.
    • 예) ”  console=xxx” -> “console=xxx”
  • 코드 라인 19~23에서 parsing할 문자열이 남아 있는 동안 루프를 돌며 토큰을 “=” 문자로 분리하여 param과 val로 담는다.
    • (abc=1)
    • space 문자로 토큰과 토큰을 분리한다.
      • (aaa=1 bbb=2 ccc=3)
    • 쌍 따옴표(“)가 사용된 경우 중간에 space가 있어도 토큰을 분리하지 않는다.
      • (aaa=”1″ bbb=”2 2″ ccc=”3″)
        • param=(aaa), val=(1)
        • param=(bbb), val=(2 2)
        • param=(ccc), val=(3)
  • 코드 라인 25~26에서 val 값이 없으면서 param 값이 “–” 인 경우 더 이상 파싱을 하지 않고 함수를 종료한다.
  • 코드 라인 27에서 SCTLR의 인터럽트 마스크(“I”, 1번 비트)가 disable된 상태인지 알아온다.
    • true = irq disabled status, false = irq enabled status
  • 코드 라인 28~29에서 파싱된 토큰의 param과 val 그리고 doing(“early options”메시지 출력용) 및 do_early_param() 함수 포인터를 인자로 parse_one()을 호출한다.
    • 파라미터 블록, 개수, 범위가 지정되는 경우 그 파라미터 범위에 해당하는 토큰과 매치되는 경우 해당 파라미터에 값을 대입한다
    • 그러나 파라미터 블록, 개수 및 범위가 0으로 전달되는 경우 각 토큰을 파싱하게 되면 param과 val 값을 가지고 항상 unknown handler인 do_early_param() 함수가 호출된다.
  • 코드 라인 30~32에서 irq 설정 상태가 바뀌었으면 어떤 파라미터 옵션에서 바뀌었는지 warning을 출력한다.
    • 파라미터로 인해 특정 코드(디바이스 드라이버 등)에서 irq를 enable하고 나오는지를 확인하기 위해 경고 출력을 위한 디버그 코드이다.
    • 커널이 초기 설정 중에는 인터럽트가 마스크되어 동작하지 않고 있는데 갑자기 커널 파라미터로 인해 인터럽트가 발생되면 안되기 때문에 이를 확인하기 위함이다.
  • 코드 라인 34~48에서 파싱한 결과가 에러인 경우 에러 메시지를 출력하고 리턴한다. 성공인 경우  다음 토큰을 위해 루프를 계속 진행한다.

 

do_early_param()

init/main.c

/* Check for early params. */
static int __init do_early_param(char *param, char *val, 
                                 const char *unused, void *arg)
{                
        const struct obs_kernel_param *p;

        for (p = __setup_start; p < __setup_end; p++) {
                if ((p->early && parameq(param, p->str)) ||
                    (strcmp(param, "console") == 0 &&
                     strcmp(p->str, "earlycon") == 0)
                ) {
                        if (p->setup_func(val) != 0)
                                pr_warn("Malformed early option '%s'\n", param);
                }
        }
        /* We accept everything at this stage. */
        return 0;
}

다음 조건에 해당되는 early 커널 파라미터를 발견하면 @val 값 인수를 가지고 해당 커널 파라미터에 등록된 함수를 호출한다.

  • 요청한 커멘드 라인 파라미터가 early 커널 셋업 파라미터와 매치된 경우
    • 예) “earlyprintk”
  • 요청한 커멘드 라인 파라미터가 “console”로 시작한 경우
    • 예) “console=pl011”
  • early_param(“earlycon”)으로 등록한 셋업 함수
    • 현재 param_setup_earlycon() 함수 하나만 사용되고 있다.

 

  • 코드 라인 6에서 __setup_start 주소 영역 부터 __setup_end 주소 영역까지에 여러 개의 커널 파라미터인 obs_kernel_param 구조체가 있는데 순서대로 검색하여 p를 대입한다.
  • 코드 라인 7~10에서 p->early 항목이 설정되어 있으면서 p->str이 인수로 받은 param 문자열과 같은 경우이거나 p->str이 “earlycon”이면서 인수로 받은 param 문자열이 “console”인 경우
  • 코드 라인 11~12에서 p->setup_func()을 호출하여 에러가 있는 경우 경고 메시지를 출력한다.

 


디바이스 트리를 통한 파라미터 지정

디바이스 트리를 통해 파라미터 및 earlycon을 지정할 수 있다.

 

다음과 같이 DTB의 chosen 노드에 stdout-path를 지정하여 earlycon에 사용할 수 있고, bootargs를 통해 커멘드 라인 파라미터를 지정할 수 있다.

        chosen {
                bootargs = "console=ttyS0,115200n8 earlyprintk";
                stdout-path = "serial0:115200n8";
        };

 

디바이스 트리를 통한 earlycon 지정

커멘드 라인 파라메터에서 “earlycon” 또는 “console”이 지정될 때 디바이스 트리의 chosen 노드에서 “stdout-path”를 early 콘솔 디바이스로 지정한다. “console” 뒤에 디바이스가 지정되면 안된다.

early_init_dt_scan_chosen_stdout()

drivers/of/fdt.c

int __init early_init_dt_scan_chosen_stdout(void)
{
        int offset;
        const char *p, *q, *options = NULL;
        int l;
        const struct earlycon_id **p_match;
        const void *fdt = initial_boot_params;

        offset = fdt_path_offset(fdt, "/chosen");
        if (offset < 0)
                offset = fdt_path_offset(fdt, "/chosen@0");
        if (offset < 0)
                return -ENOENT;

        p = fdt_getprop(fdt, offset, "stdout-path", &l);
        if (!p)
                p = fdt_getprop(fdt, offset, "linux,stdout-path", &l);
        if (!p || !l)
                return -ENOENT;

        q = strchrnul(p, ':');
        if (*q != '\0')
                options = q + 1;
        l = q - p;

        /* Get the node specified by stdout-path */
        offset = fdt_path_offset_namelen(fdt, p, l);
        if (offset < 0) {
                pr_warn("earlycon: stdout-path %.*s not found\n", l, p);
                return 0;
        }

        for (p_match = __earlycon_table; p_match < __earlycon_table_end;
             p_match++) {
                const struct earlycon_id *match = *p_match;

                if (!match->compatible[0])
                        continue;

                if (fdt_node_check_compatible(fdt, offset, match->compatible))
                        continue;

                if (of_setup_earlycon(match, offset, options) == 0)
                        return 0;
        }
        return -ENODEV;
}

이 함수는 CONFIG_SERIAL_EARLYCON이 설정되어 있는 경우 DTB에서 /chosen 노드의 stdout-path 속성에 지정된 compatible(디바이스명)을 알아와서 earlycon 테이블에 등록한 모든 earlycon 디바이스의 이름과 같은 디바이스의 설정 함수를 호출한다.

  • 코드 라인 9~13에서 “/chosen” 또는 “/chosen@0” 노드를 검색한다.
  • 코드 라인 15~19에서 발견한 노드에서 “stdout-path” 또는 “linux,stdout-path” 속성을 검색한다.
  • 코드 라인 21~31에서 발견한 속성에서 디바이스명과 옵션을 분리한다.
    • 예) “serial0:115200n8” 에서 offset은 “serial0″를 의미하고, options는 “115200n8″을 의미한다.
  • 코드 라인33~38에서 __earlycon_of_table 주소 부터 compatible[0]이 있는 동안 검색한다.
  • 코드 라인40~41에서 compatible 속성 값에서 디바이스명(match->compatible)이 매치되지 않으면 skip 한다.
    • fdt_node_check_compatible()
      • compatible 속성 값에서 문자열 비교: 0=match, 1=non match, 길이=”compatible” 속성이 발견되지 않는 경우
  • 코드 라인 43~44에서 early console 디바이스를 셋업한다.
    • match->data에는 디바이스의 setup 함수 주소가 담겨있다.
      • rpi2 예) match->data = pl011_early_console_setup()

 

예) DTB용 콘솔 디바이스 드라이버 소스 – earlycon_of_table 섹션에 저장된다.

  • OF_EARLYCON_DECLARE(pl011, “arm,pl011”, pl011_early_console_setup);

 

of_setup_earlycon()

drivers/tty/serial/earlycon.c

int __init of_setup_earlycon(const struct earlycon_id *match,
                             unsigned long node,
                             const char *options)
{
        int err;
        struct uart_port *port = &early_console_dev.port;
        const __be32 *val;
        bool big_endian;
        u64 addr;

        spin_lock_init(&port->lock);
        port->iotype = UPIO_MEM;
        addr = of_flat_dt_translate_address(node);
        if (addr == OF_BAD_ADDR) {
                pr_warn("[%s] bad address\n", match->name);
                return -ENXIO;
        }
        port->mapbase = addr;

        val = of_get_flat_dt_prop(node, "reg-offset", NULL);
        if (val)
                port->mapbase += be32_to_cpu(*val);
        port->membase = earlycon_map(port->mapbase, SZ_4K);

        val = of_get_flat_dt_prop(node, "reg-shift", NULL);
        if (val)
                port->regshift = be32_to_cpu(*val);
        big_endian = of_get_flat_dt_prop(node, "big-endian", NULL) != NULL ||
                (IS_ENABLED(CONFIG_CPU_BIG_ENDIAN) &&
                 of_get_flat_dt_prop(node, "native-endian", NULL) != NULL);
        val = of_get_flat_dt_prop(node, "reg-io-width", NULL);
        if (val) {
                switch (be32_to_cpu(*val)) {
                case 1:
                        port->iotype = UPIO_MEM;
                        break;
                case 2:
                        port->iotype = UPIO_MEM16;
                        break;
                case 4:
                        port->iotype = (big_endian) ? UPIO_MEM32BE : UPIO_MEM32;
                        break;
                default:
                        pr_warn("[%s] unsupported reg-io-width\n", match->name);
                        return -EINVAL;
                }
        }

        val = of_get_flat_dt_prop(node, "current-speed", NULL);
        if (val)
                early_console_dev.baud = be32_to_cpu(*val);

        val = of_get_flat_dt_prop(node, "clock-frequency", NULL);
        if (val)
                port->uartclk = be32_to_cpu(*val);

        if (options) {
                early_console_dev.baud = simple_strtoul(options, NULL, 0);
                strlcpy(early_console_dev.options, options,
                        sizeof(early_console_dev.options));
        }
        earlycon_init(&early_console_dev, match->name);
        err = match->setup(&early_console_dev, options);
        if (err < 0)
                return err;
        if (!early_console_dev.con->write)
                return -ENODEV;

        register_console(early_console_dev.con);
        return 0;
}

디바이스 트리를 통해 early 콘솔 디바이스 속성 값들을 읽어온 후 디바이스의 설정 함수를 호출한 후 콘솔 디바이스로 등록한다.

  • 코드 라인 12~18에서 레지스터 주소를 알아온다.
    • UPIO_MEM
      • 유저 스페이스 노출가능한 mmio(주소 지정된 io)로 처리 bit 및 endian이 지정되지 않은 driver-specific mmio이다.
      • 참고로 8250은 8bit 리틀 엔디안 이고 pl011은 16bit 리틀 엔디안이다.
  • 코드 라인 20~22에서 “reg-offset” 속성이 있는 경우 해당 값 만큼 offset을 주소에 더한다.
  • 코드 라인 23에서 early 콘솔 디바이스를 4K 페이지만큼 매핑한다.
  • 코드 라인 25~27에서 “reg-shift” 속성이 있는 경우 regshift 값에 대입해둔다.
  • 코드 라인 28~30에서 빅 엔디안 요청이 있는지 여부를 확인한다. 다음 2가지 경우로 판단한다.
    • “big-endian” 속성이 있는 경우
    • cpu가 big-endian으로 동작하면서 “native-endian” 속성이 있는 경우
  • 코드 라인 31~47에서 콘솔 버스 크기를 알아와서 크기에 따라 다음과 같이 포트 타입을 지정한다.
    • 1 바이트 크기인 경우 포트 타입을 UPIO_MEM으로 한다.
    • 2 바이트 크기인 경우 포트 타입을 UPIO_MEM16으로 한다.
    • 4 바이트 크기인 경우 포트 타입을 UPIO_MEM32BE 또는 UPIO_MEM32로 한다.
  • 코드 라인 49~51에서 “current-speed” 값을 읽어 시리얼 속도를 지정한다.
  • 코드 라인 53~55에서 “clock-frequency” 값을 읽어 클럭 값을 지정한다.
  • 코드 라인 57~61에서 콘솔 디바이스에 옵션(콘솔명 + ‘:’ + 옵션)이 지정된 경우 시리얼 속도를 지정하고, 옵션도 지정해둔다.
  • 코드 라인 62에서 early 콘솔 디바이스의 이름과 데이터를 지정한 후, 설정 정보를 출력한다.
    • 예) earlycon: pl11 at MMIO 0x0000000009000000 (options ”)
  • 코드 라인 63~65에서 매치된 디바이스의 설정 함수를 호출한다.
  • 코드 라인 66~67에서 early 콘솔 드라이버에 출력 후크 함수가 구현되지 않은 경우 -ENODEV 에러를 반환한다.
  • 코드 라인 69에서 콘솔 디바이스로 등록한다.

 

참고

 

댓글 남기기