CS/운영체제

[운영체제] 프로세스 관리

happy_life 2022. 6. 7. 11:51

목차

1. 프로그램 실행과정

2. 프로세스

3. 스케줄러

4. 프로세스의 상태변화와 쓰레드

5. 프로세스 관리

1. 프로그램의 실행과정

1) 메모리 할당 과정

실행파일은 실제 메모리에 올라가기 전 가상 메모리에 올라간다. 가상 메모리에서는 각각의 프로세스가 주소값을 가지고 있다. 그 주소에는 세부적으로 stack, data, code가 있다. stack에는 함수 등의 값이, data에는 전역변수 등의 값이, code에는 우리가 작성한 코드가 할당된다. 이런 가상 메모리에서 필요한 부분만 실제 메모리에 할당되게 되고, 쓰지 않는 부분은 Swap area에 할당된다. 

 

** 추가

등장 이유

PC는 폰 노이만 구조 기반으로, 프로그램 코드는 무조건 메모리 위에서 실행되어야 한다. 리눅스 프로세스는 하나에 4G인데, 통상적인 메모리는 8G 또는 16G이다. 멀티 프로세스를 가정했을 때 동시에 겨우 2개, 4개정도만 프로그램을 실행할 수 있다. 이에 하드 디스크 안에 SWAP이라는 영역을 두어 메모리가 부족할 때 사용할 수 있는 공간을 만들어 두게 된다. 하지만 이런 SWAP영역에도 불구하고, 물리 메모리의 크기를 벗어나는 프그램은 작동시킬 수 없다. 한 프로그램이 17G인 것을 상상해보면 된다.

 

도대체 가상 메모리란 무엇인가? 

현실에선 프로세스 하나 자체를 처리해야 하는 것이 아니다. 각 프로세스 내에 필요한 부분만 그때 그때 처리해주면 된다. CPU가 한 프로세스의 특정 부분을 참조할 때, 그 공간에 해당하는 물리 메모리가 어느 주소인지만 알면, 그 주소 공간을 사용할 때만 쓰고 다시 해제 해주면 된다. 따라서 실행에 필요한 부분만 물리 메모리에 올라가며 프로세스의 나머지는 디스크스왑 영역에 남게 된다. 

 

디스크의 가상 주소와 RAM의 실제 주소는 어떻게 연결할까?

CPU가 디스크의 가상 메모리 주소를 MMU에 넘겨주면 MMU는 그 주소를 받아 뒤쪽의 N비트는 바꾸지 않고 앞쪽의 나머지 비트를 그에 해당하는 메모리의 실제 메모리 주소로 바꾼다.

 

 

2) 커널 주소 공간의 내용

커널(OS) 또한 함수와 전역 변수 등을 사용하기 때문에 code, data, stack으로 구성된 메모리 공간을 가진다. 각각의 Process 스택을 따로 두어, 어떤 Process에서 인터럽트가 들어왔느냐에 따라 Process 별로 별도로 관리한다.

 

3) 사용자 프로그램과 운영체제의 동작 과정

A 사용자 프로그램이 동작을 하다, 예를들어, 키보드 입력이 필요할 때 System call로 운영체제를 호출하게 됩니다. 이후 I/O 키보드 입력과 관련된 동작을 수행하고, 다시 A 사용자 프로그램으로 CPU를 넘겨주게 됩니다. 

 

2. 프로세스

1) 개념

실행 중인 프로그램이다.

추가로 프로그램의 문맥(context)이라는 것도 있다. 프로그램의 문맥(context)이란, 프로세스가 어떤 일을 해왔고, 어떤 일을 하고 있는지를 말하는 것이다. 예를 들어, 과거에 CPU를 얼마나 썼는가, 파일을 몇 개 열어놓고 쓰고 있는가 등을 나타낸다. 이러한 문맥은 크게 3가지로 나눠볼 수 있다. 먼저, CPU에서 어디까지 수행했는가, 어디를 수행해야 하는가를 나타내는 하드웨어 문맥이 있다. 이는 현재 CPU의 Register에 어떤 값을 넣고 있었는지, Program Counter는 어디를 가리키고 있는지 등으로 알 수 있다. 두번째로는 프로세스의 현재 상태를 나타내는 주소 공간이 있다. data에서 전역 변수로 무엇을 가지고 있는지 체크할 수 있고, 스택에서 함수 진행 상황 등을 알 수 있다. 마지막으로 프로세스 관련 커널 자료 구조가 있다. 이는 각각의 프로세스를 운영체제가 관리하기 위해 가지고 있는 자료구조이다. 운영 체제는 자신의 주소공간으로서의 데이터 영역에 프로세스의 PCB내용을 가지고 있다. PCB에는 프로세스가 메모리는 얼마나 썼는지 등의 정보가 담겨 있다. 또한 커널을 수행할 때는 프로세스마다 별도의 커널 스택을 쓰는데, 이를 나타내기 위해 운영 체제 주소의 스택 공간에 커널 스택이라는 것이 담겨 있다. 

 

2) PCB

PCB에 register는 왜 있는지에 대해 궁금할 수 있다. 하지만 그에 앞서 프로세스가 A -> B, B -> A 등으로 변경되는 문맥 교환 과정을 아래의 그림을 보며 생각해보자.

프로세스 A를 실행하다 B로 넘어간 후, 다시 A로 넘어가서 실행을 하려면 필요한 정보가 있다. 프로세스 A가 실행하던 것은 어디까지 였고, 어디를 실행해야 하는지의 정보 등이다. 이러한 정보를 저장하기 위해 PCB register가 필요하다. PCB register에 저장되었던 정보는 다시 CPU의 register에 넘어가게 되고, 그 다음 실행해야 할 부분부터 동작할 수 있게 되는 것이다. 

 

* 참고

Sysyem call이나 Interrupt 발생 시 반드시 context switch가 일어나는 것은 아니다. 위의 그림과 같이 A->A인 경우, context의일부를 PCB에 저장하긴 하지만, 문맥 교환이라고 말하지는 않는다.

 

3) 상태

프로세스는 상태(state)가 변경되며 수행된다. 프로세스의 상태에는 3가지가 있다. 먼저, Running 상태는 CPU를 현재 사용하고 있는 상태이다. 다음으로는, Ready 상태가 있다. 이는 메모리 등 다른 조건을 만족하고 CPU를 기다리는 상태이다. 마지막으로 Blocked 상태가 있다. CPU를 주어도 당장 CPU를 사용하지 못해 막혀 있는 상태이다. 예를 들어, 디스크에서 파일을 읽어와야 하는 경우가 이에 해당한다.

 

3. 스케줄러

1) 장기 스케줄러

시작 프로세스 중 어떤 것을 메모리에 올릴지를 결정하는 스케줄러로, 현재의 OS에는 대부분 존재하지 않는다. 현재의 OS는 프로세스를 무조건 메모리에 올리는데, 이러면 메모리 부족 문제가 발생할 수 있다. 이를 해결하기 위해 중기 스케줄러르 둔다.

 

2) 중기 스케줄러

여유 공간 마련을 위해 프로세스를 통째로 메모리에서 디스크로 쫓아낸다. 

 

3) 단기 스케줄러

어떤 프로세스를 다음번에 running시킬지 결정한다. 프로세스에 CPU를 주는 문제를 결정한다는 의미이다.

 

 

4. 프로세스의 상태변화와 쓰레드

1) 프로세스 상태변화

프로세스의 상태

Running

CPU를 잡고 instruction을 수행중인 상태

Running에는 User mode와 Kernel mode가 있다. 아래의 사진을 보며 생각해보자.

예를 들어, 사용자 프로그램B에서 I/O 요청 SystemCall 일어난 후, 사용자 프로그램 A가 동작하는 중이었다고 생각해보자. 프로그램 B가 요청했던 I/O가 끝나면 CPU로 interrupt가 발생하게 된다.  이때 interrupt를 받은 CPU는 프로그램 B가 요청한 I/O를 해야하므로 CPU는 프로그램 A에서 OS로 넘어가게 된다. 이 상황에서 프로그램 A는 어떤 상태로 봐야할까? 이럴 땐 프로그램 B가 Running한다고 하지 않고, 그냥 직전의 프로그램인 A가 Running한다고 간주한다. 

 

** 추가

디스크에서 뭘 읽어와되는 것과 관계된 interrupt는 하드웨어 interrupt일까 소프트웨어 interrupt일까?

프로그램이 I/O를 수행하기 위해선 먼저 OS에 SystemCall interrupt를 보내야한다. 이는 소프트웨어 interrupt이다. 이후 I/O가 끝나면 디스크 컨트롤러가 CPU에 interrupt를 보내는데 이는 하드웨어 interrupt이다. 즉 답은 둘다이다.

 

Ready

CPU를 기다리는 상태(메모리 등 다른 조건을 모두 만족하고)

Blocked(wait, sleep)

I/O 등의 event를 스스로 기다리는 상태

ex) 디스크에서 file을 읽어와야 하는 경우

Suspended(stopped)

외부적인 이유로 프로세스의 수행이 정지된 상태로 프로세스는 통째로 디스크에 swap out된다. 중기 스케줄러에 의해 프로세스가 쫓겨난 경우도 이에 해당한다. Suspended는 Suspended Block 과 Suspended Ready로 나뉜다. Suspended Block은 Block상태(예를 들어 I/O를 위한 대기상태)에서 추가적으로 또 외부의 요인으로 프로세스가 정지까지 된 상태이다. 하지만 이러한 경우 CPU와는 상관없이 기존의 I/O는 마무리할 수 있는데, 이런 과정이 끝나면 Suspended Ready상태가 된다.

 

 

* Blocked: 자신이 요청한 event가 만족되면 Ready

* Suspended: 외부에서 resume해 주어야 Active

* Swap out: 메모리에서 쫓겨나는 것

* Swap in: 메모리를 할당하는 것

 

2) 쓰레드

쓰레드의 개념

CPU 수행 단위

 

쓰레드를 이해하기 위해선 프로세스를 이해해야 한다. 아래의 프로세스 사진을 보자.

예를 들어, 인터넷 창을 여러 개 띄운다고 생각하자. 인터넷 창을 띄우는 코드는 같고, 같은 코드 내에서 어떤 동작을 하는지, 어떤 코드를 실행할지 등만 다르다. 프로세스를 생성하는 건 비용이 드는데, 저런 경우마다 프로세스를 하나 생성해서 띄우기엔 너무 비효율적이다. 따라서 하나의 프로세스 내에 공통적인 부분과 다른 부분을 나누어 쓰레드라는 프로세스의 부분집합을 만들게 되었다. 아래사진은 한 프로세스 내의  여러 쓰레드들의 모습이다.

 

쓰레드의 구성

Program counter

Register set

Stack space

 

앞서 말한 다른 부분 즉, code 내에서 어디를 읽어 어떤 동작을 수행할지는 Program counter, 어떤 코드를 실행할지는 PC와 Register set에 담겨있다. 또한 쓰레드마다 사용하는 함수들도 다를 것이다. 따라서 위의 3가지는 쓰레드를 구성하는 요소이다.

그 외 Code section, Data section, OS resources는 쓰레드끼리 공유한다.

 

쓰레드의 장점

1. 다중 쓰레드로 구성된 태스크 구조에서는 하나의 서버 쓰레드가 Blocked(waiting) 상태인 동안에도 동일한 태스크 내의 다른 쓰레드가 실행(Running)되어 빠른 처리를 할 수 있다.

 예를 들어, url을 치고 서버에 리소스를 요청하는 경우, Text는 쉽게 받아올 수 있지만, Image는 다시 서버에 재요청을 해야하고 시간이 많이 걸린다. 웹 브라우저를 사용하는 사람입장에서 Image까지 모두 받아올 때까지 브라우저가 Block되어있으면 매우 답답할 것이다. 하지만 하나의 쓰레드가 Text를 담당해 미리 받아온 Text라도 어느정도 뿌려주는 식으로 웹 브라우저 창을 띄운다면, 사용자입장에서 답답함을 덜 수 있다.

 

2. 경제적이다.

프로세스를 만드는 것이 쓰레드를 만드는 것보다 훨씬 비용이 비싸다. 또한 프로세스에서 다른 프로세스로 넘어갈 때의 오버헤드보다 쓰레드에서 다른 쓰레드로 넘어가는 오버헤드가 훨씬 더 적다. 

 

5. 프로세스 관리

1) 프로세스 생성

부모 프로세스가 자식 프로세스를 생성한다. 하지만 프로세스가 직접 생성하는 것은 아니고, 역시 System Call로 운영체제에게 요청해야 한다. 자식 프로세스를 생성하면 프로세스의 트리 구조를 형성하게 된다. 부모 프로세스와 자식 프로세스는 서로 경쟁하는 모델도 있고, 자식이 종료될 때까지 부모가 기다리는 모델도 있다.

 

fork() 시스템 콜

fork는 자식 프로세스를 생성하는 함수이다. fork가 호출되면 아래 사진과 같이 프로세스가 두 개가 된다.

하지만 프로세스 스스로 자식인지 부모인지 알 수 있을까? 프로세스의 fork는 PC, data 등 모든 Context를 복사해온 것이다. 따라서 스스로 누구인지 알 수 없다.이를 구분하기 위해 부모는 fork() 함수가 끝난 후 자식 프로세스의 PID 값을 return 받는다. 따라서 위의 if문을 돌면서 누가 자식이고, 부모인지 알 수 있게 된다.

 

exec() 시스템 콜

자식 프로그램에 무언가를 덮어 씌우는 시스템 콜이다. 복제된 자식 프로세스를 새로운 프로세스로 덮어씌우는 것을 의미한다. 예를 들어 아래 코드의 경우 else if는 동작하지 않고 exec()부터 새로운 프로세스가 되는 것이다..

 

wait() 시스템 콜

커널은 child가 종료될 때까지 프로세스 A를 sleep시킨다.(block 상태)

child process가 종료되면 커널은 프로세스 A를 깨운다. (ready 상태)

 

exit()

프로세스가 마지막 명령을 수행한 후 운영체제에게 이를 알려준다(exit). 자식 프로세스가 종료되는 경우에는 부모에게도 output data를 보내게 된다.

 

abort()

비정상 종료라는 점에서 exit()과 차이를 갖는다.

  • 자식이 할당 자원의 한계치를 넘어선 경우
  • 자식에게 할당된 태스크가 더 이상 필요하지 않는 경우
  • 부모가 종료하는 경우 -> 운영체제는 부모 프로세스가 종료하는 경우 더이상 자식이 수행되도록 두지 않는다.

 

본 포스팅은 kowc 이화여대 반효경 교수님 운영체제 강의를 바탕으로 작성하였습니다.