운영체제

프로세스 관리

mmalmmizal 2023. 9. 6. 13:43

프로세스가 실행되기 위해서는 일정한 메모리 영역을 확보하고, 여기에 프로세스가 실행해야 할 내용이 기록되어 있어야 함.

하나의 프로세스가 차지하고 있는 메모리 영역은 크게 세 가지로 나누어 짐.

메모리 상의 프로세스 구성

  • 코드 영역
  • 데이터 영역
  • 스택 영역

 

디스크 상의 실행 프로그램 파일 내용 메모리 상의 프로세스 구성
파일 헤더 (코드/데이터 섹션 정보)  
코드 섹션(기계어 코드) 코드 영역
데이터 섹션(전역변수 등) 데이터 영역
  스택 영역 (메모리 영역만 확보, 스택 바닥부터 시작)

 

 

유닉스

fork() 시스템 콜에 의해 새로운 프로세스 생성

exec() 시스템 콜에 의해 실행 내용을 다른 프로그램으로 변경.

 

 

프로세스의 일생과 상태천이

  • 프로세스 상태

Running : CPU에 의해 현재 실행되고 있는 상태

Ready : 실행 가능 상태. 스케줄러는 이 상태의 프로세스 중 하나를 선택함. 선택되면 running 상태로 전환

Waiting : 특정 조건이 만족될 때까지는 실행될 수가 없는 상태, 스케줄러는 이 상태의 프로세스들은 선택 대상에서 제외함.

Suspended : 강제로 실행이 무기한 중지된 상태. 마찬가지로 스케줄러의 선택 대상에서 제외됨.

 

 

현재 Running 상태의 프로세스가 종료되거나 Waiting 상태로 전환되지 않은 상태에서 스케줄러가 실행되는 경우에, 스케줄러는 Ready 상태의 프로세스들과 현재 Running 상태의 프로세스를 포함한 집합에서 하나 선택. 이 과정에서 다른 프로세스가 선택되면 현재 Running 중인 프로세스는 밀려나서 레디상태로 전환됨. 이 과정을 선점(preemption)이라고 함.

 

 

프로세스 제어 블록

PCB(Process Control Block)

C언어로 운영체제를 구현할 경우, 프로세스 제어블록은 구조체 형태로 정의하고

프로세스가 하나 생성될 때마다 이 구조체를 하나씩 할당함.

 

 

  • 프로세스 제어 블록에 기록하는 정보

프로세스의 현재 상태 (ready, running, waiting)

스케줄링에 사용되는 정보 (우선순위, 실행 시간)

프로세스 별 고유 번호

사용자 관련 정보

사용 중인 메모리 영역 (코드영역, 스택영역, 데이터 영역)

사용중인 자원 관련 정보 (open 상태의 파일)

문맥 교환 과정에서 저장해야 할 정보 (CPU 내부에 존재하는 레지스터들의 값, 다음 실행해야 할 명령어 주소 등)

프로세스들의 목록 관리를 위한 항목

프로세스 종료 코드

멀티코어 cpu이면 이 프로세스를 실행하는 코어의 번호

 

 

 

PCB 예시)

NextProcess  
State(프로세스 상태) Priority (프로세스 우선 순위)
ProcessId UserId(사용자 고유 번호)
CodeAddr CodeSize
DataAddr DataSize
StackAddr StackSize
FileDescTable(현재 open 상태 파일들의 구조체에 대한 포인터 기록하는 배열)  
Context (문맥 교환 과정에서 CPU 내부의 레지스터 값 기록하는 배열)  
struct ProcessControlBlock{
	struck ProcessControlBlock *NextProcess;
	int State, Priority;
	int ProcessId, UserId;
	unsigned long CodeAddr, DataAddr, StackAddr;
	int CodeSize, DataSize, StackSize;
	struct OpenFile *FileDescTable[12];
	unsigned long Context[8];
}

멀티스레드 프로세스

  • 스레드(thread) : 멀티스레드 프로세스를 지원하는 운영체제에서 프로세스를 구성하는 독립적인 실행 단위

동일한 프로세스에 포함된 동료 스레드 간에는 코드 영역, 데이터 영역을 공유함.

각 스레드는 독자적 함수를 실행하면서 지역변수를 사용하는데, 이것은 스택 영역 일부에 해당되고 스레드 간에는 서로 독립적이어야 함.

 

  • 경량 프로세스 : 스레드의 다른 이름.
  • 중량프로세스 : 프로세스 당 하나의 스레드만 지원하던 예전 운영체제의 프로세스

하나의 프로세스를 지나치게 많은 스레드로 나누면 스레드들 간의 스케줄링 오버헤드 및 문맥교환 오버헤드가 늘어나고 스레드들 간의 동기화를 위한 오버헤드도 늘어날 수 있음.

또한 스레드 별로 스택을 할당하므로 전체 스택 합이 늘어남.

 

 

 

프로세스 제어 블록과 별도로 존재하는

  • 스레드 제어 블록 (TCB)

 

Solaris와 windows에서는 프로세스 제어블록과 스레드 제어블록을 확실히 구별하여 사용하지만,

리눅스에서는 프로세스와 스레드를 별도로 구별하지 않고.

제어블록도 task_struct라는 동일한 구조체를 사용함.

대신 시스템 콜 함수 clone을 사용하여 프로세스를 생성할 때 메모리 영역이나 open 상태의 파일 등을 공유할지 지정가능.

 

 

멀티태스킹 프로그램 예

pikoKernel은 멀티태스킹 운영체제의 구현 방법을 이해하기 쉽도록 가능한 단순하게 구현한 운영체제 커널.

운영체제는 하드웨어 상에서 직접 실행되지만 pikoKernel은 간편하게 실행시켜 볼 수 있도록 하기 위해서 스레드 패키지 형태로 구현함. 이것은 리눅스에서 하나의 프로세스로 실행되며 여기에 여러 개의 스레드들이 실행될 수 있도록 구현함.

시스템 모드와 유저 모드의 구별이 없고, 시스템 콜 함수는 소프트웨어 인터럽트를 이용하지 않고 바로 해당 함수를 호출하도록 되어 있음.

스레드의 특성 상 이들 간의 보호 기능이 전혀 없고, 스레드는 동일한 프로세스의 함수나 전역변수를 모두 공유하기 때문에, 함수를 호출하거나 전역변수들에 접근할 수 있음.

최초의 사용자 스레드는 userMain 함수로 시작한다.

이 함수에 적절한 스레드 작업을 작성하고, 여러 개의 스레드가 필요하면 여기서 적절히 생성하면 됨.

스레드의 종료 과정은 일반 운영체제에서 exit함수를 호출하거나 main함수에서 반환되는 것과 마찬가지로, threadExit 함수를 호출하거나 스레드의 시작함수로 반환되면 종료됨.

 

pikoKernel을 위한 간단한 멀티태스킹 프로그램

 

int userMain(int arg){
threadCreate(20, BodyA, 0);
threadCreate(20, BodyB, 0);
threadCreate(20, BodyB, 0);

int BodyA(int arg){
int myid = threadSelf();
int count = 0;

while(count<=20){
	count++;
	print("(A%d:%d)", myid, count);
	threadYield();
	}
}

int BodyB(int arg){
	int myid = threadSelf();
	int count = 0;
	while(count<=20){
	count++;
	printf("(B%d:%d)", myid, count);
	threadYield();
}

}

*yield 양도하다

 

 

threadCreate() 함수는 새로운 스레드를 생성하는 시스템 콜 함수

첫번째 parameter(매개변수)는 생성되는 스레드의 우선 순위를 지정.

두번째는 시작 함수를 지정.

세번째는 스레드의 시작 함수의 파라미터로 전달됨.

 

 

threadSelf() 함수는 호출하는 스레드 자신의 고유 번호를 알아내는 시스템 콜 함수

threadYield()는 다른 스레드로 실행 순서를 양보하는 시스템 콜 함수

 

멀티태스킹 프로그램의 실행 결과

 

스레드 번호가 2,3,4

 

아래는 윈도우 기능을 사용하지 않아 스레드출력이 섞여있는 창

 

(A2:1)(B3:1)(B4:1)(A2:2)

(B3:2)(B4:2)(A2:3)(B3:3)

(B4:3)(A2:4)(B3:4)(B4:4)

(A2:5)(B3:5)(B4:5)(A2:6)

(B3:6)(B4:6)

 

 

picoKernel의 문맥 교환 구현

picoKernel에서 적용한 문맥 교환 함수 contextSwitch

 

이 함수에서는 레지스터들의 값을 저장하고 복구하는 작업을 실행해야하는데, 이것은 C언어로 불가능.따라서 어셈블리 언어로 별도 파일에 작성하고 커널의 다른 부분들과 함께 링크됨.

 

contextSwitch(old, new) 형식으로 호출함. old와 new는 스레드 1과 2의 문맥을 저장해놓은 주소를 기록하고 있는 필드 (ContextAddr)의 주소를 가리킴.

스레드 1의 문맥을 저장한 후 그 주소를 old가 가리키는 필드에 기록. 다음에 new가 가리키는 필드에서 주소를 읽어오고, 그 주소가 가리키는 영역에 저장된 문맥을 읽어와 복구함.