본문 바로가기

42cursus

minishell - 명령어와 외부함수 정리하기 (command & External functions, 42seoul)

명령어 정리

echo

- 출력 관리

 

cd, pwd

- 디렉터리 관리

 

export, unset, env

- 환경변수 관리

- env와 export차이가 하나씩 존재했음

-> _=/usr/bin/env(env) , OLDPWD(export)

 

exit

- 프로세스 종료

 

세미콜론(;)

- 명령어 구분, 차례대로 실행 (실패해도 그대로 실행함, &&와 다름)

 

따옴표 quote(', ")

- SingleQuote : 모든 것을 메타문자가 아닌 문자 그대로 사용하게 함

- DoubleQuote : $, \, `을 메타문자로 인식, 나머지는 문자 그대로

- ex) echo " '$a' "

 

리디렉션 (<, >, >>)

< : 파일의 내용을 표준 입력으로 보내기

> : 표준 출력을 파일로 보내기

>> : 표준 출력을 파일로 보내는데, 추가모드

 

파이프 (|)

- 왼쪽의 표준 출력을 오른쪽의 표준 입력으로 처리

 

$char

- 변수

- $? : 마지막으로 실행된 명령의 종료 상태 (리턴 값)

 -> ex) 없는 명령 후, echo $? (127)


스크립트 제어 (Ctrl + C, D, \)

Ctrl-C : 프로세스 종료, ( SIGINT )

Ctrl-D : EOF를 표준입력으로 보냄 (라인 첫부분에서만 작동), 입력이 아닌경우 exit함

Ctrl-\ : 프로세스 종료, ( SIGQUIT )

superuser.com/questions/169051/whats-the-difference-between-c-and-d-for-unix-mac-os-x-terminal

 

 

 

모르는 외부함수 정리

42norm에서 허용되지 않은 매크로 함수는 사용할 수 없다함

 

char *getcwd(char *buf, size_t size);

현재 작업 디렉터리 이름 얻기 (절대 경로)

파라미터

 - buf : 현재 경로가 담길 공간

 - size : buf의 사이즈

반환

 - 성공 시 buf의 포인터

 - 실패 시 NULL

 

int chdir(const char *path);

현재 작업 디렉터리 변경

파라미터

 - path : 변경할 디렉터리 경로

반환

 - 성공 시 0

 - 실패 시 -1

 

DIR *opendir(const char *name);

디렉터리 열기

파라미터

 - name : 파일 경로

반환값

 - 성공 시 열린 디렉터리 스트림 포인터

 - 실패 시 NULL 

 

struct dirent *readdir(DIR *dirp);

디렉터리 읽기

파라미터

 - dirp : 디렉터리 스트림 포인터

반환값

 - 성공 시 "디렉터리 엔트리 포인터"

 - 실패 시 NULL 

cf) dirent 구조체에서 d_type과 d_name을 가지고 놀 것

 -> dirent와 d_type같은 경우는 man dirent에서 확인할 것

- 순서대로 하나씩 dirent를 뱉으므로 NULL일 때까지 읽어주면 됨

 

 

int closedir(Dir *dirp);

디렉터리 닫기

파라미터

 - dirp : 디렉터리 스트림 포인터

반환값

 - 성공 시 0

 - 실패 시 -1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include <unistd.h>
 
# define DIRTYPE_TO_STRING(X)                        \
                ((X) == DT_DIR ? "directory" :        \
                 (X) == DT_REG ? "regular file" :    \
                                  "unknown file")
 
int main(void)
{
    char path[256];
    memset(path, 0256);
    if (getcwd(path, 256== NULL)
    {
        printf("%s", strerror(errno));
        return (-1);
    }
    printf("Before chdir, current path : %s\n", path);
 
    if (chdir(".."== -1)
    {
        printf("%s", strerror(errno));
        return (-1);
    }
    if (getcwd(path, 256== NULL)
    {
        printf("%s", strerror(errno));
        return (-1);
    }
    printf("after chdir, current path : %s\n", path);
 
    DIR *p_dir;
    if ((p_dir = opendir(path)) == NULL)
    {
        printf("fail opendir()\n");
        return (-1);
    }
 
    struct dirent *dir_ent;
    while ((dir_ent = readdir(p_dir)) != NULL)
    {
        printf("*****************************************\n");
        printf("d_name : %s\n", dir_ent->d_name);
        printf("d_type : %s\n", DIRTYPE_TO_STRING(dir_ent->d_type));
        printf("*****************************************\n");
    }
 
    if (closedir(p_dir) == -1)
    {
        printf("fail closedir()\n");
        return (-1);
    }
 
    return (0);
}
 
cs

 

char *strerror(int errno);

errno를 정수 값이 아닌 message형태로 뽑아줌

파라미터

 - errno : errno.h의 errno 넣어주면 됨

반환 값

 - 정상적인 errno : message

 - 비정상적인 errno : unknown error message

 

extern int errno

syscall 에러의 형태를 define된 정수값으로 들고 있음

errno : 0은 에러가 없는 것

 

int dup(int fd);

fd를 복제함, 현재 fd가 가르키는 파일인 파일디스크립터를 복제해줌 (같은 정수가 아님)

파라미터

 - fd : 복제하고 싶은 파일디스크립터

반환 값

 - 성공 시 : 새 파일 디스크립터

 - 실패 시 : -1

 

int dup2(int fd, int fd2);

fd를 복제함, fd2가 기존의 fd쪽을 가르키게 함

즉, 새로 복제된 fd의 값을 fd2로 지정하는 것

만약 fd2가 이미 열려있다면(가르키고 있다면) 그것을 닫고 복제함

파라미터

 - fd : 복제하고 싶은 파일디스크립터

 - fd2 : 복제될 파일디스크립터

반환 값

 - 성공 시 : 복제 된 fd2값

 - 실패 시 : -1

cf) 0은 stdin, 1은 stdout, 2는 stderr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
 
int        main(void// 짧은 코드를 위해 예외처리를 하지 않음
{
    int fd1 = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    int fd2 = dup(fd1);
    int fd3 = 5;
    printf("fd1: %d, fd2: %d\n", fd1, fd2); // fd1 : 3, fd2 : 4
 
    write(fd1, "Hello\n"6);
    write(fd2, "World\n"6); // write on the same file
 
    int ret1 = dup2(fd2, fd3);
    printf("fd2: %d, fd3: %d, ret1: %d\n", fd2, fd3, ret1); // fd2 : 4, fd3: 5, ret1: 5
    write(fd3, "!!!!!\n"6); // write on the same file
    write(ret1, "?????\n"6);// write on the same file
 
    int ret2 = dup2(1, fd3); //열려있는 fd3를 1이 가르키는 stdout를 가르키게 함
    printf("fd3: %d, ret2: %d\n", fd3, ret2); // fd3: 5, ret2: 5
    write(fd3, "zzzzz\n"6); // write to stdout
    write(ret2, "hhhhh\n"6); //wrtie to stdout
 
    if (close(fd1) == -1)
    {
        printf("close(fd1) error\n");
        return (-1);
    }
    if (close(fd2) == -1// no error occurred
    {
        printf("close(fd2) error\n");
        return (-1);
    }
    return (0);
}
 
cs

 

int stat(const char *pathname, struct stat *statbuf);

int fstat(int fd, struct stat *statbuf);

int lstat(const char *pathname, struct stat *statbuf);

파일의 속성을 조회함

파라미터

- pathname : 속성을 조회하고 싶은 파일경로

- fd : 속성을 조회하고 싶은 열린 파일 디스크립터

- statbuf : 파일 속성을 저장할 버퍼

반환 값

- 성공 시 0

- 실패 시 -1

cf) stat함수의 인자로 pathname이 심볼릭링크라면 가르키는 원본파일의 속성을 담고

  + lstat함수의 인자로 pathname이 심볼릭링크라면 그 심볼릭링크파일의 속성을 담음 (즉, 그 파일자체의 속성)

cf) struct stat은 man 2 stat으로 조회

cf) struct stat중의 st_mode는 type과 mode에 대한 정보를 담고 있음 ->  S_ISXXX 매크로 함수와 사용될 것 -> 참(1), 거짓(0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sys/stat.h>
 
int        main(void)
{
    struct stat stat_buf;
 
    stat("test.txt"&stat_buf);
 
    printf("file size of test.txt: %lld\n", stat_buf.st_size);
    if (S_ISDIR(stat_buf.st_mode))
        printf("text.txt is directory\n");
    else if (S_ISREG(stat_buf.st_mode))
        printf("test.xt is regular file\n");
    else
        printf("else\n");
 
    return (0);
}
cs

cf) S_ISXXX(st_mode)참고 (www.skrenta.com/rt/man/stat.2.html)

cf) st_mode 비트연산도 가능

 

pid_t fork(void);

자식프로세스를 생성함, 프로세스 복제 (메모리, 코드 정보도 그대로 복제함, 복제 후 독립적으로 메모리 존재)

자식프로세스의 종료처리를 해줘야 함 (wait 계열 함수)

파라미터

- 없음

반환 값

- 성공 시

   -> 부모 프로세스 : 자식 프로세스의 PID를 리턴

   -> 자식 프로세스 : 0

- 실패 시

   -> 부모 프로세스 : -1

   -> 자식 프로세스는 생성되지 않음

cf) pid_t getpid(void) : 나의 pid를 얻음

cf) pid_t getppid(void) : 부모의 pid를 얻음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
 
int        main(void)
{
    pid_t pid;
 
    pid = fork();
    if (pid == 0)
        printf("child process: %d\n", getpid());
    else if (pid > 0)
        printf("parent process: %d, child pid: %d\n", getpid(), pid);
    else
    {
        printf("fork error\n");
        return (-1);
    }
    printf("end pid: %d\n", getpid());
    return (0);
}
cs

 

int execve(const char *path, char *const argv[], char *const envp[]);

해당 프로세스는 종료하고 새로운 프로세스 생성 및 실행

종료되기 때문에 자식프로세스를 fork()로 생성하고 자식프로세스에서 exec를 하는게 일반적

매개변수

- path : 실행파일 경로

- argv : main과 흡사, 마지막에 NULL 필요

- envp : 환경변수 문자열 배열리스트("key=value"형태로 저장), 마지막에 NULL 필요

반환 값

- 성공 시 : 반환하지 않음 (새로운 프로세스를 실행하기 때문)

- 실패 시 : non-zero, -1

cf) argv와 envp를 main에서 받고 그대로 넘겨주는 것도 나쁘지 않음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
 
int        main(void)
{
    char *argv[] = { "ls""-al"NULL };
    char *envp[] = { "MY_PATH=/Users/malbong"NULL };
 
    printf("!!! execve !!!\n");
    if (execve("/bin/ls", argv, envp))
    {
        printf("execve error : %s\n", strerror(errno));
        return (-1);
    }
    /* 정상적으로 실행 되었을 때 여기를 실행하는지 확인 */
    printf("!!! check !!!\n");
    /* 위의 printf가 실행되지 않음 */
    return (0);
}
 
cs

 

void exit(int status);

프로세스 종료

나의 프로세스를 생성한 부모프로세스에게 상태를 알림

파라미터

- status : 종료 상태

   -> 0 : 정상 종료 성공 상태

   -> non-zero : 정상 종료 실패 상태

 

잠깐 보는 Signal

- 프로세스나, 커널이 프로세스에게 보내주는 신호 (signum : 숫자)

자식 프로세스 종료 시그널 - SIGCHLD

- 자식 프로세스가 종료되었을 때, 부모 프로세스에게 SIGCHLD시그널을 전송함

- 부모 프로세스는 SIGCHLD에 대해 수신을 대기하고

- SIGCHLD를 수신을 하면, 자식 프로세스 상태를 확인 한 후, 종료된 자식 프로세스로 처리함 (순서중요)

- 이와 비슷하게 자식프로세스를 처리하는 방법이 있음 -> wait

 

pid_t wait(int *status);

자식 프로세스 종료 대기

자식 프로세스가 종료가 될 때까지 블록킹함

파리미터

- wstatus : child process의 종료 상태

반환 값

- 성공 시 : termminated된 자식 프로세스의 pid

- 실패 시 : -1

wstatus로 사용할 수 있는 매크로들

WIFEXITED : 정상 종료 되었는지 (참, 거짓으로 반환)

WEXITSTATUS : 자식프로세스가 넘긴 종료코드를 확인

 

WIFSIGNALED : 특정 시그널을 받아서 종료 되었는지, 다른 이유로 종료 되었는지 (참, 거짓)

WTERMSIG : 그 특정 시그널 번호를 알려줌

 

WCOREDUMP : 자식 프로세스가 코어덤프파일을 생성했는지

 

WIFSTOPPED : 자식 프로세스가 stop 되었는지

WSTOPSIG : 멈춘 이유에 대한 시그널 번호를 알려줌

WIFCONTINUED : 자식 프로세스가 resume 되었는지

 

 

pid_t waitpid(pid_t pid, int *status, int options);

pid_t wait3(int *status, int options, struct rusage *rusage);

pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

파라미터

- pid : 종료 대기할 프로세스의 pid

- status : 자식 프로세스의 종료코드

- options

  -> WNOHANG: 논블럭킹처럼 작동, 살아있는지 바로 알려줌, 그래서 while문이랑 사용가능

  -> WUNTRACED: 자식프로세스가 SIGSTOP 받아도 반환 받음

  -> WCONTINUED : 자식프로세스가 SIGCONT 받아도 반환 받음

- rusage : 자식 프로세스의 리소스 사용량을 반환 받음 (cpu, memory size... etc), man getrusage로 확인

반환 값

- 양수 : 상태가 바뀐 chlid process의 pid

- 0 : WNOHANG 지정 시 멀쩡하게 자식이 살아있다면 0을 리턴함

- 실패 : -1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int        main(void)
{
    pid_t pid;
    int status;
    char *argv[] = { "ls""-al"NULL };
    char *envp[] = { "A=a"NULL };
 
    pid = fork();
    if (pid > 0/* parent */
    {
        printf("parent pid: %d, child pid: %d\n", getpid(), pid);
        while (!waitpid(pid, &status, WNOHANG));
        if (WIFEXITED(status))
            printf("child procees exited number: %d\n", WEXITSTATUS(status));
        else
            printf("is not exited\n");
    }
    else if (pid == 0/* child */
    {
        printf("child pid: %d, execve ls\n", getpid());
        if (execve("/bin/ls", argv, envp) == -1)
        {
            printf("execve error: %s\n", strerror(errno));
            return (-1);
        }
    }
    else /* error */
    {
        printf("error... %s", strerror(errno));
        return (-1);
    }
    printf("###  end pid: %d ###\n", getpid());
    return (0);
}
 
cs

 

Signal 정의 (좀 더 알아보기 + 알아만 놓기)

정의

 - 비동기 이벤트를 처리하기 위한 메커니즘 (언제 일어날지 모르는 이벤트 처리)

 - 소프트웨어 인터럽트

쓰임

 - Ctrl + C (SIGINT)

 - Child process termination

 - Alarm

 - divide by zero

 - inter-process communication

담는 정보

 - 시그널 번호 + 추가정보 + 사용자 정의 데이터(작은 크기)

 

Signal 처리

무시

 - 아무런 동작도 하지 않음

 - SIGKILL, SIGSTOP은 무시 불가능

붙잡아 처리

 - 시그널 별 처리 함수를 수행 (사용자 정의 함수 수행)

기본 동작 (정의 되어 있음)

 - 시그널 종류 별 기본 동작 수행

  -> 프로세스 종료

  -> 코어덤프 생성 후 종료

  -> 무시 혹은 정지

 

주요 Signal 번호 (man 3 signal)

시그널 번호 기본 동작 의미
SIGHUB 종료 프로세스의 제어 터미널이 닫힐 때 (로그아웃)
설정 리로드
SIGINT 종료 사용자가 Ctrl + C
SIGQUIT 코어 덤프 파일 생성 사용자가 Ctrl + \
SIGKILL 종료 붙잡을 수 없는 프로세스 종료
SIGSEGV 코어 덤프 파일 생성 메모리 접근 위반
SIGALARM 종료 알람 발생
SIGTERM 종료 붙잡을 수 있는 프로세스 종료
SIGUSR1/2 종료 사용자 정의 시그널 (USR1, USR2임)
SIGCHLD 종료 자식 프로세스 종료
SIGCONT 진행 프로세스를 정지했다가 다시 수행함
SIGSTOP 정지 프로세스 정지

 

Signal의 실행과 상속

fork() - 자식 프로세스는 부모 프로세스의 시그널 동작을 상속 받음

exec() - 부모 프로세스가 붙잡아 처리하는 시그널은 기본동작으로 변경

시그널 동작 fork() 수행 후 exec() 수행 후
무시 상속됨 상속됨
기본 동작 상속됨 상속됨
붙잡아 처리 상속됨 상속되지 않음 (기본동작으로 처리함)
대기 중인 시그널 상속되지 않음 상속됨

 

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

Signal 처리 설정, 붙잡아 처리

파라미터

 - signum : 처리 대상 시그널 번호

 - handler : 시그널 핸들러

   -> SIG_IGN : 해당 시그널을 무시하게 함

   -> SIG_DFL : 해당 시그널을 기본 동작 처리함

   -> 그외 사용자 정의 시그널 핸들러

반환 값

 - 성공 시 : 이전 시그널 핸들러

 - 실패 시 : SIG_ERR

cf) 시그널 핸들러는 재진입이 가능한 함수로 작성해야 함 (비동기적이기 때문)

 -> 글로벌 데이터를 수정하는 시그널 핸들러는 조심해야 함

 -> 그런 것이 필요한 경우, 시그널블록 -> 데이터처리 -> 시그널언블록 하여 안전하게 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
 
void    my_signal_handler(int signum)
{
    printf("signal: %d\n", signum);
    exit(123);
}
int        main(void)
{
    pid_t    pid;
    int        status;
 
    pid = fork();
    if (pid == 0/* child */
    {
        signal(SIGTERM, my_signal_handler);
        printf("child : %d, infinity loop\n", getpid());
        while (1)
            sleep(1);
    }
    else if (pid > 0/* parent */
    {
        while (1)
        {
            int n;
            scanf("%d"&n);
            if (n == 0break ;
        }
        kill(pid, SIGTERM);
        pid = wait(&status);
        printf("terminated pid: %d\n", pid);
        if (WIFSIGNALED(status))
            printf("terminate signal number: %d\n", WTERMSIG(status));
        if (WIFEXITED(status))
            printf("terminate code: %d\n", WEXITSTATUS(status));
    }
    else
        printf("error\n");
    return (0);
}
 
cs

ps -ef | grep으로 좀비는 나오지 않음

wait로 블록으로 자식 프로세스가 종료되는 것을 기다림

WEXITSTATU(status)의 결과는 임의로 만든 handler의 exit(123) 값임

 

 

 

 

 

int kill(pid_t pid, int sig)

시그널 보내기

자식프로세스를 중지시킬 수 있음

파라미터

 - pid : 시그널 송신 대상 지정

   -> 1 이상 : 해당 프로세스 ID

   -> 0 : 프로세스 그룹 전체에

   -> -1 : 권한 내의 모든 프로세스

   -> -1미만 : 프로세스 그룹 아이디가 (-pid)인 프로세스 그룹 전체 (-10이면 프로세스그룹아이디가 10인 곳에)

 - sig : 보낼 시그널 번호

  -> 단, sig가 0인 경우 시그널을 보내지는 않으나, process 유뮤 및 권한 판단을 할 수 있음

반환 값

 - 하나 이상의 프로세스에게 송신 시 0 리턴

 - 실패 시 : -1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int        main(void)
{
    pid_t pid[10];
    for (int i = 0; i < 10++i)
    {
        pid[i] = fork();
        if (pid[i] == 0/* child */
        {
            while (1)
                printf("I'm child process: %d\n", getpid());
        }
    }
    for (int i = 0; i < 10++i)
    {
        printf("kill(pid:%d)\n", pid[i]);
        kill(pid[i], SIGTERM);
    }
    printf("Parent process is terminated\n");
    return (0);
}
 
cs

 

int pipe(int fildes[2]);

파이프를 생성하고 fd쌍을 만들어줌

fd쌍으로 프로세스간에 데이터 통신을 가능하게 함

파라미터

- fildes[2] : 파이프에 이용할 fd쌍

반환 값

- 성공 시 : 파이프 생성 후 0 반환

- 실패 시 : -1

fd[0]은 읽기를, fd[1]는 쓰기를 담당함

프로세스가 비어있는 파이프에서 읽을려고 한다면 read(fd[0])는 읽을 수 있는 상태일 때까지 블락

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <errno.h>
 
int        main(void)
{
    int        prcw[2]; /* parent read, child write */
    int        pwcr[2]; /* parent write, child read */
    pid_t    pid;
    int        status;
    char    res[10];
 
    if (pipe(prcw) || pipe(pwcr))
    {
        printf("pipe error: %s\n", strerror(errno));
        return (-1);
    }
    pid = fork();
    if (pid > 0/* parent */
    {
        write(pwcr[1], "Parent"6);
        memset(res, 010);
        read(prcw[0], res, 5); res[5= 0;
        printf("Parent Process: %s\n", res);
        while (!waitpid(pid, &status, WNOHANG));
    }
    else if (pid == 0/* chlid */
    {
        write(prcw[1], "Child"5);
        memset(res, 010);
        read(pwcr[0], res, 6); res[6= 0;
        printf("Chlid Process: %s\n", res);
    }
    else
    {
        printf("fork error\n");
        return (-1);
    }
    printf("end\n");
    return (0);
}
 
cs

pipe를 두개 생성한 이유

pipe를 하나를 쓰면 부모나 자식이 혼자서 write하고 그것을 혼자 read할 수 있기 때문

아래 예제 참고사이트

 

pipe 참고한 사이트 : codingdog.tistory.com/entry/%EB%A6%AC%EB%88%85%EC%8A%A4-pipe-%ED%95%A8%EC%88%98-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%A5%BC-%EB%A7%8C%EB%93%A0%EB%8B%A4

 

리눅스 pipe 함수 : 파이프를 만든다

 안녕하세요. 이번 시간에는 리눅스 pipe 함수에 대해서 알아보도록 하겠습니다.  file 데스크립터 2개를 저장할 배열을 넘겨주기만 하면 됩니다. 그것을 넘겨주면, 그 배열에 하나는 파이프에서

codingdog.tistory.com

 

%%% 프로세스, 파이프, 시그널에 대한 공부는 더 필요함 %%%