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);
// 이어서 이 부분부터 정리
'system' 카테고리의 다른 글
[Linux Signal] - 쉘, 프로세스 제어, 시그널 핸들링 (0) | 2025.09.14 |
---|---|
[시스템 프로그래밍] myshell.c 코드 분석 - 2 (1) | 2025.09.12 |
[시스템 프로그래밍] Tiny Shell 프로젝트: 잡 컨트롤, 시그널, 레이스 컨디션 다루기 (0) | 2025.09.09 |
[시스템 프로그래밍] execve() 시스템 콜 C vs 어셈블리 비교 분석 (0) | 2025.09.08 |
[시스템 프로그래밍] execve() 시스템 콜로 이해하는 프로세스 교체 원리 (0) | 2025.09.08 |