안녕하세요, 헤일러입니다.
4월 중순에 시작했던 컴구웅 스터디가 어느덧 마무리 되었네요!
스터디 팀원들과 함께 계획했던 기간 안에 책 한 권을 완주했다는 점이 정말 뿌듯하네요! 😊
아래는 학습한 내용 목차입니다.
실행에 필요한 페이지만 메모리에 로드하는 기법을 요구 페이징이라고 합니다.
요구 페이징은 기본적으로 아래와 같은 절차로 수행됩니다.
1. CPU가 특정 페이지에 접근하는 명령어를 실행
2. 해당 페이지가 현재 메모리에 있을 경우(유효 비트가 1일 경우) CPU는 페이지가 로드된 프레임에 접근
3. 해당 페이지가 현재 메모리에 없을 경우(유효 비트가 0일 경우) 페이지 폴트 발생
4. 페이지 폴트 처리 루틴을 통해 해당 페이지를 메모리로 로드하고 유효 비트를 1로 설정
5. 다시 1번 수행
이 요구 페이징 시스템이 안정적으로 작동하기 위해서는 페이지 교체, 프레임 할당 두 가지 문제를 해결해야 합니다.
페이지들을 로드하다 보면 언젠가 메모리가 가득 차게 되고, 로드된 페이지 일부를 보조기억장치로 내보내야 합니다. 이 때 어떤 페이지를 내보낼지 결정하는 방법이 페이지 교체 알고리즘입니다.
일반적으로 페이지 폴트를 적게 발생시키는 알고리즘을 좋은 페이지 교체 알고리즘이라고 말합니다. 페이지 폴트가 자주 발생했다는 것은 페이지 교체 알고리즘이 메모리에서 내보내면 안 되는 페이지를 보조기억장치로 자주 내보냈다는 의미고, 이는 성능 저하로 이어지기 때문입니다.
그렇다면 페이지 교체 알고리즘이 좋은지 나쁜지 평가하기 위해 페이지 폴트 발생 횟수를 계산할 수 있어야 겠죠? 이 페이지 폴트 발생 횟수는 보통 페이지 참조열을 기반으로 시뮬레이션하여 계산합니다.
페이지 참조열은 CPU가 참조하는 페이지들 중 연속된 페이지를 생략한 페이지열을 말합니다.
예를 들어 CPU가 아래와 같은 순서로 페이지에 접근했다고 가정해보겠습니다.
1 2 2 5 6 7 7 7 7 2 2
연속으로 중복된 페이지를 생략하면 아래의 페이지 참조열이 됩니다.
1 2 5 6 7 2
메모리에 오래 머무른 페이지부터 내보내!
FIFO 페이지 교체 알고리즘은 이름 그대로 메모리에 가장 먼저 올라온 페이지부터 내보내는 방식입니다. 아이디어와 구현이 간단하지만, 그다지 실용적이지는 않습니다. 메모리에 오래 머문 페이지가 자주 사용되지 않을 페이지라는 것의 상관 관계가 모호하기 때문입니다.
📌 2차 기회 페이지 교체 알고리즘(Second Chance Page Replacement Algorithm)
2차 기회 페이지 교체 알고리즘은 FIFO 페이지 교체 알고리즘과 동일하게 메모리에 가장 오래 머물렀던 페이지를 내보낼 페이지로 선택합니다. FIFO 페이지 교체 알고리즘과 다른 점은 페이지의 참조 비트가 1일 경우 바로 내보내지 않고 참조 비트를 0으로 만든 뒤, 가장 최근 적재한 것으로 취급합니다. 참조 비트가 1일 경우 메모리에 머물 기회를 한 번 더 주는 방식입니다.
가장 먼 미래에 참조될 페이지부터 메모리에서 내보내!
최적 페이지 교체 알고리즘은 미래의 페이지 참조열을 참고해, 가장 나중에 참조될 페이지를 선택해 메모리에서 내보내는 그리디 방식의 알고리즘입니다. (미래의 페이지 참조열의 순서가 정확하다는 가정 하에) 페이지 폴트가 최소로 발생하는 방법이기 때문에 가장 낮은 페이지 폴트율을 보장합니다.
하지만 프로세스가 미래에 어떤 페이지를 어떤 순서로 사용하게 될 지, 얼마만큼의 미래까지 예측해야 할 지 완벽하게 파악하는 것은 현실적으로 불가능에 가깝기 때문에 실제로 사용할 수 없는 알고리즘입니다.
최적 페이지 교체 알고리즘은 다른 페이지 교체 알고리즘의 성능을 평가할 때 용이합니다. 이미 실행이 지나간 페이지 참조열에 대해, "최적 페이지 교체 알고리즘으로 수행했다면 n번의 페이지 폴트가 발생했을 것이다"를 하한선으로 두고 그 차이를 성능의 지표로 사용할 수 있습니다.
최근에 사용되지 않은 페이지는 앞으로도 덜 사용될거야!
LRU 페이지 교체 알고리즘은 가장 오랫동안 사용되지 않은 페이지를 교체하는 알고리즘입니다. 오래 사용되지 않은 페이지는 앞으로도 사용되지 않을 가능성이 높다는 지역성(Locality) 원리를 활용하는 방법입니다.
하지만 가장 오래 사용되지 않은 페이지를 알기 위해서는 모든 페이지의 참조 이력을 기록하고 추적해야 하기 때문에 정확한 LRU 방식을 사용하기는 현실적으로 어렵습니다.
따라서 운영체제에서는 Clock 알고리즘, Aging 알고리즘과 같은 LRU 근사 알고리즘을 사용합니다.
📌 Clock 알고리즘
Clock 알고리즘은 순환 큐 형태로 페이지를 관리하고, 시계 바늘로 순환 큐를 순회하며, 참조 비트의 상태에 따라 교체할 페이지를 결정하는 방식의 알고리즘입니다.
페이지에 참조가 발생하면 참조 비트(R bit
)를 1로 설정
페이지 교체가 필요할 때 시계 바늘이 가리키는 페이지를 평가
R bit == 0
: 해당 페이지를 교체R bit == 1
: R bit = 0
으로 리셋하고, 시계 바늘 1칸 회전R bit == 0
인 페이지)가 나올 때까지 시계 바늘 회전📌 Aging 알고리즘
Aging 알고리즘은 각 페이지마다 참조 비트와 n bit 크기의 카운터를 두어, 시간 흐름에 따라 카운터에 참조 기록을 반영하는 방식의 알고리즘입니다.
정해진 주기마다 아래의 동작을 수행합니다.
모든 페이지에 대해 카운터 = 카운터 >> 1
모든 페이지를 순회하며 R bit == 1
이면
카운터 |= 10000000
)R bit = 0
)페이지 폴트가 자주 발생하는 이유가 나쁜 페이지 교체 알고리즘 때문만은 아닙니다. 각 프로세스가 사용할 수 있는 프레임 수가 너무 적어도 페이지 폴트가 자주 발생합니다.
나쁜 페이지 교체 알고리즘, 적절하지 않은 프레임 할당 방식으로 인해 페이지 교체가 자주 발생하게 되면, CPU가 계속 페이지 폴트를 처리하느라 프로세스의 작업을 제대로 처리하지 못하고 디스크 I/O만 계속 발생시키게 됩니다. 이로 인해 전체 시스템 성능이 저해되는 현상을 스래싱이라고 합니다.
일반적으로 스래싱이 발생하는 상황
스래싱 해결 방법
단순히 실행 전 프로세스의 크기만을 고려한 방식입니다. 프로세스마다 프레임 수를 고정적으로 할당합니다. (ex. A 프로세스에 10개, B 프로세스에 20개 고정 할당)
프로세스의 프레임 수가 너무 부족하면 스래싱이 자주 발생하고, 프레임 수가 과하면 자원 낭비가 일어납니다. 따라서 프레임 수를 상황에 따라 유연하게 조정할 수 없는 정적 할당 방식은 다음과 같이 제한적인 경우에만 사용하는 것이 좋습니다.
이러한 정적 할당 방식에는 균등 할당 방식, 비례 할당 방식이 있습니다.
모든 프로세스에 동일한 수의 프레임을 할당하는 방식입니다.
📌 수식
프로세스의 전체 페이지 수에 비례하여 프레임을 할당하는 방식입니다.
📌 수식
균등 할당 방식은 구현이 단순한 대신, 프레임 부족/낭비 문제가 발생할 가능성이 높습니다.
비례 할당 방식은 조금 더 그럴듯한 접근이지만, 프로세스의 전체 용량의 크기와 실행에 필요한 페이지 수는 다를 수 있습니다. 크기가 크더라도 막상 실행해보면 필요한 프레임 수가 많지 않거나, 반대로 프로세스 크기는 작지만 실행해보니 많은 프레임을 필요로 할 수 있습니다.
동적 프레임 할당은 프로세스 실행 중 프로세스의 메모리 사용량 변화에 따라 프레임 수를 조절하는 방식입니다.
동적 할당 방식은 프로세스 실행 중에 할당할 프레임 수를 가변적으로 조정할 수 있기 때문에 스래싱을 방지하고 자원을 효율적으로 사용하는데 유리하지만, 구현이 복잡하고 프레임 수를 계산하는 과정에서 오버헤드가 조금 있다는 것이 단점입니다. 하지만 단점보다 이점이 훨씬 크기 때문에 현대 운영체제는 대부분 동적 프레임 할당 방식을 사용하고 있습니다.
동적 프레임 할당 방식이 유리한 경우
동적 프레임 할당 방식이 불리한 경우
이러한 동적 할당 방식에는 작업 집합 모델 기반 프레임 할당 방식, 페이지 폴트 빈도 기반 할당 방식이 있습니다.
작업 집합 모델 기반 프레임 할당은 프로세스가 일정 기간 동안 참조한 페이지 집합(작업 집합)을 기준으로 필요한 만큼의 프레임을 동적으로 할당하는 방식입니다.
장점
단점
📌 수식
페이지 폴트 빈도 기반 프레임 할당은 아래 2개의 전제 조건으로부터 파생된 아이디어입니다.
따라서 프로세스의 페이지 폴트율을 계산하여 일정 임계값을 넘으면 프레임 수를 증가시키고, 임계 값보다 낮으면 프레임 수를 감소시킵니다.
장점
단점
의미가 있는 정보를 담는 논리적인 단위
속성 | 설명 |
---|---|
위치 | 파일의 보조기억장치 상의 현재 위치 |
크기 | 파일의 바이트 단위 크기 |
접근 권한 | 읽기/쓰기/실행 권한 |
타입 | 일반파일, 디렉토리, 심볼릭 링크 등 |
소유자(Owner) | 파일을 소유한 사용자 |
Change Time (ctime) | inode 정보가 마지막으로 변경된 시간. 메타데이터가 바뀐 시간. ex. 권한, 소유자, 링크 수 등의 변화 |
Modification Time (mtime) | 파일 내용이 마지막으로 수정된 시간 ex. write() 시스템 콜 호출 시 갱신 |
Access Time (atime) | 파일에 마지막으로 접근한 시간 ex. open(), read() 시스템 콜 호출 시 갱신 |
# 파일 생성
touch hello.txt
# mtime, ctime이 같음
# 파일 내용 수정
echo "hi" >> hello.txt
# mtime, ctime 모두 갱신
# 권한 변경
chmod 000 hello.txt
# mtime은 그대로, ctime만 갱신됨
# 읽기
cat hello.txt
# atime만 갱신됨
유형 | 기호 (ls -l 기준) | 설명 |
---|---|---|
일반 파일 (Regular File) | - | 텍스트, 바이너리, 실행파일 등 일반 파일 |
디렉터리 (Directory) | d | 다른 파일들의 목록을 관리하는 디렉터리 |
심볼릭 링크 (Symbolic Link) | l | 다른 파일의 경로를 참조하는 파일 |
블록 장치 파일 (Block Device) | b | HDD, SSD, USB 등 블록 단위로 데이터 입출력을 하는 장치 |
문자 장치 파일 (Character Device) | c | 키보드, 마우스, 직렬 포트처럼 바이트 단위 입출력을 하는 장치 |
FIFO (Named Pipe) | p | 두 프로세스 간 파이프 기반 IPC 통신을 위한 특수 파일. 선입선출 구조. |
소켓 (Socket) | s | 클라이언트-서버 간 IPC 통신을 위한 특수 파일 |
open()
int open(const char *pathname, int flags, mode_t mode);
int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open failed");
exit(1);
}
📌 open()
주요 플래그들
Flag | 의미 |
---|---|
O_RDONLY | 읽기 전용 |
O_WRONLY | 쓰기 전용 |
O_RDWR | 읽기, 쓰기 모두 가능 |
O_CREAT | 파일이 없으면 생성 |
O_TRUNC | O_WRONLY 또는 O_RDWR로 open시에 이미 존재하는 파일이면 파일의 내용을 삭제하고 크기를 0으로 만들어 open |
O_APPEND | 파일 중간에 write 금지. 파일 끝에만 write 가능한 open. |
close()
int close(int fd);
close(fd);
read()
ssize_t read(int fd, void *buf, size_t count);
char buffer[64];
int n = read(fd, buffer, sizeof(buffer));
write()
ssize_t write(int fd, const void *buf, size_t count);
write(fd, "Hello\n", 6);
lseek()
off_t lseek(int fd, off_t offset, int whence);
lseek(fd, 0, SEEK_SET); // 파일 처음으로 이동
디렉터리 또한 파일입니다. 다른 파일이나 디렉터리에 대한 메타데이터(UNIX에서는 inode 번호) 목록을 담고 있는 특수한 목적의 파일입니다.
경로는 디렉터리를 이용해 파일 위치, 파일 이름을 특정 짓는 정보입니다.
DIR *opendir(const char *name); // 새 디렉터리 생성
DIR *dp = opendir(".");
int closedir(DIR *dirp);
closedir(dp);
int mkdir(const char *pathname, mode_t mode);
mkdir("newdir", 0755); // Owner, Group, Others 권한: rwx r-x r-x
int rmdir(const char *pathname); // 비어있는 디렉터리 삭제
rmdir("newdir");
struct dirent *readdir(DIR *dirp); // 열려있는 디렉터리에서 항목 하나 읽기
struct dirent *ep = readdir(dp);
📌struct dirent
: 디렉터리 내의 개별 엔트리 정보
struct dirent {
ino_t d_ino; // inode 번호
off_t d_off; // 다음 dirent 오프셋
unsigned short d_reclen; // 전체 레코드 길이
unsigned char d_type; // 파일 타입: DT_REG(일반 파일), DT_DIR(디렉터리), DT_LNK(심볼릭 링크)
char d_name[256]; // 파일/디렉터리 이름
};
용어 | 설명 |
---|---|
디스크 | 데이터를 저장하기 위한 물리적 저장장치 (ex. HDD, SSD) |
파티션 | 디스크를 논리적으로 나눈 구역 |
파티셔닝 | 디스크를 여러 개의 파티션으로 나누는 작업 |
파일 시스템 | 저장장치에 파일을 효율적으로 저장하고 관리하는 방법 |
포매팅 | 파티션에 특정 파일 시스템을 생성하여, 운영체제가 파일을 저장하고 읽을 수 있도록 초기화하는 작업 |
Windows: NTFS(New Technology File System)
MacOS: APFS(Apple File System)
Linux: ext4(Fourth Extended File System), XFS
Android OS: ext4
번외: FAT32
운영체제는 파일과 디렉터리를 블록 단위로 읽고 씁니다. 하드 디스크의 가장 작은 저장 단위는 섹터지만, 운영체제는 하나 이상의 섹터를 블록이라는 단위로 묶어 블록 단위로 파일을 관리합니다.
📌 디스크 단위의 종류
단위 | 설명 |
---|---|
섹터 (Sector) | 디스크에서 데이터를 읽고 쓸 수 있는 가장 작은 단위. 전통적인 디스크에서는 1섹터=512 bytes를 사용하고, Advanced Format을 사용하는 고용량 HDD/SDD에서는 1섹터 = 4096 bytes(4KB)를 사용합니다. |
블록 (Block) or 클러스터(Cluster) | 파일 입출력의 최소 단위. 블록과 클러스터는 개념적으로는 동일합니다. UNIX 계열에서 블록이라는 용어를 사용하고, Windows에서 클러스터라는 용어를 사용합니다. |
파티션 (Partition) | 하나의 물리 디스크를 논리적으로 나눈 구역. 각 파티션은 서로 독립적인 파일 시스템을 가질 수 있습니다. |
볼륨 (Volume) | OS에서 인식하는 하나의 저장 장치 단위. 보통 하나의 파티션 = 하나의 볼륨이지만 RAID, LVM 등 특수한 경우 여러 파티션이 하나의 볼륨이 될 수도 있습니다. |
그리고 파일을 보조기억장치에 할당하는 방법은 연속 할당과 불연속 할당 두 가지가 있습니다. 이 둘에 대해 살펴보겠습니다.
보조기억장치 내 연속적인 블록에 파일을 할당하는 방법입니다.
연속으로 할당된 파일에 접근하기 위해 파일의 첫 번째 블록 주소와 블록 단위의 길이만 알면 됩니다.
연속된 데이터 블록을 연결 리스트로 관리하는 방법입니다. 각 블록이 다음 블록의 주소를 저장하고 있습니다.
색인 블록에 특정 파일의 모든 블록 주소를 배열로 저장하는 방법입니다.
배열로 관리하기 때문에 블록 인덱스를 이용해서 임의의 블록 위치에 접근하기 쉽습니다. 특정 파일의 i번째 블록에 접근하고 싶다면 색인 블록의 i번째 항목이 가리키는 블록에 접근하면 됩니다. UNIX 파일 시스템은 이 색인 할당 방법을 사용하고 있습니다.
FAT는 연결 할당의 단점을 보완한 파일 시스템입니다. 색인 할당 방법과 비슷한데요. 파일 할당 테이블(FAT:File Allocation Table)이라는 전역 테이블에 다음 블록들의 주소 정보를 테이블 하나에 전부 모아 놓은 방식을 사용합니다.
다음 블록이 없는 경우는 보통 EOF(-1 = 0xFFFF
)로 표시합니다.
FAT로 포매팅된 파티션은 물리적으로 메모리 공간이 아래 그림과 같이 4개의 영역으로 구분되어 사용됩니다.
📌 BPB(BIOS Parameter Block)?
BPB는 특정 파티션의 파일 시스템 구조 정보를 담고 있는 영역으로 파티션의 첫 번째 섹터에 존재합니다. 해당 파티션의 목차 같은 역할이라 생각하면 됩니다.
FAT 영역
루트 디렉터리 영역(FAT12/16에 존재)
📌 FAT32
FAT32에서는 고정된 루트 디렉터리 영역이 따로 존재하지 않습니다. 루트 디렉터리 엔트리도 일반 디렉터리처럼 데이터 영역에 위치하고 있습니다.
루트 디렉터리의 시작 클러스터 번호만 BPB의 Root Directory Cluster 필드에 저장되고, 루트 디렉터리 엔트리의 내용은 데이터 영역에 위치합니다.
UNIX 파일 시스템에서는 색인 할당 방식을 사용하고 있고, 색인 블록을 inode라고 부릅니다. 각 파일마다 고유한 inode가 존재합니다. inode는 고유한 번호가 부여되어있고, inode 정보는 파티션 내 inode 영역이라는 특정 영역에 모여 있습니다.
그리고 이 inode에는 파일 속성 정보와 15개의 블록 주소가 저장될 수 있습니다.
그런데 한 파일의 크기가 15개의 블록 크기보다 큰 경우는 어떻게 할까요?
메인보드에 따라 BIOS로 부팅이 시작되는 컴퓨터가 있고, UEFI로 부팅이 시작되는 컴퓨터가 있습니다. 그리고 부팅 가능한 디스크는 파티션 형식을 MBR 방식과 GPT 방식 중 하나를 반드시 선택해야 합니다.
그래서 부팅 방식과 파티션 형식의 총 4가지의 조합이 존재할 수 있는데요. 각 부팅 방식과 파티션 형식에 대해 간략하게 학습해 보았습니다.
특징
MBR보다 나은 점
특징
BIOS보다 나은 점
UEFI + GPT는 BIOS + MBR보다 부팅 방식이 단순해졌습니다.
.efi
파일(ex. bootx64.efi
, shim.efi
)이 존재하면 메모리에 로드하여 실행합니다..efi
실행 단계한 저장 장치의 파일 시스템에서 다른 저장 장치의 파일 시스템에 접근할 수 있도록 파일 시스템을 편입시키는 작업을 마운트라고 부릅니다.