공부/Operating system

OS : Concurrency : 병행성 - Intro(1) + Thread

상연 2022. 4. 28. 02:06

목차

    학부 수업 내용을 필기한 내용입니다.
    필자가 이해를 제대로 못하고 정리한 경우 틀린 내용이 있을 수 있습니다.
    그러한 부분이 있다면 댓글로 이야기하여 수정하게 해 주시면 감사하겠습니다.

    실행 흐름이 여러 개 있는데 CPU는 하나 있을 때

    왼쪽 Process(t0.c)

    실행 흐름이 여러 개 있는 것을 볼 수 있다

    메인 스레드가 있고 생성된 T1, T2 쓰레드가 있는데 이렇게 생성된 이유는 Program Code에서 라이브러리 함수를 사용하여 스레드를 생성하였기 때문이다.

    결과적으로는 이렇게 하여 여러 개의 실행 흐름을 갖게 되었다.

    오른쪽

    실행 흐름이 하나 있는 프로세스

    둘 다

    • Address Space 존재
    • Open File Descriptor 존재
    • PCB 존재

    Program Code에서 main() 실행함으로써 프로그램이 시작되고 프로세스가 실행됨

    프로그램이 실행된다는 것은, 스택에 흔적을 남긴다 (Execution Stack)

    어떤 함수를 호출하는 흐름으로 우리가 표현을 한다 했을 때

    지역변수, Parameter, Call 명령어 등등 그 흔적이 Stack에 남아있기 때문이다.

    그래서 프로그램이 실행된다는 것은 Execution Stack과 밀접한 관계가 있다.

    실행 흐름 하면 Stack을 떠올리자

    우리가 스레드를 만든다는 것은 별도의 실행 흐름을 만든다는 것.

    그것은 별도의 Stack 이 필요하다는 것과 동일하다.

    그래서 실행 흐름이 여러 개 있냐 없냐의 차이는 이 스택이 여러개 있냐 없냐로 볼 수 있다.

    Conccurency 문제, 모든 실행 흐름은 병행성을 갖는다. 왜냐하면 CPU는 하나인데 여러 개를 번갈아가면서 진행하게 되기 때문에 Context Switch가 발생하기 때문이다.

    어떤 실행 흐름을 실행하다가, 잠시 멈추고 - 다른 것을 진행하고-

    이러한 흐름에 대해서는 이전에는 Process 간의 과정만 봤었지만, 스레드에서도 동일하게 발생한다.

    프로세스 간의 전환에 대해 학습할 때는, 실행 흐름을 기억한 후 다시 그 실행흐름을 찾아가는 것이 중요하다고 하였다. 이것 또한 스레드에서도 마찬가지이다.

    프로세스 간에서는 PCB를 사용해서 이러한 일을 하였는데 스레드에서는 어떻게 해 줄까?


    쓰레드 별로 Context를 저장하는 공간이 있어야 한다.

    그 구조는 CPU의 레지스터와 동일하게 될 것이다.

    t0.c Loading

    #include <stdio.h>
    #include <assert.h>
    #include <pthread.h>
    #include "common.h"
    #include "common_threads.h"
    
    void *mythread(void *arg) {
        printf("%s\n", (char *) arg);
        return NULL;
     }
    
    int main(int argc, char *argv[])
    { 
        pthread_t p1, p2;
        int rc;
        printf("main: begin\n");
        Pthread_create(&p1, NULL, mythread, "A");
        Pthread_create(&p2, NULL, mythread, "B");
        // join waits for the threads to finish
        Pthread_join(p1, NULL);
        Pthread_join(p2, NULL);
        printf("main: end\n");
        return 0;
    }

    Main함수를 호출하는데 보면 새로운 변수 타입이 있다 pthread_t

    여기에는 int형 비슷한 값이 들어가게 될 것이다.

    이전에 Process는 PID로 프로세스 간 식별자가 있다고 하였는데

    스레드도 마찬가지로 그러한 식별자가 저장된다.

    Process의 경우에는 fork()를 할 때 Return 값이 PID 였다.

    Pthread_create(&p1, NULL, mythread, "A");

    쓰레드 생성하는 함수의 Parameter에 대해 알아보자

    • 첫 번째
      • p1에 생성되는 p1 스레드의 식별자 값을 받아오라는 뜻
    • 두 번째
      • Null 값을 주면, System이 알아서 Stack 크기 할당하라
    • 세 번째
      • 함수 이름, 함수 이름은 주소 값을 갖는다. 코드가 시작되는 지점
      • 스레드가 실행되었을대 처음 시작할 지점.
    • 네 번째
      • 그 세 번째 함수가 시작될 때 들어갈 Parameter
      • mythread 함수를 보면 인자로 Argument를 받기에 "A" 받으면
      • 그 "A"를 호출하게 될 것
    Pthread_join(p1, NULL);

    join이라는 것이 있는데, 우리는 이전에 wait()라는 것을 본 적이 있다.

    fork()를 했을 때 자식 프로세스가 끝나는 것을 기다리고 실행되는 것

    마찬가지로 join 또한 스레드가 있을 때 끝나는 것을 기다리는 것이다.

    여기서는 Main함수가 p1, p2 스레드를 만들고 join을 실행하였으니

    Main함수가 두 스레드가 끝나는 것을 기다린다는 것이다.

    #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)
        {
            fprintf(stderr, "fork failed\n");
            exit(1);
        }
        else if (rc == 0) 
        { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
        char *myargs[3];
        myargs[0] = strdup("wc"); // program: "wc" (word count)
        myargs[1] = strdup("p3.c"); // argument: file to count
        myargs[2] = NULL; // marks end of array
        execvp(myargs[0], myargs); // runs word count
        printf("this shouldn’t print out");
        }
        else
        { // parent goes down this path (main)
            int rc_wait = wait(NULL);
            printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n", rc, rc_wait, (int) getpid());
        }
        return 0;
    }

    위의 코드는 fork()를 사용하는 함수이다.

    fork()와 Pthread_Create()의 차이가 무엇인가?

    fork()가 되면 새로운 프로세스가 생성된다. 이 프로세스는 Memory Copy가 되어 똑같다.

    다만 fork()된 프로세스는 같은 일을 하다가 기존 Process와 다른 일을 하고 싶으면 지정을 해 줘야 한다.

    그 지정해 주는 방법에는 execvp()를 사용했는데

    execvp(myargs[0], myargs);

    코드를 다시 로딩해서 환경을 갈아엎는 것이다.

    프로세스는 실행환경을 만들고 새로 할 일을 지정하는 (fork() -> execvp()) ,

    할 일을 지정하는 것 또한 실행파일을 지정해주었다.

    myargs[0] = strdup("wc");

    ㄴ 경로지정

    그러나 스레드를 생성하는 경우에는 만들면서 어떤 일을 할지 지정해준다.

    다만, 프로세스 fork()와는 다르게 PATH값을 줘서 실행파일을 외부에서 실행하는 것이 아니라.

    프로세스 내부에서 함수의 시작 주소를 주어서 번갈아가며 일하는 것

    즉 프로세스와 스레드의 차이는 외부이냐 내부이냐

     

    TCB

    Thread Context Block

    PCB와 비슷한데 우선 무조건 있어야 하는 것들의 경우에는

    • State
      • PCB에서는 Running / Ready / Blocked 가 있었다.
    • Context
      • Register값 실행 흐름이 중단되었다가 다시 시작할 때 갖고 와야 함.
    • TID
      • 스레드 식별번호

    개념적으로는 이와 같이 PCB에 TCB들의 시작점이 내장되어있는 식으로 구현할 것이라고 배울 것이다.

    하지만, 이것은 구현하는 방법에 따라 얼마든지 차이가 날 수 있는 부분이므로 어디까지나 그럴 것이다라고 미뤄두고 생각한다고만 보면 된다.

     

    이번 글에서는 Concurrency를 설명하기 이전에, 여러 개의 실행흐름이 발생하는것에 대해 간략하게 설명하였다.

    왜냐하면 Concurrency가 발생하는 이유가 이러한 여러개의 실행 흐름으로부터 기인하기 때문이다.

     

    '공부 > Operating system' 카테고리의 다른 글

    OS : Lock - Controlling Interrupts  (0) 2022.05.03
    OS : Locks - The Basic Idea  (0) 2022.05.03
    OS : Thread - API  (0) 2022.04.28
    OS : Concurrency - Race Condition  (0) 2022.04.28
    OS : Concurrency : 병행성 - Intro(2)  (0) 2022.04.28