Bash 셸 스크립팅 완전 가이드
Bash(Bourne-Again SHell)는 리눅스에서 가장 널리 사용되는 명령행 인터프리터이자 스크립팅 언어입니다.
이 문서는 Bash의 내부 구조(렉서/파서/실행기), 변수와 확장, 제어 흐름, 함수, I/O 리디렉션(Redirection),
프로세스(Process) 모델과 잡 제어(Job Control), 시그널(Signal) 처리, 프로세스 간 통신(IPC),
/proc·/sys 인터페이스 스크립팅, 커널 빌드 자동화,
그리고 POSIX 호환성과 셸 비교까지 커널 개발자 관점에서 상세히 다룹니다.
- 셔뱅(Shebang,
#!) — 스크립트 첫 줄에 적는 인터프리터 경로.#!/bin/bash는 "이 파일을 Bash로 실행하라"는 선언입니다. - 환경 변수(Environment Variable) —
export로 표시된 변수. 자식 프로세스에게 자동으로 상속됩니다. (ARCH=arm64 make처럼 빌드 도구에 전달) - 위치 매개변수(Positional Parameter) — 스크립트·함수에 전달된 인자.
$1·$2···로 접근하고,$0은 스크립트 이름입니다. - 종료 상태(Exit Status) — 명령이 끝난 뒤 남기는 숫자.
0은 성공, 그 외는 실패. 모든 조건문(if·&&·||)이 이 값을 기반으로 동작합니다. - 명령 치환(Command Substitution) —
$(명령)형태로 명령 출력을 변수에 담는 기법. 예:VER=$(uname -r) - 프로세스 치환(Process Substitution) —
<(명령)·>(명령)으로 명령의 출력·입력을 파일처럼 사용하는 Bash 전용 기능. set -euo pipefail— 스크립트 엄격 모드 세 가지:-e(오류 즉시 종료),-u(미선언 변수 에러),-o pipefail(파이프 중간 오류 전파). 커널 스크립트의 표준 패턴.- 글로빙(Globbing) —
*·?·[...]로 파일명을 패턴 매칭하는 경로명 확장. 정규 표현식(Regular Expression)과 다르며 셸이 확장 후 명령에 전달합니다.
핵심 요약
- Bourne-Again SHell — 1989년 Brian Fox가 FSF를 위해 작성한 자유 소프트웨어 셸로, Bourne Shell(
sh)의 상위 호환(Superset)입니다. - 커널 빌드의 접착제 — 커널 소스의
scripts/디렉터리에는 수많은 셸 스크립트가 있으며,Kconfig·Kbuild·모듈 설치 등 빌드 전반에 관여합니다. - 7단계 확장 — 명령 실행 전에 중괄호·틸드·매개변수·산술·명령 치환·단어 분리·경로명 확장을 순서대로 수행합니다.
- 프로세스 모델 — 외부 명령은
fork()+exec()로 실행되며, 파이프라인 각 단계는 별도 프로세스입니다. - POSIX 호환 —
--posix나/bin/sh로 호출 시 POSIX 모드에 가깝게 동작하며, 배열·정규식 매칭 같은 Bash 전용 확장은 비활성화됩니다.
단계별 이해
Bash 스크립팅을 처음 접하는 커널 개발자가 따라가기 좋은 학습 경로입니다. 실제 코드 예시는 아래 본문 섹션에서 확인할 수 있습니다.
- 1단계: 첫 번째 스크립트 — 셔뱅(Shebang)을 선언하고
echo·$(...)로 커널 버전을 출력하는 가장 단순한 예제로 시작합니다. - 2단계: 변수와 조건문 — 환경 변수·기본값 확장(
${VAR:-default})과[[ ... ]]조건식을 사용해 소스 디렉터리와 필수 도구를 검사합니다. - 3단계: 함수와 파이프 — 반복되는 오류 처리(
die/warn)를 함수로 추출하고,grep·sort·cut같은 파이프라인으로 설정·로그를 요약합니다. - 4단계: 시그널 트랩과 /proc 활용 —
trap cleanup EXIT으로 임시 자원을 정리하고,/proc·/sys로 커널 상태를 조회합니다. - 5단계: 빌드 자동화 —
set -euo pipefail엄격 모드와 로그 파일을 결합해 커널 빌드 과정을 재현 가능하게 자동화합니다.
| 용어 | 설명 |
|---|---|
| 셸(Shell) | 사용자와 커널 사이의 명령행 인터페이스. 명령을 해석하고 실행하는 프로그램 |
| 빌트인(Builtin) | cd, echo, export 등 셸 내부에 구현된 명령. fork() 없이 현재 프로세스에서 직접 실행 |
| 서브셸(Subshell) | 현재 셸의 자식 프로세스로 생성된 셸 환경. ( ), 파이프라인, 명령 치환 등에서 생성됨 |
| 확장(Expansion) | Bash가 명령 실행 전에 변수, 와일드카드, 산술식 등을 실제 값으로 치환하는 과정 |
| 리디렉션(Redirection) | 표준 입출력(I/O)(stdin/stdout/stderr)의 대상을 파일, 파이프, 소켓(Socket) 등으로 변경하는 기능 |
| 파이프라인(Pipeline) | cmd1 | cmd2 형태로 한 명령의 출력을 다음 명령의 입력으로 연결하는 구조 |
| 잡 제어(Job Control) | 셸이 프로세스 그룹 단위로 포그라운드/백그라운드 작업을 관리하는 기능 |
| 트랩(Trap) | 시그널 수신 시 실행할 명령을 등록하는 Bash 빌트인. 정리(Cleanup) 작업에 필수적 |
Bash 개요 및 역사
Bash(Bourne-Again SHell)는 1989년 Brian Fox가 자유 소프트웨어 재단(FSF, Free Software Foundation)을 위해 개발한 유닉스 셸입니다.
이름 자체가 Bourne Shell(sh)의 "재탄생(Born Again)"을 의미하는 말장난으로,
Stephen Bourne이 1979년에 작성한 원조 Bourne Shell의 상위 호환을 목표로 설계되었습니다.
1990년부터는 Chet Ramey가 주 관리자를 맡아 왔으며, GNU Bash는 2025년 5월 18일 공개된 5.3 매뉴얼판 기준으로도 계속 발전하고 있습니다.
유닉스 셸의 역사는 1971년 Ken Thompson이 작성한 Thompson Shell에서 시작됩니다.
이후 1978년 Bill Joy의 C Shell(csh), 1979년 Stephen Bourne의 Bourne Shell(sh),
1983년 David Korn의 KornShell(ksh)이 등장하며 셸 생태계가 다양해졌습니다.
1992년 POSIX.2 표준이 셸 명령어 언어를 규격화하면서,
이후 등장한 셸들은 이 표준을 기반으로 구현되었습니다.
Bash는 Bourne Shell 호환성을 유지하면서 csh의 히스토리 기능과 ksh의 산술 확장 등
다른 셸의 장점을 흡수한 종합 셸입니다.
리눅스 커널 소스 트리에서 Bash의 역할은 매우 중요합니다.
scripts/ 디렉토리에는 scripts/config(Kconfig 조작),
scripts/diffconfig(설정 비교), scripts/setlocalversion(버전 문자열 생성),
scripts/merge_config.sh(설정 병합) 등 핵심 빌드 도구가 셸 스크립트로 구현되어 있습니다.
또한 scripts/checkpatch.pl은 Perl이지만 셸에서 호출되며,
scripts/Makefile.build는 $(shell ...)을 통해 빈번하게 셸 명령을 실행합니다.
커널 개발자에게 Bash 스크립팅은 필수 역량입니다.
대부분의 리눅스 배포판은 /bin/bash를 기본 셸로 제공합니다.
다만 데비안(Debian)과 우분투(Ubuntu)는 시스템 셸(/bin/sh)로 Dash를 사용하고,
Alpine Linux는 BusyBox ash를 사용합니다.
이러한 차이는 스크립트 이식성(Portability)에 직접적인 영향을 미치며,
이 문서 후반부의 POSIX 호환성 섹션에서 자세히 다룹니다.
Bash 내부 구조
Bash는 단순한 명령 실행기를 넘어, 완전한 프로그래밍 언어 인터프리터입니다. 사용자가 입력한 텍스트는 렉서(Lexer), 파서(Parser), 확장기(Expander), 실행기(Executor)의 네 단계를 거쳐 최종적으로 실행됩니다. 이 내부 구조를 이해하면 셸 스크립트의 미묘한 동작 차이(따옴표 처리, 단어 분리, 글로빙 등)를 정확히 파악할 수 있습니다.
렉서와 파서
렉서(Lexical Analyzer)는 입력 문자열을 토큰(Token) 단위로 분리합니다.
Bash의 토큰에는 단어(WORD), 연산자(|, &&, ; 등), 예약어(if, then, do 등)가 있습니다.
렉서는 따옴표(Quoting) 상태를 추적하여 작은따옴표('...') 내부에서는 모든 특수 문자를 리터럴로 처리하고,
큰따옴표("...") 내부에서는 $, `, \만 특수하게 처리합니다.
파서(Parser)는 토큰 스트림을 추상 구문 트리(AST, Abstract Syntax Tree)로 변환합니다.
Bash 파서는 LALR(1) 파서가 아닌, 재귀 하향(Recursive Descent) 방식에 가까운 수동 파서입니다.
이는 셸 문법의 문맥 의존적 특성(예: (가 서브셸인지 함수 정의인지는 문맥에 따라 결정) 때문입니다.
파싱 결과로 생성된 AST의 노드 유형에는 단순 명령(Simple Command), 파이프라인,
리스트(List, &&/||/;), 복합 명령(Compound Command, if/while/for/case/{ }/( )) 등이 있습니다.
따옴표 처리(Quoting)는 렉서 단계에서 이루어지는 핵심 메커니즘입니다. 커널 스크립트에서 자주 실수하는 패턴을 살펴보겠습니다:
# 잘못된 예: 단어 분리가 일어남
FILE="my kernel config"
if [ -f $FILE ]; then ... # 오류! 3개의 인자로 분리됨
# 올바른 예: 큰따옴표로 보호
if [ -f "$FILE" ]; then ... # 하나의 인자로 전달
# [[ ]]는 단어 분리를 하지 않으므로 안전
if [[ -f $FILE ]]; then ... # Bash 전용, 안전
# 작은따옴표: 모든 확장 억제
echo '$HOME은 확장되지 않습니다'
# $'...' ANSI-C 인용: 이스케이프 시퀀스 해석
echo $'\t탭\n개행'
실행 모델
Bash의 실행기(Executor)는 AST를 순회하며 각 노드를 실행합니다. 명령의 종류에 따라 실행 방식이 크게 달라집니다:
빌트인 명령(Builtin Command)은 Bash 프로세스 내부에서 직접 실행됩니다.
cd, export, read, echo, test 등이 이에 해당합니다.
cd가 빌트인이어야 하는 이유는, 자식 프로세스에서 chdir()를 호출해도 부모(셸) 프로세스의 작업 디렉토리에는 영향이 없기 때문입니다.
빌트인 목록은 enable -a 명령으로 확인할 수 있습니다.
외부 명령(External Command)은 fork()로 자식 프로세스를 생성한 뒤,
자식에서 execve()로 해당 프로그램을 로드하여 실행합니다.
Bash는 PATH 환경 변수에 나열된 디렉토리를 순서대로 탐색하되,
성능을 위해 해시(Hash) 테이블(Hash Table)에 이전 탐색 결과를 캐시(Cache)합니다.
hash 빌트인으로 이 캐시를 확인하거나 초기화할 수 있습니다.
함수(Function)는 현재 셸 컨텍스트에서 실행됩니다(서브셸이 아닙니다).
따라서 함수 내에서 변경한 변수는 호출자에게 영향을 미칩니다.
local 키워드로 지역 변수를 선언하면 동적 스코핑(Dynamic Scoping)이 적용되어,
해당 함수와 그 함수가 호출하는 하위 함수에서만 보입니다.
서브셸과 환경
서브셸(Subshell)은 현재 셸의 복제본(clone)으로 생성되는 자식 프로세스입니다. 다음 상황에서 서브셸이 생성됩니다:
( command )— 괄호 그룹$(command)또는`command`— 명령 치환(Command Substitution)- 파이프라인의 각 단계 (단,
shopt -s lastpipe설정 시 마지막 단계는 현재 셸에서 실행) &로 백그라운드 실행- 프로세스 치환(Process Substitution):
<(cmd),>(cmd)
서브셸은 부모 셸의 변수, 함수, 트랩, 옵션 등을 상속받지만, 서브셸 내에서 변경한 내용은 부모에게 전파되지 않습니다. 이것이 파이프라인에서의 흔한 함정입니다:
# 함정: 파이프라인에서 변수 변경이 반영되지 않음
count=0
cat /proc/modules | while read line; do
((count++))
done
echo "$count" # 0 출력! while이 서브셸에서 실행됨
# 해결 1: 프로세스 치환 사용
count=0
while read line; do
((count++))
done < <(cat /proc/modules)
echo "$count" # 정확한 모듈 수 출력
# 해결 2: lastpipe (Bash 4.2+)
shopt -s lastpipe
count=0
cat /proc/modules | while read line; do
((count++))
done
echo "$count" # 정확한 모듈 수 출력
# 해결 3: 리디렉션으로 파이프 자체를 제거
count=0
while read line; do
((count++))
done < /proc/modules
echo "$count" # 파일 리디렉션 → 서브셸 없음
서브셸에서의 환경 상속은 다음 규칙을 따릅니다:
| 속성 | 서브셸 상속 | 변경 전파(부모→) |
|---|---|---|
환경 변수 (export) | 예 | 아니오 |
| 셸 변수 (비export) | 예 | 아니오 |
| 함수 정의 | 예 | 아니오 |
| 트랩 설정 | 리셋됨 | 아니오 |
현재 디렉토리 ($PWD) | 예 | 아니오 |
셸 옵션 (set, shopt) | 예 | 아니오 |
$$ (원래 셸 PID) | 부모와 동일 | N/A |
$BASHPID (실제 PID) | 자식 PID | N/A |
# $$ vs $BASHPID 차이 확인
echo "부모: $$=$$ BASHPID=$BASHPID"
( echo "서브셸: $$=$$ BASHPID=$BASHPID" )
# 부모: $$=1234 BASHPID=1234
# 서브셸: $$=1234 BASHPID=5678 ← $$ 동일, BASHPID만 다름
# 서브셸 vs 그룹 명령 (중괄호)
x=10
( x=20; echo "서브셸 안: $x" ) # 20
echo "서브셸 밖: $x" # 10 (변경 없음)
{ x=30; echo "그룹 안: $x"; } # 30
echo "그룹 밖: $x" # 30 (현재 셸에서 실행됨!)
변수와 확장
왜 Bash 변수에는 타입이 없는가
C나 Python과 달리 Bash 변수는 선언 시 타입을 지정하지 않습니다. 이는 의도적인 설계입니다 — 셸의 주 목적은 명령을 조합하는 것이고, 명령의 인자와 출력은 대부분 문자열이기 때문입니다. Bash는 모든 변수를 기본적으로 문자열로 저장하며, 산술 연산이 필요한 맥락에서만 숫자로 해석합니다.
| 구분 | 셸 변수 (Shell Variable) | 환경 변수 (Environment Variable) |
|---|---|---|
| 선언 | VAR=값 |
export VAR=값 또는 declare -x VAR=값 |
| 범위 | 현재 셸 프로세스에서만 유효 | 자식 프로세스에게 자동 상속 |
| 확인 | set 명령으로 목록 확인 |
env 또는 printenv로 목록 확인 |
| 커널 빌드 예 | 스크립트 내부 임시값 (BUILD_DIR=/tmp/build) |
ARCH=arm64, CROSS_COMPILE=aarch64-linux-gnu- — make의 하위 프로세스에서도 참조 |
[A-Za-z_][A-Za-z0-9_]* 패턴이어야 합니다.
관례적으로 환경 변수·전역 상수는 대문자(ARCH, CFLAGS), 스크립트 내 지역 변수는 소문자(build_dir)로 씁니다.
$VAR와 ${VAR}는 동일하지만, 문자가 바로 이어질 때는 중괄호가 필수입니다: ${VAR}suffix.
Bash에서 변수는 데이터를 저장하고 조작하는 가장 기본적인 메커니즘입니다.
C 언어와 달리 Bash 변수에는 타입(Type)이 없으며 기본적으로 모든 값이 문자열로 저장됩니다.
declare 빌트인을 사용하면 정수(-i), 읽기 전용(Read-Only)(-r), 배열(-a),
연관 배열(-A), 대소문자 변환(-l/-u) 등의 속성을 부여할 수 있습니다.
변수 종류
Bash 변수는 크게 셸 변수(Shell Variable)와 환경 변수(Environment Variable)로 나뉩니다.
셸 변수는 현재 셸 프로세스에서만 유효하고, 환경 변수는 export를 통해
자식 프로세스에게 상속됩니다. 커널 빌드에서 ARCH, CROSS_COMPILE 등은
환경 변수로 설정해야 make의 하위 프로세스에서도 참조할 수 있습니다.
| 특수 변수 | 설명 | 커널 개발 활용 예 |
|---|---|---|
$? | 마지막 명령의 종료 상태 (0=성공) | make && echo "빌드 성공" || echo "빌드 실패" |
$$ | 현재 셸의 PID | 임시 파일명에 PID 포함: /tmp/build.$$.log |
$! | 마지막 백그라운드 작업의 PID | QEMU 실행 후 PID 추적: qemu ... & QEMU_PID=$! |
$# | 위치 매개변수 개수 | 인자 검증: [[ $# -lt 1 ]] && die "사용법: $0 <config>" |
$@ | 모든 위치 매개변수 (개별 단어) | 인자 전달: make "$@" |
$* | 모든 위치 매개변수 (단일 문자열) | 로깅: echo "인자: $*" |
$0 | 스크립트 이름 또는 셸 이름 | 사용법 출력: echo "사용법: $0 [-v] [-j N]" |
$_ | 이전 명령의 마지막 인자 | 반복 참조: mkdir build && cd $_ |
$LINENO | 현재 줄 번호 | 디버깅(Debugging): echo "DEBUG[$LINENO]: 변수=$var" |
$FUNCNAME | 현재 함수명 (배열) | 에러 메시지: echo "${FUNCNAME[0]}: 실패" |
$BASH_SOURCE | 소스 파일명 (배열) | 스크립트 경로: DIR="$(dirname "${BASH_SOURCE[0]}")" |
$PIPESTATUS | 파이프라인 각 단계의 종료 상태 (배열) | 파이프 검증: make 2>&1 | tee log; echo "${PIPESTATUS[0]}" |
# declare를 이용한 변수 속성 설정
declare -i counter=0 # 정수 속성: 산술 자동 평가
counter="1+2" # counter=3 (문자열이 아닌 산술식으로 평가)
declare -r KERNEL_VERSION="6.1" # 읽기 전용: 이후 변경 불가
declare -l lower_str="HELLO" # 소문자 변환: "hello"
declare -u upper_str="hello" # 대문자 변환: "HELLO"
declare -x EXPORTED_VAR="값" # export와 동일
# nameref (Bash 4.3+): 다른 변수를 가리키는 참조
declare -n ref=KERNEL_VERSION
echo "$ref" # "6.1" 출력 — KERNEL_VERSION의 값 참조
확장 순서
Bash는 명령을 실행하기 전에 정해진 순서로 7단계의 확장(Expansion)을 수행합니다. 이 순서를 정확히 이해하는 것이 예측 가능한 스크립트를 작성하는 핵심입니다. 각 단계는 이전 단계의 결과물 위에서 작동하므로, 순서가 중요합니다.
# 매개변수 확장의 다양한 패턴 (커널 스크립트에서 자주 사용)
KERNEL="linux-6.1.42"
echo "${KERNEL#linux-}" # "6.1.42" — 앞에서 최소 매칭 제거
echo "${KERNEL##*-}" # "6.1.42" — 앞에서 최대 매칭 제거
echo "${KERNEL%.42}" # "linux-6.1" — 뒤에서 최소 매칭 제거
echo "${KERNEL%%.*}" # "linux-6" — 뒤에서 최대 매칭 제거
echo "${KERNEL/linux/Linux}" # "Linux-6.1.42" — 첫 매칭 치환
echo "${KERNEL//./,}" # "linux-6,1,42" — 전체 매칭 치환
echo "${KERNEL:6:3}" # "6.1" — 부분 문자열 (오프셋:길이)
echo "${#KERNEL}" # "12" — 문자열 길이
# 기본값 패턴
echo "${CROSS_COMPILE:-}" # 미설정이면 빈 문자열
echo "${INSTALL_PATH:=/boot}" # 미설정이면 /boot 대입 후 반환
echo "${ARCH:?아키텍처를 지정하세요}" # 미설정이면 에러 메시지 출력 후 종료
echo "${VERBOSE:+--verbose}" # 설정되어 있으면 --verbose, 아니면 빈 문자열
배열과 연관 배열
Bash는 인덱스 배열(Indexed Array)과 연관 배열(Associative Array, Bash 4.0+)을 지원합니다. 배열은 커널 빌드 스크립트에서 다중 아키텍처, 설정 파일 목록, 테스트 케이스 관리 등에 유용합니다. POSIX sh에는 배열이 없으므로 이식성이 필요한 스크립트에서는 사용에 주의해야 합니다.
# 인덱스 배열
ARCHES=("x86" "arm64" "riscv" "mips")
echo "첫 번째: ${ARCHES[0]}" # x86
echo "전체: ${ARCHES[@]}" # x86 arm64 riscv mips
echo "개수: ${#ARCHES[@]}" # 4
ARCHES+=("s390") # 요소 추가
unset 'ARCHES[2]' # riscv 제거 (인덱스 유지, 빈 슬롯)
# 배열 순회
for arch in "${ARCHES[@]}"; do
echo "빌드 중: ARCH=${arch}"
make ARCH="${arch}" defconfig
done
# 연관 배열 (Bash 4.0+)
declare -A CROSS_COMPILERS
CROSS_COMPILERS[arm64]="aarch64-linux-gnu-"
CROSS_COMPILERS[riscv]="riscv64-linux-gnu-"
CROSS_COMPILERS[mips]="mips-linux-gnu-"
for arch in "${!CROSS_COMPILERS[@]}"; do
echo "${arch}: ${CROSS_COMPILERS[$arch]}"
done
# mapfile/readarray (Bash 4.0+): 파일을 배열로 읽기
mapfile -t modules < <(lsmod | tail -n+2 | awk '{print $1}')
echo "로드된 모듈 수: ${#modules[@]}"
# 배열 슬라이싱 (인덱스 기반 부분 추출)
echo "처음 2개: ${ARCHES[@]:0:2}" # x86 arm64
echo "2번부터: ${ARCHES[@]:2}" # riscv mips
# 배열 검색 패턴
contains_element() {
local target="$1"; shift
for item in "$@"; do
[[ "$item" == "$target" ]] && return 0
done
return 1
}
if contains_element "arm64" "${ARCHES[@]}"; then
echo "arm64 빌드 포함"
fi
배열 성능 특성: Bash 배열은 C의 배열과 달리 희소 배열(Sparse Array)입니다.
unset 'arr[5]'로 요소를 삭제해도 인덱스가 재정렬되지 않고 빈 슬롯이 남습니다.
연관 배열은 해시 테이블(Hash Table) 기반이므로 키 검색은 O(1)이지만, 수천 개 이상의 요소에서는
외부 도구(awk, sqlite3)가 더 효율적입니다.
대용량 데이터를 배열에 로드해야 한다면 mapfile을 사용하면
while read 루프보다 10배 이상 빠릅니다.
제어 흐름
종료 상태가 모든 조건문의 기반 — 스크립트가 프로그램이 되는 순간
Bash 제어 흐름을 이해하는 핵심은 모든 조건이 "명령의 종료 상태"라는 숫자라는 점입니다.
if문이 평가하는 것은 참/거짓 값이 아니라, 조건 자리에 온 명령의 종료 상태입니다.
종료 상태 0은 성공(참), 그 외 숫자는 실패(거짓)입니다.
ls, grep, make 같은 모든 명령이 종료 상태를 반환하기 때문에
별도의 boolean 타입 없이도 자연스러운 조건 처리가 가능합니다.
| 상황 | 사용 패턴 | 동작 |
|---|---|---|
| 명령 성공 시 실행 | command && echo "성공" |
종료 상태 0이면 && 우측 실행 |
| 명령 실패 시 실행 | command || die "실패" |
종료 상태 0이 아니면 || 우측 실행 |
| 파일 존재 확인 | if [ -f "$FILE" ]; then |
[ 명령이 0(파일 있음) 또는 1(없음) 반환 |
| 문자열 패턴 매칭 | if [[ "$STR" =~ ^v[0-9] ]]; then |
Bash 내장 정규식 매칭, 종료 상태로 판단 |
| 빌드 성공 확인 | if make -j$(nproc); then |
make의 종료 상태가 직접 조건이 됨 |
&& / || 단축 평가: cmd1 && cmd2는 cmd1이 성공해야만 cmd2를 실행합니다.
cmd1 || cmd2는 cmd1이 실패해야만 cmd2를 실행합니다.
커널 스크립트에서 make defconfig && make -j$(nproc) || die "빌드 실패"처럼 간결한 오류 처리에 자주 쓰입니다.
Bash의 제어 흐름(Control Flow) 구문은 C 언어와 문법적으로 다르지만 기능적으로 대등합니다.
조건문, 반복문, 패턴 매칭 등 프로그래밍의 기본 제어 구조를 모두 제공하며,
특히 case문의 글로빙 패턴 매칭은 셸 스크립팅의 강력한 도구입니다.
조건문
Bash 조건문의 핵심은 명령의 종료 상태입니다.
if문은 조건 자리에 오는 명령의 종료 상태가 0(성공)이면 참, 그 외이면 거짓으로 판단합니다.
test, [, [[는 조건 평가를 수행하는 명령으로 각각 특성이 다릅니다.
| 특성 | test / [ | [[ |
|---|---|---|
| 종류 | 빌트인 명령 (외부 /usr/bin/[도 존재) | Bash 키워드 (셸 문법의 일부) |
| POSIX 호환 | 예 | 아니오 (Bash/zsh/ksh 전용) |
| 단어 분리 | 발생 — 변수 반드시 큰따옴표 | 없음 — [[ $var == ... ]] 안전 |
| 글로빙(Glob) | 발생 가능 | 패턴 매칭으로 사용 가능 (==) |
| 정규식 | 미지원 | =~ 연산자로 ERE 매칭 |
| 논리 연산자 | -a, -o | &&, || |
| 문자열 비교 | =, != | ==, !=, <, > (로캘 순서) |
| 빈 변수 처리 | [ $var = x ] → 에러 (인용 필수) | [[ $var == x ]] → 안전 |
# test / [ — POSIX 호환, 외부 명령 또는 빌트인
if [ "$ARCH" = "x86" ]; then
echo "x86 아키텍처"
fi
# [[ — Bash 전용 키워드, 단어 분리 없음, 정규식 지원
if [[ "$KERNEL_VERSION" =~ ^6\.[0-9]+\.[0-9]+$ ]]; then
echo "커널 6.x 시리즈"
fi
# [[ 의 패턴 매칭 (glob)
if [[ "$CONFIG_FILE" == *.config ]]; then
echo "설정 파일"
fi
# case 문: 다중 패턴 매칭 (커널 스크립트에서 매우 자주 사용)
case "$ARCH" in
x86|x86_64)
SRCARCH="x86"
;;
arm|arm64|aarch64)
SRCARCH="arm64"
;;
riscv*)
SRCARCH="riscv"
;;
*)
die "지원하지 않는 아키텍처: $ARCH"
;;
esac
반복문
Bash는 for, while, until, select 네 가지 반복문을 제공합니다.
커널 개발에서는 while read 패턴(파일/명령 출력을 줄 단위로 처리)과
C 스타일 for(()) 루프가 특히 유용합니다.
# for-in: 리스트 순회
for config in arch/*/configs/*_defconfig; do
echo "설정: ${config}"
done
# C 스타일 for: 산술 반복
for ((i=0; i<$(nproc); i++)); do
echo "CPU ${i}: $(cat /sys/devices/system/cpu/cpu${i}/cpufreq/scaling_governor 2>/dev/null || echo N/A)"
done
# while read: 줄 단위 처리 (가장 실용적인 패턴)
while IFS= read -r line; do
module="$(echo "$line" | awk '{print $1}')"
size="$(echo "$line" | awk '{print $2}')"
echo "모듈: ${module}, 크기: ${size}"
done < <(lsmod | tail -n+2)
# until: 조건이 참이 될 때까지 반복
until ping -c 1 -W 1 "$BUILD_SERVER" >/dev/null 2>&1; do
echo "빌드 서버 대기 중..."
sleep 5
done
# select: 메뉴 선택 (대화형 스크립트)
echo "빌드할 아키텍처를 선택하세요:"
select arch in x86_64 arm64 riscv "전체 빌드" 종료; do
case "$arch" in
종료) break ;;
*) echo "선택: $arch"; break ;;
esac
done
테스트 연산자 레퍼런스
다음 표는 test/[/[[에서 사용하는 주요 연산자를 정리합니다.
커널 스크립트에서는 파일 테스트와 문자열 비교가 가장 빈번하게 사용됩니다.
| 분류 | 연산자 | 의미 |
|---|---|---|
| 파일 | -e file | 파일 존재 여부 |
-f file | 일반 파일인지 | |
-d dir | 디렉토리인지 | |
-L file | 심볼릭 링크인지 | |
-r file | 읽기 가능한지 | |
-w file | 쓰기 가능한지 | |
-x file | 실행 가능한지 | |
-s file | 크기가 0보다 큰지 | |
| 문자열 | -z str | 문자열 길이가 0인지 |
-n str | 문자열 길이가 0이 아닌지 | |
str1 = str2 | 문자열이 같은지 (==도 가능) | |
str1 != str2 | 문자열이 다른지 | |
| 산술 | n1 -eq n2 | 같음 (equal) |
n1 -lt n2 | 작음 (less than) | |
n1 -gt n2 | 큼 (greater than) | |
| 논리 | ! expr | 논리 부정 |
expr1 -a expr2 | 논리 AND ([[에서는 &&) | |
expr1 -o expr2 | 논리 OR ([[에서는 ||) | |
| 산술 추가 | n1 -ne n2 | 같지 않음 (not equal) |
n1 -le n2 | 작거나 같음 (less or equal) | |
n1 -ge n2 | 크거나 같음 (greater or equal) | |
| 파일 비교 | f1 -nt f2 | f1이 f2보다 새로운지 (newer than) |
f1 -ot f2 | f1이 f2보다 오래된지 (older than) | |
f1 -ef f2 | 같은 inode를 가리키는지 |
# 파일 테스트 실전 예제
KCONFIG=".config"
VMLINUX="vmlinux"
INITRD="/boot/initramfs.img"
# -f: 일반 파일 존재 확인
if [ ! -f "$KCONFIG" ]; then
echo ".config가 없습니다. 먼저 make defconfig를 실행하세요."
exit 1
fi
# -d: 디렉토리 존재 확인
if [ -d "tools/testing/selftests" ]; then
echo "kselftest 디렉토리 발견"
fi
# -x: 실행 가능 확인 (크로스 컴파일(Cross Compilation) 도구체인 존재 검증)
CROSS="aarch64-linux-gnu-gcc"
if ! [ -x "$(command -v "$CROSS")" ]; then
die "크로스 컴파일러를 찾을 수 없음: $CROSS"
fi
# -s: 파일 크기가 0이 아닌지 (빌드 결과물 확인)
if [ -s "$VMLINUX" ]; then
echo "vmlinux 크기: $(stat -c%s "$VMLINUX") 바이트"
fi
# -nt: 파일 날짜 비교 (재빌드 필요 여부)
if [ "$KCONFIG" -nt "$VMLINUX" ]; then
echo ".config가 vmlinux보다 새로움 → 재빌드 필요"
make -j$(nproc)
fi
# 산술 비교 (커널 버전 확인)
KVER_MAJOR="$(uname -r | cut -d. -f1)"
if [ "$KVER_MAJOR" -ge 6 ]; then
echo "커널 6.x 이상 — EEVDF 스케줄러 사용 가능"
fi
# 문자열 테스트 (환경 변수 검증)
if [ -z "$ARCH" ]; then
ARCH="$(uname -m)"
echo "ARCH 미설정 → 호스트 아키텍처 사용: $ARCH"
fi
함수
왜 local을 반드시 써야 하는가 — 전역 변수 오염 실험
Bash 함수는 C·Python 함수와 결정적으로 다른 특성이 있습니다.
함수 내 변수는 기본적으로 전역입니다.
local 선언 없이 변수를 쓰면, 함수 밖에서 선언된 같은 이름의 변수를 덮어씁니다.
이것이 Bash 초보자가 가장 자주 겪는 버그입니다.
# ⚠ 위험한 예: local 없는 함수가 전역 변수를 오염
result="초기값"
bad_func() {
result="함수 내부에서 변경됨" # local 없으면 전역 result를 덮어씀!
}
bad_func
echo "$result" # "함수 내부에서 변경됨" — 의도치 않은 결과
# ✅ 올바른 예: local로 지역 변수 선언
result="초기값"
good_func() {
local result="함수 내부에서 변경됨" # 지역 변수 — 전역에 영향 없음
echo "함수 내: $result"
}
good_func
echo "$result" # "초기값" — 전역 변수 보호됨
return은 종료 상태(0–255)만 반환합니다.
문자열이나 객체를 반환하려면 두 가지 방법을 씁니다:
(1) stdout 출력 + 명령 치환: result=$(my_func) — 서브셸 비용 발생,
(2) nameref(declare -n): 참조 변수로 직접 쓰기 — Bash 4.3+에서 사용 가능.
Bash 함수는 코드를 재사용 가능한 블록으로 구조화하는 핵심 도구입니다.
C 함수와 달리 매개변수 타입이나 반환 타입을 선언하지 않으며,
인자는 위치 매개변수($1, $2, ...)로 접근합니다.
함수는 현재 셸 컨텍스트에서 실행되므로, local로 선언하지 않은 변수는 전역에 영향을 미칩니다.
정의와 호출
Bash 함수 정의에는 두 가지 문법이 있습니다. function 키워드를 사용하는 형태와
POSIX 호환 형태입니다. 커널 소스 트리의 스크립트는 대부분 POSIX 호환 형태를 사용합니다.
# POSIX 호환 문법 (권장)
my_function() {
local arg1="$1"
local arg2="$2"
echo "인자: $arg1, $arg2"
return 0 # 종료 상태 (0-255)
}
# Bash 전용 문법
function my_function2 {
echo "Bash 전용"
}
# 함수 호출
my_function "hello" "world"
result=$? # 종료 상태 캡처
# 함수에서 값 반환 (return은 종료 상태만 가능)
# 방법 1: 명령 치환으로 stdout 캡처
get_kernel_version() {
make -s kernelversion
}
ver="$(get_kernel_version)"
# 방법 2: nameref (Bash 4.3+)
get_arch_info() {
local -n result_ref="$1"
result_ref="$(uname -m)"
}
get_arch_info my_arch
echo "$my_arch" # "x86_64" 등
# 동적 스코핑 (Dynamic Scoping) 이해
outer() {
local x="outer"
inner
echo "outer: $x" # "outer" — inner의 local이 보호
}
inner() {
local x="inner"
echo "inner: $x" # "inner"
}
outer
커널 스크립트의 함수 패턴
리눅스 커널 소스 트리의 셸 스크립트에서 반복적으로 나타나는 함수 패턴이 있습니다. 이 패턴들은 오랜 기간 검증된 모범 사례(Best Practice)로, 자체 스크립트에서도 활용할 수 있습니다.
#!/bin/bash
# 커널 스크립트 공통 패턴
# 1. die() — 에러 출력 후 즉시 종료
die() {
echo "ERROR: $*" >&2
exit 1
}
# 2. warn() — 경고 출력 (계속 진행)
warn() {
echo "WARNING: $*" >&2
}
# 3. verbose() — 상세 출력 (플래그 제어)
VERBOSE=0
verbose() {
[[ "$VERBOSE" -ge 1 ]] && echo "$*"
}
# 4. run() — 명령 실행 전 출력 (dry-run 지원)
DRY_RUN=0
run() {
verbose "실행: $*"
[[ "$DRY_RUN" -eq 1 ]] && return 0
"$@" || die "명령 실패: $*"
}
# 5. check_tool() — 필수 도구 존재 확인
check_tool() {
local tool
for tool in "$@"; do
command -v "$tool" >/dev/null 2>&1 || die "필요한 도구 없음: $tool"
done
}
# 6. cleanup() + trap — 정리 보장
TMPFILES=()
cleanup() {
local f
for f in "${TMPFILES[@]}"; do
rm -f "$f"
done
}
trap cleanup EXIT
# 7. usage() — 사용법 출력
usage() {
cat <<EOF
사용법: $0 [옵션] <커널소스경로>
옵션:
-a ARCH 대상 아키텍처 (기본: x86_64)
-j JOBS 병렬 빌드 수 (기본: $(nproc))
-v 상세 출력
-n dry-run (실제 실행 안 함)
-h 이 도움말 출력
EOF
exit "${1:-0}"
}
I/O 리디렉션
I/O 리디렉션(Redirection)은 Bash에서 프로세스의 표준 스트림을 파일, 파이프, 네트워크 소켓 등 다양한 대상으로 변경하는 기능입니다. 유닉스의 "모든 것은 파일입니다(Everything is a file)" 철학을 직접 활용하는 메커니즘으로, 파일 디스크립터(File Descriptor) 조작을 통해 구현됩니다. 이하에서는 약어인 FD를 함께 사용합니다.
기본 리디렉션
유닉스/리눅스 프로세스는 기본적으로 세 개의 파일 디스크립터를 가집니다: 표준 입력(stdin, FD 0), 표준 출력(stdout, FD 1), 표준 에러(stderr, FD 2). 리디렉션은 이 FD들의 대상을 바꿉니다.
# 표준 출력 리디렉션
make defconfig > build.log # stdout을 파일로 (덮어쓰기)
make defconfig >> build.log # stdout을 파일로 (추가)
# 표준 에러 리디렉션
make -j$(nproc) 2> errors.log # stderr을 파일로
make -j$(nproc) 2>> errors.log # stderr을 파일로 (추가)
# stdout과 stderr 동시 리디렉션
make -j$(nproc) > build.log 2>&1 # 순서 중요! stderr→stdout→파일
make -j$(nproc) &> build.log # Bash 전용 축약
# 표준 입력 리디렉션
wc -l < .config # 파일을 stdin으로
# /dev/null: 출력 무시 (커널 스크립트에서 매우 자주 사용)
command -v gcc >/dev/null 2>&1 # 존재 여부만 확인, 출력 무시
# exec로 FD 영구 변경 (스크립트 전체에 적용)
exec 3> debug.log # FD 3을 debug.log에 연결
echo "디버그 메시지" >&3 # FD 3에 쓰기
exec 3>&- # FD 3 닫기
# exec로 stdin 영구 변경
exec 4<&0 # 원래 stdin을 FD 4에 백업
exec < .config # stdin을 .config로 변경
while read line; do
echo "$line"
done
exec 0<&4 # stdin 복원
exec 4<&- # FD 4 닫기
Here Document와 Here String
Here Document(히어 문서)는 셸 스크립트 내에서 여러 줄의 텍스트를 명령의 표준 입력으로 전달하는 리디렉션 형태입니다. 커널 빌드 스크립트에서 설정 파일 생성, 패치(Patch) 적용, init 스크립트 작성 등에 자주 사용됩니다.
# 기본 Here Document
cat <<EOF
커널 버전: $(uname -r)
아키텍처: $(uname -m)
날짜: $(date)
EOF
# 변수 확장 억제 (작은따옴표로 구분자 감싸기)
cat <<'EOF'
$HOME은 확장되지 않습니다.
$(uname -r) 도 실행되지 않습니다.
EOF
# 들여쓰기 탭 제거 (<<- 사용, 탭만 제거)
if true; then
cat <<-EOF
이 줄의 선행 탭은 제거됩니다.
코드 가독성을 위해 들여쓰기할 수 있습니다.
EOF
fi
# Here String (Bash 전용): 한 줄 입력
read -r major minor patch <<< "$(uname -r | tr '.-' ' ')"
echo "메이저: $major, 마이너: $minor, 패치: $patch"
# 실전: Kconfig 프래그먼트 생성
cat > /tmp/debug.config <<EOF
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_DWARF5=y
CONFIG_GDB_SCRIPTS=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
EOF
scripts/kconfig/merge_config.sh .config /tmp/debug.config
Here Document의 구분자(Delimiter) 처리 규칙 정리:
| 구문 | 변수 확장 | 명령 치환 | 선행 탭 제거 | 사용 상황 |
|---|---|---|---|---|
<<EOF | 예 | 예 | 아니오 | 동적 콘텐츠 생성 (설정 파일, 스크립트) |
<<'EOF' | 아니오 | 아니오 | 아니오 | 리터럴 텍스트 (스크립트 내 스크립트, 정규식) |
<<-EOF | 예 | 예 | 예 (탭만) | 들여쓰기된 코드 블록 내에서 가독성 |
<<-'EOF' | 아니오 | 아니오 | 예 (탭만) | 들여쓰기 + 리터럴 |
# 실전: 원격 서버에서 실행할 스크립트를 heredoc으로 생성
# 구분자를 따옴표로 감싸서 로컬 변수 확장 방지
ssh build-server 'bash -s' <<'REMOTE_SCRIPT'
#!/bin/bash
cd /home/builder/linux
git pull
make -j$(nproc) # $(nproc)은 원격 서버에서 확장됨
echo "빌드 완료: $(date)"
REMOTE_SCRIPT
# heredoc을 변수에 저장
read -r -d '' USAGE <<-'HELP' || true
사용법: $0 [옵션] <커널 소스 경로>
옵션:
-a ARCH 타겟 아키텍처 (기본: x86_64)
-j JOBS 병렬 빌드 수 (기본: nproc)
-c CONFIG 커널 설정 파일
-h 이 도움말 표시
HELP
프로세스 치환
프로세스 치환(Process Substitution)은 명령의 출력을 마치 파일인 것처럼 사용할 수 있게 하는
Bash 전용 기능입니다. <(cmd)는 cmd의 출력을 읽을 수 있는 파일 경로로 치환하고,
>(cmd)는 cmd의 입력으로 쓸 수 있는 파일 경로로 치환합니다.
내부적으로 /dev/fd/N 파일 디스크립터를 사용합니다.
# 두 커널 설정 비교 (임시 파일 없이)
diff <(grep "^CONFIG_" old.config | sort) \
<(grep "^CONFIG_" new.config | sort)
# 여러 소스에서 동시에 읽기
paste <(cut -d: -f1 /etc/passwd) <(cut -d: -f7 /etc/passwd)
# tee와 프로세스 치환: 출력을 여러 곳으로 분기
make -j$(nproc) 2>&1 | tee >(grep -i error > errors.log) \
>(grep -i warning > warnings.log) \
> build.log
# while read에서 서브셸 문제 회피
while IFS= read -r line; do
# 이 루프는 현재 셸에서 실행됨
process "$line"
done < <(find /sys/class/net -maxdepth 1 -type l)
프로세스 모델과 잡 제어
Bash의 프로세스 모델을 이해하는 것은 셸 스크립팅의 고급 주제이자 커널 개발자에게는 기본 소양입니다.
Bash가 명령을 실행하는 방식은 리눅스 커널의 프로세스 관리 메커니즘과 직접 연결됩니다.
fork(), exec(), wait() 시스템 호출의 동작을 이해하면
파이프라인, 백그라운드 실행, 잡 제어의 내부 메커니즘이 명확해집니다.
프로세스 생성
Bash에서 외부 명령을 실행하면 다음 과정이 발생합니다:
(1) fork()로 자식 프로세스 생성 — 부모의 메모리를 COW(Copy-On-Write)로 복제,
(2) 자식에서 리디렉션 설정 — dup2()로 FD 재배치(Relocation),
(3) 자식에서 execve()로 프로그램 로드 — 기존 메모리 이미지를 새 프로그램으로 교체,
(4) 부모(Bash)가 waitpid()로 자식 종료 대기 — 종료 상태 수집.
파이프라인에서는 각 단계가 별도 프로세스로 생성되며, pipe() 시스템 호출로 연결됩니다.
빌트인 명령(예: cd, echo, read)은 fork 없이 현재 셸 프로세스에서 직접 실행되므로
프로세스 생성 비용이 없습니다. 이것이 성능 최적화에서 빌트인을 선호하는 이유입니다.
함수도 마찬가지로 현재 셸에서 실행되며, exec 명령은 fork 없이 현재 프로세스를 새 프로그램으로 교체합니다.
# 단일 명령: fork → exec → wait
ls -la /proc/self
# 파이프라인: 각 단계가 별도 프로세스
cat /proc/cpuinfo | grep "model name" | sort -u | wc -l
# 4개의 프로세스 생성: cat, grep, sort, wc
# 서브셸: fork만 (exec 없음)
(cd /tmp && pwd) # 서브셸에서 cd → 부모에 영향 없음
pwd # 원래 디렉토리 유지
# 명령 치환: 서브셸 생성
result="$(expensive_command)" # fork → 서브셸에서 실행 → stdout 캡처
# 백그라운드 실행: fork 후 wait하지 않음
make -j$(nproc) &
BUILD_PID=$!
echo "빌드 PID: $BUILD_PID"
wait "$BUILD_PID"
echo "빌드 종료 상태: $?"
# 프로세스 그룹: 파이프라인 전체를 하나의 그룹으로 관리
echo "현재 프로세스 그룹: $(ps -o pgid= -p $$)"
잡 제어
잡 제어(Job Control)는 Bash가 제공하는 프로세스 그룹 기반의 작업 관리 시스템입니다. 대화형(Interactive) 셸에서 기본으로 활성화되며, 세션(Session), 프로세스 그룹(Process Group), 포그라운드(Foreground)/백그라운드(Background) 개념을 사용합니다. 터미널은 하나의 세션과 연결되며, 세션 내에서 하나의 프로세스 그룹만이 포그라운드가 됩니다.
# 잡 제어 명령어
make -j$(nproc) & # 백그라운드 실행, 잡 번호 부여 [1]
jobs # 현재 잡 목록
fg %1 # 잡 1을 포그라운드로
# Ctrl+Z # SIGTSTP 전송 → 일시 정지
bg %1 # 잡 1을 백그라운드에서 재개
wait %1 # 잡 1 종료 대기
disown %1 # 잡 1을 잡 테이블에서 제거 (로그아웃 후에도 계속 실행)
# wait를 이용한 병렬 빌드 패턴
for arch in x86_64 arm64 riscv; do
(
make ARCH="$arch" defconfig
make ARCH="$arch" -j$(nproc)
) &
done
wait # 모든 백그라운드 잡 완료 대기
echo "전체 빌드 완료"
종료 상태와 에러 처리
모든 명령은 0(성공)부터 255까지의 종료 상태(Exit Status)를 반환합니다.
이 값은 $?로 참조할 수 있으며, 셸 스크립트의 에러 처리 메커니즘의 기반입니다.
시그널에 의한 종료는 128+시그널번호가 됩니다(예: SIGKILL=137).
#!/bin/bash
# 엄격 모드 (Strict Mode) — 커널 스크립트 권장 패턴
set -e # 명령 실패 시 스크립트 즉시 종료 (errexit)
set -u # 미정의 변수 참조 시 에러 (nounset)
set -o pipefail # 파이프라인 중 하나라도 실패하면 전체 실패
# 축약: set -euo pipefail
# set -e의 함정: 이 경우에는 동작하지 않음
if ! make -j$(nproc); then # if 조건에서는 set -e 비활성
echo "빌드 실패"
fi
# pipefail 효과
set -o pipefail
make 2>&1 | tee build.log
echo "파이프라인 종료 상태: $?" # make가 실패하면 0이 아닌 값
echo "개별 상태: ${PIPESTATUS[@]}" # 예: "2 0" (make=2, tee=0)
# trap ERR: 에러 발생 시 처리기 실행
on_error() {
echo "오류 발생: 줄 $1, 명령: $2, 종료코드: $3" >&2
}
trap 'on_error $LINENO "$BASH_COMMAND" $?' ERR
set -e에는 여러 함정이 있어 동작 규칙을 정확히 이해해야 합니다.
다음 상황에서는 set -e가 스크립트를 종료시키지 않습니다:
| 상황 | 이유 | 예시 |
|---|---|---|
if/while/until 조건 | 조건부 실행에서는 실패가 정상 흐름 | if ! grep -q pattern file; then ... |
&& / || 체인 내부 | 논리 연산자 좌변 실패는 흐름 제어(Flow Control) | cmd1 && cmd2 — cmd1 실패 시 종료 안 함 |
! 부정 명령 | 실패가 성공으로 반전됨 | ! false → 종료 상태 0 |
| 서브셸 내부 (부모에 미전파) | 서브셸은 별도 프로세스 | (false; echo "실행됨") — 서브셸 내에서만 적용 |
| 함수 호출이 조건 위치일 때 | 함수 전체가 조건으로 취급됨 | if my_func; then ... |
# set -e 함정 시연
set -e
# 함정 1: 함수 내부의 에러가 무시됨
my_check() {
false # 이 실패로 종료되어야 하지만...
echo "여기까지 도달"
}
if my_check; then # if 조건 → set -e 비활성화
echo "성공"
fi
# 함정 2: || 뒤에서는 set -e 비활성
false || echo "이것은 실행됨" # 종료되지 않음
# 안전한 패턴: 명시적 에러 처리 조합
set -euo pipefail
# 실패해도 되는 명령은 || true로 명시
rm -f /tmp/maybe-not-exist || true
# 반환 코드를 보존하며 처리
if output="$(make -j$(nproc) 2>&1)"; then
echo "빌드 성공"
else
rc=$?
echo "빌드 실패 (코드: $rc)" >&2
echo "$output" | tail -20 >&2
exit "$rc"
fi
시그널 처리
시그널이란 무엇인가 — 커널이 프로세스에 보내는 비동기 메시지
시그널(Signal)은 운영체제 커널이 실행 중인 프로세스에게 비동기적으로 전달하는 소프트웨어 인터럽트(Interrupt)입니다.
프로세스는 현재 무슨 작업을 하고 있든 상관없이 시그널을 받을 수 있으며,
각 시그널에는 기본 동작(종료, 무시, 코어 덤프 등)이 미리 정해져 있습니다.
시그널은 커널뿐 아니라 다른 프로세스나 프로세스 자신도 kill(2) 시스템 호출로 보낼 수 있습니다.
| 시그널이 발생하는 상황 | 발생 시그널 | 기본 동작 |
|---|---|---|
| 터미널에서 Ctrl+C 입력 | SIGINT (2) |
프로세스 종료 |
kill PID 명령 실행 |
SIGTERM (15) |
프로세스 정상 종료 요청 |
kill -9 PID 명령 실행 |
SIGKILL (9) |
강제 종료 — 프로세스가 막을 수 없음 |
| 파이프 수신 측이 먼저 종료 | SIGPIPE (13) |
쓰기 프로세스 종료 |
trap cleanup EXIT을 설정하면 스크립트가 어떻게 종료되든(정상·시그널·에러) cleanup 함수가 실행됩니다.
시그널은 리눅스 커널이 프로세스에게 비동기적 이벤트를 알리는 메커니즘입니다.
Bash는 trap 빌트인을 통해 시그널 핸들러(Handler)를 등록할 수 있으며,
이는 스크립트의 안정적인 실행을 위해 필수적인 기능입니다.
임시 파일 정리, 잠금(Lock) 파일 해제, 하위 프로세스 종료 등의 정리(Cleanup) 작업을
시그널 발생 시에도 보장하려면 적절한 트랩 설정이 필요합니다.
trap 명령
trap은 셸 스크립트에서 시그널 핸들러를 등록하는 빌트인 명령입니다.
임시 파일 정리, 잠금 해제, 하위 프로세스 종료 등의 정리 작업을 보장하기 위해 사용합니다.
Bash는 실제 시그널뿐 아니라 EXIT(스크립트 종료),
ERR(명령 실패), DEBUG(매 명령 실행 전),
RETURN(함수/소스 반환) 같은 의사 시그널(Pseudo-Signal)도 지원합니다.
| 시그널 | 번호 | 기본 동작 | Bash trap 용도 |
|---|---|---|---|
| SIGHUP | 1 | 종료 | 설정 재로드, 데몬 다시 시작 관례 |
| SIGINT | 2 | 종료 | Ctrl+C 처리, 사용자 중단 시 정리 |
| SIGQUIT | 3 | 코어 덤프(Core Dump), 즉 덤프(Dump) 파일 생성 | 디버깅용, 일반적으로 trap하지 않음 |
| SIGTERM | 15 | 종료 | kill 기본 시그널, 정상 종료 정리 |
| SIGCHLD | 17 | 무시 | 자식 프로세스 종료 감지 |
| SIGUSR1/2 | 10/12 | 종료 | 스크립트 간 커스텀 통신 |
| SIGPIPE | 13 | 종료 | 파이프 끊김 처리 (쓰기 측) |
| EXIT | 의사 | N/A | 스크립트 종료 시 항상 실행 (가장 중요) |
| ERR | 의사 | N/A | 명령 실패 시 실행 (set -e와 연동) |
| DEBUG | 의사 | N/A | 매 명령 실행 전 실행 (프로파일링(Profiling)) |
| RETURN | 의사 | N/A | 함수/source 반환 시 실행 |
# trap 기본 문법
trap '명령' 시그널 [시그널...]
trap '' 시그널 # 시그널 무시
trap - 시그널 # 기본 동작 복원
trap -p # 현재 트랩 목록 출력
# 주요 시그널과 트랩
trap 'echo "인터럽트!"; exit 130' INT # Ctrl+C (SIGINT=2)
trap 'cleanup; exit 143' TERM # kill (SIGTERM=15)
trap 'cleanup' EXIT # 스크립트 종료 시 항상 실행
trap 'echo "자식 프로세스 종료"' CHLD # 자식 프로세스 종료 시
trap 'reload_config' HUP # 설정 재로드 관례
# 실전 패턴: 안전한 임시 파일 관리
TMPDIR=""
LOCKFILE=""
cleanup() {
local exit_code=$?
[[ -n "$TMPDIR" ]] && rm -rf "$TMPDIR"
[[ -n "$LOCKFILE" ]] && rm -f "$LOCKFILE"
# 하위 프로세스 정리
kill 0 2>/dev/null || true
exit "$exit_code"
}
trap cleanup EXIT INT TERM
TMPDIR="$(mktemp -d)"
LOCKFILE="$(mktemp)"
시그널 처리 내부 동작
Bash는 시그널을 즉시 처리하지 않습니다. 커널이 시그널을 전달하면 Bash의 C 레벨 시그널 핸들러가
내부 플래그(pending_traps[])를 설정하고, Bash 이벤트 루프(Event Loop)의 안전한 지점에서
등록된 트랩 명령을 실행합니다. 이러한 보류 실행(Deferred Execution) 방식 때문에,
빌트인 명령(예: read, wait) 실행 중에 시그널이 도착하면
해당 빌트인이 먼저 완료(또는 중단)된 후 트랩이 실행됩니다.
SIGCHLD와 좀비 프로세스: 자식 프로세스가 종료되면 커널은 부모에게
SIGCHLD를 보냅니다. 부모가 wait()/waitpid()로 자식의
종료 상태를 수집하기 전까지 자식은 좀비(Zombie) 상태(Z)로 남습니다.
Bash는 대화형 모드에서 백그라운드 잡의 종료를 자동으로 감지하여 프롬프트에 표시하지만,
비대화형 스크립트에서는 wait를 명시적으로 호출해야 좀비를 방지합니다.
trap '' CHLD로 SIGCHLD를 무시하면 자식이 자동으로 수확(Reap)됩니다.
IPC (프로세스 간 통신)
셸 스크립트에서의 프로세스 간 통신(IPC, Inter-Process Communication)은 파이프, FIFO, 소켓, 공유 파일 등 다양한 메커니즘을 활용합니다. 이들은 모두 커널이 제공하는 IPC 기능을 셸 수준에서 추상화한 것입니다.
파이프와 FIFO
익명 파이프(Anonymous Pipe)는 | 연산자로 생성되며,
부모-자식 관계의 프로세스 간에만 사용할 수 있습니다.
내부적으로 커널의 pipe() 시스템 호출이 사용되며,
기본 용량은 보통 16페이지(Page)(4KB 페이지 시스템에서는 64KB)이지만, 커널 버전과
pipe-user-pages-soft 제한에 따라 더 작아질 수 있습니다.
/proc/sys/fs/pipe-max-size는 비특권 사용자가 설정할 수 있는 최대치이며,
실제 파이프 용량은 fcntl(F_GETPIPE_SZ)로 조회합니다.
버퍼(Buffer)가 가득 차면 쓰기 측이 블록(Block)되고, 비어있으면 읽기 측이 블록됩니다.
이 특성 때문에 dmesg | grep처럼 대량 출력을 필터링할 때 메모리 사용이 일정합니다.
명명된 파이프(Named Pipe, FIFO)는 mkfifo로 파일시스템(Filesystem)에 이름을 가진 특수 파일을 생성하므로
관계없는 프로세스 간 통신에 사용할 수 있습니다.
FIFO는 열기(open) 시 읽기/쓰기 양쪽이 모두 연결될 때까지 블록되는 점에 유의해야 합니다.
# 익명 파이프: 실제 기본 용량 확인
python3 - <<'PY'
import os, fcntl
r, w = os.pipe()
print(fcntl.fcntl(r, fcntl.F_GETPIPE_SZ))
os.close(r)
os.close(w)
PY
# 비특권 사용자가 설정할 수 있는 최대치
cat /proc/sys/fs/pipe-max-size
# 명명된 파이프 (FIFO)
FIFO_PATH="/tmp/kernel_build_fifo"
mkfifo "$FIFO_PATH"
# 프로듀서 (백그라운드)
(
echo "빌드 시작: $(date)"
make -j$(nproc) 2>&1
echo "빌드 완료: $(date)"
) > "$FIFO_PATH" &
# 컨슈머
while IFS= read -r line; do
echo "[모니터] $line"
done < "$FIFO_PATH"
rm -f "$FIFO_PATH"
코프로세스
코프로세스(Coprocess)는 Bash 4.0에서 도입된 기능으로,
백그라운드 프로세스와 양방향 파이프를 자동으로 설정합니다.
coproc 키워드를 사용하며, 코프로세스의 stdin/stdout에 연결된
FD가 ${COPROC[0]}(읽기)과 ${COPROC[1]}(쓰기)에 저장됩니다.
일반적으로 양방향 대화가 필요한 외부 프로세스(계산기, 데이터베이스 클라이언트, 커스텀 프로토콜)와
통신할 때 유용합니다.
# 코프로세스 기본 사용 — bc 계산기와 양방향 통신
coproc BC { bc -l; }
# 코프로세스에 계산 요청
echo "scale=4; 22/7" >&"${BC[1]}"
read -r result <&"${BC[0]}"
echo "결과: $result" # 3.1428
# 여러 번 재사용 (프로세스 재생성 비용 없음)
echo "2^32" >&"${BC[1]}"
read -r result <&"${BC[0]}"
echo "2^32 = $result" # 4294967296
# 코프로세스 종료 (쓰기 FD를 닫으면 EOF 전달)
exec {BC[1]}>&-
wait "$BC_PID"
# 이름 없는 코프로세스 (COPROC 배열 사용)
coproc { while read -r line; do echo "ECHO: $line"; done; }
echo "hello" >&"${COPROC[1]}"
read -r reply <&"${COPROC[0]}"
echo "$reply" # ECHO: hello
exec {COPROC[1]}>&-
wait
코프로세스를 사용할 때 주의할 점:
(1) 이름이 있는 코프로세스는 하나만 존재할 수 있으며(Bash 4.x 제한), 동일 이름으로 재생성하면 기존 것이 종료됩니다.
(2) 읽기 FD에서 read 시 타임아웃(read -t 5)을 설정하지 않으면 교착 상태(Deadlock)에 빠질 수 있습니다.
(3) 코프로세스가 예기치 않게 종료되면 쓰기 FD에 쓸 때 SIGPIPE가 발생합니다.
# 타임아웃과 에러 처리가 있는 안전한 코프로세스 패턴
coproc WORKER { while read -r cmd; do eval "$cmd" 2>&1 || echo "ERROR:$?"; done; }
send_to_coproc() {
local cmd="$1" timeout="${2:-10}"
echo "$cmd" >&"${WORKER[1]}" || { echo "코프로세스 통신 실패" >&2; return 1; }
if ! read -r -t "$timeout" reply <&"${WORKER[0]}"; then
echo "타임아웃: $timeout초 초과" >&2
return 1
fi
echo "$reply"
}
send_to_coproc "uname -r"
send_to_coproc "cat /proc/version"
exec {WORKER[1]}>&-
wait "$WORKER_PID"
셸 스크립트 IPC 패턴
실전 스크립트에서는 다양한 IPC 패턴을 조합하여 사용합니다.
파일 잠금(File Lock)(flock), TCP/UDP 소켓(/dev/tcp), 공유 파일 등을 활용한 대표적인 패턴을 살펴보겠습니다.
# 1. flock: 파일 락을 이용한 동시성 제어
(
flock -n 200 || die "이미 실행 중입니다"
# 임계 영역: 이 코드는 하나의 인스턴스만 실행
make -j$(nproc)
) 200>/var/lock/kernel-build.lock
# 2. /dev/tcp: 네트워크 통신 (Bash 전용)
exec 3<>/dev/tcp/build-server/8080
echo "GET /status HTTP/1.0" >&3
echo "" >&3
cat <&3
exec 3>&-
# 3. 공유 파일을 이용한 워커 패턴
WORK_DIR="$(mktemp -d)"
NUM_WORKERS=4
TASK_FILE="${WORK_DIR}/tasks"
LOCK_FILE="${WORK_DIR}/tasks.lock"
cat > "${TASK_FILE}" <<'EOF'
make -C drivers/net
make -C fs/ext4
make -C kernel/sched
make -C mm
EOF
worker() {
local worker_id="$1" task
while true; do
task=""
{
flock -x 200
if IFS= read -r task < "${TASK_FILE}"; then
tail -n +2 "${TASK_FILE}" > "${TASK_FILE}.next"
mv "${TASK_FILE}.next" "${TASK_FILE}"
fi
} 200>"${LOCK_FILE}"
[[ -n "$task" ]] || break
echo "워커 ${worker_id}: ${task} 처리 중"
bash -lc "$task"
done
}
for ((i=0; i<NUM_WORKERS; i++)); do
worker "$i" &
done
wait
rm -rf "${WORK_DIR}"
/proc과 /sys 인터페이스 활용
리눅스 커널은 /proc(procfs)과 /sys(sysfs) 가상 파일시스템(VFS)을 통해
커널 내부 상태를 사용자 공간(User Space)에 노출합니다. 이 인터페이스들은 일반 파일처럼 읽고 쓸 수 있으므로
Bash 스크립트로 커널 상태 모니터링과 튜닝을 쉽게 수행할 수 있습니다.
커널 개발자에게 이 인터페이스를 셸로 다루는 능력은 디버깅과 성능 분석의 기본입니다.
/proc 파일시스템 스크립팅
/proc은 커널과 프로세스 정보를 제공하는 가상 파일시스템입니다.
/proc/[pid]/는 개별 프로세스 정보를, /proc/sys/는 sysctl로도 접근 가능한
커널 튜너블(Tunable) 매개변수를 제공합니다.
#!/bin/bash
# CPU 정보 요약
cpu_model() {
grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | sed 's/^ //'
}
cpu_count() {
grep -c "^processor" /proc/cpuinfo
}
echo "CPU: $(cpu_model) x$(cpu_count)"
# 메모리 정보 파싱
mem_total_kb="$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)"
mem_avail_kb="$(awk '/^MemAvailable:/ {print $2}' /proc/meminfo)"
mem_used_pct=$(( (mem_total_kb - mem_avail_kb) * 100 / mem_total_kb ))
echo "메모리: ${mem_used_pct}% 사용 중 ($(( mem_total_kb / 1024 ))MB 중 $(( (mem_total_kb - mem_avail_kb) / 1024 ))MB)"
# 프로세스 정보 조회
show_proc_info() {
local pid="$1"
[[ -d "/proc/$pid" ]] || return 1
echo "PID: $pid"
echo " 실행파일: $(readlink -f /proc/$pid/exe 2>/dev/null)"
echo " 명령행: $(tr '\0' ' ' < /proc/$pid/cmdline)"
echo " 상태: $(awk '/^State:/ {print $2, $3}' /proc/$pid/status)"
echo " RSS: $(awk '/^VmRSS:/ {print $2, $3}' /proc/$pid/status)"
echo " 스레드: $(awk '/^Threads:/ {print $2}' /proc/$pid/status)"
}
# sysctl 매개변수 조회/변경
echo "vm.swappiness = $(cat /proc/sys/vm/swappiness)"
echo "kernel.pid_max = $(cat /proc/sys/kernel/pid_max)"
echo "net.core.somaxconn = $(cat /proc/sys/net/core/somaxconn)"
sysfs 스크립팅
/sys(sysfs)는 디바이스 모델, 드라이버, 버스(Bus) 정보를 계층적으로 노출하는 가상 파일시스템입니다.
/sys/class/는 디바이스를 기능별로 분류하고,
/sys/devices/는 물리적 토폴로지(Topology)를 반영하며,
/sys/module/은 로드된 커널 모듈(Kernel Module)의 매개변수를 노출합니다.
# 네트워크 인터페이스 정보
for iface in /sys/class/net/*; do
name="$(basename "$iface")"
[[ "$name" == "lo" ]] && continue
state="$(cat "$iface/operstate" 2>/dev/null)"
speed="$(cat "$iface/speed" 2>/dev/null || echo "N/A")"
mac="$(cat "$iface/address" 2>/dev/null)"
echo "${name}: 상태=${state}, 속도=${speed}Mbps, MAC=${mac}"
done
# 블록 디바이스 정보
for dev in /sys/block/sd* /sys/block/nvme*; do
[[ -e "$dev" ]] || continue
name="$(basename "$dev")"
size_sectors="$(cat "$dev/size")"
size_gb=$(( size_sectors * 512 / 1024 / 1024 / 1024 ))
sched="$(cat "$dev/queue/scheduler" 2>/dev/null)"
echo "${name}: ${size_gb}GB, 스케줄러=${sched}"
done
# 모듈 매개변수 조회
if [[ -d /sys/module/e1000e/parameters ]]; then
echo "e1000e 모듈 매개변수:"
for param in /sys/module/e1000e/parameters/*; do
echo " $(basename "$param") = $(cat "$param" 2>/dev/null)"
done
fi
커널 튜닝 스크립트 예제
다음은 Bash를 사용하여 커널 매개변수를 모니터링하고 튜닝하는 실전 스크립트 패턴입니다. 서버 성능 최적화, 네트워크 튜닝, 메모리 관리(Memory Management) 등에 활용할 수 있습니다.
#!/bin/bash
# 커널 튜닝 스크립트 예제
set -euo pipefail
# CPU 거버너 일괄 변경
set_cpu_governor() {
local governor="$1"
local cpu
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
local gov_file="${cpu}/cpufreq/scaling_governor"
[[ -w "$gov_file" ]] || continue
echo "$governor" > "$gov_file"
done
echo "CPU 거버너: $governor 설정 완료"
}
# 네트워크 성능 튜닝
tune_network() {
sysctl -w net.core.somaxconn=4096
sysctl -w net.core.netdev_max_backlog=5000
sysctl -w net.ipv4.tcp_max_syn_backlog=8192
sysctl -w net.ipv4.tcp_fastopen=3
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
}
# 투명 대규모 페이지(THP) 설정
configure_thp() {
local mode="$1" # always, madvise, never
echo "$mode" > /sys/kernel/mm/transparent_hugepage/enabled
echo "$mode" > /sys/kernel/mm/transparent_hugepage/defrag
echo "THP: $mode 설정 완료"
}
# 시스템 상태 요약 출력
system_summary() {
echo "=== 시스템 상태 ==="
echo "커널: $(uname -r)"
echo "가동시간: $(uptime -p)"
echo "로드: $(cat /proc/loadavg | cut -d' ' -f1-3)"
echo "메모리: $(free -h | awk '/^Mem:/ {print $3 "/" $2}')"
echo "스왑: $(free -h | awk '/^Swap:/ {print $3 "/" $2}')"
echo "CPU 거버너: $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo N/A)"
echo "THP: $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || echo N/A)"
}
커널 빌드 & 개발 스크립팅
리눅스 커널 소스 트리는 수백 개의 셸 스크립트를 포함하고 있으며,
빌드 시스템(Kbuild)의 핵심 구성 요소로 작동합니다.
scripts/ 디렉토리의 스크립트들은 설정 관리, 버전 생성, 헤더 설치,
모듈 서명, 패키징 등 빌드의 거의 모든 단계에 관여합니다.
이 절에서는 커널 개발에 직접 활용할 수 있는 Bash 스크립팅 패턴을 살펴봅니다.
Kconfig 관련 스크립트
커널 설정(Configuration)은 .config 파일에 저장되며,
scripts/config 스크립트로 프로그래밍 방식으로 조작할 수 있습니다.
CI/CD 환경이나 자동화 빌드에서 대화형 menuconfig 대신 이 스크립트를 사용합니다.
# scripts/config — Kconfig 설정 조작 도구
./scripts/config --enable CONFIG_DEBUG_INFO
./scripts/config --disable CONFIG_DEBUG_INFO_REDUCED
./scripts/config --set-val CONFIG_LOG_BUF_SHIFT 18
./scripts/config --set-str CONFIG_LOCALVERSION "-custom"
./scripts/config --module CONFIG_EXT4_FS
./scripts/config --state CONFIG_SMP # 현재 상태 조회
# scripts/diffconfig — 두 설정 비교
./scripts/diffconfig .config.old .config
# 출력: CONFIG_DEBUG_INFO n -> y
# CONFIG_KASAN -y (제거됨)
# +CONFIG_UBSAN y (추가됨)
# scripts/kconfig/merge_config.sh — 설정 프래그먼트 병합
# 기본 defconfig + 디버그 + 테스트 설정을 병합
make defconfig
./scripts/kconfig/merge_config.sh .config \
kernel/configs/debug.config \
kernel/configs/kvm_guest.config
# 자동화 스크립트: 다중 설정 빌드
for fragment in configs/*.config; do
name="$(basename "$fragment" .config)"
echo "=== 빌드: $name ==="
make defconfig
./scripts/kconfig/merge_config.sh .config "$fragment"
make -j"$(nproc)" 2>&1 | tee "build-${name}.log"
done
커널 테스트 자동화
커널 셀프테스트(kselftest) 프레임워크는 셸 스크립트로 테스트 실행과 결과 수집을 자동화합니다. QEMU를 활용한 가상 환경 테스트, 회귀 테스트, 성능 벤치마크 등을 Bash 스크립트로 완전히 자동화할 수 있습니다.
#!/bin/bash
# QEMU 기반 커널 테스트 자동화
set -euo pipefail
KERNEL="arch/x86/boot/bzImage"
ROOTFS="rootfs.img"
TIMEOUT=300
run_qemu_test() {
local test_script="$1"
local log="$2"
timeout "$TIMEOUT" qemu-system-x86_64 \
-kernel "$KERNEL" \
-drive file="$ROOTFS",format=raw \
-append "root=/dev/sda console=ttyS0 init=$test_script" \
-nographic \
-m 512M \
-smp 2 \
-no-reboot \
> "$log" 2>&1
if grep -q "TEST PASSED" "$log"; then
echo "PASS: $test_script"
return 0
else
echo "FAIL: $test_script (로그: $log)"
return 1
fi
}
# kselftest 실행
make -C tools/testing/selftests TARGETS="size timers" run_tests
테스트 결과 로그를 파싱하여 TAP(Test Anything Protocol) 형식의 결과를 추출하고, 자동으로 성공/실패를 집계하는 패턴은 CI/CD 파이프라인에서 필수적입니다.
# 테스트 결과 로그 파싱 — TAP 형식 (Test Anything Protocol)
parse_tap_results() {
local log="$1"
local pass=0 fail=0 skip=0
while IFS= read -r line; do
case "$line" in
ok\ *) ((pass++)) ;;
not\ ok\ *) ((fail++)) ;;
*SKIP*) ((skip++)) ;;
esac
done < "$log"
echo "결과: PASS=$pass FAIL=$fail SKIP=$skip"
[[ "$fail" -eq 0 ]]
}
# QEMU 직렬 출력에서 커널 패닉/오류 자동 탐지
check_kernel_log() {
local log="$1"
local errors=0
# 치명적 오류 패턴
if grep -qE "Kernel panic|BUG:|WARNING:|Call Trace:" "$log"; then
echo "커널 오류 탐지:" >&2
grep -nE "Kernel panic|BUG:|WARNING:|Call Trace:" "$log" >&2
errors=1
fi
# KASAN/KFENCE 메모리 오류
if grep -qE "KASAN|KFENCE|use-after-free|out-of-bounds" "$log"; then
echo "메모리 오류 탐지:" >&2
grep -nE "KASAN|KFENCE" "$log" | head -5 >&2
errors=1
fi
return "$errors"
}
# 회귀 테스트 자동화 — 여러 테스트 스위트 순차 실행
TESTS_DIR="tools/testing/selftests"
TARGETS=("size" "timers" "net" "bpf")
RESULTS_DIR="$(mktemp -d)"
total_fail=0
for target in "${TARGETS[@]}"; do
echo "=== 테스트: $target ==="
log="${RESULTS_DIR}/${target}.log"
if make -C "$TESTS_DIR" TARGETS="$target" run_tests > "$log" 2>&1; then
parse_tap_results "$log"
else
echo "FAIL: $target (종료코드: $?)"
((total_fail++))
fi
done
echo "전체 결과: ${#TARGETS[@]}개 스위트 중 ${total_fail}개 실패"
[[ "$total_fail" -eq 0 ]] || exit 1
빌드 자동화
다중 아키텍처 빌드, git bisect run을 통한 회귀 추적, CI 파이프라인 통합 등
커널 빌드 자동화의 실전 패턴을 다룹니다.
#!/bin/bash
# 다중 아키텍처 빌드 스크립트
set -euo pipefail
declare -A CROSS_COMPILERS=(
[arm64]="aarch64-linux-gnu-"
[riscv]="riscv64-linux-gnu-"
[mips]="mips-linux-gnu-"
)
build_arch() {
local arch="$1"
local cross="${CROSS_COMPILERS[$arch]:-}"
local log="build-${arch}.log"
echo "[$(date +%H:%M:%S)] $arch 빌드 시작..."
make ARCH="$arch" CROSS_COMPILE="$cross" defconfig >/dev/null 2>&1
make ARCH="$arch" CROSS_COMPILE="$cross" -j"$(nproc)" 2>&1 | tee "$log"
echo "[$(date +%H:%M:%S)] $arch 빌드 완료"
}
# 병렬 빌드 (서브셸에서 실행)
for arch in "${!CROSS_COMPILERS[@]}"; do
build_arch "$arch" &
done
wait
echo "전체 빌드 완료"
# git bisect run: 회귀 커밋 자동 탐색
# git bisect start HEAD v6.1
# git bisect run ./test-regression.sh
# test-regression.sh 예제:
#!/bin/bash
make -j$(nproc) || exit 125 # 빌드 실패: skip
./run-test.sh # 테스트 실행 (0=good, 1=bad)
모듈 개발 스크립트
커널 모듈 개발 시 빌드, 설치, 테스트, DKMS 등록을 자동화하는 스크립트 패턴을 제공합니다.
#!/bin/bash
# 커널 모듈 개발 헬퍼 스크립트
set -euo pipefail
MODULE_NAME="mymodule"
KVER="$(uname -r)"
KDIR="/lib/modules/${KVER}/build"
build_module() {
make -C "$KDIR" M="$(pwd)" modules
}
install_module() {
sudo make -C "$KDIR" M="$(pwd)" modules_install
sudo depmod -a
}
load_module() {
sudo rmmod "$MODULE_NAME" 2>/dev/null || true
sudo insmod "./${MODULE_NAME}.ko" "$@"
dmesg | tail -5
}
test_params() {
echo "=== 모듈 매개변수 테스트 ==="
for val in 1 10 100 1000; do
load_module debug_level="$val"
echo "매개변수 값: $(cat /sys/module/${MODULE_NAME}/parameters/debug_level)"
sleep 1
done
}
case "${1:-build}" in
build) build_module ;;
install) install_module ;;
load) shift; load_module "$@" ;;
test) test_params ;;
clean) make -C "$KDIR" M="$(pwd)" clean ;;
*) echo "사용법: $0 {build|install|load|test|clean}" ;;
esac
고급 주제
이 절에서는 정규 표현식, 디버깅, 성능 최적화, 보안 등 Bash 스크립팅의 고급 주제를 다룹니다. 커널 개발자가 대규모 자동화 스크립트를 작성할 때 직면하는 실전 문제와 해결책을 중심으로 설명합니다.
정규 표현식과 텍스트 처리
Bash 3.0부터 [[ string =~ regex ]] 구문으로 확장 정규 표현식(ERE, Extended Regular Expression)을
직접 사용할 수 있습니다. BASH_REMATCH 배열로 캡처 그룹에 접근할 수 있어,
간단한 패턴 매칭에서는 grep/sed 호출 없이 처리할 수 있습니다.
# Bash 정규식 매칭
VERSION_STRING="Linux version 6.1.42-generic"
if [[ "$VERSION_STRING" =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
echo "전체 매칭: ${BASH_REMATCH[0]}" # 6.1.42
echo "메이저: ${BASH_REMATCH[1]}" # 6
echo "마이너: ${BASH_REMATCH[2]}" # 1
echo "패치: ${BASH_REMATCH[3]}" # 42
fi
# 커널 설정 파싱
parse_kconfig() {
local line
while IFS= read -r line; do
if [[ "$line" =~ ^CONFIG_([A-Z0-9_]+)=(.+)$ ]]; then
echo "옵션: ${BASH_REMATCH[1]}, 값: ${BASH_REMATCH[2]}"
fi
done < .config
}
# grep/sed/awk 통합 패턴
# 커널 로그에서 에러 추출 및 분류
dmesg | grep -E "(error|fault|panic|oops)" -i | \
sed 's/\[.*\] //' | \
sort | uniq -c | sort -rn | head -20
# awk로 /proc/meminfo 파싱
awk '/^(MemTotal|MemFree|MemAvailable|Buffers|Cached):/ {
printf "%-15s %8d MB\n", $1, $2/1024
}' /proc/meminfo
디버깅과 프로파일링
Bash 스크립트 디버깅에는 set -x(xtrace), PS4 커스터마이징,
BASH_XTRACEFD, 그리고 외부 도구인 ShellCheck이 핵심적으로 사용됩니다.
프로파일링은 BASH_XTRACEFD와 타임스탬프를 조합하여 수행할 수 있습니다.
# set -x: 실행되는 각 명령을 stderr에 출력
set -x
make defconfig # + make defconfig (출력됨)
set +x # xtrace 비활성화
# PS4 커스터마이징: 파일명, 줄번호, 함수명 포함
export PS4='+${BASH_SOURCE[0]}:${LINENO}:${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
# 출력: +build.sh:42:build_kernel(): make -j8
# BASH_XTRACEFD: xtrace 출력을 별도 파일로
exec 5> debug-trace.log
export BASH_XTRACEFD=5
set -x
# 이제 xtrace는 stderr가 아닌 debug-trace.log에 기록
# 프로파일링: 타임스탬프 포함 트레이스
export PS4='+$(date +%s.%N) ${BASH_SOURCE}:${LINENO}: '
set -x
# 이후 trace 로그에서 시간 차이를 계산하여 성능 병목(Bottleneck) 식별
# ShellCheck: 정적 분석 도구 (외부 설치 필요)
# shellcheck build-kernel.sh
# SC2086: Double quote to prevent globbing and word splitting.
# SC2155: Declare and assign separately to avoid masking return values.
# 디버그 함수 패턴
DEBUG="${DEBUG:-0}"
debug() {
[[ "$DEBUG" -ge 1 ]] && echo "[DEBUG] $*" >&2
}
debug2() {
[[ "$DEBUG" -ge 2 ]] && echo "[TRACE] $*" >&2
}
성능 최적화
Bash 스크립트의 성능 병목(Bottleneck)은 대부분 불필요한 서브셸/외부 프로세스 생성에서 발생합니다.
fork()+exec()는 비용이 높은 연산이므로, 가능하면 빌트인 명령과
매개변수 확장을 활용하여 외부 명령 호출을 최소화해야 합니다.
# 대량 데이터 처리: mapfile로 파일을 배열로 읽기 (Bash 4.0+)
# 느린 방법:
while IFS= read -r line; do
lines+=("$line")
done < bigfile.txt
# 빠른 방법:
mapfile -t lines < bigfile.txt
# 리디렉션 최적화: 루프 외부에서 한 번만
# 느린 방법:
for i in {1..1000}; do
echo "$i" >> output.txt # 매 반복 open/close
done
# 빠른 방법:
{
for i in {1..1000}; do
echo "$i"
done
} > output.txt # open 1회, close 1회
# printf vs echo: printf가 더 이식성 높고 빠른 경우 있음
printf '%s\n' "${lines[@]}" > output.txt
보안 고려사항
셸 스크립트의 보안은 입력 검증, 안전한 임시 파일 관리, PATH 강화, 그리고
위험한 구문(eval) 회피에 집중됩니다. 특히 eval은 코드 인젝션(Code Injection)의
주요 공격 벡터이므로 가능하면 사용하지 않아야 합니다.
# 1. PATH 강화: 절대 경로 또는 신뢰할 수 있는 디렉토리만
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"
# 2. 안전한 임시 파일: mktemp 사용 (예측 불가능한 파일명)
tmpfile="$(mktemp)" # /tmp/tmp.XXXXXXXXXX
tmpdir="$(mktemp -d)" # /tmp/tmp.XXXXXXXXXX/
trap 'rm -rf "$tmpfile" "$tmpdir"' EXIT
# 3. eval 회피
# 위험한 코드:
# eval "cmd_$user_input" # 인젝션 가능!
# 안전한 대안: 연관 배열 + 화이트리스트
declare -A COMMANDS=(
[build]="do_build"
[test]="do_test"
[clean]="do_clean"
)
action="$1"
if [[ -n "${COMMANDS[$action]+exists}" ]]; then
"${COMMANDS[$action]}" # 화이트리스트 함수만 실행
else
die "알 수 없는 명령: $action"
fi
# 4. 입력 검증 패턴
validate_arch() {
local arch="$1"
case "$arch" in
x86|x86_64|arm|arm64|riscv|mips|s390) return 0 ;;
*) die "유효하지 않은 아키텍처: $arch" ;;
esac
}
# 5. IFS 보호: 공백 처리 안전하게
local old_ifs="$IFS"
IFS=$'\n'
files=( $(find . -name "*.config") )
IFS="$old_ifs"
Bash 5.3 신기능 (2025)
Bash 5.3은 2025년 7월에 공식 릴리스된 메이저 업데이트로, 5.2 이후 약 3년 만의 대규모 변경을 포함합니다.
가장 주목할 변화는 서브셸 없이 명령 치환(Command Substitution)을 수행할 수 있는 신규 구문과
파일명 완성 정렬을 제어하는 GLOBSORT 변수이며, 소스 코드가 C23(ISO/IEC 9899:2024) 요구사항으로 정리되어
K&R 스타일 C 컴파일러로는 더 이상 빌드되지 않습니다.
CHANGES 파일 또는
공식 NEWS 문서를 확인하세요.
LTS 배포판(RHEL 9, Ubuntu 24.04)은 아직 5.2 이하를 번들하므로, 최신 기능 사용 시 빌드 도구 체인에서 버전 확인이 필요합니다.
Fork 없는 명령 치환
전통적인 $(...)은 서브셸을 생성해 자식 프로세스에서 명령을 실행한 뒤 표준 출력을 캡처합니다.
이 방식은 정확한 격리를 보장하지만, 짧은 명령을 수만 번 반복하는 스크립트에서는 fork()/exec()
비용이 성능 병목이 됩니다. Bash 5.3은 두 가지 새로운 형태의 명령 치환을 도입하여 이 문제를 완화합니다.
| 구문 | 동작 | 용도 |
|---|---|---|
$(command) | 서브셸에서 실행 후 stdout 캡처 | Bash 5.2 이하 표준 |
${ command;} | 현재 셸에서 실행, stdout 캡처 (fork 없음) | 5.3+ 신규 |
${| command;} | 현재 셸에서 실행, 결과를 REPLY에 저장 | 5.3+ 신규 |
# 기존 (fork 발생): 반복 루프에서 느림
for i in {1..10000}; do
ts=$(date +%s) # 매 반복 fork
done
# Bash 5.3 (fork 없음): 동일 셸에서 실행
for i in {1..10000}; do
ts=${ printf '%(%s)T\n' -1;}
done
# REPLY 변종: 반환값을 암묵적으로 REPLY로 받음
${| printf -v REPLY "build-%s" "$VERSION";}
echo "$REPLY" # build-6.14
# 주의: 현재 셸에서 실행되므로 변수 변경이 부모 셸에 반영됨
count=0
result=${ count=$((count+1)); echo "done";}
echo "$count" # 1 (부모 셸 변수가 바뀜!)
${ ...;}은 서브셸을 만들지 않기 때문에 내부에서 exit, cd, 변수 할당, 함수 정의 등이
호출한 셸 환경에 그대로 반영됩니다. 성능이 중요한 짧은 루프에만 사용하고, 명령 실패 격리가 필요할 때는 여전히 $(...)을 사용하세요.
GLOBSORT 변수로 파일 정렬 제어
기존 Bash는 파일명 확장(Pathname Expansion) 결과를 항상 알파벳 순으로 반환했습니다.
Bash 5.3의 GLOBSORT 변수로 정렬 기준을 런타임에 바꿀 수 있어, 로그 회전이나 크기 기반 처리 스크립트에서
find | sort 호출을 줄일 수 있습니다.
# 기본: 이름 오름차순 (기존 동작과 동일)
ls *.log
# 수정 시각 최신순 (mtime 내림차순)
GLOBSORT="-mtime"
for log in *.log; do
echo "최근 수정: $log"
break # 가장 최근 로그 파일 하나만 처리
done
# 크기 큰 순으로 정렬 (디스크 청소 스크립트)
GLOBSORT="-size"
for f in /var/log/*.gz; do
[[ $(stat -c %s "$f") -gt $((100 * 1024 * 1024)) ]] && rm "$f"
done
# 정렬 키 전체 목록
# name | size | blocks | mtime | atime | ctime | numeric | none
# 접두사 "-"는 내림차순
빌트인 명령 개선
# 1. read -E: readline 기반 편집 + 기본 완성 사용
read -E -p "명령 입력: " cmd
# ↑ Tab 완성, 히스토리 검색(Ctrl-R), 라인 편집이 활성화됨
# 2. source -p PATH: 명시적 검색 경로 지정
source -p "/opt/kernel-scripts:/usr/local/share" common.sh
# ↑ PATH 환경 변수를 건드리지 않고 소스 파일 위치 지정
# 3. compgen -V VARNAME: 완성 결과를 stdout 대신 변수에 저장
compgen -V matches -W "build test clean install" "$cur"
echo "${matches[@]}"
# 4. BASH_MONOSECONDS: 시스템 부팅 이후 단조 증가 초
start=$BASH_MONOSECONDS
sleep 1
echo "경과: $((BASH_MONOSECONDS - start))초"
# ↑ NTP 조정에 영향받지 않음 (clock_gettime(CLOCK_MONOTONIC))
Readline RPROMPT 및 기타
READLINE_RPROMPT변수로 오른쪽 프롬프트(Right Prompt) 표시 가능 — zsh의RPROMPT와 유사- Readline에 대소문자 무시 검색(case-insensitive search), 이름 지정 명령 호출, 완성 후보 내보내기 커맨드 추가
[[ str =~ regex ]]에서 정규식 컴파일 실패 시 에러 메시지 출력 — 이전에는 조용히 false 반환#!셔뱅 검증 시 첫 두 줄을 검사하여 바이너리 오판정 감소- 소스 코드 C23 conformance 전환 — 커널 코드 스타일에서 C23으로의 이행과 같은 흐름
scripts/kernel-doc, scripts/bpf_doc.py)가 아직 Bash 5.2 이하 호환으로 작성되어 있으므로,
Bash 5.3 신기능을 사용할 때는 셔뱅에 #!/usr/bin/env bash와 함께
[[ ${BASH_VERSINFO[0]} -ge 5 && ${BASH_VERSINFO[1]} -ge 3 ]] 같은 버전 가드를 추가하는 것이 안전합니다.
POSIX 호환성과 셸 비교
셸 스크립트의 이식성(Portability)은 리눅스 배포판, 임베디드 시스템, CI 환경 등 다양한 실행 환경에서 동일하게 동작하기 위해 중요합니다. POSIX 문서에서 설명한 최신 공개 기준인 POSIX.1-2024(IEEE Std 1003.1-2024)의 셸 명령어 언어(Shell Command Language) 표준은 이식 가능한 스크립트의 기준을 제공합니다.
POSIX sh 표준
POSIX sh 표준은 셸이 반드시 지원해야 하는 기능의 최소 집합을 정의합니다.
이 표준을 따르는 스크립트는 Bash, dash, ash, mksh 등 모든 POSIX 호환 셸에서 동작합니다.
리눅스 커널 소스 트리의 scripts/ 디렉토리에 있는 많은 스크립트가
#!/bin/sh 셔뱅을 사용하며 POSIX 호환을 목표로 작성되어 있습니다.
POSIX sh에서 사용할 수 없는 Bash 전용 기능:
[[ ]]키워드 (대신[ ]사용)- 배열 (
array=()) &>,|&리디렉션 축약{1..10}중괄호 확장=~정규식 매칭$'...'ANSI-C 인용((...))산술 명령 (대신$(( ))산술 확장은 사용 가능)- 프로세스 치환
<(),>() local(대신 함수 내 변수는 전역, 일부 셸에서는 확장으로local지원)select반복문
다음 표는 대표적인 Bash 전용 구문과 POSIX 호환 대체 구문을 비교합니다.
커널 소스 트리의 scripts/에서는 대부분 POSIX 호환 스타일을 사용합니다.
| 기능 | Bash 전용 | POSIX 대체 |
|---|---|---|
| 조건 키워드 | [[ $var == pattern* ]] | case "$var" in pattern*) ... ;; esac |
| 배열 | arr=(a b c); echo ${arr[1]} | set -- a b c; eval echo \$$2 또는 반복 |
| 산술 명령 | (( count++ )) | count=$((count + 1)) |
| 소문자 변환 | ${var,,} | echo "$var" | tr '[:upper:]' '[:lower:]' |
| 프로세스 치환 | diff <(cmd1) <(cmd2) | 임시 파일: cmd1 > /tmp/a; cmd2 > /tmp/b; diff /tmp/a /tmp/b |
| 정규식 매칭 | [[ $s =~ regex ]] | echo "$s" | grep -qE 'regex' |
| 리디렉션 축약 | &> file | > file 2>&1 |
| 중괄호 확장 | cp file{,.bak} | cp file file.bak |
| 소스 명령 | source file | . file |
| 로컬 변수 | local var="val" | 변수명에 접두사 사용 또는 서브셸 격리(Isolation) |
셸 비교
다음 표는 주요 셸의 특성을 비교합니다. 커널 빌드 환경에서의 적합성, 임베디드 시스템에서의 용량, 대화형 사용 편의성 등을 기준으로 정리했습니다.
| 셸 | POSIX 호환 | 바이너리 크기 | 배열 | 정규식 | 주요 용도 |
|---|---|---|---|---|---|
| bash | 예 (확장 포함) | ~1.1MB | 인덱스+연관 | =~ | 범용 대화형/스크립팅 |
| dash | 예 (엄격) | ~120KB | 없음 | 없음 | 시스템 셸 (/bin/sh) |
| zsh | 호환 모드 | ~800KB | 고급 배열 | =~ | 대화형 사용, 플러그인 |
| ash (BusyBox) | 대부분 | ~60KB | 없음 | 없음 | 임베디드, initramfs |
| mksh | 예 | ~250KB | 인덱스 | 없음 | Android 시스템 셸 |
| ksh93 | 예 (확장) | ~1.5MB | 인덱스+연관 | =~ | 상용 유닉스, 레거시 |
이식성 가이드라인
커널 빌드 스크립트를 작성할 때는 #!/bin/sh를 기본으로 하되,
Bash 전용 기능이 반드시 필요한 경우에만 #!/bin/bash를 사용합니다.
checkbashisms 도구로 POSIX 비호환 구문을 자동으로 탐지할 수 있습니다.
# checkbashisms 설치 및 사용
sudo apt install devscripts # Debian/Ubuntu
checkbashisms my-script.sh
# 이식성 높은 대체 패턴
# Bash: [[ $var == pattern* ]]
# POSIX: case "$var" in pattern*) ... ;; esac
# Bash: local var="value"
# POSIX: var="value" (함수 내 전역, 주의 필요)
# Bash: echo "text" | read var
# POSIX: var=$(echo "text")
# Bash: (( count++ ))
# POSIX: count=$((count + 1))
# Bash: ${var,,} (소문자 변환)
# POSIX: echo "$var" | tr '[:upper:]' '[:lower:]'
# Bash: source file
# POSIX: . file (점 명령어)
빠른 레퍼런스
이 절은 Bash 개발 중 빠르게 참조할 수 있는 빌트인 명령, 셸 옵션, 특수 변수, 커널 개발 패턴 치트시트를 제공합니다.
빌트인 명령 레퍼런스
| 명령 | 설명 | 예시 |
|---|---|---|
cd | 작업 디렉토리 변경 | cd /usr/src/linux |
echo | 문자열 출력 | echo "빌드 시작" |
printf | 서식화 출력 | printf '%-20s %s\n' "$name" "$val" |
read | 표준 입력에서 읽기 | read -r -p "계속? [y/n] " ans |
export | 환경 변수 설정 | export ARCH=arm64 |
declare | 변수 속성 설정 | declare -A map |
local | 지역 변수 선언 | local result="$1" |
test / [ | 조건 평가 | [ -f .config ] && echo "있음" |
trap | 시그널 처리기 등록 | trap cleanup EXIT |
wait | 백그라운드 잡 대기 | wait $PID |
exec | FD 조작 또는 프로세스 교체 | exec 3>log.txt |
source / . | 파일을 현재 셸에서 실행 | source ./env.sh |
set | 셸 옵션 설정 | set -euo pipefail |
shopt | 셸 선택 옵션 설정 | shopt -s globstar |
getopts | 옵션 파싱 | while getopts "a:j:vh" opt; do |
hash | 명령 경로 캐시 관리 | hash -r (캐시 초기화) |
type | 명령 종류 확인 | type -a echo |
command | 빌트인 우회 또는 존재 확인 | command -v gcc |
enable | 빌트인 활성/비활성 | enable -a (목록 출력) |
mapfile | stdin을 배열로 읽기 | mapfile -t arr < file |
set과 shopt 옵션
| 옵션 | 명령 | 효과 |
|---|---|---|
-e (errexit) | set -e | 명령 실패 시 스크립트 즉시 종료 |
-u (nounset) | set -u | 미정의 변수 참조 시 에러 |
-o pipefail | set -o pipefail | 파이프라인 중 하나라도 실패하면 전체 실패 |
-x (xtrace) | set -x | 실행 명령을 stderr에 출력 (디버깅) |
-f (noglob) | set -f | 경로명 확장(글로빙) 비활성화 |
-n (noexec) | set -n | 명령 실행 없이 구문만 검사 |
nullglob | shopt -s nullglob | 매칭 없는 글로브 패턴을 빈 문자열로 |
globstar | shopt -s globstar | ** 패턴으로 재귀 디렉토리 매칭 |
lastpipe | shopt -s lastpipe | 파이프라인 마지막 명령을 현재 셸에서 실행 |
extglob | shopt -s extglob | 확장 글로빙: @(), *(), +(), ?(), !() |
dotglob | shopt -s dotglob | 글로빙에 숨김 파일(.) 포함 |
failglob | shopt -s failglob | 매칭 없는 글로브 패턴 시 에러 |
특수 변수 레퍼런스
| 변수 | 설명 |
|---|---|
$? | 마지막 명령의 종료 상태 |
$$ | 현재 셸의 PID |
$! | 마지막 백그라운드 프로세스의 PID |
$# | 위치 매개변수 개수 |
$@ | 모든 위치 매개변수 (개별 단어) |
$* | 모든 위치 매개변수 (IFS로 결합된 단일 문자열) |
$0 | 스크립트 이름 또는 셸 이름 |
$_ | 이전 명령의 마지막 인자 |
$- | 현재 셸 옵션 플래그 |
$BASHPID | 현재 프로세스 PID (서브셸에서도 정확) |
$BASH_VERSION | Bash 버전 문자열 |
$LINENO | 현재 줄 번호 |
$FUNCNAME | 현재 함수명 (배열, 호출 스택) |
$BASH_SOURCE | 소스 파일명 (배열, 호출 스택) |
$PIPESTATUS | 파이프라인 각 단계의 종료 상태 (배열) |
$BASH_REMATCH | =~ 매칭 결과 (배열) |
$RANDOM | 0~32767 난수 |
$SECONDS | 셸 시작 이후 경과 시간 (초) |
$EPOCHSECONDS | 유닉스 에포크 초 (Bash 5.0+) |
$IFS | 내부 필드 구분자 (기본: 공백·탭·개행) |
패턴 치트시트
커널 개발에서 자주 사용하는 Bash 원라이너(One-liner)와 패턴을 정리합니다.
# 1. 커널 설정에서 활성 옵션 수 세기
grep -c '^CONFIG_.*=y' .config
# 2. 모듈 설정 옵션만 추출
grep '^CONFIG_.*=m' .config | cut -d= -f1
# 3. 가장 큰 오브젝트 파일 Top 10
find . -name "*.o" -exec ls -la {} + | sort -k5 -rn | head -10
# 4. 커널 로그에서 에러 타임라인 추출
dmesg -T | grep -iE "(error|panic|oops|bug)"
# 5. 로드된 모듈의 총 메모리 사용량
awk '{sum+=$2} END {printf "총 모듈 메모리: %d KB\n", sum}' /proc/modules
# 6. CPU별 인터럽트 카운트
awk 'NR>1 {printf "%-20s %s\n", $NF, $2}' /proc/interrupts | sort -k2 -rn | head -15
# 7. git 로그에서 커밋별 변경 파일 수
git log --oneline --shortstat -20 | paste - -
# 8. 커널 소스에서 특정 함수 호출 빈도
grep -rn "mutex_lock" kernel/ drivers/ | wc -l
# 9. 심볼 테이블에서 가장 큰 함수
nm --size-sort -r vmlinux | head -20
# 10. 빌드 시간 측정
time make -j$(nproc) 2>&1 | tail -1
# 11. .config 디핑: 두 설정 차이점만
diff <(grep '^CONFIG_' old.config | sort) <(grep '^CONFIG_' new.config | sort)
# 12. /proc에서 프로세스별 메모리 사용량 Top 10
ps aux --sort=-%mem | head -11
# 13. 네트워크 인터페이스 상태 한눈에 보기
for i in /sys/class/net/*/operstate; do echo "$(dirname "$i" | xargs basename): $(cat "$i")"; done
# 14. 커널 빌드 경고 카운트
grep -c "warning:" build.log
# 15. 헤더 파일 의존성 그래프 (간이)
grep -rh '^#include' include/linux/mm.h | sort -u
참고자료
공식 문서
- Bash Reference Manual (gnu.org) — GNU Bash 공식 레퍼런스 매뉴얼 전체
- Bash Manual — Node Index (gnu.org) — 항목별 색인으로 빠른 참조
- Shell Builtin Commands (gnu.org) — 빌트인 명령 공식 설명
- POSIX Shell Command Language (opengroup.org) — POSIX 셸 명세 표준 문서
- POSIX Utilities (opengroup.org) — POSIX 유틸리티 전체 목록
학습 가이드
- Advanced Bash-Scripting Guide (tldp.org) — Bash 스크립팅 심화 학습의 고전적 참고서
- Bash Guide for Beginners (tldp.org) — 입문자를 위한 단계별 가이드
- Bash Programming Introduction HOWTO (tldp.org) — Bash 프로그래밍 입문 HOWTO
- Shell Style Guide (google.github.io) — Google 셸 스크립트 코딩 스타일(Coding Style) 가이드
위키 · FAQ · 베스트 프랙티스
- Bash Pitfalls (mywiki.wooledge.org) — 흔한 Bash 실수 패턴과 해결법 모음
- BashFAQ (mywiki.wooledge.org) — Bash 관련 자주 묻는 질문과 답변
- BashGuide (mywiki.wooledge.org) — 초보부터 중급까지 체계적 Bash 가이드
- Bash Hackers Wiki (bash-hackers.org) — Bash 내부 동작, 문법, 확장 기능 위키
- ShellCheck (shellcheck.net) — 셸 스크립트 정적 분석·린트 도구 (웹 버전)
- ShellCheck GitHub (github.com) — ShellCheck 소스 코드와 규칙 문서
맨 페이지 · 레퍼런스
- bash(1) man page (man7.org) — Bash 맨 페이지 전체
- test(1) man page (man7.org) — 조건 평가 명령 레퍼런스
- signal(7) man page (man7.org) — 시그널 목록과 동작 (trap 연계)
- glob(7) man page (man7.org) — 파일명 글로빙 패턴 문법
- regex(7) man page (man7.org) — 정규 표현식 문법 레퍼런스
- GNU Coreutils Manual (gnu.org) — Bash와 함께 사용하는 핵심 유틸리티 공식 매뉴얼
- GNU Grep Manual (gnu.org) — grep 공식 매뉴얼 (셸 스크립트 필수 도구)
- GNU Sed Manual (gnu.org) — sed 스트림 편집기 공식 매뉴얼
- GNU Awk Manual (gnu.org) — awk 텍스트 처리 공식 매뉴얼
관련 문서
| 문서 | 관련 내용 |
|---|---|
| 개발 도구 | GCC, Make, GDB 등 커널 개발 도구 개요 |
| 빌드 시스템 | Kbuild, Kconfig, make 인자와 환경변수 |
| GNU Make | Makefile 문법, 패턴 규칙, 자동 변수 상세 |
| C 언어 | 커널 C 코딩 스타일과 GCC 확장 |
| BusyBox | BusyBox ash 셸, initramfs 환경 |
| 프로세스 관리 | fork, exec, 스케줄링, 프로세스 그룹 |
| 커널 디버깅 | ftrace, perf, QEMU+GDB 디버깅 |
| 커널 모듈 | 모듈 빌드, 로드, 매개변수 |
| 임베디드 빌드 시스템 | Buildroot, OpenWrt의 셸 스크립트 활용 |
| 개발 환경 설정 | QEMU, 크로스 컴파일(Cross Compilation) 환경 구축 |