리눅스 시스템 프로그래밍 - 프로세스와 스레드

김신·2022년 12월 28일
0
post-thumbnail

0. 프로세스

프로세스는 실행 중인 오브젝트 코드를 말합니다. 그리고 프로세스는 종료 전까지 항상 활성화 상태로 실행 중인 프로그램을 의미합니다. 프로세스를 오브젝트 코드라고 말했지만, 정확히는 단순한 오브젝트 코드를 넘어 데이터, 리소스, 상태, 가상화된 컴퓨터를 포함합니다.

1. 실행 파일

프로세스는 커널이 이해하는 실행 파일 포맷으로 만들어져 실행 가능한 오브젝트 코드로부터 시작됩니다. 리눅스에서 가장 일반적인 실행 파일은 ELF(Executable and Linkable Format)입니다. 실행 파일은 여러 섹션으로 구성되는데 섹션에는 메타데이터, 코드, 데이터 등이 들어 있습니다. 이 섹션은 오브젝트 코드가 담긴 바이트 배열이며 성형 메모리 공간에 적재됩니다. 섹션에 담긴 바이트는 접근 권한이 같으며 사용 목적이 비슷하고, 동일하게 취급됩니다.

2. 섹션

실행 파일의 가장 중요한 공통 섹션은 텍스트 섹션, 데이터 섹션, bss섹션입니다. 텍스트 섹션에는 실행 가능한 코드나 상수, 변수와 같은 읽기 전용 데이터가 있으며 읽기 전용과 실행 가능으로 표시됩니다. 데이터 섹션에는 정의된 값을 할당한 C 변수와 같은 초기화된 자료가 있으며 읽반적으로 읽고 쓰기가 가능하도록 표시됩니다. bss 섹션은 초기화되지 않은 전역 데이터를 포함합니다. C 표준에 따르면 C 변수의 기본값은 보통 0이므로 디스크에 저장된 오브젝트 코드에 0을 저장할 필요가 없습니다. 그 대신 오브젝트 코드는 단순히 bss섹션에 초기화되지 않은 변수 목록을 유지하며 커널은 메모리에 올라오는 시점에서 모든 값이 0인 페이지를 이 섹션에 맵핑할 수 있습니다. bss 섹션은 오로지 이 목적에 최적화되어 있습니다. bss라는 이름은 block started by symbol 또는 block storage segment에서 유래되었습니다. 이외에도 ELF 파일에는 절대 섹션과 다양한 데이터가 들어가는 미정의 섹션이 있습니다.

3. 시스템 리소스

프로세스는 커널이 중재하고 관리하는 다양한 시스템 리소스와 관련이 있습니다. 프로세스는 일반적으로 시스템 콜을 이용해서 리소스를 요청하고 조작한다. 이런 리소스에는 타이머, 대기중인 시그널, 열린 파일, 네트워크 연결, 하드웨어, IPC 메커니즘 등이 포함됩니다. 프로세스 리소스는 자신과 관련한 데이터의 통계 정보를 포함하고 있으며 해당 프로세스의 프로세스 디스크립터의 형태로 커널 내부에 저장됩니다.

4. 가상화

프로세스는 가상화를 위한 추상 개념이다. 선점형 멀티태스킹과 가상 메모리를 지원하는 리눅스 커널은 가상화된 프로세서와 가상화된 메모리를 프로세스에 제동합니다. 프로세스 관점에서 바라보면 마치 혼자서 시스템을 통제하고 있다는 착각에 빠집니다. 다시 말해, 스케줄러를 통해 프로세스 여러 개가 실행되더라도 프로세스 각각이 전체 시스템을 독점하는 듯이 동작합니다. 커널은 동작 중인 모든 프로세스가 시스템 프로세서를 공유하도록 빈틈없고 투명하게 프로세스를 선점하고 스케줄링합니다.

프로세스 입장에서는 동작 방식에 대한 차이점을 결코 알지 못합니다. 또한, 커널은 각 프로세스에 단일 선형 주소 공간을 제공하므로 마치 프로세스 홀로 시스템에 존재하는 모든 메모리를 제어하는 것처럼 보입니다. 커널은 가상 메모리와 페이징 기법을 사용해서 프로세스마다 다른 주소 공간에서 동작하도록 만들기 때문에 여러 프로세스가 시스템상에 공존할 수 있는 것입니다. 최신 프로세서는 운영체제가 독립적인 여러 프로세스의 상태를 동시에 관리할 수 있도록 하며 커널은 이런 하드웨어의 도움을 받아 가상화를 관리합니다.

5. 스레드

각 프로세스는 실행 스레드를 하나 이상 포함합니다. 스레드는 프로세스 내부에서 실행하는 활동 단위이며, 코드를 실행하고 프로세스 동작 상태를 유지하는 추상 개념입니다.

프로세스는 대부분 스레드 하나로만 구성되어 있는데 이를 싱글스레드라고 합니다. 여러 스레드를 포함하는 프로세스를 멀티스레드라고 합니다. 유닉스의 간결함을 중시하는 철학과 빠른 프로세스 생성 시간, 견고한 IPC 메커니즘 때문에 전통적으로 유닉스 프로그램은 싱글스레드였고 스레드 기반으로 옮겨가려는 요구사항이 비교적 적었습니다.

스레드는 스택, 프로세서 상태, 오브젝트 코드의 현재 위치를 포함합니다. 기타 프로세스엣 남아 있는 대부분의 리소스는 모든 스레드가 공유합니다. 기타 프로세스에 남아 있는 대부분의 리소스는 모든 스레드가 공유합니다. 이런 방식으로 스레드는 가상 메모리를 공유하고 가상 프로세서를 관리합니다.

내부적으로 리눅스 커널은 독특한 관점으로 스레드를 구현합니다. 스레드는 단순히 몇몇 리소스를 공유하는 일반적인 프로세스일 뿐입니다. 사용자 영역에서 리눅스는 POSIX 1003.1c에 따라 스레드를 구현합니다. glibc의 일부인 현재 리눅스 스레드 구현 이름은 NPTL(Native POSIX Threading Library)입니다.

6. 프로세스의 계층 구조

각각의 프로세스는 pid(process id)라고 하는 고유한 양수 값으로 구분됩니다. 첫 번째 프로세스의 pid는 1이며 그 뒤로 생성되는 프로세스는 새로운 고유 pid를 부여받습니다.

리눅스에서 프로세스는 트리라는 엄격한 계층 구조를 형성합니다. 일반적으로 프로세스 트리는 init 프로그램으로 알려진 첫 번째 프로세스가 루트가 됩니다. 새로운 프로세스는 fork() 시스템 콜로 만들어집니다. 이 시스템 콜은 호출하는 프로세스를 복사해서 다른 프로세스를 새로 만듭니다. 원본 프로세스를 부모라고 하며 새로 만든 프로세스를 자식이라고 합니다. 첫번째 프로세스를 제외한 나머지 모든 프로세스에는 부모가 있습니다. 부모 프로세스가 자식 프로세스보다 먼저 종료되면 커널은 고아가 된 자식 프로세스를 init 프로세스에 입약시킵니다.

프로세스가 종료되면 시스템에서 바로 제거되지 않고 프로세스 일부를 메모리에 계속 유지해서 자식 프로세스가 종료될 때 부모 프로세스가 상태를 검사할 수 있도록 합니다. 이를 '종료된 프로세스를 기다린다'고 표현합니다. 부모 프로세스가 종료된 자식 프로세스를 기다렸다면 자식 프로세스는 완전히 종료됩니다. 하지만 프로세스가 종료되었는데 기다리는 부모 프로세스가 없다면 이때 좀비가 탄생합니다. 일상적으로 init 프로세스는 자기에게 딸린 자식 모두를 기다려서 새로 입양된 프로세스가 영원히 좀비로 남지 않도록 보살핍니다.

0개의 댓글