system

[시스템 프로그래밍] myshell.c 코드 분석 - 1

숨usm 2025. 9. 11. 04:11
 

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

전체 코드는 위의 링크 들어가시면 확인 가능합니다!

1. 파일 디스크립터 (File Descriptor)

개념

파일을 가리키는 번호표

기본 할당

  • 0번 (STDIN_FILENO): 표준 입력 (키보드)
  • 1번 (STDOUT_FILENO): 표준 출력 (화면)
  • 2번 (STDERR_FILENO): 표준 에러 (에러 메시지용 화면)
  • 3번, 4번, 5번...: 프로그램이 열어서 사용하는 파일들

터미널 제어 함수

#include <termios.h>

int tcgetattr(int fd, struct termios *termios_p);  // 터미널 설정 가져오기
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);  // 터미널 설정 적용

// 예시
struct termios saved_settings;
tcgetattr(shell_terminal, &saved_settings);  // 현재 설정 백업
tcsetattr(shell_terminal, TCSANOW, &saved_settings);  // 나중에 복구

2. 프로세스와 프로세스 그룹

기본 개념

  • PID (Process ID): 개별 프로세스의 고유 번호
  • PGID (Process Group ID): 관련된 프로세스들을 묶은 그룹 번호
  • 파이프라인 예시: ls | grep txt | wc -l → 3개 프로세스가 하나의 그룹

관련 함수

pid_t getpid(void);                    // 현재 프로세스 ID 가져오기
int setpgid(pid_t pid, pid_t pgid);    // 프로세스를 특정 그룹에 할당
int tcsetpgrp(int fd, pid_t pgrp);     // 터미널 제어권을 특정 그룹에 할당

쉘의 프로세스 그룹 설정

shell_pgid = getpid();           // 쉘 자신의 PID를 가져옴
setpgid(shell_pgid, shell_pgid); // 쉘을 자기 자신의 그룹 리더로 설정

3. 프로그램 종료 코드

의미

  • 0: 성공 (Everything is OK)
  • 0이 아닌 숫자: 실패/에러 (종류에 따라 다른 값)

실제 예시

$ ls /home      # 성공
$ echo $?       # 0 출력

$ ls /nonexistent   # 실패
$ echo $?          # 2 출력 (파일 없음 에러)

쉘 종료 방식

// 1. exit 명령어로 종료
static int builtin_exit(Command *c) {
    int code = (c->argc >= 2) ? atoi(c->argv[1]) : last_status;
    exit(code);
}

// 2. Ctrl+D (EOF)로 종료
if (!fgets(line, sizeof(line), stdin)) { 
    putchar('\n'); 
    break; 
}

4. 시그널 (Signal) 처리

시그널이란?

운영체제가 프로세스에게 보내는 인터럽트/알림 메시지

주요 시그널들

  • SIGINT: Ctrl+C (완전 종료)
  • SIGTSTP: Ctrl+Z (일시정지 후 백그라운드)
  • SIGCHLD: 자식 프로세스 상태 변경 알림
  • SIGTTOU/SIGTTIN: 백그라운드 프로세스의 터미널 접근 시도
  • SIGPIPE: 파이프 연결 끊어짐

시그널 처리 방법

// 1. 간단한 방법
signal(SIGPIPE, SIG_IGN);  // SIGPIPE 무시

// 2. 세밀한 제어 (sigaction)
struct sigaction sa_int = {0};
sa_int.sa_handler = sigint_handler;
sigemptyset(&sa_int.sa_mask);
sa_int.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa_int, NULL);

핸들러 함수 예시

static void sigint_handler(int sig) {
    (void)sig;  // 컴파일러 경고 방지
    if (fg_pgid > 0) kill(-fg_pgid, SIGINT);  // 포그라운드 그룹에 전달
}

5. Foreground vs Background

개념

  • Foreground: 현재 사용자와 상호작용하는 프로그램 (키보드 입력 받는 프로그램)
  • Background: 뒤에서 조용히 실행되는 프로그램

예시

myshell$ ls           # ls가 foreground에서 실행
myshell$ sleep 100 &  # sleep이 background에서 실행 (&때문에)

쉘의 처리 방식

  • fg_pgid = 0: 포그라운드에 실행 중인 프로그램 없음 (쉘만 있음)
  • fg_pgid > 0: 포그라운드에서 실행 중인 프로그램의 그룹 ID

6. kill() 함수의 특별한 규칙

사용법

int kill(pid_t pid, int sig);

PID 값에 따른 동작

  • 양수: 해당 PID 프로세스 하나에게만 시그널 전송
  • 음수: 해당 절댓값을 PGID로 하는 프로세스 그룹 전체에 시그널 전송

예시

kill(1234, SIGINT);   // PID 1234 프로세스에만
kill(-1234, SIGINT);  // PGID 1234 그룹 전체에

파이프라인에서의 활용

ls | grep txt | wc -l  # 3개 프로세스가 같은 그룹
# Ctrl+C 누르면 → kill(-pgid, SIGINT) → 3개 모두 종료

7. 작업(Job) 관리

작업 상태

typedef enum { 
    JOB_UNUSED=0, 
    JOB_RUNNING,    // 실행 중
    JOB_STOPPED,    // 일시정지 (Ctrl+Z)
    JOB_DONE        // 완료
} job_state_t;

SIGCHLD 처리

static void sigchld_handler(int sig) {
    while (1) {
        pid_t pid = waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED);
        if (pid <= 0) break;
        
        // 상태에 따라 jobs[] 테이블 업데이트
        if (WIFSTOPPED(status)) {
            jobs[jid].state = JOB_STOPPED;
        } else if (WIFCONTINUED(status)) {
            jobs[jid].state = JOB_RUNNING;
        } else if (WIFEXITED(status) || WIFSIGNALED(status)) {
            jobs[jid].state = JOB_DONE;
        }
    }
}

8. init_shell() 함수 전체 흐름

static void init_shell(void) {
    // 1. 터미널 파일 디스크립터 저장
    shell_terminal = STDIN_FILENO;
    
    // 2. 프로세스 그룹 설정
    shell_pgid = getpid();
    setpgid(shell_pgid, shell_pgid);  // 자신을 그룹 리더로
    
    // 3. 터미널 제어권 가져오기 (키보드 모드일 때만)
    if (isatty(shell_terminal)) {
        tcsetpgrp(shell_terminal, shell_pgid);  // 제어권 획득
        tcgetattr(shell_terminal, &shell_tmodes);  // 현재 설정 백업
    }
    
    // 4. 불필요한 시그널들 무시
    signal(SIGTTOU, SIG_IGN);
    signal(SIGTTIN, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    
    // 5. 중요한 시그널들 핸들러 등록
    // SIGINT (Ctrl+C), SIGTSTP (Ctrl+Z), SIGCHLD (자식 상태 변경)
}

9. 핵심 포인트

쉘은 시그널을 받아서 적절한 프로세스에게 전달하는 역할

  • Ctrl+C, Ctrl+Z를 눌러도 쉘 자체는 죽지 않음
  • 현재 foreground에서 실행 중인 프로그램만 영향받음

그룹 단위 제어

  • 파이프라인의 모든 프로세스를 하나의 그룹으로 관리
  • 사용자 입장에서는 "하나의 작업"이므로 그룹 전체를 제어

비동기적 상태 관리

  • SIGCHLD 핸들러로 백그라운드 작업 상태를 실시간 추적
  • 작업 완료 시 사용자에게 알림

-- 2025.09.11.(목) 04:07

sa_tstp.sa_handler = sigtstp_handler;
sigemptyset(&sa_tstp.sa_mask);
sa_tstp.sa_flags = SA_RESTART;
sigaction(SIGTSTP, &sa_tstp, NULL);

sa_chld.sa_handler = sigchld_handler;
sigemptyset(&sa_chld.sa_mask);
sa_chld.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &sa_chld, NULL);

// 이어서 이 부분부터 정리