보기 전에 혹시 떠올리셨나요? '아이디어'

 

제가 과거에 정리했던 내용이지만 전부 맞다고 생각이 들진 않아요.

 

본인의 아이디어를 떠올려보고 참고용으로 봐주시면 감사하겠어요.

 


1. prompt

prompt를 만들어주는 방법은 두 가지 방법이 있다.

첫 번째는 터미널 제어 속성을 수정해주는 방식이 있다.

두 번째는 readline을 이용한 방식이 있다.

 

첫 번째 방법은 시도해봤지만 너무 복잡하게 이어져있어서 두 번째 방법으로 구현했다.

 

일단 readline을 brew로 설치해야한다.

그리고 readline이 설치된 경로들을 알고있어야 한다.

컴파일시에 지정을 해줘야 하기 때문이다.

 

readline은 여러가지 변수들과 함수들을 사용할 수 있다.

하지만 함수들은 허용된 것들만 사용가능하고 변수는 따로 extern 해주어야 사용이 가능하다.

일단 허용함수들을 살펴보자.

rl_on_new_line : readline실행 중 다음줄로 이동되었다고 설정해주는 함수이다.

rl_replace_line : readline실행 중 들어온 입력을 첫번째 인자로 다시 설정해주는 함수이다.

rl_redisplay : readline실행 중 다시 readline을 실행할 때 사용하는 함수이다.

이들은 signal중 Ctrl + C를 제어할 때 사용된다.

 

변수들을 확인해보자.

rl_catch_signals : readline 실행중에 signal을 표시할지 안할지 설정하는 변수이다.(default = 1)

 

이제 readline을 알아보자.

readline은 엔터가 입력됬을때 입력된 값을 반환한다.

또한 add_history로 키보드 화살표 위, 아래키를 이용해서 이전 커멘드를 다시 표시할 수 있다.

 

이렇게 readline으로 prompt 끝!


2. tokenizing

tokenizing실행 전에 Error Check를 먼저 실행되어야 한다.

Error Check는 여러가지가 있다.

  1. ', "로 문자열을 열었지만 닫지 않은 경우
  2. '|', ';'를 연속적으로 사용했을 경우
  3. 기타 등등(전부 찾기는 힘들다.)

 

tokenizing은 readline으로 입력된 command를 '|', ';' 기준으로 자르는 과정이다.

  1. 과정중에 연속된 space는 하나의 space로 수정해주어야 한다.
  2. '$'로 시작하는 문자열은 환경변수를 참조해서 변경시켜주어야 한다.
  3. '로 둘러 쌓인 문자열은 수정이 가져와야 한다.(환경변수 참조X, 연속된 space변경X)
  4. "로 둘러 쌓인 문자열은 환경변수 참조하여 수정해준다.(환경변수 참조O, 연속된 space변경X)

위 과정으로 자른 문자열들을 queue에 넣어주어야하기 때문에 node에 넣어줄때 자른 token이 '|'라면 pipe인 것을 확인할 수 있는 변수를 만들어 설정해주는다.

또한, node에 넣을때는 space를 기준으로 넣어주는데 ', " 내부는 취급하지 않는다.

 

tokenizing 끝!


3. 로직

전체적인 로직은 아래와 같다.

  1. 모든것을 포함한 구조체 초기화
  2. 제작자들의 개성을 나타내 줄 logo 출력
  3. inf_loop 시행
    1. readline으로 cmd 입력
    2. cmd의 Error Check
    3. tokenizing
    4. execute
    5. tokenizing된 command_queue 메모리 해제
  4. 모든 구조체 메모리 해제

execute는 single command, multi command로 나눈다.

single command는 builtin function일 때만 자식 프로세스를 만들지 않고 실행된다.

왜냐하면 현재 프로세스의 데이터를 직접 수정, 생성해야하기 때문이다.

그 외는 fork를 이용해 자식 프로세스를 만들고 수행시킨다.


4. redirection

redirection은 총 4가지의 구성이 있다.

> {인자} : {인자}를 STDOUT_FILENO로 지정한다. [{인자}라는 파일을 생성(있다면 새로 만든 것처럼 생성)]

< {인자} : {인자}를 STDIN_FILENO로 지정한다.[{인자}라는 파일을 연다.]

>> {인자} : {인자}를 STDOUT_FILENO로 지정한다. [{인자}라는 파일을 생성(있다면 내부 데이터의 가장 끝을 가리켜야 한다.)]

<< {인자} : {인자}를 stop sign으로 취급하고 stop sign이 나올때까지의 문자열들을 STDIN_FILENO로 지정한다.[새로운 파일 생성해서 쓰고 그 파일을 STDIN_FILENO로 지정했다.][이 과정은 home document라고 칭한다.]

 

여기서 내가 가장 헤메게 된 이유는 하나의 문자열로 생각해서 많이 헤멨다.

이 말이 무슨 뜻이냐면 한꺼번에 처리를 해야한다는 생각이었는데 이는 맞는 말이지만 틀린말이기도 하다.

여러개의 redirection이 온다면 각각 지정하는것은 맞다.

하지만 이를 수행하는 것은 가장 마지막으로 지정한 것이다.

또한, 여러개의 redirection이 있을때 사이사이에 커멘드들이 들어갈 수 있었다.

그렇기에 따로 command를 지정할 수 있는 char **가 필요했다.

 

예시를 들어보면

$> < a > b1 > b2 cat > b3 -e >> b4

위의 풀이는 a를 STDIN_FILENO로 지정하고 b1, b2, b3, b4를 생성하는데 STDOUT_FILENO는 b4로 지정된 것이다.

또한, command는 'cat -e'으로 들어가게 되어 입력으로는 a가 들어가게 된다.

이를 검증하고 싶다면 'cat -e' 만 따로 bash의 입력으로 넣어주면 표준입력 입력할 수 있는 상황이 되는데 그 지점이 a가 되는 것이다.

그리고 만약 b4가 이미 존재하고 있다면 데이터의 가장 뒤에 a를 insert해주는 상황이 되는 것이다.

 

요약

command : cat -e

생성된 파일 : b1, b2, b3, b4

STDIN_FILENO : a라는 파일

STDOUT_FILENO : b4라는 파일

 


5. pipe

위에서 우리는 node당 pipe로 잘렸는지 확인하는 변수를 만들어주었다.

또한, 나는 node에 pipe[2]를 따로 지정해주었다.

pipe로 잘렸다면 사용이 되지만 pipe로 잘리지 않았다면 사용이 안된다.

 

pipe는 multi command 상황에서 사용된다.

3가지 경우의 수가 있다.

  1. 시작전 pipe를 열어주는 경우
  2. 자식의 pipe 관리해주는 경우
  3. 부모의 pipe 관리해주는 경우

 

시작전엔 pipe를 열어주는 것 뿐이다.

자식의 pipe관리는 이전 node의 pipe 상태를 보고 pipe[1]를 STDIN_FILENO로 넣어주고,

현재 node의 pipe상태를 보고 pipe[0]를 STDOUT_FILENO로 넣어주는 것을 관리해준다.

부모의 pipe관리는 현재와 이전 노드의 파이프 상태를 보고 pipe를 닫아주는 관리해준다.

 

즉, 이렇게 관리가 된다면 파이프로 연결되었던 커멘드들을 연결시켜줄 수 있는 상황이 되는 것이다.


6. builtin_function

 

cd

  1. cd or cd ~ -> $HOME참조
  2. cd {file_name} -> file_name이 없으면 "minishell: cd: {file_name}: No such file or directory\n"
  3. cd - -> $OLDPWD참조 -> 만약 없으면 "minishell: cd: OLDPWD not set\n"
  4. cd는 상대/절대 경로를 참조할 수 있다.
  5. cd는 인자로 2개 이상 들어온다면 첫번째 경로로 간다.
  6. cd로 움직였을경우 PWD, OLDPWD 모두 바뀌어야한다.
  7. cd로 처음 움직였을 때, OLDPWD가 없다면 생성해주어야 한다.
  8. 성공시 exit_status = 0, 실패시 exit_status = 1

env

  1. env는 입력되는 인자가 없어야한다. -> "env: {첫번째 인자}: No such file or directory"
  2. env는 node->is_ok가 1인 것만 출력한다. [node->is_ok는 '='의 존재 유무이다. 또한, $?는 -1로 고정]
  3. 성공시 exit_status = 0, 실패시 exit_status = 1

export

  1. export는 인자없이 들어왔을 때, Ascii code 오름차순 출력한다.[출력시 앞에 "declare -x "를 먼저 출력한다.]\
  2. export는 인자없이 들어왔을 때, node->is_ok가 -1이 아닌 것만 출력한다.[node->is_ok 가 0라면 key만 출력하고 '='는 출력X]
  3. export는 인자의 첫 문자가  알파벳 혹은 '_'이 아니면 "minishell: export: `인자': not a valid identifier"
  4. export는 인자의 첫 문자를 제외하고 숫자가 들어갈 수 있다.['_'이외의 특수문자가 들어가면 3번의 오류출력을 따른다.]
  5. export는 인자가 '_' 혼자 들어왔을 때 무시한다.[환경변수를 나타내지도 추가하지도 않음]
  6. export는 인자들은 반드시 나눠져있다.[node->command는 'char **' 이다.]
  7. export는 환경 변수를 추가할때 '='가 있는 인자는 node->is_ok=1 아니라면 node->is_ok=0이다.
  8. export는 중복되는 변수가 있다면 수정해준다.
  9. 성공시 exit_status = 0, 실패시 exit_status = 1

unset

  1. unset은 인자없이 들어왔을 때 행동없이 끝난다.
  2. unset은 인자가 여러개 들어올 수 있다.
  3. unset은 인자가 환경변수에 없다면 행동없이 끝난다.
  4. unset는 인자의 첫 문자가  알파벳 혹은 '_'이 아니면 "minishell: unset: `인자': not a valid identifier"
  5. unset는 인자의 첫 문자를 제외하고 숫자가 들어갈 수 있다.['_'이외의 특수문자가 들어가면 3번의 오류출력을 따른다.]
  6. 성공시 exit_status = 0, 실패시 exit_status = 1

echo

  1. echo는 인자없이 들어왔을 때 개행출력.
  2. echo는 인자가 여러개 들어올 수 있다.
  3. 내 생각엔 그냥 출력하면 될듯?[이미 tokenizing에서 "와 '구분을 전부 해놨음]
  4. exit_status = 0;

exit

  1. exit는 인자가 없으면 0
  2. exit는 인자는 256으로 나눈 나머지가 exit_status
  3. exit {인자} -> 인자가 숫자가 아니면 "minishell: exit: {인자}: numeric argument required" [exit_status = 255]
  4. exit는 여러개의 인자가 들어오면 "minishell: exit: too many arguments" [exit_status = 1]
  5. 3번이 4번보다 우선순위이다.
  6. exit는 single command일때 에러가 나도 exit를 출력하고 종료된다.[multi command일때는 에러만 출력한다.]
  7. 음수가 들어올 수 있다. 음수일 땐 256을 뺀 값이다.
  8. exit는 인자의 범위가 long long의 범위이다.[넘어가면 3번의 결과]

 

혼자 정리한 builtin_function은 여기까지지만 아직 찾을것이 많은 것 같다.

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

CPP [OOP 상속성]  (0) 2022.02.15
MINIRT(1)  (0) 2022.01.12
python Dict, set 활용  (0) 2021.10.15
Philosophers  (2) 2021.07.09
Push_swap  (0) 2021.06.24
# Chapter04_04

#   시퀀스 형 : 순서가 있고 번호가 나열됨
#   컨테이너(Container : 서로 다른 자료형[List, Tuple, collections.deque])
#   플랫(Flat : 한 개의 자료형[str, bytes, bytearray, array.array, memoryview])
#   가변(mutable) : List, bytearray, array.array, memoryview, collections.deque
#   불변(immutable) : Tuple, str, bytes
#   해쉬테이블(HashTable) -> 적은 리소스로 많은 데이터를 효율적으로 관리
#   Dict -> Key 중복허용 X, Set -> 중복 허용 X
#   Dict 및 Set 심화

#   immutable Dict
from types import MappingProxyType

d = {'key1': 'value1'}

# Read Only
d_frozen = MappingProxyType(d)

print(d, id(d))
print(d_frozen, id(d_frozen))

# 수정 가능
d['key2'] = 'value2'
print(d)

# 수정 불가 -> immutable이기 때문
#d_frozen['key2'] = 'value2'

# Set
s1 = {'Apple', 'Orange', 'Orange', 'kiwi', 'apple'}
s2 = set(['Apple', 'Orange', 'Orange', 'kiwi', 'apple'])
s3 = {3}
s4 = set() # Not {} <- <class 'dict'>
s5 = frozenset({'Apple', 'Orange', 'Orange', 'kiwi', 'apple'})

s1.add('melon')

print(s1)

# 추가 불가
# s5.add('melon') # Fronze으로 Read Only기 때문

print(s1, type(s1))
print(s2, type(s2))
print(s3, type(s3))
print(s4, type(s4))
print(s5, type(s5))

# 선언 최적화
# 바이트 코드 -> 파이썬 인터프리터 실행
from dis import dis

print('----------------------')
print(dis('{10}'))
print('----------------------')
print(dis('{set(10)}'))

# Set Comprehension

print('---------------------')
from unicodedata import name
print({name(chr(i), ) for i in range(0,256)})

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

MINIRT(1)  (0) 2022.01.12
Minishell(3)  (0) 2021.12.20
Philosophers  (2) 2021.07.09
Push_swap  (0) 2021.06.24
Minitalk(2)  (0) 2021.06.17

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

2 서클의 마지막 과제 push_swap을 시작해보자!!!

평가를 다니면서 많이 봤던 과제로 정렬을 하는데 2개의 stack에서 서로 상호작용을 하면서 여러 가지의 경우를 갖는다.

그리고 최적의 정렬 순서를 찾아 출력하는 것이 목표로 확인했다.

 

이제 자세히 알아보자!

 


 

  1. subject 탐색
  2. Algorithm
  3. bonus

 


1. subject 탐색

MANDATORY PART

 

우리는 a, b의 stack을 갖고 있다.

 

먼저 a에 무작위의 수가 들어가 있고 b는 비어있다.

그리고 우리는 다음과 같은 11개의 연산을 할 수 있다.

 

  • sa : stack a의 맨 위의 두 원소를 swap 한다.
  • sb : stack b의 맨 위의 두 원소를 swap 한다.
  • ss : sa, sb를 동시에 실행한다.
  • pa : stack b의 맨 위의 원소를 stack a의 맨 위에 넣는다. (stack b가 비어있다면 X)
  • pb : stack a의 맨 위의 원소를 stack b의 맨 위에 넣는다. (stack a가 비어있다면 X)
  • ra : stack a의 맨 위의 원소를 맨 아래로 보낸다.
  • rb : stack b의 맨 위의 원소를 맨 아래로 보낸다.
  • rr : ra, rb를 동시에 실행한다.
  • rra : stack a의 맨 아래의 원소를 맨 위로 보낸다.
  • rrb : stack b의 맨 아래의 원소를 맨 위로 보낸다.
  • rrr : rra, rrb를 동시에 실행한다.

우리는 프로그램을 실행하면 입력 인자를 받고 stack a에 넣는다. (입력 인자의 첫 번째가 stack의 맨 위)

출력되는 목록은 개행 문자(\n)로 구분되며 stack a를 최적 정렬방법의 목록을 출력해야 한다.

 

Error 상황

숫자 이외에 다른 문자가 입력되었을 때.

INT의 범위를 넘어간 숫자가 입력되었을 때.

입력된 문자에 중복된 숫자가 있을 때.

 

BONUS PART

 

위의 MANDATORY PART에서 만든 프로그램을 check 하는 프로그램 checker를 만든다.

 

checker 또한 정수의 리스트 형태로 된 stack a를 입력 인자로 받고 명령어들을 표준 입력으로 읽는다.

그리고 표준 입력으로 읽힌 문자들을 읽고 stack a를 명령대로 움직여서 정렬이 되어있는지 확인하고 stack b가 비어있다면 "OK" 정렬되지 않거나 stack b가 비어 있지 않다면 "KO"를 출력한다.

 

Error 상황

숫자 이외에 다른 문자가 입력되었을 때.

INT의 범위를 넘어간 숫자가 입력되었을 때.

입력된 문자에 중복된 숫자가 있을 때.

표준 입력으로 받은 명령어가 규정된 명령어가 아닐 때.

 


2. Algorithm

 

우리는 이미 여러 가지의 정렬 방식을 알고 있다.

예를 들어서 더블 소트, 퀵 소트, 선택 정렬, 삽입 정렬, 합병 정렬 등등...

하지만 우리는 단순히 정렬을 하는 것이 아니라 위의 11가지 방법을 이용하여 최적의 방법으로 정렬을 해야 한다.

 

자 우리는 그럼 어떤 식으로 접근을 해야 맞는 걸까?

문득, 이전 피신에서 했던 백트래킹이 생각난다.

 

뭔가 백트래킹은 조금 과정이 복잡할 거 같으면서 효율성이 떨어질 것 같은 느낌이다.

 

문득 검색하다 보니 Deque에 대한 이야기들이 나오는데 한번 알아보자!

 

Deque(Double Ended Queue)

즉, 이름과 같이 끝나는 곳이 두 개인 Queue인데 앞과 뒤에서 시작과 끝이 있다는 이야기인 듯하다.

 

구조체 선언은 아래와 같이 된다.

typedef struct	s_node
{
	int	Item;
  	t_node	*prev;
    	t_node	*next;
}		t_node;

typedef struct	s_deque
{
	t_node	*header;
  	t_node	*tailer;
}		t_deque;

이렇게 보면 전체적인 구성은 t_deque에 처음과 끝이 존재하고 header에는 prev가 NULL, tailer에는 next가 NULL이다.

그럼 이제 어디까지 손을 댈 수 있느냐 하면 prev와 next가 존재하니까 맨 앞 두 개, 맨 뒤 두 개까지 건드릴 수 있다는 뜻이다.

그럼 우리가 사용할 연산은 구현이 가능하다는 뜻이다.

 

일단은 입력 인자를 받고 숫자 외의 문자가 들어왔을 때, 중복된 숫자가 있을 때, INT의 범위를 넘어갔을 때의 로직을 짜 보자!

 

로직을 다 짰다.

그러고 나서 이제 오류가 없는 t_deque a이 존재하는데 이것을 어떤 알고리즘에 의해서 정렬을 시킬지가 의문인 것이다!

 

그전에 우리가 명령어를 먼저 만들어 보자.

 

크게 잡는다면 s, p, r, rr이 있는데 하나씩 생각해보자.

 

s의 경우는 맨 위가 그 아래의 원소를 swap 하는 것이다.

그렇다면 수정이 들어가야 하는 원소는 총 3개이다.

노드가 1=>2=>3라고 가정하면

1->next는 3을 가리키고 1->prev는 2를 가리킨다.

2->next는 1을 가리키고 2->prev는 NULL를 가리킨다.

3->prev는 2를 가리켜야 한다.

 

노드 2=>1=>3

 

p의 경우는 pa로 예시를 들면 b의 맨 위의 원소를 a의 맨 위로 옮기는 것이다.

a 헤더의 노드가 1=>2, b 헤더의 노드가 3=>4라고 가정하면

3->next는 1을 가리키고 3->prev는 NULL을 가리킨다.

1->prev는 3을 가리키고 4->prev는 NULL을 가리킨다.

 

a 헤더의 노드 3=>1=>2

b 헤더의 노드 4

 

r의 경우는 맨 위의 원소를 맨 아래로 보내는 것이다.

노드가 1=>2=>3=>4라고 가정하면

1->prev는 4를 가리키고 1->next는 NULL을 가리킨다.

4->next는 1을 가리키고 2->prev는 NULL을 가리킨다.

 

노드  2=>3=>4=>1

 

rr의 경우는 맨 아래의 원소를 맨 위로 보내는 것이다.

노드가 1=>2=>3=>4라고 가정하면

4->prev는 NULL을 가리키고 4->next는 1을 가리킨다.

1->prev는 4를 가리키고 3->next는 NULL을 가리킨다.

 

노드 4=>1=>2=>3

 

여기서 한 가지 더 집고 넘어가자면 입력 인수가 무조건 3개 이상이 여야 하는 것이 아니라

2개일 때의 경우가 3가지로 나뉜다.

./실행파일 숫자(1개)  =>   개행만 출력

./실행파일 문자(1개)  =>   에러 출력

./실행파일 "문자열"(1개)  => 문자열에서의 에러 체크 후 스플릿으로 나눈 뒤 정상출력

 

 

이제 정렬에 대한 것을 보자!

 

정렬은 많은 분들이 퀵 소트를 기본으로 진행했다.

퀵 소트를 간단히 설명하면 하나의 피봇을 잡고 피봇보다 큰 구역과 작은 구역으로 나눈다.

그리고 두 개의 구역에서 또 다른 피봇을 잡고 똑같이 진행한다.

이후 정렬이 구역의 수와 길이가 같다면 끝!

 

또 다른 정렬 중 머지 소트도 알아보자!

머지 소트는 전체 원소를 독립적으로 나누고 이를 각각의 대소를 따지면서 병합한다.

글로만 본다면 이해가 안 가니 위키를 참고하기로 하자...! 위키 최고

 

https://ko.wikipedia.org/wiki/%ED%80%B5_%EC%A0%95%EB%A0%AC

 

퀵 정렬 - 위키백과, 우리 모두의 백과사전

퀵 정렬(Quicksort)은 찰스 앤터니 리처드 호어가 개발한 정렬 알고리즘이다. 다른 원소와의 비교만으로 정렬을 수행하는 비교 정렬에 속한다. 퀵 정렬은 n개의 데이터를 정렬할 때, 최악의 경우에

ko.wikipedia.org

https://ko.wikipedia.org/wiki/%ED%95%A9%EB%B3%91_%EC%A0%95%EB%A0%AC

 

합병 정렬 - 위키백과, 우리 모두의 백과사전

합병 정렬 또는 병합 정렬(merge sort)은 O(n log n) 비교 기반 정렬 알고리즘이다. 일반적인 방법으로 구현했을 때 이 정렬은 안정 정렬에 속하며, 분할 정복 알고리즘의 하나이다. 존 폰 노이만이 1945

ko.wikipedia.org

 

왜 이 두 개의 정렬법을 사용했을까?

이는 시간 복잡도와 관련되어 있다.

시간 복잡도에 대해서 알아보자!

 

시간 복잡도는 말 그대로 "이 로직을 돌리는 데 시간이 얼마나 걸리느냐"라는 것이다.

우리가 코드 로직에서 하나의 반복문을 n번 돌린다면 시간 복잡도는 O(n)이다.

그리고 조건문이 있다면 조건문의 실행 시간을 전부 더한 것을 C라고 생각하면 시간 복잡도는 O(C*n)이다.

하지만 시간 복잡도는 상수를 무시한다. 즉, O(n)이다.

간혹 가다 log가 나오는데 이것은 보통 $log_2$이다.

몇 가지 수학적 지식이 들어가는데 그냥 2로 나누면서 반복문을 돌렸을 때 나온다?라고 생각하면 된다.

더 깊이 들어가기 싫다!!!!!!!!! 수학 멈춰!!!!!!!!!!!!!!!!!

 

시간 복잡도의 표현중에서도 표기법이 다르다.

"빅-오 표기법, 오메가 표기법, 세타 표기법"이 있는데 차례로 최악, 최상, 평균이다.

 

퀵 소트랑 머지 소트는 시간 복잡도가 어떻게 되는 걸까?

정렬 종류 평균 최선 최악
퀵소트 $O(nlogn)$ $O(nlogn)$ $O(n^2)$
머지소트 $O(nlogn)$ $O(nlogn)$ $O(nlogn)$

자 위의 표를 보면 최대한 시간 복잡도가 좋은 것을 사용해야 최고의 방법을 찾지 않을까 한다.

 

퀵 소트에 대해 자세히 알아보자!

 

퀵 소트는 pivot을 기준으로 두 개의 구역으로 나눠서 pivot의 정확한 위치를 알게 하는 것이 첫 번째 목표이다.

많은 요소가 존재할 때를 생각해보면 처음에는 pivot을 1개로 잡고 분리 진행한 후 pivot을 정확한 위치에 존재하게 한다.

그리고 두 개의 구역으로 존재한 각각의 구역에서 각각의 pivot을 잡고 그 두 개의 pivot을 정확한 위치에 존재하게 한다.

이 과정을 반복한다면 마지막엔 결국 n개의 구역에 있는 원소가 1개가 되는데 이때 정렬이 완료가 된다.

 

우리가 이 부분에서 확실히 구현 가능한 부분은 pivot의 위치를 고정한다. 그리고 거의 마지막 과정(즉, 5개 이하의 원소가 남았을 때)에서는 정렬시킬 수 있다.

우리가 생각해봐야 할 것은 두 개의 deque으로 구현해야 하기 때문에 구현 과정은 거의 2 개의 서브 노드가 독립적인 이진트리와 같은 형상으로 구현되어야 한다는 것이다.

 

작성하는 걸 까먹었다... ㅋㅋ

 

전체적으로 진행되는 방법에 대해서만 쓰겠다.

 

 

  1. 입력인자를 받아 에러 체크를 한다.
  2. 입력인자들을 deque a에 차곡차곡 쌓는다.
  3. 정렬된 배열 sorted를 quick sort로 생성한다.
  4. deque a의 size를 판단하고 5개 이하면 내가 작성한 로직대로 진행한다.
  5. 5개 이상이면 a_to_b로 가서 pivot 2개를 생성한다.(이때 pivot을 잡는 방법은 정해진 size에서 sorted에서 3등분을 해서 2개를 잡는다.)
  6. a_to_b에서는 큰 pivot보다 크거나 같을 때 a의 뒤로 보낸다.
  7. 작은 pivot보다 작거나 같으면 b로 보낸다.
  8. 큰 pivot보다 작지만 작은 pivot보다 크다면 b로 보낸뒤 뒤로 보낸다.
  9. 다 했다면 a, b에서 뒤로 보냈던 인자들을 복구한다.
  10. 그리고 a_to_b, b_to_a, b_to_a로 선언한다.
  11. b_to_a는 a_to_b와 같이 pivot을 2개 생성한다.
  12. 작은 pivot보다 작거나 같으면 b의 뒤로 보낸다.
  13. 큰 pivot보다 크다면 a로 보내고 뒤로 보낸다.
  14. 작은 pivot보다 크고 큰 pivot보다 작거나 같으면 a로 보낸다.
  15. a_to_b로 보낸다.
  16. 다했다면 a, b에서 뒤로 보냈던 인자들을 복구한다.
  17. a_to_b, b_to_a를 선언한다.
  18. 이때 a_to_b와 b_to_a에서 size가 3개 이하라면 내가 따로 작성한 로직대로 진행하도록 한다.

이것이 끝이다.

여기서 조금 주의해야 할 것은 Error의 출력은 표준에러로 출력해야 한다는 점이다.

그리고 우리가 생각해봐야할 것은 rr, ss, rrr의 사용은 기본이지만 연속해서 나오는 명령어들이라는 것이다.

예를 들어 sa이후 sa가 나온다면 굳이 표시하지 않아도 된다. 이런 경우를 생각해봤을 때, 나는 내 로직에서 나오는 명령어들에서 r이후 rr의 명령어가 가장 많이 나오는 것으로 분석했고 ra 이후 rra가 나오거나 rb 이후 rrb가 나오는 경우에 명령어를 생략하기로 했다.


3.bonus

 

bonus는 제공하는 checker의 작성이다.

사실 checker의 로직에서는 특별히 사용될 함수는 read뿐이다.

즉, 우리가 push_swap에서 사용했던 함수를 조금 수정하고 get_next_line를 가져와서 사용하면 되는 것인다.

이번에 Norm의 버전이 바뀌어서 새로 작성하기로 했다.

시험에서의 get_nest_line과 유사하게 표준입력(fd=0)으로 받고 종료되었다면 끝내고 정렬이 되었는지 확인하는 것이다.

표준입력으로 받고 개행문자(\n)로 받았다면 그 이전까지 보내준다. 그리고 명령어들을 확인하고 수행해준다. 명령어중에 속한것이 없다면 Error를 표준에러로 출력한다.

끄읕~~~~~~~~~!

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

python Dict, set 활용  (0) 2021.10.15
Philosophers  (2) 2021.07.09
Minitalk(2)  (0) 2021.06.17
Minitalk(1)  (4) 2021.06.13
Fract-ol  (6) 2021.06.08

여기서 다시 시작하자!


3. sigaction

sigaction은 아래와 같이 이뤄진다.

 

int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

 

여기서 우리는 struct sigaction라는 구조체를 넘겨주는데 이 구조체를 알아보자.

 

struct sigaction{
	void		(*sa_handler)(int);
        void		(*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t	sa_mask;
        int		sa_flags;
        void		(*sa_restorer)(void);
};

sa_handler

핸들러 함수에 대한 포인터이다. 우리가 signal에서 사용하던 핸들러 함수와 같다.

sa_sigaction

시그널 핸들러를 실행하는 다른 방법인데 추가적인 2개의 인수를 가져간다.

siginfo_t *

받은 시그널에 대한 정보를 제공한다.

sa_mask

시그널이 막힐지를 명시적으로 설정할 수 있도록 허용한다.

sa_flags

핸들링 프로세스의 동작 변경을 허용한다. sa_sigaction 핸들러를 사용하기 위해선 여기에 SA_SIGINFO를 넣어야 한다.

 

자 이제 mask를 안 했을 때의 경우를 보자!

대부분 핸들러를 제공하지 않고 시그널을 막지 않았을 경우 default로 지정된 동작은 프로세스의 종료이다.

SIGSEGV, SIGILL, SIGABRT 류의 시그널은 코어 덤프와 함께 종료한다.

SIGCHLD류의 시그널은 무시된다.

 

우리는 SIGUSR1, SIGUSR2만 사용해야 하는데 이 둘은 default로 지정된 동작은 프로세스의 종료이다.

즉, 우리는 flags에 SA_SIGINFO를 사용해야 한다.

그리고 우리는 sa_mask로 막을 signal은 따로 존재하지 않는다. 어차피 USR1과 USR2 만 사용할 것이기 때문!!!!!!

 

void	handler(int signo, siginfo_t *info, void *context)
{
	if (signo == SIGUSR1)
		write(1, "1", 1);
    else if (signo == SIGUSR2)
		write(1, "0", 1);
}

int		main(void)
{
	struct sigaction act1;

	act1.sa_sigaction = handler;
	act1.sa_flags = SA_SIGINFO;
	if (sigaction(SIGUSR1, &act1, NULL) != 0)
	{
		printf("Sigaction Error");
		exit(1);
	}
	if (sigaction(SIGUSR2, &act1, NULL) != 0)
	{
		printf("Sigaction Error");
		exit(1);
	}
	while (1)
		;
	return (0);
}
void	post(int pid, int idx)
{
	if (idx == 1)
	{
		printf("1");
		kill(pid, SIGUSR1);
	}
	else if (idx == 0)
	{
		printf("0");
		kill(pid, SIGUSR2);
	}
	else
	{
		printf("\n");
		kill(pid, 0);
	}
	usleep(1000);
}

void	conv_and_post(int pid, int c, int num)
{
	if (!c)
	{
		while (num != 8)
		{
			post(pid, 0);
			num++;
		}
		return ;
	}
	else
	{
		conv_and_post(pid, c / 2, ++num);
		post(pid, c % 2);
	}
}

void	post_office(int pid, char *str)
{
	int		c;
	int		num;
	int		idx;

	c = 0;
	idx = -1;
	while (str[++idx])
	{
		num = 0;
		c = (int)str[idx];
		conv_and_post(pid, c, num);
		post(pid, -1);
	}
}

int		main(int argc, char *argv[])
{
	if (argc != 3)
		return (0);
	else
	{
		printf("PID : %s, Massage : %s\n", argv[1], argv[2]);
		post_office(ft_atoi(argv[1]), argv[2]);
	}
	return(0);
}

 

위와 같은 코드 1번째 코드를 실행한 뒤, 2번째 코드에서 PID와 message를 입력 인자로 입력해야 한다.

 

그리고 우리는 글자를 하나하나 보내야 하는데 구분할 수 있는 것은 SIGUSR1과 SIGUSR2 뿐이므로 1, 0

즉, 이진법으로 생각해서 한 글자에 8개의 0과 1의 조합을 가져갈 수 있다. 또한, 아스키코드와 확장 아스키까지 0~255의 수는 2진법으로 바꿔도 8개의 글자를 넘지 않는다.

자! 이게 무슨 말이냐!

글자 하나당 1바이트! 1바이트는 8비트이다.

즉, 우리는 글자를 2진법으로 바꿔서 8비트만큼 쪼개서 보내 뒤 이를 받는 곳에서 다시 8비트를 1바이트로 바꿔서 글자를 복원하면 된다는 뜻이다.

 

두 번째 코드 블록을 보면 signal을 kill 해주고 나서 usleep이 있다.

이 이유를 한 가지 예시로 들면 우리가 밥을 먹을 때 무작정 쉴 틈 없이 입에 집어넣는다고 다 먹는 게 아니지 않은가?

기본적으로 씹고 삼키겠지만 그래도 컴퓨터는 씹는 건 안 하니까 삼킬 때까지의 딜레이를 발생시켜줘야 정상적으로 먹는 것이다.

예시를 벗어나서 시그널을 보내는 갭이 시그널을 받는 갭보다 짧다는 것이다.

 

그럼 이제 글자를 복원하는 작업을 해야 하는데 한 가지 문제가 있다.

0, 1을 받아와서 저장하는 곳이 문제가 된다. 매번 handler에서 선언을 해주면 새것이 되고 그렇다고 받아서 사용하기엔 정해져 있는 틀이 있어서 그러질 못한다.

그래서 전역 변수를 사용해야 하는데 되도록이면 꼭 사용해야 할 곳에만 사용을 해야 하는 것이 규칙이라 조금 애매하다. 다른 방법이 없을까...

void *context

위 부분을 주목해봤지만 이것은 신호 인터럽트 당시 문맥 정보가 포함된 ucontext_t를 가리킨다고 한다.

인터럽트? 이게 무엇인가 하면

interrupt는 마이크로프로세서(CPU)가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치에 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말한다. 위키 최고

 

음... 지금 현재 생각나는 방법은 딱히 없어 보인다.

 

전역 변수를 사용하여 생각해보면

우리가 하나씩 받아온 시그널을 전부 다 받고 8개씩 잘라서 복원하는 방법이 있고

시그널이 8개가 차게 되면 1글자를 복원하고 초기화를 해서 다시 받는 방법이 있다.

 

첫 번째 방법은 한 번에 처리를 하는 것에 장점이 있다만 시그널이 끝났다는 것을 어떻게 해야 할지 모른다.

두 번째 방법은 하나하나 처리를 하다 보니 조금 애매한 감이 있지만 시그널이 전부 들어온 이후에도 또 보내서 확인할 수 있다.

 

두 번째 방법으로 해보자!

진행하다 보니 하나의 큰 문제점이 생겼다. 처음부터 피하려고 배제하고 넘겼던 부분 중 하나가 문장이 끝났을 때다.

두 번째 방법으로 진행하니 크게 문제점이 생기는 것이 위의 경우이다. 여러 개의 시그널을 보낸다 하지만 한 덩이의 시그널을 보내고 그 시그널 덩어리가 끝이 났을 때 개행으로 진행을 싶은 욕구가 마구 차오른다. 출력된 보기가 싫어서

 

또한, 우리가 저렇게 보낸 것들의 끝을 안다면 한 번에 출력할 수 있는 상황을 만들어 낼 수 있기 때문에 방법을 찾아서 진행하면 더욱 수월할 것 같다!

 

방법은 마지막에 항상 127을 뜻하는 01111111을 보내주는 방법이다.

자 이렇게 생각했을때 우리는 2진법으로 변환된 시그널을 받고 다시 10진수로 바꾸기보단 비트 연산을 통해 받는다면 더욱 빠르게 연산이 실행될 수 있다.

 

비트 연산에 대한 이야기는 아래 코딩도장에서 너무 잘 다뤄주고 있다.

https://dojang.io/mod/page/view.php?id=175 

 

C 언어 코딩 도장: 23.4 비트 연산 후 할당하기

이번에는 비트 연산자와 할당 연산자를 함께 사용해보겠습니다. a &= b a |= b a ^= b a <<= b a >>= b bitwise_asign.c #include int main() { unsigned char num1 = 4; // 4: 0000 0100 unsigned char num2 = 4; // 4: 0000 0100 unsigned char n

dojang.io

 


4. 구현

 

전체적인 흐름은 이러하다.

server는 실행될 때 본인의 pid를 출력해준다.

이후 client는 server의 pid와 송신할 문자열을 입력 인자로 받는다.

그리고 송신할 문자열들을 ASCII 문자로 바꾸고 다시 2진수로 바꿔서 앞에서부터 server에게 송신을 해준다.

그리고 송신할 문자열이 끝났다면 127을 뜻하는 01111111을 송신하고 문자열이 끝났음을 알린다.

server에서는 수신된 문자열을 받고 count(1~7) 당 해당하는 0 또는 1을 넣어준다.(비트 연산으로)

여기서 만일 수신된 문자열이 99개가 넘어간다면 99개의 문자열을 출력해준다.

그리고 다시 수신을 받는다.

위의 상황이 아니라면 127을 뜻하는 문자를 받았을 때 끝까지 출력한 후 개행을 출력해주고 처음의 상태로 돌아간다.

코드의 로직은 아래와 같다.

server.c

void	*reset(void *b, size_t len)
{
	unsigned char	*temp;
	unsigned long	i;

	temp = (unsigned char*)b;
	i = -1;
	while (++i < len)
		temp[i] &= 0;
	return (temp);
}

void	handler(int signo, siginfo_t *info, void *context)
{
	static unsigned char	buf[100];
	static int				idx;
	static int				count;

	(void)info;
	(void)context;
	if (--count == -1)
	{
		count = 7;
		idx++;
	}
	buf[idx] &= ~128;
	if (signo == SIGUSR1)
		buf[idx] |= (1 << count);
	else if (signo == SIGUSR2)
		buf[idx] &= ~(1 << count);
	if (buf[idx] == 127 || idx == 99)
	{
		write(1, buf, idx + 1);
		if (buf[idx] == 127)
			write(1, "\n", 1);
		reset(buf, 100);
		idx = 0;
	}
}

int		display_pid()
{
	char *pid;

	if (!(pid = ft_itoa(getpid())))
		return (0);
	write(1, "My PID is ", 10);
	write(1, pid, ft_strlen(pid));
	write(1, "\n", 1);
	free(pid);
	return (1);
}

int		main(void)
{
	struct sigaction	act1;

	act1.sa_sigaction = handler;
	act1.sa_flags = SA_SIGINFO;
	if (!(display_pid()))
	{
		write(1, "PID malloc error", 16);
		exit(1);
	}
	if (sigaction(SIGUSR1, &act1, NULL) != 0)
	{
		write(1, "Sigaction Error", 15);
		exit(1);
	}
	if (sigaction(SIGUSR2, &act1, NULL) != 0)
	{
		write(1, "Sigaction Error", 15);
		exit(1);
	}
	while (1)
		;
	return (0);
}

client.c

void	post(int pid, int idx)
{
	if (idx == 1)
		kill(pid, SIGUSR1);
	else if (idx == 0)
		kill(pid, SIGUSR2);
	usleep(100);
}

void	conv_and_post(int pid, int c, int num)
{
	if (!c)
	{
		while (num < 8)
		{
			post(pid, 0);
			num++;
		}
		return ;
	}
	else
	{
		conv_and_post(pid, c / 2, ++num);
		post(pid, c % 2);
	}
}

void	post_office(int pid, char *str)
{
	int		c;
	int		num;
	int		idx;
	int		last;

	c = 0;
	idx = -1;
	last = 7;
	while (str[++idx])
	{
		num = 0;
		c = (int)str[idx];
		conv_and_post(pid, c, num);
	}
	post(pid, 0);
	while (last--)
		post(pid, 1);
}

int		main(int argc, char *argv[])
{
	if (argc != 3)
		return (0);
	else
	{
		write(1, "Client PID : ", 13);
		write(1, argv[1], ft_strlen(argv[1]));
		write(1, "\nMassage : ", 11);
		write(1, argv[2], ft_strlen(argv[2]));
		write(1, "\n", 1);
		post_office(ft_atoi(argv[1]), argv[2]);
	}
	return (0);
}

 


*. Bonus

위 코드에서 보너스를 구현하려 한다.

보너스는 두 가지 상황을 더 준다.

첫 번째 수신이 잘 되었는지 확인하는 시스템을 만든다.

두 번째 유니코드 문자도 출력하게 한다.

 

첫 번째 문제는 다음과 같이 생각했다.

우선 client는 server에게 client의 pid를 송신해주어야 한다.

왜냐하면 수신이 잘 되었으면 1이라는 신호를 받고 종료를 시키게 한다면 "done!"과 같은 문자열을 출력해줄 수 있기에 이를 수신이 잘 되었다는 시스템을 확인할 수 있게 된다.

또한, client의 pid는 언제 송신해야 하는가? 이는 127을 수신하기 전에 하면 된다.

이 또한, 수신이 잘 돼야만 들어올 수 있기 때문에 127을 받기 전에 해야 한다고 생각했다.

즉, '송신할 문자열 -> pid -> 127'이 순서가 된다.

이렇게 하면 server는 pid를 받고 127을 받으면 그 pid를 통해 SIGUSR1을 송신하게 된다.

이후 client는 SIGUSR1을 받고 수신이 잘 되었구나 생각한 뒤 끝나게 하면 된다.

여기서 한 가지 문제가 있다. 수신을 "잘 받았구나!"라고 생각했을 땐 확인이 되지만 수신이 정상적이지 않다면 어떻게 해야 할까?

 

두 번째 문제는 사실 UNICODE에 대해 생각해봐야 한다.

우리는 UNICODE가 어떻게 이뤄지는지를 먼저 알아야 한다.

여기서 우리는 UTF-8, UTF-16 UTF-32 등을 본 적이 있을 것이다.

이를 쉽게 생각해서 'UTF-숫자'에서 숫자는 비트를 생각하면 된다. 즉, 1 글자당 32비트를 사용하여 프린트되게 하는 것이다.

근데 생각해보면 1글자당 32비트라면 문자열이 엄청나게 크게 전송이 될 것이다. 1글자당 2진수 32개의 문자열을 갖는 것이니까 말이다.

그래서 UTF-16이 나왔다. 하지만  UTF-16은 이런 문제가 있다. ASCII 문자와 호환 문제가 있다. 최소 2바이트를 사용하기 때문이다.

또한 2바이트를 사용해 문자를 표현하기에 엔디안 문제가 발생한다.

엔디안은 쉽게 말해 한국의 책은 왼쪽이 첫 페이지로 오른쪽으로 진행한다. 하지만, 일본의 책은 오른쪽이 첫페이지로 왼쪽으로 진행한다.

즉, 순서를 이야기하는 것이다. 큰 것을 첫 번째에 두느냐 마지막에 두느냐에 따라 엔디안이 빅이냐 리틀이냐로 나뉜다.

다시 돌아가서! 그래서 UTF-8이 나타났다. 이는 1~4바이트로 인코딩 될 수 있으며 ASCII 문자와 호환이 된다.

즉, 이는 ASCII 문자의 조합으로 영어 이외에 문자를 출력하는 것이다.

이 조합은 아래와 같이 나타낸다.

U+0000~U+007F : UTF-8 Encoding 0xxx xxxx 8bit (총 1byte)

U+0080~U+07FF : UTF-8 Encoding 110x xxxx 10xx xxxx 16bit (총 2byte)

U+0800~U+7FFF : UTF-8 Encoding 1110 xxxx 110x xxxx 10xx xxxx 24bit (총 3byte)

U+10000~U+10FFFF : UTF-8 Encoding 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx 32bit (총 4byte)

 

자 이제 생각해보자!

우리가 어떻게 유니코드를 저런 방식으로 출력을 할 수 있을까?

 

우리는 유니코드에서 8bit는 7개, 16bit에서는 11개, 24bit에서는 15개, 32bit에서는 21개의 신호를 융합해서 보내야 한다.

즉, 우리가 생각했을 때 8bit는 그냥 ASCII 문자로 출력을 하되 만일 c가 128을 넘어간다면 유니코드로 넘기는 것으로 생각해보자.

왜냐하면 1000 0000은 128이기 때문이다.

 

우리가 생각했을 때 범위를 계산한다면

16bit는 $2^8$ ~ $2^{12} - 1$

24bit는 $2^{12}$ ~ $2^{16} - 1$

32bit는 $2^{16}$ ~ $2^{22} - 1$

이렇게 생각해서 분배한 뒤 2진수로 변환한 뒤 각 양식에 맞춰서 보내주면 된다!


결론!

 

유니코드 처리하는 방법은 만약에 유니코드 중 ☠ 이런 문양을 보여주고 싶다면

☠ 는 9760의 유니코드를 갖고 있는데 이것은 226 | 152 | 160 와도 같다.

무슨 말이냐 하면 9760을 이진수로 변환한다면 1110 0010 1001 1000 1010 0000으로 나온다!

즉, 1110 0010 | 1001 1000 | 1010 0000 = 226 | 152 | 160

이것을 총 3개의 char로 받아서 출력하니까 이것을 하나씩 받아서 출력하면서 끊기지 않으면 된다.

예를 들어 우리가 98번째에 ☠를 출력하고 싶은데 그렇다면 226에서 끊기니 그전에 출력하고 이후로 들어가야 한다는 말이다.

우리는 그럼 이 문제를 처리하기 위해 어떻게 해야 하는가????

우리는 시그널을 받아 저장한 문자열이 90번째가 넘고 그때의 문자가 192(1100 0000)를 넘어가면 그때를 temp로 저장하고 이전까지만 출력한 뒤, 문자열의 첫 번째를 temp로 선언해주면 이후 결과 출력이 가능하다!

송수신 관계는 받았다면 받은 것을 확인만 하는 것으로 결과를 내고 끝냈다!

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

python Dict, set 활용  (0) 2021.10.15
Philosophers  (2) 2021.07.09
Push_swap  (0) 2021.06.24
Minitalk(1)  (4) 2021.06.13
Fract-ol  (6) 2021.06.08

하나도 모르는 과제를 진행하는 것이 가장 많은 것을 얻어간다!

 


 

진행과정

 

 

  1. 사용할 수 있는 내장 함수에 대한 사용법 익히기
  2. 과제가 원하는 방식 찾기
  3. sigaction
  4. 구현

 

 


 

1. 사용할 수 있는 내장 함수에 대한 사용법 익히기

 

사용할 수 있는 내장 함수들은 아래와 같다.

  • write
  • signal
  • sigemptyset
  • sigaddset
  • sigaction
  • kill
  • getpid
  • malloc
  • free
  • pause
  • sleep
  • usleep
  • exit

 

여기서 익숙한 함수보다는 처음 보는 함수들이 더 많이 보인다. 하나씩 알아보자!

 

#inlcude <unistd.h>

write(int fd, const void *buf, size_t nbytes)

문자를 출력/입력하는 함수

 

getpid(void)

실행 중인 프로세스 ID를 구한다.

 

sleep(sec)

sec만큼 실행을 늦춘다.

 

usleep(micro sec)

micro sec만큼 실행을 늦춘다.

 

pause(void)

항상 -1을 반환하면서 errno에는 ERESTARTNOHAND(에러코드)로 설정

시그널을 수신할 때까지 대기 상태로 유지한다.

 

#include <siganl.h>

signal(int signum, void (*handler)(int))

signum 시그널 번호

void (*handler)(int) 시그널을 처리할 핸들러

시그널을 구분해서 어떻게 처리할지 확인하는 함수

 

sigemptyset(sigset_t *set)

set  시그널 집합 변수

실패할 시 -1 반환

시그널 집합 변수의 내용을 제거

 

sigaddset(sigset_t *set, int signum)

실패할 시 -1 반환

시그널 집합 변수에 signum을 추가한다.

 

sigaction(int signum, const stuct sigaction *act, struct sigaction *oldact)

act 새롭게 설정할 행동

oldact 이전에 설정한 행동

실패할 시 -1 반환

 

kill(pid_t pid, int signum)

pid 실행 중인 프로세스 ID

프로세스에 시그널을 보낸다.

 

#inlcude <stdlib.h>

아래의 함수들은 이전 과제에서 수도 없이 썼기에 스킵!

malloc

free

exit

 


2. 과제가 원하는 방식 찾기

 

진행하면서 알아야 할 signal의 성질을 먼저 알아보자.

 

프로세스가 시그널을 받게 되면 해당하는 기본적인 동작을 하거나, 무시하거나, 내가 정의한 함수를 통해 동작 방식을 바꿀 수 있다.

 

프로세스가 시그널을 받게 되었을 때, 보낸 곳에서 시그널을 제대로 받았는지 확인하지 않는다.

시그널을 처리 중일 때 다른 시그널을 받게 되면 무시된다.

 

시그널을 발생시키는 번호에서 SIGSTOP과 SIGKILL은 시그널을 받으면 프로세스를 멈추고 죽이는 것인데 우리가 사용하는 SIGINT처럼 제어할 수 있는 것이 아니다.

 

내가 본 과제에서 해야 할 것과 진행 과정을 알아보자!

우리는 client 와 server를 만들어야 하는데 이 둘은 서로 통신이 가능해야 한다.

시작할 때 server가 먼저 실행된 후에 client가 실행되어야 하는데

client가 실행되었을 땐 본인의 pid를 출력하고

입력 파라미터로 server의 pid와 전달할 문자열이 입력되어야 한다.

또한, server는 문자열을 전달받으면 출력해야 한다.

 

여기서 우리는 SIGUSR1과 SIGUSR2만 사용할 수 있다.

SIGINT, SIGQUIT등의 문자는 사용이 불가한 건가..?

 

진행함에 앞서 몇가지 실험을 해보자

실행중인 프로세스을 kill하는 예제이다.

 

카운트 다운
kill

위와 같은 결과로 실행중인 프로세스를 kill하는 법을 알아간다.

여기서 우리가 알수 있는 것은 하나의 프로세스에 signal을 주는 것은 이렇게 만들수 있다는 것이다.

 

두번째

SIGUSR1과 SIGUSR2를 사용하는 방법이다.

SIGUSR1, SIGUSR2 테스트
SIGUSR1, SIGUSR2의 signal 전달

위의 결과로 나온다.

우리는 SIGUSR1과 SIGUSR2를 이용해서 signal을 줄 수 있는 방법을 알았다.

 

그렇다면 이제 SIGUSR1와 SIGUSR2를 어떤 식으로 사용을 할 수 있을까?

 

그 전에 우리가 한가지 알아야 할 점이 있다.

 

signal과 sigaction은 기능이 비슷하다.

그래서 찾아보면 signal은 핸들러를 구축하는 오래되고 간단한 방법이지만 더이상 사용하지 않는다.

 

그 이유는 무엇이냐!

Unix에서 시그널을 받은 후에 디폴트 값으로 핸들러를 리셋해버리기 때문이다. 만약 죽는 프로세스를 잡기 위해 SIGCHLD 각각을 별도로 핸들링 할 필요가 있는 경우 경쟁이 필요하다. 이 때문에 핸들러 내부에 핸들러를 설정할 필요가 있으며 signal을 호출하기 전에 다른 시그널이 도착할 것이다. 사실 진행중인 프로젝트에는 그렇게 연관성이 있는지 모르겠고 구현이 signal로도 충분히 될 것이라 생각이 든다.

 

하지만, 현재 사용할 수 있는 함수 중 sigaction을 주었기에 사용하여 구현하는 것이 프로젝트가 원하는 바라고 생각이 든다.

 

그래서 우리는 sigaction에 대한 것을 조금 더 깊이 파고들어보자.

 

To Be Continued~

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

python Dict, set 활용  (0) 2021.10.15
Philosophers  (2) 2021.07.09
Push_swap  (0) 2021.06.24
Minitalk(2)  (0) 2021.06.17
Fract-ol  (6) 2021.06.08

첫 글을 Fract-ol로 시작하는 건 조금 늦은 감이 있는 듯하다.

하지만 이제라도 일기를 쓰고자 블로그를 개설한다!!

전적으로 내가 이해한 내용만으로 만 채울 것!!!!!

 

시작!


 

목    표

 

 

  1. Mandelbrot set, Julia set의 이해
  2. 간단한 예제들을 통해 mlx의 사용법 배우기
  3. mlx를 이용하여 Mandelbrot set, Julia set 만들어보기
  4. 색 입혀보기
  5. 확대 / 축소 구현하기
  6. Julia set 심화
  7. 또 다른 한 개의 Fractal 만들기

 

 

 

 

 

 


1. Mandelbrot set, Julia set의 이해

먼저, Mandelbrot은 빵 이름인지 julia는 무슨 사람 이름인지 모를 테니 WIKI의 힘을 빌리자!

https://ko.wikipedia.org/wiki/%EB%A7%9D%EB%8D%B8%EB%B8%8C%EB%A1%9C_%EC%A7%91%ED%95%A9

 

망델브로 집합 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 망델브로 집합(영어: Mandelbrot set)은 브누아 망델브로가 고안한 프랙탈의 일종이다. 망델브로 집합은 다음 점화식으로 정의된 수열이 발산하지 않는 성질을 갖

ko.wikipedia.org

https://ko.wikipedia.org/wiki/%EC%A5%98%EB%A6%AC%EC%95%84_%EC%A7%91%ED%95%A9

 

쥘리아 집합 - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

간단히 요약하자면 Mandelbrot set과 Julia set은 다음과 같은 점화식을 사용한다.

 

$f(z_{n}) = z_{n}^{2} + c$

 

여기서 우리는 $z_n$이 무엇인지 $c$가 무엇인지 확인하고 진행하여야 한다.

 

Mandelbrot set은 $z_{n}$이 발산하지 않게 하는 $c$의 집합이다.

Julia set은 $z_{n}$이 발산하지 않게 하는 $c$의 집합이다.

이게 뭔 소리야 하겠지만 두 집합의 초기값이 다르고 두 점화식에서의 고정된 값이 다르기에 다른 그래프가 나오는 것이다.

 

Mandelbrot set에서 $z_{0} = 0 + 0i ,  c = 좌표$이다.

Julia set에서는 $z_{0} = 좌표 ,    c = 고정된 복소수 값$이다.

 

★즐거운 수학 시간★

 

점화식은 아래와 같이 사용한다.

 

$f(z_{n}) = z_{n+1} = z_{n}^{2} + c$

 

여기서 복소수 $z_{n} = a + bi$ 라고 하고 복소수 $c = c_{Re} + c_{Im}i$ 라고 하자 ㄱㄱ

 

$f(z_{n}) = z_{n+1} = a^{2} - b^{2} + c_{Re} + (2ab + c_{Im})i$

 

즉, $z_{n+1}$의 Real은 $a^{2} - b^{2} + c_{Re}$    $z_{n+1}$의 Imaginary는 $2ab + c_{Im}$ 라고 된다.

 

여기서 발산하는 조건을 코딩으로 사용하기에는 너무 어렵다.

 

여기서 탈출 조건을 아래처럼 잡자!

  1. $|a^{2} - b^{2} + c_{Re}| > 4$
  2. iteration을 돌리되 iteration_max보다 커졌을 때 탈출한다.

두 탈출 조건 중 하나라도 만족하지 못한다면 탈출해야 한다.

하지만 두 탈출 조건에서의 이후 표현이 다르다.

 

 

조건 1을 만족하지 못했을 때는 Mandelbrot set과 Julia set에 속하지 못한다.

조건 2를 만족하지 못했을 때는 Mandelbrot set과 Julia set에 속한다. 

 

위 사항들을 조금만 더 세밀하게 본다면 조건 1을 만족하지 못했을 때 iteration의 값은 색 입히기 할 때 확실히 사용된다.

 

 

2. 간단한 예제들을 통해 mlx의 사용법 배우기

 

여러 가지 예제들을 가진 notion과 blog들이 존재한다.

따라 해야지만 mlx에 있는 함수들이 어떻게 사용되고 어떻게 진행되는지를 확인할 수 있다.

참고로 나는 이론 공부는 죽어라 싫어하는 입장이라 항상 예제들을 찾아다니는 입장이다.

 

이 예제들은 내가 정리한 것보단 블로그들의 예제들과 그 밑에 설명까지 있기에 가져다 써야겠다.

블로그 주인 분들에게 무한한 감사를!!!!!!!!

 

https://velog.io/@parksj3205/miniLibX%EB%A1%9C-%EC%9C%88%EB%8F%84%EC%9A%B0-%EC%83%9D%EC%84%B1%EA%B3%BC-%EA%B0%84%EB%8B%A8%ED%95%9C-%EB%8F%84%ED%98%95-%EA%B7%B8%EB%A6%AC%EA%B8%B0

 

[miniRT] 1. miniLibX로 윈도우 생성과 간단한 도형 그리기

miniRT/cub3d 프로젝트는 miniLibX 그래픽 라이브러리를 사용하여 구현합니다.그러므로 먼저 miniLibX로 윈도우를 생성하고 간단한 도형을 그려보겠습니다.1\. 그래픽 시스템 연결우선, 작성한 프로그램

velog.io

 

https://velog.io/@naranghae/miniRT-%EC%A7%84%ED%96%89-1-mlx-%ED%8A%9C%ED%86%A0%EB%A6%AC%EC%96%BC-1

 

miniRT 진행(1) mlx 튜토리얼 -1

검은 화면이 만들어진다. mlx_new_window(mlx_ptr, 500, 500, "mlx_test"); 에서 500은 각각 x, y 해상도이고,다음 " "안에 들어가는 것은 만들어진 화면의 이름을 나타낸다.https://developer.mozilla.o

velog.io

 

mlx함수들의 설명을 한 번에 볼 수 있는 블로그

https://42kchoi.tistory.com/229

 

MiniLibX 파헤치기

harm-smits.github.io/42docs/ 의 번역본입니다. 리눅스 및 windows 환경이신 분들은 원문에서 세팅방법을 참고해주세요. 42 인트라넷에 이것과 관련된 영상이 있긴 합니다. 아래 링크에서 확인하세요. https

42kchoi.tistory.com

 

정말 정말 이해가 잘 되는 예제들이 있는 깃허브

https://github.com/taelee42/mlx_example

 

taelee42/mlx_example

Some examples for those who try to use MLX library for 42 subject CUB3D or miniRT. And some helpful links. - taelee42/mlx_example

github.com

 

 

3. mlx를 이용하여 Mandelbrot set, Julia set 만들어보기

 

#include	"mlx.h"
#include	<stdio.h>
#include	<stdlib.h>
#include	<math.h>

#define		X_EVENT_KEY_PRESS		2
#define		X_EVENT_KEY_EXIT		17
#define		X_EVENT_MOUSE_PRESS		4
#define		X_EVENT_MOUSE_MOTION	6
#define		WIN_WIDTH				800
#define		WIN_HEIGHT				600
#define		KEY_ESC					53
#define		ITER_MAX				100

typedef	struct	s_img{
	void		*img_ptr;
	char		*data;
	int			size_l;
	int			bpp;
	int			endian;
}				t_img;

typedef	struct	s_mlx{
	void		*mlx_ptr;
	void		*win;
	t_img		img;
}				t_mlx;

int		mandelbrot(int count_w, int count_h, int iter)
{
	double c_re;
	double c_im;
	double x;
	double x_new;
	double y;

	c_re = ((count_w - WIN_WIDTH / 2) * 3.0 / WIN_WIDTH) - 0.5;
	c_im = ((WIN_HEIGHT / 2) - count_h) * 2.0 / WIN_HEIGHT;
	x = 0;
	y = 0;
	while ((pow(x, 2.0) + pow(y, 2.0) < 4) && (iter < ITER_MAX))
	{
		x_new = pow(x, 2.0) - pow(y, 2.0) + c_re;
		y = 2 * x * y + c_im;
		x = x_new;
		iter++;
	}
	return (iter);
}

int		julia(int count_w, int count_h, int iter, t_img *img)
{
	double	c_re;
	double	c_im;
	double	x;
	double	x_new;
	double	y;

	c_re = -0.75;
	c_im = 0;
	x = ((count_w - WIN_WIDTH / 2) * 4.0 / WIN_WIDTH);
	y = ((WIN_HEIGHT / 2) - count_h) * 4.0 / WIN_HEIGHT;
	while ((pow(x, 2.0) + pow(y, 2.0) < 4) && (iter < ITER_MAX))
	{
		x_new = pow(x, 2.0) - pow(y, 2.0) + c_re;
		y = 2 * x * y + c_im;
		x = x_new;
		iter++;
	}
	return (iter);
}

int		window_init(t_mlx *mlx)
{
	if (!(mlx->mlx_ptr = mlx_init()))
		return (0);
	if (!(mlx->win = mlx_new_window(mlx->mlx_ptr, WIN_WIDTH, WIN_HEIGHT, "A simple example")))
		return (0);
	if (!(mlx->img.img_ptr = mlx_new_image(mlx->mlx_ptr, WIN_WIDTH, WIN_HEIGHT)))
		return (0);
	if (!(mlx->img.data = mlx_get_data_addr(mlx->img.img_ptr, &mlx->img.bpp, &mlx->img.size_l, &mlx->img.endian)))
		return (0);
	return (1);
}

void	my_mlx_pixel_put(t_img *img, int x, int y, int color)
{
	char	*dst;

	dst = img->data + (x * img->bpp / 8) + (y * img->size_l);
	*(unsigned int *)dst = color;
}

void	put_pixel(t_img *img)
{
	int		iter;
	int		count_w;
	int		count_h;

	count_h = -1;
	while (++count_h <= WIN_HEIGHT)
	{
		count_w = -1;
		while (++count_w <= WIN_WIDTH)
		{
			iter = mandelbrot(count_w, count_h, 0);
			//iter = julia(count_w, count_h, 0, img);
			if (iter < ITER_MAX)
				my_mlx_pixel_put(img, count_w, count_h, 0x00FFFFFF);
			else
				my_mlx_pixel_put(img, count_w, count_h, 0x00000000);
		}
	}
}

int		key_press(int keycode)
{
	if (keycode == KEY_ESC)
		exit(0);
	else
		return (0);
	return(0);
}

int		close(int keycode)
{
	exit(0);
}

int		main(void)
{
	t_mlx	mlx;

	if (!window_init(&mlx))
		return (0);
	put_pixel(&mlx.img);
	mlx_put_image_to_window(mlx.mlx_ptr, mlx.win, mlx.img.img_ptr, 0, 0);
	mlx_hook(mlx.win, X_EVENT_KEY_PRESS, 0, key_press, 0);
	mlx_hook(mlx.win, X_EVENT_KEY_EXIT, 0, close, 0);
	mlx_loop(mlx.mlx_ptr);
	return (0);	
}

 

위 코드를 간단히 리뷰하자!!!!

  1. window와 구조체를 초기화한다.
  2. put_pixel로 mandelbrot을 실행시켜 iteration에 따라 색을 달리한다.
  3. mlx_put_image_to_window에서 offset을 계산하고 색을 넣는다. (offset을 계산하는데 이는 rgb의 자료형을 봐야 한다.)
  4. mlx_loop를 통해 hook이 들어오는지 확인한다.
  5. hook이 들어왔을 땐 mlx_hook을 통해 함수를 실행한다.

4. 색 입혀보기

 

위에서 변화된 것과 추가된 것만 표기하자!!!

int		color_set(int iter)
{
	double	r;
	double	g;
	double	b;
	int		color;

	r = sin(0.3 * (double)iter);
	g = sin(0.3 * (double)iter + 3) * 127 + 128;
	b = sin(0.3 * (double)iter + 3) * 127 + 128;
	color = ((int)(255.999 * r) << 16) + ((int)(255.999 * g) << 8) + ((int)(255.999 * b));
	return (color);
}

void	put_pixel(t_img *img)
{
	int		iter;
    int		color;
	int		count_w;
	int		count_h;

	count_h = -1;
	while (++count_h <= WIN_HEIGHT)
	{
		count_w = -1;
		while (++count_w <= WIN_WIDTH)
		{
			iter = mandelbrot(count_w, count_h, 0);
			//iter = julia(count_w, count_h, 0, img);
			if (iter < ITER_MAX)
			{
         		   	color = color_set(iter);
        		   	my_mlx_pixel_put(img, count_w, count_h, 0x00FFFFFF);
			}
            else
				my_mlx_pixel_put(img, count_w, count_h, 0x00000000);
		}
	}
}

위의 코드는 사실 이해하기엔 아직 너무 어렵다.

간단히 알아보면 iteration의 값에 따라 color가 바뀌고 그에 따른 수식들을 정리해서 color라는 변수에 비트 연산을 통해 넣어준 것이다.

 

5. 확대 / 축소 구현하기

 

마우스의 좌표를 찾고 그에 따라 폭과 높이를 Mouse Scroll UP/DOWN에 따라 비율적으로 min_width, max_width, min_height, max_height을 늘이고 줄이는 작업을 한다.

 

확대/축소했을 때

$width = width_{max}- width_{min}$

$height = height_{min} - height_{min}$

$zoom = 확대/축소를 위한 비율$

 

여기서 min과 max를 비율적으로 줄이고 늘여야 한다.

$width_{max} ±= zoom * \frac {width - x_{좌표}} {width}$

$height_{max} ±= zoom * \frac {height - y_{좌표}} {height}$

$width_{min} ±= zoom * \frac {x_{좌표}} {width}$

$height_{min} ±= zoom * \frac {y_{좌표}} {height}$

 

이렇게 계산한 값은 Mandelbrot과 Julia을 계산할 때만 쓰이고 계산 후 결괏값으로 나온 iteration에 대한 pixel에 RGB를 넣을 때는 우리가 갖고 있는 800X600이 입력되어야 한다.

 

int		mouse_event(int button, int x, int y, t_mlx *mlx)
{
	if (button == SCROLL_UP)
	{
		mlx->zoom.max_width -= ZOOM * ((mlx->zoom.width - x) / mlx->zoom.width);
		mlx->zoom.min_width += ZOOM * (x / mlx->zoom.width);
		mlx->zoom.max_height -= ZOOM * ((mlx->zoom.height - y) / mlx->zoom.height);
		mlx->zoom.min_height += ZOOM * (y / mlx->zoom.width);
	}
	else if (button == SCROLL_DOWN)
	{
		mlx->zoom.max_width += ZOOM * ((mlx->zoom.width - x) / mlx->zoom.width);
		mlx->zoom.min_width -= ZOOM * (x / mlx->zoom.width);
		mlx->zoom.max_height += ZOOM * ((mlx->zoom.height - y) / mlx->zoom.height);
		mlx->zoom.min_height -= ZOOM * (y / mlx->zoom.height);
	}
	else
		return (0);
	mlx->zoom.width = mlx->zoom.max_width - mlx->zoom.min_width;
	mlx->zoom.height = mlx->zoom.max_height - mlx->zoom.min_height;
	return (0);
}

void	put_pixel(t_mlx *mlx)
{
	int		iter;
	int		color;
	double	count_w;
	double	count_h;
	int		x_idx;
	int		y_idx;

	y_idx = -1;
	count_h = mlx->zoom.min_height;
	while (++y_idx <= WIN_HEIGHT)
	{
		x_idx = -1;
		count_w = mlx->zoom.min_width;
		while (++x_idx <= WIN_WIDTH)
		{
			iter = mandelbrot(count_w, count_h, 0, mlx);
			if (iter < ITER_MAX)
			{
				color = color_set(iter);
				my_mlx_pixel_put(&mlx->img, x_idx, y_idx, color);
			}
			else
				my_mlx_pixel_put(&mlx->img, x_idx, y_idx, 0x00000000);
			count_w += mlx->zoom.width / WIN_WIDTH;
		}
		count_h += mlx->zoom.height / WIN_HEIGHT;
	}
}

하지만 여기선 시작하는 좌표의 위치만 달라질 뿐 확대/축소가 되질 않는다.

왜지.....

 

원인은 mandelbrot set을 계산하는 함수에서 c_re과 c_im에서 찾았다.

원래는 800X600의 window에서 마우스의 좌표를 가져오는데 mandelbrot set은 표현되는 범위가 Real : (-2, 1), Imaginary : (-1, 1)이므로 축소를 시켜줘야 한다.

즉, 이렇게 축소시켜주는 단계에서 화면에 나타날 이미지의 크기가 지정되어있었다.... 꺾꺾꺾

 

완전히 갈아엎었다.

이전에는 mandelbrot을 계산하는 곳에서 좌표를 계산했지만 지금 작성한 로직은 스크롤을 하면 x, y의 좌표가 움직이지 않고 똑같이 그대로 있어야 한다는 생각이 base로 잡혔고 그에 따라 계산하는데 zoom의 비율은 현재 상태에서 0.1씩 증가하고 감소하게 작성했으며 픽셀의 색을 정하는 첫 시작인 $c_{re}$ 와  $c_{im}$가 $width_{min} ,  height_{min}$ 이 되게 만들었다.

 

이제 여기서 생각해봐야 하는 점은 우리가 x, y 좌표를 그대로 유지시키면서 어떻게 $c_{re}$ 와  $c_{im}$를 변화시킬 것인지 생각해봐야 한다.

 

결국 도달한 결론은 아래와 같다. 글씨 못씀 주의

 

 

그리고 우리는 이런 결과를 볼 수 있게 된다.

 

아주 기괴하고 요상한 그림이 되어 버린다.

정말 뿌듯하다...

 

6. Julia set 심화

여기에서는  julia set에서의 고정값 c를 마우스의 좌표로 지정하여 형태를 변형시키는 방법을 진행할 것이다.

처음에 우리가 해야 할 것은 마우스의 모션을 감지하는 hook을 만들고 mouse_move라는 함수를 만들면서 마우스의 좌표(800x600)를 실제 좌표계의 좌표(4x4)로 변환하는 작업을 거쳐야 한다.

 

이 과정은 간단하게 생각해서 일차함수의 좌표변환과 같다.

우리가 받아오는 좌표의 원점은 왼쪽 위의 꼭짓점이다.

그래서 원점을 중앙으로 만들어준 뒤 800의 width를 4로, 600의 height를 4로 표현해주면 된다.

x의 좌표 = $\frac {4(x - \frac {width} {2})} {width}$

y의 좌표 = $\frac {4(\frac {height} {2} - y)} {height}$

그리고 나는 확대를 할 때는 고정이 되게 만들고 싶어서 key f를 누르면 fix 되도록 설정해놓았다.

결과는 아래와 같다.

 

 

7. 또 다른 한 개의 Fractal 만들기

또 다른 한 개의 fractal은 burning ship으로 선택했다.

사실 snow flake를 하고 싶었는데 점화식 찾기를 실패해서 burning ship을 선택을 했는데 엄청 간단해서 바로 만들었다...

burning ship은 mandelbrot과 다른 점은 딱 한 가지이다.

점화식

$z_{n+1} = (z_{n})^{2} + c$

$z_{n+1} = a^{2} - b^{2} + c_{re} + (|2 * a * b| + c_{im})$

즉, 절댓값 하나 추가한 것으로 놀랍게도 다른 그림이 생성이 된다.

 

색이 요상해서 burning ship으로 보이진 않는데 아직 smoothly와 gradient를 이해하기엔 아주 아쉬운 지능인 것 같다,,,,

이후엔 꼭 snow frake에 검정 배경에 빛이 나는 솔로 눈송이를 만들어 볼 예정이다.

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

python Dict, set 활용  (0) 2021.10.15
Philosophers  (2) 2021.07.09
Push_swap  (0) 2021.06.24
Minitalk(2)  (0) 2021.06.17
Minitalk(1)  (4) 2021.06.13

+ Recent posts