제공 : 한빛 네트워크
저자 : 한동훈
리눅스 커널을 보면 디바이스 종류는 크게 3가지로 나뉜다. 문자 디바이스, 블록 디바이스, 네트워크 디바이스. 네트워크 디바이스는 /dev 밑에 파일로 존재하지 않는 특별한 형태로 되어 있다.
클러스터링쪽을 보면 TCP/IP가 매우 느리니까 위 그림처럼 중간에 TCP/IP를 몽땅 제거한 후 커널도 개입하지 못하도록 하여 네트워크 카드와 응용프로그램이 직접 통신하게 만드는 시도를 많이 한다.
이 그림은 U-NET이라 불리는 초기의 구현을 나타낸 것이다. 이후 VIA, SOVIA(Socket Over VIA)로 발전해 나가게 된다. UNET도 마찬가지로 서로 다른 두 시스템에서 커널과 커널간에 통신하자는 것이고, TCP/IP를 쓰지 말자는 것이다. (TCP/IP에서 90Mbps이던 것이 U-NET이나 VIA로 가면 540Mbps의 처리율을 갖는다)
정말 간단하게 프로토콜 정의해서 전달하자는 것인데, TCP/IP를 보면 한 시스템에서 통신하는 여러 응용프로그램들을 구분하기 위해 포트번호를 사용한다. 서버만 포트번호를 사용해서 구분하는 게 아니라 클라이언트에서도 포트 번호를 사용해서 데이터를 수신할 응용프로그램을 지정하고 있는 것이다.
TCP/IP를 제거하고서 단순히 Mac 어드레스만 가지고 통신하고자 하면 응용프로그램을 구분할 방법이 없는 것이다. 그래서 먹스/디먹스(Mux/DeMux)를 추가하는 방법을 사용한다. 두번째 그림의 아랫 부분을 보면 먹스/디먹스가 추가됨을 볼 수 있다. 이것이 응용프로그램을 구분해서 데이터를 전달해주는 역할을 한다.
하지만 여전히 커널이 개입하는 것을 알 수 있다. 커널도 배제하고 먹스/디먹스에서 바로 응용프로그램으로 가면 좋겠지만 커널에서는 초기 통신 성립을 위한 초기화를 수행하기 위해 개입할 필요가 있는 것이다. 초기화가 끝난 이후에는 먹스/디먹스를 통해 응용프로그램끼리 데이터를 주고 받는 게 가능해진다. U-NET 이후에 등장했던 VIA, SOVIA도 마찬가지로 초기엔 커널을 완전히 제거하지 못했다. 초기화에는 커널이 개입하는 구조였는데, 그 이후엔 커널도 완전히 배제하는 구조까지 등장하게 된다.
네트워크 인터페이스를 디바이스 노드로 꺼내는 것으로는 응용프로그램에 전달할 데이터를 효과적으로 전달하기가 어렵다. 게다가 부번호는 255개이고, 커널 2.6의 확장된 구조를 써도 2048개 이상의 부번호 할당은 어렵다. 따라서 디바이스 노드로 공개되지 않으며, 내부에서 네트워크 패킷 데이터를 해석하면서 포트 번호에 따라 그에 해당하는 응용프로그램에 데이터를 전달해주는 구조로 되어 있다. 즉, 커널 내부에서 포트번호에 기반해서 먹스/디먹스 처리를 해주는 것이다.
이런 처리는 디바이스 노드로 꺼내서는 할 수 없기 때문에 디바이스 노드로 공개되지 않는다.
사용자 응용프로그램에서 recv() 함수를 호출했을 때, 커널은 그에 따라 처리를 하지만 읽어올 데이터가 아직 도착하지 않은 경우엔 프로세스를 대기 상태로 전환한다. 네트워크 카드에 패킷이 수신되어 읽어올 데이터가 생기면 커널은 현재 처리를 중단하고 하드웨어 인터럽트를 실행한다. 하드웨어 인터럽트 핸들러는 디바이스 드라이버에 등록된 인터럽트 처리함수를 실행한다. 인터럽트 핸들러는 네트워크 카드에서 수신 처리용 데이터를 가져오고, 데이터의 수신 처리를 담당하는 소프트 인터럽트 핸들러 실행을 요청한다. 인터럽트 핸들러는 여기서 실행을 중단하고 다시 인터럽트 대기 상태로 간다. 즉 여기까지가 Top Half의 처리이고, 데이터의 수신 처리는 Bottom Half로 처리된다.
기존 유닉스들이 인터럽트 처리를 인터럽트 핸들러에서 한번에 해결했다면 리눅스 커널은 즉시 처리해야 하는 부분만 Top Half로 처리하고 나머지 처리는 Botton Half에 위임하고 즉시 인터럽트 대기 상태로 복원한다. 이 때문에 많은 인터럽트 처리가 가능해진다. 이를 보통은 Bottom Half라 부른다.
소프트 인터럽트 핸들러(Bottom Half 부분)는 수신된 패킷 데이터를 해석해서 해당 프로토콜 계층의 수신 처리 함수를 호출한다. TCP/IP 였다면 해당 계층의 수신 처리 함수를 호출한다. 헤더에 따라 해당 소켓에 데이터를 보내며, 해당 소켓에 대기중인 프로세스가 있으면 해당 프로세스를 깨운다. 그러면 프로세스는 해당 소켓에서 데이터를 읽어와서 처리를 하게 된다.
만약 해당하는 소켓이 없다면 잘못된 데이터가 되는 것이고 이런 소켓은 결국 폐기된다.