프로세스 간 양방향 통신

전창우·2024년 12월 14일

system programming

목록 보기
9/9
post-thumbnail

프로세스 간 양방향 통신을 위해 pipe, fork, dup2, fdopen 등의 함수에 대해 알아보고 양방향 통신 계산기 프로그램을 구현해보자.

pipe

pipe func은 보통 부모 프로세스와 자식 프로세스간의 '통신'을 위해 사용된다.
unistd.h 의 함수 중 하나인 pipe func의 동작은 사실 크게 복잡하지 않다.

#include <unistd.h>
int thepipe[2];
int result = pipe(thepipe);

pipe func의 argument로는 2의 크기를 가지는 int array가 들어간다. 이때 pipe func는 단순히, file descriptor 2개를 생성하여 array로 반환하는 역할을 한다.

이 file descriptor 2개로 어떻게 통신을 할 수 있을까?

부모프로세스와 자식프로세스

부모 프로세스가 fork func을 통해서 자식 프로세스를 복제한다고 하였을 때

pid_t result = fork(void)

부모 프로세스가 pipe func으로 생성한 file descriptor(thepipe)를 자식 프로세스도 참조하게 된다. 다시 말해서, 부모-자식 프로세스가 동일한 파이프를 참조하도록 설정되는 것이다.

여기서 자식 프로세스가 array(thepipe)에 저장된 두 개의 file descriptor 중에 thepipe[1] 을 통해서 해당 file descriptor의 buffer에 데이터를 보낼 수 있고,
부모 프로세스는 thepipe[0]을 통해서 buffer에 있던 데이터를 읽을 수 있게된다.
buffer에 데이터를 보낸다는 것을 반드시 기억하자

stdin이 0번 fd, stdout이 1번 fd란 것과 연결하여 암기하면 좋다

이러한 과정을 통해서 두 프로세스 간 통신이 이루어지게 되는 것이다.

부모프로세스와 자식프로세스를 구분하여 동작시키는법

fork func을 사용하여 자식 프로세스가 복제된 상황에서, 어떻게 두 프로세스를 구분하여 동작을 관리할 수 있을까? 방법은 생각보다 간단하다.

#include<unistd.h>
int main()
{
    if ((pid = fork()) == -1) {
        fprintf(stderr, "fork err\n");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) { // child work
        be_dc(to_dc, from_dc);
    }
    if (pid != 0) { // parent work
        be_bc(to_dc, from_dc);
        wait(NULL); 
    }
}

부모 프로세스의 입장에서, fork func의 return 값은 복제된 자식 프로세스의 pid이다.
하지만, 자식 프로세스의 입장에서 fork func의 return 값은 0이다.
이 fork func의 return 값을 통해서 위와 같이 부모-자식 프로세스를 구분하여 각기 다른 동작을 시행할 수 있게된다.

단, fork func이 프로세스를 복제하는데 실패했을 경우에는 -1을 return하게 된다.

pipe 사용시 주의할 점

  1. 사용자가 임의로 쓰기, 읽기 file descriptor를 조정해서는 안된다.
    thepipe는 0번째 Index에는 읽기 전용 file descriptor이고, 1번째 Index에는 쓰기 전용 file descriptor로 사용되기 때문이다

  2. 부모-자식 프로세스가 동일한 file descriptor를 사용해서는 안된다.
    자식 프로세스를 fork func을 통해서 생성하였을 때, 부모-자식 프로세스 간 동일한 file descriptor를 참조하는 것은 사실이지만, 자신의 프로세스에서 사용하지 않을 file descriptor는 close를 해주어야만 읽기/쓰기 동작이 정상적으로 작동한다.

부모-자식 프로세스 간 통신을 위한 설정

부모-자식 프로세스 간 통신을 위해서 필요한 설정은 아래와 같다.
1. 프로세스에서 사용할 file descriptor를 읽기 or 쓰기로 설정
2. 프로세스에서 사용하지 않을 file descriptor를 close하기

1.1. dup2 func를 이용하여 세팅하기

fdopen을 사용하여 프로세스에서 사용할 file descriptor를 세팅하는 것이 비교적 직관적이고, 간단하지만.
dup2 func를 이용하여 file descriptor 세팅이 어떻게 이루어지는 지 살펴볼 필요는 있다.

#include <unistd.h>
newfd = dup2(oldfd, newfd)

dup2 func은 파일 디스크립터를 복제하기 위해 사용되는데, argument는 다음과 같다.
oldfd : 복제할 fd
newfd : 덮어쓸 fd

아래 예제를 통해 더 자세히 살펴보자.

void be_dc(int to_dc[2],int from_dc[2])
{
    if(dup2(to_dc[0],0)==-1)
    {
        oops("dup2 err",3);
    }
    close(to_dc[0]);
    close(to_dc[1]);

    if(dup2(from_dc[1],1)==-1)
    {
        oops("dup2 err",4);
    }
    close(from_dc[0]);
    close(from_dc[1]);

    execlp("dc","dc","-",NULL);
    oops("cannot run dc",6);
}

be_dc func는 이후에 양방향 통신 계산기에서 자식 프로세스가 수행할 함수이다.
자식 프로세스는 to_dc pipe array를 통해서 부모 프로세스에게 데이터를 받고,
from_dc pipe array를 통해 부모 프로세스에게 데이터를 보내는 역할
을 한다.

이때 우리는 부모-자식 프로세스 간 양방향 통신을 이루도록 하기 위해서
to_dc[0]을 stdin으로, from_dc[1]을 stdout으로 덮어씌워야 한다.

여기서 '덮어씌운다'라는 것
to_dc[0]이 file descriptor 3을 가르키고 있다고 가정했을 때,
dup2(to_dc[0], 0)은 fd 3번이 참조하는 파일을 0(stdin)에 복제한다는 것이다.
이로 인해, 자식 프로세스의 stdin(0)은 to_dc[0]이 참조하는 파일을 참조하게 된다.

dup2 func을 수행하고 나서는 to_dc[0]이 참조하는 파일을 stdin(0)이 참조하고 있기 때문에 to_dc[0]을 close해줌으로써 '읽기'가 잘 수행되도록 해준다.
to_dc[1]을 close하는 이유는 해당 file descriptor는 부모 프로세스가 '쓰기'로 사용해야 하기 떄문이다.

1.2. fdopen func을 이용하여 세팅하기

#include <stdio.h>
FILE* fdopen(int fd, const char *mode); 

fdopen func는 argument로 들어가는 file descriptor를 해당 mode로 open 하여 FILE* 객체로 반환해준다.
이렇게 하면, file descriptor를 stdin/stdout으로 dup2로 덮어씌울 필요가 없어지므로 코드가 훨씬 간단해지게 된다.

아래 예제를 통해 살펴보자.

void be_bc(int to_dc[2], int from_dc[2])
{
    close(to_dc[0]);
    close(from_dc[1]);

    FILE* fpout = fdopen(to_dc[1],"w"); //이 과정을 통해서 dup 과정을 생략 가능
    FILE* fpin = fdopen(from_dc[0],"r"); 

    if(fpout == NULL || fpin ==NULL)
    {
        fprintf(stderr,"fdopen err");
        exit(EXIT_FAILURE);
    }
}

위는 부모 프로세스가 수행할 be_bc func의 일부이다. fdopen func을 사용하여 pipe를 세팅하게 되면, 단순히 해당 file descriptor를 fdopen하는 것만으로 완료된다.

단, 위의 경우에서 주의해야할 점은
자식 프로세스가 수행할 함수 be_dc의 경우에는, stdin이나 stdout을 pipe로 연결했기 때문에 scanf(), printf()를 통해 입출력이 가능한 반면,
부모 프로세스는 FILE* 객체를 통하여 file descriptor를 연결했으므로, fprintf(), fscanf()를 통해 입출력이 가능하다.

pipe를 이용한 양방향 통신

pipe를 활용하여 프로세스 간 통신을 하는 방법을 알아보았다.
그렇다면, 프로세스 간 양방향 통신은 어떻게 이루어지는 지 살펴보자.

우선 양방향 통신을 위해서는 2개의 pipe array가 필요하다. 이유는 다음과 같다.

양방향 통신을 위해서는 각각의 프로세스가 읽기 fd와 쓰기 fd를 가져야 한다.
부모 프로세스가 bc, 자식 프로세스가 dc일 때,
부모 프로세스는 to_dc[1]을 통해 buffer에 데이터를 보내고,
자식 프로세스는 이 buffer에 저장된 데이터를 to_dc[0]을 통해 받을 수
있게 된다.

그리고,
자식 프로세스는 from_dc[1]을 통해 부모 프로세스에게 데이터를 보낼 수 있고,
부모 프로세스는 이 데이터를 from_dc[0]을 통해 받을 수
있게 되는 것이다.

하나의 pipe로 양방향 통신이 이루어지지 못하는 이유는 충돌이 발생하기 때문이다.
to_dc[1]을 통해 부모 프로세스와 자식 프로세스가 데이터를 보내고,
to_dc[0]을 통해 부모 프로세스와 자식 프로세스가 데이터를 받는 게 가능하다고 생각된다면, 다시 처음부터 읽어보도록 해라.

양방향 통신을 활용한 리눅스 시스템 계산기(bc) 구현

bc : 부모 프로세스에서 수행할 함수. stdin으로 받은 수식을 parshing 하여 자식 프로세스에게 보내고, 자식 프로세스가 연산을 수행한 것을 받아 출력
dc : 자식 프로세스서 수행할 함수. 적절히 parsging된 데이터를 부모 프로세스에게 받아 dc를 실행시켜 연산하고, 연산 결과를 부모 프로세스에게 보냄.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define oops(m, x) {perror(m); exit(x);}

void be_bc(int* to_dc, int* from_dc);
void be_dc(int* to_dc, int* from_dc);
int pid;

int main()
{
	int to_dc[2]; // 0 = 자식 프로세스(dc) in, 1 = 부모 프로세스(bc) out
	int from_dc[2]; // 0 = 부모 프로세스 in, 1 = 자식 프로세스 out

	if((pipe(to_dc))==-1)
	{
		oops("to_dc pipe err",EXIT_FAILURE);
	}
	if((pipe(from_dc))==-1)
	{
		oops("from_dc pipe err",EXIT_FAILURE);
	}
	int pid;
	if((pid = fork())==-1)
	{
		oops("fork err",EXIT_FAILURE);
	}
	if(pid == 0)
	{
		be_dc(to_dc,from_dc);
	}else{
		be_bc(to_dc, from_dc);
		wait(NULL); 
		
	}
	return 0;
}
void be_dc(int* to_dc, int* from_dc)//자식
{
	close(to_dc[1]);
	close(from_dc[0]);

	if(dup2(to_dc[0],0)==-1)
	{
		oops("dup2 err",EXIT_FAILURE);
	}
	if(dup2(from_dc[1],1)==-1)
	{
		oops("dup2 err",EXIT_FAILURE);
	}
	close(to_dc[0]);
	close(from_dc[1]);

	execlp("dc","dc","-",NULL);
	oops("dc err", EXIT_FAILURE);

}
void be_bc(int* to_dc, int* from_dc) //부모
{
	close(to_dc[0]);
	close(from_dc[1]);
	
	FILE* in = fdopen(from_dc[0],"r");
	FILE* out = fdopen(to_dc[1],"w");

	if((in == NULL)||(out == NULL))
	{
		oops("fdopen err",EXIT_FAILURE);
	}
	char msg[BUFSIZ];
	int num1;
	int num2;
	char oper;
	
	int result;
	while(1)
	{
		printf("계산식 : ");
		if(fgets(msg,BUFSIZ,stdin)!=NULL)
		{
			if(sscanf(msg, "%d %c %d",&num1,&oper,&num2)!=3)
			{
				printf("syntax err : [num1] [oper] [num2]\n");
				continue;
			}
			fprintf(out, "%d\n%d\n%c\np\n",num1,num2,oper);

			fflush(out);

			if(fgets(msg, BUFSIZ, in)==NULL)
			{
				break;
			}
			printf("%d %c %d = %s",num1,oper, num2, msg);
		}
	}
	fclose(in);
	fclose(out);
}

0개의 댓글