본 글은 The Linux Kernel 을 정리한 것이며, 출처를 밝히지 않은 모든 이미지는 원글에 속한 것입니다.


컴퓨터 언어

  • 어셈블리어 CPU가 메모리에서 가져와 실행하는 가장 작은 단위의 명령어
    • 레지스터와 데이터에 대한 연산을 명시적으로 다루며, CPU 아키텍처마다 문법이 다름
    • 예를 들어, 인텔 80486 CPU에서 16진수 0x89E5는 ESP 레지스터의 내용을 EBP 레지스터로 복사하라는 의미
  • 어셈블러(Assembler) 어셈블리어로 작성된 코드를 기계어 코드로 변환하는 프로그램
    • nasm: interl, AT&T 문법 모두 지원
    • mlps: mlps 문법
    • intel: intel 문법
    • gcc: AT&T 문법 (컴파일러이지만, 소스코드를 바로 기계어 코드로 변환해줌)
    • 반대로 기계어 코드를 어셈블리어로 바꿔주는 것을 디스어셈블(Disassemble)이라고 함
  • 리눅스 커널의 소스코드는 대부분 C로 작성되었으며, 효율성을 위해 어셈블리어로 작성된 부분이 극히 일부분 존재
  • 고수준 언어의 필요성
    • 어셈블리어로 프로그램을 작성할 경우, 어렵고, 시간이 오래 걸리며, 오류를 범하기 쉬움
    • 특정 프로세서에만 국한되므로 이식성이 없음
    • C언어 같은 고수준 언어로 작성할 경우, 프로그램의 동작을 논리적인 자료와 알고리즘으로 표현 가능
  • 컴파일러(Compiler) 인간이 읽기 쉬운 문법으로 작성된 코드를 어셈블리어로 변환하는 프로그램
    • 반대로 어셈블리어로 작성된 것을 소스코드로 바꿔주는 것을 디컴파일(Decompile)이라고 함
  • C/C++ 같은 native 언어는 컴파일러가 즉시 소스코드를 기계어 코드로 변환해주지만, 파이썬이나 자바, C# 과 같은 가상 언어들은 가상 머신에서 실행되는 언어로 변환되어야 함. 따라서, 가상 머신만 있다면 운영체제 종류에 상관없이 가상 언어들로 작성된 프로그램은 실행 가능
  • 링커(Linker) 여러 개의 오브젝트 모듈과 라이브러리를 연결하여 하나의 프로그램을 만들어내는 프로그램
    • 오브젝트 모듈: 어셈블러나 컴파일러가 만들어 낸 기계어 코드 결과물로, 기계어 코드와 자료, 그리고 링커가 다른 모듈을 합치는데 필요한 정보를 포함
    • 리눅스 커널은 많은 종류의 오브젝트 모듈들을 링크하여 만들어진 거대한 프로그램
    • 리눅스 바이너리에서 링커의 동작 과정: 정적 라이브러리는 링커에 의해 하나의 파일에 함수 내용이 모두 포함되지만, 동적 라이브러리를 링킹하면 런타임(실행 환경)에서 필요한 정보(오프셋, 심볼명 등)만 포함되고, 실제 함수 내용은 이미 커널 메모리에 있다고 가정. 이후 로더에 의해 실행될 때 라이브러리 베이스 주소를 계산하고 함수가 호출되는 순간 메모리 주소를 계산

요즘은 컴파일러가 어셈블러의 역할까지 함

운영체제

  • 사용자가 응용 프로그램을 실행할 수 있도록 해주는 시스템 프로그램들의 집합
  • 운영체제는 하드웨어를 추상화하여 사용자에게 모든 기능들을 하나로 모아서 장치 종류에 상관없이 일관된 모습으로 제공
  • 하드웨어를 관리하는 부분(서브시스템)은 크게 4가지
    • 메모리 관리(Memory Management) 메모리 자원 추상화 인터페이스 (캐시, RAM 등)
    • 디바이스 드라이버(Device Driver) 주변장치 추상화 인터페이스
    • 파일 시스템(File System) 저장 공간 추상화 인터페이스, 하나의 장치에 대응하는 파일이 존재 (디스크 등)
    • 프로세스(Process) 위 3개와 CPU 자원을 사용하는 상위 계층의 인터페이스

커널 자료구조

  • 리눅스 커널은 시스템의 현재 상태에 대해 많은 정보를 가지고 있어야 하며, 시스템 내부에서는 어떤 사건이 발생하면 현재 상태에 변화를 반영하기 위해 자료구조를 변경
    • 예를 들어, 한 사용자가 시스템에 로그인하면, 새로운 프로세스가 생성되는데, 커널은 이 프로세스를 나타내는 자료구조를 생성하고, 이를 시스템 내의 프로세스를 나타내는 모든 자료구조와 연결해야 함
  • 자료구조의 대부분은 실제 메인 메모리에 존재하는 것이며, 커널과 커널의 서브시스템만이 접근 가능
    • 자료구조는 데이터와 포인터를 포함하며, 이 포인터는 다른 자료구조나 루틴(함수)을 참조
  • 연결 리스트(Linked List) 탐색은 선형 시간 O(N), 삽입/삭제는 상수 시간 O(1)

https://www.faceprep.in/data-structures/linked-list-introduction/

  • 해시 테이블(Hash Table) 탐색은 상수 시간 O(1), 삽입/삭제는 구조에 따라 다르며 최악의 경우 선형 시간 O(N)
    • 자료구조에 대한 포인터의 배열로, 인덱스는 자료구조의 내용으로부터 만들어지며 해시 함수(hash function)로 계산
    • 자주 사용하는 자료구조로의 접근 속도를 높이기 위해 사용 e.g., 캐시(cache)

https://subscription.packtpub.com/book/application_development/9781788833738/4/ch04lvl1sec28/hash-tables

  • 해시 충돌(hash collision) 해시 테이블의 크기는 제한적이므로, 해시 함수의 결과가 동일한 경우가 존재
    • 해시 충돌을 최대한 줄이기 위한 해시 테이블 구조가 2가지 있음
  • Separate Chaining 버켓은 자료구조의 리스트 헤드를 저장
    • 삽입 시 리스트의 끝에 추가
    • 리스트 노드를 따라가며 원하는 자료구조를 탐색 (충돌이 자주 발생하면 리스트 탐색 시간이 길어짐)
    • 해시 테이블의 크기를 키울 때, 버켓의 개수를 늘려서 리스트를 나누어 저장

Separate Chaining

  •  Open Addressing 버켓에 자료구조의 포인터가 아닌, "키, 값"으로 자료구조 자체를 저장
    • 리스트 포인터가 없으므로 추가 메모리 사용이 없음
    • Linear Probing 충돌이 발생한 위치에서 다음 빈 자리까지 선형 탐색. 데이터 삭제 시 빈 공간이 생겨서 다음 충돌에 탐색이 막히는 것을 막기 위해 더미값을 넣어 탐색이 끊기지 않도록 함
    • 해시 테이블의 크기를 키울 때, 버켓의 개수를 늘림

https://www.geeksforgeeks.org/implementing-hash-table-open-addressing-linear-probing-cpp/

추상 인터페이스

  • 인터페이스(interface) 특정 방법으로 동작하는 루틴과 자료구조의 모음
    • 예를 들어, 모든 네트워크 디바이스 드라이버는 특정 자료구조를 이용하여 정해진 루틴들을 커널에게 제공해야 함.
  • 하드웨어 장치마다 다른 코드로 된 하위 계층에서 제공하는 인터페이스(또는 서비스)를 사용하는 코드 계층이 존재
    • 장치마다 고유한 코드는 표준 인터페이스를 제공하여 일반적인 코드 계층을 지원
    • e.g., 네트워크 계층
  • 보통 하위 계층은 시스템 부팅 과정에서 자신을 상위 계층에 등록하는데, 이 과정은 어떤 연결 리스트에 자료구조를 추가하는 일을 포함
    • 예를 들어, 커널에 존재하는 각 파일 시스템은 부팅할 때 자신을 커널에 등록하며, 모듈을 사용하는 경우에는 처음으로 그 파일 시스템이 사용될 때 등록
  • 등록된 자료구조가 함수를 가지고 있는 경우, 포인터는 특정 작업을 수행하는 소프트웨어 함수의 주소
    • 예를 들어, 각 파일 시스템이 등록할 때 리눅스 커널에 넘겨주는 자료구조에는 파일 시스템이 마운트될 때마다 불리는 파일 시스템에 고유한 루틴의 주소가 있음

 

+ Recent posts