필로소퍼의 Mandatory 파트가 스레드와 뮤텍스를 사용하는 과제였다면, Bonus 파트는 프로세스와 세마포어를 사용하는 과제다. 때문에 허용하고 있는 함수가 다르다!
memset, printf, malloc, free, write, fork, kill, exit, pthread_create, pthread_detach, pthread_join, usleep, gettimeofday, waitpid, sem_open, sem_close, sem_post, sem_wait, sem_unlink
새로운 녀석들을 살펴보자.
- syntax :
#include <unistd.h> pid_t fork(void);
- description :
현재 실행중인 프로세스를 복사해 자식 프로세스를 생성한다.- return :
- 성공 시
부모:자식 프로세스의 pid
/ 자식:0
- 실패 시
부모:-1
/ 자식:생성 안됨
이 친구는 minitalk 과제를 할 때 사용한 적이 있다.
- syntax :
#include <signal.h> int kill(pid_t pid, int sig);
- description :
pid
가 양수일 때sig
를 해당pid
의 프로세스로 전송한다.- return :
성공 시0
실패 시-1
주요 시그널은 다음과 같다.
필로소퍼 과제에서 kill은 자식 프로세스를 죽이기 위해 사용하는데, 이때 사용되는 signal은 SIGINT
이다. 우리가 쉘에서 실행중인 프로그램을 종료할 때 ctrl
+ c
단축키를 사용하곤 하는데, 그게 바로 SIGINT
신호다!
- syntax :
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
description :
pid
인자로 지정한 자식이 상태를 바꿀 때까지 호출 스레드의 실행을 중지한다. 인자의options
는WNOHANG
,WUNTRACED
,WCONTINUED
의 값을 OR 한 것인데, 나는 사용하지 않았기 때문에0
으로 두었다. 인자의pid
의 값은 다음과 같다.
< -1
: 프로세스 그룹 ID가 pid의 절댓값과 같은 아무 자식 프로세스나 기다리기-1
: 아무 자식 프로세스나 기다리기0
: waitpid() 호출 시점에 프로세스 그룹 ID가 호출 프로세스의 프로세스 그룹 ID와 같은 아무 자식 프로세스나 기다리기> 0
: 프로세스 ID가 pid 값과 같은 자식 기다리기return :
성공 시 : 상태가 바뀐 자식의 프로세스 ID
실패 시 :-1
- syntax :
#include <fcntl.h> /* For O_* constants */ #include <sys/stat.h> /* For mode constants */ #include <semaphore.h> sem_t *sem_open(const char *name, int oflag); sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
- description :
세마포어를 초기화하고 여는 함수다. 인자의name
으로 식별하게 되는데,oflag
에 O_CREAT와 O_EXCL이 함께 선언되어 있는 경우,name
의 세마포어가 이미 존재한다면 새로운 세마포어는 생성되지 않고 error를 반환하게 된다.value
는 생성될 세마포어 lock의 초깃값으로,SEM_VALUE_MAX
이하의 값을 가져야 한다.mode
는 세마포어에 대한 접근 권한을 설정하는 값으로, 8진수의 값을 받지만<sys/stat.h>
헤더를 포함할 경우 헤더 내의 상수를 사용할 수 있다. 나는 기본 값인 0644를 사용했다. chmod에 대한 값은 아래의 링크에서 확인할 수 있다.- return :
성공 시 새로운 새마포어의 주소
실패 시SEM_FAILED
- syntax :
#include <semaphore.h> int sem_close(sem_t *sem);
- description :
sem
이 가리키고 있는 세마포어를 닫고 사용하는 메모리를 모두 반납한다.- return :
성공 시0
실패 시-1
- syntax :
#include <semaphore.h> int sem_wait(sem_t *sem);
- description :
sem
이 가리키는 세마포어를 lock 하여 사용할 수 있는 세마포어의 수를 감소시킨다. 세마포어의 수가0
보다 크다면 즉시 lock 하고 함수가 실행되지만 현재 세마포어의 값이0
이라면 사용할 수 있는 세마포어가 생기거나 프로세스가 종료될 때까지 기다리게 된다.- return :
성공 시0
실패 시-1
(세마포어의 값이 바뀌지 않는다)
- syntax :
#include <semaphore.h> int sem_post(sem_t *sem);
- description :
sem
이 가리키는 세마포어를 unlock한다. 즉, 사용 가능한 세마포어의 수를 증가 시켜sem_wait
중인 다른 프로세스에서 세마포어를 lock 할 수 있도록 한다.- return :
성공 시0
실패 시-1
(세마포어의 값이 바뀌지 않는다)
- syntax :
#include <semaphore.h> int sem_unlink(const char *name);
- description :
name
의 세마포어를 사용중인 프로세스들이 모두 세마포어를 close 했을 때 해당 세마포어를 제거한다.- return :
성공 시0
실패 시-1
사실 구현하는 건 세마포어를 사용하는 것이 더 쉬웠던 것 같다. 프로세스 단위로 실행되기 때문에 모니터링 스레드를 모든 필로소퍼들이 각자 가지고 있을 수 있어서 더 간단한 형태로 구현할 수 있었다.
세마포어는 커널에서 큐의 형태로 사용되기 때문에 기아 상태를 해결할 수 있다는 장점이 있다. 하지만 디스크에 접근해야 하기 때문에 속도가 비교적 느리다. 때문에 딜레이를 고려하여 time_to_die
를 설정해야만 한다.
💡 기아상태 vs. 교착상태
- 기아상태(Starvation) : 특정 프로세스의 우선순위가 낮아서 원하는 자원을 계속 할당 받지 못하는 상태로, 병행 컴퓨팅에서 주로 나타나는 문제다.
- 교착상태(Deadlock) : 여러 프로세스가 동일한 자원을 점유하려고 할 때 나타나는 문제다. 자세한 내용은 이전 포스트(TIL 42일차 - [42서울] Philosophers(2))에서 확인할 수 있다.
세마포어에 대한 내용을 상세하게 포스팅하고 싶은데, 다음 과제인 minishell에 공부할 내용이 폭탄인데다가 42gg 업데이트와 코드리뷰 견학을 앞두고 있어서 가능할지 모르겠다. 짬을 내서 꼭 써보도록 하겠다...!!!!
철학자 프로그램이 종료되는 조건은 두 가지다. 철학자 중 한 명이 굶어 죽든가(DIE), 모든 철학자가 배불러야 한다(FULL). mandatory part를 구현할 때는 구조체에 is_dead
라는 변수를 넣고 해당 변수를 사용하는 임계 영역에 mutex를 걸어 죽은 철학자가 있는지 없는지를 판별했는데, bonus part에서는 각 철학자가 프로세스 단위로 실행되다보니 조금 다르게 구현할 수 있었다.
waitpid() 함수는 인자로 들어가는 status에 상태가 바뀐 자식 프로세스에 대한 정보를 받아올 수 있다. 해당 함수에서 제공하는 WEXITSTATUS(status)
라는 매크로를 사용하면 자식이 exit()
호출에 지정한 status 인자나 main()
의 return 문에 지정한 인자의 하위 8비트를 확인할 수 있다. 아래의 예시를 살펴보자.
# define DIE 5
# define FULL 6
void check_death(t_info *info, t_philo *philo)
{
now = get_time() - info->t_start;
if (get_time() - philo->t_last_eat >= info->t_die)
// 현재 시간 - 마지막으로 밥을 먹은 시간이 time_to_die 보다 클 경우
{
printf("%lld %d died\n", now, philo->id + 1);
// 죽었다는 메시지를 남기고
exit(DIE);
// 죽는다.
}
}
void end_process(t_info *info)
{
int status;
while (1)
{
waitpid(-1, &wstatus, 0);
// 자식의 상태가 바뀌길 기다리다가
if (WIFEXITED(status) && WEXITSTATUS(status) == DIE)
// 바뀐 자식의 상태가 DIE 라면?
{
kill_pids();
// 모든 프로세스를 종료하는 함수를 실행하고
break ;
// whille문을 빠져나간다.
}
}
}
위의 check_death
는 자식 프로세스에서 사용되는 모니터링 함수를, end_process
는 부모 프로세스에서 자식의 종료를 기다리는 함수를 간략하게 나타낸 것이다. WEXITSTATUS()
매크로가 자식의 종료 호출에 지정한 DIE
를 확인할 수 있기 때문에 어떠한 자식이 죽었다는 사실을 바로 알아챌 수 있는 것이다.
void end_process(t_info *info)
{
int i;
i = -1;
while (++i < n_philo)
// while 문을 철학자의 수 만큼만 돈다.
{
waitpid(-1, &wstatus, 0);
// 자식의 시그널을 기다리다가
if (WIFEXITED(status) && WEXITSTATUS(status) == FULL)
// 배부르다는 소식을 받으면
info->n_full_philo += 1;
// 배부른 철학자를 체크하는 변수에 1을 더해준다.
}
if (info->n_full_philo == info->n_philo)
// 배부른 철학자의 수가 총 철학자의 수와 같다면?
printf("everyone is full!\n");
// 다들 배부르다는 메시지를 출력해준다.
}
FULL
을 체크하기 위해서는 while 문에 위와 같은 조건을 추가해 주면 된다. 배부른 철학자들이 종료될 때마다 waitpid()를 통과하기 때문에, 모든 철학자가 배부른지를 확인할 수 있는 것이다.
하지만 42서울에서는 허용하지 않은 매크로의 사용에 대한 의견이 분분하기 때문에 안전하게 사용하지 않는 편이 좋겠다는 생각을 했다. 다행히도 WEXITSTATUS()
매크로는 비트 연산을 통해 쉽게 나타낼 수 있기 때문에 코드 전체를 고칠 필요는 없었다!
https://veneas.tistory.com/entry/Linux-%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%8B%9C%EA%B7%B8%EB%84%90-%EB%AA%85%EB%A0%B9%EC%96%B4%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%A2%85%EB%A3%8C-kill
https://wariua.github.io/man-pages-ko/waitpid%282%29/
https://man7.org/linux/man-pages/man3/sem_open.3.html
https://man7.org/linux/man-pages/man3/sem_close.3.html
https://man7.org/linux/man-pages/man3/sem_wait.3.html
https://man7.org/linux/man-pages/man3/sem_post.3.html
https://man7.org/linux/man-pages/man3/sem_unlink.3.html
https://ko.wikipedia.org/wiki/%EA%B8%B0%EC%95%84_%EC%83%81%ED%83%9C