뇌 마음 반반저장소

[42_pipex] 함수1. pipe, fork, wait, waitpid 자세한 설명 본문

42/pipex

[42_pipex] 함수1. pipe, fork, wait, waitpid 자세한 설명

맹진저 2023. 1. 7. 17:44
728x90

open, close, read, write, malloc, free, perror, exit, strerror, ft_printf 함수들과 libft 라이브러리는 이미 언급한 적이 있기 때문에 아래의 링크로 확인할 수 있다. 펼치기! 👇

더보기

open, close, read, write

[42_GNL] get_next_line 개념 이해하기 (open, read, 버퍼사이즈) 👉 https://sudo-me.tistory.com/24

 

malloc : 프로그래머가 할당해서 사용하는 heap영역을 지정하는 함수

 

free

[42] NULL과NUL과 Nil과 0과 \0.... 👉https://sudo-me.tistory.com/7

 

perror, exit, strerror

[42_so_long] perror, strerror, exit 자세히 알아보기 👉 https://sudo-me.tistory.com/28

 

ft_printf

https://sudo-me.tistory.com/category/sudo_42/ft_printf

 

libft 라이브러리

https://sudo-me.tistory.com/category/sudo_42/libft

아래의 용어들을 알기 위해 이전 포스트를 읽고 오는 것을 권장합니다!

[42_pipex] 탐구2. 프로세스의 과정과 부모·자식 프로세스


1. pipe

파이프는 말 그대로 파이프, 통로를 여는 함수라고 생각하면 되겠다. 

주차동 연결통로..!?

위의 그림을 보면 독자적인 여러 프로세스가 있고 메모리가 각각 할당되어 있기 때문에, 여러 프로세스가 서로 교류를 하는 것은 컴퓨터적으로(?)는 불가능하다. 하지만 이 pipe 함수를 사용하면 통로를 열어줘 부모와 자식이 서로 데이터를 주고받을 수 있다!

#include <unistd.h>
int	pipe(int fd[2]);
  • 성공 시 0을 반환하고 실패시 -1을 반환한다.
  • 이 함수는 크기가 항상 2인 함수여야만 한다. 왜냐하면 들어가는 입력 fd[0]와 출력 fd[1]이 항상 있기 때문이다! 
  • 파이프 자체는 fork함수에 의해 복사되지 않는다.
  • 파이프는 입출구가 정해져 있다. 그래서 항상 시작은 read, 입력을 의미하는 fd[0]이고 끝은 write, 출력을 의미하는 fd[1]이다.
  • 파이프를 두 개 만들어 부모가 자식에게, 자식이 부모에게 데이터를 전송할 수 있다.
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

void    main()
{
    int fd1[2];
    int fd2[2];

    pipe(fd1); //fd1의 파이프를 열어주고
    pipe(fd2); //fd2의 파이프도 열어준다.
    printf("fd1 in %d, fd1 out %d\n", fd1[0], fd1[1]);
    printf("fd2 in %d, fd2 out %d\n", fd2[0], fd2[1]);
}
$ ./a.out
fd1 in 3, fd1 out 4 //in, out이 차례로 할당되었다.
fd2 in 5, fd2 out 6

2. fork

fork의 어원 중 가지를 나눈다는 뜻이 있다고 한다. (to divide into branches) 나는 뭐 찍어서 뭘 어떻게 하나 추측했었는데 이 함수는 fork함수를 수행하는 부모 프로세스와, 동시에 함께 실행되는 자식 프로세스라는 새 프로세스를 만든다. 

#include <unistd.h>
pid_t	fork(void)
  • fork로 자식을 생성하고, 다음으로 들어오는 호출을 부모부터 자식까지 모두 명령어를 실행한다.
  • 자식 프로세스 생성에 실패하면 -1, 성공하면 자식 프로세스의 반환값은 0, 부모는 자식의 주민등록번호(?) PID가 반환된다.

pid_t라는 데이터 타입이 궁금해서 찾아봤다.

pid_t는 pid의 고유번호만 출력해주는 signed int(unsigned int/int) 타입이다! pid_t의 헤더파일을 궁금해서 찾아봤더니, 오호! 구조체가 있었다. 그래서 결국 pid_t는 구조체였다능!

 

fork의 예제를 보자.

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

int main()
{
    int parent;
    int child;
    pid_t   pid;

    pid = fork();
    if (pid > 0) //부모 프로세스
    {
        printf("Parent pid ID : %d\n", getpid()); //getpid : pid 값 가져오는 함수
    }
    else if (pid == 0) //자식 프로세스
    {
        printf("Child pid ID : %d\n", getpid());
        pid = fork(); // 또 자식 낳아버림
        if (pid > 0) //부모가 된 자식프로세스
            printf("Child as a parent pid ID : %d\n", getpid());
        else if (pid == 0) //손자 프로세스
        {
            printf("Grand child pid ID : %d\n", getpid());
        }
    }
}

결과는!

$ ./a.out
Parent pid ID : 515 #부모님
Child pid ID : 516 #자식
Child as a parent pid ID : 516 #부모가 된 자식
Grand child pid ID : 517 #손자

3. wait

위의 부모자식 프로세스 설명 그림을 보면 "부모가 자식을 종료할 수 있어요~"라고 빨간색으로 쓰여있다. 부모가 자식 프로세스를 종료하기 위해서는 wait 함수를 사용한다. 참.. 이 함수는 잔인하게도 자식이 죽을 때까지 기다려준다는 의미이다..! 그것도 아무것도 하지 않은 채... 죽음을 기다려준다.. 

#include <sys/wait.h>
pid_t	wait(int *wstatus);
  • 자식 프로세스가 정상적으로 종료한다면 : 반환값은 프로세스 pid ID, 상위 8비트에는 종료되게 한 exit함수의 인수가 기록, 하위 8비트에는 0이 저장된다.
  • 자식 프로세스가 비정상적으로 종료 한다면 : 반환값은 프로세스 pid ID, 상위 8비트에는 0이 저장되고, 하위 8비트에는 프로세스를 종료시킨 시그널의 번호가 저장된다.
  • wait 함수가 오류가 나면 :  반환값은 -1, 에러 코드 반환
  • wstatus의 값을 아는 방법은, 아래의 종료 후 상태를 반환해주는 매크로를 사용하면 된다.

WIF 뒤에 여러 가지 변형이 붙는다. 나는 이 함수의 어원을 알고 싶어서 마구 뒤졌지만 찾지 못했다..

  • WIFEXITED(status) : 자식 프로세스가 정상적으로 종료되었다면 0이 아닌 수로 반환된다. main에서 반환하면 참을 반환한다.
  • WEXITSTATUS(status) : 자식의 종료코드를 반환한다. 이 매크로는 WIFEXITED(집 나간 아내)가 참을 반환했을 때만 돌아간다. 이 함수는 실제로 무언가를 반환하지 않고 무언가를 평가하는 함수이다. 

 

  • WIFSIGNALED(status) :  자식 프로세스가 신호로 인해 종료되었다면 참을 반환한다.
  • WTERMSIG(status) : 자식 프로세스가 종료된 신호의 번호를 반환한다. 이 매크로는 WIFSIGNALED(아내의 신호)가 참을 반환한 경우에만 돌아간다.
  • WCOREDUMP(status) : 자식이 코어파일을 만든 경우에 참을 반환한다. 이 매크로는 WIFSIGNALED가 참을 반환한 경우에만 돌아간다.

 

  • WIFSTOPPED(status) : 신호 때문에 자식프로세스가 종료된 경우 참을 반환한다. 이 매크로는 WUNTRACED 옵션으로 이루어졌거나 호출이 추적되고 있는 경우에만 의미가 있다. 
  • WSTOPSIG(status) : 자식을 멈추게 한 신호의 번호를 반환한다. 이 매크로는 WIFSTOPPED(멈춘 아내)가 참을 반환하는 경우에만 돌아간다.

 

  • WIFCONTINUED(status) : 리눅스 2.6.10부터 SIGCONT 신호를 발행하여 하위 프로세스가 다시 시작된 경우 참을 반환한다.

이 함수는 예를 보는 게 더 빠르다. 위의 fork함수에서 wait 함수만 추가하였다.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    int parent;
    int child;
    int wstatus;
    pid_t   pid;
    pid_t   wait_pid;

    pid = fork();
    if (pid > 0) //부모 프로세스
    {
        wait_pid = wait(&wstatus); //wait을 걸어줬다.
        printf("Parent pid ID : %d\n", getpid());
        printf("child pid ID : %d, record of wait : %x\n", wait_pid, wstatus);
    }
    else if (pid == 0) //자식 프로세스
    {
        printf("Child pid ID : %d\n", getpid());
        pid = fork(); // 또 자식 낳아버림
        if (pid > 0) //부모가 된 자식프로세스
            printf("Child as a parent pid ID : %d\n", getpid());
        else if (pid == 0) //손자 프로세스
        {
            printf("Grand child pid ID : %d\n", getpid());
        }
    }
}

리턴값이 흥미롭다.

$ ./a.out
Child pid ID : 631
Child as a parent pid ID : 631
Grand child pid ID : 632
Parent pid ID : 630
child pid ID : 631, record of wait : 0

fork 함수 결과는 부모부터 자식으로 차례대로 내려왔는데, wait 함수를 넣어주니 결과값이 child 먼저 나오고 부모가 가장 나중에 나왔다. 이 말인즉슨 자식을 먼저 다 처리하고 부모로 온다는 뜻이군! wstatus 값은 main에서는 정상종료 되면 참이 반환되기 때문에 0으로 결과가 나온다.


4. waitpid

waitpid 함수는 wait함수보다 업그레이드된 함수이다. 하지만 이것도 자식 프로세스를 기다리고 종료상태를 받고 싶을 때 사용한다. 자식 프로세스와 부모프로세스가 함께 일하고 싶은 경우에 사용할 수 있고, 또한 기다릴 자식 프로세스를 좀 더 상세히 지정할 수 있다. 

#include <sys/wait.h>
pid_t	waitpid(pid_t pid, int *status, int options);
  • 성공 시 프로세스 ID를 반환하고, 오류가 발생하면 -1을, 그 외의 경우에는 0을 반환한다.
  • 매개변수 1. pid_t pid 
  • pid > 0 이라면? : pid ID 값의 자식 프로세스를 기다린다.
  • pid == 0 이라면? : 프로세스 그룹 ID가 호출 프로세스의 ID와 동일한 자식 프로세스를 기다린다.
  • pid < -1 이라면? : 프로세스 그룹 ID가 pid 의 절대값과 같은 자식 프로세스를 기다린다.
  • pid == -1 이라면? : 하위 프로세스를 기다린다.
  • 매개변수 2. int *status
  • wait 함수와 같은 status, 반환값을 사용한다.
  • 매개변수 3. int options
  • 0 : 옵션 없이 진행한다.
  • WNOHANG : 자식 프로세스가 종료되어있지 않으면 바로 리턴하게 해 준다. 강제종료 같은 기능.
  • WUNTRACED : 자식이 중지되면 중단된 자식 프로세스의 상태를 반환해 준다.
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main(void)
{
    int pid;
    int wstatus;
    int i;

    pid = fork();
    i = 0;
    if (pid > 0)
    {
        printf("parent waiting\n");
        waitpid(pid, &wstatus, 0);
        printf("parents end\n");
        return 0;
    }
    if (pid == 0);
    {
        while(i < 5)
        {
            i++;
            printf("%d child progressing..\n", i);
        }
        printf("child end\n");
        return(0);
    }
}
$ ./a.out
parent waiting
1 child progressing..
2 child progressing..
3 child progressing..
4 child progressing..
5 child progressing..
child end
parents end

5. 코드 합치기

부모와 자식 관계를 함수와 함께 이미지로 정리해봤다.

부모와 자식 프로세스의 진행과정!

위에서 습득한 내용을 하나의 코드로 만들어봤다!

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>

# define BUFFER_SIZE    1024

void    main()
{
    int status;
    int fd1[2];
    int fd2[2];
    char    str1[BUFFER_SIZE];
    char    str2[BUFFER_SIZE];
    pid_t   pid;

    pipe(fd1);
    pipe(fd2);
    fd1[0] = open("./file1.txt", O_RDWR | O_CREAT);
    fd2[0] = open("./file2.txt", O_RDWR | O_CREAT);
    pid = fork();

    if (pid > 0) //부모님
    {
        waitpid(pid, &status, 0); //자식먼저 일하게 해주세요.
        printf("***parents start***\n");
        printf("!file1 text to fd2!\n");
        read(fd1[0], str1, BUFFER_SIZE); //file1에 있는 txt를 읽어서
        write(fd2[1], str1, BUFFER_SIZE); //file2에 써주셈
        printf("fd2 : %s\n", str1);
        printf("***parents end***\n");
    }
    if (pid == 0) //자식쓰
    {
        printf("***child start***\n");
        printf("!file2 text to fd1!\n");
        read(fd2[0], str2, BUFFER_SIZE); //file2에 있는 txt를 읽어서
        write(fd1[1], str2, BUFFER_SIZE); //file1에 써주셈
        printf("fd1 : %s\n", str2);
        printf("***child end***\n");
    }
    close(fd1[2]);
    close(fd2[2]);
}

결과는?

$ ./a.out
***child start***
!file2 text to fd1!
fd1 : message from child: Hi, I'm from file2
***child end***
***parents start***
!file1 text to fd2!
fd2 : message from parents: Hi, I'm from file1
***parents end***

위의 코드가 이해가 되지 않는다면 아래의 이미지를 참고해보자.

꼭 이렇게 해야 이해가 되더라구요?

궁금해서 프로세스를 지정해주지 않고 작업을 해보았다.

$ ./a.out
***parents start***
!file1 text to file2!
fd2 : message from parents: Hi, I'm from file1
***parents end***
***child start***
!file2 text to file1!
fd1 : message from child: Hi, I'm from file2
***child end***

그래도 잘 나온다. 하지만 fork함수를 선언하고 부모와 자식을 나눠서 일하게 하지 않았더니 무한루프에 빠졌다.

$ fd1 : ��@`�
***child end***

 

 

후! 그럼 나머지 함수를 알아보러 가볼까!

 

728x90
Comments