<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”인 경우
- early 파라미터 호출
- 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”
- dwc_otg.lpm_enable=0 console=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p6 rootfstype=ext4 elevator=deadline rootwait
다음 그림은 매치되는 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()
- EARLYCON_DECLARE(pl011, 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)를 사용한다.
- 1) CONFIG_CMDLINE_EXTEND
- A) 부트로더가 다음 중 하나를 전달해 준다.
- boot_command_line
- 코드 라인 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, ¶m, &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)
- (aaa=”1″ bbb=”2 2″ ccc=”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” 속성이 발견되지 않는 경우
- fdt_node_check_compatible()
- 코드 라인 43~44에서 early console 디바이스를 셋업한다.
- match->data에는 디바이스의 setup 함수 주소가 담겨있다.
- rpi2 예) match->data = pl011_early_console_setup()
- match->data에는 디바이스의 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 리틀 엔디안이다.
- UPIO_MEM
- 코드 라인 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에서 콘솔 디바이스로 등록한다.
참고
- Earlycon & Earlyprintk | 문c
- parse_early_param() | 문c – 현재 글
- parse_args() | 문c
- Kernel Parameters | kernel.org