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

와나진짜

,

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

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

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

reakwon.tistory.com

 

파이프(Pipe)

파이프(Pipe)란 프로세스간 통신을 할때 사용하는 커뮤니케이션의 한 방법입니다. 가장 오래된 UNIX 시스템의 IPC로 모든 유닉스 시스템이 제공합니다. 하지만 두가지 정도의 한계점이 있습니다.

 

첫번째 한계점으로 파이프는 기본적으로 반이중 방식입니다. 물론 전이중 방식을 지원하는 시스템이 있긴 하나, 최대의 이식성을 위해서는 파이프는 반이중 방식이라는 생각을 해야합니다. 이것은 FIFO라는 명명된 파이프로 극복할 수 있습니다.

두번째 한계점으로는 부모, 자식 관계에서의 프로세스들만 사용할 수 있습니다. 부모프로세스가 파이프를 생성하고, 이후 자식 프로세스와 부모프로세스가 파이프를 이용하여 통신합니다.

 

이러한 한계점이 잇긴 하지만 여전히 쓸모있는 IPC기법입니다.

 

파이프는 unistd.h 헤더파일이 존재합니다.

 

#include <unistd.h>
int pipe(int fd[2]);

pipe함수가 성공적으로 호출되었다면 0, 실패했을 경우 -1을 반환합니다.

인자 fd는 2개의 원소가 있는 배열이라는 점을 주목합시다. 2개의 원소를 쓰는 이유가 있습니다. 아래의 그림을 보면서 이해합시다.

 

 

파이프는 커널영역에 생성되어 파이프를 생성한 프로세스는 파일 디스크립터만 갖고 있게 됩니다. 여기서 파일디스크립터 fd[1]은 쓰기용 파이프, fd[0]은 읽기용 파이프입니다. 그러니 우리가 만약 데이터를 fd[1]에 쓰게 되면 fd[0]으로 그 데이터를 읽을 수 있는 것입니다.

 

그렇다면 자식 프로세스를 하나 더 두어서 자식과 부모가 통신할 수 있게 하려면 어떻게 해야할까요? 우선 자식 프로세스를 fork하면 파일 디스크립터는 부모의 파일디스크립터를 자식이 그대로 사용할 수 있는 것을 활용합니다. (파일디스크립터가 그대로 자식프로세스에 복제됩니다.)

부모프로세스는 파이프에 데이터를 쓰는 프로세스, 자식 프로세스는 그 파이프에서 데이터를 읽는 프로세스로 설계합시다.

 

 

우선 부모 프로세스에서 파이프를 생성하면 파이프에 데이터를 쓸것이기 때문에 읽기 파이프는 닫습니다. fd[0]이죠? 그런 후 fd[1]에 데이터를 씁니다.

자식 프로세스는 쓰기 파이프는 쓰지 않으므로 fd[1]을 닫고, 읽기 파이프로 데이터를 읽습니다.

 

다음은 그런 기능을 하는 코드입니다.

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fd[2];
        pid_t pid;
        char buf[MAX_BUF];

        if(pipe(fd) < 0){
                printf("pipe error\n");
                exit(1);
        }
        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fd[READ]);
                strcpy(buf,"message from parent\n");
                write(fd[WRITE],buf,strlen(buf));
        }else{  //child process
                close(fd[WRITE]);
                read(fd[READ],buf,MAX_BUF);
                printf("child got message : %s\n",buf);
        }
        exit(0);
}

 

결과는 아래와 같습니다.

 

child got message : message from parent

 

자식 프로세스에서 부모 프로세스가 pipe에 쓴 데이터를 읽었습니다. 

또는 자식프로세스가 데이터를 쓰고, 부모프로세스가 데이터를 읽는 설계도 가능하겠죠.

 

 

그렇다면 부모 프로세스와 자식 프로세스가 읽기, 쓰기가 가능하게 구현하려면 어떻게 해야할까요? 파이프를 한개만 사용한다고 해봅시다.

 

그리고 이런 상황을 가정해보지요.

1. 먼저 부모프로세스가 파이프에 fd[1]로 데이터를 보냅니다. 

2. 그 이후 자식 프로세스가 부모 프로세스가 쓴 데이터를 fd[0]으로 읽습니다.

3. 자식 프로세스는 바로 fd[1]로 파이프에 응답값을 보냅니다.

4. 부모 프로세스는 fd[0]으로 자식 프로세스가 보낸 응답값을 읽습니다.

 

결론을 말씀드리면 항상 위의 상황은 발생하지 않습니다. 그 이유는 누가 먼저 파이프를 읽느냐에 따라서 결과가 달라지는데, 만일 부모프로세스가 파이프에 쓰고, 자식 프로세스가 그 데이터를 읽기도 전에 부모프로세스가 먼저 데이터를 읽는다면 파이프에 데이터는 없겠죠. 허나 자식 프로세스는 없는 데이터를 계속 읽기만 기다리고 있기 때문에 프로그램이 망하게 되는 겁니다.

 

이때는 파이프를 2개 사용해야합니다.

fdA와 fdB 2개 사용합니다. 부모프로세스는 자식에게 쓰기용으로 fdA[1], 자식프로세스로부터 읽기용으로 fdB[0]만 있으면 됩니다. 필요없는 fdA[0], fdB[1]은 닫아줍니다.

그리고 자식프로세스는 부모프로세스로부터 읽기용으로 fdA[0], 쓰기용으로 fdB[1]만 있으면 되지요. 역시 필요없는 fdA[1], fdB[0]은 닫아줍니다.

이제 이런 개념으로 코드를 구현합시다.

 

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fdA[2],fdB[2];
        pid_t pid;
        char buf[MAX_BUF];
        int count=0;

        if(pipe(fdA) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if(pipe(fdB) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fdA[READ]);
                close(fdB[WRITE]);
                while(1){
                        sprintf(buf,"parent %d",count++);
                        write(fdA[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdB[READ],buf,MAX_BUF);
                        printf("parent got message : %s\n",buf);
                        sleep(1);
                }
        }else{  //child process
                close(fdA[WRITE]);
                close(fdB[READ]);
                count=100000;
                while(1){
                        sprintf(buf,"child %d",count++);
                        write(fdB[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdA[READ],buf,MAX_BUF);
                        printf("\tchild got message : %s\n",buf);
                        sleep(1);
                }
        }
        exit(0);
}

 

 

부모 프로세스는 0부터 1초마다 증가한 값을 파이프에 쓰고, 자식 프로세스로부터 파이프로 읽습니다. 자식 프로세스는 100000부터 증가한 값을 1초마다 쓰고, 읽습니다. 그 결과는 아래와 같습니다.

        child got message : parent 0
parent got message : child 100000
        child got message : parent 1
parent got message : child 100001
        child got message : parent 2
parent got message : child 100002
        child got message : parent 3
parent got message : child 100003
        child got message : parent 4
parent got message : child 100004
        child got message : parent 5
parent got message : child 100005

       

부모 자식 관계의 프로세스가 아닌 별개의 프로세스가 통신할때는 아까 위에서 말씀드린 것 처럼 FIFO를 사용해야합니다. 

 

파이프를 이용한 IPC구현, 이제 어렵지 않겠죠?

 

반응형
블로그 이미지

REAKWON

와나진짜

,