요약
잡 컨트롤 안정화 + 백그라운드 완료 알림 + 인용/주석 토크나이저 개선 + 레이스/메모리 에러 내성
프롬프트에 최근 종료 상태 표시, 변수 확장($VAR, $?), 파이프/리다이렉션 정상 동작 삽질기 요약해보았습니다(*´∀`)
전체 코드는 제 깃허브에 올려두었습니다! -> GitHub 레포지토리
suminworld-system-lab/shell at main · sumin-world/suminworld-system-lab
System programming & networking lab (C, Linux, OSTEP practice) - sumin-world/suminworld-system-lab
github.com
🛠️ 빌드 & 실행
gcc -Wall -Wextra -O2 -o myshell myshell.c
./myshell
데모 스크립트
# 파이프/리다이렉션
echo hello | tr a-z A-Z
pwd > x.txt && cat x.txt
# 변수 확장 + 종료코드 추적
false
echo "$?" # 1 출력
echo "HOME=$HOME"
# 따옴표/주석
echo "hash # stays" # 따옴표 안의 #는 문자열
echo outside // comment // <- 여기서부터 라인 끝까지 주석
# comment-only line
# 백그라운드 + 완료 알림
sleep 1 & # 바로 프롬프트 반환
# (다음 프롬프트 전) [n] Done sleep 1 ...
# 잡 컨트롤
sleep 100
# Ctrl+Z -> Stopped
jobs
bg %1
fg %1
🔧 오늘 핵심 변경점 (기능/안정성)
1️⃣ 백그라운드 완료 알림
Job { state, pgid, cmdline, notified } + pending_notifications 플래그
SIGCHLD에서 완료 표시만 설정 → 프롬프트 직전 check_background_notifications()가 안전하게 출력
static volatile sig_atomic_t pending_notifications = 0;
static void sigchld_handler(int sig) {
(void)sig; int saved = errno;
while (1) {
int status; pid_t pid = waitpid(-1, &status, WNOHANG|WUNTRACED|WCONTINUED);
if (pid <= 0) break;
pid_t pg = pidmap_get_pgid(pid);
int jid = (pg>0) ? find_job_by_pgid(pg) : -1;
if (jid > 0 && (WIFEXITED(status)||WIFSIGNALED(status))) {
jobs[jid].state = JOB_DONE;
pending_notifications = 1;
}
if (WIFEXITED(status)||WIFSIGNALED(status)) pidmap_clear_pid(pid);
}
errno = saved;
}
static void check_background_notifications(void) {
if (!pending_notifications) return;
pending_notifications = 0;
for (int i=1;i<MAX_JOBS;i++) if (jobs[i].state==JOB_DONE && !jobs[i].notified) {
printf("\n[%d] Done %s\n", i, jobs[i].cmdline);
jobs[i].state = JOB_UNUSED; jobs[i].notified = 1;
}
}
2️⃣ 레이스 안전성: safe_setpgid()
setpgid()가 ESRCH/EPERM 레이스로 실패하는 케이스 재시도
static void safe_setpgid(pid_t pid, pid_t pgid) {
int retries = 3;
while (retries-- > 0) {
if (setpgid(pid, pgid) == 0) return;
if (errno != ESRCH && errno != EPERM) break;
usleep(1000);
}
}
3️⃣ SIGCHLD에서 getpgid() 대체: pid→pgid 맵
시그널 핸들러에서 안전하게 잡 식별을 위해 간단한 배열 맵 사용
typedef struct { pid_t pid; pid_t pgid; } PidMapEntry;
static volatile PidMapEntry pidmap[4096];
static volatile int pidmap_count;
static inline void pidmap_add(pid_t pid, pid_t pgid) {
int i=pidmap_count;
if (i<4096) {
pidmap[i]=(PidMapEntry){pid,pgid};
pidmap_count=i+1;
}
}
static inline pid_t pidmap_get_pgid(pid_t pid) {
for(int i=0;i<pidmap_count;i++)
if(pidmap[i].pid==pid) return pidmap[i].pgid;
return -1;
}
static inline void pidmap_clear_pid(pid_t pid) {
for(int i=0;i<pidmap_count;i++)
if(pidmap[i].pid==pid) {
pidmap[i].pid=0; pidmap[i].pgid=0;
return;
}
}
4️⃣ 토크나이저: 인용/이스케이프/주석 처리
싱글/더블쿼트, \ 이스케이프, 토큰 경계(| < > >> &)
주석: 따옴표 밖의 # 또는 // 이후는 라인 끝까지 무시 변수 확장: $VAR, $? (싱글쿼트 내부 제외) 불일치 따옴표: 경고 후 리터럴 처리로 복구
// 라인 시작 주석
if (*p == '#') { while (*p && *p != '\n') p++; continue; }
if (p[0]=='/' && p[1]=='/') { while (*p && *p != '\n') p++; continue; }
// 토큰 스캔 중 주석
if (!in_single && !in_double && *p == '#') {
while (*p && *p != '\n') p++; break;
}
if (!in_single && !in_double && p[0]=='/' && p[1]=='/') {
p+=2; while (*p && *p != '\n') p++; break;
}
// 인용/이스케이프
if (*p == '\\') { p++; if (*p && bi<sizeof(buf)-1) buf[bi++] = *p++; continue; }
if (*p == '\''){ if (!in_double){ in_single=!in_single; p++; continue; } }
if (*p == '"') { if (!in_single){ in_double=!in_double; p++; continue; } }
5️⃣ 파이프라인 & 빌트인 믹스 실행
빌트인 단독(포그라운드, 리다이렉션/파이프 없음) → 프로세스 생성 없이 수행
그 외(파이프/리다이렉션/백그라운드/여러 스테이지) → fork + setpgid + execvp
if (pl->ncmds==1 && is_builtin(&pl->cmds[0]) && !pl->background &&
!pl->cmds[0].in_file && !pl->cmds[0].out_file) {
last_status = run_builtin(&pl->cmds[0]);
return last_status;
}
// 파이프 구성 & 프로세스 그룹
pid_t pgid=0;
for (int i=0;i<pl->ncmds;i++){
pid_t pid=fork();
if (pid==0){ /* child */
setpgid(0, pgid? pgid:0);
... execvp(...)
} else {
if (!pgid) pgid=pid;
safe_setpgid(pid, pgid);
pidmap_add(pid, pgid);
...
}
}
6️⃣ 기타 안정성
- SIGPIPE 무시: 파이프 후단 종료로 앞단 죽지 않도록
- strdup() 실패 체크 → 에러 출력 후 안전 복구
- 프롬프트에 최근 종료 상태($?) 표기
- cd/pwd/exit/jobs/fg/bg 빌트인
⚠️ 제한 사항 (다음 목표)
- 명령결합 && / || / ;, 서브셸/커맨드치환($(...), `...`) 미지원
- 글로빙(* ? []) 미지원
- here-doc (<<) 미지원
→ 다음 단계에서 파서 확장 예정
트러블슈팅 메모
- 이상한 컴파일 에러(따옴표/널 문자)는 대부분 복붙 시 특수문자 문제 → CRLF/스마트따옴표/NUL 제거 후 재빌드
- 백그라운드 잡이 "가끔" 안나오던 문제는 SIGCHLD에서 직접 프린트하지 않고 플래그로 지연 출력하여 해결
마무리
이번 커밋은 사용감(알림/프롬프트)과 안정성(레이스/시그널) 쪽에 집중했습니다.
다음 포스팅에서는 명령어 결합(&&, ||)과 글로빙(*, ?) 기능을 추가할 예정입니다!
'system' 카테고리의 다른 글
[시스템 프로그래밍] myshell.c 코드 분석 - 2 (1) | 2025.09.12 |
---|---|
[시스템 프로그래밍] myshell.c 코드 분석 - 1 (1) | 2025.09.11 |
[시스템 프로그래밍] execve() 시스템 콜 C vs 어셈블리 비교 분석 (0) | 2025.09.08 |
[시스템 프로그래밍] execve() 시스템 콜로 이해하는 프로세스 교체 원리 (0) | 2025.09.08 |
[시스템 프로그래밍] fork() 시스템 콜로 이해하는 프로세스 복제 원리 (0) | 2025.09.08 |