프로그램과 프로세스
- 프로그램 - 컴퓨터에서 동작하는 관련된 명령 및 데이터를 하나로 묶은 것 ex) executable file, 소스 코드(script 언어 케이스), kernel
- 프로세스 - 실행되어서 동작 중인 프로그램을 프로세스라고 함.
커널이 왜 필요한가
- 프로세스간에 data를 write 한다고 가정해보자. 서로 다른 프로세스가 같은 저장장치에 직접 읽고/쓰기를 한다면 커널이 없다면 data가 손상될 수 있을 것이다.(다른 프로세스들의 동작이나 순서를 알 수 없으니)
- 접근 불가능이어야할 프로그램이 장치에 접근 가능할수도 있음.
- 이런문제를 해결하기 위해, 하드웨어의 도움을 받아 커널은 프로세스가 직접 장치에 접근할 수 없도록 하고있다. 구제척으로는 CPU에 내장된 mode 기능을 사용한다.
- mode란?
- PC나 서버에서 사용하는 일반적인 CPU에는 커널모드와 사용자 모드 두 종류의 모드가 있음.
- 프로세스가 사용자 모드로 실행되고 있으면 사용자 공간(userland)에서 프로세스를 실행한다고 함.
- CPU가 커널 모드라면 명령을 실행하는데 아무 제약이 없으나 사용자 모드의 경우 제약이 걸린다.
- 리눅스는 커널만이 커널모드로 동작해서 장치에 접근함.
- 프로세스는 사용자 모드로 동작하기 장치에 직접 접근할 수 없으니 -> 프로세스는 커널을 통해서 간적접으로 장치에 접근함.
- 예시 상황
- hello world를 모니터에 출력한다고 가정했을때, 실제 Display 모니터에 hello world를 출력하는 동작은 커널에 의해 장치에 접근되어 이루어질 것임.
커널
- 커널 모드로 동작하면서, 다른 프로세스에서 불가능한 장치 제어, 시스템 자원 관리 및 배분 기능 제공.
- 시스템 내부의 모든 프로세스가 공유하는 자원을 한 곳에서 관리하고, 동작하는 프로세스에 배분할 목적으로 동작하는 프로그램임.
시스템 콜
- 프로세스가 커널에 처리를 요청하는 방법. ex) 새로운 프로세스 생성, 하드웨어 조작처럼 커널의 도움이 필요할 때 사용.
- 시스템 콜 예시
- 프로세스 생성, 삭제
- 메모리 확보, 해제
- 통신 처리
- 파일 시스템 조작
- 장치 조작
- 시스템 콜은 CPU의 특수한 명령을 실행해서 처리됨. 즉, 프로세스에서 시스템 콜을 호출하면 CPU에서는 exception이라는 예외 이벤트가 발생함. 해당 이벤트를 계기로 CPU 모드가 사용자모드에서 -> 커널 모드로 바뀜. 커널에서 처리가 되고, 시스템 콜 처리 끝나면 다시 사용자 모드로 돌아와서, 프로세스 동작이 이어짐.
프로세스 시스템 콜 호출 시스템 콜에서 복귀 커널 시스템 콜 처리 CPU 모드 사용자 모드 커널 모드 사용자 모드 - 시스템 콜 처리하기 전, 커널은 프로세스에서 온 요청이 올바른지 확인함. 처리 가능한 범위 이상으로 메모리를 요청하는지, 올바른 요청이 아니라면 시스템 콜은 실패하게 된다.
- 시스템 콜을 통하지 않고, 프로세스에서 직접 CPU 모드를 변경하는 방법은 없음. 악의적인 사용자가 직접 장치를 조작한다면, 시스템이 파괴될 것이다.
Strace
- 프로세스가 어떤 시스템 콜을 호출하는지 strace 명령어로 확인 가능
예시) 간단히 hello world를 출력하는 프로그램 만든 후, strace 명령어로 로그 남겨서 확인.
$ strace -o hello.log ./hello
hello world
$ cat hello.log
...
write(1, "hello world\n", 12) = 12 (1)
...
- strace 출력 각각의 줄이 1개의 시스템 콜 호출이다.
- (1)은 데이터를 화면이나 파일 등에 출력하는 write() 시스템 콜이다.
- 동일한 실습을 python 랭귀지로 하더라도 동일한 write 시스템 콜을 호출하는 걸 알 수 있다.
- strace 출력 내용은 용량이 크다보니, 남은 공간에 주의하며 실습해보면 됨.
시스템 콜 처리하는 시간 비율
- 시스템에 설치된 논리 CPU(커널이 CPU로 인식하고 있는 대상)가 실행하고있는 명령 비율은 sar 명령어를 사용하면 알 수 있음.
$ sar -P 0 1 1
linux 5.4.0-66-generic (coffe) ...
09:51:03 AM CPU %user %nice %system %iowait %steal %idle (1)
09:51:04 AM 0 0.00 0.00 0.00 0.00 0.00 100.00
Average: 0 0.00 0.00 0.00 0.00 0.00 100.00
- sar -P 0 1 1 뜻
- -P 0 -> CPU 코어 0의 데이터를 수집한다는 뜻
- 그다음 1 -> 1초마다 수집한다는 의미
- 그다음 1 -> 1번만 데이터를 수집한다는 뜻
- (1)은 헤더이고, 다음 줄의 첫번째 필드 시간 즉 1초동안 두번째 필드에 표시된 논리 CPU를 어떤 용도로 사용했는지 관련 정보를 출력함.
- %user 필드부터 %idle 필드까지 모두 합치면 100이 됨.
- usermode에서 프로세스를 실행하는 시간 비율은 %user와 %nice 합계로 얻을 수 있음. (이후 타임 슬라이스 구조에서 설명예정)
- %system은 커널이 시스템 콜을 처리한 시간 비율, %idle은 아무 것도 하지 않는 아이들 상태 비율.
- 즉 해당 예제는 %idle이 100.00, CPU는 거의 아무일도 하지 않았다.
다음 예제로, while True: pass 형태의 python코드를 작성해 백그라운드로 실행시켜보자. 실행 후, kill pid 번호를 통해 종료하면 됨.
$ taskset -c 0 ./inf-loop.py &
[1] 1911
$ sar -P 0 1 1
linux 5.4.0-66-generic (coffe) ...
09:51:03 AM CPU %user %nice %system %iowait %steal %idle (2)
09:51:04 AM 0 100.00 0.00 0.00 0.00 0.00 0.00
Average: 0 100.00 0.00 0.00 0.00 0.00 0.00
- taskset 명령어를 이용해서 inf-loop.py 프로그램을 CPU 0에서 동작시킴
- taskset -c <논리 CPU 번호> <명령어>
- (2)를 보면, 프로그램이 계속 동작 중이라서 %user가 100임을 알 수 있다.
inf-loop.py 프로그램 | 동작 | 동작 | 동작 |
커널 | |||
CPU 모드 | 사용자 모드 | 사용자 모드 | 사용자 모드 |
다음 예제로, 부모 프로세스의 ID를 얻는 단순한 시스템 콜 getppid()를 무한히 발생시키는 프로그램을 실행해보자.
import os 후, while True : os.getppid()를 실행하도록 한다.
$ taskset -c 0 ./syscall-inf-loop.py &
[1] 2005
$ sar -P 0 1 1
linux 5.4.0-66-generic (coffe) ...
09:51:03 AM CPU %user %nice %system %iowait %steal %idle
09:51:04 AM 0 35.00 0.00 65.00 0.00 0.00 0.00
Average: 0 35.00 0.00 65.00 0.00 0.00 0.00
시스템 콜을 끊임없이 호출하여, %system이 많아졌다.
syscall-inf-loop.py 프로그램 | 동작 getppid()호출 |
복귀 후 다시 getppid() 호출 |
복귀 후 다시 getppid() 호출 | 복귀 후 다시 getppid() 호출 | |||
커널 | 동작 | 동작 | 동작 | ||||
CPU 모드 | 사용자 모드 | 커널 모드 | 사용자 모드 | 커널 모드 | 사용자 모드 | 커널 모드 | 사용자 모드 |
시스템 콜 소요 시간
- strace -T 옵션을 사용하면, 각종 시스템 콜 처리에 걸린시간을 마이크로 초 수준으로 측정 가능.
- %system 값이 높은경우, 이 기능을 사용하면 편리하다.
$ strace -T -o hello.log ./hello
hello world
$ cat hello.log
...
write(1, "hello world\n", 12) = 12 <0.000017>
...
라이브러리
- 다수의 프로그램에서 공통으로 사용하는 처리를 라이브러리로 합쳐서 제공하는 것.
- 프로그래머는 미리 만들어진 라이브러리에서 필요한 것을 골라 효율적으로 프로그램을 개발할 수 있다.
호출 계층은 아래와 같다.
사용자 모드(프로세스) | 프로세스 고유의 코드 함수 호출 ↓, 직접 system call 호출 | ||||
OS 외부 라이브러리 함수 호출 ↓, 직접 system call 호출 | |||||
OS 라이브러리 system call 호출 ↓ | |||||
커널 모드 | 커널 | ||||
하드웨어 |
표준 C 라이브러리
- C 언어는 국제 표준화 기구(ISO)에서 정한 표준 라이브러리가 존재함.
- 리눅스에서도 이런 표준 C 라이브러리가 제공되는데, GNU 프로젝트에서 제공하는 glibc(libc)를 표준 C 라이브러리로 사용함.
- C 언어로 작성된 대부분의 C 프로그램을 libc를 링크(linking) 한다.
프로그램이 어떤 라이브러리를 링크하는지 알아보려면 ldd 명령어를 사용해서 확인해보자.
$ ldd /bin/echo
linux-vdso.so.1 (0x000~ 주소)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000~)
/lib64/ld-linux-x86_64.so.2 (0x000~ )
- 출력 결과에서 libc.so.6은 표준 C라이브러리를 뜻한다. ld-linux-x86_64.so.2 는 공유 라이브러리를 로드하는 라이브러리.
- ldd /bin/cat, ldd /usr/bin/python3 명령어도 마찬가지로 libc를 링크하고 있는데, 파이썬을 실행할때 내부적으로 표준 C 라이브러리를 사용한다는 것을 알 수 있다.
시스템 콜 래퍼 함수
- libc는 표준 C라이브러리뿐만 아니라 시스템 콜 wrapper 함수도 제공한다. 시스템 콜은 일반 함수 호출과 다르게 C 언어 같은 고급 언어에서 직접 호출할 수 없음. 아키텍처에 의존하는 어셈블리 코드를 사용해서 호출해야한다. 예를 들어서, x86_64 아키텍처 CPU 라면 getppid() 시스템 콜은 어셈블리 코드 레벨에서 다음과 같이 호출한다.
mov $0x6e, %eax
syscall
- 첫 번째 줄은 getppid()의 시스템 콜 번호 0x6e를 eax 레지스터에 대입함.(리눅스 시스템 콜 호출 규약에 정해진 내용)
- syscall 명령으로 시스템 콜을 호출하고, 커널 모드로 전환한다.
- 그 이후, getppid()를 처리하는 커널 코드가 실행된다.
같은 코드여도, arm64 아키텍처는 어셈블리 코드 레벨에서 다음과 같이 getppid() 시스템 콜을 호출한다.
mov x8, <시스템 콜 번호>
svc #0
- 만약 libc의 도움이 없다면, 시스템 콜 호출할 때마다 아키텍처 의존 어셈블리 코드를 작성해서, 고급 언어에서 계속 호출 해야한다.
사용자 모드 프로그램 A 고급 언어로 작성된 프로그램 ↓ 어셈블리 언어로 작성된 시스템 콜 호출 프로그램 ↓ 커널 모드 커널 - 이렇게 되면, 다른 아키텍처로 이식할 때 동작을 보장할 수가 없을 것임. 그래서 libc는 내부적으로 시스템 콜을 호출할 뿐인 wrapper함수를 제공함. 아키텍처별로 존재하고 있음.
사용자 모드 | 프로그램 A |
고급 언어로 작성된 프로그램 ↓ | |
OS가 제공하는 시스템 콜 wrapper 함수 ↓ | |
커널 모드 | 커널 |
정적 라이브러리와 공유 라이브러리
- 라이브러리는 정적 라이브러리, 동적(shared, 공유) 라이브러리의 두 종류로 분류할 수 있음
- 프로그램을 생성하려면 소스코드 컴파일 -> object 파일을 만듬. object 파일이 사용하는 라이브러리를 링크 -> 실행파일이 만들어짐.
- 이때 정적 라이브러리는 오브젝트 파일이 사용하는 라이브러리를 링크해서 실행 파일 만듬.
- 반면 공유 라이브러리는 '이 라이브러리의 이런 함수를 호출한다' 라는 정보만 실행 파일에 포함. 그리고 프로그램 시작하거나 실행 중에 라이브러리를 메모리에 load하고 프로그램은 그 안에 있는 함수를 호출한다.
- 동적 라이브러리로 빌드하는 경우, 프로그램 파일 크기가 작은 이유는 libc가 프로그램 자체에 포함되는 것이 아니고, 실행 시 메모리에 로드되기 때문임.
- libc 코드는 프로그램마다 각각의 복사본을 사용하는 대신에, libc를 사용하는 모든 프로그램에서 같은 내용을 공유한다.
- 공유 라이브러리를 다음과 같은 이유로 자주 사용한다고 한다.
- 시스템에서 차지하는 크기를 줄일 수 있다.
- 라이브러리에 문제가 있을때, 공유 라이브러리를 수정 버전으로 교체하기만 하면 해당 라이브러리를 사용하는 모든 프로그램에서 문제 해결이 가능하다.
- 번외로 정적 링크를 사용하는 케이스는 Go언어, 기본적으로 정적 링크를 쓴다고 함.
- 대용량 메모리, 저장 장치 사용되어서 파일 크기는 별문제가 안 됨.
- 실행 파일 하나로 프로그램 동작한다면, 복사만 해서 다른환경에서도 사용 편리
- 실행할 때 공유 라이브러리 링크하지 않아도 되어서 시작 시간 빠름.
- 공유 라이브러리의 DLL 지옥 문제회피가능 -> 라이브러리 버전업시, 하위 호환성이 미묘하게 달라지며 문제 생기는 일이 많은 이슈라고 함.
'Linux' 카테고리의 다른 글
리눅스 스터디 #6 장치 접근 (0) | 2025.01.19 |
---|---|
리눅스 스터디 #5 프로세스 관리(응용) (0) | 2025.01.19 |
리눅스 스터디 #4 메모리 관리 시스템 (0) | 2025.01.19 |
리눅스 스터디 #3 프로세스 스케줄러 (0) | 2025.01.19 |
리눅스 스터디 #2 프로세스 관리 (0) | 2025.01.19 |