728x90
이 글은 OSTEP(Operating Systems: Three Easy Pieces)의 ‘Interlude: Process API’ 문서를 직접 읽고 정리한 내용이다. Opinion(의견) 은 명시적으로 표시하고, 나머지는 설명 위주로 구성했다.
DevOps 관점(왜 중요한가/적용 방식)은 글 맨 마지막 별도 섹션에 모았다.
1) 프로세스 API가 다루는 것
- 프로그램을 프로세스로 실행·중단·종료하고, 자식 프로세스를 만들고 관리하는 시스템 콜 집합.
- 핵심 호출군:
fork(),exec*(),wait()/waitpid(),exit(),getpid()/getppid(),sleep(),kill(),signal()등. - 표준 입출력/파일 서술자와 결합해 리다이렉션·파이프라인까지 구성.
2) 프로세스 기본 수명주기
1) 생성 — fork()로 부모의 주소공간/레지스터/파일서술자를 복제(Copy-on-Write).
2) 프로그램 교체(선택) — 자식에서 exec*()로 현재 프로세스를 다른 프로그램으로 치환.
3) 실행 — OS 스케줄러가 CPU를 배분.
4) 종료 — exit(status) 또는 main() return.
5) 회수(Reap) — 부모가 wait()/waitpid()로 종료 상태를 수거하지 않으면 좀비(zombie) 로 남는다.
3) 핵심 시스템 콜
3.1 fork()
- 부모를 복제해 자식 프로세스를 하나 만든다.
- 반환값: 부모에선 자식 PID(>0), 자식에선 0, 실패 시 -1.
- 부모/자식은 같은 코드로 동시에 계속 실행되므로, 분기 로직이 필요.
// fork 기본 패턴
pid_t pid = fork();
if (pid < 0) {
// error
} else if (pid == 0) {
// child
} else {
// parent (pid is child's pid)
}
3.2 exec*()
- 현재 프로세스 이미지 전체를 교체하고 지정한 프로그램으로 점프.
execl,execv,execle,execve,execlp,execvp등: 인자/환경/경로 검색 차이.- 성공 시 복귀하지 않음(그 지점 이후 코드는 실행되지 않는다). 실패 시에만
-1반환.
// PATH 검색 + argv 배열 전달
char *argv[] = {"ls", "-l", "/tmp", NULL};
execvp("ls", argv); // 성공하면 여기로 돌아오지 않음
3.3 wait() / waitpid()
- 자식 프로세스 종료 대기 및 상태 수거(reap).
wait(&status): 임의의 자식 1개,waitpid(pid, &status, flags): 특정 자식 또는 비차단(WNOHANG) 가능.- 매크로:
WIFEXITED(status),WEXITSTATUS(status),WIFSIGNALED(status)등.
int status;
pid_t done = waitpid(-1, &status, 0); // 아무 자식이나 하나 대기
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
}
3.4 exit(status) / return
- 프로세스 종료, 상태 코드는 부모가
wait*로 조회. main()에서return n;은 내부적으로exit(n)과 동등하게 처리.
3.5 유틸 함수들
getpid()/getppid()— 현재/부모 PID 조회.sleep(sec)— 대기.kill(pid, sig)— 대상 프로세스에 시그널 전달(종료·중단·사용자 정의 등).signal(sig, handler)또는sigaction()— 시그널 핸들러 등록(권장:sigaction).
4) 표준 입출력과 파일 서술자
- 시작 시 보통 0:stdin, 1:stdout, 2:stderr 세 개의 FD가 열린다.
fork()는 열린 파일 서술자도 복제 → 부모/자식이 같은 파일 커서를 공유할 수 있음.dup2(newfd, oldfd)로 서술자 복제/치환 → 리다이렉션의 핵심.
// stdout(1)을 파일로 리다이렉트
int fd = open("out.txt", O_CREAT|O_WRONLY|O_TRUNC, 0644);
dup2(fd, 1); // 이후 printf/puts는 out.txt로 간다
close(fd);
5) 자주 쓰는 패턴
5.1 fork-then-exec (자식에서 새 프로그램 실행)
- 부모는 제어권을 유지하고, 자식만 다른 프로그램으로 바꿔 감시/수거가 쉽다.
pid_t pid = fork();
if (pid == 0) {
char *argv[] = {"grep", "-n", "main", "app.c", NULL};
execvp("grep", argv);
_exit(127); // exec 실패 시 안전 종료
} else {
int status;
waitpid(pid, &status, 0);
}
5.2 파이프라인: pipe() + dup2() + execvp()
- 한 프로세스의 stdout → 파이프 → 다른 프로세스 stdin 으로 연결.
int p[2]; pipe(p); // p[0]: read, p[1]: write
pid_t c1 = fork();
if (c1 == 0) { // child1: "cat file"
dup2(p[1], 1); close(p[0]); // stdout → p[1]
char *a[] = {"cat", "file.txt", NULL};
execvp("cat", a);
_exit(127);
}
pid_t c2 = fork();
if (c2 == 0) { // child2: "grep error"
dup2(p[0], 0); close(p[1]); // stdin ← p[0]
char *b[] = {"grep", "error", NULL};
execvp("grep", b);
_exit(127);
}
close(p[0]); close(p[1]); // 부모는 파이프 닫기
waitpid(c1, NULL, 0); // 자식 회수
waitpid(c2, NULL, 0);
5.3 리다이렉션: cmd > out.txt 2>&1
- 쉘 동작을 직접 코드로:
dup2()로 stdout(1)·stderr(2)를 파일로 치환.
int fd = open("out.txt", O_CREAT|O_WRONLY|O_TRUNC, 0644);
dup2(fd, 1); // stdout -> out.txt
dup2(fd, 2); // stderr -> out.txt
close(fd);
// 이후 execvp("cmd", argv) 실행
5.4 자식 회수(좀비 방지)
- 자식 종료 후
wait*로 반드시 회수. 여러 자식이면 루프 또는SIGCHLD핸들러에서 비차단 회수.
void reap_all_children(void) {
int status;
while (waitpid(-1, &status, WNOHANG) > 0) {
// reaped
}
}
6) 시그널과 안전한 처리
- 시그널: 비동기 알림(예:
SIGINT,SIGTERM,SIGCHLD). - 핸들러 설치:
sigaction()으로 재진입 안전한 최소 작업만 수행(플래그 세팅 등). - SIGCHLD: 자식 종료 알림. 핸들러에서
waitpid(-1, &status, WNOHANG)을 루프로 호출해 누락 없이 회수. - 주의: 핸들러에서 non-async-signal-safe 함수(
printf,malloc등) 호출 금지.
static volatile sig_atomic_t got_sigchld = 0;
void on_sigchld(int sig) { got_sigchld = 1; }
int main() {
struct sigaction sa = {.sa_handler = on_sigchld};
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
// 메인 루프
for (;;) {
if (got_sigchld) {
got_sigchld = 0;
int status;
while (waitpid(-1, &status, WNOHANG) > 0) { /* reap */ }
}
// ...
}
}
7) 오류 처리와 errno
- 대부분의 시스템 콜은 실패 시 -1 을 반환하고, 전역 변수
errno를 설정한다. - 검사 습관: 실패 시 즉시 분기하고, 필요 시
perror()또는strerror(errno)로 원인 로깅.
int fd = open("data.txt", O_RDONLY);
if (fd < 0) { perror("open"); /* handle */ }
8) 좀비와 고아(Orphan)
- 좀비: 종료했지만 부모가 아직
wait*로 회수하지 않아 프로세스 테이블 엔트리가 남은 상태. - 고아: 부모가 먼저 종료되어 init/systemd가 입양(re-parenting). 보통 즉시 회수되어 좀비로 남지 않는다.
- 대응: 항상 자식을 회수하는 루틴/핸들러를 갖추고, 장기 실행 데몬은 자식 관리를 체계화.
9) 보안·운영 실무 포인트
execvp/execlp는 PATH 검색을 사용 → 의도치 않은 바이너리 실행 위험. 필요 시 절대 경로 사용 또는 환경 정제(execve의envp).- 파일 서술자 누수 방지: 자식에게 전달되면 안 되는 FD는
FD_CLOEXEC/O_CLOEXEC. - 리소스 한도:
ulimit/rlimit(예:RLIMIT_NOFILE)으로 자원 소진 방지. - 신호 사용 규칙: 종료는
SIGTERM→ 유예 후SIGKILL순. 헬스체크·그레이스풀 셧다운을 표준화.
10) 요약
fork()로 복제, 자식에서exec*()로 프로그램 교체, 부모는wait*()로 회수.- 표준 입출력/
dup2/pipe로 리다이렉션·파이프라인 구현. - SIGCHLD 기반 회수와 FD/경로/자원 한도 관리로 운영 친화성을 확보.
(부록) DevOps 관점 — 왜 중요하고, 어떻게 적용하나 (Opinion)
A) 왜 중요한가
- 프로세스 생애주기 통제는 CI 러너/에이전트/워커의 안정성·관측성 핵심.
- 좀비/FD 누수/비정상 종료는 장기 서비스에서 자원 고갈·SLO 위반으로 직결.
B) 적용 방식
- 자식 관리 표준화:
fork-exec-waitpid패턴 + SIGCHLD 핸들러에서 비차단 회수 루프. - 클린 리다이렉션:
dup2+O_CLOEXEC로 로그/아티팩트를 분리, FD 누수 차단. - 보안 가드레일: 절대 경로 exec, PATH/env 정제,
RLIMIT_*설정. - 종료 절차:
TERM → (유예) → KILL표준화, 종료 훅에서 정리 작업 수행. - 관측 지표: 좀비 수, FD 사용량, 파이프 백업률, wait 실패율, 자식 실행시간 P50/P95.
728x90
'프로그래밍공부(Programming Study) > CS-운영체제(OS)' 카테고리의 다른 글
| OSTEP: 10. Multiprocessor Scheduling (Advanced) (0) | 2025.10.10 |
|---|---|
| OSTEP: 6. Mechanism: Limited Direct Execution (0) | 2025.10.09 |
| OSTEP: 4. The Abstraction: The Process (0) | 2025.10.07 |
| OSTEP: 9. Scheduling: Proportional Share (0) | 2025.10.07 |
| OSTEP: 8. Scheduling:The Multi-Level Feedback Queue (0) | 2025.09.23 |

댓글