더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

/etc/passwd(사용자 정보)

보안이나 리눅스를 배우신다면 /etc/passwd에 대한 이해는 필수입니다. 정보보안기사 시험에도 당골손님이기도 하죠.

/etc/passwd에는 시스템에 등록된 사용자의 정보들이 담겨있는 파일입니다. 이 파일을 이용해서 사용자의 계정과 인증을 관리하게 됩니다. 이 파일의 이름이 passwd인데, 여기에 패스워드 정보가 있는건 아니구요. 과거에 사용자의 패스워드를 이곳에 저장을 하였는데, /etc/shadow에 따로 암호화된 패스워드를 저장합니다. 과거에 패스워드를 저장했었던 것이 아직 파일 이름으로 남아 파일명이 passwd입니다.

/etc/passwd를 열어보면 아래와 같이 사용자의 정보들이 있습니다. 

/etc/passwd

 

각 필드들이 의미하는 바가 있는데, 필드 하나하나 무엇을 의미하는지 확인해봅시다. 각 필드는 콜론(:)으로 구분되어 집니다. 

     
        root:x:0:0:root:/root:/bin/bashroot:x:0:0:root:/root:/bin/bash
        

 

필드 설명
사용자 계정명 맨 앞에 필드는 사용자의 계정명을 나타냅니다
패스워드 그 다음의 필드는 패스워드 필드인데, x가 의미하는 바는 사용자의 패스워드가 /etc/shadow에 암호화되어 저장되어있다는 뜻입니다.
UID 사용자의 user id를 나타냅니다. 관리자 계정(Root)은 UID가 0입니다.
GID 사용자의 그룹 ID를 나타냅니다. 관리자 그룹(Root)의 GID는 0입니다.
comment 사용자와 관련한 기타 정보로 일반적으로 사용자의 이름을 나타냅니다.
홈 디렉토리 사용자의 홈디렉토리를 의미합니다. 관리자 계정의 홈 디렉토리는 /root이며, 다른 사용자의 홈 디렉토리는 기본으로 /home/ 하위에 계정명으로 위치합니다.
로그인 쉘 사용자가 로그인시에 사용할 쉘을 의미합니다. 보통 사용자의 쉘은 성능이 우수한 bash쉘을 사용합니다. 로그인이 불필요한 계정도 있는데요. 이때는 이 필드가 /usr/sbin/nologin, /bin/false, /sbin/nologin 등으로 표기됩니다. 이것은 사용자가 아니라 어플리케이션이기 때문이죠. 아래처럼 말이죠.

sshd:x:126:65534::/run/sshd:/usr/sbin/nologin
gnome-initial-setup:x:124:65534::/run/gnome-initial-setup/:/bin/false
gdm:x:125:130:Gnome Display Manager:/var/lib/gdm3:/bin/false


 

 

 

/etc/shadow

/etc/shadow에는 암호화된 패스워드와 패스워드 설정 정책이 기재되어 있습니다. 여기서 관리자 계정과 관리자 그룹만이 이 파일을 읽을 수 있습니다. 이 파일이 만약 평문의 비밀번호의 정보를 가지고 있다면 모든 사용자의 비밀번호가 유출될 수 있습니다.

/etc/shadow

 

/etc/shadow 파일의 root의 정보를 보게되면 아래와 같이 나오게 되는데요. 각 필드에 대해 설명해보도록 하겠습니다.

/etc/shadow field

필드 설명
사용자 계정명 맨 첫 필드는 사용자의 계정명을 뜻합니다.
암호화된 패스워드 두번째 필드는 암호화된 패스워드를 뜻합니다. 이 패스워드는 또 $로 필드가 구분되어 있는데요. 아래처럼 구분이 됩니다.

$algorithm_id$salt$encrypted_password

algorithm_id : 암호학적 해시의 id를 의미하는데요. id는 아래와 같습니다.
1 : MD5(가장 취약한 일방향 해쉬로 요즘에는 쓰지 않습니다.
2 : BlowFish
5 : SHA-256
6 : SHA-512
제 시스템은 SHA-512를 사용하는 것을 알 수 있습니다.

salt : 각 해쉬에 첨가할 랜덤값입니다. 이 랜덤값에 따라서 해시의 값이 바뀌게 됩니다.
encrypted_password : 마지막은 알고리즘과 salt로 패스워드를 암호화한 값입니다.

이 뿐만 아니라 패스워드 필드에 *, !!, 또는 빈값이 설정될 수 있습니다. 이것이 의미하는 바를 알아보겠습니다.
* : 패스워드가 잠긴 상태입니다. 로그인은 불가합니다. 별도의 인증방식을 사용하여 로그인을 할 수는 있습니다.
! : 패스워드가 잠긴 상태이고 로그인을 할 수 없습니다. 또는 사용자를 생성하고 패스워드를 설정하지 않은 상태이기도 합니다.
empty : 패스워드가 설정되지 않았지만 로그인이 가능합니다. 
마지막 변경 마지막으로 패스워드를 변경한 날을 1970년 1월 1일 기준으로 일수도 표시합니다. 여기서 마지막으로 패스워드를 변경한 날은 1970년 1월 1일 이후 18693일이 지났음을 알 수 있습니다.
패스워드 최소 사용기간 패스워드의 최소 사용기간 설정으로 패스워드를 변경한 이후 최소 이 정도의 기간은 써야한다는 것을 의미합니다. 그래서 마지막 변경이 일어난 후 이 일수가 지나기 전에 암호를 변경할 수 없습니다.
패스워드 최대 사용기간 패스워드의 최대 사용기간 설정으로 마지막 패스워드 변경 이후 만료일수를 의미합니다. 패스워드를 변경하지 않으면 공격자가 패스워드를 깰 수도 있으므로 90일을 권장합니다.
경고 패스워드 만료 이전에 경고할 경고 일수를 의미합니다.
비활성화 패스워드가 만료된 이후에 계정이 잠기기 전까지 비활성 일수(date)입니다. 해당 비활성 기간동안에 패스워드를 변경해야 계정이 잠기지 않습니다. 
만료일 계정 만료일 필드입니다. 1970년 1월 1일 기준으로 일수로 표시합니다.

 

이상으로 /etc/shadow와 /etc/passwd에 대해서 알아보았는데, 이 파일은 매우 중요하므로 보안을 배우는 사람은 잘 알아두어야합니다. 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

쉘스크립트 기본

쉘 스크립트는 쉘에게 무슨 명령들을 실행할지 알려주는 스크립트 파일입니다. 여기서는 가장 널리쓰이는 bash 쉘을 사용하는 스크립트를 설명하도록 하겠습니다.

#!/bin/bash

스크립트 최상단에는 항상 이 구문이 적혀있어야합니다. 간단하게 hello,world라는 문자열을 출력하는 스크립트를 만들어봅시다. 파일명은 exam.sh로 정해놓고요.

#!/bin/bash

echo "hello, world"
printf "hello, world"

 

./exam.sh로 스크립트를 실행하면 되는데 혹시 실행이 되지 않는다면 실행 권한이 없어서 그런것이니까 chmod 777 exam.sh로 권한을 바꾸어주면 됩니다. 그 후 결과를 보면 echo는 개행이 되지만 printf는 개행이 되지 않는 다는 것을 알 수 있습니다. 또 C언어 처럼 문자열을 전달할 수도 있는데 포맷 문자 %s를 이용하면 됩니다.

printf "%s, %s" hello, world

 

1) 변수(Variable)

쉘스크립트에서도 변수를 사용할 수 있는데요. 변수명=값 으로 변수를 선언하고, 사용할때는 앞에 $를 붙여줍니다. 중괄호를 이용하는 방법도 있습니다. ${변수}식으로 중괄호로 묶어서 사용합니다.

#!/bin/bash

h="hello"
w="world"
echo "${h}, ${w}"

 

여기서 export를 사용하여 다른 쉘 스크립트에서 사용할 수 있게 합니다. exam.sh는 변수 선언하고 export를, exam2.sh는 그 변수를 출력하는 쉘 스크립트입니다.

#!/bin/bash
#exam.sh
export MY_VAR="reakwon"

./exam2.sh

 

#!/bin/bash
#exam2.sh
echo ${MY_VAR}

 

매개변수

프로그램에서도 실행할때 인자를 주듯 쉘 스크립트도 역시 그렇게 할 수 있습니다. 실행한 스크립트 이름은 ${0}, 그 이후는 전달받은 인자 값들입니다(${1}, ${2}, ...). 아래의 예제코드를 보면서 이해하실 수 있습니다.

#!/bin/bash


echo "script name:${0}"
echo "매개변수 갯수 :${#}"
echo "전체 매개변수  값 : ${*}"
echo "전체 매개변수 값2 : ${@}"
echo "매개변수 1 : ${1}"
echo "매개변수 2 : ${2}"

 

 

 

 

 

 

 

 

예약변수

쉘 스크립트에서 사용자가 정해서 만들 수 없는 이미 정의된 변수가 존재합니다. 그것을 예약 변수라고 하는데요. 몇가지 예약 변수를 보도록 합시다.

변수 설명
HOME 사용자 홈 디렉토리를 의미합니다.
PATH 실행 파일의 경로입니다. 여러분이 chmod, mkdir 등의 명령어들은 /bin이나 /usr/bin, /sbin에 위치하는데, 이 경로들을 PATH 지정하면 여러분들은 굳이 /bin/chmod를 입력하지 않고, chmod 입력만 해주면 됩니다.
LANG 프로그램 실행 시 지원되는 언어를 말합니다.
UID 사용자의 UID입니다.
SHELL 사용자가 로그인시 실행되는 쉘을 말합니다.
USER 사용자의 계정 이름을 말합니다.
FUNCNAME 현재 실행되고 있는 함수 이름을 말합니다.
TERM 로그인 터미널을 말합니다.

 

2) 배열(Array)

쉘 스크립트에서 배열은 1차원 배열만 지원하며 중괄호를 사용해야합니다. 배열 원소는 소괄호안에 공백으로 구분하여 줍니다. 배열 안 원소는 문자열이든 정수든 상관 없이 쓸 수 있는 특징이 있습니다.

#!/bin/bash

arr=("hello" "world" 1 2 3 4 5)

echo "배열 전체 : ${arr[@]}"
echo "배열 원소의 갯수 : ${#arr[@]}"
echo "배열 첫번째 : ${arr}, 혹은 ${arr[0]}"
echo "5번 index를 갖는 배열의 원소 : ${arr[5]}"

arr[5]="five"

echo "5번 index를 갖는 배열의 원소 : ${arr[5]}"

# 5번 원소 해제
unset arr[5]
echo "5번 원소 삭제 후"
echo "5번 index를 갖는 배열의 원소 : ${arr[5]}"
echo "6번 index를 갖는 배열의 원소 : ${arr[6]}"

 

3) 함수(Function)

다른 프로그래밍 언어와 같이 쉘 스크립트에서도 함수를 사용할 수 있습니다. 함수의 정의 형식은 아래와 같습니다. 함수명 앞 function은 써주지 않아도 무관합니다. 주의 할 점은 함수가 호출되기 전에 함수가 정의되어 있어야하며 호출할때는 괄호를 써주지 않고 호출해야한다는 점입니다.

function 함수명(){
 ... 내용 ...
}
#!/bin/bash

function func(){
        echo "func()"
}

#함수 호출
func

 

4) 비교문

쉘스크립트에서 비교문은 약간 특이합니다. 아래와 같은 형식으로 비교문을 사용할 수 있습니다. 

if [ 값1 조건 값2 ]; then
	... 작업 내용 ...
fi

조건은 -eq, -gt, -lt 등이 있는데, 자세한 것은 아래의 코드에서 보도록 합시다.

#!/bin/bash

function func(){
        a=10
        b=5

        if [ ${a} -eq ${b} ]; then
                echo "a와 b는 같다."
        fi

        if [ ${a} -ne ${b} ]; then
                echo "a와 b는 같지 않다."
        fi

        if [ ${a} -gt ${b} ]; then
                echo "a가 b보다 크다."
        fi

        if [ ${a} -ge ${b} ]; then
                echo "a가 b보다 크거나 같다."
        fi

        if [ ${a} -lt ${b} ]; then
                echo "a가 b보다 작다."
        fi

        if [ ${a} -le ${b} ]; then
                echo "a가 b보다 작거나 같다."
        fi

}

#함수 호출
func

 

 

 

 

 

 

쉘스크립트는 명령어를 다루는 스크립트이기 때문에 파일이 존재하는지, 디렉토리가 존재하는지 등 파일과 관련된 조건 여부도 조건문을 통해 알 수 있습니다. 아래는 그것을 정리한 표입니다. 그리고 앞에 느낌표(!)를 써주고 조건을 기재하면 그 조건이 false일때 참이 성립합니다.

파일 관련 조건

조건 설명
if [ -d ${변수} ]; then 
if [ ! -d ${변수} ]; then
${변수}의 디렉토리가 존재하면 참이 성립합니다. 
${변수}의 디렉토리가 존재하지 않으면 참이 성립합니다. 
if [ -e ${변수} ]; then 
if [ ! -e ${변수} ]; then 
${변수}라는 파일이 존재하면 참입니다.
${변수}라는 파일이 존재하지 않으면 참입니다.
if [ -L ${변수} ]; then 파일이 symbolic link이면 참입니다.
if [ -s ${변수} ]; then 파일의 크기가 0보다 크면 참입니다.
if [ -S ${변수} ]; then 파일 타입이 소켓이면 참입니다.
if [ -r ${변수} ]; then 파일을 읽을 수 있으면 참입니다.
if [ -w ${변수} ]; then 파일을 쓸 수 있으면 참입니다.
if [ -x ${변수} ]; then 파일을 실행할 수 있으면 참입니다.
if [ -f ${변수} ]; then 파일이 정규 파일이면 참입니다.
if [ -c ${변수} ]; then 파일이 문자 장치이면 참입니다.
if [ ${변수1} -nt ${변수2}]; then 변수1의 파일이 변수2의 파일보다 최신 파일이면 참입니다.
if [ ${변수1} -ot ${변수2}]; then 변수1의 파일이 변수2의 파일보다 최신이 아니면 참입니다.
if [ ${변수1} -ef ${변수2}]; then 변수1의 파일과 변수2의 파일이 동일하면 참입니다.

 

만약 AND조건과 OR조건을 섞어서 쓴다면 어떻게 할까요? AND는 -a, OR은 -o를 이용해서 조건을 추가할 수 있습니다.

function func(){
        a=aa
        b=bb
        if [ -f ${a} -a -d ${b} ]; then
                echo "a는 파일이고 b는 디렉토리 "
        fi
}

#함수 호출
func

 

또한 여러 조건을 걸려면, 즉, C언어에서 else if역할을 하는 것은 elif라는 것을 사용하여 else if와 똑같은 역할을 할 수 있게 해줍니다. elif[ 조건 ]; then 이런식으로 쓰시면 됩니다.

만약 반복문에서 사용하여 조건이 참일때 반복문을 멈추고 싶을때 break라는 키워드를 사용하여 반복문을 멈출 수 있습니다.

C에서 switch case와 동일한 구문을 하는 문법도 존재합니다. 아래와 같이 case문을 사용하면 됩니다. 각 case의 끝을 보면 세미콜론 2개로 종료하는 것을 볼 수 있습니다.

#!/bin/bash

case ${1} in
        "linux") echo "리눅스" ;;
        "unix") echo "유닉스" ;;
        "windows") echo "윈도우즈" ;;
        "MacOS") echo "맥OS" ;;
        *) echo "머야" ;;
esac

5) 반복문

반복문 중 for문에 대한 설명은 예제와 같이 보는 것으로 하겠습니다. 

#!/bin/bash

function func(){
        echo "사용예1"
        for i in 1 2 3 4 5
        do
                echo "${i}"
        done

        echo "사용예2"
        list="1 2 3 4 5"
        for i in ${list}
        do
                echo "${i}"
        done

        echo "사용예3"
        for i in {1..5}
        do
                echo "${i}"
        done

        echo "사용예4: 크기를 2만큼 증가시키면서 출력"
        for i in {1..5..2}
        do
                echo "${i}"
        done

        echo "사용예5: 배열을 이용"
        arr=(1 2 3 4 5)
        for i in "${arr[@]}"
        do
                echo "${i}"
        done


        echo "사용예6: C와 유사한 형식의 for문"
        for ((i=0; i<5; i++)); do
                echo "${i}"
        done
}

#함수 호출
func

 

 

 

 

 

6) 명령어의 종료 코드(exit)

만약 명령어가 실패할 경우 동작을 다르게 할것이라면 어떻게 할까요? 명령어의 종료 코드를 얻어오면 됩니다. 명령어의 종료 코드는 $?에 가장 최근 실행한 명령어의 종료 코드가 있습니다. 정상적인 종료는 0이 반환되는데, 만약 0이 아닌 값으로 반환되면 오류라고 판단하시면 됩니다. 여기에 간단한 오류를 내는 c파일(main.c)을 작성하고 gcc로 컴파일(gcc main.c)합니다. 이 c파일은 실행시 전달인자가 2개 보다 작으면 exit으로 코드 16을 반환합니다.

#include <stdlib.h>
#include <stdio.h>
int main(int argc, char* argv[]){
        if(argc<2){
                fprintf(stderr,"인자는 2개 이상이어야 합니다.\n");
                exit(16);
        }
}

 

그리고 쉘스크립트를 아래와 같이 수정해서 오류의 코드가 얼마인지 확인해보면 우리가 exit에 넘겨준 16이 찍히는 것을 확인할 수 있습니다.

#!/bin/bash

function func(){
        ./a.out
        echo "오류 코드 ($?)"
}

#함수 호출
func

 

./a.out에 인자를 하나 넣어서 다시 실행시켜보세요. 0이 종료코드로 반환 될 것입니다. 이렇게 이전에 명령어의 결과를 봐야하는 경우 $?으로 판단하여 분기시킬 수 있습니다.

이렇게 종료 반환 코드는 명령어 뿐만 아니라 쉘 스크립트 자체도 반환할 수 있습니다. 즉, 쉘 스크립트의 수행이 실패했는지 알려주기 위해서는 exit 명령어를 사용하면 됩니다. 아래의 예가 그 사용법을 보여줍니다. 우선 exam2.sh라는 파일을 만들어봅시다. 이 쉘 스크립트 파일은 exam.sh에서 실행할 예정입니다. 별거없이 16의 종료코드를 반환하며 끝이 납니다. 

#!/bin/bash

exit 16

 

이 쉘 파일을 exam.sh에서 실행시켜볼까요?

#!/bin/bash

function func(){
        ./exam2.sh
        echo "오류 코드 ($?)"
}

#함수 호출
func

 

확인해보면 우리가 기재했던 16이 출력되는 것을 확인할 수 있습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

리눅스에 대한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

리눅스 커널(Kernel)

리눅스 커널은 하드웨어와 가장 가까이 있는 일종의 프로그램입니다. 가장 핵심적인 프로그램으로 사용자는 커널을 통해서 하드웨어를 제어합니다. 함부로 하드웨어를 직접 만졌다가는 돌아올 수 없는 강을 건너게 될 수도 있습니다. H/W에는 CPU, 메모리(RAM), 하드디스크(HDD), 기타 입출력 장치 등 많은 것들이 있는데 실제 직접 조작할 수도 없고 해서도 안됩니다. 그래서 커널이 존재하게 되는것이지요. 이 처럼 커널은 시스템의 자원을 관리하는 운영체제 그 자체로 보시면 됩니다. 

윈도우즈도 역시 커널을 가지고 있는데, 사용자가 커널의 코드를 직접 들여다 볼 수 없고 거기에 맞는 모듈(Module)을 만들어거 끼워넣을 수 없죠. 윈도우즈는 커널 내부를 공개하고 있지 않기 때문인데요. 그래서 윈도우즈 커널을 수정하려면 오직 MS사 밖에 할 수 없습니다. 리눅스는 윈도우즈와는 다르게 커널 내부 소스를 전부 공개합니다. 그래서 커널을 잘 아는 사람은 거기에 맞게 모듈이라는 커널의 부분을 끼워넣어서 변경할 수 있습니다. 

이전에 리눅스는 커널에 조금이라도 변경이 생기면 수정 후 전체 커널 컴파일을 해야했는데, 이 모듈이라는 개념이 도입된 후 필요한 커널의 부분만 수정하고 다시 끼울 수 있으면서 커널 컴파일하는 모든 과정을 수행하지 않아 시간이 획기적으로 줄어 들 수 있습니다. 현재 적재된 커널 모듈을 보려면 lsmod를 이용해서 볼 수 있고 모듈을 설치할때는 insmod를 이용하면 되는데, 물론 아무나는 안됩니다. 커널 프로그래밍을 이용하기 때문에 아무나 건드리면 시스템이 멈출 수 있습니다.

쉘(shell)

쉘은 사용자의 응용프로그램과 커널 사이에 위치해있으며 응용프로그램의 명령어와 커널이 대화를 하도록 만들어줍니다. 그래서 명령어 해석기라는 부릅니다.

자, 우리가 명령어를 입력하게 되면 컴퓨터에서는 쉘(shell)이 이 명령어를 받아 해석하여 커널(kernel)에게 보내면 커널은 우리가 내려주었던 동작을 하게 됩니다. 그래서 그에 대한 결과를 사용자에게 전달하려고 다시 쉘에 응답을 보내고 쉘을 거쳐 사용자에게 도달합니다. 여기서 쉘도 프로그램입니다.

쉘 종류는 여러가지고 존재합니다. 

Bourne Shell (/bin/sh)

본쉘이라고 합니다. 유닉스 최초의 쉘이기도 하지요. 최조의 쉘이라서 미흡한 기능이 많은데요. 여기서 일반 유저의 쉘 프롬프트는 $, root사용자의 프롬프트는 #으로 나타냅니다.

Bash, Bourne Again Shell (/bin/bash)

현재 리눅스에서 표준으로 채택된 쉘입니다. 여러분에게 가장 친숙한 쉘이라고 생각하시면 됩니다. 여러분이 명령을 칠때 명령어 다 쓰기 싫어서 tab쓰시죠? 그게 원래는 리눅스 자체의 기능이 아니고 bash의 기능입니다. 그 밖에도 아래와 같은 특징이 있습니다. Mac 운영체제에서도 bash가 쓰입니다.


 - History(방향키 ↑↓)
 - Alias
 - 자동 완성 (tab)
 - 연산
 - Job Control
 - 그 외

 

C shell (/bin/csh)

C언어를 기반으로 만들어진 쉘입니다. 쉘 스크립트가 C언어로 작성하는 것처럼 작성할 수 있습니다.

 

 

쉘을 이해해보자

이 외에도 쉘의 종류는 여러가지가 있는데요. 실제 시스템에서 지원하는 쉘을 보기 위해서 /etc/shells를 열어보시면 됩니다. 아래는 저의 시스템에 있는 shell들입니다.


/home/ubuntu# cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/bin/dash
/usr/bin/dash

그리고 사용자가 어떤 쉘을 쓰고 있는지 보고 싶으시다면 SHELL 환경변수를 확인하면 됩니다. echo $SHELL 으로 확인하세요. 저는 bash쉘을 사용하고 있군요.


/home/ubuntu# echo $SHELL
/bin/bash

 

여기서 bash쉘에서 지원하는 기능이 sh(본쉘)에서도 먹힐까요? 사용하는 bash쉘을 본쉘로 바꿔보도록 하겠습니다. 단순히 sh명령으로 쉘을 실행해봅시다. 그리고 명령어를 아무거나 처보세요. 그리고 우리가 이전의 명령어를 치려고 위 화살표(↑)를 누르면 이상한 문자(^[[A)가 쓰여지게 되어 이전의 명령어를 수행할 수는 없습니다. 결국 sh는 히스토리 기능을 지원하지 못하며, 정확히 얘기해서 저의 명령을 해석하지 못한다는 것입니다. bash에서는 ~/.bash_history에 그 동안의 명령어들 내역을 보관하고 있는데, 본쉘은 ~/.bash_history가 무엇인지도 모르죠.


/home/ubuntu# sh
# ls
공개  다운로드  문서  바탕화면  비디오  사진  음악  템플릿
# pwd
/home/ubuntu
# ^[[A

 

보통 bash에서 ll은 alias로 ls -alF의 명령어 별명으로 지정합니다. 지금 bash에서 ll을 치면 파일들의 목록이 자세히 나오게 되는데요. ll을 sh상에서 실행하면 실행이 되지 않습니다. bash에서 ll이 수행되지 않는다면 alias지정이 되지 않은 것인데요. vi로 ~/.bashrc를 열고 하단에 alias ll='ls -alF'를 써주시고 source ~/.bashrc를 수행하면 이제 ll이 수행될 것입니다. bashrc는 bash쉘의 설정파일입니다. 

 
     80 # some more ls aliases
     81 alias ll='ls -alF'
     82 alias la='ls -A'
     83 alias l='ls -CF'
     84

그렇다면 사용자가 접속할때 bash말고 다른 쉘을 사용하고 싶다면 어떻게 할까요? 시스템에서 root로 접속시 기본적으로 사용할 쉘이 어디에 기록되어있냐면 /etc/passwd에 기록되어있습니다. cat /etc/passwd | grep root의 결과가 아래와 같습니다. 리눅스를 조금 공부하셨다면 /etc/passwd에서 각 필드가 무엇을 뜻하는지는 아실겁니다. 

root:x:0:0:root:/root:/bin/bash

맨 오른쪽에 보이는 것이 접속 시 적용할 쉘인데요. 바꾸고 싶다면 chsh -s [shell path]를 입력하면 됩니다. 예를 들면 chsh -s /bin/sh 이렇게 쓰고 재접속을 하면 적용된 쉘로 바뀝니다. 

쉘에서 쓰는 명령어들을 하나의 대본으로 만들어 놓을 수 있는데요. 예를 들어 어느 경로에 들어가서 디렉토리를 만들고 거기에 파일을 만들고 파일의 내용을 어떤 내용으로 기록하고, 그런 작업들 말이에요. 그러려면 사용자가 수동으로 cd를 입력하고 mkdir하고 touch해서 파일을 만들고 그 내용을 echo '블라블라' > file 해서 입력하고 해야겠지요. 하지만 이런 대본(script)를 만들어서 쉘에게 알려주면 일련의 명령을 쉘이 알아서 수행해줍니다. 이것을 우리는 쉘 스크립트라고 하며 확장자가 .sh로 끝나는 파일입니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

헨리 하워드 홈즈(Heny Howord Holmes, H.H.Holmes)

헨리 하워드 홈즈는 본명이 아니라 가명입니다. 이 가명으로 유명한 이유는 가명을 쓰면서 대량학살이 이뤄지기 때문이죠. 본명은 허만 웹스터 마젯(Herman Webster Mudgett)인데요. 미국에서 기록된 최초 연쇄 살인마입니다. 오늘날의 연쇄살인마와는 클라스가 조금 다릅니다. 아예 호텔까지 지어버리거든요.

H.H.Holmes

출생

홈즈는 1861년 5월 16일, 미국 뉴햄프셔(New hampshire) 길맨턴에서 태어났습니다. 어렸을 적 홈즈는 매우 똘똘하고 예의바르며 어른 들에게 무척이나 예쁨받는 아이였습니다. 게다가 똘똘한 머리까지 가졌습니다. 하지만 어렸을 적 홈즈는 행복하다고 할 수 없었습니다. 아니, 매우 불행한 삶을 살았습니다. 아버지는 독실한 신자였었는데, 특히 잠언 13장 24절의 구문을 그렇게 가슴속에 새겼다고 합니다. 홈즈를 이유없이 폭행하게 됩니다. 왜냐면 아버지는 홈즈를 무척이나 사랑했거든요.

잠언 13장 24절 매를 아끼는 자는 그의 자식을 미워함이라 자식을 사랑하는 자는 근실히 징계하느니라

 

홈즈의 대학 졸업 사진

 

살인 호텔

1886년, 그는 시카고로 이사를 가는데요. 거기에서 헨리 하워드 홈즈라는 이름을 사용하기 시작합니다. 시카고 어떤 약국에서 조수 역할을 하면서 약국일을 했고, 약국을 인수하게 됩니다. 약국이 잘되자 홈즈는 그 앞에 호텔을 하나 짓는데요. 홈즈는 그 호텔을 성이라고 부릅니다. 이 호텔이 보통 호텔이 아닙니다. 살인을 목적으로 만들어진 호텔이었는데요. 

홈즈의 성

 

성의 내부

100개가 넘는 방과 비밀 통로, 그리고 시체를 단숨에 지하실로 옮길 수 있게 만든 낙하장, 각 방들은 파이프가 연결되어있어 이곳에서 독가스를 살포할 수 있었습니다. 벽은 석면으로 이루어져있어 내부의 소리가 밖으로 세어나가지 않았습니다. 지하실은 홈즈가 시체를 해체하고 소각할 수 있는 홈즈의 작업장이었습니다.

 

 

이러한 호텔을 짓는데 일조했던 사람은 그의 동료 밴자민 피첼. 그의 기술이 이 성에 접목이 되었습니다.

 

 

1893년 시카고 만국 박람회에서 엄청난 관광객들이 몰려오게 되며 숙박 시설이 부족해지게 됩니다. 홈즈는 이런 상황이 떼돈을 벌 수 있는 엄청난 기회였던 셈이죠. 홈즈는 객실에 있는 손님을 티나지 않게 몇몇 죽이게 됩니다. 이렇게 죽인 시체는 의과 대학에 가져다가 팔게 되죠.

체포

살인을 3년간 지속하면서 이제 꼬리를 밟히게 되는데요. 실종자의 대다수가 이 호텔에 머물렀다는 사실을 알게된 경찰은 홈즈를 유력 용의자로 지목하게 감시하게 됩니다.

물론 공범인 벤자민 피첼도 같이 감시하게 되는데, 홈즈는 돈독이 제대로 올라서 자신의 동료 피첼마저 죽입니다. 당시 피첼을 감시하고 있었던 경찰에 의해 드디어 홈즈는 붙잡히게 되면서 살인 호텔에서의 살인은 끝나게 됩니다.

H.H.Holmes는 미국에 기록된 최초의 연쇄살인범으로 매우 유명한 인물입니다. 어떻게 호감형인 외모와 명석한 지성으로 그런 짓을 저질렀을까요? 그 능력을 조금 더 좋은 곳에 썼더라면 어땠을까요? 홈즈는 차트를 달리는 남자에서 소개된바가 있습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

이분 매칭(bipartite matching)

그래프에서 정점의 짝을 이루게 해볼까요? 정점은 다른 정점과 짝이 될 수 있는데, 단 여기서 한 정점은 최대 한 정점과 짝을 이룰 수 있는 조건으로 말이죠. 정점이 모두 선택될 필요는 없습니다. 아래의 그래프가 그 예를 보여줍니다. 왼쪽은 1:1 매칭이 된 간선을 선택한 것이고 오른쪽은 1:1 매칭이 되지 않은 그래프입니다. 왼쪽의 그래프는 간선을 3개 갖는데 각 정점은 1:1로 짝을 이루고 있네요. 하지만 오른쪽 그래프에서 주황색 정점은 위, 아래 정점 2개와 짝을 이루고 있으니 1:1 매칭이 되었다고 볼 수 없습니다.

 

정점을 보기좋게 두 그룹으로 나누면 어떨까요? 왼쪽에 정점이 오른쪽에 정점과 연결될 수 있도록 보면 더 보기 편할 거 같습니다.

 

이처럼 그래프의 정점을 두 그룹으로 나눈 후 모든 간선이 서로 다른 그룹의 정점들을 연결할 수 있는 그래프를 이분 그래프라고 합니다. 여기서 가장 큰 매칭을 찾아내는 문제를 최대 매칭 문제(Maximum Matching)라고합니다. 간단하게 이분 매칭이라고 부릅니다.

이분 매칭을 네트워크 유량을 이용하여 풀게 됩니다. 그보다는 아주 간단하게 dfs를 이용해서 풀 수 있습니다. 이분 매칭을 구현한 코드가 아래에 있습니다. (이 코드는 구종만 님의 알고리즘 문제해결 전략의 코드입니다.)

#include<iostream>
#include<algorithm>
#include<vector>
#include<string.h>
using namespace std;

const int MAX_N = 201;
int adj[MAX_N][MAX_N];
vector<bool> visited;
vector<int> partner;
int N, M;
bool dfs(int here) {
	if (visited[here]) return false;
	visited[here] = true;
	for (int there = 1; there <= M; there++) {
		if (adj[here][there]) {
			//there의 짝이 정해지지 않았거나
			//there의 짝이 다른짝과 매칭된다면 true반환
			if (partner[there] == -1 || dfs(partner[there])) {
				partner[there] = here;
				return true;
			}
		}
	}
	return false;
}

int bipartiteMatch() {
	int ret = 0;
	for (int i = 1; i <= N; i++) {
		visited = vector<bool>(N + 1, false);
		if (dfs(i)) ret++;
	}
	return ret;
}

 

왼쪽의 정점은 A, 오른쪽의 정점은 B라고 칭한다면 partner라는 배열은 B와 짝이된 A정점을 뜻합니다. 초기화는 아래와 같이 -1로 초기화시킵니다. partner[b]가 -1이면 아직 짝이 정해지지 않은 정점을 의미합니다.

 

partner = vector<int>(M + 1, -1);

 

전체적으로 보면 왼쪽(here)에서 하나 씩 dfs를 탐색하는데요. dfs를 통해 짝을 찾을 수 있다면 매칭된 수를 하나 증가시킵니다. 코드와 글로는 이해가 어렵습니다. 그림이 있어야겠네요. 처음에 파란색 정점이 dfs를 수행합니다. 이때 조건문을 잘보세요. 설명을 편하게 하기 위해서 왼쪽 정점은 숫자, 오른쪽 정점은 알파벳으로 가정하도록 하겠습니다.

 

 

 

if (adj[here][there]) {
	//there의 짝이 정해지지 않았거나
	//there의 짝이 다른짝과 매칭된다면 true반환
	if (partner[there] == -1 || dfs(partner[there])) {
		partner[there] = here;
		return true;
	}
}

 

정점 1은 정점 a와 연결이 되고 있고(adj[1][a]), partner[a]의 짝은 아직 정해지지 않았으니, partner[a]=1이 되고 dfs는 true를 반환합니다. 그러니 짝을 하나 찾았네요.

 

이제 2번에서 dfs를 수행합니다. 연결된 간선의 정점이 a인데, a는 이미 짝이 있으나, a의 파트너인 1이 다른 짝과 매칭될 수 있는지(dfs(1))을 보는데요. 1은 다시 a로 가려고 하지만 이미 방문된 정점(visited[a]=true)이기 때문에 다른 연결된 c쪽으로 탐색합니다. c는 아직 짝이 정해지지 않았기 때문에 c의 파트너를 1로 정합니다. 그 결과 a의 파트너는 3이 될 수 있네요.

 

 

이제 다음 정점인 3에서 dfs를 시작합니다. 연결된 정점은 a와 c인데요. a는 이미 짝이 정해져있으나, partner[a]에서 dfs를 수행하면 다른 짝을 구할 수도 있으니 dfs(partner[a]) = dfs(1)를 수행합니다. dfs(1)은 dfs를 수행하지 않은 c로 탐색을 수행할텐데, 이때 c와 유일하게 연결된 정점 3은 이미 방문된 상태이므로 결국 다른 짝을 찾지 못했습니다.

이제 3의 다른 정점 c도 역시 같은 방법으로 경로를 찾는데 c는 이전에 방문되었던 상태라서 종료합니다. 결국 3에서 dfs를 수행한 결과 다른 짝을 찾지 못했네요.

 

결국 이렇게 이분 그래프에서 최대 2쌍을 찾을 수 있습니다.

 

예제(BOJ 2188 축사배정)

www.acmicpc.net/problem/2188

 

2188번: 축사 배정

농부 존은 소 축사를 완성하였다. 축사 환경을 쾌적하게 유지하기 위해서, 존은 축사를 M개의 칸으로 구분하고, 한 칸에는 최대 한 마리의 소만 들어가게 계획했다. 첫 주에는 소를 임의 배정해

www.acmicpc.net

이 문제는 위의 코드를 그대로 구현하여 풀 수 있습니다. N마리의 소와 M개의 축사가 있는데, 각 소들은 들어가길 희망하는 축사가 있어 그 외의 축사는 들여보낼 수 없습니다. 소가 희망하는 축사는 여러개가 될 수 있는데요. 이때 소를 최대한으로 들여보낼 수 있는 수를 구하는 것입니다. (1<= N,M <= 200)

첫줄에는 N,M을 입력으로 받고 그 다음부터 차례대로 N 줄까지 1번 소가 원하는 축사의 수, 축사의 번호를 차례대로 입력받습니다. 그때 입출력의 예는 아래와 같습니다.

입력 출력
5 5
2 2 5
3 2 3 4
2 1 5
3 1 2 5
1 2
4

 

 

 

이 문제를 해결할 수 있는 전체 코드는 아래와 같습니다.

#include<iostream>
#include<algorithm>
#include<vector>
#include<string.h>
using namespace std;

/*이분매칭
소와 방을 1:1로 짝을 지을 수 있는 최대수를 반환
*/

const int MAX_N = 201;
int adj[MAX_N][MAX_N];
vector<bool> visited;
vector<int> partner;
int N, M;
bool dfs(int here) {
	if (visited[here]) return false;
	visited[here] = true;
	for (int there = 1; there <= M; there++) {
		if (adj[here][there]) {
			//there의 짝이 정해지지 않았거나
			//there의 짝이 다른짝과 매칭된다면 true반환
			if (partner[there] == -1 || dfs(partner[there])) {
				partner[there] = here;
				return true;
			}
		}
	}
	return false;
}

int bipartiteMatch() {
	int ret = 0;
	for (int i = 1; i <= N; i++) {
		visited = vector<bool>(N + 1, false);
		if (dfs(i)) ret++;
	}
	return ret;
}
int main() {

	cin >> N >> M;
	partner = vector<int>(M + 1, -1);
	for (int from = 1; from <= N; from++) {
		int n;
		cin >> n;
		for (int i = 0; i < n; i++) {
			int to;
			cin >> to;
			adj[from][to] = 1;
		}
	}
	cout << bipartiteMatch() << endl;
	return 0;
}

 

이상으로 그래프 이분 매칭에 대한 개념과 예제 코드였습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

스케줄링의 개념

단일 처리 시스템에서는 실행 중인 프로세스(A)가 존재하는데 다른 프로세스(B)가 입출력을 요청하면 그 프로세스(B)는 이전의 프로세스(A)의 자원을 놓을때까지 대기하고 있어야합니다. 하지만 다중 프로그래밍에서는 여러 프로세스들이 동시에 돌아갈 수 있으며, 프로세스가 자원(프로세서 등)을 요청하면 운영체제는 그 자원을 적절히 분배하여 프로세스에게 할당합니다. 그래서 다음과 같은 장점을 얻을 수 있습니다.

1. CPU의 활용 극대화

2.프로세스 처리율(시간 당 작업량)을 늘릴 수 있습니다.

프로세스는 필요한 자원을 할당받기 위해 큐에 대기합니다. 그래서 그 큐에 있는 프로세스를 어떻게 스케쥴링하는지가 프로세스 스케줄링 알고리즘이라고 합니다. 그레서 어떤 스케줄링 알고리즘이 있는지 봅시다. 

잠깐 프로세스 스케줄링에 관한 용어 몇가지만 알아보고 가도록 합시다. 대기 시간, 실행 시간, 반환 시간입니다. 

용어 설명
대기 시간 자원의 할당을 대기하는 시간을 의미합니다.
실행 시간 실제로 프로세스가 자원을 할당받은 다음 작업을 수행하는 시간을 의미합니다.
반환 시간 작업을 완료하는데 소요되는 전체 시간으로 대기 시간과 실행 시간을 모두 포함합니다. 

 

1 선입선처리 스케줄링(First Come First Served, First In First Out)

선입선처리(FCFS, FIFO) 알고리즘은 말 그대로 먼저 요청한 프로세스가 먼저 자원을 제공받으며 이미 사용중이라면 사용이 끝날때까지 기다려야하는 스케줄링 방식입니다.

프로세스 5개가 있고, 각가 P1의 실행시간 10, P2의 실행시간 5, P3의 실행시간 6, P4의 실행시간 9, P5의 실행시간 10이라고 할때 반환 시간을 알아보도록 하죠.

프로세스 도착 시간 실행 시간
P1 0 10
P2 1 28
P3 2 6
P4 3 4
P5 4 14

 

먼저 요청한 프로세스가 먼저 제공받으니 P1부터 차례대로 자원을 할당받습니다. 그래서 프로세스의 반환시간과 대기시간을 구하면 아래와 같습니다.

로세스 반환시간 대기시간
P1 10 0
P2 37 9
P3 42 36
P4 45 41
P5 58 44
평균 평균 반환 시간
(10+37+42+45+58)/5 = 38.4
평균 대기 시간
(0+9+36+41+44)/5 = 
26
장점 1) 스케줄링이 단순합니다.
2) 모든 프로세스가 실행될 수 있습니다.
3) 프로세서가 지속적으로 유용한 프로세스를 수행하여 처리율이 높습니다.
단점 1) 비선점방식의 스케줄링이므로 대화형 작업에는 부적합합니다.
2) 만약 어떤 프로세스의 수행시간이 길면 대기시간이 늘어납니다. 그래서 짧고 간단한 작업은 계속 기다려야합니다.

2. 최소 작업 우선 스케줄링(Shortest Job First)

프로세스의 실행 시간을 이용하여 가장 짧은 시간을 갖는 프로세스가 먼저 자원을 할당받는 방식입니다. 이 방식은 선점할 수도 있는 스케줄링 방식입니다. 이전에 FIFO방식은 중간에 다른 프로세스가 들어오면 그 프로세스는 대기해야했죠? 이 스케줄링 방식은 선점 또는 비선점이 가능합니다. 위와 같은 프로세스라고 할때 비선점형 SJF 스케줄링의 결과는 아래와 같습니다.

로세스 반환시간 대기시간
P1 10 0
P2 61 33
P3 18 12
P4 11 7
P5 30 16
평균 평균 반환 시간
(10+61+18+11+30)/5 = 26
평균 대기 시간
(0+33+12+7+16)/5 = 13.6

 

자 P1이 먼저 요청해서 자원을 할당받아 놓은 상태인데, P2가 도착합니다. 비선점형이기 때문에 P2는 우선 기다리고 있는데 P1의 작업이 끝나기 전에 P3, P4 ,P5의 요청이 도착하네요. 그러면 실행 시간이 가장 작은 P4가 자원을 할당받아 쓰고 그 다음인 P3이 자원을 할당받아 사용합니다. 이렇게 되면 위와 같은 결과가 도출되지요.

그렇다면 비선점형 SJF는 어떨까요? 아래의 표가 선점형 SJF의 결과입니다.

로세스 반환시간 대기시간
P1 20 10
P2 61 33
P3 10 5
P4 4 0
P5 30 16
평균 평균 반환 시간
(20+61+10+4+30)/5 = 25
평균 대기 시간
(10+33+4+0+16)/5 = 12.6

이해를 돕기 위해서 아래의 간트 차트를 보세요.  P1이 먼저 도착해서 수행하고 있는 와중에 P2가 도착하는데요. P2는 수행되어야할 시간이 P1보다 크니 P1은 계속 작업을 수행합니다. 그러다가 P3가 도착하는데 P1이 수행해야할 시간보다 P3의 수행시간이 더 짧으니 P3가 작업을 수행합니다. 그런데 P4가 다음에 도착하네요. P4가 더 적은 시간을 갖으니 P4를 수행합니다. 이때 P4가 끝나면 남아있는 프로세스 중에서 가장 적은 수행시간을 갖는 P3의 작업을 이어서 하게 됩니다.

장점 1) 항상 짧은 작업을 먼저 처리하게 되니까 평균 대기시간이 가장 짧습니다.
단점 1) 수행시간이 긴 작업은 짧은 작업에 밀려 기아가 발생합니다.
2) 실행 시간을 예측할 수 없어 실용적이지 못합니다.
3) 짧은 작업이 먼저 실행되므로 공정하지 못한 정책입니다.

 

 

 

3. 우선순위 스케줄링

우선순위 스케줄링은 프로세스마다 우선순위라는 속성이 붙게 됩니다. 우선순위 스케줄링도 역시 선점, 비선점형으로 스케줄링이 가능합니다. 숫자가 높을 수록 우선순위가 높고 만약 우선순위가 같다면 FIFO방식으로 동작합니다.

프로세스 도착 시간 실행 시간 우선순위
P1 0 10 3
P2 1 28 2
P3 2 6 4
P4 3 4 1
P5 4 14 2

 

그래서 아래는 선점형에서의 결과를 나타낸 표입니다. 약간 헷갈릴 수 있을텐데요. P1 먼저 수행하다가 P3가 오면 우선 순위가 P3가 더 높으니까 P1은 대기하고 P3가 작업을 수행합니다.

로세스 반환시간 대기시간
P1 16 6
P2 43 15
P3 6 0
P4 59 55
P5 54 40
평균 평균 반환 시간
(16+43+6+59+54)/5 = 35.6
평균 대기 시간
(6+15+0+55+40)/5 = 23.2

다음은 비선점형일때의 결과 표입니다.

로세스 반환시간 대기시간
P1 10 0
P2 43 15
P3 14 8
P4 59 55
P5 54 40
평균 평균 반환 시간
(10+43+14+59+54)/5 = 36
평균 대기 시간
(0+15+8+55+40)/5 = 23.6

 

만약 우선순위가 낮은 프로세스가 높은 프로세스에 의해 실행이 되고 있지 않은 상황이라면 어떻게 될까요? 이때는 그 프로세스의 우선순위를 점차 높여 처리받게끔 하는 에이징이라는 기법을 사용합니다.

장점 1) 각 프로세스의 중요성에 따라서 작업을 수행하기 때문에 합리적입니다.
2) 실시간 시스템에서 사용가능합니다.
단점 1) 높은 우선순위를 갖는 프로세스가 계속적으로 스케줄링이 되면 우선순위가 낮은 프로세스는 자원을 할당받지 못하기 때문에 기아가 발생할 수 있습니다.

4. 라운드 로빈 스케줄링(Round-Robin)

라운드 로빈 스케줄링은 시분할 시스템을 위해 설계된 스케줄링 기법입니다. 이 스케줄링은 작은 단위의 시간인 시간 할당량(Time-Slice)을 정의하여 그 시간 만큼 자원을 할당하는 방식입니다. 그래서 그 시간안에 작업을 끝내지 못하면 다음 프로세스가 다시 그 시간만큼 자원을 할당받아씁니다. 여기서 시간 할당량을 5로 정하고 간트 차트와 결과를 보면 아래와 같습니다. 

각 프로세스들은 공정하게 5만큼의 시간 동안 작업을 진행하는 것을 볼 수 있네요. 

로세스 반환시간 대기시간
P1 29 19
P2 61 33
P3 33 27
P4 16 7
P5 45 31
평균 평균 반환 시간
(29+61+33+16+45)/5 = 36.8
평균 대기 시간
(19+33+27+7+31)/5 = 23.4
장점 1) 모든 프로세스가 공정하게 스케줄링을 받을 수 있습니다.
2) 실행 큐에 프로세스의 수를 알고 있을때 유용합니다.
3) 프로세스의 짧은 응답시간을 갖고 최악의 응답시간을 알 수 있습니다.
4) 평균 대기시간이 FIFO와 SJF보다 적습니다.
단점 1) 성능은 규정 시간량의 길이에 따라 달라지므로 작업이 비슷한 길이가 좋은데, 너무 길면 FIFO로 변하고 짧으면 문맥 교환(Context Switching) 비용이 증가합니다.
2) 하드웨어적 타이머가 필요합니다.
3) 미완성 작업은 규정 시간량(시간 할당량)을 마친 후 프로세서를 기다리니까 평균 처리 시간이 높습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

보안의 3대 요소

컴퓨터 정보 보안에 대해서 진지하게 공부하기로 마음 먹었다면 꼭 알고 있어야 할 개념이 있습니다. 바로 정보보안의 세 가지 목표입니다. 개념은 어렵지 않으니 차근차근 읽어보면 이해하실 수 있을 겁니다.

기밀성(Confidentiality) 김일성아닙니다.

기밀성이라하면 통신하는 당사자만이 아는 비밀을 말하는데 정보보안에서는 이것을 기밀이라고 합니다. 정보보안에서 가장 많이 요구하는 조건이 바로 기밀이 보장되는 기밀성입니다. 군대나 은행, 산업체의 조직에서 기밀성이 필수적으로 요구되며 정보의 보관뿐만 아니라 정보의 전송에도 적용됩니다. 기밀성을 지키기 위해서는 데이터를 다른 사람들이 이해하지 못하도록 암호화하는 방법이 있습니다.

보안에서 기밀성을 위협하는 공격은 무엇이 있을까요? 간단하게 생각하면 남의 말을 엿듣는거나 훔치는 것이라고 생각하시면 이해하기 쉽습니다. 간단하게 몇 가지 알아보도록 해보지요.

- 스누핑(Snoofing)

스누핑은 비인가된 사용자가 중요 데이터에 접근하는 것이나 탈취를 의미합니다. 예를 들면, 인터넷으로 전성되는 파일이 기밀정보를 담고 있는데, 나쁜 노무 자식이 그 정보를 가로채고 자신의 이익을 위해 사용할 수 있지요.

- 트래픽 분석(Traffic Analysis)

데이터를 이해할 수 없더라도 공격자가 온라인 상에 전송되는 트래픽을 분석함으로써 다른 정보를 얻을 수 있습니다. 예를 들면, 공격자는 송수신자의 ip, 또는 이메일 주소, 또는 전송 성향을 추측하는데 도움이 되는 질의, 응답을 수집할 수 있습니다.

무결성(Integrity)

변경이 허락된 사람에게서 인가된 메카니즘을 통해서만 이뤄져야하는 것을 의미합니다. 정보는 시시각각 변화되는데, 이것이 인정된, 인가된 사람만 변경하는 것을 보장해야합니다. 허가되지 않은 사람이 변경했을때 이를 즉각 알 수 있어야합니다. 무결성을 위협하는 공격은 변경(Modification), 가장(Masquerading), 재연(Replaying), 부인(Repudiation)이 있습니다.

- 변경(Modification)

공격자는 정보를 가로채서 자신에게 이익이 될 수 있게 조작하는 것을 의미합니다. 원래는 송신자에게 줘야할 데이터를 중간에 공격자가 가로채서 자신의 주소로 바꾸거나 하는 것이 있습니다.

 

 

- 가장(Masquerading)

공격자가 인가된 사용자인척 하는 것을 의미합니다. 정보보안에서 스푸핑(Spoofing)이라는 공격 기법이 있는데요. IP나 MAC을 인가된 시스템의 주소로 변경하여 인가된 자인 척 정보를 획득합니다.

- 재연(Replaying)

재연이라는 것은 우선 공격자가 정보를 가로채고 난 이후 나중에 그 정보를 다시 사용함을 의미합니다. 즉, 공격자가 우선 전송된 데이터를 가지고 있다가 자신이 필요할 타이밍에 그 정보를 다시 보내는 것이죠. 예를 들어, 어떤 사람이 공격자에게 송금하라고 하는 데이터를 공격자가 가지고 있는데, 이 정보를 계속 이용해서 그 사람의 계좌에서 계속 돈을 공격자에게 보내는 공격이 있습니다. 그래서 재연이라는 공격을 막기위해 OTP(One-Time Password)를 사용합니다.

- 부인(Repudiation)

현실에서는 구라라고 하죠. 정보보안에서 부인이라함은 정보를 전송하거나 변경했을때 그 사실을 인정하지 않는 것을 말합니다. 현실에서 나중에 거짓말하지 못하도록 서명이나 도장을 찍듯, 정보보안에서도 디지털화된 서명(Digital Signature)이 적용됩니다.

가용성(Availability)

가용성은 정보가 사용가능해야한다는 것입니다. 중요한 정보를 사용하지 못 할 경우 심각한 피해를 입을 수 있으므로 정보에 대한 접근이나 사용이 인가된 자에 의해서 사용이 가능해야합니다. 가용성을 위협하는 공격은 아예 그 시스템을 마비시키는 서비스 거부 공격(DoS)이 있습니다. 한번쯤 어디선가 들어보셨죠?

- 서비스 거부(Denial of Service)

서비스 거부 공격은 가용성을 무너뜨리는 가장 일반적인 형태의 공격입니다. 이 공격을 통해서 서비스를 느리게 만들거나 아예 시스템을 마비시키는 공격입니다. 여러분은 이미 경험해본적이 많을 텐데요. 수강 신청 기간에 웹 페이지가 마비되는 현상을 경험해본적 있으시죠? 이런 현상은 공격이라고 보기는 어렵지만 시스템을 이렇게 만드는 것이 DoS 공격입니다. 여러분도 만들 수 있는데요. 무한 루프를 꾸준히 생성하는 프로세스를 만들 수도 있지요.

 

보안 서비스(Security Services)

ITU-T에서는 이러한 보안의 세 가지 목표를 달성하기 위해서 정보 보안에서는 5가지 서비스를 정의하게 됩니다.

 

 

 

데이터 기밀성(Data Confidentiality)

데이터 기밀성은 공격자가 데이터를 볼 수 없게 보호합니다. 스누핑과 트래픽 분석 공격을 막기 위해서 고안되었습니다. 간단하게 남들이 보지 못하게 만드는 것으로 수 많은 암호화 알고리즘을 사용하여 달성할 수 있습니다. 암호화에는 크게 대칭 암호, 비대칭 암호 알고리즘이 존재하지요. 데이터 기밀성은 스누핑(Snoofing)과 트래픽 분석 (Traffic Anaysis)을 막을 수 있습니다.

데이터 무결성(Data Integrity)

데이터의 무결성은 앞서 말한 변경, 삽입, 삭제, 재연 등으로부터 정보를 보호하는 것으로 메시지의 일부 또는 전체를 보호합니다. 예를 들면 암호학적 해시 함수를 통해서 데이터의 해시값을 만들어 전송하고 수신받는 사람은 전송받은 데이터를 기반으로 해시값을 생성하여 비교해보는 방법이 있습니다. 간단하게 암호학적 해시라함은 데이터를 암호학적 해시함수를 통해서 1:1 대응되는 다른 데이터로 만드는 암호학적 기법입니다. 이러한 암호학적 해시는 여러 해시 알고리즘이 존재합니다.

인증(Authentication)

그 사람이 인가된, 허가된 사람이 맞는지, 또는 데이터가 원하는 데이터가 맞는지확인하는 메커니즘을 사용합니다. 연결 지향(Connection-Oriented) 통신에서 연결이 되었을때 송신자 또는 수신자에 인증이 필요할 수 있습니다. 또는 비연결 지향(Connectionless) 통신에서는 그 데이터의 출처를 인증이 필요할 수 있습니다.

부인봉쇄(Nonrepudiation)

데이터의 송신자가 전송한 데이터가 맞다는 사실, 또는 수신자가 데이터를 수신했다는 사실을 거부할 수 없게 만드는 정보 보안 서비스입니다.

예를 들어, 만약 서버에서 이상한 IP주소로 공격이 발생했을때 시스템에 남아있는 로그를 통해서 그 IP에서 공격 사실을 부인봉쇄할 수 있습니다. 그러니 서버에서는 공격 탐지와 사실을 확인하기 위해 다양한 로그를 남깁니다.

접근제어(Access Control)

비인가된 접근을 제어하여 데이터를 보호하는 보안 서비스를 말합니다. 예를 들어 리눅스 시스템에서 파일의 주인은 데이터를 쓰고(Write) 읽고(Read) 실행(Execute)할 수 있고, 같은 그룹 사용자는 파일을 읽고 실행하는 권한만 있으며, 그 외의 사람은 읽기만 할 수 있습니다

 

 

.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

뱀 섬 (퀘이마다 그란데 섬)

퀘이마다 그란데 섬은 브라질 상파울루 해안가에서 33km 떨어진 섬입니다. 여의도의 약 2배의 크기를 가진 이 섬은 사람이 살지 않는 무인도인데요. 사람이 살 수도 없는 섬이기도 합니다.

왜 들어갈 수 없을까요?

 

이 섬의 주인은 뱀이기 때문인데요. 그것도 무시무시한 독사가 무려 4000마리 이상이 서식하고 있기 때문입니다. 

그냥 독사가 아니라 지구 최강의 독을 자랑하는 독사들이 우글우글대고 있는 섬입니다.

1 제곱미터당 3~5마리의 뱀이 있다고 보시면 됩니다. 한걸음 한걸음 뱀 밭이죠.

 

이 독사들 중에서 보스가 골든 랜스 헤드 바이퍼라는 뱀입니다. 생긴거 진짜 무섭게 생긴것도 같고 멋있게 생긴것도 같네요. 얘한테 물리면 끝났다고 보시면 됩니다. 빠르게 퍼지는 맹독은 위출혈, 신장 부전, 뇌출혈을 일으켜 사망이 이르게 합니다.

 

왜 뱀만 살까?

이 뱀은 지상에 있는 뱀보다 무려 5배 이상이나 강한 독을 지녔다고 하며 물리면 사망입니다. 왜 이섬은 뱀만 득실거리는 섬이 되었을까요?

 

두 가지의 가설이 존재하는데요. 

1) 첫번째는 해적들이 보물을 이 섬에 숨겨놓고 훔쳐가지 못하도록 뱀을 풀어놨다는 것입니다. 실제 포루투칼의 정복자 알레이소가 잉카의 보물을 찾아 떠났는데요. 값 비싼 보물들을 가져오다가 상파울루 인근 섬에 숨겼다고 하는 이야기가 있습니다.

잠깐 드는 생각이 어차피 보물 숨겨놓고 너도 나도 못들어가게 할려면 독사를 왜 풀어놓은건지 의문이네요. 혹시 다른 입구가 존재하는 것일까요?

 

2) 또 다른 가설은 원래 이섬은 육지였다고 하고, 해수면이 상승하여 육지와 분리된 섬이 되었다고 하는 가설이 있습니다. 강한 맹독을 지는 이유도 이 섬이 육지와 분리되어 먹을 것이 떨어지자 새를 잡아먹기 위해서 단시간에 독을 빠르게 퍼지게 진화했다고 합니다.

아직은 정확히 밝혀지지 않은 것 같습니다.

 

실제 이 섬은 1985년 브라질 당국에 의해 출입 제한 구역이 되었고, 몇몇 의학적 연구와 관련된 조사만 허용한다고 합니다. 그리고 출입시에 엄벌에 처한다고 하지요. 그럼에도 불구하고 실제로 들어가서 시체로 돌아오는 경우가 많다고 하는데요. 그 이유는 이 뱀의 가격 때문이죠. 이 뱀은 퀘이마다 그란데 섬에서만 존재하며 황금색을 띄기 때문에 무척이나 비싼 값에 거래가 된다고 합니다.

한 마리당 무려 3800만원!

이 정도면 위험을 감수하고서라도 이 섬에서 밤을 잡을만 한가요?

 

섬에만 안들어가면 되고 섬 주위에 배를 타고 관광하는 것은 허용이 된다고 합니다. 유튜브에 보면 아예 수영하는 사람도 있더군요.

 

여기 유튜브 영상도 같이 보시면 좋을 것 같습니다.

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

해싱(Hashing)

해싱(Hashing)은 데이터를 관리하는 고급 기법으로 검색(search), 삽입(insert), 삭제(delete)를 단시간(O(1))에 할 수 있게 만들어주는 기법입니다.

예를 들면, 리스트에서 단어를 찾는 시간이 O(n)시간이 걸리고, 이진 트리의 경우 O(nlogn)이 걸리죠. 그런데 우리는 이것을 해싱이란 기법을 사용하여 O(1)만에 해결할 수 있습니다. 진짜 죠낸 빠르네요.

해싱은 산술적 연산을 이용해서 구현해야합니다. 이런 해싱은 크게 두 가지 종류가 있는데요. 하나는 정적 해싱(static hasing)이고 다른 하나는 동적 해싱(dynamic hasing)입니다. 이번 포스팅은 정적 해싱에 대한 이야기를 합니다.

 

정적 해싱(Static Hashing)

정적 해싱이라는 기법은 고정 크기의 버킷을 갖는 해시 테이블(담을 그릇)에 데이터를 담는 것을 말합니다.

해시 테이블(Hash Table)

설명 전에 해시 테이블(Hash Table)에 관해서 알고 가야합니다. 해시 테이블은 키(Key)값(Value)으로 구성되어 있는 데이터가 저장된 테이블이라고 보시면 됩니다. 해시 테이블은 행과 열로 구성되어 있다고 생각하세요. 여기서 두 가지 용어가 있는데 버킷(bucket)슬롯(slot)입니다.

 

버킷(bucket) : 버킷은 해시 테이블의 행 인덱스(주소)라고 입니다. 

슬롯(slot) : 슬롯은 그 행을 열의 인덱스(주소)라고 생각하시면 됩니다.

 

아래의 그림은 버킷이 11, 슬롯이 3인 해시 테이블입니다. 여기서 data가 전부 차있는 버킷도 존재하고 그렇지 않은 곳도 존재합니다. bucket[0]을 보시면 이미 데이터가 가득차있네요. 다음 들어올 데이터가 bucket[0]에 들어갈 데이터라면 어떻게 될까요? 넣을 수 없는 상황이 되겠죠? 

그것을 우리는 오버플로(Overflow)라고 하며 이 문제를 해결해야합니다. 잠시후 설명하도록 하겠습니다.

 

 

쳐넣는거는 알겠는데 어떻게 넣냐구요? 해시 함수를 이용해서 넣을 수 있습니다.

 

해시 함수(Hash Function)

해시 함수는 h()라 하고 찾을 키를 k라고 한다면 해시 함수는 h(k)라 표현합니다. 해시 함수는 k를 이용해서 해시 테이블에 매핑(mapping)하는 역할을 하게 됩니다. 즉, 데이터를 집어넣을 장소(주소)를 정합니다. 좋은 해시 함수는 계산이 쉬워야하고, 출돌을 최소화 시켜야합니다. 그리고 모든 버킷에  데이터가 고르게 분포되어야합니다. 앞서말한 오버플로를 최소화하기 위함이죠. 해시 함수에는 어떤 것들이 있을까요?

 

참고로 아래의 해시함수들은 모두 키가 양의 정수를 갖는데, 만약 키를 문자나 문자열로 쓰고 싶다면 문자(열)을 정수로 변환시키는 작업이 필요합니다.

1) Division

나눗셈을 이용하는 해시 함수입니다. 모듈러 연산(나머지를 구하는 연산으로 % 아시죠?)을 이용하여 집어 넣을 곳을 정합니다. 키들은 음수이면 안된다는 가정이 있습니다.

 

h(k) = k % D

 

해시 테이블은 적어도 D개의 버킷을 가지고 있어야합니다. k가 15이고 D가 7이라면 h(15) = 15 % 7 = 1 이 됩니다.

 

2) Mid-Square

키를 제곱하여 버킷을 정하는 건데요. 키를 제곱한 후 중간에 몇 비트를 선택하여 버킷의 주소를 구합니다. r비트를 선택하면 해시 함수 결과로 나올 수 있는 값의 범위는 0 ~ (2^r) -1이 되겠죠?

r을 3로 정해보고,  key값을 121으로 정해서 계산해보면 121^2 = 14,641인데 이것의 가운데 3개를 가져오면 h(121) = 464가 됩니다.

 

 

 

3) Folding

폴딩은 키를 몇몇 부분(Part)로 나눈후 그 값을 더하여 해시 함수의 결과를 도출합니다. 방식에 따라 두가지가 있는데요. 그냥 부분을 일정하게 나눈 후 더하는 방식인 시프트 폴딩(Shift Folding)과 부분의 경계를 뒤집어서 계산하는 경계폴딩(Boundary Folding)이 있습니다.

 

1. 시프트 폴딩(Shift Folding)

k가 12345678910일때 3개의 10진수로 나눈다면 h(12345678910) = 123 + 456 + 789 + 10이 되고 더하게 되면 1378이 되므로 h(12345678910) = 1,378 입니다.

 

 

2. 경계 폴딩(Boundary Folding)

k가 아까와 같이 12345678910이고 3개의 10진수로 나눈다면 h(12345678910) = 123 + 654 + 789 + 01이며 결과는 1,567입니다. 456과 10이 뒤집혀진 것을 알 수 있네요.

 

 

4) Digit Analysis

이 방식은 이미 모든 키들을 알고 있는 정적 파일에 유용한 방법으로 각 키의 주소를 결정할 때 많이 나오는 수는 제외하고 적게 나오는 키의 수만 선택됩니다. 

예를 하나 들어보겠습니다. 키가 5개가 미리 정해져있다고 칩시다. 각 키는 0112311, 0234522, 0167811, 0291022, 0111222 에서 숫자 3개를 선택해서 해시 함수의 결과를 꺼내면 hash(0112311) = 123, hash(0234522) = 345, hash(0167811) = 678, hash(0291022)= 910, hash(0111222) = 112 가 됩니다. 01 11 02 22는 많이 겹치는 수이기 때문에 고르지 않습니다.

이름에서 알 수 있듯이 키의 숫자들을 분석을 해야하는데요. 분석하려면 미리 결정된 데이터들이 있어야합니다. 즉, 키가 미리 결정되어져 있어야한다는 의미입니다. 그러니까 왜 정적 파일에 유용한지 이해가 가시죠?

 

오버플로 핸들링(Overflow Handling)

앞서 오버플로에 대해서 이야기했었죠? 오버플로를 해결하기 위해서는 두 가지 핸들링 기법이 있는데, 개방 주소법(Open Address)체이닝(Chaining) 방식이 있습니다.

 

1) 개방 주소(Open Address)

오버플로가 발생했을때 공간이 남는 버킷에 집어 넣는 것으로 공간이 남는 버킷 어느 곳이나 있으면 넣는 다고 하여 개방 주소라고 합니다. 남는 곳을 어떻게 구햐냐에 따라 4 가지 방식이 존재합니다.

 

1. 선형 탐사(Linear Probing)

선형 탐사만 알아도 나머지 probing방식은 이해할 수 있습니다. 고정된 폭을 정해서 그 폭만큼 버킷의 주소를 이동해서 남는 공간이 있는지 확인 후에 있으면 집어넣는 방식입니다. 그래도 남는 공간이 없다면 다시 그 폭만큼 버킷의 주소를 이동하죠. 만약 해시 테이블의 공간이 모자란다면 해시 테이블의 크기를 증가시키는데, 전부 찰때 증가시키는 것이 아니고 75% 정도 채워지면 해시 테이블의 크기를 증가시킵니다.

 

만약 폭을 i라고 하고 버킷의 크기를 b라고 한다면 해시 테이블은 (h(k) + i ) % b 의 식으로 계산됩니다.

 

 

 

2. 제곱 탐사(Quadratic Probing)

감 오시죠? 폭을 제곱하여 오버플로를 핸들링합니다. (h(k) + (i^2)) %b 로 계산됩니다. 만약 오버플로가 발생하면 i=1로 한칸 이동하여 찾고 그래도 실패하면 그 다음은 i=2가 되어 4칸을 이동하고, 그 다음은 i=3이 되어 9칸을 이동하여 검사하는 방식입니다.

 

아래 그림은 선형 탐사와 제곱 탐사를 비교한 그림입니다.

3. 무작위 탐사(Qandom Probing)

감 오시죠? 랜덤한 값을 폭으로 두고 계산합니다.

 

4. 이중 해시(Double Hashing)

해싱한 값을 한 번 더 해싱하는 방식입니다. 다른 방식보다 연산하는 시간이 걸립니다. 해시 연산을 한 번 더 하기 때문이죠.

 

2) 체이닝(Chaining)

체이닝은 이름과 같이 체인같이 연결하여 오버플로를 막는 방법인데요. 연결 리스트(Linked List) 배우셨죠? 각 버킷이 연결 리스트가 되고 원소가 계속 추가 될 수 있습니다.

아래는 체이닝 기법을 적용한 버킷이 6개있는 해시 테이블입니다.

이때 삽입할때 연결리스트의 앞에 삽입합니다. 이유는 매번 끝에 원소를 삽입하면 연결리스트에 끝까지 도달해야되기 때문에 삽입 시간이 오래 걸리기 때문입니다.

 

 

개방주소법이나 체이닝 방식이나 최악의 경우(Worst Case) 성공할때까지 검색하는 시간은 O(n)의 시간이 걸릴 수 있습니다. 하지만 균형잡힌 검색 트리(Balanced Search Tree)를 이용하면 이 시간은 O(logn)까지 줄일 수 있습니다.

 

다음 포스팅은 동적 해싱에 대한 이야기를 하도록 하겠습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

쿨다라(Kuldhara)

쿨다라(Kuldhara)는 인도 라자스탄의 자이살메르 지구에 한 마을입니다. 단순 그냥 버려진 마을은 아니고 저주가 걸려있다는 소문이 자자한 그런 마을입니다. 

원래는 1200년대부터 사람이 살다가 1800년대에 버려진 마을입니다.

버려진 이유에 대해서는 두 가지 이야기가 있는데요. 지역에 나도는 소문에 의하면 이렇습니다. 원래 이전에 Paliwal Brahmins라는 그래도 잘 사는 사람들이 거주했다고 합니다. 그러던 중 살림 싱(Samlim Singh)이라는 마을의 지배자가 있었나봐요. 그 인간이 마을 족장의 딸을 사랑했는데요. 아버지가 그 딸을 내어주지 않겠다고 거절했습니다.

 

원래 살림 싱이라는 사람자체가 평이 안좋게 나있었고 방탕하고 문란했다고 하죠. 살림 싱은 자신의 제안을 거절하자 마을 주민을 괴롭히기로 하는데요. 세금 등을 무자비하게 거둬들이는 등의 박해로 사람들을 못살게 굴거라고 협박했습니다. 마치 누군가가 생각이 나는 순간이네요. 우리나라도 그렇게 대처하는 사람있잖아요 문으로 시작하는

 

마을 사람들은 족장의 딸과 본인 자신들을 위해서 마을을 떠나기로 하였고 그날 새벽 마을을 떠나게 됩니다. 그러면서 마을에 저주를 걸었다고 하죠. 하지만 그 날, 마을 사람들이 떠나는 모습을 본 사람이 아무도 없고, 어디로 갔는지도 모른다고 합니다.

 

또 다른 이유가 있는데요. 2017년 연구에 따르면 지진에 의해서도 마을이 파괴되었다고 합니다. 어떤 이유가 되었건 현재는 버려진 마을이 되었습니다.

 

실제 조사된 초자연 현상

2010년에 Indian Paranormal Society의 한 팀이 이곳에 귀신이 있는지 알아내려고 12시간 동안 머물러있었는데요. 그들이 경험한 초자연 현상을 대충 이렇습니다.

 

- 누군가가 어깨를 두드렸는데 아무도 없는 경험(너무 흔하네요.)

- 귀신들이 자신의 이름을 밝힌 경험

- 특정 구역에 온도차가 심하게 나는 이상 현상

- 움직이지 않는 그림자

 

그밖에도 많겠죠? 심령 체험을 위해 사람들이 방문을 하는 경우가 많은데요.

 

실제 낮에가면 사람들도 있어서 무섭지는 않는 곳이지만 밤에 가면 분위기가 다릅니다. 아래의 사진의 하얀 소복을 입은 마귀같은 것은 실제 영상으로 돌아다닙니다. 그러나 아직 발업이 안됐는지 거동이 불편해보입니다.

 

아래는 kuldhara에 대한 설명과 실제 kuldhara를 밤에 돌아다니는 영상입니다. 위의 거동이 불편한 할머니도 나오십니다. 주작일수도 있지만 리얼하기는 하네요.

 

 

 

만약 여러분이 인도 자이살메르에 여행을 가게 된다면 쿨다라를 한번 가보는 것은 어떨까요? 워낙 유명해져서 정부는 아예 이곳을 관광 지역으로 개발하기로 합니다. 그래서 제한구역이 아니라서 밤,낮 가리지 않고 들어가도 되는 것으로 알고 있는데요. 한 번 가서 초자연 현상을 경험하는 것도 좋은 추억이 될 것 같습니다. 전 안갑니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,