segmentation fault (core dumped)

이런 무시무시한 문구가 떴는데, 눈을 씻고 찾아봐도 coredump 파일을 찾을 수가 없었습니다. 분명 메시지는 coredump 파일 생성됐다고 써있는데 말이죠... 내눈이 잘못된건가, 아니면 내가 영어를 못하는건가.. 

그래서 제가 찾은 coredump를 찾는 몇가지 해결 방법을 알려드리도록 할게요. 

1. ulimit으로 core dump size를 확인

아래와 같은 명령어로 core file size를 확인합니다. 

# ulimit -a

 

0 이상이어야합니다. 그러니까 이 부분을 고쳐하겠죠. 0보다 크면 되긴하지만, unlimited로 바꿔보도록 합시다. 

# ulimit -c unlimited

 

 

위처럼 unlimited로 바뀐것을 알 수 있습니다. 이제 프로그램을 실행시키면 coredump 파일이 생성될 수도 있습니다. 물론 저처럼 두번째 문제가 있을때는 coredump 파일이 생성되지 않을 수도 있으니, 2번까지 확인해보세요.

한가지 더, ulimit으로 설정한 core dump size는 일시적입니다. 즉, reboot은 당연하고 세션이 끊어지면 초기화된다는 것이죠. 터미널 접속시 바로 설정되도록 적용하려면 아래와 같이 /etc/security/limites.conf 파일을 설정하시면 됩니다. 

# vi /etc/security/limits.conf

맨 아래에 아래와 같이 추가하여 저장하시면 반영구적으로 적용됩니다.

 

 

2. /proc/sys/kernel/core_pattern 편집하여 현재 디렉토리에 coredump 생성

원래 coredump 파일을 프로그램 실행 위치에 생성되는 것으로 알고 있는데, 저의 경우에는 그게 아니었습니다. 도대체 왜 나만 안되는거야 이러고 있을 때 google 형님께 여쭈어본 결과 /proc/sys/kernel/core_pattern에서 설정할수 있다고 하십니다. 이 파일이 coredump 파일을 어떻게 생성하는지 정의하고 있는데, 열어보면 어떤 이상한 경로로 되어있을 가능성이 있습니다. 저는 과감하게 바꿔줬습니다. 

# echo "core.%e.%p" > /proc/sys/kernel/core_pattern

 

%e와 %p는 format문자로 저도 모릅니다. google형님이 알려준대로 썼으니까요. 이제 바꿔주고 coredump되는 실행파일을 실행시켜주면 그 디렉토리에 coredump 파일이 뜨게 됩니다.

 

core 덤프 파일이름을 보면 %e는 프로그램 이름, %p는 pid인 듯 보이네요.

 

여러분도 저처럼 삽질에 시간쓰지 마시라고 공유합니다. 그럼 이만 포스팅 마칩니다~

반응형
블로그 이미지

REAKWON

와나진짜

,

poll, select, epoll관 관련한 다중 입출력의 내용과 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

poll

select와 비슷한 함수입니다. 여러 file descriptor에 대해서 I/O를 수행하기 위한 준비가 될 때까지 기다리는 함수입니다. 여러 file descriptor를 감시하여 event가 발생할 때까지 기다린다고 보시면 됩니다.

예를 들어서 어떠한 서버가 여러 클라이언트에 대해서 연결을 갖게 되고, 입력을 처리하여야한다면 어떻게 해야할까요? 한가지 방법으로는 클라이언트 요청이 들어오면 쓰레드를 만들어서 따로 처리해줄 수 있죠. 그런데 이때 한계가 있습니다. 클라이언트의 요청이 많아질 경우에는 쓰레드를 그만큼 생성해야하는 문제가 있죠. 만약 client가 100개가 현재 연결되어 있는 서버에서는 100개의 쓰레드를 생성할 수는 없겠죠. 이보다는 100개의 socket에 대해서 지켜보다가 하나의 socket에 읽을 데이터가 생겼다면 그 socket에서 read를 하고 처리를 하면 되는 것이죠.

이 목적을 달성할 수 있는 함수가 poll입니다. poll은 여러 file descriptor에 대해서 이벤트가 발생하기를 기다렸다가, event가 발생하면 그에 대한 정보를 제공해줍니다. select와는 비슷한데, 더 섬세하게 다룰 수 있습니다. 먼저 함수의 원형을 봅시다.

 

함수 설명

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

 - fds : pollfd의 구조체 포인터입니다. pollfd는 아래와 같이 선언되어 있습니다. 

struct pollfd {
   int   fd;         /* file descriptor */
   short events;     /* requested events */
   short revents;    /* returned events */
};

        fd : 점검할 file descriptor입니다. 

         events : fd에 대해서 주시할 event입니다. 이 event들은 OR(|)로 여러 가지를 줄 수 있습니다. event에 대해서는 아래 표에 설명해놓았습니다. 사용자가 설정합니다.

         revents : fd에 발생한 event를 의미합니다. 이는 사용자가 설정하는게 아니라 kernel이 설정해줍니다. 이 revents를 보고 어떤 event가 발생했는지 확인할 수 있습니다.

아래의 표를 보면 revents에는 모든 event가 설정이 되어질 수 있습니다. 하지만 events에는 아래의 3개는 설정할 수 없죠.

 

event 이름 event에 설정 가능 kernel이 revents에 설정 설명
POLLIN O O high priority 외의 자료를 바로 읽을 수 있음
(POLLRDNORM | POLLRDBAND와 동일)
POLLRDNORM O O 보통 자료를 바로 읽을 수 있음
POLLRDBAND O O 우선 순위 자료를 바로 읽을 수 있음
POLLPRI O O high priority 자료를 바로 읽을 수 있음
POLLOUT O O 보통의 자료를 쓸 수 있음
POLLWRNORM O O 보통의 자료를 쓸 수 있음
POLLWRBAND O O 우선 순위 자료를 바로 기록 할 수 있음
POLLERR   O 오류 발생
POLLHUP   O 연결 끊어짐
POLLNVAL   O fd가 열린 파일이 아님

 

- nfds : fds 배열의 크기를 의미합니다.

- timeout : poll이 대기할 시간을 설정할 수 있습니다. 여기서 단위는 ms이므로 1초만 대기하려면 timeout은 1000값이 되어야합니다. 이외에 0이거나 -1인 경우는 아래와 같습니다.

timeout == -1 : 무한정 기다립니다.

timeout == 0 : 전혀 기다리지 않습니다.

 

이를 이용해서 지난 select와 같은 역할을 하는 서버를 만들어보도록 합시다. 역시 네트워크에 대한 내용은 뺐습니다. 순수하게 poll이 어떻게 동작하는지만 확인할 것입니다. 그런데 한가지 알아두셔야할 것은 select와 마찬가지로 정규 파일에 대해서 사용하는 것은 적절하지 않다는 점은 알아두셔야합니다.  

//multiIO_poll.c 

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <poll.h>

#define FD_SIZE 3
#define BUF_SIZE 128

int main(int argc, char *argv[]){
        char buf[BUF_SIZE];
        struct pollfd fdarr[FD_SIZE];
        int n, i, ret, end_count = 0;

        //3개의 파일의 이름을 받는다.
        if(argc != 4){
                printf("usage : %s file1 file2 file3\n", argv[0]);
                return 0;
        }


        for(i = 0; i < FD_SIZE; i++){
                int fd = open(argv[i+1],O_RDONLY);
                if(fd >= 0){
                        fdarr[i].fd = fd;
                        //차단없이 읽는 것을 주시할 event로 넣는다.
                        fdarr[i].events = POLLIN;
                }
        }

        while(1){

                memset(buf, 0x00, BUF_SIZE);

                // timeout == -1은 지정된 파일 서술자중
                // 하나라도 준비될때까지 무한정 기다린다.
                ret = poll(fdarr, FD_SIZE, -1);

                if(ret == -1){
                        perror("poll error ");
                        exit(0);
                }

                for(i = 0; i < FD_SIZE; i++){
                        //파일 디스크립터가 -1이면 볼 필요 없음.
                        //받은 이벤트인 revents에 POLLIN이 있다면
                        if(fdarr[i].fd != -1 && fdarr[i].revents & POLLIN){
                                while((n = read(fdarr[i].fd, buf, BUF_SIZE)) > 0){

                                        //quit가 들어오면 fdarr[i] read 종료
                                        if(!strcmp(buf,"quit\n")){

                                                //파일디스크립터 close
                                                close(fdarr[i].fd);
                                                //안쓰는 fd로 업데이트하기 위해 -1 지정
                                                fdarr[i].fd = -1;
                                                end_count++;
                                                if(end_count == FD_SIZE) 
                                                        exit(0);
                                                continue;
                                        }

                                        printf("fd[%d] - %s",fdarr[i].fd, buf);
                                }
                        }
                }

        }
        return 0;
}

 

역시 테스트하기 위해서는 클라이언트 프로그램이 필요합니다. 여기서는 select에서 설명했던 클라이언트 역할을 할  소스 코드 fwrite.c를 그대로 사용합니다.

https://reakwon.tistory.com/117

 

[리눅스] 다중입출력 - select개념과 설명과 예제

다중입출력 아래와 같은 상황을 생각해볼까요? 어떤 프로세스가 다음과 같이 파일 3개를 처리하는데 그 중 입력이 있는 파일을 프로세스에 처리하는 상황을 어떻게 구현할 수 있을까요? 이때 우

reakwon.tistory.com

역시 테스트 하기 위해서는 a, b, c라는 파일 3개를 새로 만들고 시작합시다. 그리고 fwrite.c와 multiIO_poll.c는 아래와 같이 컴파일 하시구요. 

# gcc multiIO_poll.c 
# gcc fwrite.c -o fwrite

 

자, 테스트할 준비는 이제 완료되었고, 아래와 같은 결과를 보입니다. 

./a.out a b c
# ./a.out a b c
fd[3] - hello, ifd[3] - 'm writifd[3] - ng file fd[3] - a
fd[4] - hello, ifd[4] - 'm writifd[4] - ng file fd[4] - b~~
fd[5] - hello, ifd[5] - 'm writifd[5] - ng file fd[5] - c..
fd[3] - bye pollfd[3] - 
fd[4] - bye! 
fd[5] - i have tfd[5] - o go nowfd[5] -
./fwrite a ./fwrite b ./fwrite c
# ./fwrite a
hello, i'm writing file a
bye poll
quit
# ./fwrite b
hello, i'm writing file b~~
bye! 
quit 
# ./fwrite c
hello, i'm writing file c..
i have to go now 
quit 

 

역시나 poll은 정규 파일에 대해서 항상 준비되어있다고 판단하기 때문에 루프에다가 로그 printf를 찍어보면 엄청나게 바쁜 루프를 도는 것을 확인할 수 있습니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,

systemd 뿐만 아니라 데몬의 특징을 담은 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

systemd

systemd에서 끝 d는 daemon을 의미합니다. deamon은 background에서 실행이 되는  프로세스입니다. 그러한 deamon을 실행하고 관리하게 해주는 daemon이 바로 systemd입니다. systemd는 부팅하고나서 가장 먼저 실행되는 데몬입니다. 

이전에는 init이라는 데몬이 있었는데 이를 대체하고 init보다 기능이 추가되어서 나온 것이 systemd입니다. 그래서 이전의 init과 같이 PID가 1이 됩니다. 부모프로세스가 없으므로 PPID 또한 1이 됩니다. 

systemd는 리소스를 unit이라고 불리는 단위로 관리합니다. 어떤 type의 unit이 있을까요? 아래의 표로 정리하였습니다. 

.service .socket .device .mount .automount .swap .target .path .timer
.snapshot .slice .scope            

 

 - 여기서는 .service만 설명합니다. 가장 많이 쓰니까요~ .service에서는 service나 application을 서버상에서 어떻게 관리할지를 명세합니다. service의 시작, 중지, 그리고 어떠한 상황에서 자동적으로 실행하는지, 어떠한 종속성을 가지고 있는지에 대한 정보를 갖고 있습니다.

만약 .service를 추가하려면 /etc/systemd/system 디렉토리에 끝 확장자로 .유닛타입 식으로 써주면 됩니다. service의 경우에는 끝 확장자가 .service가 됩니다. 그리고 시스템은 일반적으로 unit file들을 /lib/systemd/system 하위에 복사하여 유지합니다. 여기서 아주 간단한 service를 등록하고 실행하는 방법을 알아보도록 하겠습니다. 

 

1. 실행 파일 생성

사용할 실행 파일(myservice.c)

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(){
        int fd;
        char buf[64] = "myservice start!\n";

        fd = open("/tmp/log.txt",O_CREAT|O_WRONLY|O_TRUNC,0644);
        //열기 실패시
        if(fd<0) return -1;

        write(fd, buf, strlen(buf));

        while(1){
                //계속 running되게 하기 위해 무한 loop
                sleep(1);
        }
        close(fd);
        return 0;

}

위 소스코드로 컴파일하여 아래의 실행 파일로 만들어보도록 하겠습니다. 권한 문제가 생길까봐 777을 부여했습니다.

# gcc myservice.c -o myservice
# chmod 777 myservice

 

2. Unit 파일 생성

우리는 service를 생성할 것이기 때문에 /etc/systemd/system 하위에 myservice.service 파일을 생성하도록 하겠습니다. 

[Unit]
Description=test service

[Service]
ExecStart=/home/ubuntu/systemd/myservice

[Install]
WantedBy=multi-user.target

 

2.1 Unit Section

보통 가장 처음 등작하는 Section입니다. 여기서는 유닛에 대한 meta data와 다른 유닛간의 관계를 설정하는 곳입니다. 

Description : 이 Unit에 대한 설명을 뜻합니다. 짧고, 간략하지만 핵심 정보를 담고 있도록 설명하는 곳입니다.

Documentation : 이 서비스에 대한 문서가 있는 URI를 제공합니다. man 페이지나 웹 사이트가 될 수 있습니다. 이런 정보들은 systemctl status 명령어에서 노출됩니다.

After : 이 Unit보다 먼저 실행되어야할 Unit들을 나열합니다.

Before : 이 Unit보다 늦게 실행되어야할 Unit들을 나열합니다. 

Requires : 이 Unit이 의존하는 모든 Unit을 나열합니다. 여기서 나열된 Unit들은 전부 성공적으로 실행이 되고 있어야합니다.

Wants : Requires와 유사합니다만, Requires보다는 그렇게 엄격하지 않습니다. 다른 Unit이 실패하더라도 지금 Unit을 실행합니다.

BindsTo : Requires와 유사합니다. 대신 연관된 유닛이 종료되면 같이 service가 종료됩니다.

 

2.2 Service Section 

Service Section은 서비스에만 적용할 수 있는 특징을 정의합니다. Unit에는 여러 종류가 있다고 위에서 표로 정의해놓았죠? mount도 있고, swap도 있고... 그중 Service에만 적용하는 구성입니다.

Type : service가 어떤 형태로 동작이 되는지를 설정합니다. systemd에게 이 service가 어떻게 관리되고 상태를 탐지하는지 알려주는 지시자입니다. 아래 중 하나로 동작이 됩니다. 

  ● simple : 명시하지 않을 경우 simple로 동작합니다. ExecStart는 설정이 되어 있어야합니다. 

  ● forking : service가 자식 프로세스를 생성할때 사용합니다. 이때 자식을 생성한 프로세스는 곧 장 나갈때 사용됩니다.

  ● oneshot : 프로세스가 오래 실행되지 않을때 systemd가 이 service가 종료되기를 기다려야할때 이 값을 사용할 수 있습  니다. service가 종료된 후 systemd가 진행을 계속하게 됩니다.

  ● dbus : 지정된 Bus Name이 D-Bus에 준비될때까지 기다리고, D-Bus가 준비되면 service가 실행됩니다.

  ● notify : service가 startup이 끝날때 notification(signal)을 발생시킵니다. 이런 상황이 발생할때 systemd는 다음 unit으로 넘어갑니다. 

  ● idle :  모든 작업이 실행될때까지 서비스가 실행이 되지 않습니다. 즉, 모든 작업이 실행되어서야 실행됩니다.

ExecStart : 실행할 command의 절대경로를 지정합니다.  

ExecStartPre : 서비스가 실행하기 전의 command를 지정합니다. 서비스 시작전 명령어를 실행합니다.

ExecStartPost : 서비스를 시작하고 나서의 command를 지정합니다. 서비서 시작 후에 명령어를 실행하기 됩니다.

ExecStop : 서비스가 stop될때 실행되어야할 command를 정합니다. 

Restart : systemd가 자동으로 재시작하려고 시도하는 상황을 정의합니다. alway, on-success, on-failure, on-abnormal, on-abort, on-watchdog를 사용할 수 있습니다. 

TimeoutSec : systemd가 service가 실행할때까지 기다리는 시간을 의미합니다. service를 stop할때까지 기다리는 시간이라고도 말할 수 있겠네요.

FailureAction : service 실패시 어떤 동작할 할지를 정의합니다. none, reboot, reboot-force, reboot-immediate, poweroff, poweroff-force, poweroff-immediate, exit, exit-force 중 하나를 사용합니다.

User : service가 실행될때 어떠한 권한으로 실행이 되는지를 설정합니다. user 이름이 될 수 있고, user id로 설정할 수도 있습니다. 그러나 user 이름이 알아보기 좋겠죠? 

Group : User와 마찬가지로 service가 실행될때 가지는 그룹 권한을 설정합니다. 

 

2.3 Install Section

보통 파일의 마지막에 존재합니다. 이 Section은 optional입니다. Unit이 enable하거나 disable 될때 Unit의 행동을 정의합니다.

WantedBy : 어떻게 Unit이 활성화(enable)되는지 명세합니다. 

Also : service가 enable, disable될때 집합 단위로 같이 enable, disable할 serivce들을 의미합니다.

Alias : 이 service의 별칭을 줍니다. 

 

3. service 실행, 상태, 종료

service 실행 : service를 실행하는 명령어는 아래와 같습니다. 서비스가 새로 추가됐으니, daemon을 다시 load하는 과정도 있어야합니다.

# systemctl daemon-reload
# systemctl start myservice

 

자, 실행이 되었다면 /tmp 하위에 log.txt라는 파일이 생겨나고 파일 내용이 적혀져 있어야합니다. 확인해볼까요? 

# cat /tmp/log.txt
myservice start!

 

위처럼 파일이 생겼으며 내용도 채워져있음을 확인했습니다. 그리고 현재 데몬이 실행되고 있는지 확인해봅시다. 

# ps -ef | grep myservice
root       42450       1  0 19:51 ?        00:00:00 /home/ubuntu/systemd/myservice

 

위와 같이 PID가 42450인 프로세스가 있는 것을 확인했습니다. 부모 PID는 1입니다. 시스템 데몬이라는 것을 알 수 있죠?

 

service 상태 확인 : service의 상태가 어떤지 보고 싶다면 status를 옵션으로 주면 됩니다. 4번째줄에 active로 실행중인 상태를 확인할 수 있습니다.

# systemctl status myservice
● myservice.service - test service
     Loaded: loaded (/etc/systemd/system/myservice.service; static; vendor preset: enabled)
     Active: active (running) since Mon 2022-06-27 19:45:00 KST; 51s ago
   Main PID: 42434 (myservice)
      Tasks: 1 (limit: 2295)
     Memory: 140.0K
     CGroup: /system.slice/myservice.service
             └─42434 /home/ubuntu/systemd/myservice

 6월 27 19:45:00 ubuntu-VirtualBox systemd[1]: Started test service.

 

service 종료 : service를 종료하고 싶다면 stop을 옵션으로 주면 됩니다.

# systemctl stop myservice
# systemctl status myservice
● myservice.service - test service
     Loaded: loaded (/etc/systemd/system/myservice.service; static; vendor preset: enabled)
     Active: inactive (dead)

 

service 재시작 : stop과 start를 하면 재시작이 되죠. 한번에 할 수도 있습니다.

# systemctl restart myservice

 

service 부팅시 시작, 해제 : 이 서비스를 부팅시에 자동으로 실행하게 만들 수 있습니다. 단, Install Section을 정의해놔야합니다. 반대로 부팅시 시작 해제하려면 disable을 사용하면 됩니다. 

# systemctl enable myservice
# systemctl disable myservice

 

기본적으로 systemd에 service를 등록하고 실행하는 방법을 알아보았습니다. 이 밖에도 훨씬 많은 내용이 존재하여 이 포스팅에 전부 담아낼 수가 없습니다. 그래서 아래의 페이지에서 필요할때 찾아서 적용하시면 되겠습니다.

https://www.digitalocean.com/community/tutorials/understanding-systemd-units-and-unit-files#install-section-directives

 

 

Understanding Systemd Units and Unit Files | DigitalOcean

 

www.digitalocean.com

 

https://www.freedesktop.org/software/systemd/man/systemd.directives.html

 

systemd.directives

Name systemd.directives — Index of configuration directives Unit directives Directives for configuring units, used in unit files....

www.freedesktop.org

 

반응형
블로그 이미지

REAKWON

와나진짜

,

tar

tar 명령어는 Tape Archiver의 약자로 여러 파일들을 하나의 파일로 묶어주는 명령어입니다. tar 명령어에 대해서 압축 명령어로 오해하시는 분들이 많은데, tar는 단순 파일을 하나로 묶는 명령입니다. 하지만 여러 옵션을 주어서 압축까지하는 것이 가능합니다. 예를 들어서 아래의 그림은 gzip이라는 압축 방식을 사용한 과정입니다. 아래의 그림을 보시면 쉽게 이해가 가능합니다.

file 1부터 file n까지가 있다면 tar 명령어를 통해서 하나의 files.tar 파일(확장자 .tar는 tar로 묶었음을 알리는 확장자입니다.)로 묶을 수 있습니다. 이때 압축한 상태가 되지는 않지요. 만일 압축방식의 하나인 gzip압축을 거치게 된다면 files.tar.gz(확장자 .gz은 gzip으로 압축이 되었다는 것을 알려주는 확장자입니다.)이라는 하나의 압축파일이 나올 수 있습니다. 그러니 ~.tar.gz은 tar로 파일들을 일단 하나의 파일로 묶고 나서 gzip 방식으로 압축을 한 파일이라는 것을 알 수 있겠죠.

그리고 압축을 해제할때는 이 과정의 역순을 거치게 됩니다. 

tar의 명령어 옵션은 너무 많아 포스팅에 전부 담을 수 없습니다. 주요한 옵션 몇가지만 보고 사용해도 문제없습니다. 

option 설명
-c(create) 파일 합치기
-v(verbose) tar의 과정을 보여주기, 굳이 사용하지 않아도 무방합니다.
-f(file) tar 아카이브 지정, 즉 압축 파일명을 지정합니다.
-x(extract) 파일 추출
-z(gzip) gzip 압축. 확장자 gz
-j(bzip2) bzip2 압축. 확장자 bz2
-t(list) 파일의 리스트 확인

 

위의 옵션들을 이용해서 tar로 파일들을 묶고 압축, 혹은 해제하는 방법을 아래의 명령을 통해서 보실 수 있습니다.

 

1. 파일 묶기

$ ls -l
합계 8
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file1.txt
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file2.txt
$ tar -cvf files.tar file1.txt file2.txt   <-- files.tar로 묶기
file1.txt
file2.txt
$ ls -l
합계 20
-rw-rw-r-- 1 ubuntu ubuntu     6  4월 24 12:33 file1.txt
-rw-rw-r-- 1 ubuntu ubuntu     6  4월 24 12:33 file2.txt
-rw-rw-r-- 1 ubuntu ubuntu 10240  4월 24 12:46 files.tar

 

 

2. 파일 풀기

$ ls -l
합계 12
-rw-rw-r-- 1 ubuntu ubuntu 10240  4월 24 12:46 files.tar
$ tar -xvf files.tar  <-- files.tar 파일 풀기
file1.txt
file2.txt
$ ls -l
합계 20
-rw-rw-r-- 1 ubuntu ubuntu     6  4월 24 12:33 file1.txt
-rw-rw-r-- 1 ubuntu ubuntu     6  4월 24 12:33 file2.txt
-rw-rw-r-- 1 ubuntu ubuntu 10240  4월 24 12:46 files.tar

 

3. gzip으로 압축

$ ls -l files/
합계 8
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file1.txt
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file2.txt
$ tar -cvzf files.tar.gz files <-- gzip 압축
files/
files/file1.txt
files/file2.txt
$ ls -l
합계 8
drwxrwxr-x 2 ubuntu ubuntu 4096  4월 24 12:55 files
-rw-rw-r-- 1 ubuntu ubuntu  185  4월 24 12:56 files.tar.gz

 

그런데 이렇게 명령어 옵션을 섞어서 쓸때 간과하는 점이 있는데 f는 아카이브 파일의 이름을 지정해야하는 옵션입니다. 그러니 -f 뒤에는 파일명이 나와야합니다. 이런 사실을 모르면 아래와 같은 에러 메시지를 볼 수 있게 됩니다.

$ tar -cvfz files.tar.gz files
tar: files.tar.gz: stat할 수 없습니다: 그런 파일이나 디렉터리가 없습니다
files/
files/file1.txt
files/file2.txt
tar: 앞서 발생한 오류로 실패 코드를 반환하며 빠져나감

 

만약 무슨 소리가 뭔지 모르겠다면 f를 항상 마지막에 써주고 파일명을 입력하시면 됩니다. tar -cvzf files.tar.gz files 이렇게요.

 

4. gzip 압축 해제

 

$ ls -l
합계 4
-rw-rw-r-- 1 ubuntu ubuntu 185  4월 24 12:56 files.tar.gz
$ tar -xvzf files.tar.gz <-- gzip 압축 해제
files/
files/file1.txt
files/file2.txt
$ ls -l
합계 8
drwxrwxr-x 2 ubuntu ubuntu 4096  4월 24 12:55 files
-rw-rw-r-- 1 ubuntu ubuntu  185  4월 24 12:56 files.tar.gz
$ ls -l files
합계 8
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file1.txt
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file2.txt

 

5. bzip2 압축

$ ls -l files
합계 8
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file1.txt
-rw-rw-r-- 1 ubuntu ubuntu 6  4월 24 12:33 file2.txt
$ tar -cvjf files.tar.bz2 files <-- bzip2 압축
files/
files/file1.txt
files/file2.txt
$ ls -l
합계 8
drwxrwxr-x 2 ubuntu ubuntu 4096  4월 24 12:55 files
-rw-rw-r-- 1 ubuntu ubuntu  191  4월 24 13:07 files.tar.bz2

 

6. bzip2 압축 해제

$ ls -l
합계 4
-rw-rw-r-- 1 ubuntu ubuntu 191  4월 24 13:07 files.tar.bz2
$ tar -xvjf files.tar.bz2
files/
files/file1.txt
files/file2.txt

 

7. 파일의 contents(list) 확인

$ ls
files.tar.bz2
$ tar -tf files.tar.bz2 <-- files.ar.bz2 안의 파일 확인, 압축 해제한것이 아님
files/
files/file1.txt
files/file2.txt
$ ls  
files.tar.bz2

 

여기까지만 알아도 tar을 통해서 압축하고 해제하는 데에는 아무 문제없습니다. 너무 쉽죠?

반응형
블로그 이미지

REAKWON

와나진짜

,

DoS(Denial Of Service)

일명 DoS라고 하는 공격은 우리말로 풀어서 설명하면 서비스 거부 공격이라고 하는데요. 이는 공격 대상을 상대로 서비스 불가 상태로 만드는 공격입니다. 시스템의 가용성을 무너뜨리는 공격의 종류입니다. DoS에는 세가지의 공격 유형이 존재합니다. 

1. 파괴 공격 : 데이터나 디스크, 또는 시스템을 파괴하는 공격입니다.

2. 자원 소진 공격 - 시스템: 시스템의 자원을 소진시키는 공격입니다. 예를 들어 CPU의 자원을 소진시키는 공격과 메모리를 사용하는 공격, 디스크 용량을 모두 소진시키는 공격 등이 있습니다. 리눅스에서 무한정 fork()함수를 사용해서 프로세스를 무한정 생기게 만드는 것, 그리고 file을 생성시켜 의미없는 데이터로 디스크를 채우는 공격 등이 있을 수 있겠네요.

3. 자원 소진 공격 - 네트워크 : 역시 자원을 소진시키는 공격인데요. 시스템과는 다르게 네트워크의 대역폭을 소진시키는 공격입니다. DoS에서 네트워크 자원 소진을 통한 공격을 매우 많이 볼 수 있을텐데요. 위의 두 유형보다는 수행이 쉽기 때문입니다. 대표적으로 Http 서버를 DoS로 무력화시키면 해당 웹사이트가 서비스 불가한 상태가 됩니다. 

DoS 공격으로는 아래와 같은 공격이 있습니다.

- Ping Of Death : ICMP 패킷을 정상적인 크기보다 매우 크게 생성해서 전송하는 방식입니다. 이렇게 되면 MTU(Maximum Transfer Unit)에 의해 IP 패킷의 단편화(Fragment)가 발생합니다. MTU는 네트워크 패킷의 최대 전송 크기를 의미하며 이더넷의 경우 MTU는 1500 바이트입니다. 공격 타켓의 컴퓨터에서는 단편화된 페킷을 다시 재조립해야하는데, 이 과정에서 많은 부하가 발생하여 정상적인 서비스를 할 수 없게 만드는 공격입니다.

 

- Land Attack : 출발지 IP 주소와 목적지 IP 주소를 같게 만들어 보냄으로써 자시 자신에게 응답을 보내게 만드는 공격입니다. 이런식으로 서비스를 하지 못하게 하여 가용성을 무너뜨리는 DoS공격 방식입니다. 대부분 OS에서는 출발지 IP주소와 목적지 IP 주소가 같으면 Drop 시킴으로써 현재는 거의 사용하지 않는 DoS 공격입니다. 

 

- Smurf Attack : 출발지 IP 주소를 공격 대상의 IP 주소로 변경하고, ICMP Echo Request를 특정 네트워크에 브로드 캐스트로 보냅니다. 그러면 그 네트워크의 모든 컴퓨터들이 ICMP Echo 응답을 생성해서 공격 대상의 IP 주소로 보내게 되는 DoS공격 기법입니다.

 

- Teardrop Attack : IP 패킷의 재조합 과정에서 Fragment Offset을 일부러 틀리게 전달합니다. 그 결과 재조합하는 과정에서 오류가 발생이 되게 되고, 통신시스템이 문제를 일으키게 되는 DoS 공격방식입니다.

 

이 밖에도 많은 DoS 공격 방식이 존재합니다.

 

DDoS(Distrubuted Denial Of Servier)

DoS 공격을 분산적으로 여러 컴퓨터들이 수행하는 것을 DDoS 공격이라고 합니다. DDoS를 어떤 사람들은 Double DoS라고 하는 분을 봤는데, Distributed DoS입니다. DDoS 공격을 수행하는 컴퓨터는 좀비 PC라고 불립니다. 

 

공격의 흐름은 이렇습니다. 1) 공격자는 C&C 서버라는 서버를 두어서 명령어를 전달합니다. 2) C&C 서버는 공격자로부터 직접 명령을 받아서 감염되어 있는 좀비 PC에 분산하여 명령을 전달합니다.  3) 좀비 PC는 명령을 수행하여 Target PC를 공격하게 됩니다. 좀비 PC는 Bot이라고도 불리는데, Bot은 여러분의 컴퓨터가 될 수 있습니다. 

DDoS 공격의 유형은 아래와 같은 것들이 존재합니다. 

- UDP/ICMP Flooding : 공격자는 다량의 UDP/ICMP 패킷을 서버로 전송하여 네트워크 대역폭을 가득채웁니다. 그래서 다른 사용자들이 서비스를 받지 못하게 만드는 공격입니다.

 

- SYN Flooding : 무수히 많은 다량의 TCP의 SYN 패킷을 공격 대상에게 전달하여 Backlog Queue를 가득채우게 만드는 공격방식입니다. 이 경우 새로운 클라이언트의 연결 요청을 무시하게 되어서 그 클라이언트는 서비스를 받을 수 없게 됩니다. 이 공격방식은 3-Way Handshake의 취약점을 이용한 공격인데요. 클라이언트가 SYN을 보내면 서버에서 SYN+ACK 응답을 보냅니다. 이때 클라이언트가 ACK응답을 보내야만 Connection이 됩니다. 서버에서는 ACK응답이 오지 않은 경우 incomplete queue에 연결 요청정보를 삽입합니다. 반면 ACK가 도착하면 complete queue로 연결 정보를 이동시키죠. 그렇게 되면 accept() 시스템 콜을 통해 연결 socket을 생성시켜 통신하게 됩니다. 이 공격은 SYN 패킷만을 보내고 ACK응답을 주지 않는 공격으로 서버의 incomplete queue를 모두 채워 더는 연결할 수 없는 상태로 만들어버립니다. backlog queue는 incomplete queue와 complete queue를 합친 queue입니다.

 

- HTTP GET Flooding : 동일한 URL을 반복 요청하여 웹서버에 부하를 주는 공격입니다. 자, 여러분 대학교 수강신청때 수강 실패한적이 많죠? 갑자기 서버에 요청이 들어와서 처리할것이 많아 느려졌기 때문입니다. 이러한 현상을 공격으로 만든 것이 HTTP GET Flooding입니다.

 

이 밖에도 해시 도스, 헐크 도스 등 많은 DDoS 공격 방식이 존재합니다. 

DDoS 공격을 수행하는 Tool은 인터넷에 뒤져보면 여러가지 존재합니다. 헐크라던가 슬로로리스등 많은데요. Google에 서치해도 여러가지 많이 나오는데 쉽게 구한다고 해서 쓰게 되면 여러분 어떻게 되는지 아시죠? 그냥 테스트 용도로 자기 PC나 test 서버에다가만 시도하시기 바랍니다. 아래의 링크에서 슬로로리스를 다운받을 수 있습니다.

슬로로리스(HTTP Dos 공격 툴)

https://github.com/gkbrk/slowloris

 

GitHub - gkbrk/slowloris: Low bandwidth DoS tool. Slowloris rewrite in Python.

Low bandwidth DoS tool. Slowloris rewrite in Python. - GitHub - gkbrk/slowloris: Low bandwidth DoS tool. Slowloris rewrite in Python.

github.com

 

이상으로 DoS의 개념, DDoS의 개념을 알아보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

sigaction을 왜 사용할까?

이전 UNIX의 signal함수보다 더 정교한 작업이 가능한 함수입니다. 그 정교한 작업이라는 게 어떤 것들이 있을까요? 

  • 정교한 작업1 (sigaction 코드 예제 - 자식 프로세스의 현재 상태 확인)

예를 들어서 자식이 종료하면 자식 프로세스에서 SIGCHLD 신호를 자식 프로세스에서 발생이 됩니다. 그러면 부모프로세스는 자식이 종료하였는지를 알 수 있죠. 그런데 많은 분들은 자식이 종료(EXIT)할때만 SIGCHLD를 발생시키는 것으로 알고 있습니다. 그런데요. 자식 프로세스는  자신이 정지(STOP)되었을 때, 혹은 재개(CONTINUED)되었을 때, 하물며 다른 프로세스에 의해 죽었을때 (KILL) 역시 부모 프로세스에 SIGCHLD를 보내게 됩니다. 우리가 signal 콜만 이용했을 경우 이러한 차이점을 부모 프로세스가 알아서 세세하게 제어할 수가 없습니다. 그런데 sigaction을 그런 차이들을 알아내어 컨트롤이 가능합니다. 이에 대한 예제 코드는 sigaction에 대해서 설명한 이후에 등장합니다.

  • 정교한 작업2 (sigaction 코드 예제  - 시그널 함수 구현)

뿐만 아니라 read 시스템 콜이 발생하여 사용자로부터 입력을 기다리고 있는 도중에, 시그널이 발생했다고 가정해보세요. 이럴 경우 시그널 핸들러 수행 이후에 1)read를 다시 호출해서 사용자 입력을 받을까요? 아니면 그냥 read는 넘어가고 다음 코드부터 수행할까요? 이러한 제어는 어떻게 해야하는 건가요? 이렇게 재개를 할지, 말지도 sigaction을 통해서 정할 수 있습니다. 물론 재개할지 말지 정하는 sigaction 사용 예제 코드는 밑에 있습니다.  

그전에 이 함수를 알기 위해서는 어느정도 시그널에 대한 기본지식이 있어야합니다. 시그널 집합, 시그널 차단 등의 개념이 나오기 때문인데요. 아래의 포스팅을 통해서 개념을 잡고 오시면 될것 같네요.

https://reakwon.tistory.com/46

 

[리눅스] 시그널 (SIGNAL)1 시그널 다루기(시그널 핸들러)

시그널 의미전달에 사용되는 대표적인 방법은 메세지와 신호입니다. 메세지는 여러가지 의미를 갖을 수 있지만 복잡한 대신 , 신호는 1:1로 의미가 대응되기 때문에 간단합니다. 컴퓨터에서 신

reakwon.tistory.com

https://reakwon.tistory.com/53

 

[리눅스] 시그널 (SIGNAL) 2 시그널 함수 sigprocmask, sigfillset, sigemptyset, sigaddset, sigdelset

시그널 관련 함수 지난 시간에는 간단하게 시그널 기본적인 설명과 어떻게 핸들링하는지에 대해서 알아보았죠? 시그널 개념과 시그널 핸들러는 지난 포스팅을 참고하시기 바랍니다. https://reakwo

reakwon.tistory.com

 

https://reakwon.tistory.com/54

 

[리눅스] 시그널(Signal) 3 sigpending, sigismember, sigsuspend

시그널에 대해서 이야기하는 3번째 시간이 되겠네요. 지난 번에는 시그널 개념과 시그널 관련함수까지 다루어 봤습니다. 시그널 개념과 시그널 핸들러와 시그널 관련 함수(sigfillset, sigemptyset, s

reakwon.tistory.com

 

sigaction

이 함수를 이용하면 어느 특정 신호에 관련된 동작을 조회할 수 있고 수정할 수 있습니다. sigaction의 원형은 이렇습니다.

#include <signal.h>

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

 

사용하기 위해서는 signal.h 헤더 파일을 include시켜줘야합니다. 

▶ signum : signum은 시그널 번호를 의미합니다. signal함수의 처음 인자와 같습니다. 이런거 있잖아요. SIGINT, SIGQUIT 같은 시그널 번호말이죠. 단, 제어불가한 신호 번호는 SIGKILL과 SIGSTOP입니다. 

act : sigaction 구조체인 act 인자는 signum에 대해서 어떤 동작을 취할지에 대한 정보를 담고 있습니다. 즉, 시그널에 대한 동작을 수정하는 정보입니다.

oact : 역시 sigaction구조체인데, 이전에 설정된 동작에 대해서 돌려줍니다. 즉, 시그널에 대한 동작을 조회하는 정보입니다.

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 : 앞서 signum에 대한 동작을 나타내는 함수의 포인터입니다. 설정되지 않으면 기본동작을 의미하는 SIG_DFL입니다.
  •  sa_sigaction : sa_flags로 SA_SIGINFO를 사용할때 설정할 수 있습니다. 이런 경우에는 sa_handler가 사용되지 않고 이 sa_sigaction이 대신 사용됩니다. sa_sigaction에서는 신호 처리부(신호를 처리하는 함수)에 두가지 정보를 더 담아서 보냅니다. siginfo_t와 프로세스 문맥의 식별자가 그것입니다. 
    • 가장 처음 int는 시그널 번호입니다.
    • siginfo_t는 시그널에 대한 부가적인 정보를 담은 구조체입니다. 어떤 정보를 포함하는지는 이 구조체를 참고하면 됩니다. 시그널 정보에 많은 정보들이 들어가기 때문에 필드가 많으니 리눅스 메뉴얼을 참고하시기 바래요! 짧막하게 보면 아래와 같은 정보가 들어갈 수 있습니다.
    • 이후 void* 는 커널이 저장해둔 signal context의 정보를 담습니다.
siginfo_t {
   int      si_signo;     /* Signal number */
   int      si_errno;     /* An errno value */
   int      si_code;      /* Signal code */
   int      si_trapno;    /* Trap number that caused
                             hardware-generated signal
                             (unused on most architectures) */
   pid_t    si_pid;       /* Sending process ID */
   uid_t    si_uid;       /* Real user ID of sending process */
   int      si_status;    /* Exit value or signal */
   clock_t  si_utime;     /* User time consumed */
   clock_t  si_stime;     /* System time consumed */
…
}
  •  sa_mask : 차단할 신호의 집합입니다. sigprocmask를 통해서 특정 신호를 BLOCK 시킬지, 말지를 정합니다.
  •  sa_flags : 신호 옵션들입니다. 아래와 같은 옵션들이 존재합니다. 
SA_NOCLDSTOP signum이 SIGCHLD인 경우 자식 프로세스가 정지되었을때, notification을 받지 않습니다. 자식이 SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 신호를 받아 정지되었을때 신호를 안받는다는 겁니다. 
SA_NOCLDWAIT signum이 SIGCHLD일때, 자식 프로세스가 종료되었을때 시스템이 좀비프로세스를 만들지 않게 합니다.
SA_NODEFER 신호가 잡혀서 신호 처리 함수가 실행되는 도중에 다시 같은 신호가 발생됐을때, 시스템이 자동으로 차단하지 않습니다.
SA_ONSTACK sigaltstack으로 대체 스택을 선언해두었다면 신호가 대안 스택의 프로세스에 전달됩니다.
SA_RESETHAND 신호 처리 함수에 진입할때 이 신호의 처리 방식을 SIG_DFL로 재설정하고 SA_SIGINFO 플래그를 지웁니다. 
SA_RESTART interrupt된 시스템 콜 호출이 자동으로 재시작됩니다. 아래 예에서 보겠습니다.
SA_RESTORER 어플리케이션에서 사용할 의도로 만들어진 flag가 아닙니다. sa_restorer와 관련된 옵션입니다.
SA_SIGINFO 신호 처리부에 추가적인 두가지 정보를 전달합니다. 이때 sa_sigaction함수 포인터를 설정해야합니다. 위의 sa_sigaction 인자에 대한 설명을 참고하세요.
  •  sa_restorer : 이 필드는 앱 사용 목적으로 만들어진 필드가 아닙니다. sigreturn과 관련된 필드라고 하네요. 넘어가겠습니다.

 

sigaction 코드 예제 - 자식 프로세스의 현재 상태 확인

시그널 핸들러를 이용해서 자식 프로세스가 종료하여 SIGCHLD를 발생했을 때 wait을 호출해서 자식 프로세스의 종료 상태를 알 수 있습니다. 그런데 자식 프로세스의 종료뿐만 아니라 정지, 재개 상태로 바뀌었을 때도 이러한 SIGCHLD를 발생시킨다고 했었습니다. 그렇다면 자식 프로세스가 종료할 경우에만 딱 wait할 수 있는 방법이 있을까요?

SA_SIGINFO 플래그와 siginfo_t의 si_code를 이용하면 됩니다.

//sigchld_info.c

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

void action(int signo, siginfo_t *info, void* context){
        if(signo == SIGCHLD){
                printf("pid:%d, uid:%d\n", info->si_pid, info->si_uid);
                if(info->si_code == CLD_EXITED){
                        pid_t child_process = wait(NULL);
                        printf("[parent] child process(%d) exit\n", child_process);
                        exit(0);
                }
                if(info->si_code == CLD_KILLED){
                        pid_t child_process = wait(NULL);
                        printf("[parent] child process(%d) killed\n", child_process);
                        exit(1);
                }
                if(info->si_code == CLD_STOPPED)
                        printf("[parent] child process stopped\n");
                if(info->si_code == CLD_CONTINUED)
                        printf("[parent] child process continued\n");
        } 
}
int main(){
        pid_t pid;
        struct sigaction act;
        act.sa_flags = SA_SIGINFO;
        act.sa_sigaction = action;

        if((pid = fork()) < 0){
                printf("fork error \n");
                return 1;
        } else if(pid == 0){  //child process
                int count = 0;
                while(count < 30) {
                        printf("\t[child] count:%d\n", count++);
                        sleep(1);
                }
                exit(0);
        } else { //parent process 
                printf("[parent] child process : %d\n", pid);
                sigaction(SIGCHLD, &act, NULL);
                while(1) pause();
        }

}

sigaction 구조체에 SA_SIGINFO를 이용해서 sa_sigaction 핸들러를 활용합니다. SA_SIGINFO를 사용하게 되면 sa_sigaction을 사용할 수 있다고 위 설명해서 말씀드렸죠?

struct sigaction act;
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = action;

action 함수에서 siginfo_t의 si_code는 SIGCHLD 신호가 발생했을 때 부가적인 정보가 담겨져있습니다. 그래서 보다 정교한 제어를 하게 됩니다. 

void action(int signo, siginfo_t *info, void* context)

30초간 자식의 상태를 바꿀 수 있습니다. Kill 명령어를 통해서 정지, 재개를 해보세요. 그리고 kill -SIGKILL을 통해서 비정상 종료도 해보시면 자식 프로세스의 상태를 더 자세히 확인할 수 있습니다.

terminal1 terminal2
# ./a.out
[parent] child process : 236739
...
               [child] count:6
pid:236739, uid:0
[parent] child process stopped
pid:236739, uid:0
[parent] child process continued
                [child] count:7
                [child] count:8
...
                [child] count:29
pid:236739, uid:0
[parent] child process(236739) exit
# kill -SIGTSTP 236739
# kill -SIGCONT 236739

 

si_code의 값은 발생한 시그널에 따라서 다르게 설정이 됩니다. 아래의 표를 참고하시기 바랍니다.

 

sigaction 코드 예제  - 시그널 함수 구현

아래는 sigaction함수를 통해서 signal함수를 흉내낸 코드입니다. 

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

typedef void Sigfunc(int);

Sigfunc* my_signal(int signo, Sigfunc *func){
        struct sigaction act,oact;
        act.sa_handler = func;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;

        if(sigaction(signo, &act, &oact) < 0)
                return SIG_ERR;
        return oact.sa_handler;

}
void sig_int(int signo){
        printf("sig_int start\n");
}

int main(){
        Sigfunc* origin;
        //그전의 신호 처리 함수 포인터가 origin에 저장됨. 지금은 SIG_DFL
        origin = my_signal(SIGINT, sig_int);
        printf("main start\n");
        //신호가 발생할때까지 대기
        pause();
        printf("main end\n");
}
main start
^Csig_int start      <-- Ctrl+C 입력
main end

 

뭔가 되는것같긴한데 문제점이 있습니다. 아래와 같이 main함수를 변경해봅시다. 

int main(){
        int n;
        char buf[64];
        my_signal(SIGINT,sig_int);
        if((n=read(0,buf,64)) < 0){
                printf("read failed\n");
        }else{
                printf("%s\n",buf);
        }

}

 

그리고 실행시켜보면 읽기가 실패했습니다. read하는 도중에 Ctrl+C를 눌러서 신호를 발생시켰고, 그때문에 신호 처리함수가 호출이 되었습니다. 이때 read는 interrupt되었지만, 다시 복구 되지 않고 있는 현상이 문제입니다.

^Csig_int start    <-- Ctrl + C 입력
read failed

그래서 SA_RESTART 플래그를 넣어서 시스템 콜이 재시작되도록 설정합니다.

Sigfunc* my_signal(int signo, Sigfunc *func){
        //... 중략
        act.sa_flags = 0;
        act.sa_flags |= SA_RESTART;
        //... 중략
}

이후의 실행은 아래와 같습니다.

hello^Csig_int start    <-- Ctrl + C 입력
world
world

 

hello를 입력하는 와중에 Ctrl+C를 입력시켜서 SIGINT신호를 발생시켰습니다. 그래서 sig_int start라는 출력문이 수행이되었고, 신호 처리 함수가 끝난 후 다시 read를 하게 됩니다. world를 출력하고 엔터를 치면 world만 출력이 되네요.

다시 read를 호출했기 때문입니다. 

지금까지 sigaction에 대한 설명과 sigaction을 활용한 예제 2가지를 보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

오류 처리(Error Handling)

우리가 open으로 파일을 열때 -1의 값을 돌려받는다면 오류가 발생한 것입니다. 그런데 -1만 가지고는 우리는 왜 오류가 발생했는지 알 수가 없죠. 접근 권한 부족이라던가, 그런 파일이 존재하지 않는다던가 말입니다. open과 관련된 오류는 15가지 정도나 됩니다. 이런 원인을 알 수만 있다면 문제를 해결하는데 큰 도움이 되겠죠. 그래서 이번 포스팅에서는 오류의 원인을 어떻게 쉽게 알아 낼 수 있는지 관련된 내용입니다.

errno

errno는 일종의 오류의 코드가 담긴 변수입니다. 이 변수를 활용하기 위해서는 우리는 errno.h라는 헤더파일을 include시켜줘야합니다.

#include <errno.h>

 

errno.h 파일에는 errno와 errno에 설정될 수 있는 에러 코드가 담겨있습니다. 에러 코드는 상수로 담겨있으며 그 종류가 매우 많아서 여기에서는 담지 않겠습니다. 여러분들이 에러 코드에 대한 정보를 직접 확인해보시는 것을 추천해드리며 리눅스 매뉴얼 페이지에 존재합니다. 아래의 명령을 통해서 메뉴얼 페이지를 확인해보세요.

# man 3 errno

 

혹은 errno 명령을 사용하시면 됩니다. 위 명령은 moreutils라는 페키지에 존재하기 때문에 없으면 설치해줍니다. 

# apt install moreutils

 

errno -l 명령어 실행시 

# errno -l
EPERM 1 명령을 허용하지 않음
ENOENT 2 그런 파일이나 디렉터리가 없습니다
ESRCH 3 그런 프로세스가 없음
EINTR 4 중단된 시스템 콜
EIO 5 입력/출력 오류
ENXIO 6 그런 장치 혹은 주소가 없음
E2BIG 7 인수 명단이 너무 김
ENOEXEC 8 Exec 형식 오류

에러를 나타내는 상수는 모두 앞에 E가 붙는 점을 확인하세요.

 

strerror

#include <string.h>
char *strerror(int errnum);

errno 가지고는 어떤 오류인지 사람이 직접적으로 확인하기가 어렵습니다. 코드를 직접 확인하여서 에러 메시지를 출력해야합니다. 이런 번거로움없이 strerror에 인자로 errno를 전달하게 되면 알아서 오류 메시지를 반환해줍니다. 아래는 아주 간략한 예제입니다.

#include <errno.h>
#include <string.h>
#include <stdio.h>

int main(){
        char *msg;
        errno = EPERM;
        msg = strerror(errno);
        printf("error :%s\n",msg);

}

 

출력을 보게 되면 EPERM에 대한 오류 내용을 볼 수 있습니다. 

error :Operation not permitted

 

perror

#include <stdio.h>
void perror(const char *s);

perror는 strerror과 비슷하게 오류의 내용을 문자열로 출력하여 줍니다. 이때 전달되는 문자열 s는 우리가 출력해주길 원하는 문자열이며, 오류 내용은 errno에 따라서 알아서 출력을 해줍니다.

아래는 예제 코드입니다. 

#include <errno.h>
#include <stdio.h>

int main(){
        errno = EPERM;
        perror("error 발생");
}

출력을 보면 errno를 전달하지 않았음에도 함수 내부에서 errno를 확인하기 때문에 에러를 알 수 있습니다. 에러 내용은 위의 strerror의 메시지와 같은 것을 알 수 있죠? 내부적으로 strerror을 사용하는 것을 알 수 있습니다. 

error 발생: Operation not permitted

perror는 이렇게 errno를 직접 명시적으로 넣어주지 않아도 되기 때문에 매우 편리합니다.

 

open error 처리 코드

한 예로 open에 대한 error처리는 아래와 같이 수행이 됩니다. 오직 root만이 읽을 수 있는 파일이 있습니다. 

$ ls -l
-r-------- 1 root   root       0  4월 14 17:08 root_file

 

그리고 root 권한이 없는 일반 사용자로 아래의 코드를 짜서 실행해보면 열리지 않겠죠. 결국 open은 -1을 반환하게 됩니다. 그리고 왜 열지 못하는지는 perror를 통해 확인해볼 수 있습니다.

//err.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>

int main(){
        int fd = open("root_file", O_RDONLY);
        if(fd < 0){
                perror("file open error");
                exit(-1);
        }
        return 0;
}
$ gcc err.c
$ ./a.out
file open error: Permission denied

 

지금까지 오류를 쉽게 확인할 수 있는 내용이었습니다. 사실 error에 대해서 처리해주는 것이 여간 귀찮은 것이 아닌데 이러한 오류 처리 함수를 통해서 번거로움을 줄여보시기 바랍니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

alarm함수

alarm함수는 일정 초단위의 시간 후에 SIGALRM을 발생시키는 함수입니다. 이 시그널이 발생하게 되면 기본 동작은 프로세스 종료입니다. 

혹시 시그널에 대해서 모르시나요? 아래의 포스팅을 참고하며 보시기 바랍니다.

https://reakwon.tistory.com/46

 

[리눅스] 시그널 (SIGNAL)1 시그널 다루기(시그널 핸들러)

시그널 의미전달에 사용되는 대표적인 방법은 메세지와 신호입니다. 메세지는 여러가지 의미를 갖을 수 있지만 복잡한 대신 , 신호는 1:1로 의미가 대응되기 때문에 간단합니다. 신호등이 가장

reakwon.tistory.com

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

seconds : 신호가 발생할 때까지의 클록 초를 의미합니다. 이 시간이 경과가 되고다면 커널이 신호를 발생시키게 됩니다. 만약에 alarm에 0을 전달하게 되면 alarm함수는 알람 발생을 취소하게 됩니다.

반환 : 여기서 주의해야할 점은 하나의 프로세스가 작동시킬 수 있는 알람 시계는 오직 하나뿐이라는 점입니다. 이전에 프로세스가 등록해 놓은 알람이 있다면 alarm함수는 이전에 등록되어있던 알람의 남은 시간(초)를 반환합니다. 만일 알람이 지정되어있지 않은 새로운 알람을 등록하는 것이라면 반환 값은 0이 됩니다. 아래의 심플한 코드와 결과를 보고 어떻게 동작이 되는지 알 수 있습니다.

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int main(){
        //첫번째 알람으로 ret = 0
        int ret = alarm(10);
        printf("처음 등록한 알람 - 남은 시간:%d\n", ret);
        //첫번째 알람 끝나기도 전에 두번째 알람 등록
        ret = alarm(5);
        printf("두번째 등록한 알람 - 남은 시간 :%d\n",ret);

}
처음 등록한 알람 - 남은 시간:0
두번째 등록한 알람 - 남은 시간 :10

 

그런데 위의 코드에서는 alarm이 울리기 전에 코드가 모두 종료가 되기때문에 알람이 실제 울리는지 안울리는지 알수가 없는 코드가 됩니다. 아래의 pause함수도 같이 사용합시다.

 

pause

#include <unistd.h>
int pause(void);

이 함수는 신호를 기다리는 함수입니다. 이 함수가 수행이 되면 프로세스는 신호가 발생될때까지 sleep상태에 빠지게 됩니다. 위에서 언급한 SIGCHLD 외에 다른 신호도 기다립니다. 

반환 : 시그널 핸들러가 처리부를 실행하고 나서 시그널 핸들러가 반환될 경우에 이 함수가 반환됩니다. 이때 pause함수는 errno를 EINTR로 설정하고 -1을 반환합니다. 

다시 말해서 signal함수에 전달되는 시그널을 처리하는 함수가 있죠? 그 함수부터 반환되고 난 이후에 pause가 끝난다는 이야기입니다. 아래의 코드를 보게되면 signal함수로 signal_handler하는 시그널 핸들러를 등록해줍니다. 이후 pause로 신호가 발생될때까지 기다리게 됩니다. 신호가 발생하게 되면 1. signal_handler의 함수가 수행하고, 2. pause()가 반환하면서 3. pause() 이후의 코드가 수행된다는 뜻입니다.

//signal handler 등록
if(signal(SIGALRM, signal_handler) == SIG_ERR) return seconds;
// ... 수행부 ..//
pause();

 

sleep함수 구현

지금까지 소개한 alarm과 pause함수로 간단한 sleep()함수를 구현해볼 수가 있습니다. sleep함수는 원래 unistd.h에 포함되어 있는 함수이고 사용자가 지정한 초수만큼 대기(sleep상태)가 됩니다. 

#include <unistd.h>
unsigned int sleep(unsigned int seconds);

구현

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
static void sig_alarm(int signo){
        printf("signo:%d\n",signo);
}

unsigned int my_sleep(unsigned int seconds){
        if(signal(SIGALRM, sig_alarm) == SIG_ERR)
                return seconds;
        int ret = alarm(seconds);
        //이미 alarm이 존재하면 존재하는 alarm 삭제 후 다시 등록
        if(ret < seconds){
                alarm(0);       //취소
                alarm(seconds); //새등록
        }
        pause();
        return alarm(0);
}
int main(){
        printf("before my_sleep\n");
        //3초간 sleep
        my_sleep(3);
        printf("after my_sleep\n");

}

 

 

이미 sleep 중인데, 다시 sleep이 호출되게 되면 구현의 편의를 위해서 기존 sleep을 취소하고 새로운 sleep으로 교체하도록 하였습니다. 사실 이전의 sleep까지 기다리고 난 이후에 새로운 sleep을 해주어야합니다. 

보완

여기서 pause 호출전에 alarm이 먼저 호출하게 되면 pause는 다른 신호가 잡히기 전까지는 영원히 대기하게 됩니다. 이를 보완하기 위해서 setjmp를 활용할수 있습니다.

setjmp를 잘 모르신다면 setjmp와 longjmp와 관련해서는 아래의 포스팅을 참고하시기 바랍니다.

https://reakwon.tistory.com/211

 

[리눅스] 단번에 이해하는 setjmp, longjmp를 활용하는 방법

비국소(nonlocal) 분기 setjmp나 longjmp는 이름에서도 알수 있듯이 jump하는 함수입니다. 실행부를 점프한다는 것입니다. 그전에 비국소(nonlocal)라는 단어를 설명할 필요가 있습니다. C언어에서 goto구문

reakwon.tistory.com

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>
static jmp_buf buf;
static void sig_alarm(int signo){
        longjmp(buf,1);
}

unsigned int my_sleep(unsigned int seconds){
        if(signal(SIGALRM, sig_alarm) == SIG_ERR) return seconds;

        if(setjmp(buf) == 0){
        	    //pause호출 되기전 SIGALRM이 호출된다면? 
                int ret = alarm(seconds); 
                if(ret < seconds){
                        alarm(0);
                        alarm(seconds); 
                }
                pause();
        }
        return alarm(0);
}
int main(){
        printf("before my_sleep\n");
        //3초간 sleep
        my_sleep(3);
        printf("after my_sleep\n");

}

 

만약 SIGALRM이 pause 호출 전에 발생하게 된다면, sig_alarm이 호출이 되고 longjmp에 의해 setjmp쪽으로 이동이 되겠죠. setjmp의 반환값은 1이 되기 때문에 if(setjmp(buf) == 0)을 수행하지 않고 빠져나오게 됩니다. 그래서 my_sleep함수가 끝나게 되죠. 이렇게 pause가 무한히 신호를 기다리지 않게 되기 때문에 처음 my_sleep() 구현에 문제점을 해결할 수가 있습니다.

그럼에도 불구하고 여전히 다른 문제점들이 존재하기는 합니다. 이 함수를 호출하는 프로세스에서 signal함수를 통해서 다른 시그널 핸들러를 등록하게 되면 안좋은 결과가 생길 수 있습니다.

하지만 alarm과 pause를 어떻게 사용하는지에 대한 기본적인 개념을 알기 위한 코드이니, 문제점은 여러분들이 해결해보시기 바랍니다.

지금까지 alarm함수와 pause함수에 대해서 알아보았고 이것을 활용하는 sleep함수까지 살짝 맛보았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

system함수

system함수는 유닉스 운영체제에는 모두 지원합니다. system함수는 입력받은 command의 문자열을 실제로 실행시켜주는 함수입니다.

system함수를 사용하기 위해서는 stdlib.h 헤더파일을 include 해야합니다.

#include <stdlib.h>

system함수의 원형은 아래와 같습니다.

int system(const char *command);

사용하는 방법은 매우 간단합니다. command에 실행할 명령어를 전달해주기만 하면 됩니다. 아래의 사용 예를 보시면 금방 사용하실수 있을겁니다.

사용예)

//system_test.c
#include <stdlib.h>
#include <stdio.h>

int main(){
        char *command="ls -al";
        int ret;
        ret = system(command);
        printf("system함수 종료 :%d\n",WEXITSTATUS(ret));
}
# gcc system_test.c
# ./a.out
합계 208
drwxr-xr-x 19 ubuntu ubuntu  4096  4월 11 17:22 .
drwxr-xr-x  6 root   root    4096  4월  1 15:38 ..
-rw-------  1 ubuntu ubuntu   378  4월 11 17:17 .Xauthority
-rw-------  1 ubuntu ubuntu  5496  4월 11 12:42 .bash_history
-rw-r--r--  1 ubuntu ubuntu   220  2월 22  2021 .bash_logout
...
system 함수 호출 완료 ret:0

 

system함수의 내부

system함수에 NULL을 전달하게 되면 적절한 명령처리기가 존재한다면 0을 돌려줍니다. 그 외에는 상황에 따라 다릅니다.  system함수를 내부적으로 들여다보면 fork, exec, waitpid로 이루어진 함수입니다. 이 세개의 함수에 대해서 모르신다면 아래의 포스팅을 참고하시기 바랍니다.

- fork()

https://reakwon.tistory.com/45

 

[리눅스] 프로세스 생성과 특징, 종료 (fork, wait), 예제 코드

프로세스(process) 프로세스는 간단히 말하자면 실행 중인 프로그램을 의미합니다. 아마 여러분들은 컴퓨터를 하면서 아주 빈번하게 듣는 용어이기도 합니다. 실행 중인 프로그램이라?? 컴퓨터에

reakwon.tistory.com

- exec()

https://reakwon.tistory.com/207?category=300674 

 

[리눅스] exec류의 함수 사용방법,간단한 쉘구현 -execl,execv,execve...

exec famliy exec~로 되는 함수들이 리눅스에 몇가지 존재하고 있습니다. 리눅스에서는 exec family라고 부릅니다. 이 함수들은 모두 공통적으로 프로그램을 실행한다는 특징을 갖고 있습니다. 그 함수

reakwon.tistory.com

- waitpid

https://reakwon.tistory.com/99

 

[리눅스] 조건변수를 통한 스레드 동기화 설명, 예제(pthread_cond_wait, pthread_cond_signal)와 생산자-소

조건 변수 조건 변수를 설명하기 전에 다음과 같은 상황이 발생했다고 칩시다. 먼저 스레드 2개가 존재합니다. 저는 짧은 코드를 좋아하므로 아주 간단한 역할을 하는 2개의 쓰레드를 생성했습

reakwon.tistory.com

 

 

[리눅스] 조건변수를 통한 스레드 동기화 설명, 예제(pthread_cond_wait, pthread_cond_signal)와 생산자-소

조건 변수 조건 변수를 설명하기 전에 다음과 같은 상황이 발생했다고 칩시다. 먼저 스레드 2개가 존재합니다. 저는 짧은 코드를 좋아하므로 아주 간단한 역할을 하는 2개의 쓰레드를 생성했습

reakwon.tistory.com

 

system함수의 반환 값

1. fork 호출이 실패했거나 waitpid가 EINTR외의 오류를 돌려주면 system함수는 errno를 EINTR오류로 설정하고 -1를 반환합니다.

2. exec함수가 실패했다면 이런 경우에는 shell을 실행할수 없다는 뜻이며, exit(127)과 동일합니다. 

3. 그 외의 경우에는 waitpid에 지정된 셸의 종지 상태가 return됩니다.

 

아래의 코드는 system함수를 흉내낸 코드입니다. 

#include <stdio.h>
#include <errno.h>
#include <unistd.h>

int system(const char *cmd){
        pid_t pid;
        int status;

        if(cmd == NULL) return 1;       //UNIX에는 명령 처리기가 존재
        if((pid = fork()) < 0){
                status = -1;    //프로세스 생성 에러
        }else if(pid == 0){     //자식 프로세스
                execl("/bin/sh","sh","-c",cmd,(char*)0);
                _exit(127);		//위 2번읜 case
        }else{                  //부모 프로세스 : 자식이 끝날때까지 기다림
                while(waitpid(pid, &status, 0) < 0){
                        if(errno != EINTR){	//위 1번의 case
                                status = -1;
                                break;
                        }
                }
        }
        return status;
}
int main(){
        int ret;
        ret = system("ls -al");
        printf("system함수 종료 :%d\n",WEXITSTATUS(ret));
}

 

이러한 구현사항때문에 내부적으로 fork()로 자식 프로세스를 수행하고 자식 프로세스는 exec함수를 호출하는데요. 부모 프로세스는 waitpid로 자식 프로세스를 기다리기 때문에 system다음 줄의 printf가 실행될수 있는 것이죠.

 

종지 상태 확인

WEXITSTATUS로 실제 exit()이나 return값을 확인할수 있습니다. 아래는 main에서 바로 return 18로 빠져 나오는 한 프로그램입니다. 혹은 exit(18)을 해도 똑같습니다.

//program.c
int main(){
        //exit(18);
        return 18;
}
# gcc program.c -o program
# ls program
program

program이라는 실행파일이 생겨납니다. 이제 이 실행파일을 실행시키기 위해 system함수를 사용해보겠습니다.

//system_test.c
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
        int ret;
        ret = system("./program");
        printf("system함수 종료 :%d\n",WEXITSTATUS(ret));
}
# gcc system_test.c
# ./a.out
system함수 종료 :18

 

우리가 return했던 값을 확인할 수 있죠? 단순 ret값을 출력하는게 아닌 매크로를 통해서 종지상태를 확인해야한다는 점을 기억하세요. 

 

이상으로 system에 관한 포스팅을 마치도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

비국소(nonlocal) 분기

setjmp나 longjmp는 이름에서도 알수 있듯이 jump하는 함수입니다. 실행부를 점프한다는 것입니다. 그전에 비국소(nonlocal)라는 단어를 설명할 필요가 있습니다. C언어에서 goto구문을 아시죠? goto도 역시 코드를 이동시킬 수 있지만, goto와는 다릅니다. 아래는 간단한 goto의 활용예입니다.

#include <setjmp.h>
#include <stdio.h>

int go_to(){
        int i = 1;

        if(i==1)
                goto here;

        here:{
                printf("here!\n");
        }
}
int main(){
        go_to();
        return 0;
}

실행해보면 here label이 있는곳으로 코드가 이동되어 실행이되는것을 볼 수 있습니다.

label은 항상 goto가 부르는 label과 같은 함수 내에 있어야합니다. 즉, 아래와 같은 코드는 컴파일되지 않습니다. 다른 함수로 건너뛸수가 없다는 말입니다.

#include <setjmp.h>
#include <stdio.h>

int go(){
    here :
        printf("here!\n");
    go_to();
}
int go_to(){
    int i = 1;
    if(i==1)
        goto here;

}
int main(){
    go();
    return 0;
}
$ gcc jump.c
jump.c: In function ‘go’:
jump.c:7:2: warning: implicit declaration of function ‘go_to’ [-Wimplicit-function-declaration]
  go_to();
  ^
jump.c: In function ‘go_to’:
jump.c:12:3: error: label ‘here’ used but not defined
   goto here;

 

setjmp와 longjmp는 비국소(nonlocal) 분기를 수행합니다. 비국소라는 같은 함수내부에서만 점프하지는 않는다는 것입니다. 함수가 호출된 경로 안에 있는 다른 어떤 함수로 점프하는 것이 가능하다는 얘기입니다.

 

setjmp, longjmp

#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmp, longjmp 를 사용하기 위해서는 setjmp.h를 include시켜야합니다. 아래의 코드에서 보겠지만 이 함수들에 대해서 간단하게 설명하면 이렇습니다.

setjmp : 점프할 위치를 지정합니다. env는 나중에 longjmp가 호출되었을때 스택의 상태를 복원할때 필요한 모든 정보를 담는 배열입니다. setjmp의 반환값은 longjmp의 val입니다. 이 반환값으로 흐름을 분기시킬 수 있습니다.

longjmp : 실제로 점프하는 함수입니다. 이 함수를 통해서 setjmp위치로 돌아갑니다. 이때 env는 setjmp와 같은 env를 사용하며 val은 setjmp가 반환할 값입니다. 

이 함수들을 이용해서 다음의 프로그램을 수정할것인데요. 프로그램에 대해서 설명하자면 비밀번호를 입력받고 입력이 8자리인가, 포함되어서는 안될 문자가 있는가 검사하는 아주 간단한 프로그램입니다.

#include <stdio.h>
#include <string.h>
#define TRUE 1
#define FALSE 0
char password[32];

int check_size(){
        if(strlen(password)< 8) return FALSE;
}
int is_possible_word(){
        int i;
        for(i=0;i<strlen(password);i++)
                if(password[i] == '|' || password[i] == '%' || password[i] == '&') return FALSE;
        return TRUE;
}
int verify_input(){
        if(!check_size()) return 1;
        if(!is_possible_word()) return 2;
        return 3;
}
int main(){

        while(1){
                printf("비밀번호:");
                scanf("%s",password);
                int result = verify_input();
                if(result == 3) break;
                switch(result){
                        case 1:
                                printf("비밀번호는 8자 이상이어야합니다.\n");
                        break;
                        case 2:
                                printf("|, %%, &문자는 사용할 수 없습니다.\n");
                        break;
                }
        }
        return 0;
}

 

위 코드의 문제점을 파악해보도록 하지요.

1. 검증이 완료될때까지 while문으로 계속 password를 입력받습니다. 

2. verify_input에는 두가지 정도를 검증하는데 하나는 길이, 다른 하나는 포함불가한 문자를 거르는것인데요. 이때 하나의 조건이라도 맞지 않는다면 verify_input에 그에 맞는 error code를 주고, verify_input은 다시 main으로 error code를 반환하게 됩니다. 만약 검증해야될 조건이 많다면 더 복잡해지겠네요. 

이 프로그램의 특징은 비밀번호의 조건중 하나라도 맞지 않으면 처음부터 다시 수행합니다. 그냥 main 함수 밑에 점프할 곳을 딱 정해놓고 check_size()나 is_possible_word()에 그 조건이 맞지 않는다면 그쪽으로 점프시키는 쪽으로 변경하면 안될까요? 이렇게 되면 while을 쓰지 않아도 되고, verify_input()에 굳이 error code를 전달하지 않아도 되니까요. 만약 verify_mode()에서 is_possible_word()까지 들어가고 여기에 새로운 조건을 검사하는 함수가 추가되서 계속 호출되면 코드는 더 복잡해집니다. 이러한 상황은 곧 setjmp, longjmp를 통해서 간단하게 해결할 수 있습니다.

그 코드가 아래에 있습니다. 

#include <stdio.h>
#include <string.h>
#include <setjmp.h>

#define TRUE 1
#define FALSE 0

//setjmp와 longjmp가 같이 사용하는 jmp_buf로
//전역적으로 사용
jmp_buf jmpbuf;
char password[32];

void check_size(){
        if(strlen(password)< 8) 
                longjmp(jmpbuf, 1);
}

void is_possible_word(){
        int i;
        for(i=0;i<strlen(password);i++){
                if(password[i] == '|' || 
                                password[i] == '%' || password[i] == '&') 
                        longjmp(jmpbuf,2);
        }
}

void verify_input(){
        check_size();
        is_possible_word();
}

int main(){
        int ret = 0;
        //setjmp는 나중에 longjmp를 통해서 점프되었을때실행되는 부분
        if((ret = setjmp(jmpbuf)) > 2){
                printf("unknown option\n");
                return -1;
        }else{
                //longjmp에서 두번째 인자 val이 ret값이 됨.
                switch(ret){
                        case 1:
                                printf("비밀번호는 8자 이상이어야합니다.\n");
                                break;
                        case 2:
                                printf("|, %%, &문자는 사용할 수 없습니다.\n");
                                break;
                }
        }

        printf("비밀번호:");
        scanf("%s",password);

        verify_input();
        return 0;
}

 

메인의 while문이 빠졌고, check_size(), is_possible_word()에서 직접 longjmp를 통해서 main함수의 입력받는 부분으로 되돌아갑니다. setjmp의 반환값은 longjmp의 두번째로 전달되는 인자의 값인것을 알 수 있습니다.

 

만약 setjmp 이전에 longjmp를 호출하게 되면 Segmentation fault가 발생하게 됩니다. 그러니까 아래와 같은 상황에서는 점프할 수 없습니다. 점프는 함수가 호출된 순서에서 거꾸로 돌아가는 것(원복의 목적)만 가능합니다.

 

#include <stdio.h>
#include <setjmp.h>
jmp_buf jmpbuf;
int main(){
    int ret = 0;
    longjmp(jmpbuf, 1); //이 코드는 수행할수 없다.

    setjmp(jmpbuf);
    return 0;
}

 

사실 스택 프레임을 거꾸로 돌린다고 표현하는게 더 정확할 것 같네요. 여기에 위 코드의 동작과정을 그림으로 표현하였습니다. check_size()에서 longjmp를 호출했을때의 상황입니다.

check_size()에서 longjmp

 

즉, 스택을 복원하면서 점프하는 것입니다. 여기서 정적변수, 전역변수, 휘발성 변수는 복원되지 않습니다. 정적변수, 전역변수는 스택 메모리에 기억되는 값이 아니니까요.  메모리의 구조를 확인하려면 아래의 포스팅을 확인해보세요.

https://reakwon.tistory.com/20

 

[C언어] 동적 메모리 할당의 세가지 방법 malloc, calloc, realloc

동적 메모리 할당 우리는 이제껏 메모리를 할당할때 정적으로 할당했습니다. 어떤 것이냐면 int arr[100]; 이렇게 할당을 했었죠. 뭐 문제없습니다. 실행도 잘 되구요. 하지만 이런 상황은 조금 불

reakwon.tistory.com

 

지금까지 setjmp, longjmp를 통해서 비국소적 분기를 이해해보았습니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,