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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

한계

리눅스에서 프로그램을 다른 리눅스에서 실행할 수 있습니다. 혹은 다른 유닉스 계열(BSD나 솔라리스, MAC OS X 등)의 시스템에서도 동작이 될 수가 있지요. 그런데 이렇게 시스템마다 지원하는 한계라는 것이 다 제 각각이거든요. 예를 들면 어떤 시스템에서는 한 프로세스 당 열 수 있는 파일 디스크립터의 갯수가 256개를 지원하는데, 다른 시스템에서는 1024개까지 지원할 수 있는 등 시스템마다 지원할 수 있는 한계가 존재합니다. 그렇다면 어떤 시스템에서 사용자가 생성할 수 있는 자식 프로세스의 수라던가, 최대 파일 경로의 이름이라던가,  로그인이 가능한 이름의 최대 길이라던가를 알아내면 그 프로그램이 효율적으로 동작할 수 있을 것 같은데요. 과연 어떻게 알아낼 수 있을까요?

한계는 세가지로 구분할 수가 있습니다. 

  • 컴파일 시점에서 한계(헤더 파일에 명시) : 이러한 한계들은 limits.h에 명시되어 있습니다. 시스템마다 불변하는 한계를 의미하게 됩니다. 예를 들어 POSIX를 준수하는 시스템에서는 int 자료형에서 지원가능한 값은 2,147,483,647이 적어도 만족이 되어야합니다. INT_MAX 상수로 확인이 가능합니다. 여기서는 설명하지 않겠습니다. 
  • 파일이나 디렉토리와 연관되지 않은 실행 시점의 한계(sysconf 함수) : 파일과 연관이 없는 시스템에서 실제 지원할 수가 있는 한계를 확인하려면 sysconf함수를 이용해서 확인할 수 있습니다. 함수의 원형을 볼까요?
#include <unistd.h>

long sysconf(int name);

   sysconf의 인자인 name을 전달하게 되면 그에 따른 값이 나옵니다. name은 앞에 _SC_로 시작하는 매크로 상수입니다. SC는 SysConf의 약자입니다. 아래의 소개한 매크로보다 더 다양한 name이 있습니다. 여기서는 요만큼만 설명합니다.

매크로 상수 설명
_SC_ARG_MAX exec 함수의 인수 길이 최대값
_SC_CHILD_MAX uid 당 동시에 실행할 수 있는 프로세스의 최대값
_SC_HOST_NAME_MAX hostname의 최대 값, gethostname으로 구할 수 있습니다
_SC_LOGIN_NAMX_MAX 로그인 이름의 최대 길이 
_SC_OPEN_MAX 프로세스가 열 수 있는 파일의 최대 갯수
_SC_PAGE_SIZE
_SC_PAGESIZE
페이지의 크기
_SC_STREAM_MAX 프로세스가 열 수 있는 파일 스트림의 최대 갯수
_SC_TTY_NAME_MAX 터미널 디바이스 이름의 최대 길이
_SC_TZNAME_MAX 타임 존 이름의 최대 길이 
_SC_LINE_MAX 유틸리티 프로그램에서 입력 줄로 받을 수 있는 최대 길이(stdin으로든 file으로 든)
_SC_SIGQUEUE_MAX 한 프로세스에 신호 큐에 담을 수 있는 신호의 최대 갯수
_SC_SEM_VALUE_MAX 세마포어의 최대값
_SC_SEM_NSEMS_MAX 한 프로세스가 동시에 사용할 수 있는 세마포어의 최대 개수
_SC_CLK_TCK 1초 클록 틱 개수

 

  • 파일이나 디렉토리와 연관된 실행 시점의 한계(pathconf 함수 혹은 fpathconf 함수) : 파일과 관련된 한계를 알아낼 때는 pathconffpathconf 함수를 사용할 수 있습니다. 예를 들어 어떤 터미널에 대한 한계를 알고 싶다면 /dev/ 하위의 터미널 파일을 입력으로 주어 확인할 수 있습니다. 
#include <unistd.h>

long fpathconf(int fd, int name);
long pathconf(const char *path, int name);

 이 둘의 동작은 같습니다. 단지 파일을 알려주는 첫 인자를 파일 디스크립터로 전달하느냐(fd), 파일의 경로와 이름을 사용하여 전달하느냐(path)에 따른 것만 다르죠. name은 확인하려는 한계의 이름을 넣어주면 됩니다. 앞에 _PC_로 시작합니다. Path Config의 약자겠죠? 

매크로 상수 설명
_PC_LINK_MAX 파일에 최대 링크 갯수를 의미하며, 만약 파일이라면 파일에 대한 최대 링크 갯수를 가져오고, 디렉토리를 지정하면 디렉토리의 링크 최대 갯수를 가져옵니다.
_PC_MAX_CANON 터미널의 서식화된 입력줄의 최대 길이로 파일을 반드시 터미널 파일을 지정해야합니다.
_PC_MAX_INPUT 터미널의 입력줄의 최대 길이로 반드시 터미널 파일을 지정해야합니다.
_PC_NAME_MAX 지정한 디렉토리에서 파일 입력의 최대 길이로 인자를 디렉토리로 주어야합니다.
_PC_PIPE_BUF 하나의 파이프에 원자적으로 쓸 수 있는 최대 바이트 수로 pipe나 fifo 타입의 파일, 혹은 디렉토리를 주어야합니다. 디렉토리를 주었을 때는 디렉토리 안에 생성된 임의의 fifo에 대한 한계를 가져옵니다.
_PC_PATH_MAX path나 fd가 현재 작업 디렉토리일 경우 상대 경로의 최대길이

 

위 세 함수(sysconf, pathconf, fpathconf)는 실패일 경우 -1이 반환되지만 1) 실제 지원하지 않는 name이라서 실패한 경우2)확정할 수 없는 한계에 의한 실패가 있습니다. 지원하지 않는 실패의 경우 errno가 EINVAL로 설정되구요. 확정할 수 없는 한계에 의한 실패는 errno가 변하지 않습니다.

한계를 가져오는 소스 코드

//print_conf.c
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

static void print_sysconf(char *str, int name){
        long val;
        errno = 0;
        printf("%s ", str);

        if((val = sysconf(name)) < 0){ 
                if(errno != 0){
                        if(errno == EINVAL)
                                printf(" (not supported)\n");
                        else printf("syconf error\n");
                }else
                        printf(" (no limit)\n");

        }else 
                printf(" %ld\n", val);
}

static void print_pathconf(char *str, char *path, int name){
        long val;
        printf("%s ", str);
        errno = 0;
        if((val = pathconf(path,name)) < 0){ 
                if(errno != 0){
                        if(errno == EINVAL)
                                printf(" (not supported)\n");
                        else printf("pathcon error(%s)\n", path);
                }else
                        printf(" (no limit)\n");
        }else
                printf(" %ld\n", val);

}
int main(int argc, char *argv[]){
        if(argc != 2){
                printf("Usage : %s <filename> \n", argv[0]);
                return 1;
        }


        printf("======== sysconf ==========\n");
#ifdef _SC_ARG_MAX
        print_sysconf("ARG_MAX :", _SC_ARG_MAX);
#endif
#ifdef _SC_CHILD_MAX
        print_sysconf("CHILD_MAX : ", _SC_CHILD_MAX);
#endif
#ifdef _SC_HOST_NAME_MAX
        print_sysconf("HOST_NAME_MAX : ", _SC_HOST_NAME_MAX);
#endif
#ifdef _SC_LOGIN_NAMX_MAX
        print_sysconf("LOGIN_NAMX_MAX : ", _SC_LOGIN_NAMX_MAX);
#endif
#ifdef _SC_OPEN_MAX
        print_sysconf("OPEN_MAX : ", _SC_OPEN_MAX);
#endif
#ifdef _SC_PAGESIZE
        print_sysconf("PAGESIZE : ", _SC_PAGESIZE);
#endif
#ifdef _SC_STREAM_MAX
        print_sysconf("STREAM_MAX : ", _SC_STREAM_MAX);
#endif
#ifdef _SC_TTY_NAME_MAX
        print_sysconf("TTY_NAME_MAX : ", _SC_TTY_NAME_MAX);
#endif
#ifdef _SC_TZNAME_MAX
        print_sysconf("TZNAME_MAX : ", _SC_TZNAME_MAX);
#endif
#ifdef _SC_LINE_MAX
        print_sysconf("LINE_MAX : ", _SC_LINE_MAX);
#endif
#ifdef _SC_SIGQUEUE_MAX
        print_sysconf("SIGQUEUE_MAX : ", _SC_SIGQUEUE_MAX);
#endif
#ifdef _SC_SEM_VALUE_MAX
        print_sysconf("SEM_VALUE_MAX : ", _SC_SEM_VALUE_MAX);
#endif
#ifdef _SC_SEM_NSEMS_MAX
        print_sysconf("SEM_NSEMS_MAX : ", _SC_SEM_NSEMS_MAX);
#endif
#ifdef _SC_CLK_TCK
        print_sysconf("CLK_TCK : ", _SC_CLK_TCK);
#endif

        printf("======== pathconf ==========\n");
#ifdef _PC_LINK_MAX
        print_pathconf("LINK_MAX : ", argv[1], _PC_LINK_MAX);
#endif
#ifdef _PC_MAX_CANON
        print_pathconf("MAX_CANON : ", argv[1], _PC_MAX_CANON);
#endif
#ifdef _PC_MAX_INPUT
        print_pathconf("MAX_INPUT : ", argv[1], _PC_MAX_INPUT);
#endif
#ifdef _PC_NAME_MAX
        print_pathconf("NAME_MAX : ", argv[1], _PC_NAME_MAX);
#endif
#ifdef _PC_PIPE_BUF
        print_pathconf("PIPE_BUF : ", argv[1], _PC_PIPE_BUF);
#endif
#ifdef _PC_PATH_MAX
        print_pathconf("PATH_MAX : ", argv[1], _PC_PATH_MAX);
#endif

}

 

위의 소스코드는 sysconf와 pathconf 함수를 사용해서 한계를 출력해주는 프로그램입니다. 다른 시스템에서는 정의되지 않은 name이 존재할 수 있으므로 #ifdef로 정의되어있는지 파악하여 사용합니다. 

실패할 경우에는 errno이 EINVAL이면 지원하지 않는 한계 이름입니다. 그런데 errno가 0으로 변하지 않았다면 불확정인 한계로 볼 수 있습니다. 

errno = 0;
//...
if((val = sysconf(name)) < 0){ 
    if(errno != 0){
            if(errno == EINVAL)
                    printf(" (not supported)\n");
            else printf("syconf error\n");
    }else
            printf(" (no limit)\n");

 

아래는 실행 결과입니다.

# ./a.out /etc
======== sysconf ==========
ARG_MAX :  2097152
CHILD_MAX :   15044
HOST_NAME_MAX :   64
OPEN_MAX :   1024
PAGESIZE :   4096
STREAM_MAX :   16
TTY_NAME_MAX :   32
TZNAME_MAX :   (no limit)
LINE_MAX :   2048
SIGQUEUE_MAX :   15044
SEM_VALUE_MAX :   2147483647
SEM_NSEMS_MAX :   (no limit)
CLK_TCK :   100
======== pathconf ==========
LINK_MAX :   65000
MAX_CANON :   255
MAX_INPUT :   255
NAME_MAX :   255
PIPE_BUF :   4096
PATH_MAX :   4096

 

반응형
블로그 이미지

REAKWON

와나진짜

,

버퍼

C 표준입출력 라이브러리에서는 내부적으로 버퍼를 도입하여 입출력을 효율적으로 처리합니다. printf나 scanf 등의 라이브러리 함수는 결국 입력과 출력을 read, write를 통해서 이루어집니다. 버퍼라는 공간을 두는 이유는 내부적으로 write, read를 적시에 최소한으로만 호출하기 위한 것이 목적입니다. 왜 그런 뻘짓을 하느냐구요? CPU를 많이 사용하지 않기 위해서입니다. 한 글자 씩 계속 입력을 받거나 출력을 하면 그만큼 read, write 콜이 잦아지는데 이러면 CPU에 부담이가 성능에 안좋을 영향을 끼치게 되는 겁니다. 버퍼를 사용하는 것은 라이브러리 함수인면에서 응용 프로그램에서는 신경쓰지 않아도 되지만, 버퍼의 처리 방식을 모르게 되면 낭패를 봅니다.

어떻게 버퍼를 이용하는 것을 버퍼링이라고 하고  따라서 세가지 버퍼링 방식이 있습니다. 

전체 버퍼링(Full buffering)

이러한 버퍼링은 내부 버퍼에 데이터가 꽉 차게 되면 그제서야 입출력이 되는 방식입니다. 그러니까 버퍼가 전부 차기 전에는 이 데이터를 가지고만 있고 입출력은 하지 않는 것이죠. 위에서 얘기했듯이 이러한 버퍼링의 목적은 read, write를 최소한으로 사용하기 위함입니다. 그래서 버퍼가 전부 찰 때까지 기다리고 있죠. 이 때 "버퍼의 크기가 크면 무조건 좋은 거 아닌가?" 라는 물음을 던질 수 있는데, 정도라는 것이 있듯 최적의 버퍼 크기가 정해져있습니다. 이것을 표준 입출력 라이브러리가 정해줍니다. 우리는 개-꿀만 빨면 됩니다.

보통 파일을 디스크로부터 읽을 때의 버퍼링 방식입니다. 

아래와 같은 경우가 전체 버퍼링의 예를 보여줍니다. 붉은 사각형은 비어있는 데이터를 의미하며 파란 사각형은 채워진 데이터를 의미합니다. 현재는 버퍼에 2바이트의 데이터가 모자라서 파일에 기록하지 않고 있습니다. 이때 2바이트가 채워지고 있는 모습입니다. 

전체 버퍼링1

이 때 완전히 버퍼가 채워지면 그제서야 데이터를 한꺼번에 파일로 전송하게 됩니다. 

 

줄 단위 버퍼링(Line buffring)

scanf나 fgets, fgetc 등의 표준 입력 함수나 printf, fputs, putc 등의 함수를 이용한 표준 출력을 사용할 때 이러한 줄 단위 버퍼링이 적용됩니다. 줄 단위 버퍼링은 새 줄 문자('\n')가 나올 때 까지 입력이나 출력을 하는 것입니다. 또한 버퍼가 차게 되면 입출력을 진행합니다. 이 때 버퍼의 크기는 보통 전체 버퍼링의 버퍼 크기보다 작습니다.

아래와 같은 경우가 줄 단위 버퍼링을 보여줍니다. 아직 데이터가 전부 채워지지 않았으며 이 때 개행 문자인 '\n'이 입력이 되고 있는 상황입니다. 

개행 문자를 만나면 버퍼가 채워져있지 않음에도 입출력을 진행하게 됩니다. 

 

비 버퍼링(Unbuffered)

버퍼링은 하지 않는 방식입니다. 왜요? 급하기 때문입니다. 여러분도 급똥이면 장사없듯이 프로그램도 급하면 장사없습니다. 언제가 급할까요? 에러를 출력할때가 그런 상황입니다. 지체없이 에러를 해결해야할 상황이 생기기 때문이지요. 

버퍼링 정보 가져오기

그렇다면 보통의 표준 입력, 표준 출력, 표준 에러나 파일에 대한 스트림은 어떤 버퍼링 방식을 갖고 버퍼 크기는 어떻게 결정이 될까요? 아래의 코드는 상황에 따른 입,출력 버퍼에 대한 정보를 표시해주는 코드입니다.

//buffer_info.c

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

#ifdef __GLIBC__
#define _IO_UNBUFFERED 0x0002
#define _IO_LINE_BUF 0x0200
#endif

int main(int argc, char *argv[]){
        FILE *fp;

        char buf[32] = {0, };

        if(argc =! 2){
                printf("Usage : %s stdin | stdout | stderr | file_name\n",
                                argv[0]);
                return 1;
        }


        if(!strcmp(argv[1], "stdin")){
                fp = stdin;
                fgets(buf, sizeof(buf), fp);
        } else if(!strcmp(argv[1], "stdout")){
                fp = stdout;
                printf("stdout\n");
        } else if(!strcmp(argv[1], "stderr")){
                fp = stderr;
                fprintf(fp, "stderr\n");
        } else {
                fp = fopen(argv[1], "r");

                if(fp == NULL){
                        printf("fopen error\n");
                        return 1;
                }

                while(fgets(buf, sizeof(buf), fp) != NULL);
        }

        if(fp->_flags & _IO_UNBUFFERED)
                printf("비버퍼링\n");
        else if(fp->_flags & _IO_LINE_BUF)
                printf("줄단위 버퍼링\n");
        else
                printf("전체 버퍼링\n");

        printf(" 버퍼 사이즈 : %ld\n", fp->_IO_buf_end - fp->_IO_buf_base);

        fclose(fp);
}

 

# ./a.out stdin
hello
줄단위 버퍼링
 버퍼 사이즈 : 1024
# ./a.out stdout
stdout
줄단위 버퍼링
 버퍼 사이즈 : 1024
# ./a.out stderr
stderr
비버퍼링
 버퍼 사이즈 : 1
# ./a.out /etc/group
전체 버퍼링
 버퍼 사이즈 : 4096

 

단순히 stdin, stdout, stderr에 대해서 fgets나 printf를 한번 호출하지 않고서 fp->_flags를 들여다보면 다른 결과가 나올 수 있습니다. 예를 들면 아래와 같이 fgets를 주석 처리하고 실행해보시면 다른 결과를 보실 수 있을 겁니다. 

    if(!strcmp(argv[1], "stdin")){
            fp = stdin;
            //fgets(buf, sizeof(buf), fp);

아래의 결과가 위처럼 fgets를 주석처리한 예인데, 결과가 다르죠?

# ./a.out stdin
전체 버퍼링
 버퍼 사이즈 : 0

이러한 결과를 통해서 알 수 있는 것은 스트림을 열었다고 해서 그 버퍼링이 설정된다는 것이 아니라, read, write를 하는 함수들이 버퍼링을 결정해준다는 사실입니다. 

 

버퍼링 설정 

1. setbuf

#include <stdio.h>

void setbuf(FILE *stream, char *buf);

setbuf 함수를 이용해서 버퍼링 방식을 설정할 수 있습니다. 대신 시스템이 정해준 버퍼인 BUFSIZ로만 사용이 가능합니다. 반대로 버퍼링을 끌 수도 있습니다.

버퍼를 설정하려면 buf[BUFSIZ]의 버퍼를 *buf인자에 전달해야하고, 버퍼를 끄려면 NULL을 전달하면 됩니다.

아래의 예를 한번 볼까요?

//setbuf.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

//버퍼링을 키거나 끄는 쓰레드 
void* buf_control(void *arg){
        char buf[BUFSIZ] = {0,};
        int on;
        while(1){
                scanf("%d", &on);
                switch(on){
                        case 0: //OFF
                                setbuf(stdout, NULL);
                                break;
                        case 1: //ON
                                setbuf(stdout, buf);
                                break;
                }
        }

}

//1초마다 "A"를 계속 찍는 쓰레드
void* print_line(void *arg){
        while(1){
                printf("A");
                sleep(1);
        }
}
int main(int argc, char *argv[]){
        pthread_t tid1, tid2;

        printf("[0] 버퍼 동작 X\t [1] 버퍼 동작 O\n");

        pthread_create(&tid1, NULL, buf_control, NULL);
        sleep(1);
        pthread_create(&tid2, NULL, print_line, NULL);

        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
}
# ./a.out 
[0] 버퍼 동작 X  [1] 버퍼 동작 O
0
AAAAAAAAAAAAAAA1
0
AAAAAAAAAAAA1A
0
AAAAA1
0
AAA1
0
AAAAA^C

 

pthread의 개념을 몰라도 좋습니다. 단순히 buf_control이라는 함수, print_line이라는 함수가 동시에 수행되는 것만 아시면 됩니다. 

buf_control이라는 함수에서는 버퍼링을 끄거나 키거나 할 수 있는데, 1은 버퍼링을 키는 동작, 0은 버퍼링을 끄는 동작이라는 것을 볼 수 있을 겁니다. 

print_line함수는 한 글자씩 1초마다 printf를 통해서 출력을 해주는 함수입니다. 단 줄바꿈(\n)은 하지 않죠. printf는 디폴트 동작으로는 줄단위 버퍼링 방식을 사용하는 것을 위에서 확인했었죠?? 그래서 버퍼링을 설정하게 되면 줄바꿈이 나오지 않거나 버퍼 크기인 1024바이트가 채워지지 않으면 출력을 하지 않습니다.

2. setvbuf

#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

세 가지의 버퍼링 방식을 설정할 수 있습니다. stream에 대해서 size만큼의 buf를 버퍼링합니다. 이때 mode는 비버퍼링(unbuffered), 줄 단위 버퍼링(line buffering), 전체 버퍼링(full buffering)을 설정할 때 쓰입니다. mode에 대한 설명은 아래를 참고하세요. 

mode 설명 
_IONBF 비버퍼링 모드 
_IOLBF 줄 단위 버퍼링 모드
_IOFBF 전체 버퍼링(블록 버퍼링) 모드

setvbuf는 성공시 0, 실패시 0이 아닌 값을 설정하여 돌려줍니다.

확실히 setbuf 함수보다는 보다 세세한 설정이 가능하죠? 그렇다면 setvbuf함수를 통해서 버퍼링을 설정하는 예를 볼까요? 아래는 stdout을 줄 단위 버퍼링이 아닌 4바이트의 전체 버퍼링으로 바꾼 하나의 예입니다.

//setvbuf.c
#include <stdio.h>
#include <unistd.h>

#ifdef __GLIBC__
#define _IO_UNBUFFERED 0x0002
#define _IO_LINE_BUF 0x0200
#endif

#define BUF_SIZE 4

int main(){

        char buf[BUF_SIZE] = {0,};
        FILE *fp = stdout;

        if(setvbuf(fp, buf, _IOFBF, BUF_SIZE) != 0){
                printf("setvbuf _IOLBF error \n");
                return 1;
        }

        if(fp->_flags & _IO_UNBUFFERED) printf("비버퍼링\n");
        else if(fp->_flags & _IO_LINE_BUF) printf("줄단위 버퍼링\n");
        else printf("전체 버퍼링\n");

        while(1){
                printf("A");
                sleep(1);
        }

}

실행해보면 줄 단위 버퍼링이 아닌 전체 버퍼링으로 설정된 것을 볼 수 있습니다. 그리고 4초마다 버퍼가 꽉 채워지기 때문에 출력이 되는 동작을 확인할 수 있네요.

# ./a.out 
전체 버퍼링
AAAAAAAAAAA^C
반응형
블로그 이미지

REAKWON

와나진짜

,

pipe, 공유 메모리, 메시지 큐 등 IPC와 관련한 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

FIFO(Named Pipe)

pipe(fd[2])를 호출해서 만들어진 파이프는 이름이 붙어있지 않습니다. 그렇기 때문에 부모 프로세스나 자식 프로세스와 같이 연관된 프로세스에서 사용할 수 있습니다. 물론 부모 프로세스가 파이프를 생성하고, 자식 프로세스 2개를 생성한 후에 그 자식 프로세스들이 부모 프로세스가 생성한 파이프를 쓰는 것도 가능합니다. 한계점은 전혀 연관없는 프로세스는 파이프를 사용하여 입출력할 수 없다는 점인데요. 파이프의 이러한 한계를 개선한 것이 FIFO입니다.

FIFO는 다른 말로 이름있는 파이프, 명명된 파이프(named pipe)라고 합니다. 파이프라는 특징이 결국 먼저 들어간 것이 먼저 나오는 구조인 선입선출(Fitst In First Out)의 특징, 그러니까 먼저 먹은걸 먼저 싼다는 개념을 갖기 때문이죠. 이름이 있기 때문에 연관없는 다른 프로세스가 그 이름을 가진 파이프를 찾아내어 입력이나 출력을 할 수 있게 됩니다. 

mkfifo

이름있는 파이프는 아래의 함수를 호출하여 만들어집니다. fifo는 일종의 파일이기 때문에 open 시스템콜과 매우 유사한 방식으로 만들 수 있다는 점입니다.

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);

 

mkfifo와 mkfifoat의 차이점이라고 한다면 상대경로일 경우 dirfd에서 시작하느냐 아니면 현재 위치에서 시작하느냐의 차이입니다. mode : mkfifo 생성시에  권한을 부여합니다. open에서의 mode와 비슷합니다.

mkfifo를 통해서 파이프를 생성하면 왠 파일이 생성되는 것을 확인할 수 있습니다. fifo 역시 파일의 한 종류이기 때문에 unlink(삭제)가 가능합니다. 

예제

fifo는 pipe이기 때문에 한쪽에서는 읽기만, 한쪽에서는 쓰기만 할 수 있습니다. 만약 클라이언트의 메시지를 다시 되돌려주는 에코 서버를 만들려면 클라이언트로부터 읽기, 클라이언트로 쓰기를 다 해야하는데, 이럴경우는 어떻게할까요? 파이프 2개를 사용하면 됩니다. 그래서 아래와 같이 구현이 가능하죠. to-server.fifo와 to-client.fifo라는 명명된 파이프 2개를 야무지게 사용하는 것을 확인할 수 있죠?

 

fifo를 이용해서 일종의 서버와 클라이언트 프로그램을 만들어보도록 합시다.

//fifo_server.c
//
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

#define MAX_BUF 128

#define TOSERVER "to-server.fifo"
#define TOCLIENT "to-client.fifo"

void norm_exit(){
        unlink(TOSERVER);
        unlink(TOCLIENT);
        exit(0);
}
void sig_int(int signo){
        norm_exit();
}
int main(){
        int readfd, writefd, n; 
        char buf[MAX_BUF]={0,};

        if(signal(SIGINT, sig_int) == SIG_ERR){
                perror("signal error ");
                exit(1);
        }

        if(mkfifo(TOSERVER, 0666) < 0){
                perror("mkfifo for reading error ");
                exit(1);
        }
        if(mkfifo(TOCLIENT, 0666) < 0){
                perror("mkfifo for writing error ");
                exit(1);
        }

        //다른쪽에서 fifo를 열때까지 대기한다.
        readfd = open(TOSERVER, O_RDONLY);
        writefd = open(TOCLIENT, O_WRONLY);


        if(readfd < 0 || writefd < 0){
                perror("open error ");
                exit(1);
        }

        printf("server start\n");

        while(1){

                //readfd를 통해서 입력받는다
                if((n = read(readfd, buf, MAX_BUF)) < 0){
                        perror("read error ");
                        exit(1);
                }

                //한쪽에서 fifo를 닫으면 파일끝을 만나게 된다. 
                if(n == 0) {
                        printf("file end\n");
                        norm_exit();
                }

                printf("[read message ] %s\n", buf);

                //읽은 메시지를 fifo를 통해 전달한다.
                if((n = write(writefd, buf, n)) < 0){
                        perror("write error ");
                        exit(1);
                }
        }

}

 

위는 서버의 역할을 하는 프로그램입니다. 이 서버 단독으로 실행시에 아마 멈춰있을 겁니다. 

fifo의 기본동작은 파일을 쓰기 전용 - 읽기 전용으로 열려야지 그 다음으로 진행한다는 것입니다. 그래서 writefd가 다른 프로세스에서 O_RDONLY가 될때 다음 라인으로 넘어가고 다시 readfd가 O_WRONLY로 다른 프로세스에 의해서 열려야 다음의 실행으로 넘어갈 수 있다는 뜻입니다.

//다른쪽에서 fifo를 열때까지 대기한다.
writefd = open(TOCLIENT, O_WRONLY);
readfd = open(TOSERVER, O_RDONLY);

일단 열고보겠다면 open시에 O_NONBLOCK을 지정해야합니다. 

다음은 client 역할을 하는 소스코드입니다.  

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

#define MAX_BUF 128

#define TOSERVER "to-server.fifo"
#define TOCLIENT "to-client.fifo"

int main(){
        int readfd, writefd, n; 
        char buf[MAX_BUF]={0,};

        writefd = open(TOSERVER,O_WRONLY);
        readfd = open(TOCLIENT, O_RDONLY);

        if(readfd < 0 || writefd < 0){
                perror("open error ");
                exit(1);
        }

        while(1){

                printf("message :");
                fgets(buf, MAX_BUF, stdin);

                n = strlen(buf) + 1;
                if((n = write(writefd, buf, n)) < 0){
                        perror("write error ");
                        exit(1);
                }

                if((n = read(readfd, buf, MAX_BUF)) < 0){
                        perror("read error ");
                        exit(1);
                }

                printf("[read message ] %s\n", buf);

        }
}

 

컴파일은 아래와 같이 해줍시다.

 

# gcc fifo_server.c -o server
# gcc fifo_client.c -o client

 

이제 실행하면서 어떤 현상이 관찰되는지 확인해볼까요? 클라이언트 실행을 위해 터미널을 2개 사용합시다.

[1]

우선 서버쪽을 보면 그대로 멈춰있는 것을 알 수 있습니다. 이때 client 실행하기 전에 다른 터미널에서 파일 목록을 보면 fifo 파일 두개가 생성되어있음을 확인할 수 있을 건데, 이는 mkfifo를 호출하여 만든 결과입니다.

./server ./client
#./server

# ls -l
total 36
-rwxr-xr-x 1 root root  9264 Jul 13 04:36 client
-rw-r--r-- 1 root root   770 Jul 13 04:00 fifo_client.c
-rw-r--r-- 1 root root  1293 Jul 13 04:35 fifo_server.c
-rwxr-xr-x 1 root root 13432 Jul 13 04:36 server
prw-r--r-- 1 root root     0 Jul 13 04:36 to-client.fifo
prw-r--r-- 1 root root     0 Jul 13 04:36 to-server.fifo

 

이제 클라이언트를 실행해볼게요.

[2]

./server ./client
# ./server 
server start
[read message ] hello fifo server

[read message ] yo shake it!! just shake it !! 

[read message ] good bye
file end
# ./client 
message :hello fifo server
[read message ] hello fifo server

message :yo shake it!! just shake it !! 
[read message ] yo shake it!! just shake it !! 

message :good bye
[read message ] good bye

message :^C

 

서버로부터 에코가 잘되는 것을 확인 할 수가 있습니다. 

마지막 하나의 클라이언트 쪽에서 Ctrl+C를 통해서 종료를 했는데, 이렇게 종료하면 읽는 쪽 server는 read시에 파일의 끝인 0을 반환받게 됩니다. 하나의 클라이언트라서 티가 안나지만 2개 이상 클라이언트가 이런 식의 종료를 하게 되면 볼 수 있습니다.

//한쪽에서 fifo를 닫으면 파일끝을 만나게 된다. 
if(n == 0) {
        printf("file end\n");
        norm_exit();
}

그럴 경우 서버도 종료하게 하였고, 서버쪽에서도 역시 SIGINT로 종료할 수 있게 하였습니다.  종료시에는 fifo 파일을 삭제해주는 코드를 추가해서 이 다음에 서버가 실행이 될 때 이미 파일이 존재한다는 오류를 방지하도록 합시다. 

void norm_exit(){
        unlink(TOSERVER);
        unlink(TOCLIENT);
        exit(0);
}

 

fifo를 통해서 단순히 하나의 클라이언트 뿐만 아니라 여러 클라이언트들도 접속이 가능합니다. 

./server
# ./server 
server start
[read message ]  hello ! i'm first client!

[read message ] hello i'm second client

[read message ] good bye~~

file end
./client ./client
# ./client 
message : hello ! i'm first client!
[read message ]  hello ! i'm first client!

message :good bye~~
[read message ] good bye~~

message :^C
# ./client 
message :hello i'm second client
[read message ] hello i'm second client

message :^C

 

반응형
블로그 이미지

REAKWON

와나진짜

,