Bash 셸 스크립팅 완전 가이드
Bash(Bourne-Again SHell)는 리눅스에서 가장 널리 사용되는 명령행 인터프리터이자 스크립팅 언어입니다.
이 문서는 Bash의 내부 구조(렉서/파서/실행기), 변수와 확장, 제어 흐름, 함수, I/O 리디렉션(Redirection),
프로세스 모델과 잡 제어(Job Control), 시그널(Signal) 처리, 프로세스 간 통신(IPC),
/proc·/sys 인터페이스 스크립팅, 커널 빌드 자동화,
그리고 POSIX 호환성과 셸 비교까지 커널 개발자 관점에서 상세히 다룹니다.
핵심 요약
- Bourne-Again SHell — 1989년 Brian Fox가 FSF를 위해 작성한 자유 소프트웨어 셸로, Bourne Shell(
sh)의 상위 호환(Superset)입니다. - 커널 빌드의 접착제 — 리눅스 커널 소스 트리의
scripts/디렉토리에는 수백 개의 셸 스크립트가 있으며,Kconfig,Kbuild, 모듈 설치 등 빌드 과정 전반에 Bash가 관여합니다. - 7단계 확장 — Bash는 명령을 실행하기 전에 중괄호 확장, 틸드 확장, 매개변수 확장, 산술 확장, 명령 치환, 단어 분리, 경로명 확장의 7단계를 순서대로 수행합니다.
- 프로세스 모델 — 외부 명령 실행 시
fork()+exec()조합을 사용하며, 파이프라인의 각 단계는 별도 프로세스로 실행됩니다. - 잡 제어 — 세션(Session), 프로세스 그룹(Process Group), 포그라운드/백그라운드(Foreground/Background) 개념을 통해 다중 작업을 관리합니다.
- POSIX 호환 —
--posix플래그나/bin/sh로 호출 시 POSIX.1-2017 표준 모드로 동작하며, 배열·정규식 매칭 등 Bash 전용 확장은 비활성화됩니다.
핵심 용어 정리
| 용어 | 설명 |
|---|---|
| 셸(Shell) | 사용자와 커널 사이의 명령행 인터페이스. 명령을 해석하고 실행하는 프로그램 |
| 빌트인(Builtin) | cd, echo, export 등 셸 내부에 구현된 명령. fork() 없이 현재 프로세스에서 직접 실행 |
| 서브셸(Subshell) | 현재 셸의 자식 프로세스로 생성된 셸 환경. ( ), 파이프라인, 명령 치환 등에서 생성됨 |
| 확장(Expansion) | Bash가 명령 실행 전에 변수, 와일드카드, 산술식 등을 실제 값으로 치환하는 과정 |
| 리디렉션(Redirection) | 표준 입출력(stdin/stdout/stderr)의 대상을 파일, 파이프, 소켓 등으로 변경하는 기능 |
| 파이프라인(Pipeline) | cmd1 | cmd2 형태로 한 명령의 출력을 다음 명령의 입력으로 연결하는 구조 |
| 잡 제어(Job Control) | 셸이 프로세스 그룹 단위로 포그라운드/백그라운드 작업을 관리하는 기능 |
| 트랩(Trap) | 시그널 수신 시 실행할 명령을 등록하는 Bash 빌트인. 정리(Cleanup) 작업에 필수적 |
단계별 이해
Bash 스크립팅을 처음 접하는 커널 개발자를 위한 8단계 가이드입니다:
1단계: 첫 번째 스크립트
#!/bin/bash
# 셔뱅(Shebang) 라인: 이 스크립트를 실행할 인터프리터 지정
echo "Hello, Kernel Developer!"
echo "현재 커널 버전: $(uname -r)"
2단계: 변수와 매개변수
#!/bin/bash
KERNEL_SRC="/usr/src/linux"
ARCH="x86_64"
JOBS="$(nproc)"
echo "커널 소스: ${KERNEL_SRC}"
echo "아키텍처: ${ARCH}"
echo "병렬 빌드 수: ${JOBS}"
# 기본값 설정 (변수가 비어있으면 기본값 사용)
CROSS_COMPILE="${CROSS_COMPILE:-}"
INSTALL_PATH="${INSTALL_PATH:-/boot}"
3단계: 조건문으로 환경 검사
#!/bin/bash
# 커널 소스 디렉토리 존재 확인
if [[ ! -d "/usr/src/linux" ]]; then
echo "오류: 커널 소스 디렉토리가 없습니다" >&2
exit 1
fi
# 필수 도구 확인
for tool in gcc make flex bison; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "오류: ${tool}이(가) 설치되지 않았습니다" >&2
exit 1
fi
done
echo "모든 필수 도구가 설치되어 있습니다."
4단계: 함수로 구조화
#!/bin/bash
die() {
echo "오류: $*" >&2
exit 1
}
warn() {
echo "경고: $*" >&2
}
check_root() {
[[ "$(id -u)" -eq 0 ]] || die "root 권한이 필요합니다"
}
build_kernel() {
local config="$1"
make mrproper
cp "${config}" .config
make olddefconfig
make -j"$(nproc)" || die "커널 빌드 실패"
}
5단계: 파이프와 텍스트 처리
# 커널 설정에서 활성화된 옵션 수 세기
grep -c "^CONFIG_.*=y" .config
# 로드된 모듈을 메모리 사용량 순으로 정렬
lsmod | sort -k 2 -n -r | head -10
# /proc/cpuinfo에서 CPU 모델명 추출
grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2
6단계: 시그널 트랩으로 정리 작업
#!/bin/bash
TMPDIR=""
cleanup() {
[[ -n "$TMPDIR" ]] && rm -rf "$TMPDIR"
echo "임시 파일 정리 완료"
}
trap cleanup EXIT
TMPDIR="$(mktemp -d)"
echo "작업 디렉토리: ${TMPDIR}"
# 스크립트가 정상 종료하든 오류로 종료하든 cleanup() 호출 보장
7단계: /proc과 /sys 활용
# CPU 거버너 확인 및 변경
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# 투명 대규모 페이지(THP) 상태 확인
cat /sys/kernel/mm/transparent_hugepage/enabled
# 네트워크 튜닝
sysctl net.core.somaxconn
sudo sysctl -w net.core.somaxconn=4096
8단계: 커널 빌드 자동화 스크립트
#!/bin/bash
set -euo pipefail
KERNEL_VER="$(make -s kernelversion)"
LOG="build-${KERNEL_VER}-$(date +%Y%m%d%H%M).log"
echo "커널 ${KERNEL_VER} 빌드 시작..."
make -j"$(nproc)" 2>&1 | tee "${LOG}"
make modules_install INSTALL_MOD_PATH="./rootfs"
echo "빌드 완료. 로그: ${LOG}"
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가 주 관리자를 맡아 현재(5.2 버전, 2022년)까지 지속적으로 발전하고 있습니다.
유닉스 셸의 역사는 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), 파이프라인(Pipeline),
리스트(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 Table)에 이전 탐색 결과를 캐싱합니다.
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 언어와 달리 Bash 변수에는 타입(Type)이 없으며 기본적으로 모든 값이 문자열로 저장됩니다.
declare 빌트인을 사용하면 정수(-i), 읽기 전용(-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 | 현재 줄 번호 | 디버깅: 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]'로 요소를 삭제해도 인덱스가 재정렬되지 않고 빈 슬롯이 남습니다.
연관 배열은 해시 테이블 기반이므로 키 검색은 O(1)이지만, 수천 개 이상의 요소에서는
외부 도구(awk, sqlite3)가 더 효율적입니다.
대용량 데이터를 배열에 로드해야 한다면 mapfile을 사용하면
while read 루프보다 10배 이상 빠릅니다.
제어 흐름
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="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
함수
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(히어 문서)는 셸 스크립트 내에서 여러 줄의 텍스트를 명령의 표준 입력으로 전달하는 리디렉션 형태입니다. 커널 빌드 스크립트에서 설정 파일 생성, 패치 적용, 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 재배치,
(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 ... |
&& / || 체인 내부 | 논리 연산자 좌변 실패는 흐름 제어 | 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)은 리눅스 커널이 프로세스에게 비동기적 이벤트를 알리는 메커니즘입니다.
Bash는 trap 빌트인을 통해 시그널 핸들러를 등록할 수 있으며,
이는 스크립트의 안정적인 실행을 위해 필수적인 기능입니다.
임시 파일 정리, 락 파일 해제, 하위 프로세스 종료 등의 정리(Cleanup) 작업을
시그널 발생 시에도 보장하려면 적절한 트랩 설정이 필요합니다.
trap 명령
trap은 셸 스크립트에서 시그널 핸들러를 등록하는 빌트인 명령입니다.
임시 파일 정리, 락 해제, 하위 프로세스 종료 등의 정리 작업을 보장하기 위해 사용합니다.
Bash는 실제 시그널뿐 아니라 EXIT(스크립트 종료),
ERR(명령 실패), DEBUG(매 명령 실행 전),
RETURN(함수/소스 반환) 같은 의사 시그널(Pseudo-Signal)도 지원합니다.
| 시그널 | 번호 | 기본 동작 | Bash trap 용도 |
|---|---|---|---|
| SIGHUP | 1 | 종료 | 설정 재로드, 데몬 재시작 관례 |
| SIGINT | 2 | 종료 | Ctrl+C 처리, 사용자 중단 시 정리 |
| SIGQUIT | 3 | 코어 덤프 | 디버깅용, 일반적으로 trap하지 않음 |
| SIGTERM | 15 | 종료 | kill 기본 시그널, 정상 종료 정리 |
| SIGCHLD | 17 | 무시 | 자식 프로세스 종료 감지 |
| SIGUSR1/2 | 10/12 | 종료 | 스크립트 간 커스텀 통신 |
| SIGPIPE | 13 | 종료 | 파이프 끊김 처리 (쓰기 측) |
| EXIT | 의사 | N/A | 스크립트 종료 시 항상 실행 (가장 중요) |
| ERR | 의사 | N/A | 명령 실패 시 실행 (set -e와 연동) |
| DEBUG | 의사 | N/A | 매 명령 실행 전 실행 (프로파일링) |
| 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() 시스템 호출이 사용되며,
기본 버퍼 크기는 64KB(16페이지, /proc/sys/fs/pipe-max-size)입니다.
버퍼가 가득 차면 쓰기 측이 블록(Block)되고, 비어있으면 읽기 측이 블록됩니다.
이 특성 때문에 dmesg | grep처럼 대량 출력을 필터링할 때 메모리 사용이 일정합니다.
명명된 파이프(Named Pipe, FIFO)는 mkfifo로 파일시스템에 이름을 가진 특수 파일을 생성하므로
관계없는 프로세스 간 통신에 사용할 수 있습니다.
FIFO는 열기(open) 시 읽기/쓰기 양쪽이 모두 연결될 때까지 블록되는 점에 유의해야 합니다.
# 익명 파이프: 파이프 버퍼 크기 확인
echo "파이프 버퍼 크기:"
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 패턴을 조합하여 사용합니다.
파일 락(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
for ((i=0; i<NUM_WORKERS; i++)); do
(
while read -r task; do
echo "워커 $i: $task 처리 중"
eval "$task"
done < "${WORK_DIR}/tasks"
) &
done
/proc과 /sys 인터페이스 활용
리눅스 커널은 /proc(procfs)과 /sys(sysfs) 가상 파일시스템을 통해
커널 내부 상태를 사용자 공간에 노출합니다. 이 인터페이스들은 일반 파일처럼 읽고 쓸 수 있으므로
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)는 디바이스 모델, 드라이버, 버스 정보를 계층적으로 노출하는 가상 파일시스템입니다.
/sys/class/는 디바이스를 기능별로 분류하고,
/sys/devices/는 물리적 토폴로지를 반영하며,
/sys/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를 사용하여 커널 매개변수를 모니터링하고 튜닝하는 실전 스크립트 패턴입니다. 서버 성능 최적화, 네트워크 튜닝, 메모리 관리 등에 활용할 수 있습니다.
#!/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)
모듈 개발 스크립트
커널 모듈(Out-of-tree Module) 개발 시 빌드, 설치, 테스트, 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 로그에서 시간 차이를 계산하여 병목 식별
# 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 스크립트의 성능 병목은 대부분 불필요한 서브셸/외부 프로세스 생성에서 발생합니다.
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"
POSIX 호환성과 셸 비교
셸 스크립트의 이식성(Portability)은 리눅스 배포판, 임베디드 시스템, CI 환경 등 다양한 실행 환경에서 동일하게 동작하기 위해 중요합니다. POSIX.1-2017(IEEE Std 1003.1-2017)의 셸 명령어 언어(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" | 변수명에 접두사 사용 또는 서브셸 격리 |
셸 비교
다음 표는 주요 셸의 특성을 비교합니다. 커널 빌드 환경에서의 적합성, 임베디드 시스템에서의 용량, 대화형 사용 편의성 등을 기준으로 정리했습니다.
| 셸 | 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 셸 스크립트 코딩 스타일 가이드
위키 · 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, 크로스 컴파일 환경 구축 |