아래와 같이 caculator라는 프로그램이 같은 디렉토리에 있는 lib내의 libcalcops라는 라이브러리를 사용할 수 있도록 만들어보도록 하겠습니다.

 

아주 간략하게 무슨 프로그램이냐를 설명해드리면, calculator라는 이 프로그램은 사용자로부터 두 수를 입력받아 덧셈, 뺄셈, 나눗셈의 결과를 출력해주는 프로그램입니다.

우리는 더 쉽게 calculator라는 프로그램을 구현하기 위해서(사실은 그냥 짜는게 더 쉽지만 라이브러리를 연동하는 방법을 알기 위해서) lib/libcalcops의 라이브러리를 가져다가 쓸겁니다. 물론 우리가 만들어 쓸거에요.

이 라이브러리에는 실제 연산 동작(calc operation)이 구현되어있습니다.

 

 

우선 현재 디렉토리에 mkdir lib를 이용해서 lib를 저장할 수 있는 디렉토리를 만들어줍시다.

 


# mkdir lib
# cd lib

 

라이브러리 소스코드 작성

너무나 간단한 연산 동작을 하는 함수를 정의한 헤더파일과 구현한 c파일을 작성하고 컴파일 해줍니다.

 

calc_operations.h

#ifndef __CALC_OPERATION_H_
#define __CALC_OPERATION_H_
int add(int a,int b);
int sub(int a,int b);
int mult(int a,int b);
double div(int a,int b);
#endif

 

calc_operations.c

#include "calc_operations.h"

int add(int a,int b){
    return a+b;
}

int sub(int a,int b){
    return a-b;
}

int mult(int a,int b){
    return a*b;
}

double div(int a,int b){
    return (double)a/(double)b;
}

 

여기까지는 뭐 누구나 아는 프로그램이지요. 아래와 같이 컴파일하게 되면 object파일이 하나 생성되겠죠.


# gcc -c calc_operations.c
# ls
calc_operations.c  calc_operations.h  calc_operations.o 

 

이제 이것을 ar로 rc옵션을 주어 정적라이브러리 아카이브를 만듭니다. 


# ar rc libcalcops.a calc_operations.o
# ls
calc_operations.c  calc_operations.h  calc_operations.o  libcalcops.a

 

프로그램 구현

이제 lib밖으로(cd ..) 이동해주세요. 여기서 소스 코드를 구현하도록 하겠습니다.

 

calculator.c

#include <stdio.h>
#include "lib/calc_operations.h"

int main(){
        int a,b;
        scanf("%d %d",&a,&b);

        printf("%d + %d = %d\n", a,b,add(a,b));
        printf("%d - %d = %d\n", a,b,sub(a,b));
        printf("%d * %d = %d\n", a,b,mult(a,b));
        printf("%d / %d = %f\n", a,b,div(a,b));

}

 

이제 아래와 같이 컴파일하도록 해봅시다. 아래 옵션 중에서 -L은 라이브러리가 위치한 디렉토리를 명시해주고 -lcalcops는 우리가 작성한 lib파일 이름입니다. 우리가 lib파일 이름을 libcalcops.a라고 정의하였을때 옵션은 -lcalcops라고 명시해주면 됩니다. lib가 -l로 바뀌고 확장자 .a를 빼주는 식이죠.


# gcc calculator.c -L ./lib -lcalcops
# ./a.out
7 3
7 + 3 = 10
7 - 3 = 4
7 * 3 = 21
7 / 3 = 2.333333

기존의 .o파일과 .a를 지우고 나서도 calculator 프로그램이 동작할까요? 지우고 실행을 다시 해보도록 합시다.


# rm -rf *.a *.o
# ls
calc_operations.c  calc_operations.h
# cd ..
# # ./calculator
10 3
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3.333333

보시다시피 잘 동작하는 것을 알 수 있습니다. 추측해볼 수 있는 것은 런타임과는 관계가 없고 컴파일시(calculator)에 프로그램에 적재되는 것을 알 수 있고 실제로도 그렇습니다.

 

 

 

공유라이브러리

공유라이브러리는 컴파일 방식만 변경하면 됩니다. lib 디렉토리로 이동해주세요. 

아래의 명령으로 라이브러리를 컴파일해줍니다. 그런후에 /usr/lib 디렉토리에 복사해줍니다.


 # gcc -shared -o libcalcops.so calc_operations.c
 # cp libcalcops.so /usr/lib

 

이제 calculator 프로그램을 컴파일 할것인데, 위의 컴파일 옵션보다 더 간단합니다.


# gcc calculator.c -lcalcops
# ./a.out
8 3
8 + 3 = 11
8 - 3 = 5
8 * 3 = 24
8 / 3 = 2.666667

 

그렇다면 아까와 같이 /usr/lib/libcalcops.so파일을 삭제하고 위 프로그램을 다시 실행해보도록 해봅시다.


# ./a.out
./a.out: error while loading shared libraries: libcalcops.so: cannot open shared object file: No such file or directory

 

정적라이브러리와는 다르게 라이브러리 파일이 삭제되면 실행 프로그램도 실행되지 않네요. 우리가 추측해볼 수 있는것은 프로그램이 시작시에 적재되는 것을 의심해볼 수 있고 실제로도 그렇습니다.

 

왜 하필 /usr/lib에 파일을 복사했을까요? 공유라이브러리할때 /etc/ld.so.conf 설정파일로부터 라이브러리 파일이 있는 지 봅니다. 

제 시스템을 기준으로 설명드리면 아래와 같이 /etc/ld.so.conf 파일이 작성되어 있습니다.


include /etc/ld.so.conf.d/*.conf

 

다시 /etc/ld.so.conf.d/ 디렉토리를 보면 libc.conf와 x86_64-linux-gnu.conf 파일 두개가 존재합니다. 하기의 경로에 lib파일이 있으면 실행되며 추가적으로 경로를 추가 후 저장하고 나와서 ldconfig 명령을 주어 반영하면 됩니다.

libc.conf x86_64-linux-gnu.conf
# libc default configuration
/usr/local/lib
/usr/lib
# Multiarch support
/usr/local/lib/x86_64-linux-gnu
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu

 

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

리눅스 데몬(Daemon)

백그라운드에서 실행되는 일종의 프로세스이며 오랫동안 유지되는 프로세스입니다. 주로 시스템이 시작될때 같이 시작이 되서 시스템이 종료될때 같이 종료되는 경우가 대부분입니다. 백그라운드에서 돌면서 사용자가 원하는 서비스를 제공하는 프로세스를 의미합니다. 데몬은 일반 백그라운드 프로세스와는 다르게 제어 터미널이 없고 표준 입출력이 없습니다. 즉, 사용자로부터 직접적인 입력를 받지 않고 출력도 하지 않습니다.

데몬의 예는 뭐가 있을까요? 관례상 데몬은 프로세스 이름이 d로 끝납니다. 예를 들어 네트워크 인터페이스들을 감시하고 서버로부터 요청을 감지하는 inetd, 설정된 날짜와 시간이 되면 지정된 명령을 실행시켜주는 cron, 보안 원격 로그인을 제공하는 sshd와 같은 것들이 있지요. 이러한 데몬을 생각해보세요. 사용자가 입력하지도, 출력을 내보내지도 않습니다.

ps명령어를 통해서 어떤 데몬이 있는지 보도록 해봅시다.

   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
      0       1       1       1 ?             -1 Ss       0   1:42 /sbin/init splash
      0       2       0       0 ?             -1 S        0   0:00 [kthreadd]
      2       3       0       0 ?             -1 I<       0   0:00 [rcu_gp]
      2       4       0       0 ?             -1 I<       0   0:00 [rcu_par_gp]
      2       6       0       0 ?             -1 I<       0   0:00 [kworker/0:0H-kblockd]
      2       9       0       0 ?             -1 I<       0   0:00 [mm_percpu_wq]
      2      10       0       0 ?             -1 S        0   0:17 [ksoftirqd/0]
      2      11       0       0 ?             -1 I        0   0:51 [rcu_sched]
      2      12       0       0 ?             -1 S        0   0:01 [migration/0]
      2      13       0       0 ?             -1 S        0   0:00 [idle_inject/0]
      2      14       0       0 ?             -1 S        0   0:00 [cpuhp/0]
      2      15       0       0 ?             -1 S        0   0:00 [kdevtmpfs]
      2      16       0       0 ?             -1 I<       0   0:00 [netns]
      2      17       0       0 ?             -1 S        0   0:00 [rcu_tasks_kthre]
      2      18       0       0 ?             -1 S        0   0:00 [kauditd]
      2      19       0       0 ?             -1 S        0   0:00 [khungtaskd]
      2      20       0       0 ?             -1 S        0   0:00 [oom_reaper]
      2      21       0       0 ?             -1 I<       0   0:00 [writeback]
      2      22       0       0 ?             -1 S        0   0:00 [kcompactd0]
      2      23       0       0 ?             -1 SN       0   0:00 [ksmd]
      2      24       0       0 ?             -1 SN       0   0:00 [khugepaged]
      2      70       0       0 ?             -1 I<       0   0:00 [kintegrityd]
      2      71       0       0 ?             -1 I<       0   0:00 [kblockd]
      2      72       0       0 ?             -1 I<       0   0:00 [blkcg_punt_bio]
      2      73       0       0 ?             -1 I<       0   0:00 [tpm_dev_wq]
      2      74       0       0 ?             -1 I<       0   0:00 [ata_sff]
      2      75       0       0 ?             -1 I<       0   0:00 [md]
      2      76       0       0 ?             -1 I<       0   0:00 [edac-poller]
      2      77       0       0 ?             -1 I<       0   0:00 [devfreq_wq]
      2      78       0       0 ?             -1 S        0   0:00 [watchdogd]
      2      83       0       0 ?             -1 S        0   0:01 [kswapd0]
      2      84       0       0 ?             -1 S        0   0:00 [ecryptfs-kthrea]
      2      86       0       0 ?             -1 I<       0   0:00 [kthrotld]
      2      87       0       0 ?             -1 I<       0   0:00 [acpi_thermal_pm]
      2      88       0       0 ?             -1 S        0   0:00 [scsi_eh_0]
      2      89       0       0 ?             -1 I<       0   0:00 [scsi_tmf_0]
      2      90       0       0 ?             -1 S        0   0:00 [scsi_eh_1]
      2      91       0       0 ?             -1 I<       0   0:00 [scsi_tmf_1]
      2      93       0       0 ?             -1 I<       0   0:00 [vfio-irqfd-clea]
      2      95       0       0 ?             -1 I<       0   0:00 [ipv6_addrconf]
      2     104       0       0 ?             -1 I<       0   0:00 [kstrp]
      2     107       0       0 ?             -1 I<       0   0:00 [kworker/u3:0]
      2     120       0       0 ?             -1 I<       0   0:00 [charger_manager]
      2     121       0       0 ?             -1 I<       0   0:37 [kworker/0:1H-kblockd]
      2     166       0       0 ?             -1 S        0   0:00 [scsi_eh_2]
      2     167       0       0 ?             -1 I<       0   0:00 [scsi_tmf_2]
      2     187       0       0 ?             -1 S        0   0:15 [jbd2/sda5-8]
      2     188       0       0 ?             -1 I<       0   0:00 [ext4-rsv-conver]
      1     227     227     227 ?             -1 S<s      0   0:11 /lib/systemd/systemd-journald
      1     252     252     252 ?             -1 Ss       0   0:26 /lib/systemd/systemd-udevd

 

데몬 코딩 룰

1. 데몬이 파일을 생성할때 적절한 권한이 있어야하는데 umask를 사용하여 파일 모드 생성 마스크를 설정합니다. 보통 0입니다. umask는 생성되는 파일의 생성모드를 정해주는데 umask가 현재 0x022라면 group의 w, other의 w 권한을 주지 않는 다는 것입니다. 따라서 파일을 rwxrwxrwx로 생성한다 하더라도 rwxr-xr-x로 생성이 된다는 겁니다. 

데몬이 파일을 올바르게 생성해야만 하므로 umask(0)을 지정합니다.

2. 자식 프로세스를 생성(fork)한 후 부모 프로세스는 종료(exit)합니다. 이렇게 자식 프로세스보다 부모 프로세스가 먼저 종료되면 initd이 자식 프로세스를 넘겨받게 됩니다. 자식 프로세스는 세션의 리더가 아니며 프로세스 그룹 리더도 아닙니다.

3. setsid를 호출해서 새로운 세션을 생성합니다. setsid는 다음과 같은 세가지 일이 일어납니다.


#include <unistd.h>
pid_t setsid(void);

호출 프로세스가 프로세스 그룹 리더가 아니면 새 세션을 생성합니다.
1. 호출 프로세스가 새 세션의 리더가 됩니다. 
2. 호출 프로세스는 새 프로세스 그룹의 리더가 됩니다. 
3. 호출 프로세스는 제어 터미널이 없습니다.

4. 현재 작업 디렉토리를 루트 디렉토리로 변경합니다. 

5. 필요하지 않은 모든 파일 디스크립터를 닫습니다. 어떻게 열려진 파일 디스크립터를 모두 알 수 있을까요? getrlimit이나 open_max를 사용하면 됩니다.

6. 표준 입출력을 모두 /dev/null로 지정합니다. 데몬은 사용자로부터 입력을 받을 필요가 없고, 출력, 에러를 직접 하지 않습니다. 데몬에는 제어 터미널 장치가 연관되어 있지 않고 출력이 표시될 대상이나 사용자로부터 입력을 받을 통로가 없습니다.

데몬 코딩

위의 코딩 룰 대로 코딩한 데몬 프로그램입니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <syslog.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

void daemonize(const char *cmd){
        int i,fd0,fd1,fd2;
        pid_t pid;
        struct rlimit rl;
        struct sigaction sa;

        //umask를 0으로
        umask(0);

        if(getrlimit(RLIMIT_NOFILE,&rl)<0){
                printf("%s: can't get file limit\n",cmd);
                exit(0);
        }

        //자식 프로세스 생성 후 부모는 종료
        if((pid=fork())<0){
                printf("%s: can't fork\n",cmd);
                exit(0);
        }else if(pid !=0)
                exit(0);

        //세션을 만들고 리더가 됨.
        setsid();

        //SIGHUP 무시, 제어터미널 연결이 끊겨도 무시
        sa.sa_handler=SIG_IGN;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags=0;
        if(sigaction(SIGHUP,&sa,NULL)<0){
                printf("%s: can't ignore SIGHUP\n",cmd);
                exit(0);
        }
        if((pid=fork())<0){
                printf("%s:can't fork\n",cmd);
                exit(0);
        }
        else if(pid!=0)
                exit(0);


        // 작업 디렉토리를 /로 변경
        if(chdir("/") < 0){
                printf("%s: can't change directory to /\n",cmd);
                exit(0);
        }

        //열려있는 파일 디스크립터 닫기
        if(rl.rlim_max == RLIM_INFINITY)
                rl.rlim_max = 1024;
        for(i=0;i<rl.rlim_max;i++)
                        close(i);

        //모두 닫았으니 fd는 0부터 return되며 모두 /dev/null로 직행
        fd0=open("/dev/null",O_RDWR);
        fd1=dup(0);
        fd2=dup(0);

        openlog(cmd,LOG_CONS,LOG_DAEMON);
        if(fd0 != 0 || fd1 != 1 || fd2 != 2){
                syslog(LOG_LOCAL0," unexpected fd : %d %d %d",fd0,fd1,fd2);
                exit(1);
        }

        syslog(LOG_LOCAL0,"helloworld! [%s]start",cmd);
}


int main(int argc,char* argv[]){
        daemonize(argv[0]);

        //100초후 데몬 종료
        sleep(100);
}

 

이후에 compile하고 실행시켜봅시다. 


# gcc daemon.c -o dd
# ./dd

아무 변화가 없죠. 이제 ps -axj로 실행시켜보세요.


# ps -efj
...
      1  105975  105974  105974 ?             -1 S        0   0:01 ./dd
 104369  105977  105977  103273 pts/2     105977 R+       0   0:00 ps -axj
...

 

./dd라는 프로세스가 보이네요. 만약 정상적인 프로세스라면 부모 pid도 같이 보여야합니다. 찾아볼까요?


# ps -axj | grep 105974 
      1  105980  105974  105974 ?             -1 S        0   0:01 ./dd

없음을 알 수 있네요. 부모프로세스는 105974인데 먼저 종료가 되었죠. 그렇기 때문에 initd가 그 프로세스를 짬처리 당했고 자식 프로세스는 setsid로 새로운 세션의 리더가 됩니다. 

이러한 사실들을 보면 daemon이 올바르게 초기화되었다는 것을 알 수 있습니다.

 

로그 기록

데몬은 입출력 통로가 없기 때문에 사용자에게 특정한 사실을 알리기 위해서는 파일에 기록하는 식으로 알리게 됩니다.  이때 만약 데몬마다 로그파일이 다르다면 시스템 관리자 입장에서는 어떤 로그파일이 무슨 데몬의 로그인지 확인하기가 까다롭게 됩니다.

그래서 로그를 중앙 집중적으로 관리하게 되고 이때 이 로그를 관장하는 데몬이 syslogd입니다. syslogd를 사용하기 위해서는 우선 syslog가 설치가 되어있어야겠죠. 

이후에는 /etc/syslog.conf 파일 맨 아래에 다음과 같은 설정을 합니다. 

local0.*        /var/log/dd_info

이제 변경사항을 반영하기 위해서 syslogd를 재시작합니다.

# /etc/init.d/syslog restart

 

이제 우리의 데몬을 실행시켜보도록 합시다. 이제 /var/log/dd_info에 우리의 로그가 나오는것을 확인할 수 있을 것입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

다중입출력 

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

이때 우리는 쓰레드를 생각해볼 수 있겠지요. 나쁘지 않은 아이디어입니다. 그렇다면 파일 3개의 대해서 3개의 쓰레드를 돌려야하겠네요.

여러분도 알다시피 쓰레드는 많이듭니다. context switching이 대표적이죠. 그리고 파일 처리에 대해서 동기화 기법을 적용해야할 수도 있습니다. 상당히 복잡하고 어려운 구현이 될 수도 있습니다.

이제 우리는 스레드를 이용하는 방법말고 보다 경량적인 방법을 선택할 것인데 이러한 구현을 도와주는 system call이 바로 select, poll, epoll 입니다. 이 포스팅에서는 select에 대해서만 설명합니다.

1. select

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct tiemval *timeout)

- ndfs : 감시할 파일 디스크립터의 갯수인데 마지막 파일 디스크립터 번호에 +1을 지정해줍니다. fd_set은 배열인데 배열의 index는 0부터 시작하므로 +1을 해줍니다. 아래에서 설명하도록 하죠.

- readfds : 읽을 데이터가 있는지 감시하는 파일의 집합입니다.

- writefds : 파일에 데이터를 쓸 수 있는지를 검사하기 위한 파일 집합입니다.

- exceptfds : 파일의 예외 사항이 있는지 검사하기 위한 파일 집합입니다.

- timeout : 데이터의 변화를 감지하기 위해서 사용하는 time out 값입니다. 예를 들어 readfds에 지정된 시간동안 읽을 데이터가 없다면 select는 0을 반환합니다. 만약 timeout을 NULL로 지정하게 되면 무기한으로 대기하게 됩니다.

 

성공시 fd의 갯수를 반환합니다.

fd_set

fd_set은 아래와 같은 비트 배열입니다. 총 1024개의 파일을 감시할 수 있죠. 아래는 readfds를 표현했습니다. fd_set은 총 1024개의 fd를 감시할 수 있습니다. 

우리는 readfds에 0, 1, 2, 3을 read할 데이터가 있는지 감시한다고 해봅시다. 이때 fd 3에 read할 데이터가 있다면 위의 배열은 아래와 같이 fd 3에 해당하는 비트 배열의 flag가 1이 세팅됩니다. 이렇게 하여 fd 3에 읽을 데이터가 있는지 알 수 있습니다.

이때 fd 3은 마지막에 열었던 파일 디스크립터입니다. 그렇다면 지금까지 감시해야할 fd의 갯수는 몇개인지 나오지요? 마지막 fd의 번호 + 1이 됩니다. 그 때문에 nfds가 마지막 fd의 +1이 되는 것이죠.

 

그래요, 이제 fd를 감시하는 것은 알겠는데, 이것이 스레드처럼 동시에 fd를 관리할까요? 결론은 아닙니다. 만약 파일을 1024개를 관리하고 1023번 파일디스크립터(fd)에 read 플래그가 1이 되었다면 select는 모든 fd_set을 모두 검사하여 변화가 있는 fd를 알아내게 됩니다.

 

 

 

 

 

 

 

 

FD 매크로

보다 파일 디스크립터 집합(fd_set)을 관리하기 편하게 하기 위해서 리눅스에서는 다음과 같이 4개의 매크로를 지원합니다.

 

매크로 설명
void FD_CLR(int fd, fd_set *set) set에서 fd를 제거합니다. 더 이상 검사하지 않습니다.
int  FD_ISSET(int fd, fd_set *set) set에 fd의 플래그가 setting되었는지 확인합니다. 
void FD_SET(int fd, fd_set *set) set에 fd를 추가합니다. 이것은 flag를 setting하는 것이 아닌 검사를 하겠다는 뜻의 setting입니다.
void FD_ZERO(fd_set *set); set에 있는 fd를 전부 제거합니다.

 

이 select를 활용하여 파일 3개로부터 입력이 들어오면 출력해주는 일종의 서버 프로그램을 작성해보도록 합시다. 여기서 나오는 select와 poll은 정규파일에서 다루는 것은 적절하지 않습니다. 이러한 함수들은 정규 파일을 감시하게 되면 바로 준비되었다고 판단하여 return 되어 버리기 때문입니다. 정규 파일은 언제나 디스크에서 읽을 준비가 되어있습니다. 그래서 디스크 파일보다는 네트워크 입출력에서 다루는 것이 적절합니다.

여기서는 네트워크라는 개념은 배제하고 오직 상큼하게 select의 개념을 살펴보고 어떻게 다뤄보는지 느낌만 가져가 봅시다. 

예제

//multiIO_select.c 

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

#define FD_SIZE 3
#define BUF_SIZE 8

int main(int argc, char *argv[]){
        int i, n, ret, fd_count, end_count = 0;
        int fds[FD_SIZE];
        char buf[BUF_SIZE] = {0,};
        struct timeval tv;

        fd_set retfds, readfds;

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

        //readfds를 공집합으로 만든다. 
        FD_ZERO(&readfds);

        for(i = 0; i < FD_SIZE; i++){
                fds[i] = open(argv[i+1],O_RDONLY);
                if(fds[i] >= 0){
                        //readfds 집합에 파일디스크립터 fds[i]를 추가한다. 
                        FD_SET(fds[i], &readfds);
                        //맨 마지막 파일열린 파일 디스크립터가 가장 큰 값을 갖는다.
                        fd_count = fds[i];
                }
        }

        while(1){

                //readfds를 retfds로 옮기는 이유는 select를 통해서 readfds가 바뀔 수 있다.
                retfds = readfds;

                //열린 파일 디스크립터의 최대값 +1
                ret = select(fd_count + 1, &retfds, NULL, NULL, NULL);

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

                for(i = 0; i < FD_SIZE; i++){
                        //만약 fds[i]에 대해서 읽을 준비가 된 파일 디스크립터이면 
                        if(FD_ISSET(fds[i], &retfds)){
                                //읽고 출력해준다. 
                                while((n = read(fds[i], buf, BUF_SIZE)) > 0){

                                        //quit가 들어오면 fds[i] read 종료
                                        if(!strcmp(buf,"quit\n")){
                                                //readfds에서 지우고 
                                                FD_CLR(fds[i], &readfds);
                                                //파일디스크립터 close
                                                close(fds[i]);

                                                end_count++;
                                                if(end_count == FD_SIZE) 
                                                        exit(0);
                                                continue;
                                        }

                                        printf("fd[%d] - %s",fds[i], buf);
                                        memset(buf, 0x00, BUF_SIZE);
                                }
                        }
                }

        }
}

위의 코드는 아주 간단합니다.  3개의 파일에 대해서 읽을 준비가 된 파일 디스크립터가 있으면 해당 파일을 읽어서 출력해주는 프로그램입니다.

FD_ZERO를 이용해 readfds를 초기화 시킨 후 readfds에 감시할 파일 디스크립터를 추가합니다. 그것이 FD_SET입니다. readfds의 복사본인 retfds select 시스템콜을 통해서 감시를 시작합니다. 만약 복사본을 넣지 않고 쌩으로 readfds를 넣는다면 select함수는 readfds를 단순히 조회만 하지 않고 변경합니다. 이러한 변경은 불필요합니다. FD_ISSET을 통해서 현재 준비된 파일디스크립터를 처리한 후에 다시 변경되지 않은 readfds로 돌아가야합니다.

만약 retfds flag on이 되면 FD_ISSET true를 반환하게 됩니다. 그러면 파일에 업데이트 된 내용을 출력해주게 됩니다결론적으로 위 소스 코드는 파일을 읽었다고 끝내지 않고 계속 파일에 쓰인 내용을 출력해주는 프로그램입니다.

이를 시험해보기 위해서는 다른 프로그램이 필요합니다.

//fwrite.c

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

int main(int argc, char *argv[]){
        int fd, ret;
        char buffer[32];

        if(argc != 2){
                printf("usage : %s file \n", argv[0]);
                exit(0);
        }
        //파일을 쓰기 전용으로 연다.
        fd = open(argv[1], O_WRONLY|O_APPEND);

        if(fd < 0){
                perror("file erorr ");
                exit(0);
        }

        while(1){

                //한줄 처리를 위해서 fgets를 사용
                fgets(buffer, sizeof(buffer), stdin);

                //파일에 기록 
                ret = write(fd, buffer, strlen(buffer));

                if(ret < 0){
                        perror("write error ");
                        exit(0);
                }
                //quit문자열은 종료
                if(!strcmp(buffer, "quit\n")) break;
        }

        close(fd);
        return 0;
}

 

이 실행 파일은 단순히 argv[1]로 들어오는 파일명에 기록하는 프로그램입니다. 이제 두 소스 코드를 컴파일하고 쓰일 실행 파일을 생성합니다.

# gcc multiIO_select.c 
# gcc fwrite.c -o fwrite
# touch a b c

 

이제 터미널 총 4개를 띄어서 테스트해볼건데, 하나는 select함수를 사용하는 프로그램, 다른 3개 터미널에는 fwrite를 수행하는 프로그램으로 select를 실험합니다. 단, 같은 디렉토리에 있어야합니다.

./a.out a b c
# ./a.out a b c
fd[3] - writing fd[3] - a...
fd[4] - writing fd[4] - b...
fd[5] - writing fd[5] - c...
fd[3] - hello
fd[4] - world
fd[5] – bye
#
./fwrite a ./fwrite b ./fwrite c
# ./fwrite a
writing a...
hello
quit
#
# ./fwrite b
writing b...
world
quit
# 
# ./fwrite c
writing c...
bye
quit
#

위와 같이 출력이 더럽게 나오는 것은 BUF_SIZE가 작아서 그렇습니다. BUF_SIZE를 늘리게 되면 위와 같은 지저분한 출력을 예방할 수는 있습니다. 

MultiIO_select.c에서는 select를 이용해서 여러 파일 디스크립터의 내용들을 입력받아서 출력해주고 있습니다. 설명드렸듯이 select는 정규파일에 대해서 항상 준비되어있기 때문에 곧 장  return되어 버립니다. 티는 나지 않지만 multiIO_select.c의 select함수 호출 이후 printf로 아무 로그나 찍으면 엄청나게 루프를 도는 것을 확인할 수 있습니다. 여기서는 이해를 돕기 위해서 만든 프로그램일 뿐입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

재지정(Redirection)

리눅스에서 프로그램은 보통 세 개의 파일 서술사를 열게 됩니다. 바로 표준 입력(standard input, STDIN), 표준 출력(standard output, STDOUT), 그리고 표준 에러(standard error, STDERR)가 되지요. 

순서대로 파일 디스크립터(fild descriptor)는 0, 1, 2입니다.

 

리다이렉션(Redirection)이라고 하는 것은 이러한 서술자 중 하나 이상을 파일로 다시 지정하는 수단이라고 볼 수 있는데요. 어떻게 리다이렉션을 사용할까요?

 

1) program > file_name

>는 이해하기 쉽습니다. program에서 출력하는 것을 file_name이라는 파일에 기록하겠다는 것입니다. 더 잘 이해하기 위해서 저의 리눅스를 활용해보도록 하지요.

제 리눅스의 /tmp 디렉터리에는 다음과 같은 파일들이 있습니다.

무슨 파일들인지는 잘 모르겠네요.


[root@localhost tmp]# ls
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-ModemManager.service-OOhWCc
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-bolt.service-KuASd9
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-chronyd.service-mPGGCi
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-colord.service-uTlHDZ
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-fwupd.service-mV4Wkl
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-rtkit-daemon.service-eaiBmp

ls 명령을 치는 순간 화면에 보이는 파일들의 목록이 있죠? 이것은 표준 출력으로 표시한 것입니다. 쉽게 이해하기 위해서 모니터가 표준 출력이라고 보세요.

 

이제는 이것을 파일로 저장해보도록 합시다. 위의 꺽쇠를 이용하면 됩니다.


[root@localhost tmp]# ls > list_stdout
[root@localhost tmp]# cat list_stdout
list_stdout
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-ModemManager.service-OOhWCc
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-bolt.service-KuASd9
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-chronyd.service-mPGGCi
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-colord.service-uTlHDZ
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-fwupd.service-mV4Wkl
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-rtkit-daemon.service-eaiBmp
tracker-extract-files.0

그러면 list_stdout이 생겨날텐데 이것을 cat명령어로 실행해보면 파일의 내용을 볼 수 있습니다. 위의 ls 명령어의 결과와 같죠?

 

그렇다면 다음의 명령은 어떻게 출력이 될까요? 분명 위의 ls에는 aaa.txt라는 파일은 없었습니다. 과연 파일로 저장될까요? 


[root@localhost tmp]# ls -l aaa.txt > list_aaa
ls: cannot access 'aaa.txt': 그런 파일이나 디렉터리가 없습니다
[root@localhost tmp]# cat list_aaa

list_aaa가 생겨나지 않았고 메시지가 출력이 되네요. 여기서 위의 메시지는 stderr로 출력된 에러 메시지입니다. stderr로 출력된 에러 메시지는 list_aaa의 파일로 저장이 되지 않네요. 

 

그 이유는 기본적으로 ls -l aaa.txt > list_aaa는 ls -l aaa.txt 1> list_aaa와 같이 동작합니다. 여기서 숫자 1은 표줄 출력(stdout)을 의미하게 됩니다. 1이 파일 디스크립터라면 우리는 표준 에러는 다음과 같이 저장할 수 있겠군요. 

 

아래의 결과를 보면 2(표준에러)가 리다이렉션되어 파일의 내용으로 전달되는 것을 알 수 있습니다.

 


[root@localhost tmp]# ls -l aaa.txt 2> list_aaa
[root@localhost tmp]# cat list_aaa
ls: cannot access 'aaa.txt': 그런 파일이나 디렉터리가 없습니다

 

2) program >> file_name

꺽쇠가 두개 연달아 있는 것은 파일의 끝에 내용을 덧붙이라는 것으로 여러분이 직접 실험해보시기 바랍니다.

 

 

 

 

3) program < file_name

여기서부터는 조금 헷갈릴 수 있으니 잘 들으세요. 아니, 보세요. file_name의 파일 내용을 program의 표준 입력으로 사용하겠다 라는 의미가 됩니다. 

 

ls는 표준입력을 받지 않는다.

자, 프로그램에는 표준 입력으로 사용자의 입력을 받는 것이 있고 그렇지 않는 프로그램이 있습니다. 여기서 ls는 사용자의 입력을 표준입력으로 받을까요? 결론은 아닌데요. 

아래의 내용으로 확인해보겠습니다.

우선 아까 없던 aaa.txt를 만들어볼텐데 aaa.txt에는 aaa라는 문자를 기록하도록 하지요. 그리고 다시 echo를 사용해 bbb라는 내용을 갖는 aaa파일을 만들어봅시다.


[root@localhost tmp]# echo aaa > aaa.txt
[root@localhost tmp]# ls aaa.txt
aaa.txt
[root@localhost tmp]# echo bbb > aaa
[root@localhost tmp]# ls aaa
aaa

(echo는 그 다음의 입력을 표준 출력으로 출력하는데 아까 배운 >을 통해서 파일로 저장했습니다. )

 

이제 ls에 aaa.txt의 내용을 표준입력으로 받아보도록 하겠습니다. 


[root@localhost tmp]# ls < aaa.txt
aaa
aaa.txt
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-ModemManager.service-OOhWCc
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-bolt.service-KuASd9
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-chronyd.service-mPGGCi
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-colord.service-uTlHDZ
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-fwupd.service-mV4Wkl
systemd-private-b2dd06a5829f42f5b5f2e122f17b9afb-rtkit-daemon.service-eaiBmp
tracker-extract-files.0

뭐 얼핏보면 뭔가 출력이 됐는데 우리가 원하는 결과는 aaa.txt의 내용은 bbb가 ls의 입력으로 들어가서 ls aaa라는 명령어가 수행되어야하는 것입니다. 근데 제가 전달하는 입력을 씹고 현재 디렉터리의 파일 내용을 출력하고 있죠. 이것은 ls의 기본 동작입니다. 

ls는 이 처럼 표준 입력을 받지 않습니다. 대신 argv라고하는 argument vector를 통해서 사용자 입력을 받고 있는 셈입니다.

 

표준입력을 받는 명령어는 무엇이 있을까요? 

대표적으로 cat, grep 등이 있습니다. 

 

cat의 기본 동작

cat의 기본동작(아무런 옵션을 주지 않을때의 동작)은 입력받은 거 그대로 출력하는 기능입니다. 그 입력은 표준입력으로 받습니다. 

표준 입력으로 받으니 이것을 파일의 내용을 입력으로 줘보겠습니다.


[root@localhost tmp]# echo Hello > hello.txt
[root@localhost tmp]# cat < hello.txt
Hello

 

cat명령어는 argv로 파일 이름을 인자로 받을 수 있도록 프로그램이 되어있는데, 결과는 위와 같습니다.

 

 

 

 

grep의 기본 동작

grep은 기본적으로 바로 뒤에 인자가 포함된 문자열이 입력이 되면 그 문자열을 다시 출력해줍니다. 아래의 결과를 보면 알 수 있는데 이 grep은 표준 입력으로 입력을 받습니다.


[root@localhost tmp]# grep reak
AAA
BBB
rea
rreak
rreak
rreakk
rreakk
mmsmsms

자, 여기서 눈 여겨 봐야할 것은 grep은 굵은 글씨인 rreak, rreakk만 출력하였습니다. 왜냐면 인자로 받은 reak라는 문자열이 포함되있기 때문이죠.

 

표준 입력으로 받으니 우리는 파일을 입력으로 리다이렉션을 쓸 수 있겠군요. 위의 입력했던 내용을 그대로 vi 편집기로 저장합니다. 


[root@localhost tmp]# vi grep_test
[root@localhost tmp]# grep reak < grep_test
rreak
rreakk


vi 편집기 내용


AAA 
BBB 
rea 
rreak
rreakk
mmsmsms 

 

표준 입력으로 받은 결과와 동일함을 알 수 있네요.

 

리다이렉션을 통한 Copy

리다이렉션을 통해서 파일을 복사할 수도 있는데요. 아래를 보도록 합시다.  echo로 file을 생성한 후에 cat의 입력으로 file의 내용을 받습니다. 그 후에 cat은 표준 출력으로 file의 내용을 출력하는데 다시 >로 표준 출력의 내용을 file_copied라는 파일에 저장합니다. 결국은 복사를 수행한 것과 같게 됩니다.


[root@localhost tmp]# echo hello > file
[root@localhost tmp]# cat < file > file_copied
[root@localhost tmp]# cat file_copied
hello

 

 

 

 

 

파이프( | )

파이프라는 것은 program1의 표준 출력을 program2의 표준입력으로 입력받습니다. 명령어의 결과를 다시 어떤 명령어의 입력으로 받아야할때 사용되며 리눅스에서 매우 흔히 쓰입니다. 형식은 아래와 같습니다.

program1 | program2

파이프라는 것은 program1의 표준 출력을 program2의 표준입력으로 입력받습니다. 리눅스에서 매우 흔히 쓰이는데요. 가장 빈번하게 사용하는 grep과 연관시켜서 사용해봅시다. 

우선 제 리눅스에서 /bin으로 이동했습니다. 그리고 ls -l 명령을 보면 무수히 많은 파일들이 보이는데 그 중에서 zip이라는 문자열을 포함한 것을 확인해보고 싶습니다. 

그렇다면 ls -l로 우선 표준 출력을 하고 이 표준 출력된 내용을 zip이라는 인자를 전달받는 grep에게 표준 입력으로  전달시키면 되겠죠?

답은 간단합니다. 아래의 명령을 실행시켜보세요.


[root@localhost bin]# ls -l | grep zip
lrwxrwxrwx. 1 root root           5  5월 10  2019 bunzip2 -> bzip2
lrwxrwxrwx. 1 root root           5  5월 10  2019 bzcat -> bzip2
-rwxr-xr-x. 1 root root       38472  5월 10  2019 bzip2
-rwxr-xr-x. 1 root root       17560  5월 10  2019 bzip2recover
-rwxr-xr-x. 1 root root       40256  5월 10  2019 funzip
-rwxr-xr-x. 1 root root        3447  5월 10  2019 gpg-zip
-rwxr-xr-x. 1 root root        2345 11월  8  2019 gunzip
-rwxr-xr-x. 1 root root      170544 11월  8  2019 gzip
lrwxrwxrwx. 1 root root           6  5월 11  2019 mzip -> mtools
-rwxr-xr-x. 1 root root        5656  5월 13  2019 preunzip
-rwxr-xr-x. 1 root root        5656  5월 13  2019 prezip
-rwxr-xr-x. 1 root root       13352  5월 13  2019 prezip-bin
-rwxr-xr-x. 2 root root      210408  5월 10  2019 unzip
-rwxr-xr-x. 1 root root      105064  5월 10  2019 unzipsfx
-rwxr-xr-x. 1 root root      234496  5월 11  2019 zip
-rwxr-xr-x. 1 root root      105376  5월 11  2019 zipcloak
-rwxr-xr-x. 1 root root        2953 10월 10  2008 zipgrep
-rwxr-xr-x. 2 root root      210408  5월 10  2019 zipinfo
-rwxr-xr-x. 1 root root      100104  5월 11  2019 zipnote
-rwxr-xr-x. 1 root root      100112  5월 11  2019 zipsplit

굳이 그림으로 표현하면 아래와 같이 실행됩니다.

 

이렇게 리다이렉션과 파이프에 대해서 알아보았습니다.

궁금하신 점은 댓글로 달아주세요.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

fcntl

파일을 열었는데 그 속성을 바꾸려면 어떻게 할까요? fcntl함수는 이미 열린 파일의 속성들을 변경할 수 있습니다.

#include <unistd.h> 
#include <fcntl.h> 
int fcntl(int fd, int cmd, ... /* arg */ );

 

fd : 파일의 속성을 조회하거나 변경할 파일 디스크립터입니다. 

cmd : 커맨드입니다. cmd 종류는 아래와 같습니다.

cmd 설명
F_GETFL fd에 대한 파일 상태 속성들을 반환값으로 돌려줍니다. O_RDONLY, O_WRONLY, O_RDWR, O_EXEC, O_SEARCH는 개별적으로 판정할 수 없고 ACCMODE라는 마스크를 이용하여 알아낼 수 있습니다. 이는 아래의 예제를 통해서 알아보도록 하지요.
F_SETFL 파일 상태 속성들을 세번째 인수로 받아 설정합니다. O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC, O_FSYNC, O_ASYNC 속성만 변경할 수 있습니다. 읽기, 쓰기 관련 플래그(O_WRONLY, O_RDONLY, O_RDWR)은 조회할 수는 있지만 변경할 수 없음을 유의하세요.
F_GETOWN 현재 SIGIO, SIGURG 신호를 받도록 설정된 프로세스 ID 혹은 프로세스 그룹 ID를 돌려줍니다. 
비동기 입출력과 관련이 있습니다.
F_SETOWN SIGIO와 SIGURG신호를 받도록 시정된 프로세스 ID나 프로세스 그룹 ID를 설정합니다.
F_DUPFD 파일 서술자 fd를 복제합니다. 새 파일 서술자를 함수의 반환값으로 돌려줍니다. 이때 반환된 새 서술자는 FD_CLOEXEC속성이 해제된 상태입니다. 그렇기 때문에 exec류의 함수로 다른 명령을 실행했을때 열린 상태로 남아있습니다.
F_DUPFD_CLOEXEC 파일 서술자를 복제하고 새 파일 서술자에 관련된 FD_CLOEXEC를 설정합니다. 역시 새 파일 서술자가 반환됩니다.
F_GETFD fd에 대한 파일 서술자 플래그들을 반환합니다. 정의되어 있는 파일 서술자 flag는 FD_CLOEXEC입니다.
F_SETFD 서술자 플래그들을 설정합니다. 셋째 인자에 그 플래그를 전달합니다.

위 함수는 cmd에 따라 반환값이 다르고 실패시 -1을 반환하게 됩니다.

 

속성 조회, 변경 예제

아래는 파일의 속성을 조회해보고 O_APPEND 속성을 추가하여 다시 조회하는 예제입니다. 

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>

void printAccessMode(int flags){

        switch(flags & O_ACCMODE){
        case O_RDONLY:
                        printf("read only\n");
                        break;

        case O_WRONLY:
                        printf("write only\n");
                        break;
        case O_RDWR:
                        printf("read write\n");
                        break;
        }

        printf("append %s \n", (flags & O_APPEND) ? "O":"X");
        printf("nonblocking %s \n", (flags & O_NONBLOCK) ? "O":"X");
        printf("\n");
}

int main(){
        int fd = open("test.txt", O_RDONLY|O_CREAT, 0666);

        int flags = fcntl(fd, F_GETFL,0);
        printAccessMode(flags);

        flags |= O_APPEND;

        fcntl(fd, F_SETFL, flags);

        flags = fcntl(fd, F_GETFL, 0);
        printAccessMode(flags);

}

 

주의해야할 점은 파일 서술자 플래그, 파일 상태 플래그를 수정할때는 반드시 먼저 플래그를 얻어오고(GET) 얻어온 값을 통해 변경(SET)해야합니다. 그렇지 않고 바로 SET하면 이전에 설정했던 값은 지워질 수 있습니다. 

 

결과

# ./a.out
read only
append X
nonblocking X

read only
append O
nonblocking X

 

Non-blocking 예제

표준 입력(fd 0)으로 입력을 받는데, 이것을 non-blocking으로 하고 싶을때 fcntl을 통해서 구현할 수 있습니다. 바로 O_NONBLOCK을 통해서 실현할 수 있죠.

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>


int main(){
        char buf[128] = {0,};
        int fd = 0;
        int flags = fcntl(fd, F_GETFL,0);

        flags |= O_NONBLOCK;
        //fcntl(fd, F_SETFL, flags);

        read(fd, buf, sizeof(buf));
        printf("buf : %s \n", buf);
}

 

위의 코드를 그대로 실행하면 우리가 알고 있듯, 입력을 받을때까지 프로그램은 대기하게 됩니다. 사용자가 입력을 하게 되면 그때 입력 내용을 출력하고 프로그램이 끝이 납니다.

# ./a.out
Hello!!
buf : Hello!!

 

이제 주석을 해제하면 사용자 입력을 기다리지 않고 바로 버퍼를 출력하고 끝납니다. 

# ./a.out
buf :

 

이것을 조금만 응용하면 사용자가 입력할 시간을 주고 제한 시간내에 입력이 없으면 그에 따른 로직을 도는 프로그램을 짤 수도 있을 겁니다.

 

CLOEXEC 예제

어떤 파일을 열어서 fd를 얻었고 exec류의 함수로 다른 프로그램을 실행하려고 합니다. 이때 다른 프로그램은 이미 열려진 fd를 사용하지 않음에도 fd를 그대로 가져가게 되죠. 다음의 코드가 그런 현상을 보여줍니다. 

 

파일을 오픈한 후 execl함수로 loop이라는 프로그램을 실행합니다.

#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main(){
        int fd = open("test.txt",O_RDONLY|O_CREAT,0666);
        int flags = fcntl(fd, F_GETFD, 0);
        flags |= FD_CLOEXEC;

        //fcntl(fd, F_SETFD, flags);

        execl("/root/examples/file_test/loop","./loop",0);
}

 

loop프로그램은 다음의 코드로 만들어졌습니다. pid출력하고 무한루프를 돕니다. 별거없죠?

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

int main(){
        printf("loop start pid:%d\n",getpid());
        while(1);
}

 

두개를 컴파일하고 무한 루프 프로그램말고 execl을 호출하는 프로그램을 실행해보도록 하지요.

# ./a.out &
[2] 6808
# loop start pid:6808

 

 

이제 해당 pid의 fd를 보시기 바랍니다. 그러면 loop 프로그램에서 열린 fd를 볼 수 있는데, 맨 아래의 test.txt도 열려있음을 확인할 수 있습니다.

# ls -l /proc/6808/fd
합계 0
lrwx------. 1 root root 64  6월 27 18:54 0 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:54 1 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:54 2 -> /dev/pts/1
lr-x------. 1 root root 64  6월 27 18:54 3 -> /root/examples/file_test/test.txt

 

이 fd를 exec시에 닫고 싶다면 위의 주석을 해제하고 다시 실행해보세요.

#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main(){
        int fd = open("test.txt",O_RDONLY|O_CREAT,0666);
        int flags = fcntl(fd, F_GETFD, 0);
        flags |= FD_CLOEXEC;

        fcntl(fd, F_SETFD, flags);

        execl("/root/examples/file_test/loop","./loop",0);
}

 

그러면 아래와 같이 test.txt가 loop 프로세스에서 닫혀져있음을 알 수 있습니다.

# ./a.out &
[3] 6893
# loop start pid:6893

#
# ls -l /proc/6893/fd
합계 0
lrwx------. 1 root root 64  6월 27 18:57 0 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:57 1 -> /dev/pts/1
lrwx------. 1 root root 64  6월 27 18:57 2 -> /dev/pts/1

 

지금까지 fcntl 설명과 fcntl을 이용해서 파일의 속성을 바꾸는 예제를 보았습니다. 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

디스크 여유 공간과 디스크 사용량을 보기 위해서 리눅스에서는 df, du가 있습니다.

df(Disk Free)

시스템 전체에 마운트 된 디스크의 여유 공간을 출력해줍니다.

파일시스템, 디스크 크기, 사용된 용량, 사용가능한 용량, 사용된 용량의 비율, 마운트된 지점을 순서대로 출력해줍니다.
아무 옵션없이 이렇게 보면 알기가 좀 까다롭죠?

-h Human readable로 사람이 읽기 좋게 출력해줍니다.

-k 위 옵션은 키로바이트 단위로 출력해줍니다.

-i 사용가능한 또는 사용중인 inode에 대한 정보를 출력해줍니다.

 

du(Disk Usage)

디스크 사용량을 알기위한 명령어인데, 뒤 인자에 경로를 붙이지 않는다면 현재 디렉토리에서부터 각 디렉토리의 디스크 사용량을 출력합니다.

 

-h df에서와 같이 사람이 읽기좋게 출력해줍니다.

 

-s 디렉토리를 표시하지 않고 단지 사용량만 알고 싶다면 Summary의 약자인 s옵션을 쓰면 됩니다. 읽기 좋게 h와 같이 써보세요.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

IPSec(IP Security)

IP계층(네트워크 계층)을 안전하게 보호하기 위해서 IPSec이라는 보호 기법을 사용합니다. TLS와 같은 안전한 통신 기법만 있으면 되지, 왜 굳이 IPSec이 필요하느냐에 대한 물음을 갖을 수 있습니다. TLS는 TCP 프로토콜만을 보호합니다. 전송 계층에는 TCP뿐만 아니라 UDP도 있지요. TCP뿐만 아니라 UDP, 혹은 더 상위 계층까지 보호하기 위한다면 더 낮은 계층에서 보호하는 것이 효과적이겠죠. 그래서 IP계층에서 데이터를 보호하는 것입니다. 대부분의 네트워크 응용프로그램은 IP 계층을 사용하기도 하니까 IP계층에서 동작하는 보안, 즉, 페킷에 대한 보안을 제공하는 IP Security(IPSec)이 필요합니다.

 

IP Sec은 그림에서 보는바와 같이 IP계층을 집중적으로 보호합니다.

 

두가지 모드

IPSec에는 두 가지 모드가 있는데, IP의 내용(payload)만을 보호하느냐, 아니면 헤더까지 모두 보호하느냐에 따라서 전자는 전송 모드(Transport Mode), 후자는 터널 모드(Tunnel Model)라고 합니다.

 

전송 모드(Transport Mode)

전송모드는 전송 계층와 네트워크 계층 사이에 전달되는 payload를 보호합니다. 중간에 IPSec 계층이 있기 때문에 IPSec헤더가 붙고, 이후에 네트워크 계층에서는 이것이 모두 상위층에서 보낸 데이터(payload)로 취급이 되므로 IP 헤더가 붙고 아래 계층으로 전달되지요. 

 

 

 

전송모드는 host-to-host( end-to-end)간 데이터 보호가 필요할때 사용이 됩니다. 아래는 전송모드의 데이터 전송 흐름을 보여줍니다.

 

왼쪽 컴퓨터(host)는 IPSec을 적용하여 데이터를 보냅니다. 네트워크를 통해서 오른쪽 컴퓨터로 데이터가 도착합니다. 자, 이 사이에서 다른 사람이 데이터를 가져가도 IPSec에 대한 보호가 이루어져있으므로 볼 수 없고 라우터를 거쳐 두 당사자만 데이터를 보호할 수 있지요. 그래서 종단 간의 보호(End-To-End Protection, E2EP)가 이루어 질 수 있습니다. 

 

 

 

터널 모드(Tunnel Mode)

터널 모드는 IPSec이 IP 헤더를 포함한 IP 계층의 모든 것을 보호합니다. IP 헤더까지 완전히 보호하고 IPSec의 헤더를 추가하였으니 기존의 IP헤더를 볼 수 없기 때문에 새로운 IP 헤더가 추가됩니다. 

 

이 IPSec 헤더와 새로운 헤더는 누가 추가해줄까요? 바로 호스트, 종단이 아닌 그 중간자가 추가해줍니다. 보통 라우터가 되지요. 

 

 

아래는 그 흐름을 보여주는데, 전송모드와는 다르게 호스트 A는 별다른 IPSec의 조취를 취하지 않습니다. 하지만 Router A에서 IPSec을 적용하고 새로운 IP 헤더를 추가합니다. 이 헤더에는 목적지 라우터의 주소가 있어서 Router B로 보냅니다. Router B는 이후에 적절한 조취를 취하고 새로운 IP헤더와 IPSec헤더를 제거한 후 Host B에게 전달합니다. 마치 RouterA, RouterB가 터널 역할을 하는 것 같네요.

 

터널 모드는 보통 두개의 라우터간, 호스트와 라우터간, 라우터와 호스트간에 사용이 되는데, 즉, 송수신자 양쪽 모두가 호스트가 아닌 경우에 사용됩니다.

 

 

두 가지 프로토콜

IPSec은 또 두가지 보안 프로토콜을 제공하는데, 인증에 대해서만 검사하는 인증헤더 프로토콜(AH: Authentication Header Protocol)과 페이로드 전체를 보호하여 기밀성을 제공하는 보안 페이로드 캡슐화(ESP: Encapsulating Security Payload)가 그것들입니다.

 

AH(Authentication Header)

발신지 호스트를 인증하고 IP패킷의 무결성을 보장합니다. 인증을 위해서 해쉬함수와 대칭키가 사용되어 Message Digest를 생성하고 헤더에 삽입합니다. AH는 인증과 무결성을 보장하지만 비밀은 보장해주지 않습니다.

 

Next Header : IPSec 다음에 오는 페이로드의 헤더를 말합니다. TCP인지 UDP인지 또는 ICMP인지 의미합니다.

Payload Length : 인증헤더의 길이를 말하며 4바이트 배수가 됩니다. 

Security Parameter Index : 32비트 보안 매개변수 색인(SPI) 필드는 Security Association에 대한 식별자입니다.

Sequence Number : 32비트 순서번호인데 이것은 replay attack을 방지합니다.

Authentication Data: 헤더를 포함하여 전체 페킷에 대한 데이터를 인증 데이터로 만드는데, 이때 IP 헤더의 변경될 수 있는 데이터는 제외하고 인증데이터를 만들게 됩니다. 예를 들어 TTL같은 변경이 될 수 있는 데이터는 인증 데이터를 만들때 포함하지 않습니다. 만들면 AH의 Authentication Data필드에 삽입됩니다.

 

ESP(Encapsulating Security Payload)

AH가 데이터의 기밀성을 보장할 수 없지만 ESP는 기밀성을 보장할 수 있습니다. 또한 AH가 보장하는 IP패킷의 무결성 등 AH가 제공하는 서비스를 모두 보장할 수 있습니다.

 

ESP 헤더의 각각 필드는 32비트입니다. 

눈에 익은 필드들이 몇개 보이지요? 대부분은 AH의 필드와 유사합니다. 또 payload를 암호화하고 있네요.

 

Authentication Data : AH와는 다르게 인증데이터가 IP헤더를 포함하지 않습니다. ESP헤더까지만 인증데이터로 만들고 ESP Trailer에 붙이게 됩니다.

 

AH와 ESP의 대한 차이는 다음의 표로 간략하게 정리하였습니다.

Services AH ESP
Access Control O O
Message Authentication
(Message Integrity)
O O
Confidentiality X O
Replay Attack Protection O O
Entity Authentication
(Data Source Authentication)
O O

 

접근제어가 보장된다는 것을 알기 위해서는 SAD(Security Association Database), SP(Security Parameters) 등의 용어에 대해서 이해해야하는데, 이것은 다음 포스팅에서 설명하도록 하겠습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

getopt로 명령인자 분리

 

ls -il hello.txt

위와 같은 명령을 구현하고자 할때 명령어에 대한 옵션 분리가 필요합니다. -i 옵션과 -l 옵션이 있고, 위의 명령어에서 hello.txt라는 파일 이름을 줄 수도 있고 안줄 수도 있습니다. 

getopt를 모른다면 이 명령 옵션 인자를 스스로 파싱해서 써야할건데 자신있으신가요? 저는 자신이 없어서 getopt를 설명하고자 합니다.

 

getopt를 사용하기 위해서는 unistd.h 헤더파일을 추가해야합니다.


#include
<unistd.h>

int getopt(int argc, char* const argv[], const char *optstring);

extern char *optarg;
extern int optind, opterr, optopt;

getopt는 명령어를 argv에 있는 명령어를 계속적으로 parsing하는데, 정상적으로 파싱이 되면 optstring에서 지정한 문자열이 반환되고 파싱이 전부되었다면 최종적으로 -1을 반환합니다. 

 

어려운거는 별로 없으니 바로 예제를 보도록 하지요.

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

void showHelp(const char* thisName){
        printf("usage : %s [OPTION] [MESSAGE]\n"
                        " -h            help\n"
                        " -w [MESSAGE]  write file\n"
                    " -r                read file\n"
                        , thisName);
}

void writeFile(const char* message){
        int fd = open("getopt_file.txt",O_RDWR|O_CREAT,0777);
        write(fd, message, strlen(message));
        close(fd);
}

void readFile(){
        char buf[32]={0,};
        int n;
        int fd = open("getopt_file.txt",O_RDWR|O_CREAT,0777);
        while((n = read(fd, buf, sizeof(buf))) > 0)
                printf("%s",buf);

        close(fd);
}
int main(int argc, char* argv[]){
                char opt;
                opterr = 0;

                if(argc < 2){
                        showHelp(argv[0]);
                        exit(0);
                }

                while((opt = getopt(argc, argv, "hw:r")) != -1){
                        printf("opt:%c, optopt:%d\n", opt, optopt);
                        switch(opt){
                        case 'h':
                                showHelp(argv[0]);
                                break;
                        case 'w':
                                writeFile(optarg);
                                break;

                        case 'r':
                                readFile();
                                break;

                        }
                }
}

 

h는 help 옵션, w는 파일에 write하는 명령 옵션, r은 파일을 읽는데 사용하는 명령 옵션입니다. 

다른건 이해가 가실 것이고 optstring을 보면 w옆에 :이 있네요. w는 다음에는 MESSAGE라는 문자열이 오게 되지요. 이처럼 명령어 옵션 이후에 인자가 필요할때 그 문자 다음에 :을 사용하는데요. 명령 다음 인자는 optarg에 저장이 됩니다.

 

 

컴파일 후 실행하면서 어떻게 실행되는지 보겠습니다.


# gcc getopt_test.c -o getopt_test
# ./getopt_test
usage : ./getopt_test [OPTION] [MESSAGE]
 -h             help
 -w [MESSAGE]   write file
 -r             read file
# ./getopt_test -h
opt:h, optopt:0
usage : ./getopt_test [OPTION] [MESSAGE]
 -h             help
 -w [MESSAGE]   write file
 -r             read file
# ./getopt_test -w "hello"
opt:w, optopt:0
# ./getopt_test -r
opt:r, optopt:0
hello

만약 hwr이외에 옵션을 주면 어떻게 될까요?

 


# ./getopt_test -k
opt:?, optopt:107

알 수 없는 옵션을 주게 되거나 옵션 이후 인자가 필요한데 주지 않으면 ?을 반환하는 것을 알 수 있습니다.

 

이제 optopt, opterr을 보도록 하지요.

optopt는 getopt가 제대로 실행이 되었다면 0이 되는데, 그렇지 않을 경우 그 문자가 들어갑니다.

opterr은 에러 발생시 출력할지 말지를 정합니다. opterr을 1로 해놓으면 getopt에서 에러메시지를 출력하는데 0을 지정하면 출력하지 않습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

SSL(Secure Socket Layer)

SSL은 1994년 netscape사의 웹 브라우저 보안 프로토콜로 처음 고안되어 1996년까지 버전 3.0까지 발표되었는데, 3.0을 표준화한 것이 TLS라고 합니다. 통상적으로 SSL과 TLS는 같은 의미로 사용됩니다.

 

SSL은 왜 생겨났을까요? 

아마 암호화를 배우신 분이라면 대칭키 암호, 비대칭키 암호를 배우셨을텐데, 대칭키는 빠른 대신 서로간의 공통적으로 알고 있는 키를 공유해야합니다. 대칭적이죠? 그래서 대칭키 암호입니다. 비대칭키는 비대칭적으로 서로 다른 키를 갖고 있어 암복호화를 합니다. 

대칭키암호와 비대칭키 암호에 관한 설명은 지난 포스팅을 참고해주세요.

 

우리는 대칭키를 공유할때 외부로의 노출이 발생하게 됩니다. 이 키를 중간에서 가로채게 된다면? 망하는거지요. 

그렇다면 대칭키는 우리들 머릿속에서 지워버리고 비대칭키로 암호화 통신을 하면 되겠군요. 가능할 수는 있지만 너무 느리고 비효율적입니다. 

그렇다면 처음 대칭키를 교환할때만 비대칭키 암호를 사용하면 어떨까요? 바로 TLS가 그런 역할을 합니다.

TLS에서 제공하는 보안 서비스

1. 기밀성 (이거 북한 어떤 개랑 발음이 똑같네요)

- 대칭키 암호를 사용하게 되면 기밀성을 제공할 수 있습니다. 남들이 데이터를 훔쳐가도 볼 수 없는 비밀을 제공합니다. 

2. 무결성

- 메시지 인증 코드(MAC: Message Authentication Code)를 통해서 메시지 인증을 제공합니다. 위, 변조 여부를 확인할 수 있지요.

3. 인증

- 연결 초기 설정에서 주고 받는 인증서를 통해서 신뢰할 수 있는 개체인지 인증할 수 있습니다.

 

TLS는 전송 계층 위에서 TLS 계층을 따로 두어 동작하게 됩니다. TLS를 사용하는 어플리케이션 프로토콜은 끝에 s가 붙게 되는데 TLS기반의 HTTP는 HTTPS라고 지칭합니다. TLS기반의 FTP는 역시 FTPS라고 부르지요. 

 

TLS의 세부 프로토콜은 다음과 같습니다.

프로토콜 상세 설명
Handshake 양쪽 간에 연결을 설정할때 보안 협상을 위한 프로토콜입니다. 연결 설정 과정은 아래에서 설명합니다.
Change Cipher Spec 보안 파라미터를 변경하거나 적용할때 사용합니다. 예를 들어 대칭키 알고리즘을 변경할때 이 프로토콜이 사용됩니다.
Alert 오류를 전송할때 사용되는 프로토콜입니다.
Application Data 실제 데이터가 전송될때 사용되는 프로토콜입니다.
Record  협상된 보안 파라미터를 이용하여 암,복호화, 무결성 검증 등을 수행하는 프로토콜입니다.

 

상태 유지(Stateful) 프로토콜 

TLS는 세션과 연결별로 상태정보를 유지합니다. TLS는 Full Handshake를 통해서 세션을 생성하고 이 세션 정보를 공유하는 여러 연결을 Abbreviation Handshake를 통해서 성립합니다. 

Full Handshake는 아래에 그림과 같이 설명합니다. Abbrevation Handshake는 세션이 이미 존재할때 사용하는 Handshake방식입니다. 

 

용어가 조금 혼동이 될 수 있는데 연결은 서버와 클라이언트 간 통신의 단위를 말하여 세션은 그 연결의 다수로 이루어지며 세션은 한번 성립되면 다음 연결을 위해서 상태를 유지(session id, negotiated cryptography parameters 등)할 수 있습니다. 예를 들면, 이미 한번의 연결을 하여 암호화 방식, 인증서 교환, session id를 공유했고 할 일이 모두 끝나 연결을 끊었습니다. 이 후 다음 연결에 이 세션에 대한 정보를 이용하여 별도의 번거로운 과정없이 연결을 할 수 있는 겁니다.

 

Handshake

Client → Server :

Client ← Server :

1) Client Hello (Client 안녕)

Client가 서버에 접속할때 몇가지 데이터를 주는데 첫 인사치고는 조금 깁니다. 무엇이 있나 보도록할까요?

- random : 클라이언트는 32바이트 난수값을 전달합니다. 이 랜덤값은 나중에 비밀 데이터를 위해 사용이 됩니다. 비밀 데이터를 master secret이라고 합니다.

- session ID : 세션을 처음 생성할때는 빈값, 이미 생성된 세션이 있다면 그 세션 ID를 전달합니다. 

- cipher suite : 클라이언트가 지원가능한 키 교환 알고리즘, 대칭키 암호 알고리즘, 해시 알고리즘 등을 알려줍니다. 클라이언트와 서버 사이에 갖고 있는 알고리즘이 다 다르겠죠? 그래서 서버는 이 중에 최적의 방식을 선택합니다. 

EX) TLS_RSA_WITH_AES_128_GCM_SHA256 : 보통 이런식인데, 키 교환 알고리즘은 RSA, 대칭키 알고리즘은 AES_128 GCM방식을 사용하고 Hash 알고리즘으로는 SHA256을 사용한다는 것입니다.

 

2) Server Hello (Server 안녕)

사용할 TLS버전, 클라이트, 서버 공통으로 지원가능한 최적의 Cihper suite, 압축 방식 등을 client에게 전달합니다. 이 밖에도 다음 정보를 전달하는데요.

- random : 역시 server도 32바이트 난수를 생성해서 client에게 전달합니다. 역시 나중에 master secret이라는 비밀값을 생성할때 사용되는 재료입니다.

- session ID : 세션 정보를 보냅니다.

 

3) Server certificate

아까 TLS가 인증(TLS의 보안 서비스 3가지 : 기밀성, 무결성, 인증) 서비스를 제공한다고 했습니다. 이 인증서를 이용해서 클라이언트는 서버가 믿을 만한, 신뢰할 만한 서버인지 확인합니다.

 

 

4) Server key exchange

키 교환에 필요한 정보를 제공합니다. 만약 필요하지 않으면 이 과정은 생략이 가능한데, 예를 들어 키교환 알고리즘을 Diffie-Hellman으로 사용한다면 소수, 원시근 등이 필요하므로 이것을 전송합니다.

 

5) Certificate request

서버 역시 클라이언트를 인증해야할때 인증서를 요구할 수 있습니다. 요청하지 않을 수도 있습니다.

 

6) Server hello done

서버의 인사가 끝났네요. 

 

7) Certificate 

방금 전 서버가 요청했던 인증서를 줄 수 있습니다. 요청하지 않았으면 필요없는 과정이네요.

 

8) Client key exchange

키교환에 필요한 정보를 서버에 제공합니다.  이 정보를 pre-master secret이라고 하는데 이게 대칭키에 사용되는 것으로 절대 노출이 되어서는 안됩니다. pre-master secret은 이전에 서버로부터 받은 랜덤값 있었죠? 이것과 클라이언트가 생성한 랜덤값을 조합하여 서버에게 전송합니다. 그냥 보낼까요? 아니죠. 이렇게 민감한 정보는 암호화를 해서 보내야하는데 어떻게 암호화를 할까요? 

이전에 우리는 인증서를 받았습니다. 인증서 안에는 서버의 공개키가 있습니다. 이것으로 서버에게 암호화하여 전송합니다. 

클라이언트는 자기가 생성했으니 이미 가지고 있고, 서버가 무사히 암호화된 pre-master secret을 받았다면 자신의 개인키로 복호화할 수 있습니다. 이제 서로가 pre-master secret을 공유하고 있지요? 이 pre-master secret을 일련의 과정을 거쳐 client/server는 master secret으로 만들게 됩니다.

클라이언트, 서버는 master secret으로 세션에 사용될 키를 생성하게 되는데, 이 키가 바로 대칭키입니다.

 

9) Certificate verify

클라이언트에 대한 Certificate request를 받았다면 보낸 인증서에 대한 개인키를 가지고 있다는 것을 증명합니다. handshake과정에서 주고 받은 메시지 + master secret을 조합한 hash값에 개인키로 디지털 서명하여 전송합니다.

 

10) Change cipher spec

협상된 보안 파라미터를 적용하거나 변경될때 서버에게 알립니다.

 

11) Finished

클라이언트 끝

 

12) Change cipher spec

클라이언트에게 보안 파라미터 변경을 알립니다.

 

13) Finished

서버도 끝

 

14) 통신

이제 주고받은 비밀키를 통해서 안전하게 통신하면 됩니다.

 

이상으로 TLS에 대한 기본적인 개념과 연결과정을 알아보았습니다.

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

wait, waitpid 함수는 아래의 header파일을 include해야합니다. 이 두 함수는 부모프로세스가 자식프로세스를 기다리는 공통점이 있습니다만 waitpid는 어떤 자식을 기다릴지 조금 더 구체적으로 지정할 수 있습니다.

이 두 함수의 반환값은 성공시 process id를 반환하고 오류시 -1을 반환합니다. 

#include <sys/types.h>
#include <sys/wait.h>

 

wait 함수

pid_t wait(int *wstatus);

 

성공시 프로세스의 pid를 반환합니다. wstatus는 종료상태를 알 수 있는데, 굳이 종료상태를 알 필요가 없다면 NULL을 전달해주세요.

wstatus를 알 수 있는 방법은 <sys/wait.h>에 정의되어있는 매크로들을 사용하면 됩니다. 아래는 그 매크로들과 설명입니다. 

매크로 설명
WIFEXITED(wstatus) returns  true  if  the  child  terminated normally, that is, by calling exit(3) or
              _exit(2), or by returning from main().
WEXITSTATUS(wstatus) returns the exit status of the child.  This consists of the  least  significant  8
              bits  of  the  status  argument  that  the child specified in a call to exit(3) or
              _exit(2) or as the argument for a return statement in main().  This  macro  should
              be employed only if WIFEXITED returned true.
WIFSIGNALED(wstatus) returns true if the child process was terminated by a signal.
WTERMSIG(wstatus) returns the number of the signal that caused the child process to terminate.  This
              macro should be employed only if WIFSIGNALED returned true.
 WCOREDUMP(wstatus) returns true if the child produced a core dump.  This  macro  should  be  employed
              only if WIFSIGNALED returned true.

              This  macro  is  not  specified  in POSIX.1-2001 and is not available on some UNIX
              implementations (e.g., AIX, SunOS).  Therefore,  enclose  its  use  inside  #ifdef
              WCOREDUMP ... #endif.
WIFSTOPPED(wstatus) returns  true  if  the  child process was stopped by delivery of a signal; this is
              possible only if the call was done using WUNTRACED or  when  the  child  is  being
              traced (see ptrace(2)).
WSTOPSIG(wstatus) returns  the  number  of  the  signal  which caused the child to stop.  This macro
              should be employed only if WIFSTOPPED returned true.
 WIFCONTINUED(wstatus) (since Linux 2.6.10) returns true if the child process was resumed by delivery  of
              SIGCONT

 

그렇다면 이제 예제를 보도록 하겠습니다.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
        int status, cpid, endpid;

        if((cpid=fork())==0){
                printf("\tchild process:%d\n",getpid());
                sleep(10);
                printf("\tchild end\n");
                exit(10);
        }

        endpid=wait(&status);
        printf("end pid : %d\n",endpid);
        printf("WIFEXITED : %d\n",WIFEXITED(status));
        printf("WEXITSTATUS : %d\n",WEXITSTATUS(status));
        printf("WIFSIGNALED : %d\n",WIFSIGNALED(status));

        printf("\n");
        printf("parent end\n");

}

 

이후 아래와 같이 컴파일하고 정상적인 결과를 보도록 해봅시다.

# gcc process_wait.c
# ./a.out
        child process:5031
        child end
end pid : 5031
WIFEXITED : 1
WEXITSTATUS : 10
WIFSIGNALED : 0

parent end

 

EXITSTATUS는 10임을 알 수 있는데 이는 위 코드에서 exit(10)을 사용했기 때문입니다. 시그널을 받지 않고 정상적인 종료를 한 케이스는 이렇게 나오는군요.

이제 background로 프로세스를 돌리고 kill해서 시그널을 보내보도록 하겠습니다.

# ./a.out &
[4] 5060
#     child process:5061

# kill 5061
# end pid : 5061
WIFEXITED : 0
WEXITSTATUS : 0
WIFSIGNALED : 1

parent end

[4]+  Done                    ./a.out

 

그렇다면 WIFEXITED는 false이고 WIFSIGNALED는 true네요. 이처럼 위의 매크로로 자식 프로세스의 종료 상태를 알 수 있습니다.

그런데 wait은 종료되는 자식 프로세스 중 먼저 종료되는 프로세스가 있다면 바로 호출되어지는데 만약 우리가 특정 자식프로세스만을 기다린다할때는 아래의 waitpid함수를 사용해야합니다.

 

waitpid

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

역시 호출 성공시 그 프로세스의 pid, 아니라면 -1을 반환합니다. 

pid: 인자로 pid는 기다릴 프로세스 식별자인데 양수일 경우만 해당합니다. 아래는 pid에 따라서 어떻게 동작하는지를 설명합니다.

 

pid 설명
< -1  meaning wait for any child process whose process group ID is equal to the absolute value of pid. 
 -1 meaning wait for any child process. 
0 meaning wait for any child process whose process group ID is equal to that of  the calling process.
> 0  meaning wait for the child whose process ID is equal to the value of pid. 

 

wstatus : wstatus는 위의 wait과 같이 종지상태를 얻어옵니다.

option : option은 조금 더 구체적인 동작을 지정합니다. option의 종류는 아래와 같습니다.

 

OPTION 설명
WNOHANG return immediately if no child has exited. 
WUNTRACED also  return  if  a child has stopped (but not traced via ptrace(2)).  Status for traced children which have stopped is provided even if this option is not specified. 
WCONTINUED (since Linux 2.6.10) also return if a stopped child has been resumed by delivery of SIGCONT. 

 

wait(&status)은 waitpid(-1, &wstatus, 0)를 호출한 것과 같습니다.

 

waitpid 예제

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
        int state, cpid1, cpid2, endpid;

        if((cpid1=fork())==0){
                printf("\tchild1 process:%d\n",getpid());
                sleep(2);
                printf("\tchild1 end\n");
                exit(0);
        }

        if((cpid2=fork())==0){
                printf("\tchild2 process:%d\n",getpid());
                sleep(4);
                printf("\tchild2 end\n");
                exit(0);
        }

        endpid=waitpid(cpid1,&state,0);
        printf("pid : %d\n",endpid);

        endpid=waitpid(cpid2,&state,0);
        printf("pid : %d\n",endpid);


        printf("parent end\n");

}

 

여기서 자식 프로세스를 2개 생성하고 기다리는데 waitpid로 첫번째 자식 프로세스, 두번째 자식 프로세스를 기다립니다. 만약 여기서 두 번째 자식 프로세스를 기다리고 싶지 않다면 두 번째 waitpid를 하지 않으면 되겠죠.

결과는 아래와 같습니다.

#gcc process_waitpid.c
# ./a.out
        child2 process:5614
        child1 process:5613
        child1 end
pid : 5613
        child2 end
pid : 5614
parent end

 

 

만약 두번째 프로세스가 끝났을때는 그대로 종료하지만 작업이 끝나지 않았을때는 A라는 작업을 해야하는 상황이라면 어떻게 해야할까요? 

wait은 계속 자식 프로세스가 끝날때까지 block되었는데 waitpid는 옵션을 사용하면 됩니다. 

 

아래가 그 예제입니다.

 

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
        int state, cpid1, cpid2, endpid;

        if((cpid1=fork())==0){
                printf("\tchild1 process:%d\n",getpid());
                sleep(2);
                printf("\tchild1 end\n");
                exit(0);
        }

        if((cpid2=fork())==0){
                printf("\tchild2 process:%d\n",getpid());
                sleep(4);
                printf("\tchild2 end\n");
                exit(0);
        }

        while(1){
                endpid=waitpid(cpid2,&state,WNOHANG);
                if(endpid==0){  //child not finished if pid = 0
                        printf("child2 not finished\n");
                        sleep(1);
                }else{
                        break;
                }

        }

        printf("parent end\n");

}

 

위 코드에서는 첫번째 자식 프로세스는 기다리지 않습니다. waitpid에 WNOHANG 옵션을 주어서 자식이 종료되었는지 아닌지에 따라 작업을 구분지을 수 있습니다. 이때 waitpid는 지정된 프로세스가 끝나지 않았다면 0을 반환하고 끝났다면 그 pid를 반환하게 됩니다.

결과는 아래와 같습니다.

# gcc process_waitpid2.c
# ./a.out
child2 not finished
        child2 process:5670
        child1 process:5669
child2 not finished
        child1 end
child2 not finished
child2 not finished
        child2 end
parent end

 

waitpid가 wait에서 제공하지 못하는 3가지를 제공합니다.

1. waitpid함수로 특정 pid를 기다릴 수 있습니다. wait은 임의의 자식에 대해서 종료상태를 알려주지요.

2. waitpid는 wait과는 달리 자식이 종료될때까지 기다리지 않아도 됩니다. 바로 호출을 할 수 있습니다.

3. WUNTRACED와 WCONTINUED 옵션을 통해서 작업 제어를 할 수 있습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,