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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

시그널 관련 함수

지난 시간에는 간단하게 시그널 기본적인 설명과 어떻게 핸들링하는지에 대해서 알아보았죠?  

시그널 개념과 시그널 핸들러는 지난 포스팅을 참고하시기 바랍니다.

https://reakwon.tistory.com/46

 

 

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

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

reakwon.tistory.com

 

여러개의 신호를 간편하게 다루기 위해서는 신호를 집합으로 표시하는 자료 형식이 필요하게 됩니다. 단순히 생각해보면 int 형식을 한비트마다 하나의 신호로 대응시켜 집합으로 표시할 수도 있지만, int의 비트보다 더 많은 신호가 존재할 수 있기 때문에 신호만의 새로운 자료 형식이 필요하게 되었습니다. int의 비트 32비트이고 신호는 32개가 넘게되니까요.  이러한 자료 형식이 sigset_t라는 자료 형식입니다. sigset_t는 신호의 집합임을 기억합시다.

이런 신호 집합을 왜 쓰게 될까요? 앞에서 얘기했다시피 많은 신호를 간편하게 다루기 위함입니다. 모든 신호를 막는다거나(BLOCK), 막은 신호를 다시 푼다던가(UNBLOCK), 신호가 발생했지만 Block되어서 대기(PENDING) 중인 신호가 무엇이 있는가, 이러한 작업을 쉽게 할 수 있습니다. 프로세스는 신호 집합을 가지고 있고 관리합니다. 이와 연관된 함수가 추가로 필요하게 되는데, 그것이 오늘 소개할 함수들입니다.

 

시그널 중에서 SIGSTOP와 SIGKILL은 절대 제어할 수 없으므로 알아두시기 바랍니다.

 

o sigfillset, sigemptyset

int sigfillset(sigset_t *set);

int sigemptyset(sigset_t *set);

 

이 두 함수는 setset_t라는 집합에서 모든 시그널을 추가하거나 모든 시그널을 제거합니다. 성공시 0, 실패시 -1을 반환하구요. 아래는 sigfillset과 sigemptyset의 동작과정을 말해줍니다. 

sigfillset을 사용하면 모든 시그널을 집합에 추가하는데요. 반대로 sigemptyset은 set이라는 집합에서 시그널을 전부 깨끗이 지워줍니다.

 

 

 

sigaddset, sigdelset

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

 

이 함수들은 이름에서 알 수 있듯 signal을 추가하거나 삭제합니다. 역시 성공시 0, 실패시 -1을 반환합니다.

기존의 SIGSEGV와 SIGHUP이 있는 set에서 SIGINT를 추가하면 set은 {SIGINT, SIGSEGV, SIGHUP}가 있게 됩니다. 거기서 SIGHUP을 sigdelset으로 제거하면 결국 set에는 {SIGSEGV, SIGINT}가 존재하게 되는 것입니다.

 

 

 

o sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t oldset);

sigprocmask는 시그널을 블록시킬지 말지를 결정합니다. 

 

how : 시그널을 어떻게 제어할지를 말해줍니다. how에는 세가지 동작이 있는데요. 

   1. SIG_BLOCK : 기존에 블록된 시그널이 있다면 두 번째 인자는 set의 시      그널을 추가하라는 의미입니다.

   2. SIG_UNBLOCK : 기존의 블록된 시그널에서 set의 시그널을 제거합니       다.

   3. SIG_SETMASK : 기존의 블록된 시그널을 전부 제거시키고 새로운 set      의 시그널들을 블록시킵니다.

 

set : how가 동작하는 방식에 따라 기존의 block된 시그널에 대해 set을 추가시킬지, 제거시킬지, 아니면 전부 set으로 설정할지를 의미합니다. 그러니 set은 이번에 설정할 시그널이라고 기억해두세요.

oldset : 이 전에 블록된 시그널들을 저장합니다.

 

예제 코드 1)

sigprocmask는 우선 이해하기가 조금 까다로운데요. 주석을 통해서 설명을 하긴 했지만 그림이 조금 필요하겠네요. 아래는 sigprocmask를 통해서 시그널을 제어하는 코드입니다.

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

int main(){
        sigset_t set, oldset;

        sigemptyset(&set);
        sigemptyset(&oldset);

        sigaddset(&set,SIGINT);
        sigaddset(&set,SIGQUIT);
        sigprocmask(SIG_BLOCK,&set,NULL);

        printf("SIGINT와 SIGQUIT는 블록되었습니다.\n");
        printf("Ctrl+C와 Ctrl+\\ 눌러도 반응이 없습니다.\n");

		//만약 Ctrl + \(SIGQUIT)을 눌렀다면 5초후 Coredump가 생기고 종료
        //SIGQUIT의 기본동작은 Coredump + 종료
        sleep(5);

		//현재 set에서 SIGINT를 뺌. set에는 SIGQUIT만 있는 상태
        //중요한것은 프로세스에 적용하지 않은 상태
        sigdelset(&set,SIGINT);
        
        //프로세스에 Unblock을 set에 적용. SIGQUIT은 이제 Block되지 않음
        sigprocmask(SIG_UNBLOCK,&set,&oldset);

        printf("만약 Ctrl+\\을 눌렀다면 종료합니다.\n");
        printf("현재 남은 시그널은 SIGINT입니다.\n");
        printf("Ctrl+C를 눌러도 반응이 없습니다.\n");

        sleep(5);

        set=oldset;
        sigprocmask(SIG_SETMASK,&set,NULL);

        printf("다시 SIGINT와 SIGQUIT이 블록됩니다.\n");
        printf("Ctrl+C와 Ctrl+\\ 눌러도 반응이 없습니다.\n");

        sleep(5);

        sigprocmask(SIG_UNBLOCK,&set,NULL);
        //아무 시그널(Cntl +C 혹은 Cntl+\)을 주지 않았다면 아래의 메시지가 출력되고 종료
        printf("모든 시그널이 해제되었습니다.\n");

}


이제 하나하나씩 까보도록 합시다.

 

1.

초기에 현재 프로그램의 블록된 시그널은 없습니다. set과 oldset도 역시 깨끗이 비워줍니다. 

sigset_t set, oldset; 
sigemptyset(&set); 
sigemptyset(&oldset);
 

아래 그림을 보세요. 저의 머리처럼 비어있는 걸 알 수 있습니다.

 

 

이후 sigaddset으로 set에 SIGINT와 SIGQUIT을 추가합니다. 중요한것은 blocked signal에 추가한 것이 아닙니다. SIGINT의 단축키는 Ctrl+C, SIGQUIT의 단축기는 Ctrl+\랍니다.

2.

 sigaddset(&set,SIGINT);  
 sigaddset(&set,SIGQUIT);
 

아래 그림에도 set 집합에서 SIGINT와 SIGQUIT이 추가된 것을 알 수 있군요.

 

 

 

3.

이 set에 있는 시그널들을 block 시키기 위해서 sigprocmask를 호출하는데 how의 인자는 SIG_BLOCK입니다.

sigprocmask(SIG_BLOCK,&set,NULL);

현재 블록된 시그널에 set의 시그널이 추가되었음을 알 수 있네요.

 

아까도 봤다시피 SIG_BLOCK은 현재 블록된 시그널에서 set의 시그널을 추가하는 겁니다.

만약 blocked signal에 SIGHUP이 존재한다면 sigprocmask(SIGBLOCK, &set, NULL) 호출 후 blocked signal 집합에는 {SIGHUP, SIGINT, SIGQUIT)이 됩니다. 

 

그렇기 때문에 이제 SIGINT와 SIGQUIT는 이제 블록됩니다.

 

 

 

이후 5초간 SIGINT와 SIGQUIT 신호를 보내도 아무런 반응이 없는 걸 알 수 있나요?

 

4.

 

sigdelset(&set,SIGINT); 
sigprocmask(SIG_UNBLOCK,&set,&oldset);

5초가 지나면 set에서 SIGINT를 제거하는 군요. 그렇다면 set에는 SIGQUIT만 남게 되죠.  sigprocmask로 set에 있는 집합을 blocked signal에서 제거합니다. 

 

하지만 oldset이 설정되어있었네요. 그러니까 oldset은 기존에 블록된 시그널이 잡히게 됩니다. 기존에 블록된 시그널은 위의 그림에서 SIGINT와 SIGQUIT이었군요. 그 후 blocked signal에서 set에 있는 시그널을 제거하게 되면 SIGINT만 남겠네요.

아래 그림에서 보여주듯 blocked signal에는 결국 SIGINT만 남게됩니다. 따라서 5초이내에 SIGQUIT을 보냈다면 프로그램이 코어덤프와 함께 종료됩니다. SIGQUIT의 블록 상태가 풀리게 되니까요.

 

 

5.

이후 set은 oldset의 값을 복사하네요.

 set=oldset; 

 

 

6.

sigprocmask(SIG_SETMASK,&set,NULL); 

 

이제 SIG_SETMASK로 set에 있는 시그널들을 blocked signal에 설정합니다. 설정하는 것입니다. 추가하는 것이 아니에요.

 

그러니 blocked signal에는 SIGINT와 SIGQUIT이 다시 설정되는 것을 알 수 있죠.

 

 

 

그래서 다시 SIGINT와 SIGQUIT을 보내도 5초간 아무 응답이 없게 되지요.

7.

 

sigprocmask(SIG_UNBLOCK,&set,NULL); 

5초가 지난 후 set에 있는 시그널들을 blocked signal에서 제거하게 되는데요. 만약 5초 전에 SIGINT나 SIGQUIT 신호를 주었다면 바로 종료하게 되는 겁니다.

왜냐면 5초가 지나면 아래의 그림처럼 blocked signal에서 SIGINT와 SIGQUIT이 존재하지 않으니 블록된 시그널은 풀리게 되면서 프로그램이 종료가 됩니다. 

 

 

 

만약 SIGINT 또는 SIGQUIT를 보내지 않았다면 프로그램은 "모든 시그널이 해제되었습니다" 라는 메세지와 함께 종료될겁니다.

 

process의 signal 정보는 어디에?

그렇다면 프로그램 실행시, 이 process의 block된 시그널의 정보는 어디에 저장이 되고 관리가 될까요? 즉, 위의 그림에서 blocked signal은 누가 관리해주냐는 것입니다. 이에 대한 정보들은 kernel의 task_struct 구조체로 인해 관리가 됩니다. task_struct는 process나 thread가 실행될때마다 하나씩 생성이 되는데, 여기에 정말 많은 정보들이 들어있습니다. 다음은 signal 관련 field들입니다.

// linux/sched.h

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
...

	/* Signal handlers: */
	struct signal_struct		*signal;
	struct sighand_struct		*sighand;
	sigset_t			blocked;
	sigset_t			real_blocked;
	/* Restored if set_restore_sigmask() was used: */
	sigset_t			saved_sigmask;
	struct sigpending		pending;
	unsigned long			sas_ss_sp;
	size_t				sas_ss_size;
	unsigned int			sas_ss_flags;
    
 };

 

task_struct의 멤버를 보게되면 blocked, real_blocked라는 이름이 보이시죠? sigset_t 어디서 많이 보았죠? 이처럼 kernel에서 task_struct를 통해서 관리하여진다는 것입니다.

 

이상으로 signal 집합을 제어하는 함수들을 소개하고 알아보았습니다. 다음 포스팅은 sigpending과 관련된 함수에 대해서 알아보도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

시그널

의미전달에 사용되는 대표적인 방법은 메세지와 신호입니다. 메세지는 여러가지 의미를 갖을 수 있지만 복잡한 대신 , 신호는 1:1로 의미가 대응되기 때문에 간단합니다. 컴퓨터에서 신호, 즉 시그널은 소프트웨어적인 interrupt입니다. 컴퓨터 용어에서 인터럽트라는 것은 하던일 A를 중간이 잠시 멈추고 다른일 B를 하고 난 후 다시 A로 돌아와서 멈춘 부분부터 일을 하는 것이죠.  자, 이 의미를 명확히 아셔야할 필요가 있습니다. 신호가 왜 SW적인 인터럽트인가가 포스팅 아래에 코드와 함께 설명이 됩니다.

우선 컴퓨터에서 시그널의 종류에는 어떤 것들이 있을까요? 리눅스 상에서 kill -l 명령어를 입력하면 모든 시그널의 종류와 대응되는 번호를 알 수 있습니다.

 

#kill -l

 

 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP

 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1

11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM

16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP

21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ

26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR

31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3

38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8

43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7

58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2

63) SIGRTMAX-1  64) SIGRTMAX

 
이 시그널 중에 몇가지만 알아보도록 하겠습니다. 

 
1. SIGHUP : 터미널과 연결이 끊겼을 때 발생합니다. 기본적인 처리는 프로세스가 종료되는 것입니다.
2. SIGINT : 인터럽트가 발생했을때 발생합니다. 기본으로 프로세스가 종료됩니다.
9. SIGKILL : 프로세스를 무조건 종료합니다. 절대 무시할 수 없으며 제어될 수도 없습니다.
11. SIGSEGV : 프로세스가 잘못된 메모리를 참조했을 때 발생합니다. 기본 동작은 코어덤프를 남기고 종료합니다.
19 SIGSTOP : 프로세스를 중단시킵니다. 종료한 상태는 아닙니다. 이 신호 역시 제어될 수 없습니다.

 

프로세스가 시그널을 받게 되면

 

1. 시그널에 해당되는 기본 동작을 하거나
2. 그 시그널을 무시하거나
3. 사용자가 정의한 함수를 통해 동작 방식을 바꿀 수 있습니다.
 
시그널은 다음과 같은 성질이 있습니다.
 
● 비신뢰성
시그널을 보내면 그 시그널이 제대로 도착했는지, 잘 전달되었는지 확인하지 않습니다. 때문에 신뢰성이 낮습니다.
 
 대기하지 않음
만약 시그널 처리 함수를 시그널을 처리하고 있는데 그 사이에 다시 시그널을 주게 되면 그 시그널은 무시됩니다.
 
 
프로세스를 직접 생성해서 시그널을 한번 줘보도록 합시다.

 

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
        printf("give signal...\n");
        sleep(30);
        exit(0);
}

이후 컴파일을 하여 실행파일로 만들어줍니다. 이후 실행하면 30초간 프로그램이 실행될 겁니다.

 

이 상태에서 Ctrl+C를 누르게 되면 SIGINT 신호를 보내게 됩니다. 기본동작은 종료이므로 프로세스가 종료하게 됩니다.

 

또는 kill -시그널번호 프로세스 id 를 통해서 시그널을 보낼 수 있습니다. 그러니까 SIGINT를 보내려면 kill -2 pid 또는 kill -SIGINT pid를 통해서 SIGINT보낼 수 있습니다.

 

 

# ./a.out

give signal...

^C

#
 
우리는 이 시그널을 제어하고 싶습니다. SIGINT 신호 발생시 종료한다는 문자열을 출력하고 3초 이후에 종료하고 싶습니다.
 
그래서 리눅스는 signal핸들러를 제공합니다.
 
signal함수
원형은 다음과 같습니다.
 

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

 

signum은 시그널을 발생시키는 번호입니다. 아까 SIGINT는 2번이었죠?? 아니면 매크로를 쓸수도 있습니다. SIGINT 그대로가 매크로로 정의되어 있습니다.

 

두번째 인자로 handler라는 함수포인터가 보이네요. 여기에 함수를 인자를 주게 되면 시그널을 받았을때 그 함수가 호출됩니다.

 

직접 코드로 짜고 확인해보지요.

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

void interruptHandler(int sig){
        printf("this program will be exited in 3 seconds..\n");
        sleep(3);
        exit(0);
}

int main(){
        signal(SIGINT, interruptHandler);
        printf("input Ctrl+C\n");
        while(1);
}


컴파일하고 실행 후에 Ctrl+C를 입력하면 메시지와 함께 3초 후 프로그램이 종료됩니다.

# ./a.out 
input Ctrl+C
^Cthis program will be exited in 3 seconds..

이번엔 SIGTSTP이라는 시그널을 보내보도록 할게요. 위 코드에서 단지 SIGINT를 SIGTSTP이라고 바꿔주기만 하면 됩니다. (아, 물론 printf안에 문자열도 바꾸면 이쁘장하겠죠?)

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void stopHandler(int sig){
        printf("this program will stop in 3 seconds..\n");
        sleep(3);
        exit(0);
}

int main(){
        signal(SIGTSTP, stopHandler);
        printf("input Ctrl+Z\n");
        while(1);
}

그후 Ctrl+Z를 입력하여 SIGTSTP을 보내면 적절히 핸들링이 되는 것을 확인할 수 있습니다. 

# ./a.out 
input Ctrl+Z
^Zthis program will stop in 3 seconds..

 

그렇다면 SIGSTOP은 어떨까요? 아래의 코드는 SIGSTOP을 핸들링하기 위한 코드입니다. 

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

void stopHandler(int sig){
        printf("this program will stop in 3 seconds..\n");
        sleep(3);
        exit(0);
}

int main(){
        signal(SIGSTOP, stopHandler);
        printf("wait\n");
        pause();
}

a.outkill을 통해서 SIGSTOP을 보내기 위해서 잠시 백그라운드로 돌립니다. /proc/92089/status에는 92089에 대한 프로세스 정보들이 나와있습니다. 현재 StateSpeeping, , 우리가 pause를 했기 때문에 이러한 상태가 되는 것입니다. Kill 명령으로 SIGSTOP(19)를 그 PID로 보내게 되면 어떻게 될까요?

 

# ./a.out &
[2] 92089
# wait <-- a.out 프로그램에 의한 출력
# cat /proc/92089/status | head -5
Name:   a.out
Umask:  0022
State:  S (sleeping)
Tgid:   92089
Ngid:   0
# kill -19 92089
# cat /proc/92089/status | head -5
Name:   a.out
Umask:  0022
State:  T (stopped)
Tgid:   92089
Ngid:   0

[2]+  Stopped                 ./a.out

시그널을 보내고 다시 상태를 확인해보면StateTstop 상태인 것을 확인할 수 있습니다. 여기서 알 수 있는 것은 SIGSTOP은 제어할 수 없다는 것을 보여줍니다.

위에서 SIGSTOP 설명을 보고 오세요. 제어할 수 없다는 설명이 있습니다. SIGKILL과 SIGSTOP은 사용자가 절대 제어할 수 없다는 점을 알고 있으세요~! 그 이유는 어떤 이유로 인해 프로세스를 무조건 죽여야하는 경우가 있습니다. 만약 좀비프로세스를 계속해서 생성하는 프로세스가 있는데, 이 프로세스를 죽이지 못하면 안되겠죠. 그래서 2개는 핸들링을 하지 못합니다.

또한 핸들러에 전달인자 sig는 시그널의 종류를 나타냅니다. 그렇기 때문에 시그널의 종류에 따라 처리할 수 있죠.

 

#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>

void signalHandler(int sig){
        if(sig==SIGINT){
                printf("this program will stop in 3 seconds..\n");
                sleep(3);
                exit(0);
        }
        if(sig==SIGQUIT){
                printf("signal SIGQUIT\n");
        }
}

int main(){
        signal(SIGINT, signalHandler);
        signal(SIGQUIT, signalHandler);
        printf("input Ctrl+C or Ctrl+\\ \n");
        while(1);
}

 

프로그램을 실행하여 Ctrl+\를 입력해서 SIGQUIT신호를 보내도 종료하지 않고, Ctrl+C를 입력하여 SIGINT를 보냈을 때 3초안에 종료합니다.

# ./a.out

input Ctrl+C or Ctrl+\

^\signal SIGQUIT

^\signal SIGQUIT

^\signal SIGQUIT

^\signal SIGQUIT

^Cthis program will stop in 3 seconds..

 

 

SW적인 Interrupt

중간에 멈추고 다른일을 하고 다시 돌아와 아까 했던일을 처리하는것이 인터럽트라고 하였습니다. 아래의 코드가 있습니다. SIGINT와 SIGQUIT을 처리하는 함수 2개가 있죠.

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

void sig_int(int signo){
        volatile int k=0;
        int i,j;
        printf("sig_int start\n");

        for(i=0;i<300;i++)
                for(j=0;j<600000;j++)
                        k+=i*j;

        printf("sig_int end\n");
}

void sig_quit(int signo){
        volatile int k=0;
        int i,j;
        printf("sig_quit start\n");

        for(i=0;i<300;i++)
                for(j=0;j<600000;j++)
                        k+=i*j;

        printf("sig_quit end\n");
}
int main(){
        signal(SIGINT, sig_int);
        signal(SIGQUIT, sig_quit);
        //pause는 신호가 생기고 처리될때까지 대기하는 함수
        pause();
        printf("process end\n");
}

저는 SIGINT와 SIGQUIT 순서대로 시그널을 줄겁니다. for문을 도는 이유는 SIGINT가 끝나기 전에 SIGQUIT을 수행하기 위해서 시간 지연을 하는 역할을 합니다. 그럴때 결과를 여러분들이 예측해보세요.

아래와 같은 결과일까요? SIGINT를 처리하고 나서 SIGQUIT을 처리하니까요. 

^Csig_int start
^\sig_quit start
sig_int end
sig_quit end
process end

 

땡! 아래의 결과처럼 나오게 됩니다. 

^Csig_int start
^\sig_quit start
sig_quit end
sig_int end
process end

 

SIGINT신호가 발생되어 신호 처리부인 sig_int 수행합니다. for문을 도는 도중에 SIGQUIT 신호가 발생해서 sig_quit을 수행하러 갑니다. 그럼 sig_int는 sig_quit가 처리가 다 되고 나면 나중에 멈췄던 부분부터 다시 수행한다는 겁니다. 이런 사실을 망각하게 되면 아주 어려운 버그를 만날수가 있지요. 

느린 시스템 콜에서의 시그널

여기서부터는 라이브러리 함수가 아닌 시스템 콜과 시그널에 대해서 이야기합니다. 그 중에서도 반환이 느린 시스템 콜에 대해서 이야기합니다. 만약 read하는 동안 signal이 발생했다면 어떻게 될까요?

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

void sig_int(int signo){
        printf("SIGINT 발생!\n");
}
int main(){
        char buf[32];
        int n;

        signal(SIGINT, sig_int);
        //읽는 중 SIGINT를 발생 시키면 오류? 아니면 성공?
        if((n = read(STDIN_FILENO, buf,32)) < 0){
                printf("read error\n");
        }else{
                buf[n]='\0';
                printf("%s\n",buf);
        }
}

 

위의 코드를 리눅스 상에서 실행을 시키고 난 후 아래와 같이 실행시켜보았습니다. 

# ./a.out
^CSIGINT 발생!
^CSIGINT 발생!
hello^CSIGINT 발생!
reakwon
reakwon

 

read를 하는 와중에 SIGINT를 발생시켰는데 오류가 발생하지 않고 처음부터 다시 read를 하는 것처럼 보이는데요. 이렇게 리눅스에서는 read와 같은 대기 시간이 길어가 수행시간이 긴 시스템 콜이 발생할때 자동으로 그 시스템 콜을 재시작하게 됩니다. 그러므로 hello는 출력이 되지 않고, reakwon만 출력이 되는거죠. 재시작한다는 것이 중요한 포인트입니다. 

read뿐만 아니라, 느린 호출에 해당되는 ioctl, read, readv, write, writev, wait, waitpid가 있습니다. 

이런 재시작 처리 방식은 시스템마다 다른데요. SVR2, SVR3, Solaris 등의 시스템은 자동 재시작하지 않고 errno를 EINTR로 설정한 후 반환시킵니다. 그래서 프로그래머는 아래와 같이 errno를 검사하며 프로그래밍해야됩니다. 하지만 우리는 리눅스를 만지는 중이니까 이런 처리를 할 필요는 없죠. 

retry :
        if ((n = read(fd, buf, BUF_SIZE)) < 0){
                if(errno == EINTR){
                        goto retry:
                }
        }else{
                printf("%s\n",buf);
        }

 

이처럼 리눅스 신호의 대한 개념과 시그널을 어떻게 잡아서 다룰 수 있는지에 대해서 알아보는 시간을 가졌습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

프로세스(process)

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

실행 중인 프로그램이라??

컴퓨터에서 말하는 실행 중인 프로그램이라는 건 저장공간에 있는 실행 파일이 메모리를 할당받아 명령어를 수행한다는 것을 의미합니다.

우리가 예를 들어 한글이라는 프로그램을 설치했다고 가정합시다. 아직은 더블클릭으로 프로그램을 실행하지 않았으므로 프로세스가 아닙니다. 설치 후 보고서를 작성하기 위해 프로그램을 실행한다면 그 순한 한글이라는 프로세스가 생성되는 것입니다.

프로세스에서 프로세스를 생성할 수도 있습니다. 이 경우 생성한 프로세스는 부모 프로세스, 생성당한 프로세스를 자식프로세스라고 합니다.

 

 

 

코드 영역(혹은 텍스트 영역이라고도 함) : 프로세스의 명령어가 존재합니다. CPU는 이 영역에서 명령을 실행합니다. 위 그림에서는 부모 자식 프로세스간 별개로 텍스트 영역이 존재하는 것처럼 보이지만 실제로 코드영역은 프로 자식 프로세스간 공유가 됩니다.

 

데이터 영역 : 전역 변수와 정적 변수가 존재합니다. 프로그램이 실행하는 동시에 할당되고 종료시 소멸됩니다.

 

힙 영역 : 동적할당에 필요한 메모리입니다. 프로그램 종료시 해제되지 않고 직접 해제해 주어야 합니다. C언어에서는 free를 사용해서 해제하게 됩니다. 

 

스택 영역 : 함수 호출 시 매개변수, 지역변수가 이 곳에 자리잡게 됩니다. 함수가 종료하게 되면 해제됩니다.

 

자식 프로세스를 생성하는 시스템 콜이 fork라고 합니다. fork는 자기 자신을 복사합니다만 결국 다른 별도의 메모리를 갖게 됩니다. 그래서 자식 프로세스에서 변수의 값을 바꾸어도 부모 프로세스에 영향을 주지 않게 되죠.

 

자식 프로세스 생성(fork)

프로세스를 하나 생성하면서 특징을 같이 이야기 해봅시다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int g_var = 1; 
char buf[] = "a write to stdout\n"; 

int main(){ 
        int l_var; 
        pid_t pid; 
        l_var = 10; 

        if(write(STDOUT_FILENO,buf,sizeof(buf)-1) != sizeof(buf)-1){ 
                printf("write error\n"); exit(0); 
        } 

        printf("fork start!\n"); 

        //fflush(stdout); 
        if((pid = fork())<0){ 
                printf("fork error\n"); 
                exit(0); 
        } else if(pid == 0){ 
                //child 
                g_var++; 
                l_var++; 
        } else { 
                //parent sleep(3); 

        } 

        printf("pid = %ld, g_var = %d, l_var = %d\n",
                        (long)getpid(),g_var,l_var); 

        exit(0); 
}
 
이제 컴파일 후 실행시켜보도록 하지요.
 # gcc fork.c 
 # ./a.out
 a write to stdout
 fork start!
 pid = 83929, g_var = 2, l_var = 11
 pid = 83928, g_var = 1, l_var = 10

 

위 코드를 설명하자면 fork를 통해서 자식 프로세스를 생성하게 됩니다. fork가 정상적으로 호출이되면 자식 프로세스는 0이 반환되고 부모 프로세스에게는 자식의 프로세스 id가 반환됩니다. 

자식 프로세스는 변수들의 값을 변경시키고 있습니다. 그 결과는 위와 같습니다.

 

위의 실행 결과를 파일로 저장해볼까요?

 # ./a.out > fork.out
 # cat fork.out
 a write to stdout
 fork start!
 pid = 83960, glob = 2, var = 11
 fork start!
 pid = 83959, glob = 1, var = 10

자세히보면 fork_start!라는 문자열이 두번 출력되고 있네요. 이같은 결과가 우리에게 시사하는 바는 자식 프로세스는 부모 프로세스의 버퍼까지 복사한다는 사실입니다.

printf는 내부적으로 write 시스템 콜을 사용합니다. printf는 write를 최소한으로 그리고 효율적으로 사용하기 위해 내부적으로 버퍼를 사용하고 있습니다.

터미널 콘솔에 출력할때는 l줄 단위 버퍼링 방식을 사용합니다. 이 말은 줄이 바뀌면(엔터) 화면에 출력한다는 것입니다. 이와 다르게 파일에 기록할때는 full 버퍼링 방식을 사용합니다. 이 방식은 버퍼가 전부 채워져야 파일에 기록하는 버퍼링 방식입니다.

위의 테스트 결과에서 내용을 파일(fork.out)에 저장하는 것이므로 full buffering 방식을 사용하며 이때 버퍼가 다 차지 않았고 이 후에 자식 프로세스를 생성했습니다.

자식 프로세스는 부포 프로세스의 버퍼까지 전부 복제하였고 프로그램이 종료가 될때 버퍼를 비우며 종료가 됩니다.

 

위의 fflush(stdout)을 주석해제하고 다시 실행해보시기 바랍니다. fork전에 버퍼를 다 방출하기 때문에 fork_start!라는 문자열이 한번만 출력됩니다.
 

고아 프로세스

프로세스를 생성하기만 하면 다가 아닙니다. 왜 그런지 실행과 결과를 보면서 이야기 하도록 하지요.

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

int main(){ 
        int pid; 

        if((pid=fork()) == 0){ 
                sleep(1); 
                printf("\t [child process %d] created from %d\n", getpid(),getppid());
                exit(0); 
        } 

        printf("[parent process %d] create %d process, ppid:%d\n", getpid(),pid,getppid()); 

}

getpid : 현재 프로세스의 pid를 반환합니다. 

getppid : 부모의 프로세스 id를 반환하지요.

 

이 코드의 결과를 예상해보세요.

자식 프로세스에서는 부모의 프로세스 ID를 출력하니까 부모의 getpid와 같을 겁니다. 그럴까요??

 

[parent process 21162] create 21163 process, ppid:20819

[child process 21163] created from 1

 

????

우리의 예상과는 다른데요??

분명 부모프로세스의 pid는 21162인데 자식 프로세스의 부모프로세스 pid가 쌩뚱맞은 1입니다.

 

왜 그럴까요??

 

우선 이 코드는 문제가 좀 있는 코드입니다. 부모프로세스가 먼저 종료되었기 때문입니다.  자식이 먼저 종료된다는 사실을 확보하기 위해 1초를 기다리는 이유가 바로 이 사실을 확인하기 위해서 입니다.  자식 프로세스는 부모 프로세스를 잃어 다른 부모를 찾게 됩니다. 바로 init 프로세스라고 하는데요. init프로세스의 pid가 바로 1입니다.

그러니까 모든 프로세스의 선조가 init프로세스라는 이야기죠.

이런 프로세스를 부모를 잃었다고 하여 고아프로세스가 됩니다.

 

그렇기 때문에 모든 부모님이 그렇듯 자식을 기다려주어야합니다. 그렇기 때문에 wait이라는 시스템 콜이 있지요.

 

코드를 수정해서 다시 확인합시다.

 

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

int main(){         
        int pid;         
        int status;         
        int terminatedPid;         

        if((pid=fork()) == 0){  
                printf("\t [child process %d] created from %d\n", 
                                getpid(),getppid()); 

                sleep(1);       
                exit(0);     
        }      
        printf("[parent process %d] create %d process, ppid:%d\n",  
                        getpid(),pid,getppid()); 
        terminatedPid = wait(&status);      
        printf("[parent process ] process wait process %d, status %d\n",  
                        terminatedPid,status); 
}

wait : wait의 첫번째 인자는 상태를 나타냅니다. 종료 상태를 받아올 수 있는 거죠. 반환값은 종료된 자식의 pid를 반환합니다.

 

[parent process 21803] create 21804 process, ppid:20819

         [child process 21804] created from 21803

[parent process ] process wait process 21804, status 0

 
이제 우리가 원하는 결과가 나왔군요. 자식프로세스를 기다려야합니다.

 

 

좀비프로세스

하지만 반대로 부모프로세스는 종료하지 않았지만 자식 프로세스가 먼저 종료하게 되면 어떨까요? 단, wait 호출을 하지 않고 말입니다.

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

int main(){   
        if(fork()==0){    
                printf("\t child process created and exit\n");  
                exit(0);   
        }      
     
        printf("parent process sleep\n"); 

        sleep(10);  
        sleep(10);
        printf("process exit\n"); 
}
이 후 컴파일하고 실행합니다.
# gcc process.c
# ./a.out &
[1] 22111
parent process sleep
child process created and exit
process exit

# ps -ef | grep 22111

...

root     22112 22111  0 10:26 pts/2    00:00:00 [a.out] <defunct>

...

 

 
부모 프로세스는 총 20초간 수행합니다. pid가 22111인 부모프로세스의 자식 프로세스 22112가 defunct인것이 보이세요? 바로 좀비 프로세스입니다.
 
부모프로세스는 자식 프로세스를 기다리고 있지 않아 종료상태를 얻지 못하고 있습니다. 그게 좀비 프로세스가 된 이유인데요. 이 종료 상태는 의외로 중요한데 예를 들어 쉘스크립트를 통해 명령을 실행할때 종료상태에 따라 분기를 하기 위해서 사용되기도 합니다.
 
부모 프로세스가 종료상태를 얻어와야 커널에서 자식 프로세스의 정보를 갖고 있는 구조체를 해제할 수 있습니다. 그 구조체가 task_struct라는 구조체입니다.
 
부모 프로세스는 커널이 관리하는 task_struct 구조체에서 멤버 pid와 exit_code를 통해서 종료상태를 얻어올 수 있습니다. 비록 좀비프로세스는 직접적으로 CPU, 메모리를 소모하지는 않지만 task_struct를 유지해야하기 때문에 메모리 낭비가 있게 되지요.
 
task_struct에서 pid와 exit_code, 무엇이 생각나세요?
wait의 반환값과 인자값입니다. 그 때문에 wait을 통해 해결할 수 있습니다.

 

코드를 다음과 같이 수정해보세요. 

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

int main(){     
        if(fork() == 0){     
                printf("\tchild process created and exit\n");  
                exit(0);   
        }       
      
        printf("parent process sleep\n");   
  
        wait(NULL); 

        sleep(10);
        sleep(10);
        printf("process exit\n"); 
}


부모 프로세스에서 바로 wait 콜을 호출합니다. 그러니까 종료상태를 알게 되죠. 결과도 그럴까요?

# gcc process.c

# ./a.out &

[1] 23839

parent process sleep

        child process created and exit





# ps -ef | grep 23839

root     23839 20819  0 11:31 pts/2    00:00:00 ./a.out

root     23842 20819  0 11:31 pts/2    00:00:00 grep --color=auto 23839

process exit

 

오.. 좀비프로세스가 없어졌어요. 

또는 시그널을 이용한 방법도 있습니다.

 

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

void childSignal(int sig){        
        printf("signal:%d\n",sig); 
        wait(NULL);
} 

int main(){     
        signal(SIGCHLD,childSignal);     
        if(fork() == 0){ 
                printf("\tchild process created and exit\n"); 
                exit(0);   
        }    
   
        printf("parent process sleep\n"); 
        
        sleep(10);  
        sleep(10);

        printf("process exit\n"); 
}

(시그널을 알아야 코드를 이해하는 데 도움이 됩니다.)

그리고 컴파일하고 실행해보도록 합시다. 그리고 재빠르게 ps 명령어를 쳐서 확인해보세요.

# gcc process.c

# ./a.out &

[1] 23451

parent process sleep

        child process created and exit

signal:17



# ps -ef | grep 23451

root     23451 20819  0 11:09 pts/2    00:00:00 ./a.out

root     23461 20819  0 11:10 pts/2    00:00:00 grep --color=auto 23451

process exit
 

이제 좀비프로세스가 없어졌군요!

왜 그럴까요??

우선 부모프로세스는 시그널을 등록합니다. 핸들러는 childSignal입니다.

자식 프로세스가 종료가 되면 시그널 번호 SIGCHLD로 부모프로세스에 시그널을 보냅니다. 

그렇다면 그 순간 시그널 핸들러가 동작하죠. 그렇다면 부모프로세스는 childSignal함수안에 wait을 호출합니다.

그러므로 종료상태를 받을 수 있게 되고 자식 프로세스는 좀비프로세스가 되지 않습니다.

아차, sleep함수는 일정 시간 동안 시그널을 기다립니다. 그 시간안에 시그널을 받는 다면 sleep은 동작을 멈추게 됩니다. 

왜 2번 sleep을 호출하는지 알겠죠? 바로 ps를 입력할 시간을 확보하기 위해서 입니다.

 

자식 프로세스의 종료 상태 알아오기

이번에는 wait에 인자를 이용하여 자식 프로세스의 종지 상태를 알아보도록 합시다. 종지상태를 조사하는 매크로는 표에 나와있습니다.

 

 매크로  설명
 WIFEXITED(status)   자식프로세스가 정상적으로 종료되었으면 true를 반환합니다. 이때 자식이 exit, _exit, _Exit으로 넘겨준 인수의 하위 8비트를 얻을 수도 있습니다.
그럴때 WEXITSTATUS(status)를 사용하세요.
 WIFSIGNALED(status) 만일 자식프로세스가 신호를 받아 비정상적으로 종료되었다면 참을 반환합니다. 
이때 신호번호를 알려면 WTERMSIG(status)를 이용하여 알 수 있습니다. 
WCOREDUMP(status) 매크로를 지원하는 시스템은 코어덤프를 생성했는지도 알 수 있습니다. 
 WIFSTOPPED(status)  자식 프로세스가 정지된 상태라면 참을 반환합니다. 이때 정지를 유발한 신호는 WSTOPSIG(status)를 통해 알아낼 수 있습니다. 
 WIFCONTINUED(status) 만일 자식이 작업 제어 정지 이후 재개되었으면 참을 반환합니다. 

 

 

다음은 위의 매크로를 사용한 예제입니다.

 

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

void pr_exit(int status){  
        if(WIFEXITED(status))      
                printf("normal termination, exit status = %d\n",WEXITSTATUS(status));    
        else if(WIFSIGNALED(status))      
                printf("abnormal termination, signal number =%d%s\n",
                                WTERMSIG(status),
#ifdef WCOREDUMP           
                                WCOREDUMP(status) ? " (core file generated)": ""); 
#else                
        ""); 
#endif          
        else if(WIFSTOPPED(status))     
                printf("child stopped, signal number = %d\n", 
                                WSTOPSIG(status)); 
}  

int main(void){     
        pid_t pid;     
        int status;    

        if((pid = fork()) < 0){       
                printf("[1] fork error\n");       
                exit(0);      
        } else if(pid == 0) {
                // 자식프로세스 곧장 7 반환하며 종료
                exit(7);       
        }
        //부모 프로세스에서 자식 상태 점검
        if(wait(&status) != pid){      
                printf("[1] wait error\n");       
                exit(0);       
        }     
    
        //자식의 종료 상태 출력
        pr_exit(status);       


        if((pid = fork()) < 0){     
                printf("[2] fork error\n");       
                exit(0);      
        } else if(pid == 0) {
                //자식 abort signal 발생하며 종료, 신호번호 6   
                abort();
        }        

        //부모 프로세스에서 자식 상태 점검
        if(wait(&status) != pid){       
                printf("[2] wait error\n");      
                exit(0); 
        }        
       
        //자식의 종료 상태 출력
        pr_exit(status);      

        return 0;
}

 

컴파일 후 실행해보면 자식 프로세스가 어떻게 종료가 되었는지 알 수 있습니다.

 # ./a.out
normal termination, exit status = 7
abnormal termination, signal number =6 (core file generated)

 

자식 프로세스가 exit(7)로 종료되었음을 부모 프로세스는 WEXITSTATUS매크로로 알 수 가 있네요.

이제 fork와 wait을 사용하는 방법을 아시겠나요?

하지만 wait에는 문제점이 존재하는데요. 그 문제점을 보완하는 함수 waitpid가 존재합니다. 

아래의 포스팅을 참고하세요.

https://reakwon.tistory.com/99

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

파일 정보 얻어오기

 

파일에 대한 정보를 얻어올때는 lstat시스템 콜을 사용하면 됩니다. 사실 stat, lstat, fstat등 여러가지 시스템 콜이 있는데요, 가장 간단하게 사용할 수 있는 시스템 콜이기 때문에 여기서는 lstat을 사용하겠습니다.

 

우선 lstat에 관한 시스템 콜 원형부터 보겠습니다.

int lstat(const char *filename, struct stat * buf);

lstat은 두개의 인자를 받게 되어있군요.

filename : 말 그대로 파일의 이름입니다. 하지만 경로를 명시하지 않을때는 현재 디렉토리를 기준으로 파일을 찾습니다. 절대경로나 상대경로를 줄 수 있다는 점은 알고 잇으세요.

buf : 파일의stat구조체를 저장합니다. stat??

무엇일까요. 구조체부터 보도록 하지요. vi 편집기를 사용한다면 lstat에서 3+K(shift)를 입력하면 시스템 콜을 볼 수 있습니다. 조금 더 밑으로 내려가다 보면 stat구조체를 만나볼 수 있게 되는데요. 아래와 같습니다.

struct stat

{

dev st_dev;    /* device */

ino_t st_ino;    /* inode */

mode_t st_mode;    /* protection */

nlink_t st_nlink;    /* number of hard links */

uid_t st_uid;    /* user ID of owner */

gid_t st_gid;    /* group ID of owner */

dev_t st_rdev;    /* device type (if inode device) */

off_t st_size;    /* total size, in bytes */

unsigned long st_blksize;    /* blocksize for filesystem I/O */

unsigned long st_blocks;    /* number of blocks allocated *.

time_t st_atime;    /* time of last access */

time_t st_mtime;    /* time of last modification */

time_t st_ctime;    /* time of last change */

}

음.. 파일에 대한 여러정보를 알 수 있군요. 

st_ino는 inode를 말하는 것이고 st_mode는 파일이 디렉토리인가, 단순 파일인가, 케릭터 디바이스인가를 알 수 있는 것이겠구요.

uid, gid도 알 수 있고 파일의 사이즈도 알 수 있습니다. 더군다나 파일이 언제 수정되고 접근되었는지에 대한 시간 정보도 알 수 있군요! 굿!!

 

그 중에서 ls명령어에 사용되는 st_mode는 어떤 파일의 타입인지 저장되어 있습니다. 어떤 파일 타입인지 알아보기 위해서 매크로 함수를 제공하는데요. 이것에 대해서 조금 더 알아보기 위해서 표로 준비해보았습니다.

 

모드  설명 
 S_ISLNK(m) 심볼릭 링크 파일인지 확인합니다. 
 S_ISREG(m) 단순 파일인지 확인합니다. 
 S_ISDIR(m)  디렉토리인지 확인합니다. 
 S_ISCHR(m)  문자 디바이스인지 확인합니다. 
 S_ISBLK(m)  블록 디바이스인지 확인합니다. 
 S_ISFIFO(m)  FIFO파일인지 확인합니다. 선입선출 구조의 파일인지 확인하는 것이죠. 
 S_ISSOCK(m)  네트워크 통신에 필요한 소켓파일인지 확인합니다. 

 

반환값 : 성공시 0이 반환되고 실패시 -1이 반환됩니다. 에러가 발생했다면 적절한 값으로 설정된다고 하는데 에러에 대해서는 구글형님께 물어봅시다.

 

구현

이제 대충 알아보았으니 코드를 직접 짜보도록하겠습니다. 우선 코드부터 보시죠. 

#include <stdio.h> 
#include <fcntl.h> 
#include <stdlib.h> 
#include <sys/stat.h> 
#include <sys/types.h> 
#include <unistd.h> 
#include <time.h> 
#define BUF_SIZE 1024     
void printType(const char *name,const struct stat *buf){
        if(S_ISDIR(buf->st_mode))   
                printf("%s is DIR \n",name);  
        else if(S_ISREG(buf->st_mode))  
                printf("%s is FILE\n", name);  
        else if(S_ISSOCK(buf->st_mode))    
                printf("%s is SOCKET\n",name);   
        else if(S_ISCHR(buf->st_mode))   
                printf("%s is CHARACTER DEVICE\n",name);    
        else if(S_ISFIFO(buf->st_mode))    
                printf("%s is FIFO\n",name);     
        else if(S_ISBLK(buf->st_mode))   
                printf("%s is BLOCK DEVICE\n",name);   
        else if(S_ISLNK(buf->st_mode))   
                printf("%s is LINK\n",name); 
}  

void printTime(struct stat *buf){       
        struct tm *mtime;      
        mtime=localtime(&buf->st_mtime);     
        printf("%04d-%02d-%02d %02d:%02d\n",
                        mtime->tm_year+1900, mtime->tm_mon+1,
                        mtime->tm_mday, mtime->tm_hour,  
                        mtime->tm_min); 
}   

void printFileSize(const char *name, struct stat *buf){      
        printf("%s size: %ld\n",name,buf->st_size); 
}  

int main(){      
        char filename_dir[128]="dir";     
        char filename_file[128]="aaa";   
        struct stat file;       
        struct stat dir;     

        lstat(filename_dir,&dir);     
        lstat(filename_file,&file);  

        printType(filename_dir,&dir);     
        printTime(&dir); 
        printFileSize(filename_dir,&dir);    
        printf("\n");

        printType(filename_file,&file);    
        printTime(&file);  
        printFileSize(filename_file, &file); 
}

 

 

printType의 함수는 파일이 어떤 종류의 파일인지 알아오는 함수입니다. 그러니까 이 파일이 정규파일인지, 디렉토리인지, 블록 디바이스 파일인지 등등 알아오는 함수지요. 매개변수는 stat구조체의 포인터를 매개변수로 받아야 알 수 있겠죠? 파일의 이름과 같아 받게 됩니다.
 
printTime은 파일이 언제 수정되었는지에 대해서 알아오는 함수입니다. 역시 stat구조체를 매개변수로 받습니다. 시간을 우리가 더 잘알아보기 위해서 localtime이라는 함수를 사용합니다. 그래서 tm구조체의 포인터를 반환하죠.
 
printFileSize는 파일의 크기를 알아오는 함수입니다. 역시 파일의 이름과 stat구조체를 매개변수로 받고 있습니다. 파일의 크기를 알아오는 변수는 st_size입니다.
 
이제 코드를 컴파일시키고 파일을 만들어 실험해봅시다.
# gcc lstat.c
# mkdir dir
# touch aaa

 

 
이제 파일의 크기를 조금 변경시켜보도록 하겠습니다. vi 편집기를 열어 aaa파일에 ABCDEF라는 문자열을 입력하고 저장합니다.
# vi aaa
ABCDEF
:wq
 
이제 컴파일된 파일을 실행시킵니다.
# ./a.out
dir is DIR
2018-11-19 00:06
dir size : 6

aaa is FILE
2018-11-19 00:46
aaa size : 7
 
그 후에 ls 명령어로 두 파일과 디렉토리를 보게되면 같은 정보를 확인할 수가 있습니다.
반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

파일입출력

 

리눅스 상에서 파일에 대한 입출력을 소개합니다. c언어에서의 라이브러리가 아닌 시스템 콜에는 open, read, write가 있는데요. 이것을 잘 포장하여 만든것이 fopen, fread, fwrite라는 함수랍니다. 이제 우리는 원시적인 시스템 콜을 가지고 파일에 대한 입출력을 하려고 합니다. 한가지 알아두셔야할것은 read, write는 버퍼링 없는 입출력 시스템 콜이라는 것입니다. 내부적으로 버퍼링을 하지 않기 때문에 read나 write시에 매번 커널 안의 시스템 호출이 실행이 됩니다. 반면 printf와 같은 라이브러리 함수는 버퍼링을 사용한 출력 함수입니다.

 

1. open

 

int open(const char *pathname, int flags, ... /* mode_t mode */ );

int openat(int fd, const char*pathname, int oflag, ... /* mode_t mode */ );

 

파일은 열 수 있는 두개의 시스템 콜이 있습니다. openopenat이 그것들입니다. 여기서는 open 함수만을 설명하도록 하겠습니다. 가장 오른쪽 인자 mode는 주석이 처리되어있고 주석 시작 앞에 ...을 볼 수 있는데요. ...은 가변인자를 의미하고 있을 수도 있고, 없을 수도 있다는 것을 의미합니다. 있다면 mode에 인수를 지정해야합니다.

pathname : 파일의 경로와 이름입니다. 절대경로의 파일명을 주어도 되고 상대경로의 파일명을 주어도 됩니다.

flags : 파일을 어떻게 열지를 결정합니다. 읽기 전용으로 열때는 O_RDONLY, 쓰기 전용으로 열때는 O_WRONLY, 읽기 쓰기로 열고 싶을때는 O_RDWR을 사용합니다. 이 밖에도 O_EXEC, O_SEARCH 등이 있습니다. 이 다섯개 중 하나는 반드시 지정돼야 있어야합니다. 아래의 표로 다시 정리해드리죠.

flag 설명
O_RDONLY 파일을 읽기 전용으로 엽니다.
O_WRONLY 파일을 쓰기 전용으로 엽니다.
O_RDWR 파일을 읽기, 쓰기 전용으로 엽니다.
O_EXEC 파일을 실행 전용으로 엽니다.
O_SEARCH 파일을 검색 전용으로 엽니다. 디렉토리만 해당합니다.

 

이 밖에도 자주쓰이는 flag들에 대해서는 아래를 참고하세요.

flag  설명 
O_CREAT   파일이 없으면 생성합니다. 이 옵션을 사용할때는 mode 인자를 추가로 지정해야합니다. 파일의 권한을 설정해야하기 때문이죠. 
O_EXCL  O_CREAT과 같이 사용되며 파일이 이미 존재하면 에러나 파일을 여는데에 실패합니다.
O_TRUNC  파일이 이미 존재한다면 이미 존재하는 파일의 내용을 무시하여 엽니다. 파일을 처음부터 쓴다는 flag입니다. 
 O_APPEND 파일에 내용을 추가할 수 있는 옵션입니다. 
 O_NONBLOCK
O_NDELAY
파일을 블록모드가 아닌 비블록모드로 엽니다. 기본적으로 파일은 블록모드입니다. 입력이 없을때는 입력할때까지 기다린다고 이해하시면 됩니다. 
O_DIRECTORY pathname이 디렉토리가 아니면 오류를 발생시킵니다.
O_CLOEXEC FD_CLOEXEC 파일 서술자 flag를 설정합니다. exec류 함수 호출시에 파일을 close한다는 flag입니다. 
O_NOFOLLOW pathname이 기호링크(symbolic link) 가리키면 오류를 발생시킵니다.

 

이 밖에도 여러 flag들이 쓰이는데, 여기서는 이정도만 설명하고 넘어가도록 하겠습니다. 그리고 여러개의 flag들을 사용하고 싶을때는 | (OR) 연산을 사용합니다. 예를 들어 읽기 쓰기 모드로 열고 싶은데 파일이 없으면 생성하는 flag는 O_RDWR| O_CREAT로 사용하면 됩니다.

 

mode : 파일을 생성할때의 권한을 주는 옵션입니다. O_CREAT과 같이 파일을 생성할때 사용합니다.

만약 0666을 주게 된다면 모두 읽기, 쓰기 권한을 가진 파일을 만들게 되는 것이죠. 현재 umask에 따라 생성 권한이 다르게 나타나겠지만, 정확히 확인하기 위해서는 umask(0)으로 umask를 해제한 이후에 O_CREAT을 통해 실제 생성한 파일의 권한을 확인할 수 있습니다. umask를 해제하면 file은 기본적으로 0666 권한으로 생성이 됩니다. 

또한 가독성을 높이기 위해 심볼릭 상수도 제공하고 있습니다. 심볼릭 상수를 사용할때 역시 | 연산으로 권한을 줄 수 있습니다.

심볼릭 상수는 <fcntl.h> 헤더파일에 존재하고 아래와 같습니다.

상수 설명
S_IRUSR 소유자 읽기 권한 설정
S_IWUSR 소유자 쓰기 권한 설정
S_IRGRP 소유자 그룹 읽기 권한 설정
S_IWGRP 소유자 그룹 쓰기 권한 설정
S_IROTH 일반 유저의 읽기 권한 설정
S_IWOTH 일반 유저의 쓰기 권한 설정

 

반환값 : 성공적으로 파일을 열게되면 파일 디스크립터를 반환합니다. 그렇지 않으면 음수를 반환합니다.

 

2. read

ssize_t read(int fd, void* buf, size_t len);

 

fd : 파일 디스크립터입니다. open은 정상적으로 파일을 열면 그 파일에 파일디스크립터를 반환하죠? 그 파일디스크립터를 써주면 됩니다. 하지만 표준 입력과 표준 출력, 표준 에러는 각각 순서대로 0, 1, 2가 되므로 표준 입력으로 읽어들일때는 0을 써주면 됩니다.

buf : 파일에서 읽어들일 버퍼를 말하고 있습니다. 어떤 자료형으로 읽어올지 모르므로 void*로 매개변수로 받습니다.

len : 얼마만큼 읽어올지를 결정합니다.

반환값 : 정상적으로 파일에 대한 내용을 읽어온다면 읽은 바이트수를 반홥합니다. 즉 len의 값을 반환하죠. 그렇지 않다면 0을 반환합니다.

 

3. write

파일에 내용을 기록합니다. 사용방법은 read와 거의 비슷합니다.

 

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

 

fd : 파일 디스크립터랍니다. read의 파일디스크립터를 받는 것처럼 사용합니다.

buf : 버퍼에 쓸 내용을 말합니다.

nbytes : 실제 버퍼에서 얼마만큼의 길이를 파일에 쓸것인지를 결정합니다.

반환값 : 올바로 write에 성공했다면 쓰여진 bytes수, 그렇지 않다면 -1을 반환합니다.

 

파일 쓰기, 읽기

이제 이것을 바탕으로 표준 입력으로 내용을 입력받아 파일에 내용을 기록할 겁니다. 그리고 난 후에는 쓴 파일에서 그 내용을 읽어 표준출력으로 출력할 겁니다.

여기에 그 코드가 있습니다.

#include <stdio.h> 
#include <fcntl.h> 
#include <stdlib.h> 
#define BUF_SIZE 1024  
int main(){ 
	int fd,n;         
	char buf[BUF_SIZE];       
	umask(0);       //0666의 권한을 확인하기 위해 umask해제
    //file.txt를 읽기,쓰기로 생성하고(기존에 있다면 내용무시), 0666 권한으로 엶
	fd=open("file.txt",O_CREAT|O_RDWR|O_TRUNC, 0666);     
	if(fd<0){                 
		printf("file open error\n");                 
		exit(1);         
	}         
	n=read(0,buf,BUF_SIZE);         
	n=write(fd,buf,n);         
	if(n<0){                 
		printf("file write error\n");                 
		exit(1);         
	}         
	
	lseek(fd,0,SEEK_SET);          
	n=read(fd,buf,BUF_SIZE);         
	if(n==0){                 
		printf("this file is  empty\n");                 
		exit(1);         
	}         
	n=write(1,buf,n);         
	if(n<0){                 
		printf("file write error\n");                 
		exit(1);         
	}         
	close(fd);  
}
 
fd=open("file.txt",O_CREAT|O_RDWR|O_TRUNC, 0666);

우선 파일을 열어야겠죠? open 시스템콜을 이용합니다. 파일명은 file.txt이고, 파일이 없으면 만들고 읽기쓰기 전용으로 엽니다. 그리고 파일의 내용이 있다면 그 내용을 잘라내고 씁니다. 

 

n=read(0,buf,BUF_SIZE); n=write(fd,buf,n);

파일을 여는데에 성공했다면 표준입력(키보드)로 데이터를 읽어옵니다.

 

그 이후 file.txt에 그 내용을 기록합니다.

 

lseek(fd,0,SEEK_SET);

기록을 했다면 파일 포인터는 파일의 맨끝을 가리키고 있겠네요. 그러니 lseek이란 함수를 통해서 파일 포인터의 위치를 가장 처음으로 위치시킵니다. 

lseek에 대해서 간단히 설명하자면 파일이 어디부터 읽고 쓰는지에 대한 위치를 변경하는 함수라고 기억하시면 됩니다. 

파일 디스크립터 fd를 갖고 처음(SEEK_SET), 현재 위치(SEEK_CUR), 끝 (SEEK_END)을 일단 정하고 난 후에 상대적인 값을 통해 파일 포인터의 위치를 결정합니다.

n = read(fd, buf, BUF_SIZE); 
if (n == 0) { 
	printf("this file is  empty\n");         
	exit(1); 
} 
n = write(1, buf, n);

이제 read를 통해서 file.txt에서 내용을 읽고 표준 출력으로 출력하는 프로그램입니다.

 

어때요? 이해 되셨나요?

 

이제 컴파일하고 실행시킵시다. 그리고 file.txt라는 파일에 기록이 됐는지 확인해봅시다.

 

[root@localhost ~] # gcc file.c

[root@localhost ~] # ./a.out

Hello, world!

Hello, world!

[root@localhost ~] # cat file.txt

Hello, world!

 

파일에도 기록이 잘 되어있고 출력도 잘 된것을 확인할 수 있습니다. 그리고 파일의 권한도 확인해볼까요?

# ls -al file.txt
-rw-rw-rw- 1 root root 12  4월  1 15:59 file.txt

저희가 생성했던 0666(-rw-rw-rw-) 권한으로 생성되었음을 확인할 수 있네요.

파일 다루는 방법 어렵지 않죠!?

반응형
블로그 이미지

REAKWON

와나진짜

,