tip1. 상위 프로세스와 하위 프로세스 개념
이전 섹션에서 다룬 소켓의 개념과 객체 생성 방법에서 몇 가지 중요한 개념을 지나쳐서 이번 섹션에서 확실히 다뤄 보겠다. TCP 방식에 기반한 서버와 클라이언트를 구현할 때, 3 way-handshaking이라는 3단계 연결 설정을 고려해야 했다. 파이썬 소스코드로 구현할 때 서버 측에서는 어떤 함수를 사용해야 하고, 클라이언트 측에서는 어떤 함수를 사용해야 하는지 먼저 알아야 한다.
그리고 3단계 연결 이전과 이후를 구분해 3단계 연결 이전에는 부모 프로세스가 일련의 과정을 처리하지만, 그 이후에는 자식 프로세스가 일련의 과정을 처리한다는 점도 기억해야 프로세스가 따로 노는 것(고아 프로세스로 전락)을 막을 수 있다.
정리하면,
- 서버 측에서는 listen() 함수와 accept() 함수를 통해 3단계 연결과정 구현
- accept() 함수 실행까지는 부모 프로세스의 소켓 객체가 일련의 작업을 수행
- accept() 함수 실행 이후부터는 자식 프로세스의 소켓 객체가 일련의 작업을 수행
- 클라이언트 측에서는 connect() 함수를 통해 구현
따라서 소켓 객체 종료도 자식 프로세스의 소켓 객체를 종료한 뒤 부모 프로세스의 소켓 객체를 종료해야 한다. 만약 부모 프로세스의 소켓 객체를 먼저 종료하면, 자식 프로세스의 소켓 객체는 '고아 프로세스'로 전락하기 때문에 종료 순서에 특히 유념할 필요가 있다(오동진, 2020).
먼저 프로세스는 쉽게 말해 윈도로 치면 실행 중인 프로그램이라고 생각하면 된다. 리눅스에서는 두 가지 목적으로 프로세스를 생성하는데, 하나는 같은 프로그램의 처리를 여러 개의 프로세스가 나눠서 처리하는 경우('시분할' 처리로 이를테면, 웹서버처럼 리퀘스트가 여러 개 들어왔을 때 동시에 처리해야 하는 경우)이고 다른 하나는 전혀 다른 프로그램을 생성하는 경우로 예를 들어 bash로부터 각종 프로그램을 새로 생성하는 경우이다(타케우치, 2019).
아래는 fork() 함수를 사용하여 기존의 프로세스를 나눠서 처리하는 첫 번째 목적에 해당하는 도식도이다. fork() 함수를 실행하면 실행한 프로세스(Process A)와 함께 새로운 프로세스(Process B)가 1개 복제된다. 이때 복제 전의 프로세스를 부모 프로세스(Parent Process)라 번역하고, 새롭게 생성된 프로세스를 자식 프로세스(Child Process)라고 번역한다.
그림만 언뜻 보고는 개념의 이해가 헷갈릴 수 있다. 노란색 박스는 메모리를 의미하며, 먼저 fork()를 실행시켜 부모 프로세스(Process A)가 실행 중이다. 그리고 이 프로세스를 복사시켜(fork() 함수를 통해 실행한) 자식 프로세스(Process B)를 생성했다. 이후에 기존의 부모 프로세스의 메모리는 fork()로부터 복귀하고 자식 프로세스의 메모리는 fork()로부터 리턴한다(fork() 함수를 리턴(반환)할 때 부모 프로세스는 자식 프로세스의 프로세스 ID를, 자식 프로세스는 0을 리턴한다).
프로세스 B는 여기서 프로세스 A에 종속(부모 자식 관계)되어 있기 때문에 프로세스 B는 부모 프로세스 격인 프로세스 A로부터 상속받을 수 있다는 개념이 자바의 개발과정에서 언급된 것이고, 위의 도식에서는 프로세스 B가 종료된 것(프로그램 종료)을 모르고 wait() 함수를 호출하여 이제는 종속된 프로세스(자식 프로세스)가 없음에도 불구하고 기다리게 됨으로써 기존의 프로세스 B(자식 프로세스 ID)가 '좀비 프로세스'로 전락하게 되는 것을 보여준다.
이와 달리 저번 섹션에서 구현한 아래 TCP 소켓 서버 구현 소스 코드에서도 parent(부모 프로세스 역할)의 생성 후 새로 만든 child(자식 프로세스 역할)를 볼 수 있고, child 프로세스가 먼저 종료되고 parent 프로세스가 종료되어야 정상적인 프로세스 종료가 이루어진다. 만약 부모 프로세스의 소켓 객체를 먼저 종료하면, 자식 프로세스의 소켓 객체는 '고아 프로세스(Orphan process)'로 전락하기 때문에 종료 순서에 특히 유념할 필요가 있다(오동진, 2020).
#!/usr/bin/env python3
import socket
host = "127.0.0.1" # "localhost"와 동일
port = 12345 # 0~1023 까지는 well-known(잘 알려진 포트)로 이미 등록되어 있을 수 있으므로
# 1024 이상의 포트 No. 를 할당하면 잘 알려진 포트 No. 를 통해 접속을 시도하려
# 는 공격의 취약점으로 노출되지 않음.
parent = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
# TCP/IP 기반의 소켓 객체 parent를 생성함. 이때, 객체 parent는 부모 프로세스임.
parent.setsockopt(socekt.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 1은 socket.IPPROTO_TCP 인자를 사용하겠다는 의미와 동일하다, 세부 설정 인자인
# socket.IPPROTO_TCP를 넣거나 1을 넣지 않아도 동일하다.
parent.bind((host, port))
parent.listen(10)
# listen() 함수는 UDP 서버에는 없다. 3단계 연결 설정에 따라 동작하는 TCP 클라이언트로부터 연결 요청# 을 기다리겠다는 뜻임. 이때 listen() 함수의 인자 10이 의미하는 바는 10대의 TCP 클라이언트를 대상으# 로 동시 접속이 가능하다는 의미임.
(child, address) = parent.accept()
# accept() 함수 역시도 UDP 소켓 서버에는 없음. accept() 함수는 3단계 연결을 수행한 뒤 새로운 소켓
# 객체 child와 주소(IP 주소와 포트 번호)를 튜플 타입으로 반환함. 이때 accept() 함수 실행까지는 부모 # 프로세서인 소켓 객체 parent가 일련의 작업을 수행하고 accept() 함수 실행 이후부터는 자식 프로세스
# 인 소켓 객체 child가 일련의 작업을 수행한다고 말할 수 있음. 참고로 이와 같은 방식을 '다중 할당 방
# 식'이라고 함. 튜플을 생성시키는 간결한 언어 문법 등이 파이썬에서 가능하기 때문에 해킹 프로그래
# 밍에서도 python 이 많이 쓰일 수 있는 것이다.
print(parent)
print(child)
child.close()
# 자식 프로세스의 소켓 객체를 종료함.
parent.close()
# 부모 프로세스의 소켓 객체를 종료함.
실행결과
참조
1) 오동진, 박재유. (2020). 모의 침투 입문자를 위한 파이썬 3 활용 (pp. 75-77). n.p.: 에이콘.
2) 다케우치 시토루. (2019). 실습과 그림으로 배우는 리눅스 구조 (pp. 46-47). n.p.: 한빛미디어.