1장 리눅스 개요
시작하면서
컴퓨터 시스템 계층의 정형적인 모델은 사용자 프로그램 > OS 이외 라이브러리 > OS 라이브러리 > 커널 > 하드웨어
예를 들어, 운용 관리자
라면 애플리케이션 외부 사양까지만 알면 되고, 애플리케이션 개발자
라면 라이브러리까지만 알면 된다고 하지만 실제로는 다른 계층과 얽혀 있어서 일부만 알아서는 해결할 수 없는 문제도 많다.
이 책을 통해 리눅스와 커널, 그리도 하드웨어가 상위 계층과 직접 연결된 부분을 배우고 이해한다.
커널, 하드웨어 등 저수준 계층이 원인인 문제 상황을 해석
성능을 의식한 코딩
시스템의 각종 통계 정보, 튜닝 파라미터 의미를 이해
1. 프로그램과 프로세스
리눅스에서는 다양한 프로그램이 동작한다.
1) 프로그램이란?
컴퓨터에서 동작하는 관련된 명령 및 데이터를 하나로 묶은 것이다.
Go 같이 컴파일러형 언어라면 소스 코드를 빌드해서 만들어진
실행파일
을 프로그램이라고 부름파이썬같이 스크립트 언어는
소스 코드 그 자체
가 프로그램이 됨프로세스보다 넓은 의미를 가진 개념
2) 프로세스
실행되어서 동작 중인 프로그램
때로는 동작 중인 프로세스를 프로그램이라고 부르기도 함
2. 커널
1) 직접 저장 장치 접근 문제
먼저 프로세스가 저장 장치에 직접 접근 가능한 시스템을 생각해보자.
이때 동시에 여러 프로세스가 장치를 제어하려고 하면 문제가 발생한다.
저장장치에서 데이터를 읽고 쓰려면 다음과 같은 두 가지 명령어를 호출해야 한다고 하자.
직접 접근 시나리오 (그림 01-01): 여러 프로세스(예: 프로세스 0, 1, 2)가 저장 장치(HDD, SSD 등)에 직접 접근하는 경우, 각 프로세스가 독립적으로 데이터를 읽고 쓰려 할 때 명령어 실행 순서가 섞여서 문제가 발생할 수 있다.
명령 A: 데이터를 읽거나 쓸 장소(주소)를 지정
명령 B: 지정된 장소에 데이터를 읽거나 쓰는 실제 동작 실행
예시 시나리오:
프로세스 0이 명령 A를 호출해 저장 장치의 “장소 0”을 지정하고, 이후 명령 B를 호출해 데이터를 쓰려고 함
동시에, 프로세스 1이 명령 A를 호출하여 “장소 1”을 지정하는 상황에서, 프로세스 0의 명령 B가 실행되면 원래 의도했던 “장소 0” 대신 “장소 1”에 데이터가 쓰여 기존 데이터가 손상될 수 있음
이로 인해 원래 데이터가 덮어쓰여져 손상될 수 있다.
문제점:
여러 프로세스가 동시에 하드웨어 자원(저장 장치)에 직접 접근하면서 데이터 충돌이나 의도치 않은 동작이 발생할 위험이 크다.
원래라면 접근 불가능이어야 할 프로그램이 장치에 접근 가능한 문제도 생길 수 있다.
잘못된 접근으로 인해 저장 장치가 심각한 손상을 입어 더 이상 사용할 수 없게(일명 brick 상태) 될 수 있다.
2) 커널의 역할과 필요성
커널은 하드웨어 도움을 받아 프로세스가 장치에 직접 접근할 수 없도록 한다.
구체적으로는 CPU에 내장된 모드 기능 사용
커널의 기본 개념:
커널
은 운영체제의 핵심 부분으로, 시스템 자원을 통합적으로 관리하며 하드웨어 접근을 제어한다.커널 모드: CPU가 커널 모드로 동작할 때는 어떠한 제약 없이 명령을 실행할 수 있다.
사용자 모드: 프로세스는 일반적으로 사용자 모드에서 동작하며, 하드웨어 직접 접근이 제한되어 있다.
간접 접근: 프로세스는
커널
을 통해 저장 장치와 같은 하드웨어 자원에 접근한다.
커널은 저장 장치를 포함한 모든 하드웨어 자원에 대해 직접 제어 권한을 가지고 있으며, 프로세스들은 커널에 요청(시스템 콜)을 보내어 간접적으로 자원에 접근한다.
안전성 보장: 커널이 프로세스의 요청을 중재하고, 접근 순서를 제어함으로써 데이터 손상, 충돌, 그리고 심각한 시스템 장애(예를 들어, 저장 장치가 brick되는 상황)를 예방할 수 있다.
자원 관리: 커널은 시스템 자원을 한 곳에서 통합적으로 관리하여, 프로세스 간의 충돌 없이 효율적으로 자원을 배한다.
주요 기능:
장치 제어 및 관리: 저장 장치, 네트워크, 그래픽 장치 등 모든 하드웨어에 대한 접근을 제어
시스템 자원 배분: CPU 스케줄링, 메모리 관리, I/O 요청 처리 등 모든 프로세스가 공유하는 자원을 효율적이고 안정적으로 관리
보안 및 안정성 강화: 잘못된 접근이나 의도치 않은 명령 실행으로부터 시스템 전체를 보호
3) 추가 배경지식 및 보충 설명
1. 운영체제의 모드 구분
커널 모드와 사용자 모드:
커널 모드: 시스템 전체에 영향을 줄 수 있는 모든 명령을 실행할 수 있는 특권 모드입니다.
사용자 모드: 제한된 명령만 실행 가능하며, 시스템 자원에 직접 접근하지 못합니다. 이러한 모드 구분을 통해 시스템의 안정성과 보안성이 유지됩니다.
2 시스템 콜 (System Call)
인터페이스 역할: 사용자 모드의 프로세스가 커널 모드로 전환하여 하드웨어 접근 등의 작업을 요청할 때 사용된다.
예시: 파일 열기(
open()
), 읽기(read()
), 쓰기(write()
), 프로세스 생성(fork()
) 등 시스템 콜은 커널이 관리하는 자원에 안전하게 접근할 수 있도록 중개자 역할을 한다.
3. 커널 아키텍처
모놀리딕 커널: 리눅스와 BSD 계열처럼 커널 내부에 다양한 기능(드라이버, 파일 시스템 등)이 통합되어 있는 구조
마이크로커널: 핵심 기능만을 커널에 남기고 나머지 기능은 사용자 공간에서 수행하는 구조로, 안정성과 모듈화를 추구한다.
두 모드의 사용: 실제로 x86_64 등 일부 아키텍처는 여러 CPU 모드를 지원하지만, 리눅스 커널은 주로 커널 모드와 사용자 모드 두 가지로 구분하여 사용한다.
4) 왜 커널이 중요한가?
안정성: 모든 프로세스가 안전하게 공유 자원을 사용할 수 있도록 관리하여, 데이터 손상 및 시스템 오류를 예방
보안성: 사용자 모드에서의 제한을 통해 악의적이거나 실수로 인한 시스템 전체의 치명적인 손상을 막는다.
효율성: 다양한 프로세스의 자원 접근을 중앙에서 조정하여 효율적인 자원 분배와 최적의 성능을 유지한다.
추상화 제공: 프로세스가 하드웨어의 세부 사항을 몰라도, 커널이 제공하는 일관된 인터페이스(시스템 콜)를 통해 하드웨어를 사용할 수 있도록 한다.
커널 아키텍처의 종류(모놀리딕 vs 마이크로커널)와 각 방식의 장단점도 고려해보면 운영체제 설계의 다양성을 이해할 수 있다.
3. 시스템 콜이란?
시스템 콜은 프로세스가 커널에게 특정 작업(예: 새로운 프로세스 생성, 메모리 할당, 파일 및 장치 조작 등)을 요청할 때 사용하는 인터페이스이다.
사용자 모드에서 실행 중인 프로세스는 직접 하드웨어나 중요한 시스템 자원에 접근할 수 없기 때문에, 커널
에 요청하여 필요한 처리를 수행한다.
동작 방식:
사용자 모드 → 시스템 콜 호출:
프로세스는 시스템 콜을 호출하여 커널에 요청을 보낸다.
예외 발생 및 모드 전환:
시스템 콜 호출 시 CPU는 예외(exception) 이벤트를 발생시키며, 이때 프로세스의 실행 모드가 사용자 모드에서 커널 모드로 전환된다.
커널의 요청 처리:
커널은 프로세스로부터 받은 요청이 올바른지(예: 메모리 범위 체크 등) 검증한 후, 해당 작업을 수행한다.
커널 모드 → 사용자 모드 복귀:
처리가 완료되면, 다시 사용자 모드로 돌아가서 프로세스가 계속 실행된다.
예시:
프로세스 생성/삭제
메모리 확보 및 해제
파일 시스템 조작
장치 제어
통신 처리
1) 시스템 콜이 필요한 이유
보안 및 안정성:
프로세스가 임의로 CPU 모드를 변경해 커널 모드로 전환할 수 없도록 막음으로써, 악의적인 행위나 실수로 인한 시스템 자원 손상 및 데이터 유출을 예방
만약 프로세스가 직접 커널 모드로 전환할 수 있다면, 사용자는 시스템 보호 기능이 무의미해지므로, 다른 사용자의 데이터에 부정적인 영향을 미칠 수 있다.
자원 관리:
시스템 콜을 통해 커널은 여러 프로세스의 요청을 중앙에서 검증하고, 자원을 효율적으로 관리할 수 있다.
이는 시스템 전체의 안정성과 효율성을 높이는 데 중요한 역할을 한다.
2) 추가로 알아야 할 사항 및 보충 지식
1. 시스템 콜의 인터페이스와 구현
API 형태:
일반적으로 라이브러리(예: C 표준 라이브러리)가 시스템 콜 인터페이스를 감싸 사용자에게 보다 편리한 함수를 제공한다.
직접 호출 불가:
프로세스는 직접 CPU 모드를 변경하여 커널 모드로 진입할 수 없으며, 반드시 시스템 콜을 통해서만 커널 기능에 접근할 수 있다.
2. 예외 처리와 커널 진입
예외(exception) 메커니즘:
시스템 콜 호출 시 발생하는 예외는 커널이 요청의 유효성을 검사하고, 잘못된 요청(예: 과도한 메모리 요청)일 경우 실패 처리를 하도록 돕는다.
모드 전환 비용:
사용자 모드에서 커널 모드로의 전환은 일정한 오버헤드를 발생시키는데, 시스템 성능 측면에서 이를 최소화하기 위한 최적화 기법들이 연구되고 있다.
3. 시스템 콜과 커널 개발
커널 개발과 디버깅:
시스템 콜의 동작 방식을 이해하는 것은 커널 모듈 개발이나 디버깅, 운영체제 내부 구조를 분석하는 데 큰 도움이 된다.
보안 강화:
시스템 콜 인터페이스를 강화하는 보안 기법(예: seccomp, 시스템 콜 필터링)을 활용해 악의적인 시스템 콜 실행을 방지할 수 있다.
3) Go 파일을 넣어주기 위해 만들며, 시스템 콜 호출 확인해 보기
프로세스가 어떤 시스템 콜을 호출하는 strace 명령어로 확인할 수 있다.
1) Go 언어 환경이 설치되어 있는지 확인
Go 버전 확인
만약 “command not found”라는 에러가 뜨면, Go가 설치되어 있지 않은 것이다.
Go 설치 우분투 20.04에서 간단히 설치하려면:
(버전에 따라
golang-go
대신golang
을 설치해야 할 수도 있다.)
2) hello.go
파일 생성 및 내용 작성
hello.go
파일 생성 및 내용 작성권한 문제가 없으므로, 아래와 같이 바로 파일을 생성할 수 있다.
터미널에서
touch
명령어로 빈 파일 생성그리고 원하는 편집기로 열어서 내용 작성:
혹은
등 원하는 에디터 사용.
예시 코드 (Hello World)
3) 빌드 및 실행
빌드
이 명령어를 입력하면 현재 디렉토리에
hello
(또는 Windows라면hello.exe
) 실행 파일이 생성된다.실행
결과로
Hello, Go!
가 출력되는지 확인nano
편집기를 종료하는 방법은 다음과 같다:Ctrl + X를 누릅니다.
변경사항이 있을 경우, “Save modified buffer?”라는 메시지가 표시된다.
Y 또는 y를 눌러 저장
N 또는 n을 눌러 저장하지 않음
저장을 선택(Y)했다면, 저장할 파일 이름이 맞는지 확인하고 Enter 키를 누르면 종료된다.
strace -o 옵션을 파일로 저장
cat hello.log 확인
strace 로그 확인:
hello.log
파일에는 프로그램 실행 시 발생한 각 시스템 콜이 한 줄씩 기록되어 있다.예를 들어,
는
write()
시스템 콜을 통해 "hello world\n" 문자열을 출력한 것을 의미한다.
시스템 콜의 역할:
프로그램은 실행 중 커널에 작업 요청을 하게 되는데, 이 요청들이 시스템 콜 형태로 이루어진다.
총 150여 개의 시스템 콜이 호출되었는데, 이 중 대부분은 프로그램의 시작과 종료 같은 OS 기본 처리에 해당한다.
프로그래밍 언어와 관계없이:
Go 언어뿐만 아니라, 다른 언어(예: Python)로 작성된 프로그램도 동일한 방식으로 커널에 작업을 요청하기 위해 시스템 콜을 사용한다.
이를 통해 모든 프로그램은 커널의 도움을 받아 하드웨어 자원에 접근하거나 출력을 처리한다.
이와 같이, strace 로그를 통해 프로그램이 실행되는 동안 커널에 어떻게 요청하는지, 그리고 그 과정에서 발생하는 시스템 콜의 역할과 수를 확인할 수 있다.
정리 : strace는 프로그램 실행 시 발생하는 시스템 콜을 실시간으로 보여주는 도구이다.
실행 결과를 바로 터미널에 출력할 수도 있지만, 보통은 리디렉션을 통해 파일(예: hello.log)에 저장한 후에 그 파일을 cat 명령어로 확인한다. 즉,
strace 실행 시:
와 같이 실행하여 시스템 콜 기록을 hello.log 파일에 저장한 다음,
저장된 파일 확인:
명령어로 내용을 확인하는 것이다.
파이썬 파일 실행
좀 더 복잡한 프로그램을 strace로 실행하면 재미있는 결과가 나올지 모르지만, strace 출력 내용은 용량이 커지기 마련이기에 파일 시스템의 남은 공간에 주의하자.
4) 시스템 콜을 처리하는 시간 비율
[sar 명령어와 CPU 사용량 모니터링]
시스템에 설치된 논리 CPU가 실행하고 있는 명령 비율은 sar 명령어를 사용하면 알 수 있다.
참고로 여기서 논리 CPU
란 커널이 CPU로 인식하고 있는 대상을 뜻하며, CPU가 1코어라면 하나의 CPU, 멀티 코어 CPU라면 하나의 코어 SMT, 참조가 유효한 시스템이라면 CPU 코어 안에 있는 스레드를 가르킨다.
1. sar 개요
sar(System Activity Reporter)는 시스템 자원 사용량(CPU, 메모리, 네트워크 등)을 시간 단위로 측정하고 통계를 보여주는 도구이다.
주로 sysstat 패키지에 포함되어 있으며, 다양한 옵션을 통해 CPU, 디스크 I/O, 메모리, 네트워크 트래픽 등 여러 자원 사용량을 모니터링할 수 있다.
2. sar -P
옵션
sar -P 0 1 1 명령어로 CPU 코어0이 어떤 종류의 처리를 실행하는지 정보 수집 가능
여기서 잠깐 CPU 코어란? 🧐
CPU 코어란 하나의 CPU(중앙 처리 장치) 내부에서 독립적으로 명령어를 처리할 수 있는 실행 단위
역할: 각 코어는 프로그램의 명령어를 처리하는데, 여러 코어가 있으면 동시에 여러 작업을 병렬로 처리할 수 있어 멀티태스킹과 프로그램 실행 속도를 향상시킨다.
현대 CPU: 대부분의 현대 CPU는 두 개 이상의 코어를 가지고 있어, 복수의 프로세스나 스레드가 동시에 실행될 수 있다.
논리적 CPU와의 관계: 하드웨어적으로 하나의 코어가 한 번에 한 작업만 수행하지만, 하이퍼스레딩 같은 기술을 통해 한 코어가 마치 두 개의 논리적 CPU처럼 동작하여 효율을 높이기도 한다.
즉, CPU 코어는 컴퓨터가 여러 작업을 동시에 빠르게 처리할 수 있게 해주는 핵심적인 요소
-P <CPU 번호>
: 특정 CPU(코어)에 대한 사용량 통계를 출력한다. 논리 CPU를 어떤 용도로 사용했는 지 관련 정보 출력예를 들어
sar -P 0 1 5
는 CPU 0에 대한 사용량을 1초 간격으로 5번 측정한 결과를 보여준다.출력 예시에서 각 필드의 의미:
%user: 사용자 모드에서 실행된 시간 비율. 일반 애플리케이션(프로세스)이 CPU를 점유하고 있는 비율이다.
%nice: nice 값이 설정된(우선순위가 낮게 조정된) 프로세스가 사용자 모드에서 실행된 시간 비율.
%system: 커널 모드(시스템 콜, 드라이버, 네트워크 스택 등)에서 실행된 시간 비율.
%iowait: CPU가 디스크 I/O 같은 입출력 작업이 끝나길 기다리는 시간 비율.
%steal: 가상화 환경에서 하이퍼바이저가 CPU를 가져가서(steal) 실제로 해당 VM(또는 컨테이너)에 CPU가 할당되지 못하는 시간 비율.
%idle: 아무 작업도 수행하지 않는 아이들(idle) 상태 비율.
이렇게 각 항목을 합하면 100%가 되며, CPU가 어떤 방식으로 사용되고 있는지 직관적으로 알 수 있다.
[inf-loop.py 프로그램으로 CPU 부하 유발]
1. 무한 루프 스크립트
예시 코드
이 스크립트는 무한 루프를 돌면서 CPU를 계속 사용
pass
문은 아무 일도 하지 않지만, 루프 자체가 끝없이 반복되므로 CPU 사용량이 100%에 근접한다.
2. 백그라운드 실행
일반적으로 다음과 같이 실행할 수 있다.
(단, 실행 권한이 있어야 하며, 스크립트 첫 줄에
#!/usr/bin/python3
같은 shebang이 있어야 한.)또는
처럼 Python 인터프리터를 직접 호출하여 실행해도 됨.
[taskset 명령어로 특정 CPU에 고정하기]
taskset -c <논리 CPU 번호> <명령어>를 실행하면 <명령어> 인수로 지정한 명령어를 -c <논리 CPU 번호> 인수로 지정한 CPU에서 실행 가능
1. taskset 개념
taskset은 프로세스를 특정 CPU(또는 CPU 집합)에 할당하여 실행할 수 있게 하는 명령어
CPU 0번 코어에만 프로세스를 고정하려면
-c
옵션을 사용해 다음과 같이 실행한다.이렇게 하면 inf-loop.py 프로세스는 CPU 0에서만 실행된다.
2. 활용 예시
부하 분산 실험: 여러 프로그램을 각각 다른 CPU에 배치해 병렬로 돌리거나, 특정 코어에만 부하를 몰아서 테스트할 때 유용
성능 분석: CPU별로 어떤 작업이 실행되는지 확인하고, 한 코어만 바쁘게 돌아가는 병목현상이 있는지 파악할 수 있다.
[sar 출력 결과 해석]
1. CPU 0에서의 사용자 모드 100% 예시
예시 명령어:
-P 0
: CPU 0에 대한 통계를 보겠다.1 1
: 1초 간격으로 1번만 측정.
여기서 %user가 100.00%임을 볼 수 있는데, 이는 CPU 0에서
inf-loop.py
가 무한 루프를 돌면서 모든 CPU 시간을 사용 중이라는 뜻%system이나 %idle이 0.00%로 표시되는 이유는, 그 시간 동안 사용자 모드 실행이 CPU 0의 모든 시간(100%)을 차지했기 때문이다.
시스템이 실제로 일(계산)을 하는 동안 %user가 올라가며, 디스크 I/O 대기나 다른 작업 없이 계속 반복문을 돌면 %user가 100%에 가깝게 된다.
이 과정을 통해 CPU 코어별 사용률이 어떻게 변하는지, 사용자 모드(user), 커널 모드(system), 대기(iowait), 유휴(idle) 상태 등이 어떤 비율로 발생하는지 알 수 있으며, 이는 시스템 성능 분석과 튜닝, 병목 찾기에 매우 유용한 방법이다.
[getppid() 무한히 발생시키는 것 실행]
부모 프로세스의 프로세스 ID를 얻는단순한 시스템 콜 getppid()를 무한히 발생시킴
코드 개요 (syscall-inf-loop.py)
무한 루프 안에서
os.getppid()
함수를 반복 호출os.getppid()
는 부모 프로세스 ID를 얻는 시스템 콜(getppid()
)을 내부적으로 사용
왜 getppid()
를 사용하나?
getppid()
를 사용하나?시스템 콜 발생: Python에서
os.getppid()
를 호출하면 커널에 “부모 PID를 달라”는 요청을 보낸다이처럼 반복적으로 시스템 콜을 호출하면, CPU 사용량 중에서 시스템 모드(system) 비중이 높아진다.
실행 예시
taskset으로 특정 CPU에 할당
CPU 0에서만 프로그램을 동작시키도록 설정합니다.
sar로 CPU 사용량 확인
CPU 0에 대해 1초 간격으로 1번(
1 1
) 측정.결과에서
%system
이 크게 증가했음을 확인할 수 있다(커널 모드에서 많은 작업이 일어남).
3. CPU 사용량 변화
inf-loop.py vs. syscall-inf-loop.py
inf-loop.py
:사용자 모드 연산만 반복 → %user가 높게 나타남.
syscall-inf-loop.py
:시스템 콜을 계속 호출 → 커널 모드 시간이 증가 → %system이 높게 나타남.
%system
이 100% → CPU가 대부분의 시간을 커널 모드에서 보내고 있다는 뜻.
핵심 포인트
시스템 콜 빈도 증가
os.getppid()
를 무한 반복 호출하므로, CPU가 사용자 모드보다 커널 모드에 머무르는 시간이 길어진다.
CPU 코어별 분석
taskset -c 0
를 통해 CPU 0에만 부하를 주면, sar -P 0으로 해당 코어 사용량을 정확히 확인할 수 있다.
user vs. system
무한 루프 내에서 단순 연산(예:
pass
)만 수행하면 %user가 올라가고,시스템 콜(예:
getppid()
)을 반복 호출하면 %system이 올라간다.
운영체제 관점
시스템 콜
은 커널 모드로 진입하므로, 그만큼 커널이 일을 많이 처리한다는 의미이다.sar를 통해 CPU가 어떤 모드(user, system, idle, iowait 등)에서 시간을 많이 소비하는지 쉽게 파악할 수 있다.
모니터링, 경고 알림 및 대시보드 도구
시스템의 이전 상태나 트렌드를 관찰하려면 sar
같은 명령어를 사용할 수 있다. 하지만 대규모 환경이나 실시간 알림, 시각화 대시보드가 필요한 경우에는 다음과 같은 도구가 많이 활용 된다.
Prometheus
시계열 데이터베이스(time-series DB)와 강력한 쿼리 언어를 제공
다양한 Exporter를 통해 CPU, 메모리, 디스크, 네트워크 등의 지표를 수집
알림 매니저(Alertmanager)와 연동해 특정 임계치 도달 시 알림 전송 가능
Zabbix
에이전트를 통해 서버, 네트워크 장비 등의 모니터링 정보를 수집
웹 인터페이스로 편리하게 설정/관리
다양한 템플릿을 제공해 빠르게 환경 구성 가능
Datadog
클라우드 기반 모니터링 및 분석 플랫폼
서버, 애플리케이션, 로그, 메트릭을 통합적으로 관리
직관적인 대시보드 및 알림 설정 기능 제공
5) 시스템 호출 소요 시간 (strace -T 옵션)
1. strace 기본 동작
strace
는 프로세스가 커널에 요청하는 시스템 콜을 추적하는 도구입니다.예:
hello.log
파일에 실행 과정에서 호출된 시스템 콜을 기록각 줄은 한 번의 시스템 콜 호출을 나타냄
2. -T
옵션
-T
옵션-T
옵션은 시스템 콜이 실제로 소요된 시간을 표시해준다.각종 시스템 콜 처리에 걸린 시간을 마이크로 초 수준으로 정밀하게 측정할 수 있다.
%system 값이 높아서 구체적으로 어떤 시스템 콜에 시간이 걸리는 지 확인할 때 편리
예:
hello.log
안에 각 시스템 콜의 종료 시점에<소요시간>
형태로 표시된다.
3. 예시 출력
write(1, "hello world\n", 12)
파일 디스크립터
1
(표준 출력)에"hello world\n"
문자열 12바이트를 쓰는 시스템 콜
= 12
실제로 12바이트가 성공적으로 쓰임
<0.000007>
시스템 콜이 완료될 때까지 걸린 시간(단위: 초)
매우 짧은 시간(7마이크로초 정도) 안에 처리되었음을 의미
결론적으로, 모니터링 도구와 strace를 함께 사용하면 시스템 전체의 리소스 사용 현황과, 개별 프로그램의 시스템 콜 수준 상세 정보를 종합적으로 파악할 수 있다. 이를 통해 장애를 조기에 발견하고 성능 최적화를 진행할 수 있다.
4. 라이브러리
1) 라이브러리란?
공통 기능 집합
여러 프로그램에서 공통으로 사용하는 기능(함수, 메서드 등)을 묶어 놓은 코드 묶음이다.
개발자는 라이브러리를 사용함으로써 직접 코드를 작성하지 않고, 이미 검증된 기능을 효율적으로 재사용할 수 있다.
운영체제(OS) 라이브러리
운영체제 수준에서 제공되는 라이브러리로, 표준 C 라이브러리(glibc)나 각종 시스템 라이브러리가 여기에 해당한다.
프로그램은 이 라이브러리를 통해 파일 입출력, 문자열 처리, 메모리 할당, 네트워크 통신 등 다양한 기능을 손쉽게 사용할 수 있다.
언어별 표준 라이브러리
C, C++, Python, Go 등 대부분의 프로그래밍 언어는 자체 표준 라이브러리를 제공한다.
예: C 언어 표준 라이브러리(ISO C 표준), C++ STL, Python 표준 라이브러리, Go 표준 라이브러리 등.
2) OS가 제공하는 라이브러리
표준 C 라이브러리(glibc)
C언어는 국제 표준화 기구nternational Organization for Standardization(ISO)"에서 정한 표준 라이브러리가 존재한다.
GNU 프로젝트에서 제공하는 glibc가 대표적인 예이며, 리눅스 환경에서
libc.so.6
로 표시된다.대부분의 C 프로그램은 컴파일 시 glibc를 기본적으로
링크(link)하여 사용
한다.전에 컴퓨터 밑바닥의 비밀 보니 c언어는 링커가 있지만 자바는 없다고 했던 것 같음
C 언어는 컴파일러가 소스 코드를 오브젝트 파일로 변환한 후 링커(linker)가 정적 라이브러리(.a)나 공유 라이브러리(.so)를 연결하여 하나의 실행 파일을 만들어 준다. 반면, Java는 소스 코드를 바이트코드(.class 파일)로 컴파일하고, 실행 시 JVM이 클래스 로더를 통해 필요한 클래스와 라이브러리를 동적으로 로드하며 연결하기 때문에 전통적인 의미의 링커는 없다.
ldd 명령어를 통한 확인
ldd <실행 파일 경로>
를 입력하면, 해당 프로그램이 어떤 라이브러리를 동적으로 링크하고 있는지 확인할 수 있다.
libc.so.6
가 표준 C 라이브러리ld-linux-x86-64.so.2
는 공유 라이브러리를 로드하는 특별한 라이브러리
/bin/cat
,/usr/bin/python3
등 다양한 실행 파일을ldd
로 확인해 보면, 대부분libc.so.6
를 링크하고 있음을 알 수 있다.
Python 인터프리터 역시 내부적으로 C 라이브러리를 사용한다.
최근에는 C 언어를 직접 사용하는 일이 드물어졌다고 하지만 OS 수준에서는 안 보이는 곳에서 막강한 힘을 발휘하는 여전히 중요한 언어라는 걸 알 수 있다.
기타 라이브러리
리눅스 시스템에는 C++ 표준 라이브러리(
libstdc++
)나 각종 개발용 라이브러리(수학, 그래픽, 네트워크 등)도 풍부하게 설치될 수 있다.예:
dpkg-query -W | grep "lib"
등을 통해 우분투 환경에 설치된 라이브러리 패키지를 확인할 수 있다.
3) 시스템 콜 래퍼 함수
libc는 표준 C 라이브러리뿐만 아니라 시스템 콜 래퍼wrapper 함수도 제공한다.
시스템 콜 호출의 복잡성
C 언어 등 고급 언어에서 시스템 콜(예:
getppid()
)을 직접 호출하려면 아키텍처별 어셈블리 코드를 사용해야 한다.예: x86_64에서는
정해진 규약
ARM64에서는
이렇게 플랫폼마다 어셈블리 코드가 달라, 직접 사용하는 것은 매우 번거롭다.
이식성이 안좋다.
래퍼(Wrapper) 함수
표준 C 라이브러리(glibc)는 이러한 어셈블리 코드를 감춘 시스템 콜 래퍼 함수를 제공합니다.
다 래퍼 함수는 아키텍처별로 존재한다.
C, Python, Go 등 고급 언어에서
getppid()
,read()
,write()
같은 함수를 호출하면, 내부적으로는 glibc의 래퍼 함수를 통해 시스템 콜을 수행하게 된다.
이를 통해 프로그램 코드는 “함수 호출” 형태만 보이지만, 실제 동작은 커널 모드로 전환되어 시스템 콜이 실행된다.
장점
개발자가 직접 어셈블리 코드를 작성할 필요가 없어, 이식성과 개발 생산성이 높아진다.
아키텍처가 달라도(예: x86_64, ARM64) glibc가 제공하는 함수 인터페이스는 동일하게 유지된다.
4) 정적 라이브러리와 공유 라이브러리
정적 라이브러리와 공유 라이브러리 모두 같은 기능을 제공하지만 프로그램과 결합하는 방식이 다르다. 공유 라이브러리는 흔히 동적 라이브러리라고도 부른다.
개념
정적 라이브러리(Static Library)
컴파일 시점에 라이브러리를 프로그램 실행 파일에 직접 포함한다.
보통
.a
확장자를 사용한다.결과적으로 생성된 실행 파일 크기가 커지지만, 실행 시점에 별도의 라이브러리가 없어도 동작한다.
공유 라이브러리(Shared Library)
컴파일 시점에는 “이 라이브러리를 사용할 것”이라는 정보만 포함하고, 실제 라이브러리는 프로그램 실행 시(또는 실행 중)에 동적으로 로드된다.
보통
.so
확장자를 사용한다.실행 파일 크기가 작아지고, 여러 프로그램이 하나의 라이브러리를 공유하여 메모리를 절약할 수 있다.
예시: pause.c
예:
이 코드를 정적 링크와 공유 링크로 각각 빌드했을 때,
정적 링크: 실행 파일에 libc 코드가 포함 → 파일 크기 커짐
공유 링크: 실행 파일은 작아지지만, 실행 시
libc.so
를 찾아 로드해야 함
공유 라이브러리가 널리 사용되는 이유
메모리/디스크 공간 절약: 하나의 공유 라이브러리를 여러 프로그램이 동시에 사용
업데이트 용이: 라이브러리에 보안 취약점이 발견되었을 때, 해당 라이브러리만 교체하면 이를 사용하는 모든 프로그램이 수정 효과를 얻음
빠른 배포: 필요한 라이브러리를 따로 관리하고, 여러 프로그램에서 공통 활용 가능
재미삼아 여러분이 사용하는 프로그램 실행 파일에 Ldd 명령어를 실행해 어떤 공유 라이브러리 가 링크되어 있는지 확인해보는 법
프로그램 실행 파일은 컴파일되어 바이너리 형태로 만들어진 실제 실행 파일을 말한다.
보통 운영체제에 설치된 실행 파일은 /bin, /usr/bin, /usr/local/bin 등 표준 디렉토리에 위치한다. 실행 파일의 경로를 확인하는 방법은 여러 가지가 있다.
실행 파일 경로 확인 방법
which 명령어 사용 예를 들어,
python3
실행 파일의 경로를 확인하려면:
이 명령을 실행하면 /usr/bin/python3
과 같이 실제 실행 파일의 전체 경로가 출력된다.
type 명령어 사용
이 명령어도 해당 명령어가 어디에 위치해 있는지 보여준다.
패키지 관리 도구 활용 예를 들어, 우분투에서는
dpkg -L <패키지명>
명령어로 해당 패키지에 포함된 파일들을 확인할 수 있다.
Ldd 명령어 사용 예
예를 들어, python3
실행 파일이 /usr/bin/python3
에 있다면, 다음과 같이 ldd 명령어로 링크된 공유 라이브러리를 확인할 수 있다.
정적 링크를 선호하는 경우
공유 라이브러리가 자주 사용된다고 설명했지만 최근에는 조금 상황이 달라졌다. 예를 들어 최근에인 기가 좋은 Go 언어는 기본적으로 라이브러리를 모두 정적 링크한다 따라서 일반적인 Go 프로그램은 그 어떤 공유 라이브러리에도 의존하지 않는다.
Go 언어의 기본 정책
Go는 기본적으로 정적 링크를 사용해, 실행 파일 하나에 모든 라이브러리가 포함된다.
ldd
로 확인했을 때 “not a dynamic executable”이 뜨면 공유 라이브러리를 사용하지 않는다는 의미
장점
라이브러리 종속성(DLL 지옥) 문제를 피할 수 있음
실행 파일만 배포해도 동일하게 동작
공유 라이브러리를 로드하는 과정을 생략해 시작 시간이 빠를 수 있음
대용량 디스크, 메모리가 보편화되어 파일 크기가 크게 문제가 되지 않을 수도 있음
5) 추가로 알아두면 좋은 사항
동적 로더(ld-linux-*.so.2)
공유 라이브러리를 사용하는 프로그램은 실행 시
ld-linux-*.so.2
(또는ld-musl-*.so.1
등) 같은 로더가 라이브러리를 메모리에 로드해 준다.ldd
명령어 출력에 자주 등장하며, 이를 삭제하거나 손상시키면 공유 라이브러리 기반 프로그램이 실행되지 않는다.
라이브러리 버전 관리
동일한 라이브러리도 버전에 따라
ABI(Application Binary Interface)
가 달라질 수 있다.프로그램이 특정 버전의 라이브러리에만 의존하는 경우, 버전 호환 문제가 발생하기도 한다(“DLL 지옥” 문제).
C 언어와 OS의 밀접한 관계
현대적 운영체제(특히 유닉스 계열) 대부분이 C 언어로 작성되었고, OS 레벨에서 C 표준 라이브러리는 여전히 핵심적 위치를 차지한다.
Python, Java, Go 등 다른 언어를 사용하더라도, 내부적으로는 C 라이브러리를 통해 시스템 자원 접근이 이루어진다.
ldconfig 명령어
리눅스 시스템에서 공유 라이브러리를 관리할 때,
/etc/ld.so.conf
나/etc/ld.so.conf.d/
디렉토리의 설정에 따라 라이브러리를 검색한한다.sudo ldconfig
명령어로 캐시를 갱신하여, 새로 설치된 라이브러리를 인식시킬 수 있다.
정적 vs 공유 라이브러리 선택
프로젝트 규모, 배포 방식, 성능 요구 사항, 메모리/디스크 제한 등을 종합적으로 고려해야 한한다.
예: 임베디드 장비나 컨테이너 환경에서는 용량 최소화가 중요할 수 있고, 대규모 서버 환경에서는 업데이트 편의성이 중요할 수 있다.
6) 결론
라이브러리는 프로그램 개발에서 필수적인 개념으로, 운영체제(특히 리눅스) 역시 표준 C 라이브러리(glibc)를 비롯해 여러 시스템 라이브러리를 제공하고 있다.
시스템 콜 래퍼 함수는 아키텍처 의존적인 어셈블리 호출을 감춰, 개발자가 편리하고 이식성 높게 시스템 콜을 사용할 수 있도록 해준다.
정적 링크 vs 공유 라이브러리는 프로그램의 크기, 업데이트 방식, 성능, 배포 전략 등 다양한 요소와 맞물려 있어, 상황에 따라 선택이 달라진다.
C 언어의 중요성은 여전히 크며, Go, Python 등 다른 언어로 작성된 프로그램도 내부적으로 C 라이브러리와 긴밀하게 연동되어 동작한다.
이 모든 내용을 종합해보면, 라이브러리는 OS와 프로그램을 잇는 핵심 다리이며, 개발자는 이를 활용해 생산성을 높이고, 효율적으로 시스템 자원을 사용할 수 있다.
Last updated