42에서도 아주 유명한 문제를 코드로 풀어내라는 프로젝트가 있다.

그중의 하나가 Philosophers이다.

 


목차

 

  1. subject 분석
  2. 사용 가능 함수
  3. Algorithm

1. subject 분석

 

나는 클러스터를 나갈 때 평가를 적어도 한 번 이상은 하고 집을 간다.(물론 못할 때도 있다.)

그중에 2번째로 많이 봤던 프로젝트가 위 프로젝트이다.

 

내가 평가 시에 어떤 방식으로 프로젝트가 움직이는지 파악했던 것을 알아보자!

철학자와 포크의 수는 같고 철학자는 포크가 2개여야만 식사를 할 수 있다.

철학자는 식사를 한 뒤에 자야 한다.

철학자는 자고 난 뒤에 생각하기 시작한다.

철학자는 일정 시간 동안 밥을 못 먹으면 죽는다.

한 명의 철학자가 죽으면 프로그램이 끝난다.

데드락이 걸리지 않게 조작을 해야 한다.

 

위와 같이 간단하지만 프로그램의 로직으로 들어가면 그리 간단하지만은 않다.

왜냐하면 우리는 thread를 사용해서 이 프로그램을 만들어야 하기 때문이다.

 

여기서 우리는 thread의 사용이 어떤 것을 의미하는지부터 알아봐야 한다.

thread는 우리가 컴퓨터를 사용하는 것에서도 밀접하게 연관되어있다.

모두들 어떤 기능을 사용하는 것이 당연해지면 이 기능이 어떻게 이뤄지는지를 알아봐야 한다.

예를 들어, 인터넷 url을 이동하면서 코드를 작성하거나 여러 개의 다른 여러 가지 동작을 할 수 있게 하는 것 이것이 thread이다.

게임에서도 많이 사용된다. 화면을 띄우고, 배경음악을 깔고, 타이머의 동작 그리고 게임키 입력 등 많은 행동들을 여러 개의 독립적인 공간?으로 만들어주는 역할을 하는 게 thread이다.

 

pthread의 의미를 알아보자 POSIX Thread의 약자로 유닉스 계열 POSIX시스템에서 병렬적으로 작동하는 소프트웨어를 작성하기 위해 제공하는 API이다.

 

의미가 참 어렵긴 하다. 하지만 조금 쉽게 풀어서 생각해보자.

process는 컴퓨터에서 연속적으로 실행되는 컴퓨터 프로그램을 야기한다.

thread를 이야기하는 중에 process가 왜 나오냐?

 

기본적으로 하나의 process는 하나의 thread를 생성한다.

process는 자원들을 갖고 있다. 예를 들어 code, data, heap, stack이다.

code는 프로그램의 소스코드, data는 전역변수, heap은 동적할당받은 변수, stack은 지역변수로 생각하면 될 것 같다.

여기서 thread는 stack만을 보유하고 있으며, process에서 code, data, heap을 보유하여 공유한다.

즉, thread를 생성할 때 전역변수와 동적할당으로 선언한 변수의 주소는 공유받고 있지만 stack은 비어있는 상태라는 것이다.

그렇지만 우리가 본 프로젝트를 진행할 때 전역변수를 다루는 과제는 아니다.

그렇기 때문에 pthread_create 사용시에 현재 thread에서 사용하는 하나의 변수의 주소를 보내준다.

heap은 공유받고 있기 때문에 받은 변수의 주소를 새로 생성된 thread에서 입력인자로 받고 이를 사용하는 것이다.

 

이제 그럼 우리는 process에 thread를 만드는 작업을 해야 하는 데! 항상 그래 왔듯이 예제를 찾아서 확인하자!

 


2. 사용 가능 함수

 

헤더는 아래와 같다.

#include <memory.h>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <pthread.h>

 

그리고 컴파일할 때 -lpthread 옵션을 주라 하는데... 이따 하면서 확인해봐야겠다.

 

우리가 사용할 수 있는 함수는 총 14개다.

memset, printf, malloc, free, write, usleep, gettimeofday, pthread_create, pthread_detach, pthread_join, pthread_mutex_init, pthread_mutex_destroy, pthread_mutex_lock, pthread_mutex_unlock

 

하나씩 알아보자.

memset : 세 번째 인자로 받아온 byte만큼 두 번째 인자를 첫 번째 인자에 채워 넣는다.(메모리를 채운다.)

함수 원형 : void *memset(void *ptr, int value, size_t num)

헤더 : memory.h 혹은 string.h

 

printf : 출력

헤더 : stdio.h

 

write : 출력(출력되는 형식을 바꿀 수 있다.)

헤더 : unistd.h

 

usleep : (인자) micro sec만큼 쉰다.

헤더 : unistd.h

 

malloc, free : 메모리 할당과 해제

헤더 : stdlib.h

 

gettimeofday : 현재 시스템 시간을 가져온다.

함수 원형 : int gettimeofday(struct timeval *tv, struct timezone *tz)

첫 번째 인자 : 현재 시스템 시간을 저장하기 위한 구조체

struct timeval
{
	long tv_sec;
	long tv_usec;
}

두 번째 인자 : 원래는 표준시간대의 정보가 들어있었지만 지금은 사용되지 않고 그냥 NULL을 넣어주면 된다.

헤더 : sys/time.h

 

이 아래서부터의 헤더는 pthread.h이다.

 

pthread_create : thread를 생성한다.

함수 원형 : int pthread_create(pthread_t *th_id, const pthread_attr_t *attr, void* (func), void *arg)

첫 번째 인자 : pthread의 식별자로 thread가 생성되면 식별 값이 주어진다.

두 번째 인자 : pthread의 옵션, 기본은 NULL

세 번째 인자 : pthread로 분기할 함수, 함수는 void*로 선언되어야 한다.

네 번째 인자 : 분기한 함수로 넣을 인자로 처음 넣을 때는 void*지만 필요시 캐스팅하여 사용.

리턴 값 : 성공하면 0

 

pthread_detach : 첫 번째 인자로 받은 식별 값을 갖고 있는 pthread를 부모 pthread로 부터 독립시킨다.(종료 시 자동 리소스 해제)

함수 원형 : int pthread_detach(pthread_t *th_id)

 

 

pthread_join : 첫번째 인자로 받은 식별 값을 갖고 있는 pthread가 종료될 때까지 기다리다가 종료될 때 자원 해제시킨다.

함수 원형 : int pthread_join(pthread_t *th_id, void **thread_return)

 

pthread_mutex_init : mutex개체를 초기화시킨다.

함수 원형  : int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutex_attr *attr)

첫 번째 인자 : 초기화시킬 mutex 인자

두 번째 인자 : 초기화할 때의 옵션, 기본으로 할 때는 NULL

리턴 값 : 성공 시 0, 실패 시 -1

 

pthread_mutex_destroy : mutex 개체를 파괴한다.

함수 원형 : int pthread_mutex_destroy(pthread_mutext_t *mutex)

 

pthread_mutex_lock : mutex 잠금을 요청한다.

함수 원형 : int pthread_mutex_lock(pthread_mutex_t *mutex)

 

 

pthread_mutex_unlock : mutex 잠금을 해제한다.

함수 원형 : int pthread_mutex_unlock(pthread_mutex_t *mutex);

 

 


우리가 thread 간의 동기화를 위해 사용하는 2가지 방법이 있다.

첫 번째는 mutex이다.

mutex는 두 개의 thread사이에서 상호 보완적으로 lock, unlock을 사용하여 사용을 제어하는 것.

mutex : mutual exclusion(상호 배제)

 

두 번째는 semaphore이다.

semaphore는 mutex와 다르게 여러 개의 thread사이에서의 사용을 제어한다.

즉, mutex는 한 개의 동기화 대상을 갖고 있고 semaphore는 한 개 이상의 동기화 대상을 갖고 있다.

 

여기서 한 가지 알아야 할 것이 임계 영역(Critical section)이다.

임계 영역은 여러 thread가 데이터를 공유할 때, 각 thread에서 공유 데이터로 접근하는 과정을 야기한다.

하나의 thread에서 임계 영역으로 접근할 때, 만일 lock이 되어 있다면 기다리게 만들어야 한다.

또한, unlock 되었다면 기다리고 있던 thread를 들여줘야 한다.

 

즉, 우리가 임계 영역(공유된 자원을 사용할 때)에선 하나의 thread만 사용하게 만들어야 하고 그 과정에서 mutex, semaphore가 필요하다. get it?

 

 

이제 예제를 풀어보자.

pthread_create, pthread_join 예제

void	*t_func(void *data)
{
	pid_t pid;
	pthread_t tid;
	char *thread_name;
	int i;

	pid = getpid();
	tid = pthread_self();
	thread_name = (char *)data;
	i = 0;
	while (i < 3)
	{
		printf("[%s] pid : %u, tid : %x --- %d\n",
				thread_name, (unsigned int)pid, (unsigned int)tid, i);
		i++;
		sleep(1);
	}
}

int	main(void)
{
	pthread_t p_thread[2];
	int thr_id;
	int status;
	char p1[] = "thread_1";
	char p2[] = "thread_2";
	char p_main[] = "thread_main";

	sleep(1);
	thr_id = pthread_create(&p_thread[0], NULL, t_func, (void *)p1);
	if (thr_id < 0)
	{
		perror("thread create error : ");
		exit(0);
	}
	sleep(1);
	thr_id = pthread_create(&p_thread[0], NULL, t_func, (void *)p2);
	if (thr_id < 0)
	{
		perror("thread create error : ");
		exit(0);
	}
	sleep(1);
	t_func((void *)p_main);
	pthread_join(p_thread[0], (void **)&status);
	pthread_join(p_thread[1], (void **)&status);
	printf("끝!\n");
	return (1);
}

위 코드에서 알 수 있는 것은 pthread_create로 생성과 동시에 함수를 실행한다.

그리고 pthread_join으로 thread의 종료를 기다린다.

여기서 시간의 흐름을 잘 보면 결괏값이 어떻게 나오는지 예상이 될 것이다.

 

pthread_join 예시

void	*t_func(void *data)
{
	int num;

	num = *(int *)data;
	printf("num %d\n", num);
	sleep(1);
	num *= num;
	printf("The end thread function\n");
	return ((void *)num);
}

int	main(void)
{
	pthread_t p_thread;
	int thr_id;
	int result;
	int	a;

	a = 200;
	thr_id = pthread_create(&p_thread, NULL, t_func, (void *)&a);
	if (thr_id < 0)
	{
		perror("thread create error : ");
		exit(0);
	}
	pthread_join(p_thread, (void *)&result);
	printf("thread join : %d\n", result);
	printf("The end main function\n");
	return (0);
}

위 코드는 pthread_join이 있는 줄과 그 아래줄을 주석 처리하면 왜 pthread_join가 필요한지 확실히 알 수 있다.

 

pthread_detach 예제

void	*t_func(void *data)
{
	char	a[10000];
	static int	num;

	num = *((int *)data);
	printf("Thread Start\n");
	sleep(5);
	printf("Thread End\n");
	return ((void *)&num);
}

int	main(void)
{
	pthread_t p_thread;
	int thr_id;
	int status;
	int	a;

	a = 100;
	printf("Before Thread\n");
	thr_id = pthread_create(&p_thread, NULL, t_func, (void *)&a);
	if (thr_id < 0)
	{
		perror("thread create error : ");
		exit(0);
	}
	pthread_detach(p_thread);
	pause();
	return (0);
}

위 코드는 detach가 자원을 해제해주는지 확인할 수 있는 코드이다.

pthread_detach를 주석 처리하고 다른 터미널에서 ps로 확인해야 한다.

 

pthread_mutex_lock, pthread_mutex_unlock 예제

pthread_mutex_t mutex_lock;

int g_count;

void	*t_func(void *data)
{
	int		i;
	char	*thread_name;

	thread_name = (char *)data;
	g_count = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s COUNT %d\n", thread_name, g_count);
		g_count++;
		sleep(1);
	}
}

int	main(void)
{
	pthread_t p_thread1;
	pthread_t p_thread2;
	int status;

	pthread_create(&p_thread1, NULL, t_func, (void *)"Thread1");
	pthread_create(&p_thread2, NULL, t_func, (void *)"Thread2");
	pthread_join(p_thread1, (void *)&status);
	pthread_join(p_thread2, (void *)&status);
	return (0);
}
pthread_mutex_t mutex_lock;

int	g_count;

void	*t_func(void *data)
{
	int			i;
	char		*thread_name;

	thread_name = (char *)data;
	pthread_mutex_lock(&mutex_lock);
	g_count = 0;
	i = -1;
	while (++i < 3)
	{
		printf("%s COUNT %d\n%d\n", thread_name, g_count, i);
		g_count++;
		sleep(1);
	}
	pthread_mutex_unlock(&mutex_lock);
}

int	main(void)
{
	pthread_t p_thread1;
	pthread_t p_thread2;

	pthread_mutex_init(&mutex_lock, NULL);
	pthread_create(&p_thread1, NULL, t_func, (void *)"Thread1");
	pthread_create(&p_thread2, NULL, t_func, (void *)"Thread2");
	pthread_join(p_thread1, NULL);
	pthread_join(p_thread2, NULL);
	return (0);
}

우선 첫 번째 코드는 lock과 unlock이 없다. 그래서 실행해보면 g_count가 공유되어 실행되고 실행할 때마다 g_count의 출력 값이 다르다.

하지만 두 번째 코드는 lock과 동시에 임계 영역으로 진입하여 공유자원을 독자적으로 사용한다.

 

 


 

3. Algorithm

 

우리는 위의 코드 예제로 이번 프로젝트의 알고리즘을 만들어야 한다.

 

입력할 때의 인자는 아래와 같다.

첫번째 인자 number_of_philosophers : 철학자의 수(포크의 수와 같다.)

두 번째 인자 time_to_eat : 먹는 시간[ms]

세 번째 인자 time_to_die : 죽기까지 걸리는 시간[ms](먹으면 초기화)

네 번째 인자 time_to_sleep : 자는 시간[ms]

다섯 번째 인자 number_of_times_each_philosopher_must_eat : 프로그램을 종료하기 위한 철학자들이 먹은 횟수(모든 철학자)

 

철학자는 1부터 시작해서 첫 번째 인자까지의 이름을 정해준다.

철학자는 포크를 잡고, 먹고, 생각하고, 자고, 죽는 상황을 실시간으로 중계해주어야 한다.

예를 들면(timestamp_in_ms는 모든 철학자가 태어난 후부터의 시간[ms], X는 철학자의 번호)

  • timestamp_in_ms X가 포크를 잡았다.
  • timestamp_in_ms X가 먹는 중이다.
  • timestamp_in_ms X가 자는 중이다.
  • timestamp_in_ms X가 생각하는 중이다.
  • timestamp_in_ms X 죽었다.

그럼 일단 처음 경우의 수는 2가지이다.

다섯 번째 인자가 들어왔을 때와 안 들어왔을 때.

다섯 번째 인자가 안 들어왔을 때는 -1을 넣고 시작하고 그게 아니라면 인자를 넣고 시작하면 먹는 수를 헤아릴 수 있다.

 

그리고 아래와 같이 INPUT으로 들어온 인자가 잘못된 경우 에러를 출력하고 끝내기로 하자.

  • 숫자가 아닌 문자로 들어왔을 경우
  • 인자가 0보다 작거나 INT_MAX보다 큰 경우

 

로직을 만들다가 문득 시간을 어떻게 조절할 것인지의 의문이 들었다.

누군가는 먹기 시작하면 그 철학자의 시간은 이미 먹은 후로 jump가 되어 있다.

그렇다면 역시 thread가 각각 움직인다고 하지만,,, 누군가가 죽어버린다면 프로세스 자체를 종료시켜야 하는 상황이다.

그렇다면 경우는 자다가 죽는 경우, 생각하다가 죽는 경우뿐이 없다. 먹다가 죽는 경우는 없기 때문

 

그럼 철학자의 죽음은 다음과 같이 확인되어야 한다.

  1. 철학자가 자거나 생각한 뒤의 시간과 마지막으로 먹은 시간을 비교
  2. 시간의 차이가 먹지 못해 죽는 시간보다 크다면 죽음을 선언한다.

 

여기서 몇 가지 생각해봐야 할 점.

  • 철학자가 포크를 들고 생각을 하거나 잘 수 있는가?
  • 철학자가 포크를 들었다가 다음 포크를 잡지 못할 때 들었던 포크를 놓게 할 수 있는가?

 

일단 첫 번째, 철학자가 포크를 들고 생각을 하거나 잘 수 있는가?

만일, 모든 철학자의 첫번째 행동을 왼쪽(or 오른쪽)의 포크를 잡는 것이라 가정해보자!

그렇다면 모든 철학자는 왼쪽(or 오른쪽)의 포크를 잡고 반대편의 포크를 바라면서 대기하는 상황이 될 것이다.

이것을 우리는 교착상태(dead lock)라고 한다. 한없이 기다리다가 죽는 것이다.

 

두 번째, 철학자가 포크를 들었다가 다음 포크를 잡지 못할 때 들었던 포크를 놓게 할 수 있는가?

이미 실행 중인 mutex의 경우 다른 thread가 접근하면 기다리게 한다.

만일 pthread_mutex_lock이 반환 값이 있고 대기할 때의 반환값이 있다면 가능하지만 포크를 잡기 전까지 무한정 대기를 하기 때문에 내 생각에는 lock이 풀릴 때까지 무한 루프를 돈다고 생각한다. 즉, 안된다.

그렇기 때문에 우리는 포크 두 개를 잡을 수 있는 상황에서만 잡게 만들어줘야 한다.

 

그렇다면 철학자들은 짝, 홀로 나눠서 짝수는 식사를 하게 하고  홀수는 자거나 생각을 하게 하면 어떨까?

철학자들은 먹고 자고 생각하다가 먹거나, 먹고 자고 먹는 경우가 생긴다.

 

즉, 프로그램에서 요구하는 대로 될 수도 있다는 것이다.

 

자... 계속해서 내가 시간 순서를 생각하는 중이 었는데 왜 시간 차가 안 줄어드는 문제가 생기는 가에 대한 중요한 논점이 어딘지 파악하기가 어려웠다.

그래서 나는 코드를 줄이고 줄이고 했는데도 왜 안되는지... 거의 1~2주를 고민하다가 코드만 보고 있는 상황이었다.

근데 정작 중요한 한 가지의 핀트를 잡지 못해서 무려 1~2주의 시간을 버렸던 것이다.

그것은 usleep의 정확도이다.

c의 내장 함수를 너무 맹신했으며 내가 사용하는 함수는 사용에 정확도만 보고선 거의 100%에 가깝다고 생각했다.

하지만. 아니었다.

usleep의 정확도가 생각보다 큰 오차를 발생하는 듯했다.

근데 왜 이러한 결과를 보여주느냐?

man에서 봤을 때 usleep은 nanosleep을 이용해 구현된다.

usleep의 원형을 봤을때 "int usleep(useconds_t used)"인데 여기서 used는 1000000보다 클 때 에러를 출력하게 되는데,

이 말인즉슨 1000000과 가까울 때 오차가 커질 수 있다고 판단했다.

그럼 우리는 어떻게 해야 하는가로 확인했을 때 sleep 함수를 새로 만드는 것이 빠르다 판단했다.

즉, 우리가 sleep에 들어갔을 때 루프를 돌면서 조건에 만족하지 못했다면 0.1ms초만큼 usleep을 시키고 그게 아니면 탈출하는 것이다.

자 그렇게 된다면 우리는 원하는 시간이 된다면 루프를 빠져나와서 원하는 시간대로 sleep 하게 만들 수 있다는 점이다.

let's go!

 

여기서 한 가지 더 문제점, 순서대로 진행이 되지 않으며 unlock 됨과 동시에 다른 철학자가 바로 잡아버리는 상황이 발견되었다.

이 문제는 내가 eat 다음에 할 행동이 바로 sleep이라는 것을 확인했을 때 벌어지는 점을 주목했을 때, unlock을 sleep 이후에 unlock 한다면 될 것으로 확인했다.

이렇게 sleep을 출력한 후에 바로 다른 철학자가 포크를 잡는 상황을 보여준다.

이건 내가 오른쪽 포크를 먼저 잡게 해서 생긴 문제로 보여서

왼쪽 포크를 먼저 잡게 함으로써 만족했다.

 


조금 다른 이야기지만 현재 클러스터를 못 가서 MacBook Air m1으로 진행 중인데 leaks를 사용해서 메모리 누수를 확인하려 하니까..

 

Process (PID_num) is not debuggable. Due to security restrictions, leaks can only show or save contents of readonly memory of restricted processes.

이런 문구가 뜨는데.... 과카몰리로 해보면 전혀 뜨질 않는다.

음.. 누수 확인할 때는 앞으로 과카몰리로만 해야겠다.

 


마지막 체크해야 할 사항

 

  1. 철학자당 하나의 thread가 설정되었는가?
  2. 철학자당 하나의 fork가 설정되었는가?
  3. fork당 mutex가 있는지 확인하고 그것을 사용하는지 확인해라.
  4. 출력할 때 순서대로 진행되는가?
  5. 철학자가 죽었을 때 어떻게 처리하는지 확인하면서 서로 다른 철학자가 먹기와 죽음을 시작할 때 어떻게 처리가 되는가?
  6. 200명의 철학자보다 적게 테스트할 것, 먹고 자고 죽음의 설정 시간은 60ms보다 작아야 한다.
  7. 1 800 200 200의 입력 인수를 넣었을 때 먹지 못하고 죽어야 한다.
  8. 5 800 200 200의 입력 인수를 넣었을 때 아무도 죽지 말아야 한다.
  9. 5 600 200 200 7의 입력 인수를 넣었을 때 아무도 죽지 말아야하며 모든 철학자가 7번을 먹었을 때 프로그램이 멈춰야 한다.
  10. 4 410 200 200의 입력인수를 넣었을 때 아무도 죽지 말아야 한다.
  11. 4 310 200 100의 입력 인수를 넣었을 때 한 명의 철학자가 죽어야 한다.
  12. 2명의 철학자로 테스트하면서 서로 다른 시간을 확인해라.
  13. 죽음에 있어서 10ms이상의 딜레이가 없어야 한다.

 


4. bonus

 

보너스는 mutex를 semaphore로 바꿔야 한다.

 

사용 가능한 추가 함수 : fork, kill, waitpid, sem_open, sem_close, sem_post, sem_wait, sem_unlink

 

fork : pid_t fork(void)

헤더 : unistd.h

반환 값 : 성공 시 자식 PID, 실패 시 -1 반환

설명 : 호출하는 process는 부모 process가 되고 성공시 자식 process PID가 반환되어 fork 이후의 커멘드를 실행한다.

 

kill : int kill(pid_t pid, int sig)

헤더 : signal.h

반환 값 : 성공 시 0, 실패 시 -1, 그 외의 음수가 반환될시엔 process 그룹에 속하는 모든 process에 시그널을 전송

설명 : pid에 sig전송

 

waitpid : pid_t waitpid(pid_t pid, int *statloc, int options)

헤더 : sys/wait.h

반환값 : 성공시 process PID, 실패시 -1

설명 : 자식 프로세스를 기다림

 

sem_open : sem_t *sem_open(const char *sem_name, int oflags, ...)

헤더 : semaphore.h

반환 값 : 성공 시 semaphore의 주소를 반환, 실패 시 SEM_FAILED 반환

설명

  1. const char *sem_name : semaphore 객체에 이름을 지정하는 문자열
  2. oflags
    1. O_CREAT : mode_t mode, unsigned int value의 인수를 필요로 하며 새로운 세마포어를 생성한다. 그러나 이미 명명된 semaphore를 다루면 해당 semaphore의 핸들을 반환한다.
    2. O_CREAT | O_EXCL : 여기서는 위와 같지만 이미 명명된 semaphore를 다룰 때 실패하도록 한다.
  3. mode
    1. 해당 semaphore의 권한 정보를 뜻한다.
    2. 이 정보는 semaphore를 생성할 때 디렉터리가 하나 생성되는데 그때 그 디렉터리의 권한 정보를 넣어주기 위함이다.
  4. value
    1. semaphore의 초깃값을 지정하는 값이다.
    2. 즉, 수락 횟수, 공유자원의 수를 뜻한다.
    3. 이 값으로 우리가 wait, post에서의 semaphore의 상태를 알 수 있다.

sem_close : int sem_close(sem_t *sem)

헤더 : semaphore.h

반환 값 : 성공 시 0, 실패 시 -1

설명 : 이름이 명명된 semaphore를 종료시키며 할당한 자원을 전부 할당 해제한다.

 

sem_post : int sem_post(sem_t *sem)

헤더 : semaphore.h

반환값 : 성공시 0, 실패시 -1

설명 : semaphore의 값을 증가시킨다.

 

sem_wait : int sem_wait(sem_t *sem)

헤더 : semaphore.h

반환값 : 성공시 0, 실패시 -1

설명 : semaphore의 값을 확인하여 0이라면 양수일 때까지 기다리고 0보다 큰 값이라면 0으로 만들고 다음 커멘드를 실행한다.

 

sem_unlink : int sem_unlink(const char *name)

헤더 : semaphore.h

반환 값 : 성공 시 0, 실패 시 -1

설명 : name으로 명명된 semaphore를 제거한다.

 

 


fork를 여러 개 생성해보자.

 

int main(void)
{
	pid_t	childpid[5];
	int		i;

	i = -1;
	while (++i < 5)
	{
		childpid[i] = fork();
		if (childpid[i] > 0)
		{
			printf("parent pid is %d\nchild pid is %d\n", getpid(), childpid[i]);
			printf("parent is write now\t%d\n\n", i);
		}
		else if (childpid[i] == 0)
		{
			printf("child is write now\n\n");
			break ;
		}
		else
		{
			printf("Fork Error\n");
		}
	}
	return (1);
}

 

여기서 중요한 점은 5개의 자식 process를 생성하는데 자식 process의 실행 과정 마지막에 break를 걸어주어야 한다.

그렇게 하지 않는다면 5개가 아닌 31개가 생성될 것이다.

그 이유는 프로세스를 fork 하면 "반으로 나눠진다."라고 생각해야 한다.

즉, 처음 process 1개 $2^0$, process 2개 $2^1$, process 4개 $2^2$, process 8개 $2^3$, process 16개 $2^4$

다 합하면 31개이기 때문이다!

여기서 처음 생성된 자식 process가 다음엔 부모 process가 되기 때문에 연속적으로 자식 process가 할 행동을 총 31번을 하는 것이다.

 


semaphore를 이용해서 프로세스의 흐름을 제어해보자.

 

일단 process 5개를 생성하고 위에서 봤듯이 fork의 반환 값이 0이라면 자식 process의 진행이라는 것을 알았다.

이를 이용해서 부모와 자식의 행동을 구분 짓는다.

그리고 우리는 자식의 행동에서 semaphore의 값을 1로 두고 접근할 수 있는 process의 수를 1개로 제한하여 행동하게 한다.

아래와 같은 로직을 보자.

 

int main(void)
{
	pid_t			childpid[5];
	sem_t			*sem;
	int				idx;
	int				tmp;
	int				status;

	idx = -1;
	sem = sem_open("sema", O_CREAT | O_EXCL, 644, 1);
	printf("-------Start!------\n\n");
	while (++idx < 5)
	{
		childpid[idx] = fork();
		if (!childpid[idx])
		{
			tmp = 0;
			sem_wait(sem);
			while (++tmp <= 5)
			{
				sleep(1);
				printf("Child%d is count %d \n", idx, tmp);
			}
			printf("\n");
			sem_post(sem);
			exit(1);
		}
		else if (childpid[idx] < 0)
		{
			printf("Fork Error\n");
			return (1);
		}
	}
	idx = -1;
	while (++idx < 5)
		waitpid(childpid[idx], &status, 0);
	tmp = 0;
	while (++tmp <= 5)
	{
		sleep(1);
		printf("parent is count %d\n", tmp);
	}
	sem_unlink("sema");
	sem_close(sem);
	printf("\n-------END-------\n");
	return (0);
}

이렇게 진행하면 아래와 같은 process의 진행이 된다.

사실 위에서 설명한 대로만 하면 자식의 행동 중 부모의 카운트가 하나씩 존재해야 하는데 이는 waitpid로 끝날 때까지 기다리는 상황을 만들어줌으로써 제한해준다.

 

이 예시는 위 코드에서 아래 부분을 주석처리 or 삭제처리해주면 아주 간단하게 볼 수 있다.

	while (++idx < 5)
		waitpid(childpid[idx], &status, 0);

그리고 sem_open의 value값을 2로 증가시키면 2개의 프로세스가 동시에 진행되는 것을 볼 수 있다.

 

그래서 이를 보자면 우리가 semaphore를 어떻게 설정할지 어떤 방식으로 사용해야 할지 알 수 있다.

 


로직 설계

 

나가 mandatory에서 사용했던 thread는 process, mutex는 semaphore로 바꿔서 진행하면 된다.

하지만 우리가 mandatory에서와 다른 점은 포크의 위치뿐인 듯하다.

mandatory에서는 철학자의 양 옆에 포크가 존재했기에 잡을 수 있는 포크가 제한되었다.

그러나 bonus에서는 semaphore를 이용하기 때문에 모두가 모든 포크를 잡을 수 있다.

그리고 내장 함수로 봤을 때 한 가지 더 생각하면 나가 pthread_creat를 할 때 입력인자로 void* 하나를 넣어야 하는데 이를 나는 philosophers의 구조체를 보내주었다.

이런 부분이 왜 불편했느냐하면 너무 복잡하게 연결되어있기 때문이었다.

예를 들면 A는 B, C를 원소로 갖고있는데 B도 C를 원소로 갖고있는 상황이 벌어졌기에 이 무슨 비효율인가 했다.

이를 문제로 삼는다면 process를 생성했을때 우리는 모든 것을 복사하여 가져가는데 그렇다면 A만 보내주면 된다는 말이다.

하지만 프로세스당 주어진 이름(id)는 구분되어야 맞는 것이다.

 

전체적인 로직은 아래처럼 만들것이다.

 

진행해보자.

 

진행하면서 semaphore의 unlink문제가 있는것으로 보여져서 조금 고민을 하고 있었다.

우선 어떤 문제가 있었냐하면, 처음 컴파일을 해서 실행을 했을땐 세마포어가 잘 작동했다.

하지만 중간에 Ctrl + C를 눌러 program을 강제 종료했는데 이후 실행을 했을 때 sem_open에서 SEM_FAILED가 반환되었다.

그리고 errno를 확인했는데 0가 나왔다...

음,, 0면 no error인데 도데체 무슨 문제일까 생각했는데 예상하건데 강제종료를 해서 sem_unlink가 수행되지 않았다는 생각이 들었다. 

그래서 sem_open하기 전에 sem_unlink를 해보았는데 예상대로 맨 처음 진행했을때 처럼 잘 진행되었다.(연속적으로)

하지만 우리가 semaphore를 몇개를 사용하는지? 만약 많이 사용한다면 우리가 한줄씩 더 추가되게 만드는 조금은 비효율적인 커멘드라고 생각한다.


도움받은 출처

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=nawoo&logNo=80179306072 

 

Semaphore (세마포어) 란? [쉬운예제포함] Unix / Linux / C

출처 : http://blog.naver.com/tacma/20100065245 - 프로세스간 메세지 전송을 하거나, 혹은 공유메모리를 ...

blog.naver.com

 

'42일기' 카테고리의 다른 글

Minishell(3)  (0) 2021.12.20
python Dict, set 활용  (0) 2021.10.15
Push_swap  (0) 2021.06.24
Minitalk(2)  (0) 2021.06.17
Minitalk(1)  (4) 2021.06.13

+ Recent posts