진정한 개발자라면 리눅스를 한번쯤은 건드려 봐야죠!
그래서 운영체제를 공부하는 김에 리눅스를 설치하고 공부해보기로 했습니다.
실행 환경은 우분투 리눅스 22LTS 버전입니다.
시작전에 먼저 필수 유틸리티 설치부터 손으로 치기 귀찮으실테니 이거 복사하세요
$sudo apt install binutils build-essential golang sysstat python3-matplotlib python3-pil 'fonts-nanum*' fio qemu-kvm virt-manager libvirt-clients virtinst jq docker.io containerd libvirt-daemon-system
$sudo adduser `id -un` libvirt
$sudo adduser `id -un` libvirt-qemu
$sudo adduser `id -un` kvm
운영체제 1장 실습
시스템콜 실습
시스템 콜은 프로세스 생성이나 하드웨어 조작처럼 커널의 도움이 필요할때 사용합니다.
프로세스는 사용자 모드로 실행되지만 커널에 처리를 요청하면 예외처리가 발생하며 CPU모드가 커널모드로 바뀌고 동작합니다. 그리고 시스템콜 처리가 끝나면 다시 사용자 모드로 돌아와 프로세스 동작을 이어감
프로세스가 어떤 시스템콜을 호출하는지는 strace 명령어로 확인가능합니다.
컴파일 언어 GO 언어 helloworld가 어떤식으로 호출하는지 확인해봅시다.
strace를 사용하여 출력되는 모든 줄들은 시스템 콜입니다. -- 대략 160개의 시스템 호출이 나옴
대부분의 콜은 main()함수 전후의 프로그램 시작과 종료처리 시스템 콜이므로 신경쓰지 않고 우리는 화면을 호출하는 시스템 콜만 봅시다.
write() : 데이터를 화면이나 파일 등에 출력하는 시스템 콜 --> hello world\n을 표시해줍니다.
그러면 이제 인터프리터 언어도 동일한지 확인해볼까요?
python helloworld가 어떻게 호출되는지 봅시다.
앗 허가가 거부되는군요 이럴땐 chmod +x hello.py로 권한을 부여합시다.
chmod +x hello.py
GO와 마찬가지로 write을 호출하네요!
시스템 콜을 처리하는 시간 비율
논리CPU가 실행하고 있는 시간 비율은 sar 명령어를 사용하면 알 수 있습니다.
sar -P 0 1 1 ( -P 0 : 논리 CPU 0번 데이터 수집 , 그 다음 1은 1초마다 , 마지막 1은 한번만 수집 명령)
헤더 부분 설명
%user :사용자 영역에서 사용한 CPU 시간.
%nice : 프로세스의 우선순위에 따라 사용된 CPU의 시간
%system : system 영역에서 사용된 CPU 시간. -커널이 시스템 콜을 처리한 사용한 시간
%idele : 사용되지 않는 유휴 상태의 시간
%iowait : 다른 통신으로 인해 CPU의 작업이 일시적으로 대기하는데 소비된 CPU 시간 (입출력장치,보조기억장치 등등)
%steal : 가상머신에서 사용한 시간
무한 루프를 동작하면서 측정
taskset -c 0 ./inf-loop.py & : 논리 cpu 0 에서 무한루프 실행
while True:
pass
실행후 user에서 100%사용 하는것을 볼 수 있다.
시스템 콜을 호출하면서 측정
import os
while True:
os.getppid()
%system이 많아진것을 확인 할 수 있습니다! 33%정도 사용하네요
※이렇게 시스템이 제대로 동작하는지 sar 명령어를 비롯한 도구를 사용해서 시스템 통계정보를 수집하는 것은 중요합니다.
이렇게 수집,관리하는 구조를 모니터링이라 하며, 도구로는 프로메테우스,자빅스,데이터독이 유명합니다.
인간이 계속해서 확인하는건 힘들고, 정상상태와 이상상황을 미리 정의해두고 관리자에세 경고를 알립니다.
그리고 이를 확인하고 최종적으로 사람이 문제해결하는데 대시보드를 사용하여 가시화합니다.
시스템 콜 소요시간
strace -T 옵션을 사용하면 시스템 콜 처리에 걸린시간을 확인 가능합니다.
GO언어의 helloworld : 0.000101 초가 걸린것을 확인할 수 있습니다.
라이브러리
표준 C 라이브러리
프로그램이 어떤 라이브러리를 링크하는지 확인 : ldd
출력결과 libc.so.6을 사용하며 이는 표준 C 라이브러리를 뜻합니다.
그리고 /lib64/ld-linux-x86-64.so.2의 경우 운영체제에서 제공하는 특별한 라이브러리 입니다.
cat의 경우에도 동일한 라이브러리를 사용합니다.
파이썬의 경우
마찬가지로 libc.so.6을 사용하며 내부적으로 C 라이브러리를 사용하는 것을 확인할 수 있습니다.
시스템 콜 래퍼 함수
시스템 콜은 일반 함수 호출과는 다르게, 아키텍처에 의존하는 어셈블리 코드를 사용해서 호출해야합니다.
x86_64 아키텍처에서 getppid() 시스템 콜은 아래처럼 호출됩니다.
mov $0x6e, $eax
syscall
eax 레지스터에 getppid의 시스템 콜 번호 0x6e를 대입합니다.
이후 syscall을 호출하면서 커널 모드로 전환합니다.
이후 getppid를 처리하는 코드가 실행됩니다.
이때 만일 libc의 도움이 없다면 시스템 콜을 호출하는 아키텍처 의존 어셈블리를 호출해야 하며 이렇게 되면 아키텍처가 달라질때 마다 동작을 보장할 수 없습니다.
따라서 아키텍처 별로 어셈블리 코드를 만드는 것보다, OS에 있는 wrapper 함수를 사용하는게 효율적입니다.
운영체제 2장 실습
프로세스 생성의 목적
목적 1 : 같은 프로그램의 처리를 여러 프로세스가 분산해서 처리 (웹 서버의 리퀘스트 처리 등) -fork()
목적 2 : 전혀 다른 프로그램 생성 (bash에서 여러 프로그램 실행 등) -fork() & execve()
위의 작업을 할 때 fork() 함수와 execve() 함수가 호출되며, 내부적으로는 clone() 과 execve() 시스템 콜이 호출된다.
같은 프로세스를 2개로 분열시키는 fork()
프로세스의 복사본을 만들는 함수!
동작 순서
- 부모프로세스가 fork()를 호출한다
- 자식 프로세스용 메모리영역을 확보후 그곳에 부모 프로세스를 복사한다.
- 부모, 자식 프로세스 모두 fork()에서 복귀한다.
- 둘 모두 fork()반환값이 달라 처리 분기가 가능하다.
실행 중인 프로세스에서 자식 프로세스를 생성한다.
프로세스 생성 순서
자식 프로세스용 메모리 영역을 작성하고 부모 프로세스의 메모리를 복사한다.
fork() 함수의 리턴 값이 다른 것을 이용하여 코드를 분기한다.
fork.c 프로그램 예제
프로세스를 새로 만든다.
부모 프로세스는 자신의 PID와 자식 프로세스의 PID를 출력한다.
자식 프로세스는 부모의 PID와 자신의 PID를 출력하고 종료합니다.
import os, sys
ret = os.fork()
if ret == 0:
print("자식 프로세스: pid={}, 부모 프로세스의 pid={}".format(os.getpid(), os.getppid()))
exit()
elif ret > 0:
print("부모 프로세스: pid={}, 자식 프로세스의 pid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
부모 = 19812 , 자식 =19813 다른 PID를 가진 프로세스가 나오는걸 확인할 수 있다!
다른 프로그램을 기동하는 execve()
전혀 다른 프로세스로 치환하는 함수이다.
동작순서
- execve()를 호출합니다.
- execve()로 지정한 파일을 읽어서 프로그램을 메모리에 배치하는데 필요한 정보를 가지고옴
- 현재 프로세스 메모리를 가져온 프로세스 데이터로 치환합니다.
- 새로운 프로세스의 최초 실행 명령부터 실행한다.
execve() 예제
fork()로 분기한 후 자식 프로세스를 읽어드린 파일의 프로세스로 치환함
import os, sys
ret = os.fork()
if ret == 0:
print("자식 프로세스: pid={}, 부모 프로세스 pid={}".format(os.getpid(), os.getppid()))
os.execve("/bin/echo", ["echo", "pid={}에서 안녕".format(os.getpid())], {})
exit()
elif ret > 0:
print("부모 프로세스: pid={}, 자식 프로세스 pid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
자식 프로세스에서 안녕이라는 메세지가 추가된 것을 확인 할 수 있다
실행 파일의 구성 요소
- 코드와 데이터
- 코드 영역의 파일상 오프셋, 사이즈, 메모리 맵 시작 주소
- 데이터 영역의 파일상 오프셋, 사이즈, 메모리 맵 시작 주소
- 최초로 실행할 명령의 주소 (엔트리 포인트)
프로그램을 실행하면 위의 정보들을 바탕으로 메모리에 매핑하고, 엔트리 포인트부터 명령을 실행한다.
리눅스의 실행 파일은 ELFExecutable and Linkable 포맷형식을 사용하는데,
readelf -h : 엔트리 포인트 확인 명령어
readelf -s : 코드, 오프셋 크기, 시작 주소 확인 명령어
모두 볼 필요는 없고 지금은 간단하게 이해만 하면 된다.
- 실행파일은 여러 영역에 나눠져 있고 각각을 섹션이라 부른다.
- 섹션정보는 두줄이 한묶음이다.
- 숫자는 모두 16진수
- 섹션의 중요 정보는 다음과 같다.
- 섹션명 : 두번째 필드 (Name)
- 메모리 맵 시작 주소 : 네번째 필드(Adress)
- 파일 오프셋 : 다섯번째 필드 (Offset)
- 크기 : 두번째 줄 첫번째 필드 (Size)
- 섹션명이 .text라면 코드 섹션 , data라면 데이터 섹션
/proc/PID번호/maps 파일을 통해 프로세스의 메모리 맵을 볼 수 있다.
'컴퓨터 사이언스 > 컴퓨터 구조 & 운영체제' 카테고리의 다른 글
[혼공컴운] 4주차 미션 (1) | 2024.01.30 |
---|---|
[혼공컴운] 파이썬을 지우면..... (0) | 2024.01.28 |
[혼공컴운] 운영체제 chapter 11 (0) | 2024.01.28 |
[혼공컴운] 운영체제 chapter 10 (0) | 2024.01.28 |
[혼공컴운] 운영체제 Chapter 9 (0) | 2024.01.26 |