반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 4.6 의 내용을 읽고 요약한 글입니다. 

 

4.6.1 프로그래머의 눈에 보이는 CPU

  • CPU는 단순하게 메모리에서 명령어를 읽어 실행하기만 하면 됨
  • 어떤 프로그램이든 컴파일러에 의해 기계 명령어로 변환될 뿐

4.6.2 CPU의 능력 범위: 명령어 집합

  • 명령어 집합은 우리에게 CPU가 할 수 있는 일을 알려줌
  • 서로 다른 형태의 CPU는 다른 유형의 명령어 집합을 가지고 있음

4.6.3 추상화: 적을수록 좋다

  • 1970년까지 컴파일러가 성숙하지 못해 신뢰도 낮았음
  • 많은 프로그램이 어셈블리어로 작성되었는데 명령어 집합이 더욱더 풍부해야되고 명령어 자체 기능도 더 강력해야 한다고 여겼음

4.6.4 코드도 저장 공간을 차지한다

  • 실행파일은 기게 명령어와 데이터를 모두 포함하며 디스크 저장 공간을 차지하고, 실행시에는 메모리에 적재 되어 메모리 저장 공간을 차지
  • 옛날 컴퓨터는 메모리의 크기가 몇 KB로 작았는데 더 많은 프로그램을 적재하려면 기계 명령어는 더 세밀하게 설계해서 프로그램이 차지하는 저장공간을 줄였어야 함

아래 3가지 요구 사항을 만족해야 함

  1. 하나의 기계 명령어로 더 많은 작업을 완료할 수 있도록
  2. 기계 명령어 길이 가변적 (간단한 명령어는 짧고, 복잡한 명령어는 길게)
  3. 밀도를 높여 공간을 절약하기ㅇ 위해 고도로 인코딩

4.6.5 필연적인 복잡 명령어 집합의 탄생

당시 산업계의 요구사항을 충분히 만족시켰지만 새로운 문제가 발생

  • CPU명령어 집합은 모두 hardwired (직접 연결) 방식으로 효율적이지만 유연성이 떨어짐

하드웨어를 변경하는 것은 번거롭지만 소프트웨어를 통해 대체 가능 → microcode 등장

단일 복잡 명령어 → 마이크로코드 ROM → 간단한 명령어들

  • 더 많은 명령어를 추가할 때 주요 작업은 마이크로 코드 수정에 집중되며 하드웨어 수정은 거의 필요하지 않기에 CPU 설계 복잡도를 낮출 수 있음

4.6.6 마이크로코드 설계의 문제점

  • 버그 수정이 훨씬 어렵고 마이크로코드 설계가 트랜지스터를 매우 많이 소모
반응형
반응형

 

 

이 글은  컴퓨터 밑바닥의 비밀 chapter 4.2 의 내용을 읽고 요약한 글입니다. 

 

4.2.1 컴퓨터의 CPU 사용률은 얼마인가?

  • 게임, 영상 편집, 이미지처리 같은 일을 하지 않는 한 대부분 컴퓨터의 CPU 사용률은 낮음(7~8%)
  • Windows 경우 아래와 같이 Task Manager 의 Details 탭의 CPU 컬럼에서 확인할 수 있다.
  • 근데 System Idle Process 항목은 뭐길래 96%를 차지하고 있는걸까?

 

4.2.2 프로세스 관리와 스케쥴링

  • OS는 프로세스에 우선순위를 할당하고 이에 따라 스케쥴러가 스케쥴링할 수 있도록 대기열에 프로세스를 넣음

4.2.3 대기열 상태 확인: 더 나은 설계

  • 준비 완료 대기열이 비어있다면 OS가 스케쥴링해야 할 프로세스가 없고 CPU는 유휴상태임을 의미
if(queue.empty())
{
    do_something();
}
  • 위처럼 코드를 작성할 수도 있지만 커널은 if 같은 예외처리 구문으로 가득하기 때문에 코드가 번잡해보일 수 있음 → 항상 실행할 수 있는 프로세스를 찾을 수 있도록 하면 됨
    • linked list에서 sentinel 노드를 사용하는 이유
  • NULL판단 로직이 제거되어 코드 오류가능성이 줄고 구조가 깔끔하게 됨

*System Idle Process가 유휴 작업이라는 프로세스 by 커널 설계자. 항상 준비완료 상태이며 우선순위가 가장 낮음

  • 유휴 프로세스는 무엇을 하나? 이를 설명하기 위해서 CPU를 이야기 해야한다.

 

4.2.4 모든 것은 CPU로 돌아온다

  • halt 명령어를 통해 CPU 내부의 일부 모듈을 절전 상태로 전환하여 전력 소비를 크게 줄임
    • 커널 상태에서 CPU로만 실행할 수 있음

참고) 일시중지(suspend) vs halt
- sleep같은 함수는 일시 중지로 해당 함수를 호출한 프로세스만 일시 중지
- CPU가 halt 명령어를 실행하는 것은 시스템 내 더 이상 실행할 준비가 완료된 프로세스가 없다는 의미

4.2.5 유휴 프로세스와 CPU의 저전력 상태

  • halt 명령어를 지속적으로 실행해주는 순환이 있어 CPU는 저전력 상태로 진입하기 시작
while (1)
{
    while (!need_resched())
    {
        cpuidle_idle_call(); // halt 명령어 실행
    }
}

4.2.6 무한 순환 탈출: 인터럽트

위 코드의 이상한점들

  1. 위 코드를 보면 무한 while loop 구조 내부에 break/return 문이 없는데 어떻게 빠져나올까?
  2. 무한 loop이 있어서 프로그램이 CPU 독점하는 것으로 보이지 않음
  • OS는 일정 시간마다 타이머 인터럽트를 생성하고, CPU는 인터럽트 신호를 감지해 인터럽트 처리 프로그램을 실행함
  • 인터럽트 처리함수에는 프로세스의 상태를 파악해 준비완료된 프로세스가 있으면 기존 프로세스를 중단하고 준비완료된 프로세스를 실행
반응형
반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 4.1 의 내용을 읽고 요약한 글입니다. 

 

트랜지스터 (https://news.samsungdisplay.com/23538/)

트랜지스터의 기능은 단자 한쪽에 전류를 흘리면 나머지 단자 두 개에 전류가 흐르게/흐르지 못하게 할 수 있는데 본질은 스위치와 동일하다.

트랜지스터를 이용해 아래의 3가지 회로를 만들 수 있다.

  • 논리곱 게이트(AND 게이트): 스위치기 두개가 동시에 켜질 때만 전류가 흐르고 등이 켜짐
  • 논리합 게이트(OR 게이트): 두 스위치 중 하나라도 켜져 있으면 전류가 흐를 수 있으며 등이 켜짐
  • 논리부정 게이트(NOT 게이트): 스위치를 닫으면 전류가 흘러 등이 켜지지만, 스위치를 열면 전류가 흐르지 않고 등이 꺼짐

 

연산 능력은 어디에서 나올까?

CPU의 가장 중요한 능력인 연산중 덧셈을 예로 들면, 0과 1만 아는 2진법을 사용

  • 0 + 0 의 결과(result)는 0이며, 자리올림수(carry)도 0
  • 0 + 1 의 결과는 1, 자리올림수 0
  • 1 + 0 의 결과는 1, 자리올림수 0
  • 1 + 1 의 결과는 0, 자리올림수 1

먼저 자리올림수는 두 입력 값 두개가 모두 1일 때만 1 ⇒ AND 게이트를 사용하면 된다.

그렇다면 결과는? 두 입력값이 다를 때만 1이고 같으면 0이다. → 배타적 논리합 게이트 (XOR) 을 사용하는데 이도 AND, OR, NOT 게이트로 구성할 수 있다.

⇒ 즉 2진법 덧셈을 AND 게이트 1개, XOR 게이트 1개를 조합해 구현할 수 있다.

배타적 논리 합 게이트 예시

 

신기한 기억 능력

정보를 기억하는 회로란?

  • S=1, R=0인 경우 Q는 항상 1을 출력
  • S=0, R=1인 경우 Q는 항상 0을 출력하는데 이때 회로에 해당 값이 저장되었다고 할 수 있다.

위는 정보를 저장하기 위해 S, R 단자 값을 동시에 설정해야 하는데 아래처럼 하면 실제로 저장하는데 필요한 입력은 D의 비트 하나이다.

* EN 은 저장 여부를 선택하는데 사용되는 단자

  • D단자가 0이면 전체 회로가 저장하는 값은 0
  • D단자가 1이면 전체 회로가 저장하는 값은 1

 

레지스터와 메모리의 탄생

여러비트를 저장하기 위해서는 위의 회로를 복제하여 붙여 넣기만 하면 된다. → 이 조합회로를 레지스터(Register)라고 부른다

전원이 연결되어있는 한 이 회로는 정보를 저장할 수 있지만 전원이 끊기면 정보는 사라짐 → 메모리가 전원이 꺼지면 더이상 데이터를 저장할 수 없는 이유이다.

하드웨어의 기본 기술: 기계 명령

하나하나의 명령어에는 수많은 조합이 있을 수 있으므로 모든 경우를 기계 명령어로 설계하는 것은 불가능하다

⇒ CPU는 작동 방식이나 기능이고, 프로그래머는 명령어 집합을 이용해 전략을 제시하면 된다

소프트웨어와 하드웨어 간 인터페이스: 명령어 집합(Instruction set)

  • 명령어 집합: CPU가 실행할 수 있는 명령어와 피연산자를 묶은 것
  • 고급 프로그래밍 언어는 컴파일러를 통해 기계 명령어로 변환되는데, 이 집합을 명령어 집합이라 함
반응형
반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 3.4의 내용을 읽고 요약한 글입니다. 

 

3.4.1 힙 영역이 필요한 이유

  • 프로그래머가 수명 주기를 포함하여 완전히 직접 제어할 수 있는 매우 큰 메모리 영역
  • 어떻게 힙 영역에 메모리를 할당하고 해제하는지 구현

3.4.2 malloc 메모리 할당자 직접 구현하기

  • 메모리 할당자는 적절한 크기의 메모리 영역을 제공하기만 하고 무엇을 저장할지 신경쓰지 않음
    • integer, floating number 등 신경쓰지 않고 단순한 바이트의 연속에 지나지 않음
    • 커다란 배열 형태

힙 영역 위에서 두가지 문제 해결

  1. malloc 함수 구현
    1. 메모리 영역을 요쳥하면 힙 영역에 가능한 메모리 영역 찾아 요청자에게 반환
  2. free 함수 구현
    1. 메모리 영역의 사용이 완료되었을 때 메모리 영역을 반환하는 방법 구현

 

3.4.3 주차장에서 메모리 관리까지

주차장에 비유하여 표현하면 2가지 목표를 만족시켜야 함

  1. 주차할 위치를 빠르게 찾음. 요청된 크기를 만족하는 여유메모리를 빨리 찾음
  2. 주차장 사용률 극대화. 메모리를 요청할 때 가능한 많은 메모리 할당 요청을 만족시켜야 함

4가지 문제점

  1. 메모리 조각의 할당 상태 추적
  2. 단일 메모리 할당 요청의 요구사항을 만족하는 사용 가능한 메모리 조작이 매우 많을 때 할당 기준
    1. 6바이트 요청했는데 여유 메모리 공간이 16, 32, 8 바이트 일때
  3. 16바이트 메모리를 요청해야 하는데 여유 메모리 조각의 크기가 32바이트라서 16바이트가 남는 경우
  4. 사용 반환된 메모리를 어떻게 처리해야 하나?

3.4.4 여유 메모리 조각 관리하기

  • 위의 4가지 문제점들 중 1번 해결을 위한 방법으로 linked list로 메모리 사용정보를 기록하는 방식

2가지 정보를 기록

  • 메모리 조각의 크기를 기록한 숫자 - header라고 하며 32비트
    • 메모리 조각이 비어있는지 알려주는 설정값(flag) - 1비트
  • 할당 가능한 메모리 조각을 payload라고 함 (메모리 주소)

3.4.5 메모리 할당 상태 추적하기

한조각은 4바이트

  • 16/1: 할당된 메모리 조각 크기가 16바이트를 의미
  • 32/0 : 여유 메모리 조각이 32 바이트
  • 0/1: 끝을 표시하는 특수한 표시로 4바이트

이 방식으로 메모리 조각의 여유/할당 상태 파악 및 추적할 수 있음

3.4.6 어떻게 여유 메모리 조각을 선택할 것인가: 할당 전략

  • 4바이트 메모리를 요청받을 때 요구사항을 충족하는 여유 메모리가 두조각이 있음
    • 8/0, 32/0 중 어떤것을 반환할 것인가? 다양한 할당 전략 방식이 있음

메모리 할당 전략 방식

1. 최초 적합 방식

  • 제일 앞부터 탐색하다가 가장 먼저 발견된 요구사항을 만족하는 항목을 반환
  • 단순하지만, 처음부터 사용가능한 메모리 조각을 찾으므로 앞부분에 작은 메모리 조각이 많이 남을 가능성이 높음

2. 다음 적합 방식

  • 최초 적합 방식과 유사하지만 메모리를 요청할 때 처음부터 검색하는 대신 적합한 여유 메모리 조각이 마지막으로 발견된 위치에서 시작한다는 점이 다름
  • 탐색 시작 위치가 달라져, 더 빠르게 여유 메모리 조각을 탐색할 수 있음
  • 단점: 메모리 사용률은 최초 적합 방식에 미치지 못한다는 것이 연구로 밝혀짐

3. 최적 적합 방식

  • 사용 가능한 메모리 조각을 모두 찾은 후 요구 사항을 모두 만족하면서 크기가 가장 작은 조각 반환
  • 메모리를 더 잘 활용한다는 장점이 있음
  • 사용가능한 모든 메모리 조각을 탐색하기 때문에 빠르지 않음

실제로는 더 복잡한 원리로 적합한 방식을 찾아 할당

3.4.7 메모리 할당하기

  • 12바이트 메모리를 요청했을 때 적절한 여유 메모리 조각이 12 바이트라고 가정
    • 헤더인 4바이트를 제외한 나머지 크기

  • 할당된 것으로 표시하고 머리 정보 뒤에 따라오는 메모리 조각의 주소를 요청자에게 반환하면 됨
  • 위는 이상적인 경우로 12바이트 메모리를 요청했을 때 찾아낸 여유 메모리 조각의 크기가 더 큰 경우가 대부분
  • 아래와 같이 찾아낸 메모리 조각이 32바이트라면? 메모리가 낭비되고 내부 단편화(fragmentation) 발생함

내부 단편화의 예

  • 이 문제를 해결하기 위해 여유 메모리 조각을 두개로 분할하여 앞부분은 할당한 후 반환하고 뒷부분은 좀 더 작은 크기의 새로운 여유 메모리 조각으로 만듦

3.4.8 메모리 해제하기

  • 사용자가 메모리를 요청할 때 얻은 주소를 ADDR이라고 가정
  • free(ADDR)을 호출하면 매개변수인 ADDR에서 머리 정보 크기인 4바이트를 빼는 것으로 메모리 조각의 머리 정보 얻을 수 있음
  • 머리 정보에서 얻은 할당 설정값을 여유 메모리로 바꾸면 해제가 완료됨

메모리 해제 시 중요한 점 하나

  • 해제되는 메모리 조각과 인접한 메모리 조각이 여유 메모리 조각일 때 단순히 해제 여부만 기록하면 아래 상황 발생

  • 해제된 메모리 조각에 인접한 아래쪽 메모리 조각도 비어있음
  • 메모리를 해제 할 때 둘을 합치는게 나을까? 그대로 두는게 나을까?

그대로 두는 경우

  • 20 바이트 요청이 오는 경우 두 조각 중 어느 것도 요구사항을 만족시키지 못함

메모리를 해제 할 때 메모리 조각을 병합할 때

  1. 즉시: 비교적 간단하지만 부담이 발생. 하지만 간단해서 여전히 이 전략을 많이 선택
  2. 요구사항을 충족하는 여유 블록을 찾을 수 없을 때: 실제 메모리 할당자가 선택하는 전략

3.4.9 여유 메모리 조각을 효율적으로 병합하기

  • 해제하는 메모리 조각의 위 아래가 모두 비어있다면? 아래는 헤더 정보로 알 수 있지만 위의 정보는 어떻게 알 수 있나?

  • 메모리 조각 끝에 footer 정보를 추가

  • 조각의 꼬리 정보는 그 다음에 위치한 조각의 머리 정보와 인접해 있어 현재 조각의 머리 정보에서 4바이트를 빼면 이전 조각의 꼬리 정보를 획득할 수 있음

  • header와 footer 정보는 메모리 조각을 doubly linked list로 만듦
반응형
반응형

이 글은  컴퓨터 밑바닥의 비밀 chapter 3.2-3.3 의 내용을 읽고 요약한 글입니다. 

3.2 프로세스는 메모리 안에서 어떤 모습을 하고 있을까?

3.2.1 가상 메모리: 눈에 보이는 것이 항상 실제와 같지는 않다

  • 모든 프로세스의 코드 영역은 0x400000에서 시작
    • 서로 다른 두개의 프로세스가 메모리를 할당하기 위해 malloc을 호출하면 동일한 시작 주소를 반환할 가능성이 매우 높음

Ubuntu 에서 테스트

// malloc_test.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    void *ptr = malloc(10);  // Allocate 10 bytes
    printf("Address returned by malloc: %p\\n", ptr);
    free(ptr);  // Don't forget to free the memory
    return 0;
}

$ ./malloc_test Address returned by malloc: 0x56342fa8d260
$ ./malloc_test Address returned by malloc: 0x55a3e5a9d010

다른 주소가 반환됨. 이유는? → ASLR 이라는 기술이 최근 OS에는 적용되어있음

ASLR (Address Space Layout Randomization)

  • 스택, 힙, 동적 라이브러리 영역의 주소를 랜덤으로 배치해서 공격에 필요한 target address를 예측하기 어렵게 만드는 기술

ASLR 기술을 끄고 테스트하면 같은 주소가 반환되는 걸 볼 수 있다

$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
$ ./malloc_test
$ Address returned by malloc: 0x40068c
$ ./malloc_test
$ Address returned by malloc: 0x40068c
  • 두 프로세스가 모두 같은 주소에 데이터를 쓸 수 있다는 의미인데 괜찮은가?

⇒ 주소값은 가짜 주소(가상 메모리 주소)이고 메모리에 조작이 일어나기 전 실제 물리 메모리 주소로 변경되기 때문에 문제 되지 않음

실제 물리 메모리 구조 예시

주목할 2가지 사항

  1. 프로세스는 동일한 크기의 chunk로 나뉘어 물리 메모리에 저장
    - 위 그림에서 힙 영역은 3개의 chunk로 나뉨
  2. 모든 조각은 물리 메모리 전체에 무작위로 흩어져 있음

보기에 아름답지 않지만 운영 체제가 프로세스에 균일한 가상의 주소 공간을 제공하는 것을 방해하지는 않음 → 가상 메모리와 물리 메모리 사이의 mapping을 나타내는 page table로 관리

3.2.2 페이지와 페이지 테이블: 가상에서 현실로

  • 각각의 프로세스에는 단 하나의 페이지 테이블만 있어야 함
  • mapping은 ‘page’ 라는 단위로 이루어짐
    • 프로세스의 주소 공간을 동일한 크기의 ‘조각’으로 나누고, 이 ‘조각’을 페이지(page)라고 부름
  • 그러므로 두 프로세스가 동일한 메모리 주소에 기록하더라도 페이지 테이블 통해 실제는 다른 물리 메모리 주소에 저장됨

 

3.3 스택 영역: 함수 호출은 어떻게 구현될까?

아래 코드의 문제점은?

void func(int a)
{
	if(a>100000000)
	{
		return;
	}
	
	int arr[100] = { 0 };
	
	func(a + 1);
}
  • 함수 실행 시간 스택(runtime stack)과 함수 호출 스택(call stack) 이해 필요

3.3.2 함수 호출 활동 추적하기: 스택

  • A, B, C, D 4가지 단계가 존재하고 아래그림 처럼 의존성을 가짐

단계별로 진행 과정

  • A→B→D→B→A→C→A
  • 선입 선출(Last In First Out, LIFO) 순서로 stack 과 같은 데이터 구초가 처리하기 적합

 

3.3.3 스택 프레임 및 스택 영역: 거시적 관점

  • 함수 실행시의 자신만의 ‘작은 상자’가 필요한데 이를 call stack 혹은 stack frame 이라고 함
  • 스택은 낮은 주소 방향으로 커지므로 아래와 같이 표현됨

  • stack frame에는 어떤 정보들이 포함되는지?

3.3.4 함수 점프와 반환은 어떻게 구현될까?

  • 함수 A가 함수 B를 호출하면, 제어권이 A에서 B로 옮겨짐
    • 제어권: CPU가 어떤 함수에 속하는 기계 명령어를 실행하는지 의미

제어권이 넘어갈 때는 2가지 정보가 필요함

  1. 반환(return): 어디에서 왔는지에 대한 정보
    • 함수 A의 명령어가 어디까지 실행되었는지에 대한 정보
  2. 점프(jump): 어디로 가는지에 대한 정보
    • 함수 B의 첫 번째 기계 명령어가 위치한 주소

위의 정보는 어디서 가져오는지? → 스택 프레임!

함수 A가 함수 B를 호출할 때 CPU는 함수 A의 기계 명령어(주소: 0x400564)를 실행중이라 가정

  • CPU는 다음 기계 명령어를 실행하는데 call 뒤에 명령어 주소가 함수 B의 첫번째 기계 명령어
    • 이 명령어를 실행한 직후 CPU는 함수 B로 점프하게 됨
  • 함수 B실행이 완료되면 어떻게 함수 A로 돌아오나?
    • call 명령어 실행 후 지정한 함수로 점프 + call 명령어 다음 위치 주소(0x40056a)를 함수 A의 스택 프레임에 넣음

call 명령어를 실행하면 반환주소가 스택 프레임에 저장됨

반환 주소가 추가되면서 스택 프레임이 아래 방향으로 조금 커짐

  • 함수 B에 대응하는 기계 명령어를 실행하면서 B에 대한 스택 프레임도 추가됨
  • 함수 B의 마지막 기계 명령어인 ret은 CPU에 함수 A의 스택 프레임에 저장된 반환주소로 점프하도록 전달하는 역할

 

3.3.5 매개변수 전달과 반환값은 어떻게 구현될까?

대부분의 경우 레지스터를 통해 매개변수와 반환값을 전달

  • 함수 A가 B를 호출하면, A는 매개변수를 상응하는 레지스터에 저장
  • CPU가 함수 B를 실행할 때 이 레지스터에서 매개변수 정보를 얻을 수 있음
  • CPU 내부의 레지스터 수가 제한되는 경우 나머지는 스택 프레임에 넣음

스택 프레임에 함수 호출에 필요한 매개변수 보관

3.3.6 지역 변수는 어디에 있을까?

  • 레지스터에 저장할 수 있지만 로컬 변수가 레지스터 수보다 많으면 스택프레임에 저장

 

3.3.7 레지스터의 저장과 복원

  • 함수 A와 B가 지역변수 저장을 위해 모두 레지스터를 사용하면 값이 덮어써 질 수 있음
  • 그렇기 때문에 초기에 저장된 값을 레지스터에서 사용하고 나면 다시 그 초기값을 함수의 스택 프레임에 저장해야 함

 

3.3.8 큰 그림을 그려보자, 우리는 지금 어디에 있을까?

void func(int a)
{
	if(a>100000000)
	{
		return;
	}
	
	int arr[100] = { 0 };
	
	func(a + 1);
}
  • 앞에서 본 코드를 다시 보면, 자기 자신 함수를 100000000번 호출
  • 호출 할 때마다 스택 프레임이 함수 실행시 정보를 저장하기 위해 생성되며, 함수 호출 단계가 증가하며 스택 영역이 점점 더 많은 메모리 차지 → stack overflow 발생

위 과정 시각화 링크

 

Python Tutor code visualizer: Visualize code in Python, JavaScript, C, C++, and Java

Please wait ... your code is running (up to 10 seconds) Write code in Python 3.11 [newest version, latest features not tested yet] Python 3.6 [reliable stable version, select 3.11 for newest] Java C (C17 + GNU extensions) C++ (C++20 + GNU extensions) JavaS

pythontutor.com

  • 코드가 실행되면서 stack frame이 계속 쌓이는 모습을 볼 수 있음 

 

반응형
반응형

이 글은 책 컴퓨터 밑바닥의 비밀 chapter 3.1 의 내용을 읽고 요약한 글입니다. 

3.1 메모리의 본질, 포인터와 참조

3.1.1 메모리의 본질은 무엇인가? 사물함, 비트, 바이트, 객체

  • 메모리는 메모리 셀(memory cell)로 구성되며 셀에는 0과 1 (1bit) 가 보관될 수 있음
  • 1bit로는 표현할 수 있는 정보가 너무 적기 때문에 8개를 묶어 byte로 표현
  • byte는 메모리 내 자신의 주소를 가지며 memory address 라고 함
  • 1byte도 $2^{8}$=256 개의 조합만 만들 수 있으므로 더 많은 정보를 포함하기에는 부족
  • 12바이트를 묶어서 사용하며 프로그래밍 언어에서는 구조체(structure)또는 객체(object)라고 표현

 

3.1.2 메모리에서 변수로: 변수의 의미

  • java script/python 등의 고급 프로그래밍 언어는 사용할 수 없고 8byte만 사용가능한 메모리가 주어진다면? 메모리 읽기와 쓰기를 직접 수행해야 함
 

  • 8칸의 메모리 공간이 있고 옆에 붙은 번호가 각각 메모리의 주소

1 + 2 값을 계산하고 싶다고 가정하면,

  • 숫자 1과 2를 메모리에 저장해야 함 (메모리 값을 읽어 레지스터에 저장해야 CPU가 연산 수행 가능)
  • 숫자 1을 메모리 주소 6에 넣기 위해 store라는 명령어를 사용한다면,
store 1 6
  • 숫자 2개중 하나는 저장할 숫자 값이고 하나는 메모리 주소

쓰기를 위해 load 라는 명령어를 사용한다면,

load r1 6
  • 명령어 뜻 모호
    • 숫자 6을 r1 레지스터에서 읽는거지,
    • 메모리 주소 6에 저장된 숫자를 r1레지스터에 읽는건지,

즉, 메모리 주소와 숫자 값을 구분하기 위한 방법 필요

  • 예를 들어 $ 기호가 붙어 있으면 값이고, $ 기호가 없다면 메모리 주소를 의미
store $1 6
load r1 6

메모리 주소 6에 1의 값을 저장

  • 이제 주소 6은 숫자 1을 나타냄 (주소 6 → 숫자 1)
  • 하지만 ‘주소 6’이라는 표현은 익숙하지 않기 때문에 쉬운 별칭 필요 (a 같은) ⇒ 변수
a = 1

a 변수라는 의미

  • 값 1을 나타냄
  • 값은 메모리 주소 6에 저장됨
b = a

위 표현은? b변수를 위해 메모리 주소를 하나 할당할 수 있음

하지만 아래그림처럼 a 변수가 여러 바이트(5bytes)를 차지하는 데이터를 나타내고 b=a 를 표현해야 한다면, 메모리 공간이 부족한데 방법은?

3.1.3 변수에서 포인터로: 포인터 이해하기

  • b 변수가 a변수를 가리키고 있다면 데이터의 복사본을 만들 필요가 없음

  • a 변수는 메모리 주소 3에 위치하므로 b 변수에 메모리 주소 숫자 3을 저장 → 포인터

포인터를 메모리 주소로만 이해한다면 간접 주소 지정을 알아야 함 (indirect addressing)

load r1 1 
  • 메모리 주소 1에 있는 값(숫자 3)을 r1 레지스터에 적재하는데, 메모리 주소 1에는 주소값이 있기 때문에 이를 나타내 줘야 함
load r1 @1
  • 메모리 주소1 → 메모리 주소 3→ 데이터 값(1)

어셈블리어에는 변수라는 개념이 없기 때문에 위와 같이 간접주소 지정 계층을 알아야 하지만 고급 언어에서는 변수를 사용하면 됨

메모리 주소 1 -> 메모리 주소 3 -> 데이터 # 어셈블리어 수준
b -> 데이터 # 고급언어 수준

3.1.4 포인터의 힘과 파괴력: 능력과 책임

#include <stdio.h>

void main()
{
	int a = 1;
	printf("variable a is in %p\\n", &a);
}

variable a is in 0x7fff328

  • 포인터를 통해 메모리를 직접 조작할 수 있는 능력은 강력하지만, 모든 상황에 필요한 것은 아니며, 포인터 없는 프로그래밍 언어에도 대신 사용할 수 있고 포인터를 한번 더 추상화한 참조가 있음

3.1.5 포인터에서 참조로: 메모리 주소 감추기

  • 포인터는 메모리를 추상화하 것이고 참조는 포인터를 한번 더 추상화 한 것
반응형
반응형

event-based concurrency를 이용한 event-driven programming은 아래 2가지가 필요

  1. 이벤트: 네트워크 데이터의 수신 여부, 파일의 읽기 및 쓰기 가능 여부 등의 관심 대상
  2. 이벤트를 처리하는 함수: 이벤트 핸들러(event handler) 라고 함
 
while (true)
{
	event = getEvent(); // 이벤트 수신 대기
	handler(event);     // 이벤트 처리 
}

해결해야 할 2가지 문제

  • 첫번째 문제: getEvent 같은 함수 하나가 어떻게 여러 이벤트를 가져올 수 있나?
  • 두번째 문제: 이벤트를 처리하는 handler 함수가 반드시 이벤트 순환과 동일한 스레드에서 실행되어야 하나?

첫번째 문제: 이벤트 소스와 입출력 다중화

  • 리눅스와 UNIX 세계에서 모든 것이 파일로 취급됨
  • 프로그램은 file descriptor(fd)를 사용하여 입출력 작업을 실행(socket도 예외 아님)

동시에 fd 여러개를 처리하기 위해서는?

ex) 사용자 연결이 10개고 이에 대응하는 소켓 서술자가 열개 있는 서버가 데이터 수신 대기중

recv(fd1, buf1);
recv(fd2, buf2);
recv(fd3, buf3);
recv(fd4, buf4);

  • 첫번째 줄 코드에서 사용자가 데이터를 보내지 않는한 recv(fd1, buf1) 는 반환되지 않으므로 서버가 두 번째 사용자의 데이터를 수신하고 처리할 기회가 사라짐

 

더 좋은 방법은?

  • 운영체제에게 내용을 전달하는 작동 방식 사용
    • ‘저 대신 소켓 서술자 열 개를 감시하고 있다가, 데이터가 들어오면 저에게 알려주세요’
  • 입출력 다중화라고 하며 리눅스 세계에서 가장 유명한 것이 epoll
// epoll 생성
epoll_fd = epoll_create();

// 서술자를 epoll이 처리하도록 지정
Epoll_ctl(epoll_fd, fd1, fd2, fd3, fd4, ...);

while(1)
{
	int n = epoll_wait(epoll_fd); // getEvent 역할로 지속적으로 다양한 이벤트 제공
	
	for(i=0;i<n;i++)
	{
		// 특정 이벤트 처리
	}
	
	
}
  • epoll은 이벤트 순환(event loop)을 위해 탄생했음

위 사진처럼 epoll을 통해 이벤트 소스 문제가 해결됨

두번쨰 문제: 이벤트 순환과 다중 스레드

이벤트 핸들러에 2가지 특징이 있다고 가정

  1. 입출력 작업이 전혀 없음
  2. 처리함수가 간단해서 소요시간이 매우 짧음

이 경우 간단히 모든 요청을 단일스레드에서 순차적으로 처리가능

하지만 CPU시간을 많이 소모하는 사용자 요청을 처리해야 한다면?

  • 요청의 처리 속도를 높이고 다중 코어를 최대한 사용하려면 다중 스레드의 도움이 필요

 

  • 이벤트 핸들러는 더 이상 이벤트 순환과 동일한 스레드에서 실행되지 않고 독립적인 스레드에 배치됨
    • 이벤트 순환은 요청을 수신하면 간단한 처리 후 바로 각각의 작업자 스레드에 분배
    • 작업자 스레드를 thread pool로 구현하는 것도 가능

⇒ 이러한 설계 방법을 reactor pattern(반응자 패턴)이라고 부름

카페/음식점에서 주문 받는 사람(이벤트 순환)과 요리하는 사람(작업자 스레드)을 다르게 운영하는 것과 같다. 

 

이벤트 순환과 입출력

요청 처리 과정에 입출력 작업도 포함된다고 가정하면 2가지 상황 발생 가능

  1. 입출력 작업에 대응하는 논 블로킹 인터페이스가 있는 경우
    1. 직접 논블로킹 인터페이스를 호출해도 스레드가 일시 중지 않고, 인터페이스가 즉시 반환되므로 event loop에서 직접 호출하는 것이 가능
  2. 입출력 작업에 블로킹 인터페이스만 있는 경우
    1. 이때는 절대로 event loop에서 어떤 블로킹 인터페이스도 호출하면 안됨
    2. 호출하게 된다면 순환 스레드가 일지 중지될 수 있음
    3. 블로킹 입출력 호출이 포함된 작업은 작업자 스레드에 전달해야 함

 

비동기와 콜백 함수

  • 비지니스가 발전하면서 서버 기능은 점점 복잡해지고 여러 서버가 조합되어 하나의 사용자 요청을 처리
void handler(request)
{
	A;
	B;
	GetUserInfo(request, response); // A 서버에 요청 후 응답을 받아 매게변수 response에 저장
	C;
	D;
	GetQueryInfo(request, response); // B 서버에 요청 후 응답을 받아 매게변수 response에 저장	
	E;
	F;
	GetStorkInfo(request, response); // C 서버에 요청
	G;
	H;
}
  • Get 으로 시작하는 호출은 모두 블로킹 호출로 CPU 리소스를 최대한 활용하지 못할 가능성이 매우 높음 → 비동기 호출로 수정 필요
void handler_after_GetStorkInfo(response)
{
	G;
	H;
}

void handler_after_GetQueryInfo(response)
{
	E;
	F;
	GetStorkInfo(request, handler_after_GetStorkInfo); // 서버 C에 요청
}

void handler_after_GetUserInfo(response)
{
	C;
	D;
	GetQueryInfo(request, hanlder_after_GetQueryInfo); // 서버 B에 요청
}

void handler(request)
{
	A;
	B;
	GetUserInfo(request, handler_after_GetUserInfo); // 서버 A에 요청
}
  • 저 프로세스가 4개로 분할되었고 콜백 안에 콜백이 포함되어있음
  • 사용자 서비스가 더 많아지면 관리가 불가능한 코드 형태

비 동기 프로그래밍 효율성과 동기 프로그래밍의 단순성을 결합할 수 있다면? → 코루틴

 

코루틴: 동기 방식의 비동기 프로그래밍

  • 언어나 프레임워크가 코루틴을 지원하는 경우 handler 함수가 코루틴에서 실행되도록 할 수 있음
  • 코드 구현은 여전히 동기로 작성되지만 yield로 CPU제어권을 반환할 수 있음
  • 코루틴이 일시 중지 되더라도 작업자 스레드가 블로킹되지 않음 (코루틴과 스레드의 차이)
  • 코루틴이 일시중지되면 작업자 스레드는 다른 코루틴을 실행하기 위해 전환됨
  • 일시 중지된 코루틴에 할당된 사용자 서비스가 응답한 후 그 처리 결과를 반환하면 다시 준비상태가 되어 스케쥴링 차례가 돌아오길 기다림

코루틴 추가 후 서버의 전체 구조

  • 이벤트 loop은 요청을 받은 후 우리가 구현한 handler 함수를 코루틴에 담아 스케쥴링과 실행을 위해 각 작업자 스레드에 배포
  • 작업자 스레드는 코루틴을 획든한 후 진입 함수인 handler 를 실행
  • 어떤 코루틴이 CPU의 제어권을 반환하면 작업자 스레드는 다른 코루틴을 실행
    • 비록 코루틴이 블로킹 방식이더라도 작업자 스레드는 블로킹되지 않음

 

  • 스레드는 일반적으로 커널 상태 스레드라고 부름
    • 커널로 생성되고 스케쥴링을 함
    • 커널은 스레드 우선순위에 따라 CPU 연산 리소스를 할당
  • 코루틴은 커널 입장에서는 알 수 없는 요소로 코루틴 수는 커널과 무관하게 스레드에 따라 CPU시간 할당
    • 코루틴을 사용자 상태 스레드라고도 함
반응형
반응형

본 글은  컴퓨터 밑바닥의 비밀  2.7 장 내용을 요약한 내용입니다.

 

블로킹와 논블로킹

  • 함수를 호출할 때 주로 사용됨

함수 A, B가 있다고 가정

함수 A가 B를 호출할 때, 함수 B를 호출함과 동시에 OS가 함수 A가 실행중인 스레드나 프로세스를 일시 중지 시킨다 → 블로킹

그렇지 않으면 → 논-블로킹

  • 블로킹 호출 핵심은 스레드/프로세스가 일시 중지되는 것

모든 함수 호출이 호출자의 스레드를 일시 중지 시키는 것은 아님

int sum(int a, int b)
{
	return a + b;
}

void func()
{
	int r = sum(1, 1);
}
  • 위 코드는 운영체제가 func 함수 호출 시 해당 스레드를 중지시키지 않음. 핵심은 입출력

 

블로킹의 핵심 문제: 입출력

  • 입출력 요청 완료시간은 CPU의 클럭 주파수보다 훨씬 느림
  • 입출력 과정이 실행되는 동안 CPU 제어권을 다른 스레드로 넘겨 다른 작업을 할 수 있도록 해야 함
  • 이후 입출력 작업이 완료되면 다시 CPU제어권을 우리 쓰레드/프로세스로 받아와 다음 작업을 실행할 수 있도록 함

  • CPU 제어권을 상실했다가 되찾는 시간 동안 블로킹되어 일시 중지됨
    • 스레드 A가 입출력 작업을 실행하여 블로킹되면 CPU는 스레드 B에 할당됨
    • 스레드 B가 실행되는 동안 OS는 입출력 작업이 완료된 것을 확인하면 다시 스레드 A에 CPU 할당

 

논블로킹과 비동기 입출력

네트워크 데이터 수신을 예로 설명. 데이터를 수신하는 함수인 recv가 논블로킹이면 이 함수를 호출할 때 OS는 스레드를 일시 중지시키는 대신 recv 함수를 즉시 반환

  • 호출 스레드는 자신의 작업을 계속 진행하고 데이터 수신 작업은 커널이 처리

데이터를 언제 수신했는지 알 수 있는 방법은?

  1. 논블로킹 방식의 recv 함수 외에 결과를 확인하는 함수를 함께 제공하고, 해당 함수를 호출하여 수신된 데이터가 있는지 확인
  2. 데이터가 수신되면 스레드에 메시지나 신호등을 전송하는 알림 작동 방식
  3. recv 함수를 호출 할 때 데이터 수신 처리를 담당하는 함수를 콜백 함수에 담아 매개변수로 전달할 수 있음(recv 함수가 콜백 함수를 지원해야 함)

논블로킹 호출이며, 이런 유형의 입출력 작업을 비동기 입출력 이라고도 함

피자 주문에 비유하기

  • 피자를 직접 주문하러 피자가게에 가서 주문하고 기다려 받는것: 블로킹
  • 피자 전화로 주문 후 받는 것: 논 블로킹
    • 피자 완성됬는지 계속 체크하면? 동기
    • 일반적으로는 비동기

동기와 블로킹

  • 블로킹 호출은 확실한 동기 호출
  • 동기호출은? 블로킹 호출이 아닐 수 있음
    • sum 함수에 대한 호출은 동기이지만 funcA가 sum 함수 호출을 했다고 해서 블로킹되거나 하지 않음
int sum(int a, int b)
{
	return a + b;
}

void func()
{
	int r = sum(1, 1);
}

 

비동기와 논블로킹

네트워크 데이터 수신을 예로 들어 설명

  • 데이터를 수신하는 recv 함수를 논블로킹 호출로 설정하기 위해 flag 값 추가(NON_BLOCKING_FLAG)
void handler(void *buf)
{
	//수신된 네트워크 데이터를 처리
}

while(true)
{
	fd = accept();
	recv(fd, buf, NON_BLOCKING_FLAG, handler); // 호출 후 바로 반환, 논블로킹
}
  • recv 함수는 논블로킹 호출 → 네트워크 데이터를 처리해주는 handler 함수를 콜백 함수로 전달, 즉 비동기이자 논 블로킹

하지만 데이터 도착을 감지하는 전용 함수인 check 함수를 제공해서 아래와 같이 구성한다면,

void handler(void *buf)
{
	//수신된 네트워크 데이터를 처리
}

while(true)
{
	fd = accept();
	recv(fd, buf, NON_BLOCKING_FLAG, handler); // 호출 후 바로 반환, 논블로킹
	
	while (!check(fd))
	{
		// 순환 감지
		;
	}
	
	handler(buf);
}
  • while 문에서 끊임없이 데이터를 도착하기 전까지 체크하기 때문에 그 전에 handler 함수를 사용할 수 없음 ⇒ recv 함수는 논블로킹이지만 전체적인 관점에서 이 코드는 동기

정리하면, 논블로킹이더라도 전체적으로 반드시 비동기라는 의미는 아님

반응형

'OS' 카테고리의 다른 글

[책리뷰] 컴퓨터 밑바닥의 비밀: ch3.1 메모리의 본질, 포인터와 참조  (0) 2024.08.25
Event loop와 coroutine  (0) 2024.08.11
동기/비동기  (0) 2024.08.10
파일 시스템  (0) 2024.07.28
가상 메모리(Virtual memory)  (0) 2024.07.28
반응형

본 글은 책 컴퓨터 밑바닥의 비밀  2.6 장 내용을 요약한 내용입니다.

고된 프로그래머 예시를 통한 동기/비동기 비교

동기 요청(왼쪽)과 비동기 요청(오른쪽)의 예

 
  • 상사가 프로그래머에게 일을 시킬 때, 옆에서 계속 기다리고 있으면 동기, 일을 시키고 다른 일을 하면 비동기라고 예를 들어 설명

또 다른 예로 전화통화와 이메일을 비교해보면,

  • 전화통화 : 상대방이 말할 때 들으며 기다려야 함(동기)
  • 이메일: 작성하는 동안 다른 사람들은 다른 일 처리 가능(비동기)

 

동기 호출

  • 일반적인 함수 호출
funcA()
{
	// funcB 함수가 완료될 때까지 기다림
	funcB();
	
	// funcB 함수는 프로세스를 반환하고 계속 진행
}

funcA와 funcB 함수가 동일한 스레드에서 실행됨

입출력 작업의 경우,

...
read(file, buf); // 여기에서 실행이 중지됨
...
// 파일 읽기가 완료될 때가지 기다렸다가 계속 실행함
  • 최하단 계층은 실제로 system call로 운영체제에 요청을 보냄
    • 파일 읽기 작업을 위해 read 호출 스레드를 일시 중지하고, 커널이 디스크 내용을 읽어 오면 일시 중지 되었던 스레드가 다시 깨어남 (Blocking input/output 이라고 함)

Blocking input/output

 

비동기 호출

  • 디스크의 파일 읽고 쓰기, 네트워크 IO, 데이터 베이스 작업처럼 시간이 많이 걸리는 입출력 작업을 백그라운드 형태로 실행
read(file, buf); // 여기에서 실행이 중지되지 않고 즉시 반환
// 이후 내용의 실행을 블로킹하지 않고 바로 
  • read 함수가 비동기 호출되면 파일 읽기 작업이 완료되지 않은 상태에서도 read 함수는 즉시 반환될 수 있음

  • 호출자가 블로킹 되지 않고 read 함수가 즉시 반환되기 때문에 다음 작업을 실행할 수 있음

 

그럼 비동기 호출에서 파일 읽기 작업이 언제 완료되었는지 어떻게 알 수 있을까? 이 경우, 처리에 대한 2가지 상황이 있을 수 있음

  1. 호출자가 실행 결과를 전혀 신경쓰지 않을 때
  2. 호출자가 실행 결과를 반드시 알아야 할 때

 

1. 호출자가 실행 결과를 전혀 신경쓰지 않을 때

void handler(void* buf)
{
	... // 파일 내용 처리 중
}

read(buf, handler)

"계속해서 파일을 읽고, 작업이 완료되면 전달된 함수(handler)를 이용해 파일을 처리해주세요."
⇒ 파일 내용은 호출자 스레드가 아닌 콜백 함수가 실행되는 다른 스레드(호출되는 스레드) 또는 프로세스 등에서 처리

2. 호출자가 실행 결과를 반드시 알아야 할 때

  • notification 작동 방식을 사용하는 것
    • 작업 실행이 완료되면 호출자에게 작업 완료를 알리는 신호나 메시지를 보내는 것
    • 결과 처리는 이전과 마찬가지로 호출 스레드에서 함

 

웹 서버에서 동기와 비동기 작업

아래의 작업을 한다고 가정

  • A, B, C 세 단계를 거친 후 데이터 베이스를 요청
  • 데이터 베이스 요청 처리가 완료 되면 D, E, F 세 단계를 거침
  • A, B, C, D, E, F 단계에는 입출력 작업이 포함되어 있지 않음
// 사용자 요청을 하는 단계
A;
B;
C;
데이터 베이스 요청;
D;
E;
F;

 

먼저 가장 일반적인 동기 처리 방식

// 메인 스레드
main_thread()
{
	while(1)
	{
		요청 수신;
		A;
		B;
		C;
		데이터베이스 요청을 전송하고 결과가 반환될 때까지 대기;
		D;
		E;
		F;
		결과 반환;
	}
}

// 데이터베이스 스레드
database_thread()
{
	while(1)
	{
		요청 수신;
		데이터베이스 처리;
		결과 반환;
	}
}
  • 데이터 베이스 요청 후 주 스레드가 블로킹 되어 일시 중지됨
  • 데이터 베이스 처리가 완료된 시점에서 D, E, F가 계속 실행됨

  • 주 스레드에서 빈공간은 유휴 시간(idle time)으로 기다리는 과정
  • 유휴 시간을 줄이기 위해서 비동기 작업을 활용할 수 있다

 

1. 주 스레드가 데이터 베이스 처리 결과를 전혀 신경 쓰지 않을 때

  • 주 스레드는 데이터 베이스 처리 완료 여부 상관하지 않음
  • 데이터베이스 스레드가 D, E, F 세 단계를 자체적으로 직접 처리
  • 데이터 베이스 처리 후 DB 스레드가 D, E, F 세 단계를 알 수 있는 방법은?
    • 콜백 함수
void handle_DEF_after_DB_query()
{
	D;
	E;
	F;
}

주 쓰레드가 데이터베이스 처리 요청을 보낼 때 위 함수를 매개변수로 전달

DB_query(request, handle_DEF_after_DB_query);
  • 데이터 베이스 쓰레드는 데이터 베이스 요청을 처리한 후 handle_DEF_after_DB_query 함수 호출하기만 하면 됨

이 함수를 데이터 베이스 쓰레드에 정의하고 직접 호출하는 대신 콜백 함수를 통해 전달받아 실행하는 이유은?

⇒ 소프트웨어 조직 구조 관점에서 볼 때 데이터베이스 쓰레드에서 해야 할 작업이 아니기 때문

 

2. 주 쓰레드가 DB 작업 결과에 관심을 가질 때

  • 알림 작동 방식을 이용하여 작업 결과를 주 스레드로 전송
  • 주 스레드는 사용자 요청의 후반부를 계속 처리
  • 주 스레드에는 유휴시간이 없음
    • 비 동기 호출만큼 극단적으로 효율적이진 않지만 동기 호출에 비하면 여전히 효율적
반응형

'OS' 카테고리의 다른 글

Event loop와 coroutine  (0) 2024.08.11
블로킹/논블로킹  (0) 2024.08.10
파일 시스템  (0) 2024.07.28
가상 메모리(Virtual memory)  (0) 2024.07.28
교착상태(Deadlock)  (0) 2024.07.19
반응형

본 글은 책 "혼자 공부하는 컴퓨터 구조+운영체제" 의 Chapter 15. 파일시스템 부분을 읽고 정리한 내용입니다.

파일

  • 하드 디스크나 SSD 같은 보조기억장치에 저장된 관련 정보의 집합, 관련있는 정보를 모은 논리적 단위

파일을 이루는 정보

  • 이름
  • 파일을 실행하기 위한 정보
  • 파일 관련 부가 정보 (속성, 메타데이터)
    • 파일 형식, 위치, 크기 등
 

파일 속성과 유형

속성 이름  의미
유형 운영체제가 인지하는 파일의 종류 (텍스트, 실행, 음성)
크기 현재크기 & 허용 가능한 최대 크기
보호 어떤 사용자가 파일을 읽고/쓰고/실행할 수 있는지
생성 날짜 ...
마지막 접근 날짜  
마지막 수정 날짜  
생성자  
소유자  
위치  

* 속성 이름 자체가 의미를 담고 있는 내용인 경우는 생략

파일 유형

  • 확장자를 이용해 유형을 나타냄

 

파일 연산을 위한 시스템 호출

  • 파일을 다루는 모든 작업은 운영체제에 의해 이뤄지고 응용프로그램은 파일을 조작할 수 없음

파일 연산을 위한 시스템 호출

  • 생성, 삭제, 열기, 닫기, 읽기 ,쓰기

디렉토리

  • 파일들을 정리하기 위해 사용되고 윈도우에서는 폴더라고 부름

옛 운영체제에서는 하나의 디렉토리만 존재했음 (single-level directory)

 

이후, 여러 계층을 가진 tree-structured directory가 생겨남

최상위 디렉토리(root directory, '/'로 표현) 아래 여러 서브 디렉토리가 있을 수 있음

 

절대 경로와 상대 경로

  • 절대 경로: 루트 디렉토리에서 자기 자신까지 이르는 고유한 경로
    • /home/minchul/a.sh
  • 상대 경로: 현재 디렉토리부터 시작하는 경로
    • 현재 디렉토리 경로가 /home이라면 a.sh의 상대 경로는 minchul/a.sh

디렉토리 연산을 위한 시스템 호출

  • 디렉토리 생성, 삭제, 열기, 닫기, 읽기

디렉토리 엔트리

  • 많은 운영체제에서 디렉토리를 그저 ‘특별한 형태의 파일’로 간주
  • 디렉토리는 보조기억장치에 테이블 형태의 정보로 저장되는데 각각의 행을 디렉토리 엔트리라고 함
파일 이름  위치를 유추할 수 있는 정보
   

파일 시스템에 따라 디렉터리 엔트리에 파일 속성을 명시하는 경우도 있음

파일 이름 위치를 유추할 수 있는 정보 생성 시간 수정된 시간     크기
         

 

예시

 

home 디렉토리는 대략 다음과 같이 구성됨

파일 이름  위치를 유추할 수 있는 정보
.. (상위 디렉토리)  
. (현재 작업 디렉토리)  
minchul  
guest  

minchul 디렉토리 엔트리에는 디렉토리에 포함된 파일들의 이름(a.sh, b.c, c.tar)과 이들의 위치를 알 수 있는 정보들이 포함되어있음

 

파일 시스템

  • 파일과 디렉토리를 보조기억장치에 일목요연하게 저장하고 접근할 수 있게 하는 운영체제 내부 프로그램

파티셔닝과 포매팅

  • 보조기억장치를 사용하려면 파티션을 나누는 작업과 포맷 작업을 거쳐야 함
  • 파티셔닝: 저장 장치의 논리적인 영역을 구획하는 작업 (일종의 칸막이로 영역을 나누는 작업)
    • 파티션: 파티셔닝 작업으로 나눠진 영역 하나하나
  • 포매팅: 파일 시스템을 설정하여 어떤 방식으로 파일을 저장하고 관리할 것인지를 결정하고 새로운 데이터를 쓸 준비를 하는 작업

Windows에서 USB의 파일 시스템 예시

  • 포매팅 할 때 파일 시스템이 결정됨
  • 파일 시스템은 여러 종류가 있고 파티션마다 다른 파일 시스템을 설정할 수도 있음

 

파일 할당 방법

  • 운영체제는 파일과 디렉토리를 블록(block) 단위로 읽고 씀
    • 하나의 파일이 보조기억장치에 저장될 떄 하나 이상의 블록에 걸쳐 저장됨
  • 하드 디스크의 가장 작은 저장단위는 섹터지만 운영체제는 하나 이상의 섹터를 블록이라는 단위로 묶어 관리

  • 하드 디스크내에 여러 블록이 있음

 

파일을 보조기억장치에 할당하는 방법

  1. 연속 할당
  2. 불연속 할당
    • 연결 할당
    • 색인 할당

 

연속 할당(contiguous allocation)

  • 가장 단순한 방식
  • 블록을 3, 2, 5개 차지하는 정도의 크기를 가진 파일 a, b, c가 있다면 아래처럼 할당

  • 연속으로 할당된 파일에 접근하기 위해서는 파일의 첫번째 블록 주소와 블록 단위의 길이만 알면됨

 

  • 연속 할당을 사용하는 파일 시스템에서는 디렉토리에 엔트리에 파일 이름과 더불어 첫번째 블록 주소와 블록 단위의 길이를 명시

장점

  • 구현이 단순

단점

  • 외부 단편화를 야기할 수 있음

 

단점 예시

위의 사진과 같이 파일이 연속할당방식으로 저장된 상황에서 파일 D, F가 삭제 된다면?

  • 블록은 총 11개가 남지만, 크기가 블록 7개 이상을 사용하는 파일은 할당될 수 없음

 

연결 할당(linked allocation)

  • 불연속 할당의 종류
  • 파일을 이루는 데이터를 linked list로 관리

  • 4개의 블록으로 구성된 a라는 파일이 있다고 할 때
  • 10→5→13→2 순으로 블록이 연결되어 있고 2번 블록에는 다음 블록이 없다는 특별한 표시자(-1)를 기록

 

  • 디렉토리 엔트리에는 파일 이름과 함께 첫번째 블록 주소와 블록 단위의 길이를 명시

길이가 아닌 마지막 블록의 주소를 기록할 수도 있음

단점

1. 반드시 첫번째 블록부터 하나씩 차례대로 읽어야 함

  • 파일 내 임의의 위치에 접근하는 속도(random access 속도)가 매우 느려 성능면에서 비효율적

2. 하드웨어 고장이나 오류 발생 시 해당 블록 이후 블록은 접근할 수 없음

  • 하나의 블록 안에 파일 데이터와 다음 블록 주소가 모두 포함되어 있다보니, 파일을 이루는 블록에 하나라도 문제가 발생하면 이후 블록에 접근할 수 없음

 

색인 할당(indexed allocation)

  • 파일의 모든 블록 주소를 색인 블록(index block)이라는 하나의 블록에 모아 관리하는 방식

색인 할당 예시

  • 파일 a의 색인 블록은 4번 블록
  • 파일의 데이터는 7, 13, 11 블록이 저장되어 있다고 가정
  • 파일 a에 순차적으로 접근하고 싶다면 생인 블록에 저장된 주소로 차례대로 접근하면 됨

 

  • 임의의 위치에 접근하고 싶다면 색인 블록의 i번째 항목이 가리키는 블록에 접근하면 됨
  • 디렉토리 엔트리에는 파일 이름과 색인 블록 주소를 명시

색인 할당을 기반으로 만든 파일 시스템이 유닉스 파일 시스템

 

파일 시스템 살펴보기

대표적인 파일 시스템 2가지

  1. FAT 파일 시스템: USB, SD 카드등의 저용량 저장 장치에 사용됨
  2. 유닉스 파일 시스템:유닉스 계열 OS에 사용됨

FAT 파일 시스템 (File Allocation Table)

  • 연결 할당 기반 파일 시스템의 단점을 보완
    • 단점의 근본적인 원인은 블록 안에 다음 블록의 주소를 저장했기 때문

 

각 블록에 포함된 다음 블록의 주소들을 한데 모아 테이블 형태로 관리하면? 파일 할당 테이블(FAT)!

  • 파일의 첫번째 블록 주소(4번블록)만 알면 4→8→3→5 순으로 파일의 데이터가 담긴 모든 블록에 접근할 수 있음
  • 이렇게 FAT을 이용하는 파일 시스템이 바로 FAT 파일 시스템
  • MS-DOS에서 사용되었고 최근까지 저용량 저장 장치용 파일 시스템으로 많이 사용됨
  • 버전에 따라 FAT12, 16, 32가 있으며 숫자는 블록을 표현하는 비트수를 의미

FAT(FAT12) 파일 시스템을 사용하는 파티션 도식도

  • FAT 영역: FAT가 저장되고 파티션의 앞부분에 만들어짐
  • 루트 디렉토리 영역: 루트 디렉토리가 저장되는 영역
  • 데이터 영역: 서브 디렉토리와 파일들을 위한 영역

FAT가 메모리에 캐시될 경우 느린 임의 접근 속도도 개선 가능

FAT파일 시스템의 디렉토리 엔트리

시작 블록을 통해 FAT에서 해당 파일의 내용들에 접근할 수 있음

  • 속성은 읽기전용/숨김 파일/시스템 파일/ 일반 파일/ 디렉토리 등을 식별하기 위한 항목

아래 디렉토리 구조를 이루는 FAT 파일 시스템에서 /home/minchul/a.sh 파일을 읽는 과정

 

/home/minchul/a.sh 에 접근하는 과정

  1. 루트 디렉토리에서 home 디렉토리는 몇번 블록에 있는지 살펴봄 → 3번 블록
  2. 3번 블록을 읽어 home 디렉토리 내용을 살펴보고 minchul 디렉토리가 몇번 블록에 있는지 살펴봄 → 15번
  3. 15번 블록을 읽어 a.sh 파일의 첫번째 블록 주소를 확인 → 9번
  4. FAT을 통해 9→8→11→13 블록 순서로 a.sh 파일 내용에 접근 가능

 

유닉스 파일 시스템

  • 색인 할당 기반의 파일 시스템
  • 색인 블록 == i-node
    • 파일의 속성 정보와 15개의 블록 주소가 저장될 수 있음

  • 파일마다 i-node를 가지고 있고 i-node 마다 고유한 번호가 부여되어있음

 

UNIX 파일 시스템을 사용하는 파티션 도식도

  • 파티션 내에 i-node 영역에 i-node들이 있고 데이터 영역에 디렉토리와 파일들이 있음

 

문제: i-node의 크기는 유한한데 블록을 20개, 30개, 그 이상 차지하는 파일은?

첫째, 블록 주소 중 12개에는 직접 블록 주소를 저장

  • i-node가 가리킬 수 있는 열다섯 개의 블록 주소 중 처음 12개에는 파일 데이터가 저장된 블록 주소를 직접 명시
    • direct block: 파일 데이터가 저장된 블록

 

둘째, ‘첫째’ 내용으로 충분하지 않다면 13번째 주소에 단일 간접 블록 주소를 저장

  • 단일 간접 블록(single indirect block): 파일 데이터가 저장된 블록 주소가 아닌 파일 데이터를 저장한 블록 주소가 저장된 블록을 의미

 

 

셋째, ‘둘째’ 내용으로 충분하지 않다면 14번째 주소에 이중 간접 블록 주소를 저장

  • 이중 간접 블록(double indirect block): 데이터 블록 주소를 저장하는 블록 주소가 저장된 블록

넷째, ‘셋째’ 내용으로 충분하지 않다면 열다섯 번째 주소에 삼중 간접 블록 주소를 저장

삼중 간접 블록: 이중 간접 블록 주소가 저장된 블록

 

unix 파일 시스템의 디렉토리 엔트리도 파일 이름과 i-node 번호로 구성됨

UNIX 파일 시스템의 디레고리 엔트리

 

예시

  • 유닉스 파일 시스템에서 /home/minchul/a.sh 파일을 읽는 과정

 

  • 루트 디렉토리 위치는 루트 디렉토리의 i-node를 통해 확인
    • 유닉스 파일 시스템은 루트 디렉토리의 i-node를 항상 기억하고 있음
    • 2번 i-node가 루트 디렉토리의 i-node라고 가정
  1. i-node 2를 통해 루트 디렉토리 위치 파악 → 1번 블록
  2. 1번 블록을 읽으면 루트 디렉토리 내용을 알 수 있음 → home 디렉토리의 i-node 번호는 3
  3. i-node 3에 접근하여 home의 디렉토리 위치 파악 →210번 블록
  4. 210번 블록을 읽으면 home 디렉토리 내용을 알 수 있음 → minchul의 i-node 번호는 8
  5. i-node 8에 접근하면 minchul 폴더의 위치 파악 → 121번 블록
  6. 121번 블록을 읽으면 minchul 디렉토리 내용을 알 수 있음 → a.sh 파일의 i-node 번호는 9번
  7. i-node 9에는 98, 12, 13 블록이 있음

⇒ 결론적으로 a.sh 파일을 읽기 위해 98→12→13 블록에 접근하면 됨, i-node를 통해 위치 정보를 알고, 블록을 통해 내용을 알 수 있음

 

 

반응형

'OS' 카테고리의 다른 글

블로킹/논블로킹  (0) 2024.08.10
동기/비동기  (0) 2024.08.10
가상 메모리(Virtual memory)  (0) 2024.07.28
교착상태(Deadlock)  (0) 2024.07.19
프로세스 동기화  (0) 2024.07.19

+ Recent posts