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

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

와나진짜

,