iBetter Books
수정

kqueue 맛보기

epoll이 리눅스의 무기라면, kqueue는 macOS와 BSD 계열의 무기입니다. 둘은 이름과 사용법이 다르지만 발상은 똑같습니다. 준비된 소켓만 알려 준다는 것입니다. 이 장은 짧게 kqueue를 맛보며, 같은 아이디어가 다른 운영체제에서 어떻게 구현되는지 확인합니다. 앞 장의 epoll을 macOS에서 실행할 수 없었던 독자라면, 이 장의 코드는 직접 돌려 볼 수 있습니다.

이벤트와 필터라는 개념

kqueue의 사용법은 epoll과 비슷하면서도 용어가 다릅니다. 핵심 개념은 케벤트입니다. "어떤 소켓의, 어떤 일을, 지켜보겠다"는 하나의 관심사를 케벤트 하나로 표현합니다. 예를 들어 "이 소켓에 읽을 데이터가 생기는 것을 지켜보겠다"가 하나의 케벤트입니다.

여기서 어떤 일을 지켜볼지를 정하는 것이 필터입니다. 읽기를 지켜보는 필터, 쓰기를 지켜보는 필터 등이 있습니다. 우리는 주로 읽기 필터를 씁니다. epoll의 EPOLLIN에 해당합니다. 그리고 그 케벤트를 추가할지 뺄지를 함께 지정합니다. 추가 표시와 함께 등록하면 지켜보기 시작하고, 나중에 뺄 수도 있습니다.

kqueue 에코 서버

kqueue로 에코 서버를 만듭니다.

# 새 파일: kqueue_server.pyimport socketimport selectserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server.bind(("127.0.0.1", 9000))server.listen()server.setblocking(False)kq = select.kqueue()event = select.kevent(server.fileno(), select.KQ_FILTER_READ, select.KQ_EV_ADD)kq.control([event], 0)connections = {server.fileno(): server}print("kqueue 에코 서버 대기 중")while True:    events = kq.control(None, 16, None)    for ev in events:        fd = ev.ident        s = connections[fd]        if s is server:            conn, addr = s.accept()            conn.setblocking(False)            kq.control([select.kevent(conn.fileno(), select.KQ_FILTER_READ, select.KQ_EV_ADD)], 0)            connections[conn.fileno()] = conn        else:            data = s.recv(1024)            if data:                s.sendall(data)            else:                connections.pop(fd)                s.close()

select.kqueue()로 kqueue를 만듭니다. epoll의 감시 본부에 해당합니다. select.kevent(server.fileno(), select.KQ_FILTER_READ, select.KQ_EV_ADD)로 듣기 소켓에 대한 읽기 관심사를 하나 만듭니다. 파일 디스크립터 번호, 읽기 필터, 추가 표시를 묶은 것입니다. 그리고 kq.control([event], 0)으로 이 관심사를 등록합니다.

kq.control(None, 16, None)이 epoll의 기다리기에 해당합니다. 준비된 이벤트를 최대 16개까지 받아 옵니다. 돌려받은 각 이벤트의 ident가 준비된 소켓의 파일 디스크립터 번호입니다. 그래서 epoll 때와 마찬가지로 connections 딕셔너리로 번호와 소켓을 짝지어 둡니다. 나머지 흐름은 epoll 서버와 똑같습니다.

이 코드는 macOS에서 그대로 실행됩니다. 여러 클라이언트를 동시에 붙여 보면, epoll 서버와 똑같이 한 흐름으로 모두를 매끄럽게 처리합니다. 닫힌 소켓은 kqueue가 자동으로 감시 명단에서 빼 주므로, 우리는 딕셔너리에서만 지우고 소켓을 닫으면 됩니다.

같은 발상, 다른 옷

epoll과 kqueue를 나란히 놓고 보면 차이는 표면적입니다. 만들고, 등록하고, 준비된 것만 받아 처리하는 흐름이 완전히 같습니다. 용어만 epoll은 register와 poll, kqueue는 kevent와 control로 다를 뿐입니다.

이것이 알려 주는 바가 있습니다. 운영체제마다 도구의 이름과 모양은 달라도, 고성능 동시성을 푸는 발상은 하나로 수렴한다는 것입니다. 준비된 것만 알려 받아 전부를 훑는 헛수고를 없앤다는 발상입니다. 이 발상만 단단히 잡고 있으면, 어떤 운영체제의 어떤 도구를 만나도 금세 적응할 수 있습니다. 그리고 앞 장에서 본 selectors 모듈이 바로 이 둘을 하나로 감싸, 우리가 운영체제를 신경 쓰지 않게 해 주는 것입니다.

저수준 도구를 두 가지나 봤습니다. 그런데 이 모든 것 위에 세워진 더 높은 도구가 있습니다. 요즘 Python으로 비동기 서버를 짤 때 가장 많이 쓰는 asyncio입니다. 다음 장에서 그 정체를 밝히고, 지금까지 만든 epoll과 kqueue가 사실 asyncio의 엔진 속에서 돌아가고 있음을 확인합니다.