Linux System Programming [1]: Introduction to Core Concepts

Drawing

시스템 프로그래밍

시스템 프로그래밍의 세가지 가장 중요한 키워드는 시스템 콜, C라이브러리, C컴파일러 이다.

  • 시스템 콜 - 운영체제에 리솟스나 서비스를 요청하려고 사용자 영역(텍스트 편집기나 게임)에서 시작해서 커널 내부로 들어가는 함수 호출. 64비트 리눅스 운영체제 기준 약 300여개의 시스템콜이 있다. 보안과 안정성의 이유로 사용자 프로그램은 커널 코드를 직접 실행하거나 커널 내부 데이터를 실행할 수 없다.
  • C 라이브러리 - 유닉스 어플리케이션의 핵심, 최신 리눅스 시스템에서는 GNU C Library인 glibc가 제공된다.
  • C 컴파일러 - gcc라는 리눅스 c 컴팡일러가 있다.

API 와 ABI

  • API - 소프트웨어의 소스코드 레벨에서 서로 인터페이스를 하는 방식을 정의, API의 표준 인터페이스는 함수이며 상위 레벨의 소프트웨어에서 하위 레벨의 소프트웨어의 함수를 호출할 수 있다.
  • ABI - API가 소스 코드 수준의 인터페이스를 정의한다면 ABI는 특정 아키텍쳐 간에서 동작하는 소프트웨어 간의 바이너리 인터페이스를 정의한다. ABI는 바이너리의 호환성을 보장하며 이는 오브젝트 코드를 다시 컴파일하는 수고 없이 같은 ABI를 진원하는 시스템이라면 동일한 기능을 수행하도록 보장한다. 예를 들어, ABI는 함수가 실행되는 방식, 인자가 전달되는 방식, 레지스터에 값을 저장하는 방식 등(calling convention)를 정의한다. 따라서 컴파일러나 툴체인에 의해서 강제되므로 일반적인 프로그래머는 알고 있을 필요가 없지만, 이를 개발 할 필요가 있는 시스템 프로그래머는 ABI를 알아야 함.

리눅스 프로그래밍의 개념

리눅스를 포함한 유닉스 시스템은 상호간의 추상화와 인터페이스를 제공한다. 파일과 프로세스, 파이프와 소켓을 다루기 위한 인터페이스 등 이러한 것들이 유닉스의 핵심이다.

파일과 파일시스템

파일에 접근하기 위해서는 파일의 정보를 갖고 있는 메타이데이와 연결된 파일 디스크립터를 통해 참조할 수 있다. 리눅스 커널 내에서 파일 디스크립터(fd)는 정수형으로 표현되며, 응용프로그램이 파일에 접근할 때 직접 사용한다. 리눅스 시스템 프로그래밍의 상당부분은 바로 이 파일 디스크립터를 열고 조작하고 닫는 작업이다.

  • 파일 - 연속적으로 나열된 바이트 배열에 저장된 데이터
  • 오프셋(offset) - 파일내부의 특정 위치, 열려있는 파일의 오프셋은 커널이 유지하는 메타데이터의 핵심, 오프셋의 최대 크기는 시스템에서 저장할 수 있는 한 파일의 최대 크기와 같으며 최신 리눅스 시스템에서는 64비트 값이다.
  • 아이노드(inode) - 사용자는 파일의 이름을 통해 파일에 접근하지만 실제로 파일은 inode number라고 하는 파일시스템 내에서 고유한 정수 값으로 참조된다. inode 는 inode number를 포함하여 소유자, 접근날자, 파일 크기 등의 메타데이터를 저장한다.
  • 디렉토리 - 파일의 이름과 mapping되는 inode number를 저장하는 파일의 한 종류, 따라서 디렉토리도 자체의 inode가 있고 이를 통해 계층적 구조를 나타낼 수 있음. (ex, /home/rocky/Document/happy.txt)
  • 절대경로/상대경로 - root에서부터의 경로/working directory에서부터의 경로
  • 하드링크 - 동일한 inode에 여러 이름을 mapping하는 것. 파일의 삭제 시 모든 하드링크가 삭제될 때 까지 파일을 삭제하지 못하도록 보장하기 위해 각 inode는 파일시스템 내부에 링크 카운터를 두어 이를 추적한다. 링크 카운터가 0이 되면 실제로 삭제
  • 심볼릭 링크 - 링크로 연결된 파일의 완전한 경로 이름을 포함하는 독자적인 inode와 데이터를 갖는다.

  • 파일시스템 - 파일시스템은 파일과 디렉토리를 정형적이고 유효한 계층 구조 안에 모아놓은 것. 파일시스템을 파일과 디렉토리라는 전역 네임스페이스 안에 개별적으로 추가하거나 제거할 수 있다. 이러한 과정을 mount/unmount라고 한다. 네임스페이스에저 정해진 장소를 마운트 포인트라고 하며, 개별 파일시스템은 여기에 마운트 된다. 마운트 된 파일시스템의 루트 디렉토리는 마운트 포인트에서 접근 가능하다. 예를 들어, 이동식 USB 메모리를 /media/extusb 에 마운트하면 USB메모리에 있는 파일시스템 루트는 마운트 포인트인 /media/extusb 에서 접근 가능하다. 리눅스 시스템에서는 루트 파일시스템 이 “/”에 존재하며 그 외 다른 파일시스템을 다른 마운트 포린트에 마운트 하는 과정은 필요에 따라 선택한다
프로세스

리눅스에서의 프로세스는 작은 범위에서는 실행중인 오브젝트 코드, 넓게는 단순한 오브젝트 코드를 넘어 데이터, 리소스, 상태, 가상화 된 컴퓨터를 포함한다.

프로세스는 가상화를 위한 추상 개념이다. 선점형 멀티태스킹과 가상 메모리를 지원하는 리눅스 커널은 가상화 된 프로세서와 가상화 된 메모리를 프로세스에 제공한다. 커널은 동작중인 프로세스가 시스템 프로세서를 공유하도록 빈틈없고 투명하게 프로세스를 선점하고 스케쥴링한다.

  • 스레드 - 각 프로세스는 스레드를 하나 이상 포함한다. 스레드는 프로세스 내부에서 실행하는 활동의 단위이며, 코드를 실행하고 프로세스 동작 상태를 유지하는 추상 개념이다. 스레드에 대한 자세한 내용은 7장에서 다루도록 한다.
  • 프로세스의 계층 구조 - 각각의 프로세스는 PID(프로세스 ID)라고 하는 고유한 양수 값으로 구분된다. 리눅스에서 프로세스는 프로세서 트리라는 엄격한 계층 구조를 형성한다. 보통 init program 이라는 첫 번째 프로세스가 루트가 되고 fork() 시스템콜을 통해 프로세스를 복제 하여 자식프로세스로 만드는 과정을 통해 계층 트리를 형성한다.
사용자(User)와 그룹(Group)

리눅스에서 권한은 사용자(User)와 그룹(Group)의 형태로 제공된다.

  • 사용자(User) - uid(user id)는 고유한 양수의 값을 가지면 사용자를 구분한다. 따라서 프로세스마다 프로세스의 사용자가 누구인지 파악하기 위해 uid가 존재한다. 실제 사용자 이름(문자) 와 uid는 역시 파일의 형태로 /etc/passwd 파일에 저장되어 있다. uid 0은 “root”라는 특수한 사용자를 가르킨다. root 사용자는 특별한 권한이 있으며, 시스템에서 거의 모든 작업을 실행할 수 있다.
  • 그룹(Group) - 특정 파일이나 디렉토리를 특정 권한이 있는 사용자”들” 만 사용하기 위해 해당 사용자들에게 권한을 부여하여 그룹 단위로 묶어 관리할 수 있다. 모든 사용자는 하나 이상의 그룹에 속해있다. 주 그룹이나 로그인 그룹은 etc/passwd에 지정하며 추가 그룹은 ect/group 에 지정한다. 따라서 프로세스마다 대응하는 gid가 있다.
권한

파일마다 소유자, 소유자 그룹, 그리고 세 가지 접근 권한 비트가 있다. 이 비트는 소유자, 소유자 그룹, 그 외 모든 사용자가 파일을 읽고, 쓰고, 실행하는 능력을 기술한다. 세 가지 각각의 그룹마다 세 비트가 할당되어 총 9bit로 권한에 대한 정보를 표현하며, 이 역시 파일의 inode에 저장된다.

  • 권한 비트와 값
비트 8진수 텍스트 접근 권한
8 400 r - - - - - - - - 소유자 읽기
7 200 - w - - - - - - - 소유자 쓰기
6 100 - - x - - - - - - 소유자 실행
5 040 - - - r - - - - - 그룹 읽기
4 020 - - - - w - - - - 그룹 쓰기
3 010 - - - - - x - - - 그룹 실행
2 004 - - - - - - r - - 그 외 사용자 읽기
1 002 - - - - - - - w - 그 외 사용자 쓰기
0 001 - - - - - - - - x 그 외 사용자 실행
시그널(Signal)

시그널은 비동기식(Asynchronous) 단방향 알림 메커니즘이다. 커널에서 프로세스로, 프로세스에서 다른 프로세스로, 아니면 프로세스 자기 자신에게 시그널을 보낼 수 있다. 일반적으로 시그널은 segmentation fault나 사용자가 ctrl-c 를 누르는 경우와 같이 특정 이벤트가 발생했음을 알려준다. 리눅스 커널에는 약 30여개의 시그널이 존재한다.

프로세스간 통신(Inter Process Communication; IPC)

리눅스에서 지원하는 IPC메커니즘은 파이프, 네임드파이프, 세마포어, 메세지큐, 공유메모리, 뮤텍스 등이 있다.

에러처리

개인적으로 좀 더 자세히 다루고 싶고 중요한 내용이기에 새로운 포스팅으로 다루겠다.