Ch8.4 Process Control

Park Choong Ho·2021년 9월 26일
0

8.4 Process Control

Unix 운영체제는 C 프로그램 프로세스들을 다루기 위한 여러가지 system call들을 제공합니다.

  • getpid, getppid
  • fork
  • waitpid
  • execve

8.4.1 Obtaining Process IDs

  • 각 프로세스들은 양의 정수인 식별자 Process ID (PID)를 가집니다.
  • getpid 함수는 호출한 프로세스 PID값을 반환.
  • getppid 함수는 호출한 프로세스의 부모 프로세스 PID를 반환.

ex) getpid, getppid 예시

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

pid_t getpid(void);
pid_t getppid(void);

int main(){
    int pid = getpid();
    int ppid = getppid();

    printf("pid는 %d입니다.\n", pid);
    printf("ppid는 %d입니다.", ppid);
}

8.4.2 Creating and Terminating Processes

프로세스 3가지 상태

  • Running(동작)
  • Stopped(중지)
  • Terminated(종료)

Running

  • CPU에 의해 실행되고 있거나 언젠가 kernel에 의해 결국 스케줄(scheduled)되기를 기다리는 상태.
  • 이 모든 것을 종합해 프로세스가 "돌아가고 있다 (Running)" 라 한다.

Stopped

  • 진행이 중지되고 (suspended) 스케줄되지 않은 상태(미래에 스케줄러에 의해 스케줄링되지 않는다)
  • 프로세스는 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU signal를 받으면 멈춥니다.
  • SIGCONT signal을 받기 전까지 그 상태가 유지되고 SIGCONT를 받으면 다시 프로세스가 Running 상태로 전환됩니다.

ex) 프로세스 Stopped 상태 예시

import time
import os

pid = os.getpid()

print(pid)

while 1:
    time.sleep(10)
    print(1)
❯❯❯ python3 sigcont.py
7471

여기서 보이는 숫자가 동작하는 파이썬 프로세스 id입니다.
이제 이 프로세스를 중지시켜 보도록 하겠습니다.

❯❯❯ ps aux | grep "6805"
choonghopark      6857   0.0  0.0  4268424    688 s002  R+    6:54PM   0:00.00 grep --color=auto 7471
choonghopark      7471   0.0  0.0  4253060   6804 s001  S+    6:52PM   0:00.04 /usr/local/Cellar/python@3.9/3.9.1_1/Frameworks/Python.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python sigcont.py

보시다시피 프로세스는 S+ 상태입니다.

❯❯❯ kill -s STOP 7471
❯❯❯ python3 sigcont.py
7471
1
1
[1]  + 7471 suspended (signal)  python3 sigcont.py

동작하는 프로세스에 SIGSTOP signal을 보내니 멈춘 것 같이 보입니다. ps aux 명령어를 통해 실제로 멈췄는지 확인해보겠습니다.

❯❯❯ ps aux | grep "7471"
choonghopark      7605   0.0  0.0  4268316    504 s002  R+    7:04PM   0:00.00 grep --color=auto 7471
choonghopark      7471   0.0  0.0  4254084   6728 s001  T     6:58PM   0:00.04 /usr/local/Cellar/python@3.9/3.9.1_1/Frameworks/Python.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python sigcont.py

7471 process의 상태가 S+에서 T로 바뀐것을 확인할 수 있습니다.
이번에는 SIGCONT signal을 줘서 멈춘 프로세스를 다시 동작하게 해보겠습니다.

❯❯ kill -s CONT 7471
❯❯❯ 1
1
1

SIGCONT signal을 주니 멈춘 프로세스가 다시 동작하는 것처럼 보입니다. ps aux 명령어를 통해 확인해 보겠습니다.

❯❯❯ ps aux | grep "7471"
choonghopark      7681   0.0  0.0  4268424    688 s002  R+    7:09PM   0:00.00 grep --color=auto 7471
choonghopark      7471   0.0  0.0  4254084   6728 s001  S     6:58PM   0:00.04 /usr/local/Cellar/python@3.9/3.9.1_1/Frameworks/Python.framework/Versions/3.9/Resources/Python.app/Contents/MacOS/Python sigcont.py

7471 프로세스의 상태가 T에서 S로 바뀐것을 확인할 수 있습니다.

Terminated

영구적으로 멈춘 상태. 프로세스는 3가지 경우에만 종료됩니다. (Terminated)

  1. 프로세스를 종료하는 시그널을 받은 경우
  2. 메인 루틴에서 반환한 경우
  3. exit 함수를 호출한 경우

각 언어마다의 main routine

Python

if __name__ == "__main__":
  pass

Java

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

Javascript

// 자바스크립트는 시작지점이 entry potint입니다. 따로 main routine임을 나타낼 함수가 없습니다.
// 시작하는 곳이 main이다!

C

int main() {
}

각 언어마다의 exit 함수

Python

import sys
sys.exit()

Java

import java.lang.*; 

public class HelloWorld {
    public static void main(String[] args) {
        System.exit(0);
    }
}

Javascript

process.exit();
// javascript는 자체 내장되어 있는 exit 함수가 없습니다. 대신 nodejs는 `process.exit()`가 존재합니다.

C

#include <stdlib.h>

int main() {
  exit(0);
}

Fork

ex) fork 함수를 통해 자식프로세스를 생성하는 예시

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

pid_t fork(void);

int main() {
    int pid;

    if((pid = fork()) == 0) {
        printf("자식! \n");
    }
    printf("부모! \n");
}

fork 함수는 새로운 프로세스를 생성하는 함수입니다. 크게 4가지 특징을 가지고 있습니다.
1. Call once, return twice: fork 함수는 부모 프로세스에 의해 1번 호출되고 1번은 부모프로세스에 한번은 자식프로세스, 이렇게 2번 반환하는 특별한 함수입니다. 부모에게 반환할 때는 자식 프로세스 PID를 자식에게 반환할 때는 0을 반환합니다. 앞서 말한 프로세스 PID는 0이 아닌 정수이므로 fork 함수의 반환값을 보고 현재 흐름이 자식인지 부모인지를 명확히 판별할 수 있습니다.
2. Concurrent execution: 자식과 부모는 concurrent하게 동작하는 별도의 프로세스입니다. 각 프로세스 흐름상에 있는 instruction은 여러 형태로 커널이 언제든 끼어들 수 있습니다. 따라서 프로그래머는 자식과 부모 프로세스의 인스트럭션 순서를 예측할 수 없습니다.
3. Duplicate but separate address spaces: fork 함수가 자식과 부모 프로세스에 반환한 직후에 각 프로세스의 가상 주소 공간을 보면 동일한 것을 확인할 수 있습니다. 스택, 지역 변수, 힙, 전역변수, 코드 모두 같습니다. 하지만 각 프로세스는 별개의 주소공간을 가지게 됩니다. 똑같은 카피본을 서로 가지고 있다고 생각하시면 됩니다. 따라서 그 이후부터 각 프로세스가 만드는 변화들은 서로 영향을 미치지 않게됩니다.
4. Shared files: 위 코드를 실행하면 부모와 자식 모두 같은 화면에 결과값을 출력하는 것을 확인할 수 있습니다. 그 이유는 자식이 부모의 모든 열려있는 파일들을 상속하기 때문입니다. 부모가 fork 함수를 호출하면, 자식은 부모의 열려있는 파일을 모두 상속받고 (기본적으로 프로세스가 있으면 stdin, stdout, stderr는 열려져 있습니다.) 따라서 같은 stdout을 상속받으므로 결과값이 같은 화면에 출력되게 됩니다.

자식 프로세스 특징

  • 새로 생성된 자식 프로세스는 부모프로세스와 거의 동일
  • 부모와 동일한 가상 메모리(data, code ,heap, stack, shared libraries)와 file descriptor 복사본을 받음
  • file descriptor가 같다는 것 = 부모가 오픈한 그 어떤 파일이든 읽고 쓸 수 있음을 의미
  • 자식 프로세스와 부모 프로세스 PID는 다름
  • fork 함수는 호출은 한번, return은 2번하는 함수(부모 프로세스에는 자식 pid, 자식프로세스에는 integer 0을 반환)

8.4.3 Reaping Child Processes

프로세스가 종료되면, 커널은 해당 프로세스를 바로 시스템에서 제거하지 않습니다. 대신, 자식 프로세스는 자기 부모 프로세스에게 수거될 (reap) 때까지 종료 상태를 유지합니다. 부모가 자식을 수거하면, 커널은 자식 exit status를 부모에게 전달하고 종료된 프로세스를 제거합니다.(이 시점부터 자식 프로세스는 존재하지 않게됩니다.) 여기서 종료 상태인데 아직 수거되지 못한 프로세스좀비라고 합니다. 이러한 좀비 프로세스는 종료되었지만 여전히 시스템의 자원을 잡아먹고 있는 존재이기에 적절히 reap하여 시스템에서 제거해주어야 합니다.

부모 프로세스가 종료되면, init process가 자식 프로세스들의 부모 프로세스가 됩니다.

좀비 프로세스

ex) 좀비 프로세스 예시

import os
import sys
import time

print(os.getpid())
stat = os.fork()
print(stat)
if stat == 0:
    sys.exit()
time.sleep(12000)
os.wait()
❯❯❯ python3 zombie.py
10270
10271
0

현재 부모 프로세스 id는 10270이고 자식의 프로세스 id는 10271입니다.

choonghopark@ChoongHoui-MacBookPro ~/De/P/csapp/ch8-exceptional-control-flow/8.4_process_control   main ✱ 3 ?4    ✔
❯❯❯ ps aux | grep "10271"
choonghopark     10271   0.0  0.0        0      0 s001  Z+    8:51PM   0:00.00 (Python)

자식 프로세스의 상태가 좀비가 된것을 확인할 수 있습니다.

Init 프로세스

Init 프로세스는 3가지 특징을 가지는 프로세스입니다.

  1. PID가 항상 1.
  2. 시스템 시작시점(컴퓨터 전원이 켜지는 시점)에서 커널이 생성.
  3. 시스템 종료전까지 종료되지 않음.
  4. 모든 프로세스의 조상.

부모 프로세스가 자식 좀비 프로세스들을 수거하지 않고 종료되면, 커널은 init 프로세스가 자식 프로세스들을 수거하게끔 합니다. (init 프로세스의 경우 지속적으로 wait 함수를 호출합니다. wait 함수에 대한 내용은 추후에 설명하겠습니다.) 이렇게 커널에서 좀비 프로세스를 관리해 주지만, 자식 좀비 프로세스들을 부모 프로세스에서 수거하는 것은 필요한 작업입니다. 그 이유는 앞서 말했듯 좀비 프로세스가 돌고있지 않더라도 여전히 시스템 메모리 리소스를 잡아먹기 때문입니다.

Waitpid

프로세스는 waitpid 함수 호출을 통해, 자식 프로세스가 종료 또는 멈출 때까지 기다릴 수 있습니다. waitpid 함수는 다소 복잡한 함수인데, 기본적으로 자식 프로세스가 종료될 때까지 호출한 부모 프로세스 진행을 중지합니다.(options 인자로 0을 준 경우가 이에 해당) waitpid 함수는 종료된 자식의 PID를 반환합니다.(자식이 waitpid 함수 호출전 종료했어도 자식 PID를 반환합니다.) 이 시점에서 종료된 자식프로세스는 수거되고 커널은 시스템으로부터 해당 프로세스를 완전히 제거합니다.

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

pid_t waitpid(pid_t pid, int *statusp, int options)

waitset 멤버들은 pid 인자에 의해 결정됩니다.

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

pid_t waitpid(pid_t pid, int *statusp, int options);
  • pid > 0: PID의 값이 pid인 프로세스
  • pid = -1: waitpid 호출한 프로세스의 모든 자식프로세스중 프로세스 하나

waitpid 함수는 Unix process group을 포함한 여러 다른 형태의 waitset 또한 지원합니다. waitpid 함수는 waitset에 있는 프로세스 중 하나가 terminate될때까지 기다립니다. waitpid를 호출하기전에 terminate된 프로세스가 하나라도 있으면 호출시 그 중 하나의 프로세스 id를 즉시 반환합니다.

waitpid 함수는 options 인자를 통해 default behavior를 변경할 수 있습니다.

  • WNOHANG: waitset에 존재하는 어떤 자식 프로세스들도 아직 terminate되지 않은 경우 0을 반환합니다. default behavior는 child가 terminate될 때까지 호출한 프로세스를 suspend합니다. 기다리는 동안 다른 어떤 작업을 하기를 원할 경우, 유용하게 사용할 수 있는 options입니다.

  • WUNTRACED: waitset에 있는 프로세스가 terminate 또는 stop될 때까지 호출한 프로세스를 suspend합니다. 자식 프로세스의 PID를 반환합니다. default behavior는 terminate된 자식인 경우에만 반환합니다.

  • WCONTINUED waitset에 있는 프로세스가 terminate되거나 stop한 작식 프로세스가 SIGCONT 시그널을 받아 재개할 때까지 호출한 프로세스를 suspend합니다.

|(oring)을 통해 이 옵션들을 조합할 수 있습니다.

  • WNOHANG | WUNTRACED: waitset에 있는 어떤 프로세스들도 terminate되거나 stop되지 않은 경우 즉시 0을 리턴합니다. 또는 stopped되거나 terminated된 프로세스의 PID를 반환합니다.

만약 statusp 인자가 non-NULL인 경우, waitpid 함수가 자식 status 정보를 인코딩합니다. wait.h는 status 인자를 해석할 몇몇 매크로들을 정의한 파일을 포함하고 있습니다.

  • WIFEXITED(status): exit을 call하거나 반환함으로써 자식 프로세스가 terminate된 경우 true를 반환합니다.(normally terminate)

  • WEXITSTATUS(status): 일반적으로 terminate된 자식 프로세스 exit status를 반환합니다. 이 상태는 WIFEXITED()가 true를 반환한 경우에만 정의됩니다.

  • WIFSIGNALED(status): signal에 의해 자식 프로세스가 terminate된 경우 true를 반환합니다.

  • WTERMSIG(status): 자식 프로세스가 terminate되게끔 야기한 signal 숫자를 반환합니다. 이 상태는 WIFSINALED()가 true를 반환한 경우에만 정의됩니다.

  • WIFSTOPPED(status): child가 stop한 경우 true를 반환합니다.

  • WSTOPSIG(status): 자식 프로세스가 stop되게끔 야기한 signal 숫자를 반환합니다. 이 상태는 WIFSTOPPED()가 true를 반환한 경우에만 정의됩니다.

  • WIFCONTINUED(status): 자식 프로세스가 SIGCONT signal을 받아 재시작한 경우 true를 return합니다.

Error Conditions: 만약 호출하는 프로세스가 자식이 없는 경우 waitpid는 -1을 반환합니다. 그리고 errno를 ECHILD로 설정합니다. 만약 waitpid 함수가 signal에 의해 interrupt된 경우 -1을 반환하고 errno을 EINTR로 설정합니다.

Examples of Using waitpid

waitpid 함수 관련된 코드 몇가지를 살펴보겠습니다.

#include "csapp.h"
#define N 2

void unix_error(char *msg) /* Unix-style error */
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}

pid_t Fork(void)
{
    pid_t pid;

    if ((pid = fork()) < 0)
	unix_error("Fork error");
    return pid;
}

int main(){
    int status, i;
    pid_t pid;

    for(i = 0; i < N; i++){
        if((pid = Fork()) == 0){
            exit(100 + i);
        }
    }

    while((pid = waitpid(-1, &status, 0)) > 0){
        if(WIFEXITED(status)){
            printf("child %d terminated normally with exit status=%d\n", pid, WEXITSTATUS(status));
        }
        else{
            printf("child %d terminated abnormally\n", pid);
        }
    }

    if(errno != ECHILD){
        unix_error("waitpid error");
    }
    exit(0);
}
for(i = 0; i < N; i++){
    if((pid = Fork()) == 0){
            exit(100 + i);
    }
}

위 부분에서 child process를 n개 생성하고 각 process가 고유한 exit status와 함께 terminate합니다.

while((pid = waitpid(-1, &status, 0)) > 0){
        if(WIFEXITED(status)) {
            printf("child %d terminated normally with exit status=%d\n", pid, WEXITSTATUS(status));
        }
        else {
            printf("child %d terminated abnormally\n", pid);
        }
}

여기서 부모 프로세스는 모든 자식 프로세스등 중 하나가 terminate될 때까지 기다립니다.(waitpid 함수에 넘겨진 인자 값이 -1) 자식 프로세스가 terminate될 때마다 자식 프로세스 PID를 반환합니다.

모든 자식 프로세스들이 reap되면, 그 다음 waitpid 함수 호출은 -1을 반환하고 ECHILD에 errno을 설정합니다.

여기서 중요한 점은 프로그램이 자기 자식들을 어떤 특정한 순서에 따라 reap하지 않는다는 점입니다. 시스템에 따라 판이합니다. 이건 nondeterministic 이란 특징인데 이것이 concurrency를 추론하는 것을 어렵게 만드는 요소입니다. 따라서 이 부분도 프로그래머는 절대 예측할 수 없으며 해서도 안됩니다.

8.4.4 Putting Processes to Sleep

sleep 함수는 일정시간 동안 프로세스를 suspend합니다.

#include <unistd.h>

unsigned int sleep(unsigned int secs);

시간이 경과하면 0을 반환하고, 시간이 남으면 남은 시간을 초로 환산해 반환합니다. 후자 예시는 프로세스가 signal에 의해 방해받은 경우입니다. Signal이란 개념은 추후에 더 자세히 공부하도록 하겠습니다.

pause 함수는 시그널이 프로세스에 의해 받아들여지기까지 suspend합니다.

#include <unistd.h>

int pause(void)

Practice Problem 8.5

앞서 설명한 sleep 함수의 wrapper 함수인 wakeup 함수를 아래 인터페이스에 맞게 작성하시오.

unsigned int wakeup(unsigned int secs);
#include <unistd.h>
#include<stdio.h>

unsigned int wakeup(unsigned int secs);

int main(){
    wakeup(4);
}


unsigned int wakeup(unsigned int secs){
    int left_secs = sleep(secs);

    printf("Woke up at %d secs.", secs - left_secs);
    return left_secs;
}

8.4.5 Loading and Running Programs

execve 함수는 현재 프로세스 context 상에서 프로그램을 로드하고 실행합니다.

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

execve 함수는 executable object file의 filename, argument list인 argv, 환경변수 list인 envp를 모두 함께 로드하고 실행합니다. execve 함수는 에러가 있는 경우에만 반환합니다. fork 함수와 다르게 (한번 호출, 두번 반환), 한번 호출되고 반환하지 않는 것이 특징입니다.

argv, envp 변수 모두 null로 끝나는 포인터 array를 가리키고 있습니다. argv array 각 element는 argument 문자를 가리키고 있는 상태입니다. 일반적으로 argv[0]은 excutable object file의 이름인 경우가 많습니다. envp 변수도 null로 끝나는 포인터 array를 가리키고, 각 요소는 name=value 형태의 string을 가리킵니다.

execve 함수가 로드를 하고 나면 start-up 코드를 호출합니다. start-up 코드는 stack을 할당하고 새로운 프로그램의 main routine에 제어권을 넘겨줍니다. main routine의 프로토타입은 다음과 같습니다.

int main(int argc, char **argv, char **envp);

or

int main(int argc, char *argv[], char *envp[]);

x86-64 system에 따른 stack discipline으로서 main function에 들어가는 argument중 3가지는 레지스터에 각각 저장됩니다.

  1. argc: argv[]의 non-null pointer 갯수
  2. argv: argv[]의 첫번째 entry를 가리키는 포인터
  3. envp: env[]의 첫번째 entry를 가리키는 포인터

Linux는 환경변수 배열(env[])를 다룰 수 있는 몇가지 함수를 제공합니다.

#include <stdlib.h>

char *getenv(const char *name);

getenv 함수는 name=value에 맞는 환경변수 배열을 찾습니다. 만약 찾는다면 해당 value를 반환합니다. 못찾은 경우, NULL를 반환합니다.

#include <stdlib.h>

int setenv(const char *name, const char *newvalue, int overwrite);

void unsetenv(const char *name);

만약 환경변수 배열이 name=oldvalue 형태의 문자를 포함하고 있다면 unsetenv 함수는 이를 삭제하며, setenv 함수는 oldvalue를 newvalue로 대체합니다(overwrite의 값이 0이 아닐 때만 해당). 만약 해당 이름이 존재하지 않으면 setenv 함수는 name=newvalue를 해당 배열에 추가합니다.

Practice Problem 8.6

자신의 command-line arguments와 환경변수들을 프린트하는 myecho 프로그램을 작성하라.

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

int main(int argc, char *argv[], char *envp[]){
    printf("Command Line Arguments:\n");
    for(int i = 0; i < argc; i++){
        printf("argv[%d]: %s\n", i, argv[i]);
    }
    printf("Environment Variables:\n");
    for(int i = 0; envp[i] != NULL; i++){
        printf("envp[%d]: %s\n", i, envp[i]);
    }
    return 0;
}

8.4.6 Using fork and execve to Run Programs

Unix Shell이나 Web server 같은 프로그램들은 forkexecve 함수를 활용합니다. shell 프로그램은 유저를 대신해 다른 프로그램들을 실행시켜주는 application-level 프로그램입니다. 쉘은 연속된 read/evaluate 단계를 밟고 난 후 종료됩니다. read 단계는 유저로 부터 command line을 읽는 단계입니다. evaluate 단계는 command line을 파싱하고 유저를 대신해서 프로그램을 실행하는 단계입니다.

#include "csapp.h"
#define MAXARGS 128

void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);

int main()
{
    char cmdline[MAXLINE];

    while(1){
        printf("> ");
        Fgets(cmdline, MAXLINE, stdin);
        if(feof(stdin))
            exit(0);
        eval(cmdline);
    }
}

void eval(char *cmdline)
{
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    pid_t pid;

    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;

    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) {
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: waitpid error");
        }
        else
            printf("%d %s", pid, cmdline);
    }
    return;
}

int builtin_command(char **argv) {
    if (!strcmp(argv[0], "quit"))
        exit(0);
    if (!strcmp(argv[0], "&"))
        return 1;
    return 0;
}

int parseline(char *buf, char **argv) {
    char *delim;
    int argc;
    int bg;

    buf[strlen(buf) - 1] = ' ';
    while (*buf && (*buf == ' '))
        buf++;

    argc = 0;
    while ((delim = strchr(buf, ' '))) {
        argv[argc++] = buf;
        *delim = '\0';
        buf = delim + 1;
        while (*buf && (*buf == ' '))
            buf++;
    }
    argv[argc] = NULL;

    if(argc == 0)
        return 1;

    if ((bg = (*argv[argc - 1] == '&')) != 0)
        argv[--argc] = NULL;

    return bg;
}
profile
백엔드 개발자 디디라고합니다.

0개의 댓글