지금까지 lock의 개념을 살펴보았고, hardware와 OS의 적절한 조합으로 어떻게 구현되는지 살펴보았다. 하지만, concurrent programs을 빌드하기 위한 기본 요소로 lock만이 있는 건 아니다.

 

특히, 스레드는 종종 실행을 계속하기 전에 condition이 true인지 확인하고 싶어한다. 예를 들어, 상위 스레드는 계속하기 전에 하위 스레드가 완료되었는지 확인하고 싶을 수 있다. 이를 join()이라고도 부른다. 이러한 기다림(wait)은 어떻게 구현할 수 있을 지 살펴보자. 

 

Spin-based Aproach - inefficient


Shared Variable(공유 변수)를 사용해 볼 수 있다. 이러한 Spin-based Approach라 칭한 이 솔루션은 일반적으로 작동하지만 상위 항목(parent)이 CPU 시간을 낭비(spin and waste)하고 있기 때문에 매우 비효율적이다.

 

대신에 우리가 원하는 것은 child의 실행이 끝날 때까지, 즉 우리가 기다리고 있는 condition이 실현될 때까지  parent를 sleep하는 것이다.(CPU를 낭비하지 않고).

 

멀티 스레드 프로그램에서, 스레드를 계속하기 전에 어떤 조건이 참일 때까지 기다리도록 하는 것이 종종 유용하다. condition이 true가 될 때까지 회전(spin)하는 간단한 방법은 매우 비효율적이며 CPU Cycles를 낭비하며, 경우에 따라서는 부정확할 수 있다. 그렇다면, 어떻게 thread를 wait하는 것이 좋을까?

 

 

 

 

Definition and Routines


조건이 참일 때까지(until the condition becomes true) 기다리기 위해서, 스레드는 조건 변수(conditional variable)라고 하는 것을 사용할 수 있다. 조건 변수는 condition에 따라 실행이 기다려지는 상황에서 적용할 수 있는 명시적 대기열(explicit queue)이다. 다른 스레드는 상태(state)가 변하면 대기 중인 하나 이상의 스레드를 깨울 수 있으며 따라서 스레드가 계속 진행될 수 있도록 허용한다.

 

이러한 Conditional Variable(조건 변수)는 크게 wait()와 signal() 두 가지 연산과 연관되어 있다. wait() 호출은 스레드가 절전 모드(sleep)로 전환할 때 실행되며, signal() 호출은 스레드가 프로그램에서 무언가를 변경했기 때문에 해당 조건에 대해서 대기 중인 절전 스레드를 깨우려고 할 때 실행된다.

 

여기서 done은 State Variable(상태 변수)다.


Conditional Variable (조건 변수)란,  Condition Variable은 특정 조건을 만족하기를 기다리는 변수이며, thread간의 신호 전달을 위해 사용한다.

 

출처 : ju-hy.tistory.com/39

 


아래는 Conditional Variable(조건 변수)와 State Variable(상태 변수)를 사용하여 thread_join과 thread_exit을 구현한 코드 예제이다.

 

 

위 코드의 두 가지 케이스를 살펴보자.

 

# Case 1.

부모는 자식 스레드를 만들지만 스스로 실행을 계속한다(우리는 하나의 프로세서만 가지고 있다). 따라서 자식 스레드가 완료될 때까지 기다리기 위해 즉시 thr__join()으로 호출한다. 이 경우 lock을 획득하고, child가 완료되었는지(아닌지) 확인한 후 wait()를 호출해 스스로 sleep한다.(따라서 unlock된다).

The child will eventually run, print the message “child”, and call thr exit() to wake the parent thread

 

하위 스레드가 마침내 실행되며, child메시지를 보내고, thr_exit()를 호출하여 부모에게 신호를 보낸다. 이 과정에서 lock을 걸고, 완료 상태(done=1)를 설정하며, 부모에게 신호를 보내어 깨운다. 마지막으로, 상위 스레드가 실행되며 잠금이 설정된 상태에서 wait()로부터 돌아오고, 잠금을 해제하고, "parent: end" 메시지를 출력한다.

 

# Case 2.

자식 스레드가 생성 즉시 실행되고 done을 1로 설정하며 signal을 호출하여 절전중(sleeping)인 스레드를 깨우고 완료된다. 그런 다음 상위 스레드가 실행되고  thr_join()을 call하며 done이 1인것을 확인하고 wait()하지 않고 return한다.  

 

 

* done이라는 상태 변수(state variable)가 꼭 필요한 지 의문이 들 수 있다. 만약 상태 변수가 없다면 어떻게 될까?

잘못된 접근 방식이다. Case 2의 상황에 대처할 수 없게 된다.

 

자식 스레드가 즉시 실행되서 thr_exit()를 호출하는 경우 자식 스레드는 signal을 보내지만 해당 condition에 대해서 asleep상태의 스레드가 없다. 부모 스레드가 실행되면, wait를 호출할 것이고 asleep 상태에 갇히게(stuck) 될 것이다. 어떠한 스레드도 해당 스레드를 깨우지 않을 것이다. 해당 예시로부터, 상태 변수 done의 중요성을 깨달을 수 있다. done이라는 상태 변수는 스레드가 알고자 하는 값을 기록한다. sleeping, waking, locking은 해당 변수를 둘러싸고 build된다.

 

 

* 만약 lock 기능이 없다면 어떻게 될까?

race condition이 발생한다. 만약 부모가 thr_join()을 호출한 다음 done의 값을 확인하면, 그것은 0일 것이고 wait()을 호출하여 sleep 상태빠지려 할 것이다. 이 때 lock 기능의 부재로 인해 parent가 interrupt되며 child가 실행된다면 child는 state variable인 done을 1로 변경하고 signal을 보낼 것이다. 하지만 waiting상태의 잠든 스레드가 없을 것이며, 다음으로 parent가 다시 실행될 때 영원히 sleep상태에 빠진다.

 

해당 join 예제를 통해서 조건 변수를 올바르게 사용하기 위한 몇 가지 기본 요소를 확인할 수 있었다.

 

 

 

signal()이나 wait() 호출 시 잠금을 유지하라 -Hold the lock when calling signal or wait.


wait()호출 시 lock을 유지하는 것은 선택사항이 아니라, wait() 의미론적으로 보았을 때 강제되어진다(필수사항이다).

 

왜냐하면,

(a) wait()를 호출할 때 lock이 유지된다고 가정하고,

(b) 호출자를 sleep 상태로 전환할 때 lock을 해제하며,

(c) sleep 상태로부터 돌아오기 직전에 다시 lock을 획득하기 때문이다.

 

따라서 signal() 또는 wait()를 호출할 때 lock을 유지하자. 

 

 

The Producer/Consumer (Bounded Buffer) Problem


이번에 다룰 동기화 문제는 Producer/Consumer Problem(생산자/소비자 문제)로 알려져있고, dijkstra에 의해 처음 제기되었다. 이 문제로부터 잠금 또는 조건 변수로 사용될 수 있는 일반화된 semaphore가 등장하였다.

 

하나 이상의 생산자 스레드와, 하나 이상의 소비자 스레드가 있다고 상상해보자. 생산자는 데이터 항복을 생성하여 버퍼에 배치하고, 소비자는 버퍼에서 지정된 항목을 가져와 어떠한 방식으로 소비한다.

 

이러한 방식은 실제 많은 시스템에서 사용되는 방식이다. 예를 들어, 멀티 스레드 웹 서버에서, 생산자는 HTTP 요청을 작업 대기열(the bounded buffer)에 넣고, 소비자 스레드는 이 대기열에서 요청을 꺼내서 처리한다.

 

Bounded buffer는 하나의 프로그램의 출력을 다른 곳으로 파이프(pipe)할 때도 쓰인다. 예를 들어, "grep foo file.txt | wc -l"이라는 커맨드를 입력하면, 두 프로세스를 동시에 실행한다. 프로세스 grep은 file.txt로부터 standard output에 한줄 한줄 출력하고, 이는 파이프를 통해 리디렉션되어  프로세스 wc의 표준 입력으로 들어온다. wc는 input stream의 라인 개수를 결과로 출력하는 프로세스이다. 그러므로, grep프로새스는 생산자이고, wc 프로세스는 소비자이다. 이들 사이에는 in-kernel bounded buffer가 있다.

 

bounded buffer는 공유 리소스이기 때문에 race condition을 막기 위해 동기화된 접근(synchronized access)를 필요로 한다.

 

 

Producer/Consumer with If statement


if문을 이용한 Producer/Consumer 코드

위 코드는  if를 사용한 producer/consumer 구현 코드이다. 이는 생산자와 소비자 스레드가 각각 하나일 때는 문제가 없지만, 여러개일 때 문제가 발생한다.

 

예를 들어, 생산자 스레드 1개, 소비자 스레드 2개가 있다고 생각해보자.

if문을 이용한 Producer/Consumer 구현에서 발생할 수 있는 문제 상황 (Broken Solution)

1. Tc1(첫번째 소비자 스레드)가 먼저 실행되고, c2 if문에서 count==0이므로(버퍼에 데이터가 존재하지 않으므로) sleep상태가 된다.

3. Tp1 스레드가 실행되며 count==0이므로 버퍼에 데이터를 넣고, Tc1에 신호를 보내어 sleep상태의 Tc1을 다시 스케줄링한다.

4. 먼저 스케줄링되어있던 Tc2가 버퍼에 있는 데이터를 소비한다.

5. 다음으로 sleep상태에서 깨어난 Tc1가 실행되지만, buffer가 비어있는 문제가 발생한다.

 

즉, 여기서 볼 수 있는 문제는 Tp1(생산자가) Tc1(소비자)를 깨운 후에, Tc1이 다시 실행되기 전에 bounded buffer의 상태가 변경되었다(Tc2에 의해)는 점이다. 즉, 깨어난 스레드가 실행될 때 원하는 대로 상태(state)가 존재할 것이라는 보장이 없다.

 

이러한 문제를 해결하기 위해서는 아래처럼 while문을 이용하면 된다.

 

Producer/Consumer with While statement (with one condition variable)


하지만, while 문을 사용하더라도 해결하지 못하는 문제 하나가 더 있다. 바로 조건 변수가 하나라는 점이다.

 

While문을 이용한 Producer/Consumer 코드

 

이전과 같이 예를 들어, 생산자 스레드 1개, 소비자 스레드 2개가 있다고 생각해보자.

 

While문을 이용한 Producer/Consumer 구현에서 발생할 수 있는 문제 상황 (Broken Solution)

 

1.Tc1, Tc2가 순서대로 실행되며, 소비할 데이터가 존재하지 않으므로 sleep상태가 된다.

2. Tp1이 데이터를 생산하여 bounded buffer에 데이터를 넣고, Tc1을 깨운다.

3. Tc1이 데이터를 소비하고, Tc2를 깨운 후 sleep 상태에 빠진다. (조건 변수가 하나이므로 다음 대기열에 있는 Tc2를 깨우는 것이다.)

4. Tc2가 실행되지만 buffer에 데이터가 없으므로 sleep상태에 빠진다.

5. Tp1, Tc1, Tc2 모두가 sleep 상태에 빠지는 문제가 발생한다.

 

이 문제를 해결할 수 있는 단순한 방법은 조건 변수를 2개 사용해서 생산자는 소비자만 깨우도록, 그리고 소비자는 생산자만 깨우도록 만드는 것이다.

 

 

Producer/Consumer with While statement (with two condition variable)


While문을 이용한 Producer/Consumer 코드 (조건 변수 2개)

 

위처럼 조건 변수 2개를 사용하여 생산자는 소비자만 깨우고, 소비자는 생산자만 깨우도록 바꾸어 문제를 해결할 수 있다.

 

 

Producer/Consumer with While statement (with two condition variable and multiple buffer size)


이번엔, buffer size가 1 이상 일때의 구현을 살펴보자.

 

이전과 달라진 점은 buffer의 크기가 1이 아닌 다수라는 점이다. 이에 따라 put()과 get()코드가 수정되고, 생산자는 버퍼가 가득찬 경우에만 wait()을 수행하며, 소비자는 버퍼가 비어있을 경우에만 wait()를 사용한다.

 

 

 

Covering Conditions


이번엔 다른 문제 상황을 살펴보자.

 

위 코드는 멀티 스레드에서 메모리를 할당하고 해제하는 일부 라이브러리의 코드이다. 메모리를 할당받는 allocate() 코드는 소비자(메모리 할당받는) 스레드가 원하는 size만큼의 메모리를 할당받을 수 있을 때까지 wait하고, 메모리를 해제하는 스레드는 사용중인 메모리를 할당 해제한 후 신호를 보내 메모리를 할당받으려하는 스레드를 깨운다.

 

여기서 문제는 어떠한 스레드에 신호를 보내야 하는 가이다. 예를 들어, free()를 통해서 여유 메모리가 100이 되었는데, 메모리를 할당받으려하는 두 스레드(첫번째 스레드는 150만큼의 메모리를, 두번째 스레드는 50만큼의 메모리를 필요로한다.) 중 첫번째 스레드를 깨운다면 여유 메모리가 부족하여 할당받을 수 없으므로 다시 sleep상태가 되고 할당받을 수 있던 두번째 스레드에게는 기회가 가지 않는 문제가 발생한다. 즉, 수행할 수 있는 스레드가 있음에도 아무것도 수행하지 않는 상태가 된다.

 

이러한 문제를 해결하기 위해서 signal() 대신 broadcast()를 사용하여 sleep상태의 모든 스레드를 깨우는 방법이 있다. 물론 성능 면에서 부정적인 영향을 줄 수는 있지만, 위와 같은 이슈를 해결할 수 있다. 이러한 trade-off를 잘 고려해야 한다.

 

 


pages.cs.wisc.edu/~remzi/OSTEP/

 

Operating Systems: Three Easy Pieces

Blog: Why Textbooks Should Be Free Quick: Free Book Chapters - Hardcover - Softcover (Lulu) - Softcover (Amazon) - Buy PDF - EU (Lulu) - Buy in India - Buy Stuff - Donate - For Teachers - Homework - Projects - News - Acknowledgements - Other Books Welcome

pages.cs.wisc.edu

 

+) 참고한 블로그 : icksw.tistory.com/164?category=878876

 

[OS] Synchronization(동기화)를 위한 condition variables(조건 변수) - OS 공부 21

안녕하세요! Pingu입니다. 오늘도 열심히 OS에 대해 알아보겠습니다! 지난 글에서는 일반적인 자료구조에 Lock을 상호 배제 구현하여 thread safety 하게 만드는 방법에 대해 알아봤었습니다. 여러 가

icksw.tistory.com

 

Classic vs Multi-threaded


Classic View - Single Point of Execution within a program, 하나의 프로그램에는  단일 실행 포인트.

-> 하나의 프로그램 안에는 명령어를 가져오고 실행하는 Single Program Counter(단일 PC)만이 존재했다.

 

Multi-threaded program - 멀티 스레드 프로그램은 하나 이상의 실행 포인트가 존재한다.

스레드들은 같은 주소공간을 공유한다. (same Page Table) -> 같은 데이터에 접근할 있다.

하지만, 각각의 스레드는 own private set of registers(including PC) 레지스터 set 스레드별로 각자 가지고 있다.

 

스레드를 T1에서 T2 전환할 TCB(Thread Control block) switch하지만, address space 그대로 남는다.(, page table 전환할 필요가 없다.)

 

 

 

 

스레드를 쓸까?


1. parallelism : 병렬성 때문에 쓴다. 병렬적 실행이 가능해진다. 예를 들어, CPU 하나당 하나의 스레드를 갖고, 멀티 프로세서 시스템에서 여러 프로세서가 동시에 프로그램을 실행한다면 프로그램의 실행 속도가 향상될 것이다.

 

2. I/O overlapping : 느린 I/O 때문에 프로그램의 진행이 block되는 것을 피할 있다.

While one thread in your program waits (blocked waiting for I/O). , 프로그램에서 하나의 스레드가 I/O 기다리며 blocked상태인 동안 CPU 스케줄러는 다른 스레드로 전환하여 그동안 프로그램에서 무언가를 실행할 있다.

 

3. 스레드 대신에 멀티 프로세스를 사용할 수도 있지만, 스레드들은 address space 공유하므로 데이터를 공유하기가 쉽다. 다만, 논리적으로 분리된 task 관해서는 multi process 사용하는 것이 나은 선택이다.

 

 

오늘날 멀티 스레드 프로그램의 예시


오늘날 대부분의 어플리케이션은 멀티스레드이다. 스레드들은 어플리케이션 내에서 실행된다. 어플리케이션 내의 여러 개의 tasks(작업들)은 분리된 스레드들에 의해서 구현될 있다.

 

-디스플레이 업데이트

-데이터 가져오기

-맞춤법 체크

-네트워크 요청에 응답

등등

 

프로세스 생성보다 스레드를 생성하는 것이 가볍다(비용이 적게 든다.)

코드를 단순화해 효율을 높일 있다.

커널은 일반적으로 멀티스레드화되어있다.

 

멀티 스레드(multi-threaded)의 장점 4가지


뛰어난 반응성/응답성(Responsiveness)

프로세스의 일부가 blocked상태여도 실행을 계속하는 것이 가능하다. 이는 특히 유저 인터페이스에 있어 중요하다.

 

자원 공유(Resource Sharing)

스레드들은 프로세스의 리소스들을 공유하므로, 이는 메시지를 전달하거나 메모리를 공유하는 것보다 쉽다.

 

경제성(Economy)

스레드를 생성하는 것이 프로세스를 생성하는 것보다 비용이 적게 들며, thread switching(스레드 전환) context switching(컨텍스트 전환, 프로세스 전환)보다 오버헤드가 적다.

 

확장성(Scalability)

프로세스가 멀티 프로세서 구조의 이점을 적극적으로 활용할 수 있다. 즉, 다중 CPU 구조에서는 각각의 스레드가 다른 프로세서에서 병렬로 수행될 수 있으므로 병렬성이 증가한다.

 

프로세스의 정의


 

프로세스는 운영체제(os)가 사용자에게 제공하는 가장 근본적인 추상화 (fundamental abstraction)다.

프로세스의 정의는 단순하다. 실행중인 프로그램(running program)이 프로세스의 정의다.

 

프로그램은 그 자체로는 생명이 없다. 그저 디스크에 상주하는 명령어 묶음이며, 이는 실행이 되기를 기다리고 있다.

운영체제(OS)가 이들을 실행시킬 때 비로소 프로그램은 제 역할을 한다고 볼 수 있다.

 

 

 

CPU 가상화 (virtualizing CPU)


우리가 컴퓨터를 사용할 때, 마치 여러 개의 프로세스들이 동시에 실행되고 있다고 느낀다.

또한, 우린 CPU가 어떠한 프로세스를 실행시키고 있는지 고려할 필요가 없다.

 

하나의 CPU로 여러 개의 프로세스를 동시에 실행시키고 싶지만, 이는 불가능하다.

대신에, CPU time sharing을 통해서 여러 개의 프로세스를 번갈아가며 실행하여 마치 하나의 CPU가 여러 개의 프로세스를 동시에 실행하는 것처럼 보이게 할 수 있다.

 

이와 같이,  운영체제는 단일 physical CPU로 여러개의 virtualizing CPUs를 사용하는 것처럼 환상(illusion)을 만들어내며, 이를 CPU 가상화라고 한다.

 

CPU time sharing은 유저가 여러 개의 프로세스를 동시에 실행할 수 있도록 제공하지만, CPU가 공유되므로 각각의 프로세스는 더 느리게 실행될 것이며, 잠재적인 성능에 대한 trade-off가 있다. 

 

time sharing 메카니즘에서 context-switching이 쓰이며, context-switching은 주어진 CPU에서 하나의 프로그램을 종료시키고 다른 또 하나의 프로그램을 실행시킬 수 있는 운영체제의 ability이다.

 

 

 

프로세스와 Machine State


위에서 말했듯이, 프로세스는 OS가 실행중인 프로그램이다. 그리고 프로그램은 OS가 실행하기 전까진 디스크에 상주하는 instructions(명령어) 묶음이다.

 

그렇다면, 프로세스를 구성하는 것은 무엇일까? 이를 이해하기 위해서, 프로세스의 machine state에 대해 알 필요가 있다.

 

Memory

프로세스를 구성하는 핵심 machine state 구성 요소 중 하나는 메모리(memory)다.

실행하는 프로그램의 명령어들과 읽고 쓰는 데이터들 모두 메모리에 적재되어 있다.

 

 

 

Register

프로세스의 machine state 구성 요소 중 또 다른 하나는 레지스터다.

많은 명령어들이 명시적으로 레지스터를 읽고, 업데이트한다. 이는 프로세스의 실행에 있어 필수적이다.

 

프로세스의 machine state를 구성하는 특수한 레지스터들을 몇 개 소개한다.

 

Program Counter, PC (=instruction pointer, IP)

- 다음에 실행할 프로그램 명령어를 지시한다.

 

Stack Pointer 와 Frame Pointer

- 함수 파라미터(function parameters), local variables(지역 변수)가 담긴 스택을 관리하고 주소(address)를 반환(return)하기 위해 사용된다.

 

 

Persistent Storage Device

Persistent Storage Device는 hard disk drive와 같이 영구적으로 데이터가 저장되는 저장소를 뜻한다.

프로세스는 때때로 persistent storage device에 접근(access)한다. (ex. file open 및 I/O)

 

 

 

 

대표적인 Process API


 

Create : 운영체제가 새로운 프로세스를 생성하는 메소드다. 쉘(shell)에 명령어(command)를 입력하거나, 어플리케이션 아이콘을 더블 클릭할 때, 운영체제는 사용자가 지시한 프로그램을 실행하기 위한 새로운 프로세스를 생성한다.

 

Destroy : 운영체제가 프로세스를 강제적으로 종료하도록 하는 메소드다. 물론 많은 프로세스들이 실행이 완료된 후 스스로 종료되겠지만, 그렇지 않을 경우, 사용자가 프로세스를 kill(강제 종료)하고 싶을 때 사용한다.

 

Wait : 프로세스의 실행이 종료될 때까지 기다린다.

 

Miscellaneous Control : 프로세스를 kill 또는 wait하는 것과는 또 다른 control(제어)가 가능하다. Miscellaneous Control은 프로세스를 연기(suspend)한다. 즉, 잠깐동안 실행을 멈추었다가 다시 실행을 재개한다.

 

 

 

 

Process Creation Detail, 프로세스 생성에 대한 고찰


프로그램들이 프로세스로 변화(transform)하는 과정을 좀 더 살펴보자. 운영체제는 어떻게 프로그램을 가져와 실행하고, 프로세스를 생성할까?

 

첫 번째로, 운영체제는 프로그램 코드와 정적 데이터(static data)를 메모리로 로드한다. 프로그램은 처음엔 실행가능한 형식으로 디스크(disk, 현대에 와서는 flash기반 ssd)에 상주하므로 이러한 과정이 필요하다.

 

초기 운영체제는 프로세스 로딩을 eager하게 수행(프로그램이 실행되기 전에 한 번에 모든 로딩 처리)했지만,

현대의 운영체제는 lazy-loading을 채택하고 있다. 즉, 프로그램을 실행하는 동안, 필요한 코드, 데이터만을 부분적으로 가져온다. 

lazy-loading을 깊게 이해하기 위해서, paging(페이징)과 swapping에 대한 이해가 필요하다. 이 부분은 메모리 가상화에서 다룬다.

지금은, 운영체제가 프로그램을 실행하기 위해 필요한 프로그램 비트들을 디스크로부터 메모리로 가져온다고만 알아두자.

 

두 번째로, 운영체제는 프로그램이 실행될 때 메모리의 일부를 런타임 스택으로 할당하고 프로세스에게 넘겨준다. 예를 들어, C 프로그램에서는 스택을 지역 변수(local variable), 함수 파라미터(function parameters), 주소 반환(return address)을 위해 사용한다. OS는 일반적으로 arguments(ex. main() 함수의 argc, argv)를 담은 스택을 초기화할 것이다.

 

세 번째로, 운영체제는 프로그램이 실행될 때 메모리의 일부를 프로그램의 힙(heap)으로 할당하고 프로세스에게 넘겨준다. C 프로그램에서, 힙은 명시적으로 요청받은 동적 할당 데이터들을 위해 사용된다. 예를 들어 malloc()으로 할당하고, free()로 할당 해제한다. 힙은 연결 리스트(linked list), 해쉬 테이블(hash table), 트리(trees) 등의 자료 구조 사용에 필요하다. 힙은 처음엔 작을지 모르지만, 프로그램이 실행되고, malloc()을 통해 많은 요청이 발생하면 OS는 이러한 요청을 받아들여 더 많은 힙 메모리를 할당할 것이다.

 

이 뿐만 아니라, 운영체제는 다른 초기화 작업들도 수행한다. 특히, I/O(Input/Output, 입출력) 관련 초기화 작업도 포함하는데, 예를 들어, UNIX 시스템에서는 각각의 프로세스는 디폴트하게 3 개의 open 파일 디스크립터(file descriptor)를 가진다. 3개는 각각 표준 입력, 표준 출력, 그리고 오류를 의미한다. 이 디스크립터들은 프로그램이 터미널로부터 쉽게 입력을 읽고(read input), 스크린에 출력을 프린트할 수 있도록 한다(print output).

 

이처럼, 코드와 정적 데이터를 메모리에 로드하고, 스택과 힙을 생성, 초기화 및 할당하고, I/O관련 셋업과 같은 다른 작업들을 수행한 후 OS는 비로소 프로그램 실행을 위한 준비를 거의 마친다. 마지막으로, main() 시작 포인트(entry point)에서 프로그램을 실행시키기 위한 작업을 수행한다. 운영체제는 CPU에 대한 제어권을 새롭게 생성된 프로세스에 넘겨준다. 그리고 프로그램이 실행된다.

 

 

 

Process State (프로세스 상태)


이제 우린 프로세스가 무엇인지, 어떻게 생성되는 지 어느 정도 파악했다. 이제, 프로세스가 주어진 시간 동안 어떠한 상태로 존재할 수 있는지 process states에 대해 알아보자. 단순화했을 때, 프로세스는 아래 세 가지 상태 중 하나로 존재한다.

 

Running : 프로세스가 프로세스에서 실행되고 있는 상태, 즉 명령어가 실행되고 있는 상태를 의미한다.

 

Ready : ready 상태는 프로세스가 실행될 준비가 되었지만, 어떠한 이유에서인지 운영체제가 지금은 해당 프로세스를 실행하지 않는 상태를 의미한다.

 

Blocked : blocked 상태는 프로세스가, 특정 이벤트(event)가 발생하기 전까지는 실행할 수 없는 연산을 수행하고 있는 상태이다. 흔한 예시로, 프로세스가 disk I/O request를 시작했을 때 blocked 상태가 되며, 그 동안 다른 프로세스가 프로세서를 사용할 수 있다. 

 

 

 

 

Operating System의 Data Structure


운영체제도 프로그램이며, 다른 프로그램들과 마찬가지로 다양한 관련 정보를 추적하는 주요 데이터 구조를 가지고 있다.

 

예를 들어, 각각 프로세스의 상태를 추적하기 위해 운영체제는 프로세스 리스트(process list)를 가지고 있다. 프로세스 리스트는 ready상태의 모든 프로세스를 포함하며, 어떠한 프로세스가 실행되고 있는 지를 추적하기 위한 정보들을 포함한다.

운영체제는 또한 blocked 상태의 프로세스 또한 추적해야 한다. 예를 들어, I/O event가 완료되었을 때, 운영체제는 올바른 프로세스를 깨워(wake) 다시 실행하기 위해 ready상태로 전환해야 한다.

 

이처럼, 운영체제는 프로세스를 추적하기 위해 stopped process(중지된 프로세스)의 레지스터 값들을 register context에 저장한다. 프로세스가 중지되면, 중지된 프로세스의 레지스터 값들은 레지스터 컨텍스트에 저장되며, 이 레지스터들을 복구함으로써 운영체제는 다시 프로세스를 재개(resume)할 수 있다. 이와 같은 테크닉은 context switch에서 자세하게 배운다.

 

실제로, process state는 위의 running, ready, blocked가 아닌 다른 상태로도 존재할 수 있다.

 

예를 들어, 프로세스가 생성중일 때는 initial state, 프로세스가 종료(exit)되었지만 회수(cleaned up)되지 않았을 때 final state(unix 시스템에서는 이를 zombie state라고 칭한다)가 있다.

 

final state는, 프로세스를 생성한 부모 프로세스(parent)가 종료된 자식 프로세스의 반환 코드(return code)를 검사하고, 종료된 자식 프로세스가 성공적으로 실행되었는 지 확인할 수 있도록 한다.(일반적으로 작업이 성공적으로 수행되었으면 0을, 그렇지 않으면 0이 아닌 값을 반환한다).

 

프로세스가 종료될 때, 자식 프로세스가 완료되기까지 기다리기 위한 final call(최종 호출, ex. wait())을 하고, 운영체제에게 이제 사라지는 프로세스와 관련된 자료 구조들을 정리(cleaned up)해도 된다고 명시한다.

 

 

 


 

 

출처 : pages.cs.wisc.edu/~remzi/OSTEP/

 

Operating Systems: Three Easy Pieces

Blog: Why Textbooks Should Be Free Quick: Free Book Chapters - Hardcover - Softcover (Lulu) - Softcover (Amazon) - Buy PDF - EU (Lulu) - Buy in India - Buy Stuff - Donate - For Teachers - Homework - Projects - News - Acknowledgements - Other Books Welcome

pages.cs.wisc.edu

 

+ Recent posts