728x90
CSAPP
8. 예외적인 제어흐름
- 시스템들은 또한 내부 프로그램 변수에 의해 표시되지 않으며, 프로그램의 실행과는 반드시 관련되어 있지 않은 시스템 상태의 변화에도 반응할 수 있어야 한다.
- 급격한 변화를 예외적인 제어흐름이라고 부른다. 예외적인 제어흐름은 컴퓨터 시스템의 모든 수준에서 발생한다.
8.1 예외 상황
- 예외 상황은 부분적으로는 하드웨어와 운영체제에 의해서 구현된 예외적인 제어흐름의 한 가지 형태다.
- 예외 상황은 어떤 프로세서 상태의 변화에 대한 대응으로, 제어흐름의 갑작스런 변화다.
- 프로세서가 이벤트가 발생했다는 것을 감지하면, 예외 테이블이라고 하는 점프 테이블을 통해서 이 특정 종류의 이벤트를 처리하기 위해 특별히 설계 된 운영체제 서브루틴(예외처리 핸들러)으로 간접 프로시저 콜을 하게된다.
- 예외처리 핸들러가 처리를 끝마치면, 예외상황을 발생시킨 이벤트의 종류에 따라서 다음과 같은 세 가지 중의 하나의 일이 발생한다.
- 핸들러는 제어를 현재 인스트럭션으로 돌려준다.
- 핸들러는 제어를 다음 인스트럭션으로 돌려준다.
- 핸들러는 중단된 프로그램을 종료한다.
8.1.1 예외처리
- 한 시스템 내에서 가능한 예외 상황의 종류마다 중복되지 않는 양의 정수를 예외번호로 할당하고 있다. 이 숫자들의 일부는 프로세서 설계자가 부여한 것이다. 나머지 번호는 운영체제 커널(운영체제의 메모리가 상주하는 부분)설계자가 할당한다.
- 시스템 부팅 시, 운영체제는 예외 테이블이라고 하는 점프 테이블을 할당하고 초기화해서 엔트리 k가 예외 상황 k에 대한 핸들러 주소를 갖는다.
- 런타임에 프로세서는 이벤트가 발생했다는 것을 감지하고, 대응되는 예외번호 k를 결정한다. 프로세서는 그 후에 예외 테이블의 엔트리 k를 통해서 간접 프로시저 콜을 하는 방법으로 예외 상황을 발생시킨다. 예외 상황은 프로시저콜과 유사하지만 차이점이 있다.
- 프로세서는 프로시저 콜을 사용해서 핸들러로 분기하기 전에 스택에 리턴 주소를 푸시한다. 그렇지만 예외의 종류에 따라 리턴 주소는 현재 인스트럭션이거나 다음 인스트럭션이 된다.
- 프로세서는 핸들러가 리턴할 때 중단된 프로그램을 다시 시작하기 위해 필요하게 될 스택상에 추가적인 프로세서 상태를 푸시한다.
- 제어가 사용자 프로그램에서 커널로 전환하고 있을 때 이 모든 아이템들은 사용자 스택 위가 아니라 커널 스택상에 푸시된다.
- 예외 핸들러는 커널모드에서 돌아가는데, 이들이 모든 시스템 자원에 완전히 접근할 수 있다는 것을 의미한다.
- 하드웨어가 예외 상황을 촉발해서 남은 작업은 예외 핸들러에 의해 소프트웨어로 진행된다.
8.1.2 예외의 종류
인터럽트
- 인터럽트는 프로세서 외부에 있는 입출력 디바이스로부터의 시그널의 결과로 비동기적으로 발생한다. 하드웨어 인터럽트를 위한 예외 핸들러는 종종 인터럽트 핸들러 라고 부른다.
- 입출력 디바이스들은 프로세서 칩의 핀에 시그널을 보내서 인터럽트를 발생시키고, 인터럽트를 발생시킨 디바이스를 식별하는 예외번호를 시스템 버스에 보낸다.
- 현재의 인스트럭션이 실행을 완료한 후에, 프로세서는 인터럽트 핀이 high로 올라갔다는 것을 발견하고 시스템 버스에서 예외번호를 읽으며, 적절한 인터럽트 핸들러를 호출한다. 핸들러가 리턴할 때, 제어를 다음 인스트럭션으로 돌려준다.
트랩과 시스템 콜
- 트랩은 의도적인 예외 상황으로, 어떤 인스트럭션을 실행한 결과로 발생한다. 트랩 핸들러는 제어를 다음 인스트럭션으로 리턴한다. 트랩의 가장 중요한 사용은 시스템 콜이라고 알려진 사용자 프로그램과 커널 사이의 프로시저와 유사한 인터페이스를 제공하는 것이다.
- 사용자 프로그램은 여러 서비스를 종종 커널에게 요청할 필요가 있다. 이런 커널 서비스의 제한된 접근을 하기 위해서 프로세서는 특별한 ‘n’ 인스트럭션을 제공하며, 이들은 사용자 프로그램이 서비스 n을 요청하고자 할 때 사용자 프로그램이 사용할 수 있는 인스트럭션이다. syscall인스트럭션을 실행하면 트랩이 인자들을 해독하고 적절한 커널 루틴을 호출하는 예외 핸들러로 가게한다.
- 보통의 함수는 사용자 모드에서 돌아가며 이 때문에 이들이 실행할 수 있는 인스트럭션은 제한적이며, 이들은 호출하는 함수와 동일한 스택을 사용한다. 시스템 콜은 커널 모드에서 돌아가며, 이로 인해 커널 내에서 정의된 스택에 접근하며, 특권을 가진 인스트럭션을 실행할 수 있도록 해준다.
오류(fault는 error와 다르다)
- 오류는 핸들러가 정정할 수 있는 가능성이 있는 에러조건으로부터 발생한다. 오류가 발생하면 프로세서는 제어를 오류 핸들러로 이동해준다. 만일 핸들러가 에러 조건을 정정할 수 있다면, 제어를 오류를 발생시킨 인스트럭션으로 돌려주어서 거기서부터 재 실행한다. 그렇지 않다면, 핸들러는 커널 내부의 abort 루틴으로 리턴해서 오류를 발생 시킨 응용 프로그램을 종료한다.
중단
- 패리티 에러와 하드웨어 같은 복구할 수 없는 치명적인 에러에서 발생한다. 중단 핸들러는 절대로 응용 프로그램으로 제어를 리턴하지 않는다.
8.2 프로세스
- 프로세스의 고전적인 정의는 실행 프로그램의 인스턴스이다. 시스템 내의 각 프로그램은 어떤 프로세서의 문맥(context)에서 돌아간다. 문맥은 프로그램이 정확하게 돌아가기 위해서 필요한 상태로 구성된다. 이 상태는 메모리에 저장된 프로그램의 코드와 데이터, 스택, 범용 레지스터의 내용, 프로그램 카운터, 환경변수, 열려있는 파일의 식별자를 포함한다.
- 프로세스가 응용에 제공하는 주요 추상화
- 우리의 프로그램이 프로세서를 혼자서 사용한다는 착각을 제공하는 독립적 논리제어 흐름
- 우리의 프로그램이 혼자서 메모리 시스템을 가진다는 착각을 제공하는 사적주소 공간
8.2.1 논리적인 제어흐름
- 프로그램 실행을 단일 스텝으로 실행하기 위해서 디버거를 사용하고자 한다면, 우리의 실행 목적파일 내에 들어있거나 프로그램과 동적으로 런타임에 링크된 공유 객체 내의 인스트럭션들에게 일련의 프로그램 카운터PC값들이 대응된다는 것을 관찰할 수 있다. 이러한 PC값의 배열을 논리적 제어흐름 또는 논리흐름이라고 부른다.
- 하나의 프로세서를 사용해서 여러 프로세스들이 교대로 돌아간다. 각 프로세스는 자신의 흐름의 일 부분을 실행하고나서 다른 프로세스들로 순서를 바꾸어 실행하는 동안 선점된다.(일시적으로 정지된다.) 이 프로세스들 중에서 하나의 문맥에서 실행되는 프로그램은 이것이 마치 프로세서를 배타적으로 소유한 것처럼 보인다. 사실은 만일 각 인스트럭션의 소모시간을 정확히 측정하려고 한다면 CPU가 주기적으로 프로그램의 순차적인 인스트럭션 실행이 이유없이 멈춰 있는 것처럼 보인다는 것을 발견한다. 이렇게 동작한다고 문제가 생기지는 않는다. 왜냐하면 프로세서가 정지(Stall)할 때마다 프로그램의 메모리 위치나 레지스터 내용에 변경되는 사항 없이 프로그램 실행은 순차적으로 다시 실행된다.
8.2.2 동시성 흐름
- 자신의 실행시간이 다른 흐름과 겹치는 논리 흐름을 동시성 흐름이라고 부르며, 이 두 흐름은 동시에 실행한다고 말한다. 공동으로 실행되는 흐름의 일반적인 현상이 동시성이라고 알려져 있다. 또한 프로세스가 다른 프로세스들과 교대로 실행된다는 개념은 멀티태스킹이라고 알려져 있다. 한 프로세스가 자신의 흐름 일부를 실행하는 매 시간 주기를 타임 슬라이스 라고 부른다. 그래서 멀티태스킹은 타임 슬라이싱이라고도 부른다.
- 동시성 흐름에 대한 개념은 흐름들이 돌아가는 프로세서 코어나 컴퓨터 개수와는 무관하다.
- 두 개의 흐름이 서로 다른 프로세서 코어나 컴퓨터에서 동시에 돌아간다면 이것은 병렬흐름이다.
8.2.3 사적 주소공간
- n비트 주소를 갖는 머신에서, 주소공간은 2^n의 가능한 주소들로 0,1,…,(2^n)-1을 갖는다. 프로세스는 각 프로그램에 자신만의 사적 주소공간을 제공한다. 이 공간의 특정 주소에 연결된 메모리의 한 개의 바이트가 일반적으로 다른 프로세스에 의해서 읽히거나 쓰일 수 없다는 의미로 이 공간은 사적이다.
8.2.4 사용자 및 커널모드
- 운영체제가 완벽한 프로세스 추상화를 제공하기 위해서 프로세서는 응용 프로그램이 접근 할 수 있는 주소공간 부분뿐만 아니라 응용 프로그램이 실행할 수 있는 인스트럭션들을 제한하는 메커니즘을 제공해야 한다.
- 프로세서는 대개 이러한 작업을 지원하기 위해 프로세스가 현재 가지고 있는 특권을 저장하는 일부 제어 레지스터로 모드 비트를 제공한다. 모드 비트가 설정되면 프로세스는 커널모드로 동작한다(슈퍼 바이저 모드라고도 불린다.) 커널모드에서 돌고 있는 프로세스는 인스트럭션 집합의 어떤 인스트럭션도 실행할 수 있으며, 시스템 내의 어떤 메모리 위치도 접근할 수 있다.
- 모드 비트가 세트되지 않을 때, 프로세스는 사용자 모드에서 돌고 있는 것이다. 사용자 모드의 프로세스는 특수 인스트럭션을 실행할 수 없다. 또한 주소공간의 커널 영역에 있는 코드나 데이터를 직접 참조 할 수도 없다. 이러한 시도를 하게 되면 치명적인 보호 오류가 발생한다. 그 대신 사용자 프로그램은 시스템 콜을 통해서 커널코드와 데이터에 간접적으로 접근해야 한다.
- 응용 코드를 실행하는 프로세스는 처음에는 사용자 모드에 있다. 예외가 발생해서 제어가 예외 핸들러로 넘어가면, 프로세서는 사용자 모드에서 커널모드로 변경한다. 핸들러는 커널모드에서 돌아간다. 제어가 응용 코드로 돌아오면 프로세서는 모드를 커널모드에서 사용자 모드로 변경한다.
8.2.5 문맥전환 context switching
- 운영체제 커널은 문맥전환이라고 알려진 예외적인 제어흐름의 상위 수준 형태를 사용해서 멀티태스킹을 구현한다.
- 커널은 각 프로세스마다 컨텍스트를 유지한다. 컨텍스트는 커널이 선점된 프로세스를 다시 시작하기 위해서 필요로 하는 상태다. 이것은 범용 레지스트러, 부동소수점 레지스터, 프로그램 카운터, 상태 레지스터, 사용자 스택, 커널 스택, 여러 커널 자료구조(페이지 테이블, 프로세스 테이블, 파일 테이블)같은 객체들의 값들로 구성된다.
- 커널은 프로세스가 실행되는 동안의 어떤 시점에 현재 프로세스를 선점(일시적으로 정지)하고 이전에 선점된 프로세스를 다시 시작하는 것을 결정할 수 있다. 이 결정은 스케줄링이라고 알려져 있으며, 스케줄러라고 부르는 커널 내부의 코드에 의해 처리된다. 커널이 실행 할 새 프로세스를 선택할 때 커널이 그 프로세스를 스케줄 했다고 말한다.
- 커널이 실행 할 새 프로세스를 스케줄 한 후에 현재 프로세스를 선점(일시적으로 정지) 하는것을 문맥 전환이라고 하며, 이 메커니즘을 사용해서 새로운 프로세스로 제어를 이동한다.
- 문맥전환 수행절차
- 현재 프로세스의 컨텍스트 저장
- 이전에 선점된(일시적으로 정지된) 프로세스의 저장된 컨텍스트를 복원
- 제어를 이 새롭게 복원된 프로세스로 전달한다.
- 문맥전환은 커널이 사용자를 대신해서 시스템 콜을 실행하고 있을 때 일어날 수 있다. 만일 시스템 콜이 어떤 이벤트의 발생을 기다리기 때문에 블록된다면 커널은 현재 프로세스를 sleep시키고 다른 프로세스로 전환한다. 일반적으로, 어떤 시스템 콜이 블록되지 않았다 하더라도 커널은 시스템 콜을 호출했던 프로세스로 제어를 돌려주는 대신에 문맥전환을 수행하는 것을 결정할 수 있다.
- 또한 문맥전환은 인터럽트의 결과로 발생할 수 있다.
8.3 시스템 콜의 에러처리
- Unix의 시스템 수준 함수가 에러를 만날 때 이들은 대개 -1을 리턴하고, 전역 정수 변수인 errno를 세팅해서 무엇이 잘못 되었는지를 나타낸다.
8.4 프로세스의 제어
- Unix는 C프로그램으로 부터 프로세스를 제어하기 위한 많은 시스템 콜을 제공한다.
8.4.1 프로세스 ID가져오기
- 각각의 프로세스는 고유의 양수(0이 아닌) 프로세스ID(PID)를 가진다. getpid함수는 호출하는 함수의 PID를 리턴한다.
8.4.2 프로세스의 생성과 종료
- 프로그래머의 관점에서 프로세스는 다음의 세 가지 상태중의 하나로 생각할 수 있다.
- 실행중 : 프로세스는 CPU에서 실행하고 있거나 실행을 기다리고 있으며, 궁극적으로 커널에 의해서 스케줄 될 것이다.
- 정지 : 프로세스의 실행은 정지한 상태이고 스케줄 되지 않는다.
- 종료 : 프로세스는 영구적으로 정지된다. 프로세스는 다음의 세 가지 이유 중의 하나로 종료된다.
- 프로세스를 종료하는 시그널을 받았을 때
- 메인 루틴에서 리턴할 때
- exit 함수를 호출할 때
- exit 함수는 종료 상태 status로 프로세스를 종료한다.
- 부모 프로세스는 fork 함수를 불러서 자식 프로세스를 생성한다. 새롭게 생성된 자식 프로세스는 완벽하게는 아니지만 부모와 거의 동일하다. 자식은 코드, 데이터 세그먼트, 힙, 공유된 라이브러리, 사용자 스택을 포함하는 부모의 사용자 수준 가상 주소공간과 동일한(그러나 분리된) 복사본을 갖는다. 자식은 또한 부모가 오픈한 파일 식별자 모두와 동일한 사본을 갖는다. 이것은 부모가 fork를 호출했을 때 부모가 오픈한 파일 모두를 읽고 쓸 수 있다는 것을 의미한다. 부모와 새롭게 생성된 자식간의 가장 중요한 차이는 이들이 서로 다른 PID를 가진다는 것이다.
- fork 함수는 흥미로운데, 그것은 이들이 한 번 호출되지만 두 번 리턴하기 때문이다.
- 한 번은 호출한 프로세스(부모)
- 한 번은 새롭게 생성된 프로세스(자식)
- fork 함수는 흥미로운데, 그것은 이들이 한 번 호출되지만 두 번 리턴하기 때문이다.
- 부모에서 fork는 자식의 PID를 리턴한다. 자식에서 fork는 0을 리턴한다. 자식의 PID가 항상 0이 아니기 때문에 리턴값은 프로그램이 부모에서 실행하고 있는지 자식에서 실행하고 있는지를 구분하는 명확한 방법을 제공한다.
- fork 함수를 처음 배울 때 프로세스 그래프를 그려보면 도움이 된다. 이 그래프는 프로그램 문장들의 부적인 순서를 나타내는 순서 그래프의 단순한 종류다. 각 꼭짓점은 프로그램 문장 하나의 실행에 대응된다.
- 하나의 프로세서에서 돌아가는 어떤 프로그램에 대해, 해당 프로세서 그래프에서 꼭짓점들의 위상학적 정렬은 프로그램에서 문장들의 가능한 전체 정렬을 나타낸다.
8.4.3 자식 프로세스의 청소
- 프로세스가 어떤 이유로 종료할 때, 커널은 시스템에서 즉시 제거하지 않는다. 그 대신, 프로세스는 부모가 청소할 때까지 종료된 상태로 남아있다. 부모가 종료된 자식을 청소할 때 커널은 자식의 exit 상태를 부모에게 전달하며, 그 후 종료된 프로세스를 없애며 이 시점에서 프로세스가 사라지게 된다. 종료되었지만 아직 청소되지않은 프로세스를 좀비라고 한다.
- 부모 프로세스가 종료할 때, 커널은 init 프로세스로 하여금 모든 고아가 된 자식들의 입양된 부모가 되도록 한다. 이 init 프로세스는 PID 1번이며, 시스템 초기화 과정에서 커널에 의해 생성되고, 결코 종료되지 않으며, 모든 프로세스의 조상이다. 만일 어떤 부모 프로세스가 자신의 좀비 자식들을 소거하지 않고 종료하려면, 커널은 이 init 프로세스가 이들을 소거하도록 한다. 그렇지만, 쉘이나 서버같이 오랫동안 실행하는 프로그램들은 항상 자신의 좀비를 소거해야 한다
- 프로세스는 waitpid 함수를 호출해서 자신의 자식들이 종료되거나 정지되기를 기다린다.
에러조건
- 호출하는 프로세스가 자식이 없다면, waitpid는 -1을 리턴하며 errno를 ECHILD로 설정한다. waitpid 함수가 어떤 시그널에 의해 중단되었다면, -1을 리턴하며 errno를 EINTR로 설정한다.
8.4.4 프로세스 재우기
- sleep 함수는 일정 기간동안 프로세스를 정지 시킨다.
- sleep은 요청한 시간이 경과하면 0을 리턴하고, 그렇지 않은 경우에는 남은 시간동안 잠을 잔다. 만일 sleep 함수가 시그널에 의해서 중단되어 완료되지 못한 채로 리턴된다면 후자의 경우가 가능해진다.
8.4.5 프로그램의 로딩과 실행
- execve 함수는 현재 프로그램의 컨텍스트 내에서 새로운 프로그램을 로드하고 실행한다.
- argu 변수는 널로 종료되는 포인터의 배열을 가리키며, 각각의 포인터는 하나의 인자 스트링을 가리킨다. 관습에 의해 argu[0]은 실행가능 목적파일의 이름이다.
8.5 시그널
- 시그널은 작은 메세지 형태로, 프로세스에게 스스템 내에 어떤 종류의 이벤트가 일어 났다는 것을 알려준다.
- 각 시그널 타입은 특정 종류의 시스템 이벤트에 대응된다. 하위 수준 하드웨어 예외는 커널의 예외 핸들러에 의해 처리되며, 정상적으로 사용자 프로세스에서는 볼 수 없다. 시그널은 이러한 예외들을 사용자 프로세스에 노출해주는 메커니즘을 제공한다.
8.5.1 시그널 용어
- 시그널을 목적지 프로세스로 전달하는 것은 두 단계로 이루어진다.
- 시그널 보내기 : 시그널을 목적지 프로세스로 보낸다. 시그널은 두 이유중 하나로 배달된다.
- 시스템 이벤트를 감지했다.
- 어떤 프로세스가 커널에 명시적으로 시그널을 목적지 프로세스에 보낼 것을 요구하기 위해서 kill 함수를 호출했다.
- 시그널 받기 : 목적지 프로세스는 배달된 신호에 대해서 커널이 어떤 방식으로 반응해야 할 떄 목적지 프로세스는 시그널을 받는다. 프로세스는 시그널 핸들러라고 부르는 사용자 수준 함수를 실행해서 시그널을 무시하거나, 종료하거나, 획득할 수 있다.
- 시그널 보내기 : 시그널을 목적지 프로세스로 보낸다. 시그널은 두 이유중 하나로 배달된다.
- 보내졌지만 아직 받지 않은 시널은 펜딩(pending) 시그널이라고 부른다. 시간상으로 어떤 시점에서 특정 타입에 대해 최대 한 개의 펜딩 시그널이 존재할 수 있다. 만일 어떤 프로세스가 타입 k의 펜딩 시그널을 가지고 있다면 이 프로세스로 다음에 발생하는 k 타입의 시그널은 큐에 들어가지 않고 단순히 버려진다.
- 프로세스는 선택적으로 어떤 시그널의 수신을 블록할 수 있다. 어떤 시그널이 블록될 때 배달은 될 수 있지만 펜딩 시그널은 이 프로세스가 시그널 블록을 끌 때 까지는 수신되지 않는다.
- 펜딩 시그널은 최대 한 번만 수신된다.
8.5.2 시그널 보내기
- Unix 시스템은 시그널을 프로세스로 보내는 여러가지 메커니즘을 제공한다. 모든 메커니즘은 프로세스 그룹 개념을 사용한다.
프로세스 그룹
- 모든 프로세스는 정확히 한 개의 프로세스 그룹에 속하며, 이것은 양수 Process group ID로 식별한다.
- 기본적으로 자식 프로세스는 자신의 부모와 동일한 프로세스 그룹에 속한다.
키보드에서 시그널 보내기
- Unix 쉘은 작업의 추상화를 사용해서 한 개의 명령줄을 해석한 결과로 만들어진 프로세스에 반영한다. 시간상의 어떤 시점에서도 최대 한 개의 포그라운드 작업과 0또는 그 이상의 백그라운드 작업이 존재한다.
- 쉘은 각 작업마다 별도의 프로세스 그룹을 만든다. 일반적으로 프로세스 그룹 ID는 작업 내에 부모 프로세스들 중의 하나에서 가져온다.
kill 함수로 시그널 보내기
- 프로세스는 kill 함수를 호출해서 시그널을 다른 프로세스로 보낸다.(자기 자신을 포함한다)
8.5.3 시그널의 수신
- 커널이 프로세스 P를 커널모드에서 사용자 모드로 전환할 때, 커널은 프로세스P에 대한 블록되지 않은 펜딩 시그널(pending & blocked)의 집합을 체크한다. 만일 이 집합이 비어있다면(대개의 경우), 커널은 제어를 P의 논리 제어흐름 내의 다음 인스트럭션으로 전달한다.
- 그렇지만 만일 이 집합이 비어있지 않다면, 커널은 집합 내 어떤 시그널 k를 선택해서(대개 가장 작은k) P가 시그널k를 수신하도록 한다. 시그널을 수신하면 프로세스는 어떤 동작을 개시한다. 프로세스가 이 동작을 완료하면, 제어는 P의 논리 제어흐름내의 다음 인스트럭션으로 돌아간다.
- 시그널 타입은 사전에 정의된 기본동작을 가진다.
- 프로세스가 종료한다.
- 프로세스는 종료하고 코어를 덤프한다
- 프로세스는 SIGCONT 시그널에 의해 재시작 될 때까지 정지한다.
- 프로세스는 시그널을 무시한다.
- 핸들러가 return 문장을 실행할 때, 제어는(대개) 프로세스가 시그널의 수신으로 중단되었던 제어흐름 내 인스트럭션으로 다시 전달된다. 여기서 ‘대개’ 라고 했는데, 그것은 일부 시스템에서 중단된 시스템 콜들이 에러가 발생하면 즉시 리턴하기 때문이다.
8.5.4 시그널 블록하기와 블록 해제하기
- 리눅스는 시그널을 블록하기 위해 묵시적이고 명시적인 방법을 제공한다.
- 묵시적 블록 방법 : 기본적으로 커널은 핸들러에 의해 처리되고 있는 유형의 모든 대기 시그널의 처리를 막는다.
- 명시적 블록 방법 : 응용 프로그램들은 sigprocmask 함수와 이들의 도움 함수를 이용해서 시그널들을 명시적으로 블록하거나 블록 해제 할 수 있다.
8.5.5 시그널 핸들러 작성하기
- 핸들러는 이해하기 어렵게 만드는 몇 가지 특성을 가진다.
- 핸들러는 메인 프로그램과 동시적으로 돌아가고, 전역변수를 공유하며, 그래서 메인 프로그램과 다른 핸들러에 뒤섞일 수 있다.
- 어떻게 그리고 언제 시그널들이 수신될 수 있는지는 종종 직관적이지 않다.
- 다른 시스템들은 다른 시그널 처리 방식을 갖는다.
- 안전한 시그널 처리
- G0. 핸들러는 가능한 한 간단하게 유지하라.
- G1. 핸들러에서 비동기성-시그널-안전한 함수만 호출하라
- G2. errno를 저장하고 복원하라
- G3. 모든 시그널을 블록시켜서 공유된 전역 자료구조들로의 접근을 보호하라
- G4. 전역변수를 volatile로 선언하라
- G5. sig_atomic_t로 플래그들을 선언하라(읽기와 쓰기가 원자형 : 중단불가)
호환성 있는 시그널 핸들링
- Unix 시그널 핸들링의 또 다른 지저분한 측면은 서로 다른 시스템들이 서로 다른 시그널 처리 방식을 갖는다는점이다.
- 이런 이슈들을 다루기 위해 POSIX 표준은 sigaction 함수를 정의하고 있으며, 이것은 사용자들이 핸들러를 설치할 때, 사용자들이 원하는 시그널 처리 개념을 명확히 명시하도록 해준다.
- signal 이라고 부르는 래퍼 함수를 통해 sigaction을 호출한다.
- signal 래퍼는 다음과 같은 시그널 처리 개념으로 시그널 핸들러를 설치한다.
- 현재 핸들러에 의해 처리되고 있는 시그널 유형들만 블록된다.
- 모든 시그널 구현에서 처럼 시그널들은 큐에 들어가지 않는다.
- 중단된 시스템 콜들은 필요할 때마다 자동으로 재시작된다.
- 시그널 핸들러가 설치되면, signal이 핸들러의 인자 SIG_IGN, ISIG_DFL 중의 하나를 갖는 핸들러로 불린다.
8.6 비지역성 점프
- C는 비지역성 점프라고 부르는 사용자 수준의 예외적 제어흐름의 형태를 제공하며, 이것은 보통의 콜 - 리턴 순서를 통할 필요없이 하나의 함수에서 현재 실행하고 있는 다른 함수로 제어를 이동한다. 비지역성 점프는 setjmp와 longjmp 함수로 제공된다.
- 비지역성 점프의 중요한 응용은 심하게 중첩된 함수 콜에서, 대개 어떤 에러 조건을 검출한 결과로 즉시 리턴을 허용하는 것이다. 또 다른 중요한 응용은 시그널의 도착으로 중단되었던 인스트럭션으로 돌아가는 대신 특정 코드 위치로 시그널 핸들러를 벗어나서 분기하는 경우다.
8.7 프로세스 조각을 위한 도구
- 리눅스 시스템은 프로세스를 관찰하고 조작하기 위한 여러가지 유용한 도구를 제공한다.
- STRACE. : 돌고있는 프로그램과 자식들이 호출한 각 시스템 콜의 경로를 인쇄한다.
- PS. : 현재 시스템 내의 프로세스들(좀비를 포함해서)을 출력한다.
- TOP. : 현재 프로세스의 자원 사용에 관한 정보를 출력한다.
- PMAP. : 프로세스의 메모리 맵을 보여준다.
- /proc. : 여러가지 커널 자료구조의 내용을 사용자 프로그램이 읽을 수 있는 ASCII 문자 형태로 내보내는 가상 파일 시스템
Stack & Queue
createQueueFromLinkedList
void createQueueFromLinkedList(LinkedList *ll, Queue *q)
{
/* add your code here */
ListNode* temp = ll->head;
int ll_size = ll->size;
for (int i =0; i<ll_size;++i)
{
enqueue(q,temp->item);
temp = temp->next;
}
}
removeOddValues
void removeOddValues(Queue *q)
{
int q_size = q->ll.size;
for(int i = 0; i<q_size;++i)
{
int temp = dequeue(q);
if(0 == temp%2)
enqueue(q,temp);
}
}
createStackFromLinkedList
void createStackFromLinkedList(LinkedList *ll, Stack *s)
{
ListNode* temp = ll->head;
int ll_size = ll->size;
while(temp != NULL)
{
push(s,temp->item);
temp = temp->next;
}
}
removeEvenValues
void removeEvenValues(Stack *s)
{
Stack temp_s;
temp_s.ll.head = NULL;
temp_s.ll.size = 0;
int s_size = s->ll.size;
for(int i = 0; i<s_size;++i)
{
int temp = pop(s);
if(0 != temp%2)
push(&temp_s,temp);
}
s_size = temp_s.ll.size;
for(int i = 0; i<s_size;++i)
{
push(s,pop(&temp_s));
}
}
isStackPairwiseConsecutive
int isStackPairwiseConsecutive(Stack *s)
{
int check;
int i = 1;
if(1 == s->ll.size%2)
return 0;
while(s->ll.size>0)
{
if(0 == i%2)
{
if(1 != abs(check-pop(s)))
return 0;
++i;
}
check = pop(s);
++i;
}
return 1;
}
reverse
void reverse(Queue *q)
{
Queue temp;
temp.ll.head =NULL;
temp.ll.size =0;
temp.ll.tail=NULL;
int q_size = q->ll.size;
while(q_size>0)
{
for(int i = q_size; i>1; --i)
enqueue(q,dequeue(q));
enqueue(&temp,dequeue(q));
--q_size;
}
*q = temp;
}
recursiveReverse
void recursiveReverse(Queue *q)
{
int temp;
if(NULL == q->ll.head)
return;
temp = dequeue(q);
recursiveReverse(q);
enqueue(q,temp);
}
removeUntil
void removeUntil(Stack *s, int value)
{
int s_size = s->ll.size;
int temp;
for (int i =0; i<s_size; ++i)
{
temp = pop(s);
if(value == temp)
{
push(s,temp);
break;
}
}
}
balanced
int balanced(char *expression)
{
Stack str;
str.ll.head = NULL;
str.ll.size = 0;
char temp;
int idx = 0;
while(expression[idx])
temp = expression[idx];
if(peek(&str) == temp -1 || peek(&str) == temp - 2)
pop(&str);
else
push(&str,temp);
++idx;
if (isEmptyStack(&str))
return 0;
else
return 1;
}
마지막 balanced 문제에서 애 먹었는데, 정글의 호랑이 종문이의 힘을 받아 풀 수 있었다.

정글엔 괴물들이 너무나도 많다.
치타가 살아남기 위해서는 노력을 더 해야겠다
하지만 오늘 치-타는 웃는다.
치타는 웃고있다..
728x90
'Study > TIL(Today I Learned)' 카테고리의 다른 글
24.02.14 간단한 정리, 백준, C++, Keyword (2) | 2024.02.14 |
---|---|
24.02.13 CSAPP 9 (3) | 2024.02.14 |
24.02.11 CSAPP 6.1.2 - 6, LinkedList 구현 (9) | 2024.02.12 |
24.02.08 CSAPP 6.1 (2) | 2024.02.09 |
24.02.07 C & C++ (0) | 2024.02.07 |