시프 18-signals2

강준호·2024년 6월 2일

시스템프로그래밍

목록 보기
12/18

POSIX 신호 2

리눅스 프로세스 계층 구조

프로세스 계층 구조 개요

  • 로그인 셸에서 파생된 자식 프로세스, 손자 프로세스 등 다양한 프로세스 계층 구조를 형성합니다.
  • 데몬 프로세스는 백그라운드에서 실행되며, 예를 들어 httpd와 같은 웹 서버가 있습니다.
  • init 프로세스 (PID 1)는 시스템의 루트 프로세스입니다.
  • pstree 명령어를 통해 프로세스 계층 구조를 시각화할 수 있습니다.

셸 프로그램

셸의 역할

  • 셸은 사용자를 대신하여 프로그램을 실행하는 애플리케이션 프로그램입니다.
  • 대표적인 셸:
    • sh: 오리지널 Unix 셸 (Stephen Bourne, AT&T Bell Labs, 1977)
    • csh/tcsh: BSD Unix C 셸
    • bash: "Bourne-Again" 셸 (기본 Linux 셸)

셸 프로그램 예시

int main()
{
    char cmdline[MAXLINE]; /* command line */

    while (1) {
        /* read */
        printf("> ");
        fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))
            exit(0);

        /* evaluate */
        eval(cmdline);
    }
}
  • 셸 프로그램은 읽기/평가(read/evaluate) 단계를 반복합니다.

간단한 셸의 eval 함수

eval 함수 예시

void eval(char *cmdline)
{
    char *argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg? */
    pid_t pid;           /* Process id */

    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;   /* Ignore empty lines */

    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) {   /* Child runs user job */
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        /* Parent waits for foreground job to terminate */
        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: waitpid error");
        }
        else
            printf("%d %s", pid, cmdline);
    }
    return;
}

간단한 셸의 문제점

백그라운드 작업 문제

  • 포그라운드 작업을 올바르게 기다리고 회수(reap)하지만 백그라운드 작업은 문제가 됩니다.
  • 백그라운드 작업이 종료되면 좀비(zombie) 상태가 되고, 셸이 종료되지 않으면 회수되지 않습니다.
  • 이는 메모리 누수를 일으켜 커널의 메모리가 부족해질 수 있습니다.

예외적 제어 흐름 (ECF)

해결책: 예외적 제어 흐름

  • 커널은 백그라운드 프로세스가 완료될 때 정규 처리를 중단하고 신호를 통해 알립니다.
  • Unix에서 이 알림 메커니즘은 신호입니다.

신호 개념: 신호 수신

신호 수신 과정

  • 커널에 의해 신호를 전달받으면 프로세스는 어떤 방식으로든 반응해야 합니다.
    • 신호 무시
    • 프로세스 종료 (선택적 코어 덤프와 함께)
    • 신호 핸들러를 실행하여 신호 포착
  • 이는 비동기 인터럽트에 대한 하드웨어 예외 처리기와 유사합니다.

신호 전송: 프로세스 그룹

프로세스 그룹

  • 모든 프로세스는 정확히 하나의 프로세스 그룹에 속합니다.
  • getpgrp(): 현재 프로세스의 그룹을 반환합니다.
  • setpgid(): 프로세스 그룹을 변경합니다.

키보드로 신호 전송

  • Ctrl-C (SIGINT) 또는 Ctrl-Z (SIGTSTP)를 입력하면 커널은 포그라운드 프로세스 그룹의 모든 작업에 신호를 보냅니다.
    • SIGINT: 기본 동작은 각 프로세스를 종료하는 것입니다.
    • SIGTSTP: 기본 동작은 각 프로세스를 중지(일시 중지)하는 것입니다.

신호 핸들러의 동시 실행 흐름

동시 실행 흐름으로서의 신호 핸들러

  • 신호 핸들러는 별도의 논리적 흐름으로, 메인 프로그램과 동시에 실행됩니다.
  • 신호가 전달되면 컨텍스트 스위치가 발생하여 신호 핸들러가 실행됩니다.

비지역 점프: setjmp/longjmp

비지역 점프 개념

  • 사용자 수준에서 임의의 위치로 제어를 전달하는 강력하지만 위험한 메커니즘입니다.
  • 프로시저 호출/반환 규율을 깨는 통제된 방법입니다.
  • 오류 복구 및 신호 처리에 유용합니다.

setjmp 함수

int setjmp(jmp_buf j);
  • longjmp 전에 호출되어야 합니다.
  • 이후 longjmp를 위한 반환 지점을 식별합니다.
  • 한 번 호출되며, 한 번 이상 반환됩니다.

longjmp 함수

void longjmp(jmp_buf j, int i);
  • 의미: 기억된 setjmp로부터 다시 반환하지만, 이번에는 0 대신 i를 반환합니다.
  • setjmp 호출 후 호출되며, 한 번 호출되지만 반환하지 않습니다.

setjmp/longjmp 예시

jmp_buf buf;

void foo(void)
{
    if (error1)
        longjmp(buf, 1);
    bar();
}

void bar(void)
{
    if (error2)
        longjmp(buf, 2);
}

int main()
{
    switch(setjmp(buf)) {
    case 0:
        foo();
        break;
    case 1:
        printf("Detected an error1 condition in foo\n");
        break;
    case 2:
        printf("Detected an error2 condition in foo\n");
        break;
    default:
        printf("Unknown error condition in foo\n");
    }
    exit(0);
}

비지역 점프의 한계

  • 스택 규율 내에서 작동합니다.
  • 호출되었지만 아직 완료되지 않은 함수의 환경으로만 longjmp를 수행할 수 있습니다.

신호 처리 예시: Ctrl-C로 프로그램 재시작

프로그램 재시작 예제

sigjmp_buf buf;

void handler(int sig)
{
    siglongjmp(buf, 1);
}

int main()
{
    if (!sigsetjmp(buf, 1)) {
        signal(SIGINT, handler);
        puts("starting\n");
    }
    else
        puts("restarting\n");

    while(1) {
        sleep(1);
        puts("processing...\n");
    }
    exit(0); /* Control never reaches here */
}
  • Ctrl-C 입력 시 프로그램을 재시작합니다.

0개의 댓글