 |  |
|
난이도 : 초급 M. Tim Jones, 컨설턴트 엔지니어, Emulex Corp.
옮긴이: 박재호 이해영 dwkorea@kr.ibm.com
2008 년 4 월 22 일 리눅스(Linux®) 운영체제 중에서 가장 큰 기능은 네트워킹 스택입니다. 초기에 BSD 스택에서 갈라져 나왔으며, 깔끔한 인터페이스 집합으로 제대로 조직화되어 있습니다. 지원 인터페이스는 공통 소켓 층 인터페이스나 드라이버 층과 같은 프로토콜 중립 층부터 시작해서 구체적인 개별 네트워크 프로토콜 인터페이스까지 범위가 다양합니다. 이 기사에서는 리눅스 네트워킹 스택 구조를 계층 관점에서 살펴보며, 몇몇 주요 자료 구조 또한 검토합니다.
프로토콜 개요
공식적인 네트워킹 개요 소개는 흔히 OSI(Open Systems Interconnection) 모델 언급부터 시작하지만, 리눅스에 탑재된 기본 네트워킹 스택을 설명하는 이 기사에서는 인터넷 모델로 알려진 4계층 모델을 사용한다(그림 1 참조).
그림 1. 네트워크 스택을 설명하는 인터넷 모델
스택 하단에 링크 층이 있다. 링크 층은 물리 층에 접근하는 디바이스 드라이버를 참조한다. 링크 층은 직렬 링크나 이더넷 디바이스 같은 다양한 매체가 될 수 있다. 링크 층 위에는 네트워크 층이 존재하는데, 패킷을 목적지로 전달하는 책임을 맡는다. 전송 층이라 불리는 다음 층은 (호스트 내부에서) 점 대 점 통신을 담당한다. 네트워크 층이 호스트 사이에 일어나는 통신을 관리하는 반면, 전송 층은 호스트 내부에서 종단점 사이에 일어나는 통신을 관리한다. 마지막으로 응용 층이 있는데, 일반적으로 이동하는 자료를 이해하는 의미론적인 층이다. 예를 들어, HTTP(HyperText Transfer Protocol)는 서버와 클라이언트 사이에서 웹 내용 요청과 응답을 전송한다.
현실에서 네트워킹 스택 계층은 좀더 친숙한 이름으로 불리고 있다. 링크 층에서, 가장 일반적인 고속 매체인 이더넷을 찾을 수 있다. 더 오래된 링크 층 프로토콜은 SLIP(Serial Line Internet Protocol), CSLIP(Compressed SLIP), PPP(Point-to-Point Protocol) 같은 직렬 프로토콜을 포함한다. 가장 일반적인 네트워크 층 프로토콜은 IP(Internet Protocol)인데, ICMP(Internet Control Message Protocol)나 ARP(Address Resolution Protocol) 같은 다양한 목적을 충족시키는 프로토콜도 네트워크 층에 존재한다. 전송 층은 TCP(Transmission Control Protocol)와 UDP(User Datagram Protocol)다. 마지막으로 익숙한 프로토콜을 포함하는 응용 층에는 표준 웹 프로토콜인 HTTP와 SMTP(Simple Mail Transfer Protocol)라는 전자편지 프로토콜이 존재한다.
핵심 네트워크 아키텍처
이제 리눅스 네트워크 스택 구조에 들어가서 인터넷 모델을 구현하는 방법을 살펴보자. 그림 2는 리눅스 네트워크 스택의 고차원적인 모습이다. 상위에는 사용자 영역 층(응용 층)이 있으며, 네트워크 스택 사용자를 정의한다. 하단에는 물리적인 장치가 있으며, (이더넷과 같은 고속 네트워크나 직렬과 같은) 네트워크 연결을 제공한다. 중간에는 커널 영역이 있으며, 이 기사에서 초점을 맞추는 네트워크 하위 시스템을 포함한다. 네트워크 스택 내부 흐름을 관장하기 위해 소켓 버퍼(sk_buffs)가 시작점과 도착점 사이에 패킷 자료를 전송한다. sk_buffs 구조체는 잠시 후 살펴보겠다.
그림 2. 리눅스 고차원 네트워크 스택 구조
우선 리눅스 네트워크 하위 시스템의 핵심 구성 요소를 간략하게 개괄한 다음에 좀더 세부 사항을 다루겠다. (그림 2에서) 상위 단은 시스템 호출 인터페이스다. 시스템 호출 인터페이스는 단순하게 보면 사용자 영역 응용 프로그램이 커널 네트워크 하위 시스템에 접근하는 길을 제공한다. 다음으로 프로토콜 중립 층이 존재하는데 기반 전송 층 프로토콜과 함께 작업하도록 공통 기능을 제공한다. 다음으로 실제 프로토콜이 존재하며, 리눅스는 내장 프로토콜인 TCP와 UDP는 물론이고 당연히 IP를 포함한다. 다음으로 또 다른 중립 층이 있어 개별 디바이스 드라이버 자체에서 오가는 공통 인터페이스를 허용한다.
시스템 호출 인터페이스
시스템 호출 인터페이스는 두 가지 관점에서 기술할 수 있다. 사용자가 네트워킹을 호출할 때, 시스템 호출 인터페이스를 통해 커널 내부로 멀티플렉싱을 수행한다. 이는 ./net/socket.c에 있는 sys_socketcall 호출로 끝나며, 목적지로 디멀티플렉싱을 수행한다. 시스템 호출 인터페이스의 또 다른 측면은 네트워킹 I/O를 위한 일반 파일 연산을 사용한다는 점이다. 예를 들어 전형적인 read와 write 연산은 (일반 파일과 마찬가지로 파일 기술자로 표현되는) 네트워킹 소켓에서 수행될지도 모른다. 또한 (socket 호출로 소켓 만들기, connect 호출로 목적지에 연결하기와 같은) 네트워킹에 특화된 몇몇 연산이 존재하지만, 일반 파일과 마찬가지로 네트워킹 객체에 적용되는 표준 파일 연산이 몇 가지 존재한다. 결국 시스템 호출 인터페이스는 사용자 영역 응용 프로그램과 커널 사이에서 제어를 넘기는 수단을 제공한다.
프로토콜 중립 인터페이스
소켓 층은 다양한 프로토콜을 위한 공통 함수 집합을 제공하는 프로토콜 중립 인터페이스다. 소켓 층은 전통적인 TCP와 UDP 프로토콜은 물론이고 IP, 가공하지 않은 이더넷, SCTP(Stream Control Transmission Protocol) 같은 여러 전송 프로토콜을 지원한다.
네트워크 스택을 통한 통신은 소켓으로 시작한다. 리눅스에 있는 소켓 구조체는 struct sock이며, linux/include/net/sock.h에 정의되어 있다. 이 구조체는 소켓이 사용하는 특정 프로토콜과 수행에 필요한 연산을 포함한, 특정 소켓 상태에 필요한 모든 항목을 담고 있다.
네트워킹 하위 시스템은 기능을 정의하는 특별한 구조체를 통해 활용 가능한 프로토콜을 인식한다. 각 프로토콜은 (linux/include/net/sock.h에 들어있는) proto라는 구조체를 유지한다. 이 구조체는 소켓 층에서 전송 층으로 수행하는 (소켓 생성 방법, 소켓 연결 확립 방법, 소켓 닫는 방법과 같은) 특별한 소켓 연산을 정의한다.
네트워크 프로토콜
네트워크 프로토콜 절은 (TCP, UDP 등) 사용 가능한 특정 네트워킹 프로토콜을 정의한다. 이 프로토콜은 (TCP와 UDP는 프로토콜 중에서 inet 가족으로) linux/net/ipv4/af_inet.c에 있는 inet_init라는 함수가 시작할 때 초기화된다. inet_init 함수는 proto_register 함수를 사용해 각각의 내장 프로토콜을 등록한다. 이 함수는 linux/net/core/sock.c에 정의되어 있으며, 활성 프로토콜 목록에 프로토콜을 추가할 뿐만 아니라 필요하다면 슬랩 캐시를 하나 이상 선택적으로 할당한다.
이제 linux/net/ipv4에 있는 tcp_ipv4.c, udp.c, raw.c 파일에서 proto 구조체를 통해 개발 프로토콜이 자신을 인식하는 방법을 살펴볼 차례다. 각 프로토콜 구조체는 내장 프로토콜을 연산으로 사상하는 inetsw_array로 type과 protocol을 사상한다. inetsw_array 구조체와 관련 내역은 그림 3에 정리했다. 이 배열에 있는 각 프로토콜은 inet_init에서 수행한 inet_register_protosw 호출을 통해 inetsw 시작 과정에서 초기화된다. 함수 inet_init는 또한 ARP, ICMP, IP 모듈, TCP/UDP 같은 다양한 inet 모듈을 초기화한다.
그림 3. 인터넷 프로토콜 배열 구조
 |
소켓 프로토콜 연관성 소켓이 생성될 때, my_sock = socket( AF_INET, SOCK_STREAM, 0 )처럼 타입과 프로토콜을 정의해야 한다는 사실을 기억하자. AF_INET은 인터넷 주소 가족을 표시하며, (여기서 inetsw_array로 나타나는) SOCK_STREAM은 스트림 소켓을 정의한다. |
|
그림 3에서 proto 구조체는 전송 층에 밀접한 메서드를 정의하며, proto_ops는 일반 소켓 메서드를 정의한다는 사실에 주목하자. 추가적인 프로토콜은 inet_register_protosw 호출을 통해 inetsw 프로토콜 스위치에 추가될 수 있다. 예를 들어, SCTP는 linux/net/sctp/protocol.c에 정의된 sctp_init를 호출해서 자기 자신을 추가한다. SCTP에 대한 정보가 더 필요하면 참고자료 절을 살펴보자.
소켓을 위한 자료 이동은 소켓 버퍼(sk_buff)라고 불리는 핵심 구조체를 사용해서 진행한다. sk_buff는 패킷 자료와 프로토콜 스택에서 다중 층을 다루는 상태 자료를 포함한다. 주고받는 각 패킷은 sk_buff로 표현한다. sk_buff 구조체는 linux/include/linux/skbuff.h에 정의되어 있으며, 그림 4에 정리해 놓았다.
그림 4. 소켓 버퍼와 다른 구조체 사이의 관계
그림에서 보듯이 다중 sk_buff는 연결을 위해 서로 연결되어 있다. 각 sk_buff는 패킷을 주고받을 디바이스 구조체(net_device)를 인식한다. 각 패킷은 sk_buff로 표현되는데, 패킷 헤더는 (th, iph, MAC(Media Access Control) 헤더를 위한 mac과 같은) 포인터 집합을 통해 편리하게 정의한다. sk_buff는 소켓 자료 관리의 중심이므로, 이를 관리하기 위해 만들어진 몇몇 지원 함수가 존재한다. sk_buff 생성과 제거, 복제, 큐 관리를 위한 함수가 존재한다.
소켓 버퍼는 주어진 소켓을 위해 서로 연결된 방식으로 설계되었으며 프로토콜 헤더, (패킷을 주고 받은 시각을 기록한) 타임스탬프, 패킷과 관련이 있는 디바이스를 포함하는 다양한 정보를 담고 있다.
디바이스 중립 인터페이스
프로토콜 층 아래에서는 다양한 기능으로 무장한 다양한 하드웨어 디바이스 드라이버를 프로토콜로 연결하는 중립 인터페이스 층이 있다. 이 층은 고수준 프로토콜 스택과 협력하도록 저수준 네트워크 디바이스 드라이버가 사용하는 공통 함수 집합을 제공한다.
먼저 디바이스 드라이버는 register_netdevice나 unregister_netdevice 호출을 통해 커널에 자신을 등록하거나 등록을 해제한다. 호출하는 쪽에서 우선 net_device 구조체를 채운 다음에 등록 과정에서 이를 전달한다. 커널은 (정의되어 있을 경우) 해당 init 함수를 호출하고, 몇 가지 이상 유무 검사를 수행하고, sysfs 항목을 만들고, 디바이스 목록에 새로운 디바이스를 추가한다(커널에 활성 디바이스 목록을 연결 리스트로 관리한다). linux/include/linux/netdevice.h에서 net_device를 찾을 수 있다. 다양한 함수가 linux/net/core/dev.c에 구현되어 있다.
프로토콜 층에서 디바이스로 sk_buff를 전달하려면, dev_queue_xmit 함수를 사용한다. 이 함수는 궁극적으로 (net_device나 sk_buff에서 sk_buff->dev 참조가 정의하는 네트워크 디바이스와 같은) 기반 디바이스 드라이버가 전송하는 sk_buff를 큐에 넣는다. dev 구조체는 sk_buff의 송신을 시작하는 드라이버 함수를 담고 있는 hard_start_xmit라는 메서드를 포함한다.
일반적으로 패킷을 받아들이는 과정에서 netif_rx 함수를 사용한다. 저수준 디바이스 드라이버가 (할당된 sk_buff에 포함된) 패킷을 받을 때, sk_buff는 netif_rx 호출을 통해 네트워크 층으로 상승 전파한다. 그리고 나서 이 함수는 상위 층 프로토콜 큐로 sk_buff를 밀어넣으며, netif_rx_schedule 함수를 통해 다음 처리 과정을 밟는다. linux/net/core/dev.c에 정의된 dev_queue_xmit와 netif_rx 함수를 살펴보자.
최근에 NAPI(new application program interface)를 커널에 도입하는 방법으로 드라이버 인터페이스를 디바이스 중립 계층(dev)과 인터페이스하도록 허용되었다. 몇몇 드라이버는 NAPI를 사용하지만 (대략 6대 1 비율로) 여전히 대다수는 과거 프레임 수취 인터페이스를 사용 중이다. NAPI는 들어오는 프레임마다 인터럽트를 피하는 방법으로 부하가 높은 경우에도 좋은 성능을 유지한다.
디바이스 드라이버
네트워크 스택 하단에는 물리적인 네트워크 디바이스를 관리하는 디바이스 드라이버가 있다. 이 층에 존재하는 디바이스 예는 직렬 인터페이스를 위한 SLIP, 이더넷 디바이스를 위한 이더넷 드라이버가 있다.
초기화 시점에서 디바이스 드라이버는 net_device 구조체를 할당한 다음 필요한 루틴을 통해 초기화한다. dev->hard_start_xmit라고 부르는 이 루틴 중 하나는 상위 층이 전송을 위해 sk_buff를 큐에 밀어넣는 방법을 정의한다. 이 루틴은 sk_buff를 받는다. 이 함수가 수행하는 연산은 기반 하드웨어에 의존적이며, 일반적으로 sk_buff가 기술하는 패킷은 하드웨어 링이나 큐로 이동한다. 디바이스 중립 층에서 설명한 프레임 수취자는 netif_rx 인터페이스나 NAPI 순응 네트워크 드라이버를 위한 netif_receive_skb를 활용한다. NAPI 드라이버는 기반 하드웨어 기능에 제약을 가한다. 세부 사항은 참고자료 절을 참조한다.
디바이스 드라이버가 dev 구조체에서 인터페이스 환경 설정을 마친 다음에 register_netdevice를 호출하면 사용 가능 상태로 바뀐다. 네트워크 디바이스에 밀접한 드라이버는 linux/drivers/net에 들어있다.
한걸음 더 나가면
네트워크 디바이스 드라이버를 포함해 다양한 디바이스 유형을 위한 설계 기법을 익히려면 리눅스 원시 코드가 최고의 수단이다. 활용 가능한 커널 API 설계와 용법에서 다양성을 찾을 수 있으며, 새로운 디바이스 드라이버를 위한 시작점이나 지침으로도 활용이 가능하다. 네트워크 스택에서 남아 있는 코드는 새로운 프로토콜을 요구하지 않는 이상 일반적으로 쓸 만하다. 새로운 프로토콜이 필요한 경우라면 TCP(스트림 프로토콜)나 UDP(메시지 기반 프로토콜) 구현을 새로운 개발 과정에서 출발점으로 삼자.
참고자료 교육
- TCP/IP, UDP, ICMP를 개괄하는 자료가 필요하다면 www.linuxjunkies.org에서 "Introduction to the Internet Protocols"를 살펴보기 바란다.
- "리눅스 시스템 호출을 사용하는 커널 명령어" (developerWorks, 2007년 3월)는 리눅스 시스템 호출 인터페이스를 다룬다. 시스템 호출 인터페이스는 사용자 영역과 커널 사이에 함수 호출을 중재하는 사용자 영역 쪽 GNU C 라이브러리(glibc)와 맞물린 커널 층이다.
- "/proc 파일시스템을 사용하여 리눅스 커널에 액세스 하기" (developerWorks, 2006년 3월)는 /proc 파일 시스템을 살펴본다. /proc은 가상 파일 시스템으로 사용자 영역 응용 프로그램이 커널과 통신하도록 만들어주는 참신한 방식을 제공한다. 이 기사는 적재 가능한 커널 모듈은 물론이고 /proc 예제를 보여준다.
- 네트워크 프로토콜에 관심이 있다면 BSD와 마찬가지로 리눅스는 훌륭한 운영체제다. "Better networking with SCTP" (developerWorks, 2006년 2월)는 SCTP(Stream Control Transmission Protocol)라는 가장 흥미로운 네트워크 프로토콜 중 하나를 다룬다. SCTP는 TCP와 유사하게 동작하지만 메시징, 멀티 홈, 멀티 스트림과 같은 다른 많은 유용한 기능을 추가한 프로토콜이다.
- "리눅스 슬랩 할당자 해부 (한글)" (한국 developerWorks, 2008년 4월)는 리눅스에서 사용하는 메모리 관리 기법 중에 가장 흥미로운 슬랩 할당자를 다룬다. 이 메커니즘은 썬OS에서 기원했지만, 리눅스 커널 내부에 안착한 사실을 발견할 수 있다.
- NAPI 드라이버는 과거 패킷 처리 프레임워크를 사용하는 드라이버와 비교해서 패킷 압박이 심할 경우 인터럽트 관리를 강화하는 장점을 제공한다. NAPI's interface and design은 OSDL에서 찾을 수 있다.
- 사용자 영역에서 진행하는 리눅스 프로그래밍 관련 정보가 필요하면 Tim이 쓴 GNU/Linux Application Programming을 살펴보자.
- BSD 소켓 API를 활용한 소켓 프로그래밍을 익히려면 Tim이 쓴 BSD Sockets Programming from a Multi-Language Perspective를 살펴보자.
-
developerWorks 리눅스 영역에서, 리눅스 튜토리얼과 지난 달 인기 있는 리눅스 기사와 튜토리얼을 찾아 개발자를 위한 더 많은 정보를 습득하기 바란다.
-
developerWorks 기술 행사와 웹 캐스트를 놓치지 말기 바란다.
제품 및 기술 얻기
-
DB2®,
Lotus®, Rational®, Tivoli®, WebSphere® 같은 최신 IBM 평가판 소프트웨어를 포함하는 두 장짜리 DVD 세트인 SEK for Linux를 주문하자.
-
IBM 평가판 소프트웨어: developerWorks에서 직접 내려 받아 다음번 리눅스 프로젝트에 활용하자.
토론
필자소개  | |  | M. Tim Jones는 임베디드 소프트웨어 아키텍트이자 GNU/Linux Application Programming, AI Application Programming, BSD Sockets Programming from a Multilanguage Perspective의 저자이기도 한다. Jones의 공학 배경은 정지 위성을 위한 커널 개발에서 시작해 임베디드 시스템 아키텍처와 네트워크 프로토콜 개발에 이르기까지 다양한 분야를 아우른다. Jones는 콜로라도 주, 롱몬트 소재 Emulex 사에서 컨설턴트 엔지니어로 활약한다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|  |