[크래프톤 정글 3기] 11/24(금) TIL

ClassBinu·2023년 11월 24일
0

크래프톤 정글 3기 TIL

목록 보기
42/120

08:29 입실
채용 정보 보다가 또 늦게 잠.
점심 먹고 수건 빨래 해야 됨


프로세스

운영체제 굉장히 유용한 재생목록을 찾았다!
https://www.youtube.com/watch?v=GIz4NNGr_LU&list=PLV1ll5ct6GtzIovBUtBb6MXhxqwvKLKRj&index=7

프로세스를 사람으로 비유하면 이해가 쉽다!

커널 기능: 호흡, 소화, 등등
유저 기능: 노래 부르기, 말하기 등등..

프로세스가 포크되는 건 부모가 자식을 낳는 과정으로 비유!
즉, 프로세스 생성은 모두 부모 프로세스로부터 나온다!


OS가 관리하는 프로세스 기능

  • 생성(create)
  • 소멸(destroy)
  • 일시정지(suspend)
  • 재시작(resume)
  • 우선순위 변경(change priority)
  • 블록(block)
  • 깨우기(wake up)
  • 디스패치(dispatch)
  • 프로세스 간 통신(InterProsses communication, IPC)

프로세스의 상태 변화

프로세스는 크게 보면 실행, 비실행의 상태를 지닌다.
디스패치: 비실행 -> 실행
인터럽트: 실행 -> 비실행

구체적인 프로세스 상태는 다음과 같다.

  • new(생성): 프로세스가 생성됨. PCB 생성. 가상 메모리 생성. 프로세스를 메모리에 할당.
  • ready(준비): 프로세서에게 할당되기를 대기(대부분의 프로세스 상태)
  • running(실행): 프로세서를 점유
  • waiting(대기): 이벤트 발생을 기다림
  • terminated(종료): 프로세스가 실행 끝마침

프로세스 테이블

프로세스도 테이블이 있다!
각 프로세스의 PCB를 가리키는 포인터


Unix 프로세스

  1. 부팅 시 Swapper(PID=0) 프로세스 생성
  2. 이어서 Init(PID=1), PageDeamon(PID=2) 생성

0, 2는 커널 프로세스로 OS가 생성함.
1은 모든 사용자 프로세스의 조상


프로세스 생성

부모와 자식은 동시에 실행됨
부모는 자식이 모두 종료될때까지 대기

자식 프로세스는 부모 프로세스의 주소 공간을 복사: fork()
단, 전혀 새로운 기능을 하는 프로세스를 생성할 경우에는 fork() 후 exec()로 메모리 공간에 새로운 프로그램 적재하여 사용
exec()은 현재 프로그램 내용을 새로운 프로그램 내용으로 대체하는 것


프로세스 종료(termination)

프로세스는 마지막 문장을 실행하고 OS에게 삭제를 요청
프로세스에 할당된 모든 자원을 반납
PCB도 회수, 프로세스 테이블에서도 제거

스스로 종료

자신이 직접 exit() 시스템 호출 사용
부모에게 상태값 반환 가능(via wait())

부모에 의해 종료

부모 프로세스가 abort() 시스템 호출
자식 프로세스가 자원을 초과해서 사용할 때
자식 프로세스에 할당된 작업이 더 이상 필요 없을 때
부모 프로세스의 종료 후 자식의 수행을 불허할 때


스레드

프로세스와 스레드는 결국 실행 흐름이다.
프로세스를 Heady Wdight Process(HWP, 중량 프로세스)로 표현하고,
스레드를 Light Weight Process(LWP, 경량 프로세스)로 표현한다.

스레드는 고유의 스택 영역을 갖는다.
하지만 프로세스 입장에서는 하나의 가상 메모리의 스레드 마다의 스택 포인터가 생기는 것!


스레드 종류

사용자 스레드

커널 도움 없이 사용자 주소 공간에 구현된 스레드
라이브러리에 의해 운용
커널은 이 스레드 인식 못하므로 커널 지원 없이 생성, 스케줄링, 관리
한 번에 하나의 스레드만 커널 접근 가능

커널 스레드

OS 커널에 의해 스레드 운용
커널이 생성, 스케줄링, 관리
다중 프로세서에서 병렬 실행 가능


핀토스 Project 1. 스레드

목적: 최소한의 기능을 갖춘 스레드 시스템에서 동기화 문제 이해하기
thread 디렉토리와 devices 디렉토리에서 작업
컴파일은 thread 디렉토리


동기화 이해

동기화를 수행하는 가장 조잡한 방법은 인터럽트를 비활성화 하는 것(응?ㅋㅋ..)


동기화 방법

  • 세마포어
  • 뮤텍스
  • 모니터

모니터

상호 배제와 조건변수를 조합한 추상화된 데이터 구조

  • 상호 재베: 뮤텍스
  • 조건 변수: 스레드 간의 통신과 조정을 가능하게
    스레드를 특정 조건이 충족될 때까지 대기시킴

모니터 조건 변수

모니터에는 두 개의 큐가 존재한다.
1. enry queue: critical section에 진입을 기다리는 뮤텍스에서 관리하는 큐
2. waiting queue: 조건이 충족되길 기다리는 조건 변수가 관리하는 큐

조건 변수가 무엇인지 예시 코드로 알아보기!

import threading

buffer = []  # 공유 버퍼
buffer_size = 5  # 버퍼 크기
lock = threading.Lock()  # 뮤텍스 락
buffer_empty = threading.Condition(lock)  # 비어있는지 여부 확인용 조건 변수
buffer_full = threading.Condition(lock)  # 가득 찼는지 여부 확인용 조건 변수

def producer():
    for i in range(10):
        item = f'Item {i}'
        with buffer_full:
            while len(buffer) >= buffer_size:
                buffer_full.wait()  # 버퍼가 가득 차면 대기
            buffer.append(item)
            print(f'Produced: {item}')
            buffer_empty.notify()  # 소비자에게 신호 보냄

def consumer():
    for i in range(10):
        with buffer_empty:
            while len(buffer) == 0:
                buffer_empty.wait()  # 버퍼가 비어있으면 대기
            item = buffer.pop(0)
            print(f'Consumed: {item}')
            buffer_full.notify()  # 생산자에게 신호 보냄

# 생산자와 소비자 스레드 생성
producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)

# 스레드 시작
producer_thread.start()
consumer_thread.start()

# 스레드 종료 대기
producer_thread.join()
consumer_thread.join()

생산자-소비자 문제

  1. 여러 생산자 스레드가 데이터를 생산하고 공유 버퍼에 저장
  2. 여러 소비자 스레드가 공유 버퍼에서 데이터를 소비
  3. 생산자, 소비자는 동시에 실행 가능. 공유 버퍼 한정된 크기
  4. 생산자는 버퍼에 저장, 소비자는 버퍼에서 제거

이때 발생하는 문제
1. 버퍼가 가득 차 있을 때 생산자가 버퍼에 데이터 추가함
2. 버퍼가 비었을 떄 소비자가 버퍼에서 데이터를 소비함.
3. 생산자와 소비자 간의 작업이 동기화되지 않아 버퍼가 손상됨.

뮤텍스 락과 똑같은 거 아니야?
뮤텍스는 공유 자원에 단일 스레드만 접근하게 하는 것
모니터는 뮤텍스와 조건 변수를 결합, 공유 자원에 대한 접근을 뮤텍스로 제어하면서, 조건 변수로 스레드 간의 통신과 조정이 가능


핀토스 스레드 이해하기

  • 스레드 생성
  • 스레드 완료
  • 스케줄러
  • 동기화 프리미티브(세마포어, 락, 모니터, 최적화 장벽)

timer.c 작업

목표: imter_sleep()을 재구현한다.
문제: 현재 타이머는 busy_wait() 방식으로 루프를 계속 돌면서 대기한다.


바쁜 대기란?

바쁜 대기는 프로세스가 진짜 대기 상태가 아니라 프로세스를 점유하면서 대기하는 상태


timer_sleep(int64_t ticks)

자, 이 함수는 ticks만큼 대기하는 함수이다.

timer_ticks()

현재 틱 값을 가져온다.

timer_elapsed (start)

start 틱 시점에서 얼마나 경과했는지를 반환한다.

thread_yield ()

스케줄러에게 현재 실행 중인 스레드를 준비 상태로 변경하고,
준비 중인 스레드를 실행 상태로 변경하게 요청하는 함수!

loop 종합 설명

이거 다시 공부하기

근데 한 번 양보하면 대기 상태로 돌아가니까 끝난 거 아닌가?

아님!
준비 상태가 된다고 해도 CPU 자원을 완전히 반납한 거라고 볼 수 없음.
다른 스레드에 실행 흐름이 완전히 넘어갈 때 까지 기존 스레드는 준비 상태일지라도 CPU를 점유하고 있는 것임.
즉, 현재 실행 중인 스레드가 yield 함수를 통해 양보를 한다고 해도,
추가로 실행 상태로 바뀌는 스레드가 없기 때문에,
양보한 스레드는 CPU를 점유한 상태에서 아무런 동작도 하지 않은 채 틱이 지날 때까지 자원을 낭비하고 있는 것!

> thread_yield () 함수의 요청하는 대상이 스케줄러라는 걸 몰랐음!
왜 양보하는데 자원이 낭비된다는거야? -> 양보하고 그냥 계속 기다리니까!!!
이거 깨닫는데 1시간 넘게 걸림.. 2줄짜리 코드..
후련하다!!!!

맞는 건지 모르겠음. 다시 검토하기.


timer.c 다른 코드 분석

timer_init()

뭔가 비트연산 같은 걸로 타이머 값을 초기화..?


timer_calibrate()

타이머 영점 맞추는 거?


timer_ticks()

현재 물리적인 타이머 틱 값 반환..?


timer_elapsed(then)

이건 명료하다! 타이머 값에서 틱 수를 뺀다.


timer_sleep(ticks)

ticks값 동안 스레드를 대기상태로 한다!


timer_msleep(ms)

틱이 기준이 아니라 밀리세컨이 기준!


timer_usleep(us)

틱이 기준이 아니라 마이크로초가 기준!


timer_nsleep(ns)

틱이 기준이 아니라 나노초를 기준!


timer_interrupt()

틱을 1개 증가시키고 tread_tick()을 실행


too_many_loops(loops)

LOOPS 반복이 하나의 타이머 틱을 기다리는 시간보다 더 길게 대기하면 true를 반환하고, 그렇지 않으면 false를 반환합니다. 즉, 어떤 작업이 주어진 타이머 틱 기준치보다 더 길게 대기하면 true를 반환.


1차 실험(while문의 무서움)

pintos -T 10 -- -q run alarm-single

수정 전 코드

void timer_sleep(int64_t ticks)
{
	int64_t start = timer_ticks();

	ASSERT(intr_get_level() == INTR_ON);
	while (timer_elapsed(start) < ticks)
		thread_yield();
}

수정 후 코드

void timer_sleep(int64_t ticks)
{
	int64_t start = timer_ticks();

	ASSERT(intr_get_level() == INTR_ON);
	// 대기 시간이 끝날 때까지 대기
	while (timer_elapsed(start) < ticks)
		continue;

	// 대기 시간이 끝났을 때 다음 스레드에게 CPU 양보
	thread_yield();
}

택도 없는 시도였다. 결국 while문이 존재하는 한 계속 자원은 반납되지 않고 있는 거였음!


컴퓨터 구조


폰 노이만 구조

명령어 기반 아키텍처
프로그램 내장 방식
인스트럭션과 데이터 분리
현대 컴퓨터 아키텍처 구조


시스템 버스

  • 주소 버스
  • 데이터 버스
  • 제어 버스

0개의 댓글