운영체제는 컴퓨터 하드웨어 바로 위에 설치되어 사용자 및 다른 모든 소프트웨어와 하드웨어를 연결하는 소프트웨어 계층이다.

그리고 컴퓨터 시스템의 자원을 효율적으로 관리하는데, 이때 자원은 크게 물리적인 자원과 추상적인 자원으로 구분할 수 있다.

  • 물리적인 자원: CPU, 메모리, 디스크, 터미널, 네트워크 등 시스템을 구성하는 요소 및 주변 장치
  • 추상적인 자원:
    • 물리적 자원을 운영체제가 관리하기 위해 추상화 시킨 객체 ex) 태스크(CPU 추상화), 세그먼트와 페이지(메모리 추상화), 파일(디스크 추상화), 통신 프로토콜과 패킷(네트워크 추상화)
    • 물리적인 자원에 대응되지 않으면서 추상적인 객체로만 존재하는 자원 ex) 보안 및 사용자 ID에 따른 접근 제어

운영체제는 일반적으로 커널 뿐만 아니라 각종 주변 시스템 프로그램을 나타내는 개념이지만, 좁은 의미로는 운영체제의 핵심 부분으로 메모리에 상주하는 커널을 의미하기도 한다.

 

커널은 이러한 자원들을 관리하는 여러 관리자들로 구성되어 있다.

CS746G. Kernel Subsystem Overview

  • 태스크 관리자: 태스크의 생성, 실행, 상태 전이, 스케줄링, 시그널 처리, 프로세스 간 통신 등
  • 메모리 관리자: 물리 메모리 관리, 가상 메모리 관리, 그리고 이들을 위한 세그멘테이션, 페이징, 페이지 폴트 처리 등
  • 파일 시스템: 파일의 생성, 접근 제어, inode 관리, 디렉터리 관리, 수퍼 블록 관리 등
  • 네트워크 관리자: 소켓 인터페이스, TCP/IP 와 같은 통신 프로토콜 등의 서비스 제공
  • 디바이스 드라이버: 디스크나 터미널, CD, 네트워크 카드 등과 같은 주변 장치를 구동하는 드라이버들로 구성된다.

 

리눅스 소스와 각 디렉터리의 내용을 조금 더 살펴보자면,

linux kernel source

 

# kernel

커널 핵심 코드를 포함하는 디렉터리로, 태스크 관리자가 구현된 디렉터리이다. 태스크의 생성과 소멸, 프로그램의 실행, 스케줄링, 시그널 처리 등의 기능이 구현되어 있다.

 

# arch

아키텍쳐 종속적인 코드를 포함하는 디렉터리로, CPU 타입에 따라 하위 디렉터리로 다시 구분된다. /arch/${ARCH} 아래에도 여러 디렉터리가 있다.

  • /boot: 시스템 부팅 시 사용하는 부트스트랩 코드 구현
  • /kernel: 태스크 관리자 중에서 문맥 교환이나 스레드 관리 등 구현
  • /mm: 메모리 관리자 중에서 페이지 폴트 처리 등의 하드웨어 종속적인 부분 구현
  • /lib: 커널이 사용하는 라이브러리 함수 구현
  • /math-emu: Floating Point Unit에 대한 에뮬레이터 구현

 

# fs

리눅스에서 지원하는 다양한 파일 시스템 및 파일 시스템 콜이 구현된 디렉터리로, 다양한 파일 시스템을 사용자가 일관된 인터페이스로 접근할 수 있도록 하는 가상 파일 시스템도 이 디렉터리에 존재한다.

 

# mm

메모리 관리자가 구현된 디렉터리로, 물리 메모리 관리, 가상 메모리 관리, 태스크마다 할당되는 메모리 객체 관리 등의 기능이 구현되어 있다.

 

# driver

리눅스에서 지원하는 디바이스 드라이버가 구현된 디렉터리로, 디스크/터미널/네트워크 카드 등 주변 장치를 추상화시키고 관리한다. 

  • 블록 디바이스 드라이버: 파일 시스템을 통해 접근
  • 문자 디바이스 드라이버: 사용자 수준 응용 프로그램이 장치 파일을 통해 직접 접근
  • 네트워크 디바이스 드라이버: TCP/IP를 통해 접근

 

# net

리눅스가 지원하는 통신 프로토콜이 구현된 디렉터리로, 리눅스 커널 소스 중 상당히 많은 양을 차지한다. 

 

# ipc

리눅스 커널이 지원하는 message passing, shared memory, semaphore 등 프로세스 간 통신 기능을 구현한 디렉터리이다.

 

# init

커널 초기화 부분, 즉 커널의 메인 시작함수가 구현된 디렉터리이다.

하드웨어 종속적인 초기화가 /arch/${ARCH}/kernel 아해의 head.s 와 mics.c에서 이루어진 후,

/init 아래에 구현되어 있는 start_kernel() 으로 커널 전역적인 초기화를 수행한다.

 

# include

리눅스 커널이 사용하는 헤더 파일들이 구현된 디렉터리로,

헤더 파일들 중에서 하드웨어 독립적인 부분은 /include/linux 아래에 정의되어 있으며

하드웨어 종속적인 부분은 /include/asm-${ARCH} 디렉터리 아래에 정의되어 있다.

 

# others

  • documentation: 리눅스 커널 및 명령어들에 대한 자세한 문서 파일 등
  • lib: 커널 라이브러리 함수들이 구현
  • scripts: 커널 구성 및 컴파일 시 이용되는 스크립트 정의

 


리눅스 커널은 위 커널 소스들을 gcc로 컴파일 해서 만들 수 있으며, 

리눅스 커널을 만드는 과정은 크게 kernel configuration, kernel compile, kernel installation 세 단계로 이루어진다.

  • kernel configuration: 새로 만들어질 리눅스 커널에게 현재 시스템에 존재하는 하드웨어 특성, 커널 구성 요소 , 네트워크 특성 등의 정보를 알려주는 과정이다. 커널 구성 단계에서 사용자는 모듈 사용 여부와 시스템 정보, 그리고 시스템에 존재하는 디바이스들의 특성 등에 대한 질문에 답하게 된다. 이러한 선택 사항은 /include/linux/autoconf.h 와 .config 파일에 저장된다. 
    • make config
    • make menuconfig
    • make xconfig
    • 커널 구성은 위 명령어 들을 사용해서 할 수 있으며 텍스트 기반 설정인지 gui로 제공되는지 등에 따라 명령어가 나뉜다.
  • kernel compile: 커널 소스파일을 이용하여 실행가능한 커널을 만들게 되는데, 컴파일 과정에서 아까 만들었던 /include/linux/autoconf.h 와 .config 파일이 사용되며, 커널 컴파일이 끝나면 새로운 커널이 /kernel/arch/${ARCH}/boot 디렉터리에 생성된다.
    • make bzImage
    • make zImage
    • 커널 버전 2.6 이후부터는 그냥 `make` 명령어만 사용해도 된다. 
    • bzImage vs zImage? bzImage는 Big zImage를 의미한다. zImage는 최대 크기 512k만 가능한데, 커널이 컴파일 후 512k 크기를 넘어가면 bzImage로 만들어야 한다. 
  • kernel installation: 생성된 커널로 시스템이 부팅될 수 있도록 만드는 과정으로, 생성된 커널 이미지를 루트 파일 시스템으로 복사 후 모듈 설치 및 부트 로더 수정을 통해 이루어진다. 이로써 모듈로 구성된 커널 내부 구성 요소를 알려주고, 이후 그 구성 요소들이 사용될 때 자동으로 커널에 적재될 수 있도록 설정하는 단계이다. 
    • make modules: 커널 환경설정에서 모듈로 설정한 기능들을 컴파일
    • make modules_install: 컴파일 된 모듈을 /lib/modules 아래 설치
    • make install: 커널 설치

 

커널 소스 컴파일 시 파일 생성 단계는 아래 게시글에 잘 설명되어 있다.

http://www.iamroot.org/xe/index.php?mid=Kernel&document_srl=24595

그림의 내용을 간단히 정리하자면

(1, 3, 6) 커널 소스들을 컴파일하여 .o(오브젝트 파일) .a(라이브러리 파일)을 생성하고, 이들을 링킹하여 vmlinux 파일을 생성한다.

(2) vmlinux 파일에서 .note와 .comment 섹션을 삭제하고 재배치 정보와 심볼 정보를 삭제한 뒤 바이너리 포맷의 파일(Image)을 출력한다.

(4) 이를 gzip 압축 알고리즘을 이용하여 piggy.gz를 생성한다.

(5) 여기서 디버깅 정보 등을 삭제하여 piggy.o를 생성한다.

 

(3, 6, 7) 다음으로 커널의 압축을 해제시켜주는 코드 (head.S, misc.c)를 커널 앞부분에 덧붙여 링커를 통해 bvmlinux 또는 vmlinux 파일을 생성한다. 

(8) bvmlinux 또는 vmlinux가 bzImage 또는 zImage가 되는 것이다. 만약 물리 메모리의 1M 위치에 로드될 수 있는 작은 크기의 커널인 경우 zImage 형태로 파일을 생성하며, 그렇지 않은 경우 bzImage 형태로 파일을 생성한다.

 

 

이러한 단계를 지나 새로운 커널을 만든 후, 모듈로 선택한 커널 구성 요소를 컴파일하고 설치하면 새로 만들어진 리눅스 커널을 이용해 시스템을 부팅할 수 있다.

'CS > OS + Linux' 카테고리의 다른 글

[운영체제] 메모리 관리  (0) 2022.07.19

프로그램을 실행시키기 위해서는 프로그램과 접근 데이터를 메인 메모리에 올려야한다.

컴퓨터 시스템은 시스템 실행 중에 여러 프로세스를 메모리에 유지할 수 있다. 

아래 그림과 같이 메모리는 각 프로세스가 사용할 뿐만 아니라 커널 자체도 메모리를 사용한다.

 

 

메모리의 통계 정보

시스템의 총 메모리의 양과 사용중인 메모리의 양은 `free` 명령어를 통해 확인할 수 있다. (단위는 Kbyte)

free

  • total: 시스템에 탑재된 전체 메모리 용량
  • free: 이용하지 않는 메모리
  • buff/cache: 버퍼 캐시 또는 페이지 캐시가 이용하는 메모리. 시스템의 free 값이 부족하면 커널이 해제한다.
  • available: 실질적으로 사용 가능한 메모리 = (free) + (buff/cache)
  • swap
    • total: 설정된 스왑 총 크기
    • used: 사용중인 스왑 크기
    • free: 사용되지 않은 스왑 크기

htop을 사용하면 실시간으로 좀 더 눈에 잘 들어오는 형태로 모니터링 할 수 있다.

htop

 

메모리 사용량이 늘어나면 free 의 크기가 줄어든다.

이러한 상태가 되면 메모리 관리 시스템은 커널 내부의 해제 가능한 메모리 영역을 해제한다.

이후에도 메모리 사용량이 계속 증하가면 시스템은 메모리가 부족해 동작할 수 없는 메모리 부족 상태가 된다 = Out of Memory

이러한 경우 메모리 관리 시스템에서는 적절한 프로세스를 강제 종료(kill) 시켜 메모리 영역을 해제한다.

 

 

메모리 주소의 할당

커널이 프로세스에 메모리를 할당하는 경우는 크게 두 가지 타이밍에 벌어진다.

1. 프로세스를 생성할 때

2. 프로세스를 생성한 뒤 추가로 동적 메모리를 할당할때

 

좀 더 자세히 살펴보자.

[운영체제 10판] 9장 메인 메모리

c.f. 파이썬의 경우 인터프리터 언어라서 조금 다를 수 있을 것 같아서 찾아보니,
https://stackoverflow.com/questions/19791353/linking-and-loading-in-interpreted-languages
- 파이썬은 컴파일 과정이 없다.
- 실행 파일(예: /usr/bin/python)이 실제로 실행되는 프로그램이고, 실행할 스크립트를 읽으며 한 줄씩 실행하게 된다.
- 그 과정에서 다른 파일이나 모듈(예: /usr/python/lib/math.py)에 대한 참조를 만날 수 있으며 그 때, 이를 읽고 해석한다.
- 바이너리 라이브러리들은 런타임에 코드에서 요청 시, 해당 라이브러리를 로드한 다음 사용할 수 있다.
- 파일을 처음 실행할 때 바이트코드(.pyc 파일)로 컴파일되고, 다음에 모듈을 가져오거나 실행할 때 코드 실행이 향상된다.

 

물리 주소를 논리 주소로 변환하는 방법

[운영체제 10판] 9장 메인 메모리

  • Dynamic Loading: 프로세스가 실행되기 위해 프로세스 전체가 메모리에 미리 올라와 있을 필요는 없다. 메모리 공간을 더 효율적으로 이용하기 위해 동적 적재를 할 수 있다. 동적 적재에서 각 루틴은 실제 호출 전까지 메모리에 올라오지 않고 재배치 가능한 상태로 디스크에 대기하고 있다. 동적 적재는 운영 체제로부터 특별한 지원이 필요없으며, 사용자 자신이 프로그램의 설계를 책임져야 한다. 운영체제는 동적 적재를 구현하는 라이브러리 루틴을 제공해 줄 수는 있다.
    • 예: main 프로그램이 메모리에 올라와 실행된다. 이 루틴이 다른 루틴을 호출하게 되면 호출한 루틴이 이미 메모리에 적재됐는지 조사한다. 만약 적재되어 있지 않으면 재배치 가능 연결 적재기(relocatable linking loader)가 불려 요구된 루틴을 메모리로 가져오고, 이러한 변화를 테이블에 기록해둔다. 그 후 CPU 제어는 중단되었던 루틴으로 보내진다.
  • Dynamic Linking and Shared Library: 동적 연결 라이브러리(DLL)은 사용자 프로그램이 실행될 때, 사용자 프로그램에 연결되는 시스템 라이브러리이다. 동적 연결에서는 linking이 실행 시기까지 미루어진다. 동적 연결은 주로 표준 C 언어 라이브러리와 같은 시스템 라이브러리에 사용된다. DLL은 여러 프로세스 간 공유될 수 있어 메일 메모리에 DLL 인스턴스가 하나만 있을 수 있으며 공유 라이브러리라고도 한다. 프로그램이 동적 라이브러리에 있는 루틴을 참조하면 로더는 DLL을 찾아 필요한 경우 메모리에 적재한다. 그런 다음 동적 라이브러리의 함수를 참조하는 주소를 DLL이 저장된 메모리의 위치로 조정한다. 동적 로딩과 달리 동적 연결과 공유 라이브러리는 일반적으로 운영체제의 도움이 필요하다. 메모리에 있는 프로세스들이 각자의 공간을 자기만 엑세스 할 수 있도록 보호된다면 운영체제만이 메모리 공간에 루틴이 있는지 검사해 줄 수 있고, 운영체제만이 여러 프로세스가 같은 메모리 주소를 공용할 수 있도록 해줄 수 있다. 
    • c.f. 정적 연결(static linking): 라이브러리가 프로그램의 이진 프로그램 이미지에 끼어 들어간다.

동적 메모리를 할당할 때, 추가적으로 메모리가 필요한 경우 프로세스는 커널에 메모리 확보용 시스템 콜을 호출하여 메모리 할당을 요청한다. 커널은 메모리 할당 요청을 받으면 필요한 사이즈를 빈 메모리 영역으로부터 잘라내, 그 영역의 시작 주소값을 반환한다.

 

논리 주소를 물리 주소로 변환하는 방법

가상 주소를 물리 주소로 변환하는 과정은 커널 내부에 보관되어 있는 `페이지 테이블`을 사용한다.

가상 메모리는 전체 메모리를 페이지 단위로 나누어 관리하고 있으므로 변환은 페이지 단위로 이루어진다.

(c.f. 물리 메모리는 프레임 단위로 불리는 동일한 크기의 블록으로 나누어진다.)

이때, 페이지 사이즈는 CPU 구조에 따라 다른다. 흔히 사용하는 x86_64의 페이지 사이즈는 4kb이다.

리눅스 시스템에서 페이지 크기를 알아내고 싶다면 `getconf PAGESIZE` 명령을 입력하면 된다. (단위는 바이트)

 

CPU 에서 나오는 모든 주소는 (페이지 번호) + (페이지 오프셋) 두 개의 부분으로 나누어진다.

  • 페이지 번호: 페이지 테이블 접근 시 사용된다. 페이지 테이블은 물리 메모리의 각 프레임 시작 주소를 저장한다.
  • 페이지 오프셋: 참조되는 프레임 안에서의 위치를 나타낸다.

만약 페이지 테이블에 매핑되지 않은 영역대의 가상 주소에 접근하게 되면 CPU 에는 `페이지 폴트` 인터럽트가 발생한다.

페이지 폴트에 의해 현재 실행 중인 명령이 중단되고, 커널 내의 `페이지 폴트 핸들러` 라는 인터럽트 핸들러가 동작하게 된다.

커널은 프로세스로부터 메모리 접근이 잘못되었다는 내용을 페이지 폴트 핸들러에 알려준 후, `SIGSEGV` 시그널을 프로세스에 통지한다. (이 시그널을 받은 프로세스는 강제 종료된다.)

 

실제 메모리 할당(Linux)

## C

C언어에서는 표준 라이브러리에 있는 `malloc()` 함수가 메모리 확보 함수이다.

리눅스에서는 내부적으로 `malloc()` 함수에서 `mmap()` 또는 `brk()` 함수를 호출하여 메모리 할당을 구현한다.

## Python

파이썬같이 직접 메모리 관리를 하지 않는 스크립트 언어의 오브젝트 생성에도

최종적으로는 내부에서 C언어의 malloc 함수를 사용하고 있다.

 

mmap 함수는 페이지 단위로 메모리를 확보하지만, malloc 함수는 바이트 단위로 메모리를 확보한다.

즉, glibc에서는 사전에 mmap 시스템 콜을 이용하여 메모리 풀을 확보한 뒤, 프로그램에서 malloc이 호출되면 메모리 풀로부터 필요한 양을 바이트 단위로 잘라내어 반환하는 처리를 한다. 풀로 만들어 둔 메모리에 더 이상 빈 공간이 없으면 다시 mmap 을 호출하여 새로운 메모리 영역을 확보한다.

glibc의 메모리 풀

사용하는 메모리 양을 체크하여 알려주는 기능의 프로그램들이 있으나, 그 프로그램이 알려주는 사용량과 실제 리눅스에서 사용하는 메모리의 양이 서로 다른 경우가 매우 많다. (일반적으로 리눅스에서 프로세스가 사용하는 메모리 양이 더 많음)

왜냐하면 리눅스에서 측정한 메모리 양은 프로세스가 생성될 때, mmap 함수를 호출했을 때 할당한 메모리 전부를 더한 값을 나타내고, 프로그램이 체크한 메모리 사용량은 malloc 함수 등으로 획득한 바이트 수의 총합을 나타내기 때문에 서로 다르게 표시되기 때문이다. 이러한 프로그램이 체크한 메모리 사용량이 정확히 무엇을 나타내는지는 프로그램에 따라 다르므로 확인해봐야 한다.

 

 

'CS > OS + Linux' 카테고리의 다른 글

[운영체제] 리눅스 커널이란  (0) 2023.01.10

+ Recent posts