** 한빛미디어의 '리눅스 커널의 이해'를 읽고 내용을 정리하는 글입니다. **
- 시그널과 프로세스 간 통신
유닉스 '시그널(Signal)'은 프로세스에 시스템 이벤트를 알려주는 메커니즘을 제공한다. 각 이벤트는 SIGTERM과 같이 기호로 된 상수로 참조하는 자신만의 시그널 번호를 가지고 있으며, 이에는 두 종류가 있다.
- 비동기적 알림(Asynchronous Notification)
예를 들어, 사용자가 터미널에서 인터럽트 키를 눌러(Ctrl + C) Foreground Process에 SIGINT 인터럽트 시그널을 보낼 수 있다.
- 동기적 에러(Synchronous Error)나 예외(Exception)
예를 들어, 프로세스가 잘못된 주소에 있는 메모리 위치에 접근하려고 하면 커널은 프로세스에 SIGSEGV 시그널을 보낸다. (BOF 등에서 자주 등장한다.)
POSIX 표준에서는 20개 가량의 시그널을 정의하고 있으며 이중 2개는 사용자가 정의할 수 있다. 프로세스는 시그널을 받을 때 보통 시그널을 무시하거나, 비동기적으로 특별한 절차(시그널 핸들러)를 실행하는 두 가지 반응을 보인다.
(POSIX 시그널 : http://www.comptechdoc.org/os/linux/programming/linux_pgsignals.html)
프로세스가 이 중 하나를 지정하지 않으면 커널은 시그널 번호에 따라 정해진 기본 동작을 수행한다. 기본 동작은 다음의 다섯 가지이다.
- 프로세스를 종료한다
- 실행 컨텍스트와 주소 공간의 내용을 파일에 기록하고(Core dump) 프로세스를 종료한다. (BOF시 자주 본다)
- 시그널을 무시한다
- 프로세스를 보류한다
- 프로세스가 중단된 상태라면 프로세스 실행을 재개한다
SIGKILL과 SIGSTOP 시그널은 프로세스가 직접 처리할 수 없으며, 무시할 수도 없다.
- 프로세스 관리
유닉스에서는 프로세스와 프로세스가 실행하는 프로그램을 명확하게 구별한다. 새로운 프로세스를 생성하고 종료하는 데는 각각 fork()와 _exit() 시스템 콜을 사용하지만, 새로운 프로그램을 로드할 때는 exec()계열의 시스템 콜을 사용한다.
fork()를 호출하는 프로세스는 '부모(Parent)', 새로운 프로세스는 '자식(Child)'이 된다. 프로세스를 기술하는 자료 구조에는 친부모와 모든 친자식들을 가리키는 포인터가 있어 부모와 자식은 서로를 알 수 있다. fork() 함수를 단순하게 구현하면 부모의 코드 및 데이터를 전부 자식에게 복사하면 되지만 이럴 경우 시간이 많이 걸린다. 그래서 최신 커널은 페이징 유닛(Paging Unit)기능을 활용하여 'Copy On Write'한다. 이는 페이지 복사를 최후의 순간까지 미루는 것이다.
_exit() 시스템 콜은 프로세스를 종료한다. 커널은 프로세스가 점유하고 있는 자원을 반납하고 부모 프로세스에 SIGCHLD 시그널을 보내는데, 기본적으로 부모는 이 시그널을 무시한다.
- 좀비 프로세스
부모 프로세스는 wait4() 시스템 콜을 호출하여 자식 프로세스 중 하나가 종료할 때까지 기다릴 수 있다. 이 시스템 콜은 종료한 자식의 PID(Process ID)를 돌려준다. 이 시스템 콜을 호출하면 커널은 이미 종료한 자식이 있는지 검사하는데, wait4() 시스템 콜을 호출하기 전까지 종료한 프로세스는 '좀비 프로세스(Zombie Process)' 상태로 남는다. wait4() 시스템 콜을 호출할 때, 그 전에 종료한 자식 프로세스가 없으면 커널은 보통 자식 프로세스가 종료할 때까지 부모 프로세스를 대기 상태로 만든다.
만약 부모 프로세스가 wait4() 시스템 콜을 호출하지 않고 종료한다면, 커널은 좀비 프로세스로 남아 있는 자식 프로세스를 'init'이라는 시스템 프로세스의 자식으로 만든다. init 프로세스는 모든 자식의 실행 상태를 지켜보고 정기적으로 wait4() 시스템 콜을 호출하여 모든 좀비 프로세스를 제거하는 효과를 갖는다.
- 가상 메모리
모든 최신 유닉스 시스템은 '가상 메모리(Virtual Memory)'라는 유용한 추상화 개념을 제공한다. 가상 메모리를 사용하는 데는 다양한 목적과 장점이 있다.
- 여러 프로세스를 동시에 실행할 수 있다.
- 사용할 수 있는 물리 메모리보다 많은 메모리를 필요로 하는 애플리케이션을 실행할 수 있다.
- 프로그램 코드 중 일부만 메모리에 로드해도 프로세스를 실행할 수 있다.
- 각 프로세스는 사용 가능한 물리 메모리의 일부에만 접근할 수 있다.
- 라이브러리나 프로그램의 메모리 이미지 하나를 프로세스 사이에서 공유할 수 있다.
- 프로그램을 재배치(Relocation)할 수 있다.
- 프로그래머는 물리 메모리의 구조에 신경 쓸 필요가 없으므로 편하게 코드를 작성할 수 있다.
프로세스가 가상 주소를 사용하면 커널과 MMU(Memory Management Unit)가 서로 협력하여 요구한 메모리 주소의 실제 물리적 위치를 찾는다. 현재의 CPU에는 자동으로 가상 주소를 물리 주소로 변환하는 하드웨어 회로가 있다. 이에 따라 사용 가능한 램을 일반적으로 4KB나 8KB단위의 '페이지 프레임(Page Frame)'으로 쪼개고 페이지 테이블(Page Table)을 통해 가상 주소와 물리 주소 사이의 대응 관계를 지정한다. 이 회로를 사용하면 연속된 가상 주소에 메모리 블록을 할당하려는 요청을 실제 물리 주소에서는 연속되지 않는 페이지 프레임 그룹에 할당하여 처리할 수 있어 메모리 할당이 간단해진다.
- 램 사용
모든 유닉스 운영체제는 RAM(Random Access Memory)을 두 부분으로 나누어 구분한다.
몇 메가바이트는 커널 이미지(커널 코드 및 커널의 정적 자료 구조)를 저장하는 데 사용한다. 나머지는 보통 가상 메모리 시스템이 다루는 부분으로, 다음의 세 가지 용도로 사용한다.
- 커널에서 필요로 하는 버퍼와 디스크립터, 다른 동적으로 만들어지는 커널 자료 구조용
- 프로세스가 필요로 하는 일반적인 메모리 영역과 파일 메모리 매핑용
- 디스크나 버퍼를 통하는 다른 장치로부터 더 나은 성능을 얻기 위한 캐시용
사용 가능한 램은 제한되어 있기 때문에 위의 세 가지 요구 사이에 균형을 맞추는 것은 매우 중요하다.
- 커널 메모리 할당자
'커널 메모리 할당자(KMA, Kernel Memory Allocator)'는 시스템의 모든 부분에서 오는 메모리 영역의 관련 요청을 처리하는 서브시스템이다. 이 요청 중 일부는 커널에서 사용할 메모리를 위한 것이고, 다른 일부는 사용자 프로그램이 자신의 프로세스 주소 공간을 늘리기 위해 시스템 콜을 통해 호출한 것이다. 좋은 커널 메모리 할당자는 다음과 같은 특징을 갖추어야 한다.
- 빨라야 한다.
- 낭비되는 메모리 양을 최소화해야 한다.
- 메모리 단편화 문제를 줄여야 한다.
- 다른 메모리 관리 서브시스템과 협력하여 이들로부터 메모리를 빌려오거나 이들의 메모리를 해지할 수 있어야 한다.
프로세스 주소 공간에는 프로세스가 접근할 수 있는 모든 가상 메모리 주소가 들어 있다. 커널은 프로세스의 가상 주소 공간을 '메모리 영역 디스크립터(Memory Area Descriptor)'의 리스트로 저장한다. 예를 들어, 프로세스가 exec() 계열의 시스템 콜을 통해 어떤 프로그램을 실행하면 커널은 프로세스에 다음과 같은 메모리 영역으로 구성된 가상 주소 공간을 할당한다.
- 프로그램의 실행 코드
- 프로그램의 초기화된 데이터
- 프로그램의 초기화되지 않은 데이터
- 초기 프로그램 스택(즉 유저 모드 스택)
- 프로그램이 필요로 하는 공유 라이브러리의 실행 코드와 데이터
- 힙(Heap)
- 장치 드라이버
커널은 '장치 드라이버(Device Driver)'를 통해 입출력 장치와 상호 작용한다. 장치 드라이버는 커널에 들어 있으며 하드 디스크나 키보드, 마우스, 모니터, SCSI 버스에 연결된 장치, 네트워크 카드 같은 장치를 하나 이상 제어하는 자료 구조와 함수로 이루어진다. 각 드라이버는 정해진 인터페이스에 따라 커널의 다른 부분과 상호 작용한다. 이런 접근 방법에는 다음과 같은 이점이 있다.
- 장치에 따라 고유한 코드를 특정 모듈 속으로 넣을 수 있다.
- 제품을 파는 사람은 커널 소스 코드를 모르더라도 인터페이스 명세(Interface Specification)만 알면 새로운 장치를 추가할 수 있다.
- 커널은 모든 장치를 동일한 방법으로 다루고, 동일한 인터페이스로 접근한다.
- 시스템을 재부팅하지 않고도 커널에 동적으로 로드할 수 있는 모듈 형태로 장치 드라이버를 만들 수 있으며 더 이상 필요하지 않은 모듈을 동적으로 언로드하여 커널 이미지가 램에서 차지하는 크기를 줄일 수 있다.
'Knowledge' 카테고리의 다른 글
Windows에서 python-magic 사용하기 (0) | 2017.10.27 |
---|---|
리눅스 커널 공부 정리 0x04 - 가상 파일 시스템 (0) | 2017.06.28 |
리눅스 커널 공부 정리 0x02 (1) | 2017.06.21 |
리눅스 커널 공부 정리 0x01 (0) | 2017.06.20 |
Memory Segmentation (0) | 2016.12.27 |