POSIX Threads - 시스템프로그래밍

POSIX Threads - 시스템프로그래밍

Tag
Computer Science Engineering
System Programming

Motivation

  • Monitoring file descriptors
    • 프로세스를 여러개 만들자
      • 자식 프로세스들 간에는 어떠한 변수를 공유하지 못한다.
    • select(), poll()
      • 파라미터로 모니터링 하고 싶은 file descriptors 를 넣어서 가능하다.
      • 함수들 자체가 blocking call을 사용한다.
    • Nonblocking I/O with polling
      • 얼마나 자주 file descriptor를 모니터링 해야하는지에 대한 timing에 대해 고려를 해야한다
    • POSIX asynchronous I/O
      • 핸들러는 오직 async-signal-safe 함수를 써야한다.
    • thread를 여러개 만들자.
      • 다른 접근들 보다 상대적으로 간단하다.
 

왜 다중 쓰레드를 사용하는가?

  • 여러 독립적인 task를 concurrent하게 실행하여, 전체 task 퍼포먼스를 올리기 위해서
  • 쓰레드간에 동기화 하는 문제가 있다.
    • asynchronous events를 효율적으로 처리할 수 있다.
    • shared-memory 상에서 parallel performance를 얻을 수 있다.
  • OS를 개발하는데에 있어서도 다중 스레드 방식으로 커널이 작동하도록 만든다
 

thread는 무엇인가?

  • 각각의 쓰레드는 stack, CPU state, register 정보를 별도로 가지는 실행 단위이다 ( 실행 흐름 ).
    • 쓰레드들은 서로 공유하는 메모리 공간이 있다. ( 코드, 전역 변수, heap )
  • 두개의 프로세스가 통신을 하기 위해선 OS를 통해서 ( IPC ) 통신을 해야한다.
  • 두개의 스레드는 공유하는 메모리 공간을 통해서 통신할 수 있다.
  • 다중 쓰레드를 사용하면 동시에 실행되는 것 처럼 보인다. ( concurrent하게 )
 

Multitasking

  • 다중프로세스 / 다중쓰레드를 통해서 할 수 있다.
  • single process에서 진행되는 것은 time-division multiplexing 으로 진행된다.
    • 시간을 쪼개서 switch 하며 진행하는 것이다.
  • multiprocessor / multi-core system인 경우 실제로 동시에 실행이 된다.
 

Processes vs threads

  • 프로세스는
    • 독립적이다.
      • 프로세스가 생성이 될때, address space는 OS에 의해 다른 프로세스와 겹치지 않게 할당되기 때문이다.
    • 관리해야 하는 state information이 많다.
      • 쓰레드는 프로세스의 subset 개념으로 생각되기도 한다.
      • context switch의 overhead 또한 마찬가지이다.
        • 프로세스간의 context switch가 일어나면, 상태 정보를 저장 / load 하는 것 또한 스레드보다 overhead가 더 크다.
    • 프로세스들은 분리된 address space를 가지고 있다.
    • 프로세스간에 소통을 하려면 inter-process communication mechanisms를 사용해야 한다.
    • 커널이 scheduling할 수 있는 가장 heaviest unit 이다.
    • OS에게 할당받은 자신의 자원을 갖고 있다.
      • memory, file handlers, sockets, device handles, windows
    • address spaces 및 file resources를 공유하지 않는다.
      • 예외) file handles를 상속 받거나, 공유 메모리 segments 사용, 메모리 map file을 사용
    • preemptively multitask 된다.
      • 프로세스는 실행되다가 중간에 쫓겨나고, 우선순위가 더 높은 프로세스가 ready queue에 있다가 context switch 될 수 있다.
      • 언제든 실행되다가 쫓겨날 수 있다.
  • 쓰레드는
    • 프로세스안의 쓰레드들 끼리는 해당 프로세스의 state information을 공유한다.
      • code, global, static, heap
    • 쓰레드 간에 context switch가 일어나며 concurrent하게 작동 가능하지만, overhead가 적어서 context switch가 더 빠르다.
    • 커널이 scheduling할 수 있는 가장 lightest unit 이다.
    • 프로세스는 적어도 하나의 쓰레드가 같이 존재한다.
    • 하나의 프로세스 안에서 여러 쓰레드가 존재할 수 있고, 다중 쓰레드는 프로세스에게 할당받은 memory, file resources를 공유한다.
    • 만약 OS 프로세스 scheduler가 preemptive하다면, 쓰레드는 preemptively multitask될 수 있다.
    • 쓰레드는 자신의 resource를 소유하지 않는다.
      • 예외) stack, registers 값, program counter, thread-local storage
    • kernel thread와 user thread로 나뉠 수 있다. ( 최근에는 UNIX에서 따로 나누지 않는다. )
      • 커널에서 관리할 수 있고, 스케쥴링 할 수 있는 쓰레드 ⇒ kernal threads
      • 커널이 쓰레드의 존재를 모르고, user level에서의 라이브러리에서 관리하는 쓰레드 ⇒ user threads
 

User space

user space: application이 사용하는 공간 ↔ kernel space: 커널이 사용하는 공간
 
  • kernel space
    • 커널, kernel extensions, some device drivers이 실행되는데에 사용하는 공간
    • disk로 swap out되지 않는다.
      • 하드디스크에 프로그램이 존재하는데, 이 정보를 메모리에 로드한다.
      • 커널이 이 메모리에 적재된 정보를 로드하여 실행하는 것이다.
      • 메모리는 한정된 공간이기 때문에, 하드디스크에서 다 로드하는 것이 아니라, 필요한 것만 로드함.
        • 디스크와 하드디스크 간에 와리가리 하는 것을 swapping 이라 한다.
        • HD → memory : swap in
        • memory → HD : swap out
  • user space
    • swap in, swap out이 된다.
 

Why Pthreads?

⇒ POSIX threads의 줄임말
 
프로세스 혹은 쓰레드를 50,000개를 만드는데 드는 시간
notion image
 

Thread management

  • pthread_cancel
    • 쓰레드의 동작을 중지시키도록 요청
  • pthread_create
    • 쓰레드를 생성
  • pthread_detach
    • 쓰레드가 resource를 release하도록 한다. ( 뒤에서 자세한 설명.. )
  • pthread_equal
    • 두 쓰레드 id가 같은지를 확인한다.
  • pthread_exit
    • 쓰레드 하나를 종료 시키겠다.
  • pthread_kill
    • 쓰레드에게 시그널을 보냄
  • pthread_join
    • 쓰레드 wait
  • pthread_self
    • 쓰레드 id가 무엇인지 확인한다. ↔ getpid
  • 대부분의 함수는 성공했을 시 0 / 실패했을 시 nonzero를 반환한다.
    • errno를 세팅하지 않는다.
  • EINTR를 리턴하지는 않는다. ⇒ interrupt가 되었을 때 재시작할 필요가 없다.
 

Referencing threads by ID

#include <pthread.h> pthread_t pthread_self(void); pthread_t pthread_equal(thread_t t1, thread_t t2);
pthread를 link 해주어야 한다. ⇒ -lpthread
  • pthread_t
    • OS마다 타입이 달라질 수 있지만, 구조체 타입의 포인터 ( LINUX 일 땐 숫자니까 일단 숫자로 생각하자 )
  • pthread_self
    • 본인의 ID
  • pthread_equal
    • 같다면 0 / 다르다면 nonezero
 

Creating a thread

#include <pthread.h> int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg);
  • 자동으로 runnable한 쓰레드를 만들어서 시작까지 시켜준다.
  • parameters
    • thread: output parameter / 생성된 쓰레드의 id
    • attr: 프로세스와 마찬가지로 쓰레드도 속성 정보들이 존재한다.
      • NULL을 대입하는 경우 default attributes
    • start_routine: 새로 만들어진 쓰레드가 기존 쓰레드와 독립적으로 concurrent하게 실행할 task를 함수로 지정
    • arg: start_routine은 파라미터가 하나 있다. 쓰레드가 start_routine이 실행될 때 들어갈 파라미터
  • return values
    • 쓰레드가 생성된 이후, 동작을 완료하고 나서 반환하는 것이 아니라 OS가 요청을 성공적으로 받아 들였을 시점. 즉 함수 호출을 받아들였을 때 ( 실질적으로 쓰레드가 언제 만들어져서 start_routine을 언제 실행할 지는 모른다. )
    • 성공했을 시 0 / 실패했을 시 nonzero
 

Detaching

#include <pthread.h> int pthread_detach(pthread_t thread);
→ detach thread 로 만들겠다. ↔ joinable thread ( wait )
 
  • 쓰레드를 기다릴 수 없게 하기 때문에, 쓰레드가 종료되면 리소스가 바로 OS로 release 한다.
    • 즉, joinable한 thread는 자신의 resource를 바로 release하지는 않는다. ⇒ 다른 쓰레드가 기다릴 수도 있기 때문이다. ( 내 정보를 줘야할 수도 있어서 )
  • 이 함수는 쓰레드의 internal option을 쓰레드를 위한 storage를 포함한 resource 들을 바로 reclaim할 수 있도록 detach 하게 만들어주는 함수이다.
  • 종료 상태를 report하지 않는다. ( 바로 resource를 반환하기 때문에 )
  • 성공했을 시 0 / 실패했을 시 nonzero
 

Joining

#include <pthread.h> int pthread_join(pthread_t thread, void **value_ptr);
  • thread가 종료되면 return 값을 value_ptr를 통해 받아올 수 있다.
    • thread로 인해 포인터의 위치가 바뀔 수 있기 때문에 이중포인터
  • target thread가 종료될 때까지 calling thread를 suspend 시킨다.
  • 또다른 쓰레드가 pthread_join을 호출하여 기다리는 동안, 혹은 해당 쓰레드가 속한 프로세스가 exit 되기 이전까지는 nondetached thread의 resource를 release 하지 않는다.
  • parameters
    • thread: target thread
    • value_ptr: 리턴값을 받을 포인터의 위치 / 없다면 NULL
  • return values
    • 성공했을 시 0 / 실패했을 시 nonzero
  • Example
    • pthread_join(pthread_self())
      • 본인이 종료할 때까지 기다리겠다. 즉 deadlock
 

Example of creation/joining

void monitorfd(int fd[], int numfds) { /* create threads to monitor fds */ int error, i; pthread_t *tid; if ((tid = (pthread_t *)calloc(numfds, sizeof(pthread_t))) == NULL) { perror("Failed to allocate space for thread IDs"); return; } for (i = 0; i < numfds; i++) /* create a thread for each file descriptor */ if (error = pthread_create(tid + i, NULL, processfd, (fd + i))) { fprintf(stderr, "Failed to create thread %d: %s\n", i, strerror(error)); tid[i] = pthread_self(); } for (i = 0; i < numfds; i++) { if (pthread_equal(pthread_self(), tid[i])) continue; if (error = pthread_join(tid[i], NULL)) fprintf(stderr, "Failed to join thread %d: %s\n", i, strerror(error)); } free(tid); return; }
 

Exiting

#include <pthread.h> void pthread_exit(void* value_ptr);
  • 나를 기다리고 있는 쓰레드가 있다면 그 쓰레드에게 반환하는 값 value_ptr
  • 쓰레드를 종료
 

Cancellation

#include <pthread.h> int pthread_cancel(pthread_t thread); int pthread_setcancelstate(int state, int *oldstate);
  • pthread_cancel
    • 파라미터로 입력받은 thread를 종료시킨다.
    • 성공했을 시 0 / 실패했을 시 nonzero
    • target thread의 state, type에 따라 결과가 다르다.
  • pthread_setcancelstate
    • cancel을 받을 것이냐, 말거냐
    • state
      • PTHREAD_CANCEL_ENABLE: 받겠다
      • PTHREAD_CANCEL_DISABLE: pending 시키겠다
    • oldstate: output parameter / 변경되기 이전 값
    • 성공했을 시 0 / 실패했을 시 nonzero
 

Cancellation type

일반적인 내용 아니다..
#include <pthread.h> int pthread_setcanceltype(int type, int *oldtype); void pthread_testcancel(void);
  • cancellation type
    • PTHREAD_CANCEL_ASYNCHRONOUS: 취소 요청을 받은 순간 바로 종료
    • PTHREAD_CANCEL_DEFERRED: 취소 요청을 지연시킨다음, 내가 하고있던 일만 하고 취소하겠다.
      • 취소할 시점을 직접 지정해야한다.
      • ⇒ pthread_testcancel(void);
      • 테스트 요청이 들어왔다면 이 시점에 취소
      •  

Passing parameters and returning values

파일을 카피하는 예시.
쓰레드 하나가 쓰레드를 생성하여 생성된 쓰레드를 이용하여 카피 동작을 진행시킨다.
카피 완료되면 카피 함수의 리턴값과 같이 카피된 bytes를 기다리는 쓰레드에게 return
 
기존 쓰레드는 생성된 쓰레드에게 1. 카피될 함수의 소스파일 descriptor / 2. target file descriptor 를 넘긴다.
 

callcopymalloc.c

#include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #define PERMS (S_IRUSR | S_IWUSR) #define READ_FLAGS O_RDONLY #define WRITE_FLAGS (O_WRONLY | O_CREAT | O_TRUNC) // 별도 쓰레드가 실행할 함수 void *copyfilemalloc(void *arg); int main(int argc, char *argv[]){ int *bytesptr; int error; int fds[2]; pthread_t tid; if(argc != 3){ fprintf(stderr, "Usage: %s fromfile tofile\n", argv[0]); return 1; } // 복사될 파일: fd[0] / 내용을 붙여넣을 파일: fd[1] if(((fd[0] = open(argv[1], READ_FLAGS)) == -1) || ((fd[1] = open(argv[2], WRITE_FLAGS, PERMS)) == -1)){ perror("Failed to open the files"); return 1; } // tid를 ID로 가진 쓰레드를 만들며, fds 파라미터를 넘긴 copyfilemalloc 함수를 실행시킬 것이다. if(error = pthread_create(%tid, NULL, copyfilemalloc, fds)){ fprintf(stderr, "Failed to create thread: %s\n", strerror(error)); return 1; } // 기존 쓰레드는 여기에서 리턴값을 받을 때까지 대기하며, 리턴값을 받으면 bytesptr에 저장 if(error = pthread_join(tid, (void **)&bytesptr)){ fprintf(stderr, "Failed to join thread: %s\n", strerror(error)); return 1; } printf("Number of bytes copied: %d\n", *bytesptr); // 종료되지 않고 계속 돌아가는 프로그램이라면 free(bytesptr) 해야함 // 동적 할당하는 것이 귀찮으니, static하게 잡으면 어떠한가? => 두개 이상의 쓰레드로 복사하게 되면 불가능 ( 덮여쓰여지니까 ) return 0; }
 

copyfilemalloc.c

#include <stdlib.h> #include <unistd.h> #include "restart.h" void *copyfilemalloc(void *arg){ // 왜 포인터를 써야함? 그냥 int를 쓰면 되지 않나? => 로컬 변수 내용은 쓰레드가 종료되면서 없어지기 때문 int *bytesp; int infd; int outfd; infd = *((int *)(arg)); outfd = *((int *)(arg) + 1); if((bytesp = (int *)malloc(sizeof(int))) == NULL) return NULL; // copyfile은 기존에 만들어져있던 함수 *bytesp = copyfile(infd, outfd); r_close(infd); r_close(outfd); return bytesp; }

동적할당 하지도 않고, static 변수도 사용하지 않는 방법이 있을까?
  • fd[0], fd[1] 뿐만 아니라 세번째 인자에 복사한 bytes수를 담을 수 있도록 하자
    • 파라미터에 들어가는 배열의 인자를 3개로 만들자.
    • 쓰레드는 세번째 array의 주소를 그대로 리턴
 

callbypass.c

#include <errno.h> #include <fcntl.h> #include <pthread.h> #include <stdio.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #define PERMS (S_IRUSR | S_IWUSR) #define READ_FLAGS O_RDONLY #define WRITE_FLAGS (O_WRONLY | O_CREAT | O_TRUNC) // 별도 쓰레드가 실행할 함수 void *copyfilepass(void *arg); int main(int argc, char *argv[]){ int *bytesptr; int error; int targs[3]; pthread_t tid; if(argc != 3){ fprintf(stderr, "Usage: %s fromfile tofile\n", argv[0]); return 1; } // 복사될 파일: targs[0] / 내용을 붙여넣을 파일: targs[1] if(((targs[0] = open(argv[1], READ_FLAGS)) == -1) || ((targs[1] = open(argv[2], WRITE_FLAGS, PERMS)) == -1)){ perror("Failed to open the files"); return 1; } // tid를 ID로 가진 쓰레드를 만들며, fds 파라미터를 넘긴 copyfilepass 함수를 실행시킬 것이다. if(error = pthread_create(%tid, NULL, copyfilemalloc, fds)){ fprintf(stderr, "Failed to create thread: %s\n", strerror(error)); return 1; } // 기존 쓰레드는 여기에서 리턴값을 받을 때까지 대기하며, 리턴값을 받으면 bytesptr에 저장 // targs[2] 와 값은 같다 if(error = pthread_join(tid, (void **)&bytesptr)){ fprintf(stderr, "Failed to join thread: %s\n", strerror(error)); return 1; } printf("Number of bytes copied: %d\n", *bytesptr); return 0; }
 

copyfilepass.c

#include <unistd.h> #include "restart.h" void *copyfilepass(void *arg){ int *argint; argint = (int *)arg; argint[2] = copyfile(argint[0], argint[1]); r_close(argint[0]); r_close(argint[1]); return argint + 2; }
 

Wrong parameter passing

별도의 쓰레드가 실행하는 함수
#include <pthread.h> #include <stdio.h> #include <string.h> #define NUMTHREADS 10 static void *printarg(void *arg) { fprintf(stderr, "Thread received %d\n", *(int *)arg); return NULL; }
메인 함수
int main (void) { /* program incorrectly passes parameters to threads */ int error; int i; int j; // 10개의 쓰레드를 만들겠다. pthread_t tid[NUMTHREADS]; for (i = 0; i < NUMTHREADS; i++){ if (error = pthread_create(tid + i, NULL, printarg, (void *)&i)) { fprintf(stderr, "Failed to create thread: %s\n", strerror(error)); tid[i] = pthread_self(); } } for (j = 0; j < NUMTHREADS; j++) { if (pthread_equal(pthread_self(), tid[j])) continue; if (error = pthread_join(tid[j], NULL)) fprintf(stderr, "Failed to join thread: %s\n", strerror(error)); } printf("All threads done\n"); return 0; }
 
⇒ 기대 결과는 0~9까지 순서에 상관없이 화면에 출력되는 것이다. + All threads done 출력
notion image
 
문제는 무엇이냐?
⇒ 꼭 pthread_create를 호출했다고 바로 쓰레드를 생성하는 것이 아니다. ( 위에서 언급함 )
⇒ 파라미터로 넘어가는 값이 변경되고 있어서 생기는 오류이다.
 
위의 예시에서 왜 10이 나왔냐? 9가 끝 아닌가?
⇒ for문을 돌고나서 i는 10이 되어있기 때문
 
해결 방법?
  1. for문을 돌때마다 sleep 함수를 호출하면 되지 않을까?
    1. 꼭 sleep이 끝났다고 쓰레드가 생성되었다고 할 순 없다. 뭐 가능은 하겠지
  1. 변수를 10개 만들어라..
 

Thread safety

덧붙이자면 async-safety 한 것보다 thread-safety를 만들기가 쉽다.
⇒ async-safety는 단일 쓰레드에서도 충돌 문제가 일어날 수 있기 때문