Philosophers - Bonus
개요
이전에는 pthread의 mutex를 이용하여 공유 자원의 접근을 통제하였으나, 이번에는 semaphore를 통하여 프로세스 & 스레드의 공유 자원의 접근 제어를 하게 됩니다.
mandtorty와 크게 다르지 않은 보너스입니다. 과장하여 말하면 thread를 process로, mutex를 semaphore로 바꾸면 끝난다! 정도??
저는 bonus 과제가 아쉽다고 느꼈습니다. mutex는 semaphore가 되었고, fork, waitpid, kill을 사용할 수 있게 해주었으니... "더욱 실시간으로 철학자들의 상태를 반영한 정확한 시뮬레이션을 만들 수 있겠다!"라고 느꼈습니다. status, philosopher, message 등등을 각각 프로세스나 스레드를 통해 역할이 적절히 분배된 멋진 시뮬레이션을 만들려고 하였습니다마는...
자원 회수나 프로세스 관련 함수들이 추가된 것은 아니었기 때문에, 역할을 분리할수록 책임은 저에게 온전히 돌아왔습니다. 학습자의 입장에서는 타협할 수밖에 없더군요.
목표
- All the forks are put in the middle of the table.
- 모든 포크는 테이블의 중간에 놓여 있습니다.
- They have no states in memory but the number of available forks is represented by a semaphore.
- 메모리의 상태는 알 수 없지만, 대신 사용가능한 포크의 개수가 세마포어로 표현됩니다.
- Each philosopher should be a process. But the main process should not be a philosopher.
- 각 철학자는 프로세스로 이루어져야 하고, 메인 프로세스가 철학자이어서는 안됩니다.
에? 포크가 철학자 사이에 있지 않다고? 이거 더 쉬운건가...?
구현 전 사전지식
Semaphore
Semaphore
Semaphore(세마포어)는 동기화와 상호 배제를 위한 도구로 사용되는 개념입니다. 주로 다중 프로세스 또는 스레드 간에 공유된 자원에 대한 접근을 조절하기 위해 활용됩니다. 세마포어는 정수형 변수로, 여러 프로세스 또는 스레드 간에 값을 주고 받아 동기화를 달성하는 데 사용됩니다.
- sem_open: sem_open 함수는 세마포어를 생성하거나 기존 세마포어를 열기 위해 사용됩니다. 세마포어를 생성하거나 열 때, 이름을 지정하여 다른 프로세스 또는 스레드 간에 공유할 수 있습니다.
- sem_close: sem_close 함수는 세마포어를 닫습니다. 이 함수는 세마포어를 더 이상 사용하지 않을 때 호출되며, 이는 자원을 정리하고 세마포어와의 연결을 종료하는 역할을 합니다.
- sem_post: sem_post 함수는 세마포어의 값을 증가시킵니다. 세마포어 값이 0보다 작은 경우 대기 중인 프로세스 또는 스레드가 있다면 하나를 깨우고 세마포어 값을 증가시킵니다.
- sem_wait: sem_wait 함수는 세마포어의 값을 감소시킵니다. 만약 세마포어 값이 0보다 작으면, 호출자는 블록되어 세마포어 값이 양수가 될 때까지 대기하게 됩니다.
- sem_unlink: sem_unlink 함수는 이름이 주어진 세마포어를 삭제합니다. 이 함수를 호출하면 세마포어가 물리적으로 삭제되며, 다른 프로세스나 스레드는 해당 세마포어에 접근할 수 없게 됩니다.
구현
static void eating(t_philo *philo)
{
ft_sem_wait(philo->system->forks);
message("has taken a fork", philo);
ft_sem_wait(philo->system->forks);
message("has taken a fork", philo);
message("is eating", philo);
ft_usleep(philo->system->time_to_eat, philo);
ft_sem_wait(philo->status);
philo->lifespan = get_time();
philo->num_of_meals++;
ft_sem_post(philo->status);
ft_sem_post(philo->system->forks);
ft_sem_post(philo->system->forks);
}
식사는 이런식으로 구성할 수 있겠습니다.
void simulate(t_sys *system)
{
int *idx;
if (system->num_of_philo == 1)
{
printf("0 1 has taken a fork\n");
ft_usleep(time_to_die);
printf("%u 1 died\n", system->time_to_die);
}
else
{
idx = (int *)ft_malloc(sizeof(int), system->num_of_philo);
memset(idx, 0, sizeof(int) * system->num_of_philo);
enter(system);
monitoring(system, idx);
free(idx);
}
}
static void enter(t_sys *system)
{
t_uint i;
pid_t philo;
i = 0;
system->time = get_time();
while (i < system->num_of_philo)
{
philo = fork();
if (philo == 0)
{
system->num_of_philo = i + 1;
routine(system);
exit(0);
}
else
system->pids[i] = philo;
i++;
}
}
static void monitoring(t_sys *system, int *idx)
{
t_uint i;
int status;
unsigned char test;
while (1)
{
i = 0;
while (i < system->num_of_philo)
{
if (idx[i] == 0 && waitpid(system->pids[i], &status, WNOHANG))
{
test = WEXITSTATUS(status);
idx[i] = 1;
if (test == 1)
return (process_exit(system, idx));
if (test == 0)
return (monitoring(system, idx));
}
i++;
}
if (check_all_process(system, idx))
return ;
}
}
static int check_all_process(t_sys *system, int *idx)
{
t_uint i;
i = 0;
while (i < system->num_of_philo)
{
if (idx[i] == 0)
return (0);
i++;
}
return (1);
}
static void process_exit(t_sys *system, int *except)
{
t_uint i;
i = 0;
while (i < system->num_of_philo)
{
if (except[i] == 1)
{
i++;
continue ;
}
kill(system->pids[i], SIGTERM);
i++;
}
i = 0;
while (i < system->num_of_philo)
{
if (except[i] == 1)
{
i++;
continue ;
}
waitpid(system->pids[i], NULL, 0);
i++;
}
}
int형 배열 idx는 생성한 프로세스(철학자만큼 생성)의 종료 상태를 매핑하기 위해 선언한 동적 배열입니다. idx[n]은 n + 1번 철학자의 프로세스 종료를 기록할 것입니다. (n > -1) 이 배열은 '0'으로 초기화하고 종료된 프로세스 index는 '0'이 아닌 값이 됩니다.
enter에서 철학자는 프로세스로 루틴을 실행하게 됩니다. 그리고 먹은 횟수를 모두 채우면 routine이 종료되고 exit(0)으로 올바르게 철학자 프로세스가 종료될 수 있도록 했습니다. 메인(부모) 프로세스는 생성한 자식의 프로세스 pid를 기록합니다. 자식 프로세스는 식사 횟수를 채웠을 경우 프로세스는 0으로, 죽었을 경우 1로 exit하게 됩니다.
monitoring에서는 다수의 철학자 프로세스가 실행 중에, 메인 프로세스는 계속 실행 중인 자식 프로세스의 종료 상태를 추적합니다. idx[i]가 0인 것 중에서(프로세스가 종료되지 않은 것만 고르기 위함) waitpid에 옵션을 주어 프로세스 종료를 기다리지 않고 상태만 확인합니다. 확인한 프로세스가 '0'으로 끝났는지 '1'로 끝났는지를 확인하고 해당 index를 '1'로 매핑합니다.
프로세스가 확인한 프로세스가 '0'(누군가 먹는 횟수를 다 채움)인지 '1'(누군가 죽음)인지를 확인하여 모니터링을 더 할지, 프로그램을 종료시킬지 결정합니다. 그리고 확인한 프로세스는 monitoring의 if문과 process_exit의 kill에서 두 번 확인하지 않도록 확인을 제외합니다.
check_all_process는 while(1)의 종료 조건입니다. monitoring 재귀가 무한 반복하지 못하도록 모든 프로세스가 종료된 경우에 이를 확인하고 빠져나올 수 있게 구성했습니다.
process_exit에서 kill 함수는 프로세스 종료 시그널을 보냈다고 해서 그 프로세스가 바로 종료되는 것이 아니었습니다. 바로 강제 종료시키는 옵션도 있다고 하지만 위험한 방법이라 사용하지 않았습니다. 아무튼 종료시그널을 보내고 waitpid에 '0' 옵션을 주어 이번에는 진짜 프로세스가 전부 종료되는지를 확인합니다.
마치며
이 과제는 구현 방향에 따라 부피와 난이도가 크게 달라지는것 같습니다. 재미를 느끼고 싶다면 역할을 많이 분리해 보세요!