선언된 구조체 s 는 멤버 변수로 1바이트 크기의 문자형 타입 c, 4바이트 크기의 정수형 i 를 갖습니다. (C언어 변수 타입에 대한 표준 정의는 ISO/IEC 9899:TC3, Programming languages — C 문서의 p21에 기술되어 있습니다.) 이를 토대로 유추되는 sizeof(st) 의 결과 값은 5입니다. 정말 그럴까요?
프로그램을 실행해 보면 실제로 출력되는 값은 sizeof(st) 의 결과 값은 8 입니다. 왜 이런 결과값이 출력되는 걸까요?
컴퓨터는 32비트/64비트 CPU 아키텍쳐에 따라 4바이트 혹은 8바이트 단위로 메모리에 접근합니다. 만약 32비트 컴퓨터에서 1바이트 크기의 데이터에 접근하려고 하면 추가적인 시프트 연산을 해야 하기 때문에 효율이 떨어집니다. 그래서 C언어 컴파일러는 데이터 접근의 효율성을 위해서 구조체를 일정한 크기로 정렬을 하게 됩니다. 이를 구조체 정렬(Data structure alignment)이라고 하며 이를 위해 남는 공간의 메모리를 채우는 것을 메모리 패딩이라고 합니다.
tmux는 terminal multiplexer의 약자로서, 하나의 터미널에서 여러 프로그램을 쉽게 전환하고 분리하며 다른 터미널에 다시 연결할 수 있습니다. 주요한 특징으로는 터미널을 빠져 나오거나 전환하더라도 기존 진행중이던 작업이 백그라운드에서 계속 실행되고 있기 때문에 갑작스럽게 터미널 연결이 끊기게 되더라도 중단없이 작업을 이어가실수 있습니다. 또한, tmux는 서버에 설치되기 때문에 다른 환경의 클라이언트에서 접속을 하더라도 동일한 기능을 사용할 수 있습니다. 기존에 사용되던 screen 이 tmux 로 대체되는 추세입니다.
tmux를 사용하기 전에 몇가지 알고 가셔야 되는 내용이 있습니다.
session : tmux 실행 단위. 여러개의 window로 구성.
window : 터미널 화면. 세션 내에서 탭처럼 사용할 수 있음.
pane : 하나의 window 내에서 화면 분할.
status bar : 화면 아래 표시되는 상태 막대.
tmux 설치
지금 부터 tmux를 설치해 보도록 하겠습니다. 현재(2020.08.21) 시점에서 tmux 최신버전은 3.1a 입니다. 아래에서는 패키지 관리자를 통해서 설치하는 방법이 아닌 직접 tmux 오픈 소스를 빌드 후 설치하는 방법을 설명합니다. 패키지 관리자를 통해서 설치하셔도 무관하지만 되도록 tmux 2.6 버전 이상을 사용하시는 것을 추천드립니다. 왜냐하면 해당 버전을 기준으로 설정 파일(~/.tmux.conf)을 세팅하는 방법이 달라졌기 때문입니다.
Stop after the preprocessing stage; do not run the compiler proper. The output
is in the form of preprocessed source code, which is sent to the standard output.
Input files that don’t require preprocessing are ignored.
결과적으로, gcc -E 옵션은 preprocess 단계에서의 소스 코드를 확인할 수 있습니다.
실제로 바이너리에 포함되는 라이브리러 등의 정보 등을 확인할 때 -E 옵션을 유용하게 활용할 수 있습니다.
프로그램은 실행 가능한 바이너리와 바이너리가 실행되기 위해 필요한 데이터(라이브러리 등)으로 구성된다.
프로그램은 사용자에 의해서 실행되면 커널로 부터 CPU, 메모리 자원을 할당받고 프로세스로 실행된다.
이때 프로세스는 Process Address Space 구조에 의해 코드/데이터/힙/스택 영역으로 구분된 메모리 영역을 할당받는다. (https://jhnyang.tistory.com/32)
프로세스가 실행 중에 쓰레드를 생성하면 코드/데이터/힙 메모리 영역은 공유하고 스택 메모리에 해당 쓰레드를 위한 스택만 생성한다.
리눅스 커널은 메모리를 관리할때 프로세스가 요청하는 메모리를 가상 메모리는 할당하지만 실제 메모리는 사용할때 할당된다. (첨부된 메일의 memory.c 코드를 통해 확인)
해당 이슈처럼 과도하게 쓰레드가 생성되는 경우(과도하게 생성되었다기 보다 사용이 완료된 쓰레드 자원이 정리되지 않은 것으로 보임) 실제 시스템에 메모리는 충분하지만 스택 메모리를 할당하기 위한 가상 메모리 주소 공간이 부족할 경우가 발생할수 있다.
그렇기 때문에 해당 이슈는 JVM에서 java.lang.OutOfMemoryError: thread creation failed 로그를 출력하고 새로운 쓰레드 생성을 위한 스택 메모리를 할당하지 못하는 것이다.
일반적인 리눅스 환경에서는 /etc/resolv.conf 파일에 DNS 주소값을 세팅합니다.
그러나, 우분투가 12.04 부터는 /etc/resolvconf 디렉토리와 /etc/network/interfaces 등을 종합하여 /etc/resolv.conf를 덮어씌웁니다.
아래와 같이 /etc/resolvconf 디렉토리에 있는 base 파일에 DNS 서버를 설정하면, /etc/resolv.conf 에 설정내용이 반영됩니다.
쉘 스크립트에는 $? 라는 예약된 특수변수가 있다.
이 특수변수는 이전에 실행된 자식 프로세스의 main() 함수 리턴값을 의미한다.
성공시 0, 실패시 non-zero 을 리턴한다.
이를 스크립트에 넣으면 다음과 같은 코드가 된다.
make 어쩌구 저쩌구
if [ $? -ne 0 ]; then
echo "Fail(make) ... $?"
exit 1
fi
makefile은 error가 나면 중지되겠지만,
make를 호출한 script는 make의 다음 문장을 계속 수행하겠지요.
bash의 경우 -e 옵션을 주면 error가 발생하면 종료하게 됩니다.
예를 들어 #!/bin/bash -e 를 script의 첫줄에 써주면 됩니다.
이번 장은 비례 배분(Proportional Share) 스케줄링을 다룹니다. 앞의 장들이 반환 시간, 응답 시간, 대화형 작업의 반응성에 초점을 맞췄다면, 이 장의 핵심 질문은 조금 다릅니다. 여러 작업이 있을 때 CPU를 정해진 비율로 나누어 줄 수 있는가가 주제입니다.
OSTEP은 그 대표적인 방법으로 Lottery Scheduling을 소개합니다. 이름 그대로 CPU를 사용할 다음 작업을 추첨으로 고르되, 각 작업이 가진 추첨권(ticket)의 수가 CPU 몫을 나타냅니다.
한 문장 요약
Lottery scheduling은 각 작업에 추첨권을 나누어 주고, 매 타임 슬라이스마다 무작위 추첨으로 실행할 작업을 골라 장기적으로 티켓 비율에 가까운 CPU 배분을 달성하는 스케줄링 방식입니다.
핵심 질문
질문
핵심 답변
CPU를 정해진 비율로 배분하려면 어떻게 해야 하는가?
각 작업에 추첨권을 나누어 주고, 작업의 티켓 수를 전체 티켓 수로 나눈 값이 CPU 몫이 되게 합니다.
Lottery scheduling은 어떻게 다음 작업을 고르는가?
전체 티켓 범위에서 난수를 하나 뽑고, 준비 큐를 순회하며 누적 티켓 수가 당첨 번호를 넘는 첫 작업을 실행합니다.
무작위 방식인데 공정한가?
짧은 구간에서는 목표 비율과 어긋날 수 있지만, 실행 시간이 길어질수록 실제 CPU 배분은 티켓 비율에 가까워집니다.
Stride scheduling은 왜 등장하는가?
Lottery scheduling의 무작위성을 없애고 비례 배분을 결정론적으로 달성하기 위해 등장합니다.
비례 배분과 티켓
Lottery scheduling에서 티켓은 CPU 지분을 표현하는 단위입니다. 예를 들어 A가 75장, B가 25장의 티켓을 가지고 있으면 전체 티켓은 100장이고, 장기적으로 A는 약 75%, B는 약 25%의 CPU를 받게 됩니다.
작업
티켓 수
기대 CPU 몫
A
75
75%
B
25
25%
전체
100
100%
스케줄러는 매 타임 슬라이스마다 전체 티켓 중 하나를 뽑고, 그 티켓을 가진 작업을 실행합니다. 중요한 점은 이 방식이 매 순간 정확한 비율을 보장하는 것이 아니라 확률적으로 장기 비율에 가까워지는 방식이라는 점입니다.
추첨 과정
준비 큐에 A, B, C가 있고 티켓 수가 각각 100, 50, 250이라고 가정해 보겠습니다. 전체 티켓은 400장입니다. 당첨 번호가 300이면, 누적합이 300을 처음 넘는 C가 선택됩니다.
그림 12.1: 당첨 번호를 뽑고, 작업 리스트를 순회하며 누적 티켓 수가 당첨 번호를 넘는 작업을 찾습니다.
핵심 로직은 다음과 같이 정리할 수 있습니다.
1
2
3
4
5
6
7
8
counter=0
winner=random(0,total_tickets)
forjob inready_queue:
counter+=job.tickets
ifcounter>winner:
run(job)
break
이 구현은 관리해야 할 상태가 적습니다. 작업별 티켓 수와 전체 티켓 수만 있으면 됩니다. 새 작업이 들어와도 해당 작업의 티켓 수를 추가하고 전체 티켓 수만 갱신하면 됩니다.
무작위성의 의미
Lottery scheduling은 무작위 추첨을 사용하므로 짧은 실행 구간에서는 불공정해 보일 수 있습니다. A와 B가 같은 티켓 수를 가지고 있어도, 짧은 시간 동안은 한쪽이 더 자주 뽑힐 수 있습니다.
그림 12.2: 작업 길이가 길어질수록 추첨 결과는 목표 비율에 가까워지고, 불공정 정도는 완화됩니다.
실행 기간
특징
짧음
운의 영향이 커서 목표 비율과 차이가 날 수 있습니다.
김
추첨 횟수가 늘어나 실제 비율이 목표 비율에 가까워집니다.
이 특성 때문에 lottery scheduling은 즉각적인 공정성보다 장기적인 확률 공정성이 중요한 상황에 더 잘 맞습니다.
티켓을 다루는 기법
OSTEP은 티켓을 더 유연하게 다루기 위한 세 가지 기법을 소개합니다.
기법
의미
사용 예
Ticket currency
사용자나 그룹이 자기 기준의 로컬 티켓을 쓰고, 시스템이 전역 티켓으로 환산합니다.
사용자별 몫은 같게 유지하되, 각 사용자가 자기 작업 안에서 자유롭게 지분을 나눕니다.
Ticket transfer
한 작업이 다른 작업에 티켓을 잠시 넘깁니다.
클라이언트가 서버에 요청을 맡긴 동안 서버가 클라이언트의 티켓까지 받아 빠르게 처리합니다.
Ticket inflation
작업이 자신의 티켓 수를 일시적으로 늘리거나 줄입니다.
서로 신뢰 가능한 환경에서 급한 작업이 자기 CPU 몫을 임시로 키웁니다.
특히 ticket inflation은 신뢰할 수 없는 환경에서는 위험합니다. 아무 작업이나 티켓 수를 마음대로 늘릴 수 있다면 CPU를 독점할 수 있기 때문입니다.
Stride Scheduling
Stride scheduling은 lottery scheduling과 같은 비례 배분 목표를 결정론적으로 달성하려는 방식입니다. 핵심 값은 stride와 pass입니다.
값
의미
tickets
작업의 CPU 지분입니다.
stride
큰 수 / tickets로 계산합니다. 티켓이 많을수록 stride는 작아집니다.
pass
지금까지 CPU를 얼마나 받았는지 나타내는 누적 값입니다.
스케줄러는 항상 pass가 가장 작은 작업을 실행하고, 실행 후 그 작업의 pass에 stride를 더합니다.
그림 12.3: pass 값이 가장 작은 작업을 고르고, 실행 후 pass에 stride를 더하면 티켓 비율에 맞는 실행 순서가 만들어집니다.
예를 들어 A=100 tickets, B=50 tickets, C=250 tickets이고 큰 수를 10000으로 잡으면 stride는 A=100, B=200, C=40입니다. 한 주기에서 C는 5번, A는 2번, B는 1번 실행되며, 이는 티켓 비율 250:100:50과 일치합니다.
Lottery와 Stride 비교
구분
Lottery scheduling
Stride scheduling
선택 방식
무작위 추첨
가장 작은 pass 선택
비례 배분
장기적으로 근접
짧은 구간에서도 더 정확
상태 관리
작업별 티켓 수와 전체 티켓 수 정도면 충분
각 작업의 pass, stride를 유지해야 함
새 작업 추가
상대적으로 쉬움
새 작업의 초기 pass 설정이 민감함
핵심 장점
단순함, 낮은 상태 비용, 동적 변화에 강함
결정론적이고 비율 오차가 작음
실습 코드
이 장의 homework 시뮬레이터는 ostep-homework/cpu-sched-lottery/lottery.py입니다. 작업 목록 형식은 다음과 같습니다.
1
실행시간:티켓수
예를 들어 10:100,20:100은 실행 시간이 10인 작업과 20인 작업이 각각 100장의 티켓을 가진다는 뜻입니다. 숙제를 풀 때는 먼저 -c 없이 직접 계산하고, 이후 -c로 결과를 검산하는 흐름이 좋습니다.
정리
Lottery scheduling은 CPU 몫을 티켓 수로 표현하고 무작위 추첨으로 다음 실행 작업을 고르는 비례 배분 스케줄러입니다. 구현이 단순하고 상태가 적으며, 작업이 오래 실행될수록 실제 CPU 배분은 티켓 비율에 가까워집니다.
하지만 짧은 실행 구간에서는 운의 영향으로 불공정할 수 있고, 티켓을 누구에게 얼마나 줄지 정하는 문제도 쉽지 않습니다. Stride scheduling은 같은 목표를 결정론적으로 달성하지만, pass 같은 상태를 유지해야 하므로 동적인 작업 추가와 상태 초기화가 더 까다롭습니다.
따라서 이 장의 핵심은 CPU 스케줄링 목표가 항상 반환 시간이나 응답 시간만은 아니며, 때로는 정해진 지분을 얼마나 단순하고 유연하게 보장할 수 있는지도 중요하다는 점입니다.