컴퓨터/운영체제(주로 리눅스)

[리눅스] 프로세스 통신 FIFO(이름있는 pipe) 개념과 예제

REAKWON 2023. 7. 13. 14:03

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

 

반응형