- 0x0000007b
- 2차세계대전
- 3.20해킹
- 4대강
- 502 error
- 53빌딩
- 7840hs
- 88체육관 수영장
- ABI
- abortive close
- abortive shutdown
- AHCI
- akmods
- ALC1220
- alma linux
- alternatives
- AM4 메인보드
- AMD
- amd 7840hs
- amd 그래픽 게임용
- amd 내장 그래픽 최적화
- anonymous file
- API
- apple.com
- APT
- apt-get
- Armagnac
- Asrock
- ASTALIFT
- audacious player
- Today / Yesterday
- /
- Total
Linux Programmer
fork, vfork 그리고 posix_spawn 이야기 본문
* TOC
fork란?
fork는 유닉스/리눅스 계열에서 새로운 프로세스를 만드는 표준 함수이다. 그런데 fork는 새로운 프로세스를 만들 때 기존 프로세스를 복제하는 방식을 사용한다. 이 때 원본 프로세스를 부모 프로세스(parent process)라고 부르고 새로 복제된 프로세스를 자식 프로세스(child process)라고 부른다.
1. 부모와 자식 프로세스의 관계
현실 세계에서 부모와 자식은 끊을 수 없는 강한 연결점이 있지만, 유닉스 계열의 부모와 자식 프로세스는 그런 것과는 상관이 없이 이름이 지어졌다. 그냥 유전자가 복제되듯이 정적 자원 만을 복제하는 것이라고 생각하면 된다.
정적 자원에는 file, mask, memory등이 해당되며 복제되지 않는 것으로는 pid, ppid와 같이 프로세스 고유 정보들이 있다.
간혹 몇몇 인터넷이나 책에 부모 프로세스가 죽으면 자식 프로세스도 종료된다고 쓰여있지만 완전히 틀린 이야기다. 부모와 자식 프로세스는 별도의 공간에서 실행되므로 아무런 관련이 없다. 심지어 부모 프로세스가 부지불식간에 종료하거나 자식 프로세스가 비정상 종료한다고 해서 서로에게 문제를 발생시키는 경우는 없다. 오히려 데몬(daemon) 프로세스는 자식 프로세스가 시동되면 부모 프로세스는 일부러 죽도록 설계된 형태를 가진다.
부모 프로세스가 먼저 죽은 경우에 자식 프로세스를 고아 프로세스(orphan process)라고 부른다. 이와 반대로 자식이 먼저 죽은 경우에 부모 프로세스가 return status를 읽지 않은 경우, 즉 PCB의 meta 정보들이 해제되지 않은 경우를 defunct (zombie) process라고 부른다. 이 두가지 개념을 정확하게 알고 있어야만 한다.
그러면 왜 부모, 자식 프로세스로 분류를 해두었을까?
유닉스 계열에서 부모, 자식 프로세스 관계를 만들어 둔 것은 세션(SID)과 프로세스 그룹(PGID) 그리고 제어 터미널(Control terminal)을 통해 프로세스를 관리하기 위해서 만들어놓은 개념일 뿐이다. 이에 대한 것은 운영체제 개론 및 실습 시간에 배우기 때문에 여기서는 SID, PGID, control terminal의 힌트만 적겠다. 책을 봐도 모르겠고 비전공자라면 메일로 콕 찔러보면 답변을...
2. fork와 fork-exec
fork로 프로세스를 복제하는 목적은 2가지 유형이 있다.
첫번째는 부모 프로세스가 자식 프로세스에게 어떤 작업을 위임하는 경우다. 일반적으로 서버 프로세스가 여기에 해당한다. 아래 그림처럼 작업 태스크가 서로 의존성(dependency)이 없다면 fork를 통해 멀티태스킹을 할 수 있다. (보통 이 과정에서 부모 자식 프로세스가 데이터 교환이 필요하다면 IPC를 이용해서 통신하기도 한다.)
대표적인 예로 잘 알려진 웹서버인 아파치(apache)가 이런 방식을 사용한다. 아파치 웹서버는 처음 기동된 부모 프로세스가 fork를 통해 몇 개의 자식 프로세스를 복제해둔다. 그리고 새로운 접속은 자식 프로세스들이 받아서 처리하도록 설계되어있다. 부모는 자식 프로세스의 개수를 조절하는 일만 담당한다. 이렇게 접속이 있기 전에 미리 fork해두는 서버 형태를 pre-fork MPM(Multi Process Model) 방식이라고 부른다.
두번째는 쉘(shell)처럼 다른 프로세스를 실행시키는 경우다. 이 경우는 fork 후에 exec 계열 명령을 실행하게 된다. exec 계열 명령은 현재 프로세스의 실행 이미지를 다른 파일로 교체시키는 기능이다. exec가 실행되면 기존의 메모리는 모두 해제되고 file, mask, pid 등의 정보만 유지된채 다른 실행 파일 이미지로 교체된다. 쉽게 말해 다른 실행 파일이 작동되는 것이다. 이렇게 fork후에 exec가 연속해서 등장하는 과정을 fork-exec라고 부른다.
그런데 fork를 하면서 복제되었던 메모리가 곧바로 exec를 하면서 해제되므로 무의미한 메모리 복제 과정, 즉 오버헤드가 존재하게 된다. fork-exec의 오버헤드를 제거하려면 메모리 복제를 피하도록 설계해야만 했다. 하지만 문제는 fork후 exec 호출 할지 아닐지를 어떻게 예측할 수 있느냐가 관건이었다.
결국 예측할 수 없으므로 프로그래머의 용도에 따라 fork-exec인 경우라면, 메모리 복제를 하지 않는 새로운 함수를 쓰도록 대안 함수가 제시되었다. 그 결과 실험적인 유닉스인 BSD3에서 vfork라는 기능이 탄생되었다.
3. vfork와 문제점
vfork는 1980년 경에 BSD3에 포팅되었고, 주된 기능은 부모 프로세스의 메모리를 복제하지 않도록 하는 것이었다. 즉 부모 프로세스와 페이지 테이블을 공유하도록 설계하는 것이었다. 이를 위해 vfork는 몇 가지 제약점을 가지게 되었다.
첫째로 vfork로 만들어진 자식 프로세스가 종료하거나 exec로 교체되기 전까지 부모 프로세스가 대기(wait)상태로 만들어야만 했다. 이는 vfork에서는 부모와 자식이 페이지 테이블을 공유하므로 메모리를 변경하면 문제가 발생할 수 있기 때문이었다.
둘째로 만에 하나라도 메모리가 변경되면 어떤 일이 발생할지 알 수 없으므로 자식 프로세스는 페이지 테이블을 읽기 전용으로 접근 해야만 했다.
셋째로 스케줄러는 vfork로 만들어진 자식 프로세스가 exec를 호출하기 전에 부모, 자식 프로세스가 스케줄링 되지 않도록 조정해야만 했다. 이는 스케줄러를 비효율적으로 만드는 예외상황이기도 했다.
결과적으로 vfork는 1980년대 당시에는 혁신적이었지만 메모리 테이블 및 스케줄러에 대한 예외를 발생시키는 문제점을 가지고 있었다. 더군다나 exit 함수나 IPC자원, 쓰레드를 사용하는 환경에서는 위의 제약점을 위반시킬 수 있게 되었다. 이는 몇몇 라이브러리를 사용할 때 예기치 못한 잠재적 오류를 발생시킬 수 있었다. 표준안의 semantic은 함수가 프로세스의 상태나 조건에 따라 다르게 작동하는 예외상황을 인정하지 않으므로 vfork는 표준안에서 당연히 채택될 수 없었다.
이런 문제점 때문에 표준안이 정해지면서 vfork는 제외되었다. 과거에 vfork로 개발된 코드들도 문제를 일으키기 십상이었기 때문에 사용을 금지해야 할 정도로 유해한 함수가 되었다.
또한 성능에 대한 장점도 fork에 COW(copy-on-write) 기법이 탑재된 뒤로는 차이가 없어져서 현실적으로 vfork는 어드밴티지는 없고 부작용만 남게 되었다. 가까운 미래에 커널 내부에서조차 vfork는 퇴출거거나 호환성을 위해 남겨두지만 fork로 대체될 가능성도 있다. 1
4. posix_spawn의 등장
앞서 언급한대로 fork-exec에서 COW가 도입되고 성능적인 제약은 사라졌지만 fork-exec는 여전히 문제점을 가지고 있었다. 그것은 Signal mask나 파일, 스케줄링 정책/우선순위가 상속되는 점과 스왑영역 확보등의 문제였다. 특히 파일(or 소켓)이나 스케줄링 정책/우선순위의 상속 문제는 매우 민감하여 예기치 못한 작동을 발생시킬 소지가 있었다.
결국 POSIX 표준에서 fork-exec를 대체할 기능으로 posix_spawn을 제안하게 되었고 1999년 IEEE-1003.1d에 정식 채택되었다. (POSIX 표준에 대한 이야기는 여기에서 살펴보자)
posix_spawn은 fork와 exec가 1개의 함수 내에서 행해지도록 되어있다. fork만 따로 했을 때 생기는 스왑 영역을 확보하거나 스케줄링, 파일기술자들을 상속하는 오버 헤드 부분이 생략되므로 저성능 임베디드 시스템이나 실시간 시스템에서 좀 더 나은 성능을 보여준다. 2 물론 임베디드가 아닌 경우에도 큰 장점이 된다.
문제는 1999년에 채택된 표준임에도 불구하고 15여년이 지난 지금까지도 posix_spawn을 사용하지 않는 구식 코드들이 많다. 이는 보수적인 유닉스 업계의 문제도 있지만 가장 걸림돌은 posix_spawn을 소개하지 않는 시스템 프로그래밍 책들도 문제다. 실례로 최근에 졸업한 신입사원들을 교육시키다 보면 posix_spawn에 대해 배우지 않았다고 한다. 이 외에 SUS 표준에 새로 추가된 IPC나 동기화, 쓰레드 기능들을 모르는 사원들이 태반이다.(심지어 SUS가 뭔가요?하는 사람들도...) OTL~
사진처럼 대학 강의에서 필기가 사라져가고 스마트 폰으로 촬영할 정도로 시대는 변해가고 있다. 그런데 15년 전에 나온 표준 함수인 posix_spawn 같은 기능조차 배우지 않는 것은 좀 심하다는 생각이 든다.
IT업계는 자꾸 변화하고 있다. 변화에 뒤쳐지지 않으려면 새로운 기능을 배워야만 한다. 유닉스 업계는 몇 년에 한 번 정도 새로운 기능이 추가될 정도로 변화가 느린 편이다.(자바나 다른 언어에 비하면 엄청 발전이 느리다.) 그럼에도 불구하고 몇 년에 한번씩 나오는 새로운 기능조차 습득하지 않는다면 게으른게 아닐까?
5. 결론 요약 (2줄 요약)
vfork는 쓰지 말자.
지금 fork-exec를 코딩하고 있다면 posix_spawn으로 바꿔보자. - 끝 -
참조
'컴퓨터 관련 > 프로그래밍 일반' 카테고리의 다른 글
오류를 잡자 : TCP에는 우아한 종료라는 것은 없다. (15) | 2020.02.02 |
---|---|
dart : Invalid UTF8 sequence encountered (0) | 2019.08.24 |
TCP의 TIME_WAIT를 없애는 법 (29) | 2013.04.15 |
리눅스에서 XSI와 POSIX 메시지큐 비교 (0) | 2012.10.22 |
TCP/IP 소켓 프로그래밍 주의할 점 (5) | 2012.10.16 |
gettimeofday를 대체하는 clock_gettime 함수 (17) | 2012.09.25 |
위키의 TS와 reentrant 관련 오류가 삭제되었다 (0) | 2011.08.01 |
네이버 나눔 글꼴 (개발용 고정폭 폰트) (0) | 2010.11.02 |