-
Threads대학/시스템소프트웨어 2022. 12. 11. 15:51
프로그램의 실행 흐름으로 c에서는 보통 posix threads library를 이용하여 구현한다.
스레드의 주된 활용법은 I/O, 네트워크 통신 등 처리시간이 오래 걸리거나, 언제 종료될지 모르는 동작을
수행하면서 동시에 다른 중요한 동작을 수행해야 할 때, 대기시간이 긴 동작을 자식 스레드에게
위임하는 방식으로 주로 활용한다.
- pthread_create
void *processfd(void *arg) { char buf[BUFSIZE]; int fd; ssize_t nbytes; fd = *((int *)(arg)); for ( ; ; ) { if ((nbytes = read(fd, buf, BUFSIZE)) < 0) break; // process buf data } return NULL; } int main(void) { int error; int fd; pthread_t tid; fd = open("a.txt", O_CREAT); if (error = pthread_create(&tid, NULL, processfd, &fd)) { fprintf(stderr, "Failed to create thread: %s\n", strerror(error)); } if (error = pthread_join(tid)) { fprintf(stderr, "Failed to join thread: %s\n", strerror(error)); } }
위 프로그램은 a.txt라는 파일을 열고 I/O작업을 자식 스레드에게 위임하는 프로그램이다.
pthread_create함수로 스레드를 생성하는데,
tid에는 스레드의 id값이 저장되게 되고, processfd 부분에는 스레드가 실행할 함수의 포인터가 들어간다.
fd 부분에는 함수를 실행할 때 넘겨줄 인자가 들어간다.
참고로 스레드가 수행할 함수의 인자는 void * 타입으로 아무런 인자나 다 받을 수 있다.
하지만, 함수 내부에서 사용할 때는 적절하게 형변환을 한 후에 사용해야 한다. (e.g. *((int *)arg) )
pthread_join에 대해서는 아래에서 설명.
- pthread_detach
pthread_create를 수행하면 이 함수를 수행한 스레드가 부모 스레드, 생성된 스레드가 자식 스레드로
서로 부모-자식관계로 묶이게 된다.
하지만, 필요에 따라 이 관계를 끊어야할 필요가 있을 때 pthread_detach 함수를 사용한다.
void *processfd(void *arg); int error; int fd; ptherad_t tid; if (error = pthread_create(&tid, NULL, processfd, &fd)) { fprintf(stderr, "Failed to create thread: %s\n", strerror(error)); } if (error = pthread_detach(tid)) { fprintf(stderr, "Failed to detach thread: %s\n", strerror(error)); } // or void *processfd(void *arg) { int i = *((int *)arg); if (!pthread_detach(pthread_self()) return NULL; fprintf(stderr, "My arg is %d\n", i); return NULL; }
pthread_detach의 인자로 들어가는 tid의 스레드는 부모와의 관계가 끊어지게 되는데,
이를 부모 스레드에서 tid로 호출하던, 자식 스레드 본인이 pthread_self()로 호출하던 관계를 끊을 수 있다.
주의해야 할 점은 관계가 끊어진 스레드 사이에선 pthread_join을 사용하면 에러가 발생한다는 점이다. (tid를 알고있다 하더라도)
- pthread_join
이 함수는 detach되지 않은 스레드 사이에서 동작하며, 인자로 들어간 tid 스레드가 종료할 때 까지
호출한 스레드를 정지시키는 동작을 수행한다.
부모-자식 관계에서는 부모 스레드가 자식 스레드보다 먼저 죽는 일을 방지하는 셈이다.
int error; int *exitcodep; pthread_t tid; if (error = pthread_join(tid, &exitcodep) { fprintf(stderr, "Failed to join thread: %s\n", strerror(error)); } else { fprintf(stderr, "The exit code was %d\n", *exitcodep); }
부모 스레드는 pthread_join 다음줄에서 정지하다가 자식 스레드가 종료되면 다시 실행된다.
이 때, 자식 스레드의 void * 자료형의 리턴 값이 exitcodep에 전달된다.
- pthread_exit, pthread_cancel
프로세스가 종료되는 조건은 다음과 같다.
1. 어떠한 스레드에서 SIGSEGL, SIGINT 등 종료 시그널을 수신하는 경우
2. 어떠한 스레드에서 exit()을 호출하는 경우
3. main 스레드가 종료되는 경우
그렇다면 자식 스레드만 종료하는 방법은 무엇일까.
바로 위의 함수들을 사용하는 것이다.
void pthread_exit(void *value_ptr);
pthread_exit는 호출한 스레드를 종료시키고 인자로 들어간 상태값을 부모 스레드의 pthread_join에 exitcodep로 전달한다.
return과 큰 차이점은 없는거 같지만, 스레드 내부에서 호출한 함수 내부에서 pthread_exit을 호출하는 순간
그 스레드가 종료된다는 점에서 종료시점을 원할 때에 선택할 수 있다는 이점이 있다.
int pthread_cancel(pthread_t thread);
pthread_cancel은 인자로 들어간 tid 스레드를 종료시킨다.
한 스레드에서 다른 스레드의 종료 요청을 보낼 수 있다는 점이 유용하다.
- thread attributes
스레드를 생성할 때, 설정값을 정의해 스레드를 만들수도 있다.
필요한 상황의 예시와 코드를 함께 살펴보자.
1. 스레드를 생성할 때 부터 부모와의 관계를 끊고싶은 경우
int error, fd; pthread_attr_t tattr; pthread_t tid; // attribute 객체 초기화 if (error = pthread_attr_init(&tattr) { fprintf(stderr, "Failed to create attribute object: %s\n", strerror(error)); } // detach 설정 if (error = pthread_attr_setdetechstate(&tattr, PTHREAD_CREATE_DETACHED)) { fprintf(stderr, "Failed to set attribute state: %s\n", strerror(error)); } // 스레드 생성: 부모와 관계가 끊어진 채 생성 if (error = pthread_create(&tid, &tattr, processfd, &fd) { fprintf(stderr, "Failed to create thread: %s\n", strerror(error)); }
2. 스레드가 사용하는 스택공간이 매우 큰 경우
가상 메모리 공간상에서 malloc과 같이 동적할당되는 메모리는 heap영역에서 아래에서 위로 차오르고,
함수 호출, 스레드의 스택공간은 stack영역에서 위에서 아래로 차오른다.
이 때, 스레드에서 크기가 매우 큰 지역변수를 사용하거나,
스레드 내부에서 다른 함수를 호출할 때 아주 큰 크기의 인자를 넘긴다면?
stack영역이 아래로 확장하다가 heap 영역의 데이터에 손상을 가할 수도 있다.
그렇게 된다면 메인 함수에서 지역변수의 값이 깨진다던지,
함수의 리턴 주소가 손상되어 실행흐름이 점프한다던지, 원치 않은 일이 발생할 수 있다.
따라서 이런 경우에는 malloc을 사용해서 큰 배열을 저장하고 그 포인터를 전달하는 방식으로 사용하던지,
또는 heap영역에 스레드를 위한 stack공간을 만드는 방식을 활용해야 한다.
int main(void) { pthread_attr_t attr; int fd; pthread_t tid; void *stackaddr; void *mystack; size_t stacksize; size_t mystacksize = 2 * 4096; pthread_attr_init(&attr); // 스레드 설정 객체 초기화 mystack = malloc(mystacksize); // 8mb heap 공간 확보 pthread_attr_setstack(&attr, mystack, mystacksize); // heap영역의 공간을 스레드의 stack공간으로 활용 pthread_create(&tid, &attr, processfd, &fd); // 그 공간을 사용하도록 스레드 생성 pthread_attr_getstack(&attr, &stackaddr, &stacksize); // heap영역의 공간에 대한 정보를 가져옴 printf("Retrieved stackaddr is %x, %d\n", stackaddr, stacksize); pthread_attr_destroy(&attr); // 스레드 설정 객체 삭제 return 0; }
- clone
스레드를 생성하는 다른 방법으로 clone이란 함수도 있다.
사실은 pthread_create보다 fork와 더 비슷한 동작을 수행하는데,
fork와 차이점으로 clone으로 생성된 프로세스는 부모 프로세스와 가상 메모리, 파일지시자 테이블,
시그널 핸들러 등을 공유할 수 있다는 점이다.
즉, 스레드와 유사한 기능을 수행할 수 있게 해준다.
int thread_func(void *arg); void main(void) { int pid, arg; int flags = SIGCHLD | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_VM; char *pStack = malloc(STACK_SIZE); pid = clone(thread_func, (char *)pStack+STACK_SIZE, flags, &arg); waitpid(pid, NULL); }
여기서 메모리 공간의 주소를 pStack이 아닌, pStack+STACK_SIZE로 한 이유는,
스레드는 메모리 영역을 stack 영역처럼 활용하기 때문에 heap영역에 할당된 메모리 공간이라 해도,
메모리 공간을 위에서 아래로 사용하기 때문에, 메모리 공간 가장 윗 부분의 주소를 보내주기 위함이다.
'대학 > 시스템소프트웨어' 카테고리의 다른 글
Memory Management (0) 2022.12.11 Synchronization (0) 2022.12.11 Inter-Process Communication (2) 2022.10.23 Timer (0) 2022.10.22 Exceptional Control Flow 2 - Signal (0) 2022.10.22