8장 예외적인 제어흐름
- 프로세서가 전원이 켜질 때부터 꺼질 때까지 프로그램 카운터는 다음 그림과 같이 연속된 값들을 가정한다.
- 인스트럭션 I_k 에 대응되는 주소는 a_k 이고, a_k에서 a_k+1 로의 전환은 제어이동이라 부른다.
- 이러한 제어이동의 배열은 제어흐름 또는 프로세서의 제어흐름이라고 부른다.
- 가장 간단한 유형은 점진적인 순서로, 각각의 I_k와 I_k+1이 메모리에서 나란히 있는 경우고, 인접해 있지 않는 경우는 jump, call 리턴 같은 프로그램 인스트럭션에 의해 발생한다.
- 현대의 시스템들은 제어흐름에 급격한 변화를 만드는 방법으로 여러 상황에 반응하는데 이러한 변화를 예외적인 제어흐름(exceaptional control flow(ECF))라고 한다.
- ECF는 하드웨어 수준에서부터, 운영체제 커널 수준, 응용 프로그램 수준 등 컴퓨터 시스템의 모든 수준에서 일어난다.
ECF를 이해하면 좋은 이유
1) 중요한 시스템 개념을 이해하는 데 도움이 된다. ECF는 OS가 입출력, 프로세스, 가상메모리를 구현하기 위해 사용하는 기본 메커니즘이기 때문이다.
2) 어떻게 응용 프로그램들이 OS와 상호작용하는지를 이해하는 데 도움이 된다. 응용 프로그램들은 트랩(trap), 시스템 콜(system call)이라고 알려진 ECF의 한 형태를 이용하여 OS로부터 서비스를 요청하기 떄문이다.
3) 흥미로운 새로운 응용 프로그램 작성에 도움이 된다. ECF는 응용 프로그램에 여러 강력한 ECF 메커니즘들을 제공하는데 이 메커니즘들을 이해한다면, Unix 쉘과 웹 서버 같은 흥미로운 프로그램들을 작성할 수 있다.
4) 동시성을 이해하는데 도움이 된다. ECF는 컴퓨터 시스템에서 동시성을 구현하는 기본 메커니즘이다.
5) 소프트웨어적인 예외 상황이 어떻게 동작하는지 이해하는 데 도움이 된다. C++과 Java 같은 언어는 try, catch, throw 구문을 통해 소프트웨어 예외 메커니즘을 제공한다. 소프트웨어 예외는 프로그램이 에러 발생 시에 비지역성(nonlocal) 점프(일반적인 call / return 스택 방식에 위배되는 점프)를 하도록 해준다.
8.1 예외상황(Exceptions)
- 예외상황은 부분적으로 하드웨어, 나머지 부분적으로는 OS에 의해서 구현된 예외적인 제어흐름의 한 형태이다.
- 하드웨어와 OS에 따라 다를 수 있지만 기본적인 아이디어는 모든 시스템에서 동일하다.
- 예외상황은 프로세서 상태의 변화에 대한 대응으로, 제어흐름의 갑작스런 변화이다.
- 그림 8.1에서 현재 어떤 명령어 I_curr을 실행하고 있을 때 프로세서 상태에 중요한 변화가 일어나고 있다.
(상태는 프로세서 내 다양한 비트들과 신호들로 인코드된다, 상태 변화는 event로 알려져있다.)
- 이 이벤트는 현재 인스트럭션 실행에 직접적으로 관련될 수도 있고 없을 수도 있는데, 있는 것으로는 가상메모리 페이지 오류, 산술 오버플로우가 발생하거나 인스트럭션이 divide by zero를 시도하는 경우가 있고, 없는 것은 시스템 타이머가 정지하거나 I/O 요청이 완료되는 경우가 있다.
- 프로세서가 이벤트가 발생했다는 것을 감지하면, 예외 테이블이라고 하는 점프 테이블을 통해 특정 종류의 이벤트를 처리하기 위해 운영체제 서브루틴(예외처리 핸들러)으로 간접 프로시저 콜을 하게 된다.
- 예외처리 핸들러가 처리를 끝마치면 예외상황을 발생시킨 이벤트의 종류에 따라 다음 세 가지 중 하나의 일이 발생한다.
1) 핸들러는 제어를 현재 인스트럭션 I_curr로 돌려준다. 이 인스트럭션은 이벤트가 발생했을 때 실행되고 있던 인스트럭션을 말한다.
2) 핸들러는 제어를 I_next로 돌려주는데, 이 인스트럭션은 예외상황이 발생하지 않았다면 다음에 실행되었을 인스트럭션이다.
3) 핸들러는 중단된 프로그램을 종료한다.
8.1.1 예외처리
- 시스템 내에서 가능한 예외 상황의 종류마다 중복되지 않는 양의 정수를 예외번호(exception number)로 할당하고 있다.
(이 숫자의 일부는 프로세서 설계자가 부여한 것이다.)
- 나머지 숫자는 운영체제 커널 설계자가 할당한다.
- 첫 번째의 예시로는 divide by zero, 페이지 오류, 메모리 접근 위반, breakpoint, 산술연산 오버플로우 등이 있고,
두 번째의 예시로는 시스템 콜, 외부 I / O 디바이스로부터의 시그널 등이 있다.
- 시스템 부팅 시(컴퓨터 리셋 or 전원 공급), OS는 위 그림과 같은 예외 테이블(exception table)이라고 하는 점프 테이블을 할당하고 초기화 해서 엔트리 k가 예외상황 k에 대한 핸들러의 주소를 갖게 한다.
- 런타임(시스템이 프로그램을 실행하고 있을 때)에 프로세서는 이벤트가 발생했다는 것을 감지하고, 대응되는 예외번호 k를 결정한다. 그 후, 예외 테이블의 엔트리 k를 통해 간접 프로시저 콜을 하는 방법으로 예외상황을 발생시킨다.
- 위 그림은 프로세서가 예외 테이블을 이용해서 해당 예외 핸들러의 주소를 어떻게 만드는지를 보여준다. 예외 번호는 예외 테이블에서의 인덱스이며, 이 테이블의 시작 주소는 '예외 테이블 베이스 레지스터' 라는 특별한 CPU 레지스터에 저장되어 있다.
예외상황과 프로시저 콜의 차이점
· 프로세서는 프로시저 콜을 사용해서 핸들러로 분기하기 전에 스택에 리턴 주소를 푸시한다.(예외 종류에 따라 현재 인스트럭션이거나 다음 인스트럭션 주소를 넣음)
· 프로세서는 핸들러가 리턴할 때 중단된 프로그램을 다시 시작하기 위해 필요하게 될 추가적인 프로세서 상태를 푸시한다.
ex) x86-64 시스템은 현재 조건 코드를 포함하는 EFLAGS 레지스터를 푸시한다.
· 제어가 사용자 프로그램에서 커널로 전환하고 있을 때, 푸시되는 데이터들은 사용자 스택이 아닌 커널 스택 상에 푸시된다.
· 예외 핸들러는 커널 모드에서 돌아가는데, 이는 예외 핸들러가 모든 시스템 자원에 완전히 접근할 수 있음을 의미한다.
8.1.2 예외의 종류
- 예외상황은 인터럽트(interrupts), 트랩(traps), 오류(faults), 중단(aborts) 4가지 종류로 구분할 수 있다.
(실제로 트랩, 오류, 중단 모두 인터럽트의 한 종류지만, 책에서는 I/O interrupt와 같은 것은 비동기성 interrupt이므로 구분해 놓기 위해 이렇게 해놓은 것 같음)
인터럽트(interrupts)
- 인터럽트는 프로세서 외부에 있는 입출력 디바이스로부터의 시그널의 결과로 비동기적으로 발생한다.
(비동기적 = 하드웨어 인터럽트는 특정 인스트럭션을 실행해서 발생한 것이 아니라는 의미)
- 하드웨어 인터럽트를 위한 예외 핸들러는 인터럽트 핸들러라고 부른다.
- 위 그림은 인터럽트의 처리과정을 요약한 것이다.
1) 현재 인스트럭션이 실행 완료한 뒤, 인터럽트 핀이 high로 올라간다.
2) 프로세서가 인터럽트 핀이 high로 올라간 것을 발견하고 시스템 버스에서 예외번호를 읽으며 적절한 인터럽트 핸들러를 호출한다.
3) 핸들러가 인터럽트를 처리한다.
4) 핸들러가 리턴할 때, 제어를 다음 인스트럭션으로 돌려준다.
다음과 같은 과정으로 인터럽트가 발생하지 않았던 것처럼 계속해서 실행되게 만든다.
- 나머지 예외의 종류들은 지금의 인스트럭션을 실행한 결과로 동기적으로 일어난다. 이를 오류 인스트럭션(fault instruction)이라고 한다.
트랩과 시스템 콜(traps and system calls)
- 트랩은 의도적인 예외 상황으로, 어떤 인스트럭션을 실행한 결과로 발생한다.
- 트랩 핸들러도 제어를 다음 인스트럭션으로 리턴한다.
- 트랩의 사용 중 가장 중요한 것은 시스템 콜(system call)이라고 알려진 사용자 프로그램과 커널 사이의 프로시저와 유사한 인터페이스를 제공하는 것이다.
- 사용자 프로그램을 읽거나(read), 새로운 프로세스를 만들거나(fork), 새 프로그램을 로드하고(execve), 현재 프로세스를 종료하는 등의 서비스를 커널에 요청할 필요가 있다.
이를 위해 프로세서는 syscall n 이라는 인스트럭션을 제공하는데, 이는 서비스 n을 요청하고자 할 때 사용자 프로그램이 사용할 수 있는 인스트럭션이다.
- 위 그림은 syscall 인스트럭션의 처리작업을 요약한 것이다.
- 시스템 콜은 일반적인 함수 호출과 비슷하지만, 시스템 콜은 커널 모드에서 돌아가고, 커널 내에서 정의된 스택에 접근하며, 더 강력한 인스트럭션을 실행할 수 있도록 해준다는 점에서 다르다.
===============================================================
시스템 콜과 커널
- 대다수의 운엥체제들은 커널 모드(Kernel mode)와 사용자 모드(User mode)가 구분되어 있다.
- 사용자 모드에서는 사용자 애플리케이션 코드가 실행되고, 사용자가 접근할 수 있는 영역에 제한이 있기 때문에 해당 모드에서는 하드웨어(디스크, I / O 등)에 접근할 수 없다.
- 커널 모드는 운영체제(OS)가 CPU를 사용하는 모드이다. 시스템 콜을 통해 사용자 모드에서 커널 모드로 전환되면 운영체제는 하드웨어를 제어하는 명령어(Privileged instructions)를 실행한다.
(Privileged instructions는 사용자 모드에서 실행되면 exception이 발생한다.)
- 쉽게 말하면 사용자 영역에서는 접근할 수 없는 커널에 있는 데이터나 명령어 등에 접근하기 위해서 사용하는 것이 시스템 콜이라고 할 수 있다.
===============================================================
오류(faults)
- 오류는 핸들러가 정정할 수 있는 가능성이 있는 에러 상황으로부터 발생한다.
- 오류가 발생하면 프로세서는 제어를 오류 핸들러로 이동해주는 데, 오류를 정정할 수 있다면 오류를 발생시킨 인스트럭션으로 제어를 돌려주어서 거기서부터 재실행시키는 반면,
정정할 수 없다면 커널 내부의 abort 루틴으로 리턴해서 오류를 발생시킨 응용 프로그램을 종료시킨다.
- 오류의 대표적인 예시로는 page fault exception(페이지 오류 예외)가 있다.
이는 인스트럭션이 가상 메모리 테이블을 참조했을 때 대응되는 실제 메모리 page가 존재하지 않는 상황에 발생한다.
페이지 오류 핸들러는 디스크에서 적절한 페이지를 로드해서 오류를 해결하여 해당 인스트럭션으로 제어를 넘겨준다.
중단(aborts)
- 중단은 DRAM이나 SRAM이 고장날 때 발생하는 parity 에러와 하드웨어가 고장나는 등의 복구할 수 없는 에러에서 발생한다.
8.1.3 리눅스 / x86-64 시스템에서의 예외상황
- 리눅스 / x86-64 시스템에서는 총 256개의 서로 다른 예외 종류들이 있다.
0 ~ 31은 인텔 아키텍처에서 정의된 예외들에 대응되며, 모든 x86-64 시스템에서 동일하다.
32 ~ 255은 운영체제에서 정의된 인터럽트와 트랩에 대응된다.
리눅스 / x86-64 오류와 중단(faults and aborts)
· Divide error : 나누기 에러(exception 0)는 응용 프로그램이 0으로 나누려고 할 때, 또는 나눗셈 인스트럭션의 결과가 목적지 오퍼랜드에 비해 너무 큰 경우에 발생한다.
Unix는 나누기 에러에서 복구하려는 시도를 안하고 프로그램을 중단한다. 리눅스 쉘은 나누기 에러를 "Floating exception"(부동소수 예외)로 오류를 알려준다.
· General protection fault : 일반 보호 오류(예외 13)는 여러 가지 이유로 발생하는데, 대개 프로그램이 가상메모리의 정의되지 않은 영역을 참조하거나, 프로그램이 read-only 텍스트 세그먼트에 쓰려고 할 때 발생한다.
리눅스는 이 오류에서 복구하려는 시도를 하지 않으며, "Segmentation faults"로 오류를 알려준다.
· Page fault : 페이지 오류(예외 14)는 오류 발생 인스트럭션으로 다시 되돌아가는 대표적인 예시이다.
핸들러는 필요한 디스크의 가상 메모리의 페이지를 물리 메모리의 페이지로 매핑하고, 오류 인스트럭션을 다시 시작한다.
· Machine check : 머신 체크(예외 18)는 오류 인스트럭션을 실행하는 동안 검출된 치명적인 하드웨어의 에러의 결과로 발생한다.
머신 체크 핸들러는 절대 제어를 응용 프로그램에 돌려 주지 않는다.
리눅스 / x86-64 시스템 콜(system calls)
- 리눅스는 파일을 읽거나 쓸 때, 새로운 프로세스를 만들 때 응용 프로그램이 사용할 수 있는 수백 개의 시스템 콜을 제공한다.
- 위 그림은 많이 쓰이는 리눅스 시스템 콜 중 일부를 보여준다.(각 시스템 콜은 커널 점프 테이블의 오프셋에 대응되는 중복되지 않는 정수를 갖는다.)
(이 점프 테이블은 예외 테이블과는 다르다.)
- C 프로그램에서는 syscall 함수를 사용해서 직접 시스템 콜을 호출할 수 있지만, 보통 더 편리한 래퍼(wrapper) 함수를 사용한다.
- 래퍼 함수는 인자들을 패키징하고, 커널을 적절한 시스템 콜 인스트럭션으로 트랩을 걸고, 호출하는 프로그램으로 시스템 콜의 리턴 상태를 전달한다.
- 리눅스 시스템 콜에 전달되는 모든 인자들은 스택보다는 범용 레지스터를 통해 이루어진다.
관습적으로 레지스터 %rax는 시스템 콜 번호를 보관하며, %rdi, %rsi, %rdx, %r10, %r8, %r9에 최대 6개의 인자들을 보관할 수 있다.
(첫 번째 인자는 %rdi에, 두 번째 인자는 %rsi에 와 같은 방식으로 저장된다.)
- 시스템 콜이 리턴될 때, %rcx, %r11의 값들이 지워지고, %rax는 리턴 값을 보관한다.
- -4095 ~ -1 의 음수 리턴 값은 음수의 errno에 대응하는 에러를 나타낸다.
EX) write의 첫 번째 인자는 출력을 stdout으로 보내고, 두 번째는 write로 가는 바이트 배열이고, 세 번째는 write할 바이트 수를 나타낸다.
- 위 그림은 write와 exit 시스템 콜을 직접 호출하는 syscall 인스트럭션을 사용하는 hello의 어셈블리 버전을 보여준다.
- 9~13 줄은 write함수를 호출하고, 14~16 줄은 _exit 시스템 콜을 호출한다.
8.5 시그널(signal)
- 시그널은 상위 수준의 소프트웨어 형태의 예외적 제어 흐름이다.
(시그널은 프로세스와 커널이 다른 프로세스를 중단하도록 한다.)
- 시그널은 작은 메시지 형태로, 프로세스에게 시스템 내에 어떤 종류의 이벤트가 일어났다는 것을 알려준다.
- 위 그림은 리눅스 시스템에서 지원되는 30개의 서로 다른 종류의 시그널이다.
- 각 시그널 타입은 특정 종류의 시스템 이벤트에 대응된다.
- Low-level 하드웨어 예외는 커널의 예외 핸들러에 의해 처리되고, 보통은 사용자 프로세스에서 볼 수 없다.
시그널은 이러한 예외들을 사용자 프로세스에 노출해주는 메커니즘을 제공한다.
EX) 어떤 프로세스가 0으로 나누려고 한다면, 커널은 SIGFPE 시그널(8번)을 프로세스에 보내준다.
어떤 프로세스가 잘못된 인스트럭션을 실행하려 한다면, 커널은 SIGILL 시그널(4번)을 보낸다.
어떤 프로세스가 잘못된 메모리를 참조한다면, 커널은 SIGSEGV 시그널(11번)을 보낸다.
- 어떤 시그널들은 커널 내 또는 다른 사용자 프로세스 내부의 high-level 소프트웨어 이벤트에 대응된다.
EX) 어떤 프로세스가 foreground에서 돌아가고 있을 때, Ctrl + C를 입력하면 커널은 SIGINT(2번)을 foreground 프로세스 그룹에 속한 각 프로세스들에게 보낸다.
어떤 프로세스는 다른 프로세스에 SIGKILL 시그널(9번)을 보내 강제로 종료시킬 수 있다.
자식 프로세스가 종료하거나 정지할 때, 커널은 SIGCHILD 시그널(17번)을 부모에게 보낸다.
8.5.1 시그널 용어
- 시그널을 목적지 프로세스로 전달하는 것은 두 단계로 이루어진다.
· 시그널 보내기(sending a signal) : 커널은 목적지 프로세스의 컨텍스트 내에 있는 일부 상태를 갱신해서 시그널을 목적지 프로세스로 보낸다.
시그널은 두 가지 이유 중 하나로 인해 배달된다.
1) 커널이 0으로 나누기나 자식 프로세스의 종료 같은 시스템 이벤트를 감지한 경우
2) 어떤 프로세스가 커널에 명시적으로 시그널을 목적지 프로세스에 보낼 것을 요구하기 위해 kill 함수를 호출한 경우
또, 프로세스는 자기 자신에게 시그널을 보낼 수 있다.
· 시그널 받기(receiving a signal) : 목적지 프로세스는 커널이 시그널이 보내진 것에 대해 반응하라고 할 때 시그널을 받는다.
프로세스는 시그널 핸들러라 부르는 사용사 수준 함수를 실행해서 시그널을 무시하거나 종료하거나 받을 수 있다.
- 보내졌지만 아직 받지 않은 시그널은 펜딩(pending) 시그널이라 부른다.
- 특정 시점에서 특정 타입에 대해 최대 한 개의 펜딩 시그널 만이 존재할 수 있다. 만약 특정 타입의 펜딩 시그널을 갖고 있을 때, 같은 타입의 시그널이 발생한다면 그 시그널은 큐에 들어가지 않고 버려진다.
- 프로세스는 선택적으로 특정 시그널의 수신을 블록할 수 있는데, 해당 시그널은 배달은 될 수 있지만 펜딩 식널은 블록을 풀 때까지 수신되지 않는다.
- 펜딩 시그널은 최대 한 번만 수신된다.
각 프로세스에 대해, 커널은 pending 비트 벡터 내에 펜딩하고 있는 시그널의 집합을 관리하며, blocked 비트 벡터 내에서 블록된 시그널의 집합을 관리한다.
커널은 pending 내에 비트 k를 타입 k의 시그널이 배달될 때 마다 설정하며, 해당 시그널이 수신될 때마다 pending의 비트 k를 0으로 만든다.
8.5.2 시그널 보내기
- Unix 시스템은 시그널을 프로세스로 보내는 여러 가지 메커니즘을 제공한다. 이 메커니즘들은 모두 프로세스 그룹(process group) 개념을 사용한다.
프로세스 그룹(Process groups)
- 모든 프로세스는 하나의 프로세스 그룹에 속하며, 어떤 그룹에 속하는지는 양수 process group ID로 식별한다.
getpgrp 함수를 통해 그룹 ID를 알 수 있다.
- 자식 프로세스는 자신의 부모와 동일한 프로세스 그룹에 속하는데, 프로세스는 자신의 프로세스 그룹이나 다른 프로세스의 그룹을 setpgid를 통해 변경할 수 있다.
이 함수는 프로세스 pid의 프로세스 그룹을 pgid로 변경한다.
만약 pid가 0이면 현재 프로세스의 PID가 사용되고, pgid가 0이면 pid에 들어간 PID값이 프로세스 그룹 ID로 사용된다.
ex) 만약 프로세스 15213이 다음과 같이 호출하면,
프로세스 그룹 ID가 15213인 새로운 그룹을 만들고 프로세스 15213을 이 그룹에 추가한다.
시그널을 /bin/kill 프로그램을 사용해서 보내기
- /bin/kill 프로그램은 다른 프로세스로 임의의 시그널을 보낸다.
ex) 다음과 같은 명령은 시그널 9번(SIGKILL)을 프로세스 15213에 보낸다.
- 이 때, pid값을 음수로하면 프로세스 그룹 PID 내의 모든 프로세스로 보내지게 한다.
위와 같은 명령은 SIGKILL 시그널이 프로세스 그룹 15213 내의 모든 프로세스로 보내지게 한다.
- /bin/kill과 같이 완전한 경로를 사용하고 있는 이유는 일부 Unix 쉘이 자신 만의 내장 kill 명령어를 가지고 있기 때문이다.
키보드에서 시그널 보내기
- Unix 쉘은 작업의 추상화를 이용하여 한 개의 명령줄을 해석한 결과로 만들어진 프로세스에 반영한다.
- 어떤 시점이든 최대 한 개의 포그라운드 작업과 0개 이상의 백그라운드 작업이 존재한다.(포그라운드는 결과가 나올 때 까지 추가적으로 명령을 입력하는 등의 작업을 할 수 없기 때문)
ex) 다음과 같이 입력하면, 두 개의 프로세스가 Unix 파이프로 연결된 포그라운드 작업을 만든다.
하나는 ls 프로그램을 실행하고, 다른 하나는 sort 프로그램을 실행한다.
=============================================================
포그라운드(foreground) 작업 : 사용자가 입력한 명령이 실행되어 결과가 출력될 때까지 기다리는 방식으로 처리되는 작업을 의미한다.
포그라운드 프로세스 : 포그라운드 작업 방식으로 처리되는 프로세스
쉘은 포그라운드로 실행 중인 프로세스가 중료되기를 기다렸다가, 종료하면 다시 쉘 프롬프트를 보여주며 명령 대기 상태가 된다.
백그라운드(background) 작업 : 사용자가 입력한 명령이 끝나는 것과 상관없이 곧바로 프롬프트가 출력되어 다른 작업을 진행할 수 있는 작업
백그라운드 프로세스 : 백그라운드 작업 방식으로 처리되는 프로세스
쉘은 백그라운드 프로세스 종료 여부에 관계없이 즉시 명령 대기 상태가 되어 다른 명령을 받아들일 준비를 갖춘다.
==============================================================
- 쉘은 각 작업마다 별도의 프로세스 그룹을 만든다. 일반적으로 프로세스 그룹 ID는 작업 내의 부모 프로세스들 중 하나에서 가져온다.
ex) 다음 그림은 하나의 포그라운드 작업과 두 개의 백그라운드 작업을 갖는 쉘을 보여준다.
포그라운드 작업은 PID 20과 프로세스 그룹 ID 20을 갖는다.
그 자식들도 해당 부모의 프로세스 그룹 ID를 따라 20을 갖게 되는 것을 볼 수 있다.
- 키보드에서 Ctrl + C 를 입력하면 커널은 SIGINT 시그널(포그라운드 작업을 종료하는 시그널)을 포그라운드 프로세스 그룹에 속한 모든 프로세스에게 보낸다.
Ctrl + Z 를 입력하면 커널은 포그라운드 프로세스 그룹에 속한 모든 프로세스에 SIGSTP 시그널(포그라운드 작업을 정지(suspend)시키는 시그널)을 보낸다.
kill 함수로 시그널 보내기
- 프로세스는 위와 같은 kill함수를 호출하여 자신 또는 다른 프로세스로 시그널을 보낼 수 있다.
pid가 0보다 크면 해당 pid값을 가진 프로세스에 시그널 번호 sig에 해당하는 시그널을 보낸다.
pid가 0이면 kill을 호출한 프로세스인 자신을 포함한 프로세스 그룹 내 모든 프로세스에 시그널 sig을 보낸다.
pid가 0보다 작으면 시그널 sig를 프로세스 그룹 |pid| (pid의 절대값) 내의 모든 프로세스로 보낸다.
alarm 함수로 시그널 보내기
- 프로세스는 SIGALRM 시그널을 alarm 함수를 호출해서 자기 자신에게 보낼 수 있다.
alarm 함수는 커널이 sec초마다 프로세스로 SIGALRM 시그널을 보내게 한다.
만약 sec == 0 이라면 새로운 알람이 가지 않는다.
- 어떤 이벤트가 발생해도 alarm이 새로 호출되면, pending 되어있던 alarm들을 취소하고, 대기하던 알람이 전달됐을 시간까지 남은 시간을 초 단위 숫자로 반환하거나, pending된 alarm이 없으면 0을 return한다.
8.5.3 시그널의 수신
- 커널이 프로세스 p를 커널 모드에서 사용자 모드로 전환할 때(시스템 콜에서 리턴하거나 문맥 전환을 끝마치는 경우), 커널은 프로세스 p에 대해 블록되지 않은 펜딩 시그널(pending & ~blocked)의 집합을 체크한다.
집합이 비어 있다면, 커널은 제어를 p의 논리 제어흐름 내의 다음 인스트럭션(I_next)로 넘긴다.
집합이 비어있지 않다면, 커널은 집합 내 특정 시그널 k(대부분은 가장 작은 숫자)를 선택해 p가 시그널 k를 수신하도록 한다.
해당 시그널을 수신하면 프로세스는 다음 4가지 동작 중 하나를 수행하게 되는데, 동작을 완료하면 제어는 p의 다음 인스트럭션(I_next)로 넘어간다.
· 프로세스가 종료한다.(Terminates)
· 프로세스가 종료하고 코어를 덤프한다.(Terminates and dumps core)
· 프로세스가 SIGCONT 시그널에 의해 재시작될 때까지 정지(지연)한다.(Stop(suspend) until restarted by SIGCONT)
· 시그널을 무시한다.(Ignore signal)
- 그림 8.26에서 각 시그널 타입과 연결된 기본 동작이 무엇인지 보여준다.
프로세스는 시그널과 연결된 기본 동작을 signal 함수를 사용해서 수정할 수 있지만, SIGSTOP과 SIGKILL의 기본 동작은 변경할 수 없다.
- signal 함수는 시그널 signum과 연결된 동작을 다음 세 가지 방법 중 하나로 바꿀 수 있다.
· handler가 SIG_IGN이면, signum 타입의 시그널은 무시된다.
· handler가 SIG_DFL이면, signum 타입의 시그널에 대한 동작은 기본 동작으로 돌아간다.
· 그 외의 경우, handler는 signal handler라 불리는 사용자가 정의한 함수의 주소가 되며, signum 타입의 시그널을 수신할 때마다 호출될 것이다.
핸들러의 주소를 signal 함수로 넘겨주는 방법은 기본 동작을 변경하는 것으로, 핸들러를 설치한다(installing)라고 말한다. 핸들러의 호출은 시그널을 잡는다(catching the signal), 핸들러의 실행은 시그널을 처리한다(handling the signal)고 부른다.
- 어떤 프로세스가 타입 k의 시그널을 잡을 때, 시그널 k를 처리하기 위해 설치된 핸들러는 k에 설정된 한 개의 정수 인자를 사용해서 호출한다.
이 인자는 같은 핸들러 함수가 다른 종류의 시그널을 잡을 수 있도록 한다.
- 핸들러가 return 문장을 실행할 때, 제어는 대부분 시그널의 수신으로 중단됐던 인스트럭션으로 다시 전달된다.(시스템 콜들이 에러가 발생하면 즉시 리턴하기 때문)
- 위 그림은 사용자가 Ctrl + C를 키보드에서 입력할 때마다 보내진 SIGINT를 잡는 프로그램을 보여준다.
기본적으로 SIGINT는 즉시 프로세스를 종료하는 것이지만 위 그림에서는 시그널을 잡고, 메세지를 출력하고, 프로세스를 종료하도록 수정된 버전이다.
- 시그널 핸들러는 위 그림에 나타낸 것처럼 다른 핸들러에 의해 interrupt를 받아 중단될 수 있다.
위 예제에서 메인 프로그램은 메인 프로그램을 중단하고, 제어를 핸들러 S로 넘기는 시그널 s를 붙잡는다.
S가 돌고 있는 동안 이 프로그램은 s가 아닌 시그널 t를 붙잡으며, 이 시그널 t는 S를 중단하고 제어를 핸들러 T로 넘긴다.
T가 리턴할 때, S가 중단됐던 위치에서 다시 실행되고, S가 리턴하고, 제어를 메인 프로그램으로 넘기며 메인 프로그램이 중단됐던 위치에서 다시 실행하게 된다.
8.5.4 시그널 블록하기와 블록 해제하기
묵시적 블록 방법(Implicit blocking mechanism)
- 커널이 핸들러에 의해 처리되고 있는 타입의 모든 pending 시그널을 블록한다.(막는다)
ex) 그림 8.31에서 프로그램이 시그널 s를 잡고 현재 핸들러 S를 실행하고 있다고 하자.
만약 또 다른 시그널 s가 이 프로세스로 보내졌다면, s는 대기(pending)하게 되지만 핸들러 S가 리턴하기 전까지 수신할 수 없게 된다.
명시적 블록 방법(Explicit blocking mechanism)
- 응용 프로그램들이 sigprocmask 함수와 이 함수의 도움 함수를 이용해 시그널들을 명시적으로 블록하거나 블록 해제할 수 있다.
- sigprocmask는 현재 블록된 시그널의 집합을 변경한다. 특정 동작은 how 값에 따라 달라진다.
SIG_BLOCK : set에 있는 시그널들을 blocked에 추가한다(blocked = blocked | set).
SIG_UNBLOCK : set에 있는 시그널들을 blocked에서 제거한다(blocked = blocked & ~set).
SIG_SETMASK : blocked = set
- 만약 oldset이 NULL이 아니라면, blocked 비트 벡터의 이전 값이 oldset에 저장된다.
- set 같은 시그널 집합들은 다음과 같은 함수를 사용해서 조작한다.
sigemptyset : set을 비어있는 집합으로 초기화한다.
sigfillset : 모든 시그널을 set에 추가한다.
sigaddset : signum을 set에 추가한다.
sigdelset : signum을 set에서 지운다.
setismember : signum이 set의 멤버면 1, 아니면 0을 리턴한다.
- 위 그림은 SIGINT 시그널의 수신을 일시적으로 블록하기 위해 sigprocmask를 어떻게 사용하는지를 보여준다.
'TIL & WIL' 카테고리의 다른 글
[CS] 크래프톤 정글 6주차 - DMA(Direct Memory Access) (1) | 2024.05.01 |
---|---|
[TIL] 크래프톤 정글 5주차 CS:app 9(가상 메모리 & 동적 메모리 할당) (1) | 2024.04.26 |
[TIL] 크래프톤 정글 5주차 - 자료구조(RB Tree) (1) | 2024.04.20 |
[TIL] 크래프톤 정글 5주차 CS:app 7(링커 Linker) (0) | 2024.04.19 |
[TIL] 크래프톤 정글 4주차 - 동적 메모리 할당(Dynamic Memory Allocation) (0) | 2024.04.15 |