개요


오늘은 프로세스에서 중요한 운영체제 몇가지 시스템 콜에 대해 알아보겠습니다.

아래의 개념을 알아야 운영체제의 기본 동작과 sh 프로그램을 이해할 수 있습니다.

예제에서는 예외처리를 하지 않았으니, 실제로 사용하실 때는 예외처리를 엄격하게 해주세요.

자식 프로세스 생성 (fork)


1
2
3
4
#include <unistd.h>

pid_t fork(void);

설명


새로운 프로세스를 생성하는 시스템 콜 입니다. 새롭게 생성된 자식 프로세스는 새로운 PID를 갖게되며 호출한 부모 프로세스를 그대로 복사합니다. 복사를 통해 자식 프로세스는 부모와 완전히 독립된 물리 메모리 공간을 갖습니다.

단, 파일 디스크립터는 동작이 다릅니다.

자식 프로세스는 부모 파일디스크립터의 복사본을 갖습니다. 이 디스크립터들은 같은 오브젝트를 참조하므로 후속 읽기 또는 쓰기에 영향을 미칠 수 있습니다.

리턴 값


  • 자식: 0
  • 부모: 자식의 PID
  • 에러: -1 리턴하고 errno 설정

예제


예제 1

예제 2

fork를 하게 될 경우 가상 주소 공간(주소 레지스터 값)을 그대로 복사합니다.

주소 레지스터 값이 같지만 서로 다른 값이 대입 되었습니다.

왜냐하면 주소 레지스터 값이 같다고 해서 실제 물리 메모리 주소가 같지 않기 때문입니다.

OS에서는 프로세스마다 각자의 페이지 테이블(Page Table)이 존재하고 MMU라는 하드웨어의 도움을 받아 실제 물리 메모리 주소를 찾습니다.

이러한 방식을 가상 주소 공간(virtual address space)이라고 합니다.

자식 프로세스의 종료 대기 (wait)


1
2
3
#include <sys/wait.h>

pid_t wait(int *stat_loc);

설명


자식 프로세스가 종료되기 전까지 부모 프로세스를 기다리게 하는 시스템 콜 입니다.

좀비 프로세스를 회수하고 고아 프로세스가 되는 것을 방지합니다.

리턴 값


  • 성공: 종료된 자식 프로세스의 PID 리턴
  • 실패: -1 리턴

stat_loc의 구조


상황 하위 8bits 상위 8bits
자식프로세스가 exit를 호출 exit()의 인자 0x00
자식프로세스가 시그널에 의해 종료 0x00 1bit (dump) 7bit (signal no)
자식 프로세스가 SIGSTP, SIGSTOP에 의해 잠시 중단 8bit (signal no) 0x7f

stat_loc 검사 (매크로)


매크로 내용
WIFEXITED 자식 프로세스가 정상적으로 종료되었으면 참
WEXITSTATUS exit()의 인자에서 하위 8비트 값을 리턴
WIFSIGNALED 자식 프로세스가 시그널을 받았으나 그것을 처리하지 않아 비정상적으로 종료되었으면 참
WTERMSIG 시그널 번호를 리턴
WIFCOREDUMP 코어 파일이 생성된 경우에 참
WSTOPSIG 실행을 일시 중단시킨 시그널 번호를 리턴

예제


예제3

예제4

exit 함수를 호출하면 어떤 종료 코드를 사용하던 “정상 종료”가 됩니다. WIFEXITED 매크로로 정상 종료 여부를 확인 할 수 있고, WEXITSTATUS 매크로로 종료 코드를 확인 할 수 있습니다.

exit 이외에 다른 방법으로 프로세스가 종료될 경우 “비정상 종료”로 취급 됩니다. WIFSIGNALED로 비정상 종료 여부를 확인할 수 있고, WTERMSIG로 시그널 번호를 확인 할 수 있습니다.

좀비 프로세스와 고아 프로세스


  • 좀비 프로세스: 프로세스가 종료되면 모든 메모리가 회수 되지 않고 일부가 남아 있는 좀비 프로세스가 됩니다. 부모 프로세스가 wait() 시스템 콜을 호출해주지 않으면 계속 좀비 프로세스 상태로 남습니다.
  • 고아 프로세스: 자식 프로세스가 종료되기 전에 부모 프로세스가 먼저 종료될 경우 고아 프로세스가 됩니다. 따라서 자식 프로세스를 생성한 경우에 반드시 wait() 시스템 콜을 호출하는 것이 바람직합니다.

프로세스 변환 (exec류 함수)


1
2
3
#include <unistd.h>

int execve(const char *pathname, char *const argv[], char *const envp[]);

설명


보통 fork()로 자식 프로세스를 생성하고 자식 프로세스에서 exec류 함수를 호출합니다.

exec류 함수의 종류는 여러 가지가 있지만 execve() 시스템 콜(2)을 제외하고 나머지 함수들은 라이브러리 함수(3) 입니다.

exec류의 라이브러리 함수는 여기를 참고해주세요.

sh 계열의 프로그램들이 fork 이후에 자식 프로세스에서 exec()를 호출하는 방식으로 구현 합니다.

자식 프로세스를 만드는 이유는 두 가지입니다.

첫번째는 sh 프로세스가 직접 exec()를 호출하면 완전 다른 프로세스로 변환되기 때문에 프로세스 유지가 불가능합니다.

두번째는 sh 프로세스를 유지함으로서 파이프(||)나 리디렉션(>, >>, >&, <) 등을 처리하기 위함입니다.

리턴 값


  • 성공: 리턴하지 않음
  • 에러: -1 리턴하고 errno 설정

예제


예제5

예제6

파일 디스크립터 복제 (dup, dup2)


1
2
3
4
5
#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

설명


dup() 시스템 콜은 oldfd 파일 디스크립터의 복사본을 생성합니다. 사용되지 않는 파일 디스크립터 값 중에 가장 낮은 숫자를 새로운 디스크립터 값으로 사용합니다.

dup2() 시스템 콜은 oldfd의 복사본을 만드는 것은 동일하나 직접 newfd를 지정할 수 있습니다. 이미 open된 fd 값을 사용할 수 있습니다.

리턴 값


  • 성공: 새로운 파일 디스크립터 리턴
  • 에러: -1을 리턴하고 errno 설정

예제


예제7

예제8

dup을 이용해 0(표준 입력), 1(표준 출력)을 각각 3, 4번 fd로 복사하였습니다.

복사 한 뒤에 read, write를 이용해 tty에 입력을 받고 출력을 하였습니다.

프로세스간 통신 (pipe)


1
2
3
4
5
#include <unistd.h>


int pipe(int pipefd[2]);

설명

pipe()는 프로세스간 단방향 통신을 하기 위해 데이터 채널을 만듭니다.

pipefd 배열에 각각 파일 디스크립터가 리턴됩니다.

pipefd[0]은 파이프의 읽기 끝을 나타냅니다. pipefd[1]은 파이프의 쓰기 끝을 나타냅니다. 파이프의 쓰기쪽에 기록 된 데이터는 파이프의 읽기 쪽에서 읽을 때까지 커널에 의해 버퍼링됩니다.

리턴 값

  • 성공: 0 리턴하고, pipefd 갱신
  • 에러: -1 리턴하고, errno 설정하고, pipefd는 갱신되지 않음

예제 1


예제9

예제10

자식 프로세스에서 파이프 fd[1]에 “Hi, I’m Child”를 write 했습니다.

부모 프로세스에서는 fd[0]을 read 해서 자식 프로세스에서 write한 “”Hi, I’m Child”를 읽을 수 있습니다.

위에서 언급한 dup2()를 이용하면 아래와 같이 코드를 변경할 수 있습니다.

예제 2


예제11

예제12

자식 프로세스에서 표준 출력(1)을 fd[1]로 변경해서 printf()를 호출 했을 때 내용이 fd[1]에 write 됩니다.

부모 프로세스에서 표준 입력(0)을 fd[0]로 변경해서 scanf()를 호출 했울 때 fd[0]의 내용이 read 됩니다.

출처


홍지만, 리눅스 시스템 프로그래밍 V1.0, 도서출판 그린(2019), 91p-95p, 233p-239p, 243p-251p, 253p-262p, 408p-413p
리눅스 매뉴얼

  • man 2 fork
  • man 2 wait
  • man 2 execve
  • man 2 dup
  • man 2 pipe