system

[시스템 프로그래밍] Tiny Shell 프로젝트: 잡 컨트롤, 시그널, 레이스 컨디션 다루기

숨usm 2025. 9. 9. 23:08

요약

잡 컨트롤 안정화 + 백그라운드 완료 알림 + 인용/주석 토크나이저 개선 + 레이스/메모리 에러 내성

프롬프트에 최근 종료 상태 표시, 변수 확장($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에서 직접 프린트하지 않고 플래그로 지연 출력하여 해결

마무리

이번 커밋은 사용감(알림/프롬프트)과 안정성(레이스/시그널) 쪽에 집중했습니다.


다음 포스팅에서는 명령어 결합(&&, ||)과 글로빙(*, ?) 기능을 추가할 예정입니다!