운영체제

The Abstraction : The process

땅콩콩 2023. 3. 12. 17:15
process와 virtualizing

소스프로그램을 컴파일하여 얻은 실행파일을 program이라고 하고, 이는 하드디스크에 저장되어 있다가 메인메모리로 로딩된다.(폰노이만 아키텍쳐) 그러면 메인메모리에 있는 이 코드를 cpu가 읽어다가 처리하고, 이 과정이 반복 수행되는 것을 '프로그램이 수행된다'라고 한다.

그리고 이렇게 프로그램이 실행될 때 나타나는 현상들을 추상화하여 개념적으로 정리해놓은 것이 process이다.

 

우리는 cpu를 가상화해서 여러개의 프로그램이 동시에 실행되는 효과를 얻을 수 있는데, 이때 실행되는 상태를 추상화한 것이 process이고, 메모리도 모자라는 상황에서 이것을 여러개의 프로그램이 사용하기 위해서는 메모리도 가상화가 필요하다. 여기에 필요한 것이 주소공간(address space)이라는 개념이다. 물리적 공간이 아니라 가상화된 virtual memory같은 것을 각 프로세스별로 보유하게 되는것이다.

결국 프로세스 입장에서는 cpu도, memory도 자기가 독점하고있다고 착각하게 된다. 수많은 프로그램이 한번에 돌아가고 있는데도 불구하고 말이다.

 

process API

이렇게 생성된 프로세스와 관련해서 process API가 존재하게 되는데, 결국 운영체제가 제공하는 기능이기 때문에 system call이라는 것으로 제공한다. (system call은 운영체제에서 커널을 통해 제공한다.) 

  • Create : 프로세스를 만들 수 있어야 한다.
  • Destroy : 프로세스를 파괴할 수도 있어야 한다.
  • Wait : 특정 프로세스가 다른 프로세스의 결과를 기다린다든지 할때 기다릴 방법이 있어야 함.
  • Miscellaneous Control : 프로세스를 제어할 수 있어야 한다.
  • Status : 프로세스의 상태를 파악할 수 있어야 함.

 

프로그램의 로딩부터, 메모리를 거쳐 cpu가 코드를 처리하기까지의 과정.
  1. 프로그램을 실행시키면, 프로그램이 하드디스크에서 메인메모리로 탑재된다(Loading). 그럼 그 내용이 copy해서 메모리로 올라오게 된다.
  2. 그러면 cpu에서 메모리에 올라온 그 code부분을 처리하는데, 이때 pc(program counter)가 가리키는 곳에 있는 명령어를 cpu로 읽어온다. 그리고 그 명령어를 해독하기 위해 IR(instruction register)에 집어넣는다. 그럼 cpu가 이 명령어를 어떻게 실행해야 할지를 알게된다.(=해독과정)
  3. 가져온 명령어를 cpu에서 fetch, decode, execution 한다.
  4. 이렇게 cpu는 명령어를 가져오고, 해독하고, 실행하는 일을 반복적으로 수행하고(by ALU), 이때 pc는 별다른 제어없이는 명령어 실행 후 다음 명령어를 순차 실행한다.

 

Process States

프로세스는 실행될 때 여러가지 상태를 갖는다.

  • Running : 프로세스가 cpu에 의해 처리되고 있을 때.
  • Ready : 모든 준비가 되었지만 다른 프로세스가 cpu가 쓰고있어서 running상태가 아닐 때.
  • Block : io요청(cpu와 memory영역을 벗어난 요청)을 했을 때, 이때는 cpu를 쓰지 않고 io요청의 응답을 기다리기 때문에 cpu의 낭비를 줄이기 위해 요청한 결과를 가져오기 전까지 해당 프로세스는 block상태가 됨. (이후 요청결과를 가져오면 ready상태가 된다.)

 

process에 대해 더 구체적으로 살펴보기
int
main(int argc, char *argv[])
{
	int *p = malloc(sizeof(int)); 
	assert(p != NULL);
	printf("(%d) address pointed to by p: %p\n",
		getpid(), p); 
	*p = 0; 
	while (1) {
		Spin(1);
		*p = *p + 1;
		printf("(%d) p: %d\n", getpid(), *p); // io요청
	}
	return 0;
}
//mem.c

 

코드에서 주의깊게 볼 부분! heap공간에서 allocation받음. p가 가리키는 것은 물리적인 그 값의 위치, *p가 가리키는 것이 저장되어있는 실제 값!

위의 프로그램을 두번 로딩하여 프로세스가 두개 생성되는 과정을 살펴보자.

우선 실행파일, 즉 프로그램은 하나이고, 이것을 두번 실행하여 메모리의 두 군데에 로딩이 된다. (cpu가 처리해야하는 모든 프로그램은 메모리에 올라와야 한다.) 따라서 프로세스가 두개 형성되었다고 볼 수 있다.

하드디스크에는 운영체제의 핵심이 되는 부분인 kernel도 저장이 되어있는데, 이 부분의 코드 역시 메모리로 올라와있어야 실행이 된다.(Booting) kernel은 system call을 처리하거나 장치들을 효율적으로 공유해서 사용하도록 관리해준다.

또, 물리적 레벨에서 cpu는 machine cycle을 반복적으로 빠르게 수행하는 단순한 기계이다.

Loading과 Booting의 차이

Loading은 실행파일의 코드가 메모리로 탑재되는 것인 반면, Booting은 커널을 위해서 정의된 코드가 메모리로 탑재되는 것을 뜻한다.

위에서 언급했듯이 프로그램이 메모리의 두 군데에 로딩이 되었고, 프로세스가 2개 생성이 되었다. 그런데 이 프로세스는 가상의 추상화된 개념이다. 실제 물리적으로는 그냥 메모리의 여기저기에 위치하지만, 추상화된 공간에서는 위처럼 정리된 모습으로 표현할수 있다.

 

어쨌든 두개의 다른 프로세스(pid 23113, 24114)는 하나의 cpu를 time sharing하며 번갈아 사용하고 있다.

그리고 가상화된 공간이긴 하지만 이 프로세스 안에는 주소공간(address space)이 존재하고, 그 안에는 code, data, stack, heap 등이 자리잡고 있다. 즉, 추상 개념인 프로세스의 안에도 가상화된 메모리가 존재하는 것이다.

 

그래서 cpu가 두 프로세스를 번갈아 수행한다는 것은, pc(program counter)가 한쪽 프로세스의 코드 메모리 위치를 가리키다가, pc가 다른쪽 프로세스의 코드를 가리키는 것이다.(메모리의 code가 cpu로 load) cpu는 단순한 기계에 불과하므로, 내가 어느 쪽 프로세스를 처리하는지 구별할 방법이 없다. cpu는 단순하게 그냥 machine cycle을 돌 뿐이고, pc가 왔다갔다하면서 마치 두 영역의 코드가 동시 실행되는것 같은 착각, 즉 process입장에서 내가 cpu를 독점해서 사용하는 것 같은 착각을 주는 것이다.

 

이제 process부분의 흐름을 자세히 보자.

  • p는 지역변수이므로 stack에 자리를 잡고, 이 변수가 가리키는 공간, 즉 malloc()을 통해서 할당받은 주소값 0x200000은 양쪽 프로세스에서 동일하다. 이것이 가능한 이유는 메모리가 가상화되었기 때문이다. (실제 물리적으로는 다른 위치에 존재할것이다.)
  • 프로세스 24113이 실행될때 이 프로세스는 running상태, 24114는 ready상태이다. 그런데 24113에서 printf()를 만나서 io를 요청하게 되면 block상태가 되고, 운영체제 커널의 스케줄러가 스케줄링을 통해 그 전에 ready상태였던 프로세스(24114)를 running상태로 바꿔준다. 기계적인 레벨에서는 pc가 파란색 화살표에서 빨간색 화살표로 바뀐 것이다. 
  • 그런데 머지않아 프로세스 24114도 printf() 코드를 만나고, 두 프로세스 모두 block상태가 된다. 그러다가 24113이 io요청에 응답을 받아 다시 ready상태가 되면, 이것을 관찰하던 스케줄러가 23113을 running상태로 바꿔준다.
  • running>block 상태 변화는 io호출로 인해 일어나지만 running>ready 상태변화는 일반적으로는 타이머 타임아웃이 일어날 경우 강제 인터럽트로 일어난다.
File descripter table이란?

io요청이 필요할 때, File descripter table를 통해 io를 요청하고 처리하게 되는데, 0번(stdin), 1번(stdout), 2번(stderr)은 미리 연결이 되어있어서 별도로 오픈하지 않아도 오픈한 것으로 되어있다. 하지만 printf()와 같이 다른 io요청의 경우, 라이브러리와 system call을 거쳐서 stdout으로 출력하게 되어있다.

그런데 프로세스들의 상태(state)가 변할때마다 보존되어야 하는것은 상태 뿐만이 아니다.

프로세스가 running중에 있다가 다양한 이유에 의해 ready/block상태가 되고 다른 프로세스에게 cpu를 넘겨줘야 할때, 즉 context가 switching될때는 그 끊어진 단면. 다시 말해 그 순간의 cpu 레지스터 값들이 어딘가에 잘 보존되었다가(save) 다시 복원되어야 한다.(restore)

이렇게하기 위해서는 context switching이 일어나는 순간의 문맥(context)을 PCB(process control block)에 저장해두어야 한다.

이 PCB의 구조를 살펴보자.

// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
	int eip;
	int esp;
	int ebx;
	int ecx;
	int edx;
	int esi;
	int edi;
	int ebp;
};

// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
	RUNNABLE, RUNNING, ZOMBIE };
    
// the information xv6 tracks about each process
// including its register context and state
struct proc {
	char *mem; // Start of process memory
	uint sz; // Size of process memory
	char *kstack; // Bottom of kernel stack
	// for this process
	enum proc_state state; // Process state
	int pid; // Process ID
	struct proc *parent; // Parent process
	void *chan; // If non-zero, sleeping on chan
	int killed; // If non-zero, have been killed
	struct file *ofile[NOFILE]; // Open files
	struct inode *cwd; // Current directory
	struct context context; // Switch here to run process
	struct trapframe *tf; // Trap frame for the
	// current interrupt
};

구조체로 context라는 것이 있고, 그 안의 인텔 계열 레지스터 이름들을 가진 integer변수들이 있다.

그리고 process를 끊었다가 다시 이을때 복원해줘야 하는 정보들이 proc안에 들어있다. (proc 전체가 pcb가 된다.)

pid(프로세스 식별 번호), 프로세스의 상태 정보, context(레지스터의 값들) 등등이 들어간다.

즉 pcb안에 context라는 구조체가 또 들어가는 것이다.

 

* 2023 국민대학교 소프트웨어학부 황선태 교수님의 운영체제 수업을 듣고 정리한 내용입니다.

* 원서 출처 : https://pages.cs.wisc.edu/~remzi/OSTEP/