leebaek

[UNIX 프로그래밍] 10강 - 파이프 본문

수업 정리/UNIX 프로그래밍

[UNIX 프로그래밍] 10강 - 파이프

leebaek 2024. 12. 13. 22:25

  • pipe
  • pipe를 이용한 client-server
  • select 시스템 호출
  • fd_set 관련 매크로
  • timeval의 구조
  • select 시스템 호출
  • FIFO

pipe

- 한 프로세스에서 다른 프로세스로의 단방향 통신 채널

- write와 read로 data 통신 가능


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

 

  • fildes[0] : 읽기용
  • fildes[1] : 쓰기용

- process당 open file 수, 시스템 내의 file 수 제한됨

-> pipe 제거해줘야 함 ( file은 아니지만, file table에서 관리하기 때문 )


□ pipe의 특성

  • FIFO 처리 ( 순서 보장이 필요한 데이터 전송에 적합함 )
  • lseek는 작동하지 않음 ( 읽은 데이터는 사라지기 때문 )
  • pipe는 fork()에 의해 상속 가능 ( fildes가 자식 process에 복사되기 때문 )

□ pipe를 이용한 단방향 통신 ( 부모 -> 자식 ) 

  1. pipe 생성
  2. fork()에 의해 자식 생성 & pipe 복사
  3. 부모는 읽기용, 자식은 쓰기용 pipe를 close
#include <unistd.h>
int main() {
    char ch[10];
    int pid, p[2];

    if ( pipe(p) == -1) {
        perror("pipe call");
        exit(1);
    }

    pid = fork();
    if ( pid == 0 ) {
        close(p[1]);
        read(p[0], ch, 10);
        printf("%s\n", ch);
    }

    close(p[0]);
    scanf("%s", ch);
    write(p[1], ch, 10);
    wait(0);
    exit(0);
}

□ pipe를 이용한 양방향 통신

  1. pipe 2개 생성
  2. fork()에 의해 자식 생성 & pipe 2개 복사
  3. pipe1: 부모는 읽기용, 자식은 쓰기용 pipe를 close
  4. pipe2: 부모는 쓰기용, 자식은 읽기용 pipe를 close

- blocking read / blocking write

- read가 blocking되는 경우 : pipe가 비어 있는 경우

- write가 blocking 되는 경우 : pipe가 가득 찬 경우 ( 실패가 아니라 기다렸다가 pipe가 비면 쓰기를 수행함 )

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

int main() {
    int pipe1[2], pipe2[2];
    pid_t pid;
    char buffer[100];

    // 파이프 2개 생성
    if (pipe(pipe1) == -1 || pipe(pipe2) == -1) {
        perror("pipe");
        return 1;
    }

    // 자식 프로세스 생성
    pid = fork();

    if (pid == 0) {  // 자식 프로세스
        close(pipe1[0]);  // pipe1의 읽기 닫기
        close(pipe2[1]);  // pipe2의 쓰기 닫기

        // 부모에게 메시지 보내기
        char child_message[] = "Hello from child!";
        write(pipe1[1], child_message, strlen(child_message) + 1);

        // 부모로부터 메시지 읽기
        read(pipe2[0], buffer, sizeof(buffer));
        printf("Child received: %s\n", buffer);

        close(pipe1[1]);  // pipe1의 쓰기 닫기
        close(pipe2[0]);  // pipe2의 읽기 닫기
    } else {  // 부모 프로세스
        close(pipe1[1]);  // pipe1의 쓰기 닫기
        close(pipe2[0]);  // pipe2의 읽기 닫기

        // 자식으로부터 메시지 읽기
        read(pipe1[0], buffer, sizeof(buffer));
        printf("Parent received: %s\n", buffer);

        // 자식에게 메시지 보내기
        char parent_message[] = "Hello from parent!";
        write(pipe2[1], parent_message, strlen(parent_message) + 1);

        close(pipe1[0]);  // pipe1의 읽기 닫기
        close(pipe2[1]);  // pipe2의 쓰기 닫기
    }

    return 0;
}

□ pipe를 닫기

 

  • 쓰기 전용 pipe 닫기 : 다른 writer가 없는 경우, read를 위해 기다리던 process들에게 0을 return ( EOF와 같은 효과 )
  • 읽기 전용 pipe 닫기 : 더 이상 reader가 없는 경우, wrtier들은 SIGPIPE signal을 받음
    • signal handling이 되지 않으면 process는 종료
    • signal handling이 되면 signal 처리 후 wrtier는 -1을 return

□ non-Blocking read / non-blocking write

- 여러 pipe를 차례로 polling 하는 경우 ( 여러 파이프를 순차적으로 검사하는 폴링에서 유용 )

- 데이터가 없거나 공간이 부족하면, 작업이 대기하지 않고 바로 실패

#include <fcntl.h>
int fcntl(filedes, F_SETFL, O_NONBLOCK);

 

 

  • 쓰기 전용 파이프 (filedes가 쓰기 디스크립터):
    • 파이프가 가득 차 있으면, 쓰기 작업은 대기하지 않고 즉시 -1 반환.
    • errno는 EAGAIN으로 설정됨.
  • 읽기 전용 파이프 (filedes가 읽기 디스크립터):
    • 파이프가 비어 있으면, 읽기 작업은 대기하지 않고 즉시 -1 반환.
    • errno는 EAGAIN으로 설정됨.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    int pipe1[2], pipe2[2];
    char buffer[100];

    // 두 개의 파이프 생성
    pipe(pipe1);
    pipe(pipe2);

    // 읽기 디스크립터를 non-blocking 모드로 설정
    fcntl(pipe1[0], F_SETFL, O_NONBLOCK);
    fcntl(pipe2[0], F_SETFL, O_NONBLOCK);

    // 부모 프로세스에서 파이프 사용
    if (fork() == 0) {  // 자식 프로세스
        close(pipe1[0]);  // 읽기 닫기
        close(pipe2[0]);  // 읽기 닫기

        write(pipe1[1], "Data from pipe1", 16);  // 파이프 1에 데이터 쓰기
        sleep(1);
        write(pipe2[1], "Data from pipe2", 16);  // 파이프 2에 데이터 쓰기

        close(pipe1[1]);  // 쓰기 닫기
        close(pipe2[1]);  // 쓰기 닫기
        _exit(0);
    } else {  // 부모 프로세스
        close(pipe1[1]);  // 쓰기 닫기
        close(pipe2[1]);  // 쓰기 닫기

        // 폴링 방식으로 두 파이프 읽기
        for (int i = 0; i < 10; i++) {
            ssize_t n = read(pipe1[0], buffer, sizeof(buffer));
            if (n > 0) {
                printf("Read from pipe1: %s\n", buffer);
            } else if (errno == EAGAIN) {
                printf("Pipe1 is empty, try again later.\n");
            }

            n = read(pipe2[0], buffer, sizeof(buffer));
            if (n > 0) {
                printf("Read from pipe2: %s\n", buffer);
            } else if (errno == EAGAIN) {
                printf("Pipe2 is empty, try again later.\n");
            }

            sleep(1);
        }

        close(pipe1[0]);  // 읽기 닫기
        close(pipe2[0]);  // 읽기 닫기
    }

    return 0;
}

 

 

- 폴링 방식 데이터 읽기 (부모):

  • 두 파이프를 반복적으로 읽음
  • 데이터가 없으면 EAGAIN을 확인하고 다음 시도로 넘어감

 pipe를 이용한 client-server

□ Client는 하나의 pipe로 request를 write

□ Server는 여러 개의 pipe로부터 request를 read

 

  • 클라이언트:
    • 하나의 파이프에 요청 데이터를 씀
    • 요청이 없으면 대기하지 않고 즉시 쓰기를 시도
  • 서버:
    • 여러 클라이언트가 요청을 보낼 수 있는 여러 개의 파이프를 사용함
    • 요청이 없으면 블로킹 상태에서 대기함
    • 요청이 하나 이상 오면 요청 순서대로 읽기를 수행함

 

 select 시스템 호출

□ select system call : 읽는게 아니라 확인만 함

- 지정된 file descriptor 집합 중 어느 것이 읽기/쓰기가 가능한지 표시

- 읽기, 쓰기가 가능한 file descriptor가 없으면 blocking

- 영구적으로 blocking을 막기 위해 time out 사용 가능

 

□ select의 return 값

  • -1 :  오류 발생시
  • 0 : timeout 발생시
  • 0보다 큰 정수 : 읽기/쓰기 가능한 file descriptor의 수

- return시 mask를 지우고, 재설정해야함


□ select

#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *wrtiefds, fd_set *errorfds, struct timeval *timeout);

 

 

  • int nfds
    • 감시할 파일 디스크립터의 최대 값 + 1.
    • 예를 들어, 감시할 파일 디스크립터가 0, 1, 2라면 nfds는 3이어야 함
  • fd_set *readfds
    • 읽기가 가능한 파일 디스크립터를 감지하기 위한 집합
    • NULL일 경우 읽기 파일 디스크립터를 감시하지 않음
  • fd_set *writefds
    • 쓰기가 가능한 파일 디스크립터를 감지하기 위한 집합
    • NULL일 경우 쓰기 파일 디스크립터를 감시하지 않음
  • fd_set *exceptfds
    • 예외 상황(예: out-of-band 데이터)을 감지하기 위한 집합
    • NULL일 경우 예외 상황을 감시하지 않음
  • struct timeval *timeout
    • 타임아웃 시간을 지정
      • NULL: 영구적으로 블로킹(읽기/쓰기 가능할 때까지 대기)
      • 0초: 파일 디스크립터를 즉시 검사하고 반환

- readfds, writefds, errorfds 중 하나만 고르고, 나머진 NULL로 지정


 fd_set 관련 매크로

□ void FD_ZERO(fd_set *fdset);

- fdset 초기화

 

□ void FD_SET(int fd, fd_set *fdset);

- fdset의 fd bit를 1로 설정

 

□ void FD_ISSET(int fd, fd_set *fset);

- fdset의 fd bit가 1인지 검사

 

□ void FD_CLR(int fd, fd_set *fset);

- fdset의 fd bit를 0으로 설정


timeval의 구조

□ timeval

struct timeval {
    long tv_sec;
    long tv_usec;
}

 

- timeout이

  • NULL이면 해당 event가 발생시까지 blocking
  • 0이면, non-blocking
  • 0이 아닌 값이면, read/write가 없는 경우 정해진 시간 후에 return

■ FIFO ( 이름을 가진 파이프 )

- pipe는 동일 ancestor를 갖는 프로세스들만 연결 가능

- but! fifo는 모든 프로세스들을 연결 가능

- UNIX의 file 이름을 부여 받음

- 소유자, 크기, 연관된 접근 허가를 가짐

- 일반 file 처럼, open, close, read, wrtie, remove가 가능함


□ mkfifo() : FIFO 만들기

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

 

- pipe는 read, write 준비가 다 되어야 open 가능함 ( 그 전까지 blocking 상태 )

- open("/tmp/fifo", O_WRONLY, | O_NONBLOCK) 기본설정이 block이라 따로 non-block 설정해줘야 함

- Non-blocking open 일 경우, 상대 프로세스가 준비되지 않으면, -1 return ( errno = ENXIO )


int main(void) {
	int fd, n;
        char buf[512];

        mkfifo("fifo", 0600);

        fd = open("fifo", O_RDWR);

        for (;;) {
            n = read(fd, buf, 512);
            write(1, buf, n);
        }
}

 

-> O_RDONLY로 변경, if ( n == 0 ) printf("..."); 변경 시

서버 쪽에서 wrtie 클라이언트 대기하고 있음

그런데 fd가 RDONLY여서 wrtie해주는 클라이언트가 없으면

반복문에서 read할 경우 0으로 리턴됨

그렇기 때문에 "..."을 계속 출력하게 됨

wrtie가 종료하고 read 클라이언트가 blocking 된 채로 기다리려면 서버는 RDWR로 open 하는게 더 효율적임