기초 리눅스 API Vol.1 (2)

Erdos·2025년 6월 2일

LINUX/UNIX

목록 보기
2/8
post-thumbnail

ABOUT


☕1차적으로 읽으려는 부분

-- 프로세스 위주로
[Vol.1]
1~5장까지는 가볍게 읽기
6장 프로세스
24장 프로세스 생성
25장 프로세스 종료
26장 자식 프로세스 감지
27장 프로그램 실행

28~31장 읽기

-- 파이프란?
[Vol.2]
7장 파이프와 FIFO

6장 프로세스

6.1 프로세스와 프로그램

  • process: 실행 중인 프로그램.
    실행 중인 프로그램 코드와 그 실행에 필요한 메모리, 상태, 자원(파일, 네트워크 등) 전체를 포함하는 운영체제의 관리 단위.
  • program: 실행 시에 프로세스를 어떻게 만들 지에 대한 광범위한 정보를 담고 있는 파일

program(실행파일)

안에 무엇을 담고 있나

  1. binary format identification: 실행 파일의 포맷에 대한 정보

    • 요즘은 ELF(Executable and Linkable Format)로 생성한다.
    • 궁금하면 다음과 같은 명령어 쳐보기
      file [실행파일명]
  2. 기계어 명령

  3. 프로그램의 진입점 주소(entry point)

  4. 데이터: 변수의 초기값과 문자 상수

  5. symbol table, relocation table

  6. 공유 라이브러리와 동적 링크 정보

  7. 기타

6.2 프로세스 ID와 부모 프로세스

  • getpid() 시스템 호출은 호출하는 프로세스의 PID를 리턴한다.
  • getppid() 언제나 성공적으로 호출자의 부모의 PID를 리턴한다.

PID_MAX

sysctl kernel.pid_max

[출력⬇️]

kernel.pid_max = 4194304

6.3 프로세스 메모리 레이아웃

segment: 프로세스의 메모리 공간을 용도별로 나누어 관리하는 논리적 영역

  1. text segment(코드/명령어 영역): 기계어 코드가 저장되는 곳. 보통 읽기 전용
  2. initial data segment(데이터 영역): 초기화된 전역 변수, static 변수 등. 프로그램이 시작할 때 값이 정해져 있는 데이터가 위치. 보통 읽기/쓰기
  3. uninitial data segment(bss segment): 초기화되지 않은 전역/정적 변수가 저장되는 영역. 프로그램 시작 시 0으로 자동 초기화.
  4. stack: 함수 호출 시 생성되는 지역 변수, 리턴 주소, 인자 등. 일반적으로 아래 방향(주소 감소)으로 자란다.
  5. heap: 동적 메모리 할당 영역. 일반적으로 위 방향(주소 증가)으로 자란다.


그림 출처

6.4 가상 메모리 관리

가상메모리(virtual memory)

각 프로세스에 독립적인 주소 공간 부여

운영체제가 왜 가상 메모리를 관리하는가

1) 프로세스 격리 & 보호

  • 각 프로세스가 서로의 메모리 공간을 침범할 수 없게 보장한다
  • 한 프로그램의 버그, 악의적인 코드가 다른 프로세스의 메모리를 건드릴 수 없게 한다

2) 단순하고 일관된 주소 공간 제공
3) 효율적인 메모리 관리 및 자원 활용

  • 실제로 프로세스가 사용하는 메모리만 물리 메모리에 할당
  • 자주 사용되지 않는 메모리는 disk로 swap하여 전체 시스템 효율 ↑
  • 프로세스 메모리 사용량(가상메모리)은 실제 RAM 용량보다 클 수도 있다

4) 공유 라이브러리, 코드 재사용

  • 여러 프로세스가 같은 라이브러리/코드 영역(.text)를 각자 가상 메모리에서 공유(중복 복사/로드 없이 효율적으로!)

5) 안정성 & 에러 탐지

6.7 환경 변수 목록

환경 변수(environment variable): 프로세스 동작 방식에 영향을 주는 문자열 기반의 키-값 쌍의 집합

  • 예시
    • PATH=/usr/local/bin:/usr/bin:/bin
    • LANG=ko_KR.UTF-8

특징

1) 프로세스마다 별도의 환경 변수 집합을 가짐

  • 각 프로세스가 소유하는 고유한 정보
  • 부모 프로세스가 자식 프로세스를 생성(fork/exec)할 때, 부모의 환경 변수를 복사해서 자식에게 전달

2) 프로그램의 동작, 실행 환경에 영향
3) 환경 변수 목록은 순서대로 접근하기보다 각 환경변수에 개별적으로 접근한다

프로그램에서 환경 변수에 접근하기

1) 메인 함수의 세 번째 인자로

int main(int argc, char *argv[], char *envp[])
  • SUSv3 등의 표준에서 main함수의 세번째 인자는 비공식 확장/비표준이다.
    고로, 이식성, 표준성 면에서 보장되지 않는다.
    (버그가 발생할 수도 있고 컴파일러/os에 따라 제공되지 않을 수도 있음)
    사용하지 않는 게 좋음!

2) 전역 변수로 접근

extern char **environ;

3) 환경 변수 조회 함수

char *getenv(const char *name);

12장 시스템과 프로세스 정보

12.1.1 프로세스 정보 얻기: /proc/PID

/proc = procfs: 리눅스의 가상 파일 시스템
시스템과 프로세스의 상태를 파일/디렉토리처럼 "실시간으로" 제공

/proc/PID 주요 파일

PID = 1234인 프로세스를 예시로 하자면

파일명내용예시 (PID=1234)
cmdline실행 명령줄(인자 포함)cat /proc/1234/cmdline
environ환경변수 목록 (null 문자로 구분)cat /proc/1234/environ
cwd현재 작업 디렉토리(심볼릭 링크)ls -l /proc/1234/cwd
exe실행파일 경로(심볼릭 링크)ls -l /proc/1234/exe
fd열린 파일 디스크립터 목록(디렉토리)ls -l /proc/1234/fd/
status프로세스 상태(읽기 쉬운 텍스트)cat /proc/1234/status
stat다양한 프로세스 통계(공백 구분)cat /proc/1234/stat
maps메모리 매핑 상태cat /proc/1234/maps
smaps상세 메모리 매핑/사용량cat /proc/1234/smaps
mem실제 프로세스 메모리(읽기 어려움)
task스레드 정보 디렉토리ls /proc/1234/task

어떻게 보면 되나

  • 예시) 환경변수 목록 (null 문자로 구분)
cat /proc/1234/environ | tr '\0' '\n'

24장 프로세스 생성

24.1 fork(), exit(), wait(), execve() 소개


그림출처

1) fork()

  • 시스템 호출을 통해 프모 프로세스가 새로운 자식 프로세스 생성
  • 부모 자식은 거의 동일하지만 반환값으로 구분 가능
    • 부모에게는 자식의 PID, 자식에게는 0이 반환됨

2) exit(status)

  • 프로세스 종료 (현재 프로세스가 사용한 모든 자원인 메모리, 열린 fd 등을 다른 프로세스에 재할당 할 수 있게 한다.

3) wait(status)

  • 자식 프로세스가 종료되지 않았다면: 자식 프로세스가 exit()를 통해 아직 종료하지 않은 경우 자식 프로세스 중 하나가 종료할 때까지 현재 프로세스를 중지(block)시킴
  • 자식 프로세스 중 하나가 종료되었다면: 자식 종료 상태는 wait()의 인자를 통해 리턴

4) execve(pathname, argv, envp)

  • 실행 중인 현재 프로세스의 "코드, 데이터, 스택, 힙, 환경"을 완전히 새로운 프로그램으로 교체(치환)
  • 프로세스는 그대로지만 실행 중인 프로그램은 새로 바뀜
  • 함수가 성공하면 return을 하지 않음. 실패하면 -1 리턴
int execve(const char *pathname, char *const argv[], char *const envp[]);

1) copy-on-wite: fork() 직후에 부모/자식이 물리 메모리 공유. 둘 중 하나가 메모리 내용을 바꿀 떄만 실제로 복사
2) 좀비 프로세스: 자식이 종료됐는데, 부모가 wait/watipid로 회수하지 않으면 남음
3) 고아 프로세스: 부모가 먼저 종료되면 자식은 init이 부모가 된다.

  • 뒤에 구체적으로 더 나올 예정

25장 프로세스 종료

25.1 프로세스 종료하기: _exit(), exit()

1) _exit()

  • 운영체제 커널에 지금 즉시 프로세스를 종료하라고 직접 명령하는 시스템콜.
  • C 라이브러리 계층(버퍼 flush, 종료 핸들러 실행 등)을 모두 건너뛰고 바로 커널에 전달.
  • 실패하거나 오류를 반환하지 않고 즉시 "종료"상태가 됨(예외 없음)

2) exit()

  • 종료 핸들러(exit handler)가 등록된 역순으로 호출
  • stdio 스트림 버퍼 출력(남아 있는 출력 버퍼가 flush됨)
  • _exit() 시스템 호출이 주어진 status값으로 불린다 -> 나는 끝났다고 OS에게 알림. 부모 프로세스가 wait()/waitpid()로 이 status 값을 받을 수 있음.

3) main문의 return

4) 비정상 종료

  • abort(): 즉시 프로세스 비정상적 종료(SIGABRT 시그널 발생)
  • kill 명령 등 시그널

26장 자식 프로세스 감시

-- 6.3 1차 정리(요약 위주로.. 추후에 추가 정리할 수 있으면 그 때 마무리)

왜 자식 프로세스 감시가 중요할까?

  • 프로세스는 독립적이지만 부모-자식 관계로 연결되어 있다.
  • 부모는 자식의 종료, 비정상 종료, 신호, 일시 정지 등 상태 변화를 알아야 리소스를 회수하고(잘못하면 좀비..!) 필요한 대응을 할 수 있다.

26.1.1 wait() 시스템 호출

부모 프로세스가 wait(&status)를 호출하면

  • 아직 종료되지 않은 자식이 있으면: 그 중 하나가 종료될 때까지 block
  • 자식이 종료된 후 그 자식의 PID와 종료(status) 반환
  • 여러 자식이 있을 때 누가 먼저 끝날지는 모른다.
    • wait()는 가장 먼저 종료된 자식의 상태만을 처리
#include <sys/wait.h>
#include <time.h>
#include "curr_time.h"              /* 선언문 currTime() */
#include "tlpi_hdr.h"

int main(int argc, char *argv[])
{
    int numDead;       /* 기다리고 있는 자식의 수 */
    pid_t childPid;    /* 기다리는 자식의 프로세스 ID */
    int j;

    if (argc < 2 || strcmp(argv[1], "--help") == 0)
        usageErr("%s sleep-time...\n", argv[0]);

    setbuf(stdout, NULL);           /* stdout이 버퍼링되지 않게 함 */

    for (j = 1; j < argc; j++) {    /* 인수의 수만큼 자식 프로세스 생성 */
        switch (fork()) {
        case -1:
            errExit("fork");

        case 0:                     /* 자식 프로세스는 잠시 잠들었따가 종료 */
            printf("[%s] child %d started with PID %ld, sleeping %s "
                    "seconds\n", currTime("%T"), j, (long) getpid(),
                    argv[j]);
            sleep(getInt(argv[j], GN_NONNEG, "sleep-time"));
            _exit(EXIT_SUCCESS);

        default:                    /* 부모 프로세스 계속 진행 */
            break;
        }
    }

    numDead = 0;
    for (;;) {                      /* 부모는 자식 프로세스들이 종료되기를 기다린다 */
        childPid = wait(NULL);
        if (childPid == -1) {
            if (errno == ECHILD) {
                printf("No more children - bye!\n");
                exit(EXIT_SUCCESS);
            } else {                /* 예기치 못한 에러 발생 */
                errExit("wait");
            }
        }

        numDead++;
        printf("[%s] wait() returned child PID %ld (numDead=%d)\n",
                currTime("%T"), (long) childPid, numDead);
    }
}

26.1.2 watipid() 시스템 호출

pid_t waitpid(pid_t pid, int wstatus, int options);

자식의 프로세스 ID 또는 0 반환. 에러가 발생하면 -1 리턴

특정 자식(PID)를 지정해서 감시할 수 있는 함수

  • pid 가 0보다 크면, pid와 동일한 프로세스 ID를 가진 자식 프로세스를 기다린다.
  • pid 가 0이면, 부모 프로세스와 동일한 프로세스 그룹에 속한 자식 프로세스를 기다린다.
  • pid 가 -1보다 작으면, pid의 절대값과 동일한 프로세스 그룹 ID를 갖는 자식 프로세스를 기다린다.
  • pid 가 -1이면, 아무 자식 프로세스 중 하나가 끝나기를 기다린다.

주요 감시 방법

1) wait(), waitpid() 함수

  • 두 함수 모두 자식의 종료 상태 코드를 받아올 수 있음

2) Non-blocking(비동기) 감시

  • WNOHANG : 플래그. id에서 명시한 조건에 맞는 어떤 자식 프로세스도 리턴할 정보가 없다면 바로 리턴한다.(0)
  • 위 플래그를 사용해 자식이 끝나지 않았으면 즉시 리턴하도록 할 수 있음
  • 풀링, 데몬, 서버 등에서 자식 상태를 주기적으로 확인할 때 유용

3) 여러 자식 관리

  • wait()는 아무 자식이나 먼저 종료된 순서로 처리
  • waitpid()는 특정 자식 혹은 -1로 모든 자식 감시 가능

종료 상태 해석

  • wait 계열 함수의 status 통해 해석한다
  • 정상 종료(WIFEXITED, WEXITSTATUS)
  • 시그널로 종료(WIFSIGNALED, WTERMSIG)
  • 일시 정리, 재시작 등도 감시가 가능하다

좀비 프로세스 방지

부모가 wait/waitpid로 종료된 자식의 상태를 반드시 "회수"해야 좀비 프로세스가 쌓이지 않는다

27장 프로그램 실행

27.1 execve()

  • 시스템콜
  • 새 프로세스를 만드는 것이 아니라 현재 프로세스가 다른 프로그램으로 치환되는 것이다.
int execve(const char *pathname, char *const argv[], char *const envp[]);
  • 성공하면 아무것도 리턴하지 않는다. 에러가 발생하면 -1
  • pathname 인자: 프로세스의 메모리로 로드될 새 프로그램의 경로 정보를 담고 있다. 절대 경로 또는 상대 경로
profile
수학을 사랑하는 애독자📚 Stop dreaming. Start living. - 'The Secret Life of Walter Mitty'

0개의 댓글