전공/시스템 프로그래밍(운영체제)

9. Process Management

문정훈 2021. 11. 11. 03:09

1. Programms / Process / Thread 개요 

1) Programms / Process

프로그램이라고 하는 것은 2진 형태의 데이터의 집합이다.  dormant라고 하는데 이는 아직 동작을 시작하지 않았지만 실행될 수 있는 상태를 말한다. 

프로그램이 실행된다고 하는 것은 HDD에 저장되어 있는 프로그램이 process 형태로 바뀌는 것이다. 

그럼 왜 프로그램은 2진 형태로 존재해야하는가?

=> 프로그램이 메인 메모리에 적재되었다고 모두 실행되는 것이 아니다. 

그럼 어떤 프로세스가 현재 실행 중이다 애기할 수 있냐면 CPU 자원을 할당 받았을 때만 프로세스가 실행되는 것이다. 

메인 메모리에 적재된 명령어 중  CPU 자원을 할당 받지 않는 명령어는 sleep 한다라고 한다. 

 

프로세스 A를 실행하다가 잠시 멈추고 프로세스 B에게 자원을 할당한다고 해보자. 

그럼 프로세스 A는 sleep mode로 들어간다. CPU 자원을 최대한 할당할 수 있는 시간이 정해져있다. 

특정 시간이 되면 CPU 자원은 다른 프로세스로 할당을 넘겨야한다. 

 

정리=>

Process는 언제나 Running Program이 아니다. CPU 자원을 할당 받았을 때만이다. 

★ create Process는 HDD-> RAM으로 load 하는 작업을 말한다.

run Process는 CPU 자원을 할당 받았을 때 실행시킬 때를 말한다.

 

프로세스에 대한 정보(사용자, 사용자 정보와 연계된 프로세스 등..)도 커널에서 가지고 있다.

 

우리가 만든 프로그램은 프로세스에게 할당된 파일들이 있다. 

그리고 프로세스는 프로세스를 실행한 사용자 정보도 가지고 있다. 

그리고 스레드에 대한 정보도 가지고 있다. 

 

2) Process ID

프로세스를 관리하기 위해서는 프로세스를 특정할 수 있어야한다. 

PC에서 실행되는 모든 프로세스는 ID를 가진다. ID는 현 시점에서 컴퓨터에서 실행되는 모든 프로세스간에는 ID 값이 서로 Unique하다.  

즉 오늘 실행한 프로세스 A가 할당 받은 ID가 내일 실행할 프로세스 A의 ID와 다를 수 있다. 

PID라고 하는데 PID 0번은  만약 컴퓨터가 시작되고 프로세스가 0개라면 (사용자도 os도 없음) 

이때 스스로 무엇인가를 반드시 실행해야하는데 이 무엇인가에 해당하는 것이 idle process이며 이것의 PID는 0이다. 

 

그럼 PID가 1번은 init process이다. 커널이 부팅을 한 다음에 커널이 실행하는 최초 프로세스이다. 

1. /sbin/init
2. /etc/init
3. /bin/init
4. /bin/sh

커널이 부팅하면 1번 프로세스를 먼저 찾고 실행하는데 만약 1번 프로세스가 없으면 2번 프로세스를 찾아서 실행한다. 만약 2번도 없으면 3번을 3번도 없으면 4번을 찾아 커널의 최조 프로세스로 실행을 한다. 

 

PID를 저장하는 값은 pid_t라는 값이 있다. 프로그램 측면에서 PID는 pid_t 타입에 저장하는데 

이것이 정의된 헤더 파일은 <sys/types.h> 이다. 

#include <sys/types.h>
#inlcude <unistd.h>

pid_t getpid(void);

pid_t getppid(void);

printf("My pid=%jd\n", (intmax_t) getpid());
pritnf("Parent's pid=$jd\n", (intmax_t) getppid());

사실 리눅스에서 pid_t는 int typde임 위 함수는 현재 실행되고 있는 프로세스의 PID를 가져올 수 있다. 

두 번째 getppid 메소드는 현재 실행되고 있는 프로세스의 부모 프로세스의 PID를 가져온다. 

출력할 때는 위 방법을 사용한다. 

 

 

3) Thread

스레드는 프로세스 안에서 진행되는 활동이라고할 수 있다. 

스레드는 프로세스가 원하는 작업을 실제로 수행해준다. 

스레드는 stack, processor state, register를 가지고 있다. 

프로세스끼리는 메모리를 공유하지 않는다.

스레드는 한 프로세스 안에 여러개 존재하며 프로세스의 메모리를 공유하며 동작한다.

 

 

4) Process ID Allocation

Process ID는 프로세스가 할당 될 때 마다 마지막에 할당된 프로세스 ID 다음의 ID를 할당해준다. 즉
Process ID는 linear 하게 증가한다. (0,1,2,3,4....)

예를 들어 ProcessA->ProcessB->ProcessA종료->ProcessC라고하면

Process ID 할당은 ProcessA(PID: 1번)->ProcessB(PID: 2번)->ProcessA종료->ProcessC(PID: 3번(1번 할당x)) 이렇게 할당한다. 

그리고 만약 PID가 /proc/sys/kernel/pid_max에 도달하면 어디로 가냐? 1번 할당을 준다. 

즉 프로세스의 종료로 낱알이 빈 PID를 다시 순차적으로 부여한다. 

 

5) Process Hierachy

init 프로세스를 제외한 모든 프로세스는 부모 프로세스를 가진다.  

init process(1번) 프로세스가 2,3,4 프로세스를 실행할때 이 2,3,4 프로세스는 1번 프로세스의 자삭 프로세스라고 한다.

모든 프로세스는 각각의 프로세스는 어떤 사용자 또는 그룹에 의해 포함되어 있다.

자식 프로세스의 권한은 부모 프로세스의 권한을 상속 받는다. 

 

각각의 프로세스는 프로세스 그룹에 속해있는데 이 프로세스 그룹은

프로세스들이 같은 부모를 가지거나 같은 사용자로부터 시작된 프로세스들은 같은 프로세스 그룹에 속하게 된다. 

예를 들어 리눅스 셸 명령어로 ls || less  라는 두 명령어를 파이프라인으로 연결하여 실행하면 

Shell은 부모 프로세스가 되고 자식 프로스세로 ls 프로세스와 less 프로세스가 있게 되며 이들은 같은 프로세스 그룹에 속하게 된다. 

 

 

6) Executing a Program  / Creating a New Process

● Executing a Program(프로그램 실행)

Executing a Program이란 즉 프로그램을 실행하는 것은

★ create Process는 HDD-> RAM으로 load 하는 작업을 말한다. 

 run Process는 CPU 자원을 할당 받았을 때 실행시킬 때를 말한다. 

위 두 과정을 진행하는 것을 말한다. 

 

● Creating a New Process

기존에 RAM에 존재하는 프로세스 (부모 프로세스)가 새로운 자식 프로세스를 실행하는 것을 말한다. 

아주  가끔은 새로운 프로세스가 새로운 프로그램을 실행하는 경우가 있다. 

 

자세히 설명하면

기존의 프로세스A가 1부터  10까지 명령어가 실행된다고 해보자.

5라는 구간은 fork() 이다.

그럼 6부터 10까지를 duplication하여 다른 새로운 프로세스B로 실행시키는 것을 말한다. (fork 과정)

A와 B는 부모와 자식 관계로 존재한다.

 

이때 6부터 10 사이에 execl() 라는 코드가 있다고 가정하면 프로세스 B는 (자식 프로세스) execl()를 만나는 순간 프로세스 B의 RMA을 모두 비운다. 그리고 exec()가 실행하고 하는 새로운 프로그램을 그 지운곳에 적재하고 실행을 한다. 

이때는 부모 프로세스 A와 자식 프로세스 B(지금은 execl()에 의해 적재된 새로운 프로세스임)의 내용은 달라진다. 

 

7) 메소드 정리

● execl

#include <unistd.h>

int execl(const char *path, const char *arg, ...);

int ret;
ret = execl("/user/bin/vi", "vi", "/home/kidd/hooks.txt", NULL);
	if(ref == -1) perror("execl");

위 명령어는 path에 있는 vi라는 명령을 실행하는 것이다. 그럼여기서 의문이 결국 vi라는 명령어가 /bin/vi 명령어인데 왜 첫 번쨰, 두 번째 매개변수로 vi명령어 지정을 두번하냐?

때론 /bin/(링크) 첫 번째 매개변수는 링크를 가리킬수도 있다. 그 링크가 vi 명령어일 수 있기 때문이다. 

 

매개변수를 ...이라고 한것은 printf의 인자가 다양하듯이 execl의 인자도 다양한데
(이것을 variadic이라고 한다. ) 인자의 끝은 항상 NULL 이어야한다. 

vi 명령어는 편집기를 여는 것인데 세 번째 매개변수는 vi 명령어에 전달되는 input 값으로

vi /home/kidd/hooks.txt 명령을 실행하라는 거임

근데 여기서 중요한 것은 execl 명령은 프로세스 vi를 불러와서 적재를 하는데 이 코드를 호출한 프로세스의 버퍼 영역을 다 지우고 그곳에 적재를 하는 것이다. 

 

execl을 호출하면 기존의 프로세스의 코드 내용을 모두 지운다고 하였는데 실제로 지우는 것과 지우지 않는 것을 정리하면

지우는 것 지우지 않는것
1. Peding signals
2. Memory lock
3. Thread attributes
4. States
1. PID
2. parent PID
3. priority
4. owngin user
5. group
6. open file are inherited across an exec

open file are inherited across an exec

이것에 대해 부연 설명을 하면 A라는 프로세스에서 fork를 하면 B라는 자식 프로세스가 만들어진다 하였다(위에서 설명함) 그리고 B에서 execl을 해도 B의 프로세스가 A로부터 상속 받은 file들은 지우지 않는다는 것이다. 

 

 

● execlp

#include <unistd.h>
//v로 끝나는 것들은 벡터임
int execlp(const char file, const char *arg, ...);

int execle(const char* path, const char* arg,...);

int execv(const char* path, char* const argb[]);

int execvp(const char *file, char* const argvp[]);

int execve(const char *filename, char *const argv[], char *const envp[]);
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
  int ret;
  char *args[] = {"vi", "./test.txt", NULL};
  
  ret = execv("/user/bin/vi", args);
  
  if(ret == -1) {
    perror("execl");
  }
  
  return 0;
}

위 다양한 코드들은 execl의 다양한 형태인거임

 

 

● fork()

#include <sys/types.h>
#inlcude <unistd.h>

pid_t fork(void);

위 메소드는 Creating a New Process를 해주는 System call이다. 

return 값으로 생성되는 자식 프로세스의 PID이다. 

부모 프로세스에서의 fork의 반환값은 0보다 큰 자식 프로세스의 PID이다. 즉 

위 그림을 보면 fork시 위 그림과 같이 자식 프로세스가 생성된다.

왼쪽의 부모 프로세스의 fork의 반환값은 0보다 큰 값인,  자식 프로세스의 PID이다.

근데 오른쪽의 자식 프로세스에서 fork를 통해 return 되는 값은 0이다. 

아래는 코드 예시이다. 

#include <stdio.h>
#include <errno.h>
#include <sys/types/h>
#include <unistd.h>

int main(void) {
  pid_t pid;
  pid = fork();
  
  if(pid > 0) printf(" I am the parent of pid = %d\n", pid);
  else if(pid == 0/*!pid*/) printf(" I am the child of pid = %d\n", pid);
  else if(pid == -1) perror("fork");
  
  return 0;
}

 

2. Lazy optimization

fork로 생성된 새로운 프로세스에 새로운 프로그램을 얹는 방법을 설명한다.

즉 fork로 생성된 자식 프로세스에서 exec 메소드를 실행하여 자식 프로세스에 새로운 프로세스를 만드는 것을 말함.

1) Example to spawning a new process

pid_t pit;

pid = fork();

if (pid == -1) perror("fork");

if(!pid) {
  const char *args[] = {"vi", NULL};
  int ret;
  
  ret = execv("/user/bin/vi", args);
  if( ret == - 1) {
    perror("execv");
    exit(EXIT_FAILURE);
  }
}

 

2) Lazy optimization

※ 선행 개념: page 개념
페이지라고 하는 것은 메인 메모리에 적재되는 단위인데 HDD 에 있는 프로그램은 page 단위로 저장되어 있다. 
이것을 load할 때 HDD 에 page 단위 명령어를 프로세스로 load하는 것이다. 

근데 여기까지 정리로 봤을 떄는 fork시 자식 프로세스가 fork이 후의 부모 프로세스의 코드를 복사한다고 표현하였는데 fork될 때 부모 프로세스의 명령어를 page 단위로 자식 프로세스 공간을 만들고 그곳에 복사가 된다.

 

Lazy optimization의 개념을 설명하면,

fork될 때 이전까지는 자식 프로세스 공간을 만들고 그 공간에 부모 프로세스의 코드를 page 단위로 복사한다고 하였는데, 실제로는 복사하지 않는다. 

우선 그 이유는 메인 메모리에 적재되어있는 부모 프로세스의 내용을 자식 프로세스 공간을 메인 메모리에 만들고 복사하는 과정인 IO 작업이라고 할 수 있는데( 메인 메모리의 read, write) 이것은 CPU 입장에서 time consuming이다. 즉 CPU 시간 낭비이다. 이런 낭비 적인 일을 굳이 하지 않는다는 것이다. 그럼 어떻게 하냐?

 

부모 프로세스가 실행되다가 fork를 만나면 자식 프로세스가 생성되는데 자식 프로세스는 메모리 공간을 아직은 가지지 않는다. 자식 프로세스의 PC를 부모 프로세스의 fork 부터 가리키게 된다.

그러다가 자식 프로세스의 PC 가 execl을 만나면 그제서야 자식 프로세서는 메인 메모리에 공간을 만들고 그 공간에 execl 메소드의 매개변수인 path 에 저장된 프로세스를 실행하게 된다. 

 

3) vfork()

vfork를 이르킨 시점부터 자식 프로세스가 생성되는데 자식 프로세스가 exit되거나 또는 exec 메소드를 만나기 전까지 부모 프로세스는 block 된다. 

이떄 자식 프로세스는 공유하고 있는 부모 프로세스의 공간을 훼손해서는 안된다. 

 

만약 부모 프로세스가 block된 상태에서 자식 프

로세스 실행되고 있다가 비정상적으로 종료되면 부모 프로세스는 계속 block 된 상태로 기다린다. -> 문제점 

 


 

3. Terminating a Process

1) exit 함수

#include <stdlib.h>

void exit(int status);

프로세스는 종료할 때 위 메소드를 호출할 때 반환하는 값이 필요없다. 

대신 status라는 파라미터 값이 있는데 프로세서의 종료 상태를 나타낼 수 있는 값인데

다른 프로그램에서 이 값을 나중에 확인해볼 수 있다. 

표준 exit 함수는 실행되면 모든 남은 IO 작업을 flush 한다. 

그리고 temfile() 로 생성된 temporary 한 파일들은 제거 된다. 

 

2) System Call: _exit

#include <unistd.h>

void _exit (int status);

exit이라는 API가 호출되면 시스템 콜로는 _exit 메소드가 호출된다. 

이 시스템 콜이 호출되면 커널은 프로세스에 의해 만들어진 리소스(open file, System V,  sempahores)를 cleans up 한다.   

그리고 exit된 프로세스의 부모 프로세스에게 이를 알린다. 

 

3) atexit, on_exit 

#include <stdio.h>
#include <stdlib.h>

void out() {
  printf("atexit() succeeded\n");
}

int main () {
  if(atexit(out)) fprintf(stderr, "atexit() failed\n");
}

종료되는 시점에 out() 을 실행하는데 만약 에러 값을 리턴하면 if문이 돌게 된다. 

종료 시점에 즉 atexit을 만나면 이 함수의 매개변수로 등록한 함수가 실행되면서 프로그래머가 프로스세가 종료되는 시점에 실행되야할 작업을 정의할 수 있다.

근데 atexit에 등록할 수 있는 함수는 여러개일 수 있다. 이때 이때 실행되는 순서는 파라미터의 역순으로 실행된다. 

 

 

4) wait 함수

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

자식 프로세서가 종료되면 프로세스는 사라지는데 , 자식 프로세서가 부모 프로세서 보다 먼저 종료된다면 

커널은 이를 자식 프로세서를 좀비 프로세서(자식 프로세스의 뼈대만 남기는) 로 만든다. 

부모 프로세서가 자식 프로세서가 어떻게 죽었는지 조사하기 전까지 좀비 프로세서로 남아 있게 된다.  

 

부모 프로세스가 자식 프로세스의 종료 상태를 확인하는 함수가 wait 함수이다. 

근데 이 wait 이 호출되고 만약 자식 프로세스가 종료되지 않았다면 wait을 호출한 부모 프로세스는 block 되어 대기한다. 

 

매개변수 status는 우선 부모 프로세서의 status라는 값에 커널이 자식 프로세서의 상태를 저장한다. 

그리고 wait의 반환 값은 자식 프로세서의 PID이다.

예를 들어 부모가 있고 자식 프로세서가 여러개 인데 그 중 죽은 자식 프로세서의 PID를 wait은 반환하고 죽은 자식 프로세스의 상태를 매개변수 status 에 저장한다.

 

위 메소드에 status 값을 저장하면 어떻게 자식 프로세스가 죽었는지 알 수 있는 메소드 들이다. 

 

wait을 사용한 에시이다. 메인 함수를 가진 부모 프로세서에서 status 변수를 만들고 이 wait(&status)를 호출하면

이때 만약 자식 중 종료된 프로세스가 있으면 pid 변수에 해당 PID를 반환하고 죽은 자식 프로세스가 없다면 wait은 -1을 반환하고 block 상태가 되어 죽은 프로세스가 나타날 때 까지 기다리게 된다. 

'전공 > 시스템 프로그래밍(운영체제)' 카테고리의 다른 글

10. Threading  (0) 2021.12.27
8. Multy-Process와 Mutual Locking  (0) 2021.11.06
7. Buffered IO  (0) 2021.10.17
6. Blocking, non-Blocking  (0) 2021.10.17
5. 추가적인 System Call  (0) 2021.10.17