[OS] 프로세스 API

장선규·2023년 6월 20일
0

[OS] OSTEP Study

목록 보기
2/28

Process API

사용자 프로그램의 프로그램 실행, 메모리 할당, 파일 접근과 같은 가상 머신과 관련된 기능들을 운영체제에게 요청할 수 있도록, 운영체제는 사용자에게 API를 제공한다.
보통 운영체제는 응용 프로그램이 사용 가능한 수백 개의 시스템 콜을 제공한다.
운영체제가 프로그램 실행, 메모리와 장치에 접근, 기타 이와 관련된 여러 작업을 진행하기 위해 이러한 시스템 콜을 제공하기 때문에, 우리는 운영체제가 표준 라이브러리(standard library)를 제공한다고 일컫기도 한다.

프로세스 API를 왜 사용하는가?

  • 사용자가 가상머신과 관련된 기능들을 운영체제한테 요청할 수 있도록 하려고
  • 프로그램 실행, 메모리 할당, 파일 접근과 같은 기능들을 요청 가능

1. fork() 시스템 콜

fork(): 자식 프로세스를 생성, 이때 생성된 프로세스는 호출한 프로세스의 복사본이고, 새 프로세스를 위한 메모리가 할당된다.

  • p1.c

    // p1.c
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
    
        printf("hello world (pid:%d)\n ", (int)getpid());
        int rc = fork(); // 자식 프로세스 생성
        if (rc < 0)
        { // fork  실패; 종료
            fprintf(stderr, " fork failed\n ");
            exit(1);
        }
        else if (rc == 0)
        { // 자식 (새 프로세스)
            printf(" hello, I am child(pid: % d)\n ", (int)getpid());
        }
        else
        { // 부모 프로세스는 이 경로를 따라 실행한다 (main)
            printf(" hello, I am parent of % d(pid: % d)\n ", rc, (int)getpid());
        }
        return 0;
    }
  • 실행결과

위의 프로그램 p1.c는 실행을 한 후 자식 프로세스를 fork()로 생성한다.
이때 생성된 프로세스틑 호출한 프로세스의 복사본이고, 해당 프로세스를 위한 메모리가 새로 할당된다.
특이한 점은, 자식 프로세스는 main() 함수의 첫 부분부터 시작하지 않는다는 것이다.
(hello world가 한번밖에 실행되지 않았음!)

그리고 한가지 더 알아야 할 점은, 이 프로그램은 부모 프로세스와 자식 프로세스의 실행 순서를 보장할 수 없다는 것이다. 즉 자식 프로세스가 먼저 실행되고 그 다음에 부모 프로세스가 실행될 수도 있다.
이는 CPU 스케줄러가 실행할 프로세스를 선택할 때 비결정성 문제가 발생하기 때문. 이는 병행성 부분에서 더 다룰 예정이다.

부모 프로세스 vs 자식 프로세스

  • 부모 프로세스 PC != 자식 프로세스 PC
  • 부모 프로세스는 fork() 반환값이 자식의 PID, 자식 프로세스는 fork() 반환값 0
  • 운영체제 입장에서는 프로그램 p1이 두개가 생기는 것

2. wait() 시스템 콜

wait(): 자식 프로세스가 끝날 때까지 부모 프로세스는 대기, 자식 프로세스가 끝나면 자식 프로세스의 pid를 리턴한다.

  • p2.c

    // p2.c
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
    
        printf(" hello world (pid:%d)\n ", (int)getpid());
        int rc = fork();
        if (rc < 0)
        { // fork 실패; 종료
            fprintf(stderr, " fork failed\n ");
            exit(1);
        }
        else if (rc == 0)
        { // 자식 (새 프로세스)
            printf(" hello,   I am child (pid:%d)\n ", (int)getpid());
        }
        else
        { // 부모 프로세스는 이 경로를 따라 실행한다 (main)
            int wc = wait(NULL); // 자식 프로세스가 종료될 때까지 대기
            printf(" hello,   I am parent of %d (wc:%d) (pid:%d)\n ", rc, wc, (int)getpid());
        }
        return 0;
    }

위의 프로그램 p2.c는 실행을 한 후 자식 프로세스를 fork()로 생성한다.
이때 생성된 프로세스틑 호출한 프로세스의 복사본이고, 해당 프로세스를 위한 메모리가 새로 할당된다.

이후 부모 프로세스는 wait()을 마주하게 되는데,

  • 부모 프로세스가 먼저 실행되는 경우 wait()에 의해 자식 프로세스가 종료될 때까지 기다린다.
  • 만일 자식 프로세스가 먼저 실행될 때는 그냥 그대로 실행이 된다.

즉, 이 프로그램 p2.c는 항상 자식 프로세스가 먼저 실행이 되는 것이다.

3. exec() 시스템 콜

exec(): 자기 자신이 아닌 다른 프로그램을 실행해야 할 때 사용. 다른 프로세스를 실행시킬 때, 현재 프로세스의 메모리 공간을 덮어쓴다.

  • 즉, 실제론 다른 프로세스가 실행되는 것이 아니라 현재 프로세스에 새 프로그램을 덮어씌우는 것이다.
    • 이 경우 a.out이라는 프로그램에서 exec()이 호출되면, 해당 메모리에 새롭게 부른 /bin/ls 프로세스가 덮어씌워지는 것이다.
    • 따라서 기존의 프로세스를 덮어버리고 싶지 않을때는 fork() 후에 exec() 을 하면 된다
  • 사실 정확하게는 exec() 이라는 함수는 없고, exec류의 함수들을 일컫는 말이다
    • execl,execlp,execle, execv,execvp,execve
  • p3.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <sys/wait.h>
    int main(int argc, char *argv[])
    {
    
        printf("hello world (pid:%d)\n ", (int)getpid());
        int rc = fork();
        if (rc < 0)
        { // fork 실패함; exit
            fprintf(stderr, "fork failed\n ");
            exit(1);
        }
        else if (rc == 0)
        { // 자식 (새 프로세스)
            printf("hello, I am child(pid: % d)\n ", (int)getpid());
            char *myargs[3];
            myargs[0] = strdup("wc");   // 프로그램:"wc"(단어 세기)
            myargs[1] = strdup("p3.c"); // 인자: 단어 셀 파일
            myargs[2] = NULL;           // 배열의 끝 표시
            execvp(myargs[0], myargs);  //"wc"실행
            printf("this shouldn't print out");
        }
        else
        { // 부모 프로세스는 이 경로를 따라 실행한다 (main)
            int wc = wait(NULL);
            printf("hello, I am parent of % d(wc: % d)(pid: % d)\n ", rc, wc, (int)getpid());
        }
        return 0;
    }

    위의 프로그램은 먼저 부모 프로세스가 main()을 따라 실행되고, 자식 프로세스를 fork()한다.
    이후 wait()을 만나 자식 프로세스가 완료될 때까지 기다린다.
    fork된 자식 프로세스는 일단은 부모 프로세스의 복제품이다. 그러나 여기서 차이점은, execvp()를 통해 부모 프로세스와는 전혀 상관없는 프로그램을 새로 실행시킨다는 것이다.
    execvp(myargs[0], myargs); 을 보면 wc(word count) 명령어에 myargs(=p3.c)의 인자를 넣고 실행시킨다.

  • 실행결과

    • 특이한 점은 printf("this shouldn't print out");가 실행되지 않는 것이다.
    • 이는 execvp가 성공적으로 수행되어 자식 프로세스(pid=43423)에 있는 메모리를 wc가 덮어써버렸기 때문이다.
    • 이후 부모 프로세스는 정상적으로 수행

4. 왜 이런 API를 사용하는지?

왜 사용? 그래야만 Unix 쉘을 구현할 수 있다

  • 쉘의 대부분의 명령어는 fork()를 호출하여 자식 프로세스를 만들고, exec()으로 이를 변형함
  • 부모 프로세스는 자식 프로세스를 생성하고 wait() 함
  • 이후 자식 프로세스가 끝나면 쉘은 wait()로부터 값을 리턴받고 다시 프롬프트를 출력하고 다음 명령어를 기다린다.

예시: wc p3.c > newfile.txt

  • 위 명령어는 p3.c 파일을 워드카운트 하고, 그 결과를 newfile.txt 에 담는 명령어다.
  • 부모 프로세스에서 자식 생성 (fork)
  • exec()이 호출되기 전에 표준출력을 닫고, newfile.txt를 연다
  • exec()으로 자식 프로세스에서 wc 실행, 이후 newfile.txt에 출력

5. 여타 API들

  • fork(): 자식 프로세스 생성 (부모 프로세스의 복제본으로)
  • exec(): 다른 프로세스 실행 (현재 프로세스 메모리 공간에 덮어쓰기)
  • wait(): 자식 프로세스가 끝날때까지 대기
  • kill(): 프로세스에게 시그널을 보냄(프로세스 중단, 삭제 등)
  • ps: 현재 어떤 프로세스 실행중인지 보여줌
  • top: 시스템에 존재하는 프로세스와 그 프로세스가 CPU 및 다른 자원들을 얼마나 사용하고 있는지 보여줌

등등

문제

문제 1

fork() 를 호출하는 프로그램 작성.
fork 호출 전에 x=100, 자식 프로세스에서는 그 변수 무엇? 만약 자식이 값 변경하면?

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

int main(int argc, char *argv[])
{
    int x = 100;
    printf("hello world (pid:%d)\n ", (int)getpid());
    int rc = fork(); // 자식 프로세스 생성
    if (rc < 0)
    { // fork  실패; 종료
        fprintf(stderr, "fork failed\n ");
        exit(1);
    }
    else if (rc == 0)
    { // 자식 (새 프로세스)
        printf(" hello, I am child(pid: % d)\n ", (int)getpid());
        printf(" child x = %d\n ", x);
        x = 200;
        printf(" child changed x = %d\n", x);
    }
    else
    { // 부모 프로세스는 이 경로를 따라 실행한다 (main)
        wait(NULL);
        printf("hello, I am parent of % d(pid: % d)\n ", rc, (int)getpid());
        printf("parent x = %d\n", x);
    }
    return 0;
}
  • 실행 결과
  • 자식 프로세스는 fork로 부모 프로세스의 복제본을 받기 때문에 x는 처음에 부모 프로세스와 동일하게 100이다.
  • 자식 프로세스는 fork로 새로운 메모리를 할당받기 때문에 x는 부모 프로세스의 x와 다른 x임.
    그래서 자식 프로세스의 x를 바꿔줘도 부모 프로세스에 있는 x는 그대로 100이다.

문제 2

open() 시스템 콜 이용하여 파일 여는 프로그램 작성, 새 프로세스를 위해 fork() 호출할 것.
자식과 부모가 open()에 의해 반환된 파일 디스크립터에 접근 가능?
부모와 자식이 동시에 파일 쓰기 가능?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    char *file_path = "example.txt";
    int file_descriptor;

    // 부모 프로세스에서 파일 열기
    file_descriptor = open(file_path, O_WRONLY | O_CREAT, 0644);

    // fork()를 호출하여 자식 프로세스 생성
    pid_t pid = fork();

    if (pid == 0)
    {
        // 자식 프로세스
        // 자식 프로세스에서 파일에 쓰기 작업 수행
        char *child_message = "Child\n";
        write(file_descriptor, child_message, 7);
        close(file_descriptor);
        exit(0);
    }
    else if (pid > 0)
    {
        // 부모 프로세스
        // 부모 프로세스에서 파일에 쓰기 작업 수행
        char *parent_message = "Parent\n";
        write(file_descriptor, parent_message, 8);
        // wait(NULL); // 여기에 넣어도 파일 잘 작성됨
        close(file_descriptor);
        wait(NULL); // 자식 프로세스의 종료를 기다림
    }
    else
    {
        // fork() 실패
        fprintf(stderr, "Fork failed\n");
        return 1;
    }

    return 0;
}
  • 실행결과
  • 자식과 부모가 open()에 의해 반환된 파일 디스크립터에 접근할 수 있다.
  • wait()의 위치를 바꿨을 때에도 잘 실행됨 -> 동시에 파일 쓰기 가능

문제 3

자식은 "hello" 부모는 "goodbye" 출력하는데, 항상 자식이 먼저 출력하게끔 하는 방법은? (wait() 없이)

  • 방법1. wait 대신 sleep 사용. 근데 좋은 방법같아 보이진 않음
  • 방법2. pipe 사용
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    int pipefd[2];
    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        return 1;
    }

    pid_t pid = fork();

    if (pid == 0)
    {
        // 자식 프로세스
        close(pipefd[0]);             // 자식은 읽기용 파이프 닫기
        write(pipefd[1], "hello", 6); // 쓰기용 파이프에 hello 쓰기
        close(pipefd[1]);             // 자식 쓰기용 파이프 닫기
        exit(0);
    }
    else if (pid > 0)
    {
        // 부모 프로세스
        close(pipefd[1]); // 부모는 쓰기용 파이프 닫기
        char buffer[6];
        read(pipefd[0], buffer, 6); // 읽기용 파이프에서 읽어오고, 그 값을 buffer에 넣어줌. 읽을 것 없으면 대기.
        printf("%s\n", buffer);     // 읽어온 버퍼 출력
        close(pipefd[0]);           // 부모의 읽기용 파이프 닫기
        printf("goodbye\n");
    }
    else
    {
        // fork() 실패
        fprintf(stderr, "Fork failed\n");
        return 1;
    }

    return 0;
}

문제 4

exec() 계열의 함수 호출. 여러 변형이 있는 이유는?

// execl(): l이 붙으면 리스트 형태로 연달아서 명령어 및 옵션 넣음, 
// 프로그램이 들어있는 디렉토리 명까지 넣어줘야함
execl("/bin/ls", "ls", "-l", NULL);

// execle(): e가 붙으면 환경변수 지정 가능
char *envp[] = {"PATH=/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, envp);

// execlp(): p가 붙으면 현재 경로에서 명령 실행, 디렉토리명 필요 없음
execlp("ls", "ls", "-l", NULL);

// execv(): v 붙으면 인수를 배열로 넣음, 
// 프로그램이 들어있는 디렉토리 명까지 넣어줘야함
char *argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);

// execvp(): p가 붙으면 현재 경로에서 명령 실행, 디렉토리명 필요 없음
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);

// execve(): e가 붙으면 환경변수 지정 가능
char *argv[] = {"ls", "-l", NULL};
char *envp[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, envp);
  • 다양한 실행 방식과 편의성을 제공하기 위해 (API)

문제 5

wait()가 반환하는 것은 무엇?

  • 자식 프로세스의 pid

자식 프로세스가 wait을 호출하면?

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

int main()
{
    pid_t pid = fork();

    if (pid == 0)
    {
        int c = wait(NULL);
        printf("%d\n", c);
        // 자식 프로세스
        printf("child\n");
    }
    else if (pid > 0)
    {
    	int p = wait(NULL);
        printf("%d\n", p);
        // 부모 프로세스
        printf("parent\n");
    }
    else
    {
        // fork() 실패
        fprintf(stderr, "Fork failed\n");
        return 1;
    }

    return 0;
}
  • 자식 프로세스는 자식이 없기 때문에 -1 반환함

문제 6

wait() 대신 waitpid() 사용하면 좋은점?

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

int main()
{
    pid_t pid = fork();

    if (pid == 0)
    {
        // 자식 프로세스
        printf("child\n");
    }
    else if (pid > 0)
    {
        int p = waitpid(pid, NULL, 0);
        printf("%d\n", p);
        // 부모 프로세스
        printf("parent\n");
    }
    else
    {
        // fork() 실패
        fprintf(stderr, "Fork failed\n");
        return 1;
    }

    return 0;
}
  • waitpid(child_pid, status, options) 는 wait() 보다 더 다양한 기능이 있음. 좀 더 정교하게 자식 프로세스의 변화에 따른 제어가 가능
  • options 에 WNOHANG 넣으면 자식 프로세스 종료를 기다리지 않음, WUNTRACED, WCONTINUED 같은 옵션도 있음

문제 7

자식 프로세스에서 표준 출력을 닫으면 어떻게 되는가?

  • 자식 프로세스에서 표준 출력을 닫으면, 자식 프로세스에서는 printf 해도 아무것도 안 나옴

  • 근데 자식 프로세스에서 표준 출력을 닫았다고 해서 부모 프로세스에 영향을 주진 않음. 부모 프로세스에서는 출력 잘 됨.

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main()
    {
        pid_t pid = fork();
    
        if (pid == 0)
        {
            // Child process
            close(STDOUT_FILENO); // Close STDOUT_FILENO
    
            printf("This will not be printed\n"); // Attempt to print, but no output will occur
    
            return 0;
        }
        else if (pid > 0)
        {
            // Parent process
            // Wait for the child process to finish
            wait(NULL);
            printf("Child process completed\n");
        }
        else
        {
            // Fork failed
            printf("Failed to create child process\n");
        }
    
        return 0;
    }
  • 실행 결과

문제 8

두 개의 자식 프로세스 만들고, pipe() 시스템 콜을 사용하여 한 자식의 표준 출력을 다른 자식의 입력으로 연결하는 프로그램 작성

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

int main()
{
    int pipefd[2];
    pid_t child1, child2;

    // Create pipe
    if (pipe(pipefd) == -1)
    {
        perror("pipe");
        return 1;
    }

    // Create first child process
    // 쓰기용 자식 프로세스
    child1 = fork();
    if (child1 == 0)
    {
        // Child 1 process
        close(pipefd[0]); // 읽기용 파이프 닫기

        // Redirect stdout to the write end of the pipe
        dup2(pipefd[1], STDOUT_FILENO); // 이 자식의 표준출력을 쓰기용 파이프로 연결
        close(pipefd[1]); // 이후 쓰기용 파이프 닫기

        // Execute the desired command
        execlp("ls", "ls", "-l", NULL); // ls -l 실행 -> 표준 출력으로 안 가고 출력파이프로 감
        perror("execlp");
        return 1;
    }
    else if (child1 == -1)
    {
        perror("fork");
        return 1;
    }

    // Create second child process
    // 읽기용 자식 프로세스
    child2 = fork();
    if (child2 == 0)
    {
        // Child 2 process
        close(pipefd[1]); // 쓰기용 파이프 닫기

        // Redirect stdin to the read end of the pipe
        dup2(pipefd[0], STDIN_FILENO); // 이 자식의 표준입력을 읽기용 파이프로 연결
        close(pipefd[0]); // 이후 읽기용 파이프 닫기

        // Execute the desired command (e.g., read from stdin)
        execlp("grep", "grep", "example", NULL); // 입력파이프에서 값을 읽어옴 -> 이 입력을 가지고 grep 실행
        perror("execlp");
        return 1;
    }
    else if (child2 == -1)
    {
        perror("fork");
        return 1;
    }

    // Close both ends of the pipe in the parent process
    close(pipefd[0]);
    close(pipefd[1]);

    // Wait for both child processes to finish
    waitpid(child1, NULL, 0);
    waitpid(child2, NULL, 0);

    return 0;
}
  • 명령어 실행 결과
  • 해당 프로그램 실행 결과
profile
코딩연습

0개의 댓글