Documentation/networking/packet mmap.txt

AWiki
이동: 둘러보기, 검색

기준 버전: 4.14


요약

이 파일에서는 2.4/2.6/3.x 커널의 PACKET 소켓 인터페이스에서 사용 가능한 mmap() 기능을 서술한다. 이 소켓 유형을 사용하는 경우는 i) tcpdump 같은 유틸리티로 네트워크 트래픽을 캡처 할 때, ii) 네트워크 트래픽을 전송할 때, 기타 네트워크 인터페이스에 대한 직접 접근이 필요할 때이다.

이 문서의 최신 버전:

http://wiki.ipxwarzone.com/index.php5?title=Linux_packet_mmap

하우투:

http://wiki.gnu-log.net (packet_mmap)

의견 보낼 곳:

Ulisses Alonso Camaró <uaca@i.hate.spam.alumni.uv.es>
Johann Baudy <johann.baudy@gnu-log.net>

PACKET_MMAP 사용 이유

리눅스 2.4/2.6/3.x에서 PACKET_MMAP이 켜져 있지 않으면 캡처 과정이 매우 비효율적이다. 아주 제한된 버퍼를 사용하며 각 패킷 캡처마다 시스템 호출이 한 번씩 필요하다. 그리고 (libpcap에서 항상 하듯) 패킷의 타임스탬프까지 얻으려면 두 번이 필요하다.

반면 PACKET_MMAP은 아주 효율적이다. PACKET_MMAP에서는 사용자 공간에 매핑 된 크기 조정 가능한 원형 큐가 있어서 이를 이용해 패킷을 보내거나 받을 수 있다. 이 방식에서 패킷을 읽으려면 패킷을 기다리기만 하면 된다. 대부분의 경우 단 한 번의 시스템 호출도 할 필요가 없다. 전송과 관련해선 시스템 호출 한 번을 통해 여러 패킷을 보낼 수 있으므로 아주 높은 대역폭을 얻을 수 있다. 커널과 사용자 사이의 공유 버퍼 사용은 패킷 복사를 최소화 해 주는 효과도 있다.

PACKET_MMAP을 캡처 및 전송 과정 성능 향상에 사용하는 것도 좋지만 이게 전부는 아니다. 일단 고속(CPU 속도에 따라 상대적)으로 캡처를 하는 경우 네트워크 인터페이스 카드가 어떤 종류의 인터럽트 부하 완화를 지원하는지 확인해 봐야 하고, 아니면 (더 좋게는) NAPI를 지원한다면 그 기능을 꼭 켜야 한다. 전송에선 사용 MTU(Maximum Transmission Unit)와 네트워크 장치가 지원하는 MTU를 확인해야 한다. 네트워크 인터페이스 카드의 CPU IRQ 고정도 도움이 될 수 있다.

mmap() 사용해서 캡처 과정 개선하기

사용자 관점에서는 상위 계층의 libpcap 라이브러리를 사용하는 게 좋다. 이 라이브러리는 실질적 표준이며 Win32를 포함한 거의 모든 운영 체제에 이식 가능하다.

그렇기는 한데, 이 글을 쓰는 시점에 libpcap 0.8.1 공식 버전이 나왔지만 PACKET_MMAP 지원을 포함하고 있지 않다. 아마 여러분의 배포판에 포함된 libpcap도 마찬가지일 것이다.

libpcap에 PACKET_MMAP을 구현한 경우를 두 가지 알고 있다.

http://wiki.ipxwarzone.com/ (Simon Patarin. libpcap 0.6.2 기반)
http://public.lanl.gov/cpw/ (Phil Wood. 최신 libpcap 기반)

이 문서의 나머지 내용은 하위 계층의 세부 내용을 이해하고 싶거나 PACKET_MMAP 지원을 포함시켜서 libpcap을 개선하고 싶은 사람들을 위한 것이다.

mmap() 직접 사용해서 캡처 과정 개선하기

시스템 호출 관점에서 PACKET_MMAP 사용에는 다음 과정이 수반된다.

[구성]

socket() ---> 캡처 소켓 생성
setsockopt() ---> 원형 버퍼(링) 할당. 옵션: PACKET_RX_RING
mmap() ---> 할당한 버퍼를 사용자 프로세스로 매핑

[캡처]

poll() ---> 들어오는 패킷 기다리기

[닫기]

close() ---> 캡처 소켓 없애고 모든 관련 자원 해제

소켓 만들기와 없애기는 단순 명확하며 PACKET_MMAP이 있으나 없으나 마찬가지이다.

int fd = socket(PF_PACKET, mode, htons(ETH_P_ALL));

mode 값으로 SOCK_RAW는 링크 계층 정보를 얻을 수 있는 생(raw) 인터페이스이고, SOCK_DGRAM은 링크 계층 정보를 얻을 수 없고 커널이 링크 계층 가상 헤더를 제공하는 가공(cooked) 인터페이스이다.

close(fd)만 호출하면 소켓 및 모든 관련 자원 제거가 이뤄진다.

PACKET_MMAP 아닌 경우와 마찬가지로 한 소켓을 캡처와 전송 모두에 이용하는 것이 가능하다. 한 번의 mmap() 호출로 할당된 RX 및 TX 버퍼 링을 매핑 하면 된다. "원형 버퍼(링) 매핑과 사용"을 보라.

다음으로 PACKET_MMAP 설정 방법과 제약 사항을 설명하는데, 사용자 프로세스에서의 원형 버퍼 매핑과 그 버퍼 사용에 대해서도 설명할 것이다.

mmap() 직접 사용해서 전송 과정 개선하기

아래에서 보듯 전송 과정은 캡처와 비슷하다.

[구성]

socket() ---> 전송 소켓 생성
setsockopt() ---> 원형 버퍼(링) 할당. 옵션: PACKET_TX_RING
bind() ---> 전송 소켓을 네트워크 인터페이스에 결속
mmap() ---> 할당한 버퍼를 사용자 프로세스로 매핑

[전송]

poll() ---> 빈 패킷 기다리기 (선택적)
send() ---> 링 내에 준비 상태로 설정된 모든 패킷 송신. MSG_DONTWAIT 플래그를 사용해 전송 완료 전에 반환할 수 있음.

[닫기]

close() ---> 전송 소켓 없애고 모든 관련 자원 해제

소켓 만들기와 없애기는 마찬가지로 단순 명확하며 앞서 설명한 캡처에서와 같은 방식으로 이뤄진다.

int fd = socket(PF_PACKET, mode, 0);

이 소켓을 통해 전송만 하려는 경우에는 선택적으로 프로토콜을 0으로 할 수 있는데, 비용이 큰 packet_rcv() 호출을 피하게 한다. 이 경우 TX_RING을 bind(2) 할 때도 sll_protocol = 0로 설정해야 한다. 다른 경우에선 htons(ETH_P_ALL)이나 기타 프로토콜을 쓰면 된다.

원형 버퍼 내 프레임의 헤더 크기를 알아야 하므로 소켓을 네트워크 인터페이스에 결속시키는 것이 (제로 카피에서는) 필수이다.

캡처에서처럼 각 프레임은 두 부분으로 되어 있다.

 --------------------
| struct tpacket_hdr | 헤더. 이 프레임의 상태를 담음.
|                    |
|--------------------|
| data buffer        |
.                    . 네트워크 인터페이스로 보낼 데이터.
.                    .
 --------------------

bind()struct sockaddr_llsll_ifindex 매개변수의 도움을 받아 소켓을 네트워크 인터페이스로 연계시킨다.

초기화 예:

struct sockaddr_ll my_addr;
struct ifreq s_ifr;
...

strncpy (s_ifr.ifr_name, "eth0", sizeof(s_ifr.ifr_name));

/* eth0의 인터페이스 번호 얻기 */
ioctl(this->socket, SIOCGIFINDEX, &s_ifr);

/* sockaddr_ll 구조체 채워서 결속 준비 */
my_addr.sll_family = AF_PACKET;
my_addr.sll_protocol = htons(ETH_P_ALL);
my_addr.sll_ifindex =  s_ifr.ifr_ifindex;

/* 소켓을 eth0에 결속 */
bind(this->socket, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_ll));

완전한 튜토리얼을 찾을 수 있는 곳: http://wiki.gnu-log.net/

기본적으로 사용자는 다음 위치에 데이터를 넣어야 한다.

프레임 기준점 + TPACKET_HDRLEN - sizeof(struct sockaddr_ll)

따라서 소켓 모드(SOCK_DGRAM 또는 SOCK_RAW)를 뭘로 고르든 간에 사용자 데이터의 시작은 다음 위치일 것이다:

프레임 기준점 + TPACKET_ALIGN(sizeof(struct tpacket_hdr))

사용자 데이터를 (가령 SOCK_RAW에서 페이로드 정렬을 위해) 다른 프레임 시작점 기준 위치에 두고 싶다면 (SOCK_DGRAM에서) tp_net이나 (SOCK_RAW에서) tp_mac을 설정할 수 있다. 이게 동작하려면 setsockopt()PACKET_TX_HAS_OFF 옵션으로 미리 켜 두어야 한다.

PACKET_MMAP 설정

사용자 코드에서의 PACKET_MMAP 구성은 다음 호출로 이뤄진다.

  • 캡처 과정
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, (void *) &req, sizeof(req))
  • 전송 과정
setsockopt(fd, SOL_PACKET, PACKET_TX_RING, (vodi *) &req, sizeof(req))

이 호출에서 가장 중요한 인자는 req 매개변수이다. 이 매개변수는 다음 구조여야 한다.

struct tpacket_req
{
    unsigned int    tp_block_size;  /* 연속 블록의 최소 크기 */
    unsigned int    tp_block_nr;    /* 블록 개수 */
    unsigned int    tp_frame_size;  /* 프레임의 크기 */
    unsigned int    tp_frame_nr;    /* 프레임 총 개수 */
};

이 구조체는 /usr/include/linux/if_packet.h에 정의되어 있으며 스왑 불가 메모리로 원형 버퍼(링)를 수립한다. 캡처 프로세스에 매핑 되어 있으므로 시스템 호출 없이도 캡처 된 프레임과 타임스탬프 같은 관련 메타 정보를 읽을 수 있다.

프레임들이 묶여서 블록에 들어간다. 각 블록은 물리적으로 연속인 메모리 영역이며 tp_block_size/tp_frame_size개 프레임을 담는다. 블록 총 개수는 tp_block_nr이다. 참고로 다음과 같기 때문에 tp_frame_nr은 중복 매개변수이다.

frames_per_block = tp_block_size/tp_frame_size

실제로 packet_set_ring에서 다음 조건이 참인지 확인한다.

frames_per_block * tp_block_nr == tp_frame_nr

예를 들어 다음과 같은 값들이 있으면

tp_block_size= 4096
tp_frame_size= 2048
tp_block_nr  = 4
tp_frame_nr  = 8

다음과 같은 버퍼 구조가 나온다.

        block #1                 block #2
+---------+---------+    +---------+---------+
| frame 1 | frame 2 |    | frame 3 | frame 4 |
+---------+---------+    +---------+---------+

        block #3                 block #4
+---------+---------+    +---------+---------+
| frame 5 | frame 6 |    | frame 7 | frame 8 |
+---------+---------+    +---------+---------+

프레임은 한 블록 안에 들어갈 수 있어야 한다는 조건 하에 아무 크기든 가능하다. 블록은 프레임을 정수 개만 담는다. 달리 말해 프레임이 두 블록에 걸칠 수 없다. 그래서 frame_size를 고를 때 고려해야 할 세부 사항들이 좀 있다. "원형 버퍼(링) 매핑과 사용"을 보라.

PACKET_MMAP 설정 제약

커널 버전 2.4.26 (2.4 브랜치) 및 2.6.5 (2.6 브랜치) 전에서는 PACKET_MMAP 버퍼가 32비트 아키텍처에서는 32768개 프레임만, 그리고 64비트 아키텍처에서는 16384개만 담을 수 있었다. 이 커널 버전들에 대한 정보는 http://pusa.uv.es/~ulisses/packet_mmap/packet_mmap.pre-2.4.26_2.6.5.txt 를 보라.

블록 크기 제한

앞서 말한 것처럼 각 블록은 연속적인 물리적 메모리 영역이다. __get_free_pages() 함수 호출로 이 메모리 영역을 할당한다. 이름에서 알 수 있듯 이 함수는 메모리 페이지들을 할당하며, 두 번째 인자는 페이지 수의 "차수(order)", 즉 2의 제곱 횟수이다. 즉 (PAGE_SIZE == 4096일 때) order=0이면 4096바이트이고 order=1이면 8192바이트, order=2이면 16384바이트인 식이다. __get_free_pages()로 할당 가능한 영역 최대 크기는 MAX_ORDER 매크로에 의해 결정된다. 더 정확히는 다음과 같이 제한값을 계산할 수 있다.

PAGE_SIZE << MAX_ORDER
i386 아키텍처에서 PAGE_SIZE는 4096바이트이다.
2.4/i386 커널에서 MAX_ORDERᅟ는 10이다.
2.6/i386 커널에서 MAX_ORDER는 11이다.

따라서 i386 아키텍처에서 get_free_pages는 2.4/2.6 커널에서 각각 4MB와 8MB까지 할당할 수 있다.

사용자 공간 프로그램에서는 /usr/include/sys/user.h/usr/include/linux/mmzone.h를 포함시켜서 PAGE_SIZEMAX_ORDER 선언을 얻을 수 있다.

getpagesize(2) 시스템 호출로 동적으로 페이지 크기를 알아낼 수도 있다.

블록 개수 제한

PACKET_MMAP의 제약들을 이해하려면 각 블록에 대한 포인터를 담는 데 쓰는 구조체를 살펴봐야 한다.

현재 이 구조체는 kmalloc으로 동적으로 할당하는 pg_vec라는 벡터인데, 그 크기가 할당 가능한 블록 개수를 제한한다.

+---+---+---+---+
| x | x | x | x |
+---+---+---+---+
  |   |   |   |
  |   |   |   v
  |   |   v  block #4
  |   v  block #3
  v  block #2
 block #1

kmalloc은 미리 정해진 크기의 풀들에서 임의 바이트의 물리적으로 연속된 메모리를 할당한다. 그 메모리 풀은 slab 할당자가 관리하는데, 최종적으로 할당 수행을 맡는 그 할당자가 kmalloc이 할당할 수 있는 최대 메모리를 정한다.

2.4/2.6 커널 및 i386 아키텍처에서는 131072바이트가 제한이다. kmalloc에서 사용할 수 있는 미리 정해진 크기들을 /proc/slabinfo의 "size-<bytes>" 항목들로 확인할 수 있다.

32비트 아키텍처에서는 포인터가 4바이트 길이이므로 블록에 대한 포인터 총 개수는 다음과 같다.

131072/4 = 32768개 블록

PACKET_MMAP 버퍼 크기 계산

정의:

  • <size-max>: kmalloc으로 할당 가능한 최대 크기 (/proc/slabinfo 참고)
  • <pointer size>: 아키텍처에 따라 다름 -- sizeof(void *)
  • <page size>: 아키텍처에 따라 다름 -- PAGE_SIZE 내지 getpagesize(2)
  • <max-order>: MAX_ORDER로 정의하는 값
  • <frame size>: 프레임 캡처 크기의 상한 (잠시 후 더 얘기함)

이 정의들로부터 다음을 유도하게 된다.

<block number> = <size-max>/<pointer size>
<block size> = <pagesize> << <max-order>

버퍼 최대 크기는 다음과 같고,

<block number> * <block size>

따라서 프레임 개수는 다음과 같다.

<block number> * <block size> / <frame size>

2.6 커널 및 i386 아키텍처에 적용되는 다음 매개변수들을 가정하자.

<size-max> = 131072 바이트
<pointer size> = 4 바이트
<pagesize> = 4096 바이트
<max-order> = 11

그리고 <frame size> 값으로 2048바이트를 가정하자. 이 매개변수들에서 다음을 얻는다.

<block number> = 131072/4 = 32768개 블록
<block size> = 4096 << 11 = 8 MiB

그러면 버퍼는 262144 MiB 크기가 된다. 따라서 262144 MiB / 2048 바이트 = 134217728개 프레임을 담을 수 있다.

실제로는 i386 아키텍처에서 이 버퍼 크기가 가능하지 않다. 알다시피 그 메모리는 커널 공간 내에서 할당되는데 i386에서 커널의 메모리 크기는 1GiB로 제한되어 있다.

할당한 메모리 모두 소켓이 닫힐 때까지 해제되지 않는다. 메모리 할당은 GFP_KERNEL 우선도로 이뤄지는데, 기본적으로 이는 필요한 메모리 할당을 위해 다른 프로세스의 메모리를 기다리거나 스왑 할 수 있다는 뜻이다. 따라서 정상적으로 제한까지 도달할 수 있다.

기타 제약

소스 코드를 살펴보면 여기서 프레임이라고 하는 것이 링크 계층 프레임만이 아님을 알게 된다. 각 프레임 시작 위치에는 struct tpacket_hdr라는 헤더가 있어서 PACKET_MMAP에서 타임스탬프 같은 링크 수준 프레임 메타 정보를 담는 데 쓴다. 그래서 여기서 프레임이라고 하는 것은 실제로 다음과 같다 (include/linux/if_packet.h에서 가져옴).

/*
   프레임 구조:

   - 시작점. 프레임이 TPACKET_ALIGNMENT=16으로 정렬되어 있어야 함
   - struct tpacket_hdr
   - TPACKET_ALIGNMENT=16으로 패딩
   - struct sockaddr_ll
   - 패킷 데이터(시작점+tp_net)가 TPACKET_ALIGNMENT=16에 정렬되도록
     크기를 선정한 빈 공간
   - Start+tp_mac: [ 선택적인 MAC 헤더 ]
   - Start+tp_net: 패킷 데이터, TPACKET_ALIGNMENT=16에 정렬
   - TPACKET_ALIGNMENT=16으로 정렬하는 패딩
 */

다음은 packet_set_ring에서 검사하는 조건들이다.

  • tp_block_sizePAGE_SIZE(1)의 배수여야 함
  • tp_frame_sizeTPACKET_HDRLEN보다 커야 함 (당연하다.)
  • tp_frame_sizeTPACKET_ALIGNMENT의 배수여야 함
  • tp_frame_nr이 정확히 frames_per_block*tp_block_nr이어야 함

참고로 tp_block_size를 2의 제곱수로 선택하는 게 좋은데, 안 그러면 메모리 낭비가 생기게 된다.

원형 버퍼(링) 매핑과 사용

사용자 프로세스에서 버퍼 매핑은 전통적인 mmap 함수로 이뤄진다. 원형 버퍼가 여러 개의 물리적으로 불연속인 메모리 블록들의 집합체이지만 사용자 공간에게는 연속적이며, 따라서 mmap 호출은 한 번이면 된다.

mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

tp_block_sizetp_frame_size로 나눠떨어지는 경우에는 프레임들이 tp_frame_size 바이트씩 연속으로 위치하게 된다. 그렇지 않으면 tp_block_size/tp_frame_size개 프레임마다 프레임 사이 빈 공간이 있게 된다. 프레임이 두 블록에 걸쳐 있을 수 없기 때문이다.

캡처와 전송에 한 소켓을 사용하려면 RX 버퍼 링과 TX 버퍼 링 모두를 mmap 호출 한 번으로 매핑 해야 한다.

...
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &foo, sizeof(foo));
setsockopt(fd, SOL_PACKET, PACKET_TX_RING, &bar, sizeof(bar));
...
rx_ring = mmap(0, size * 2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
tx_ring = rx_ring + size;

RX 링 메모리가 커널 맵에서 앞에 오고 그 바로 다음에 TX 링 메모리가 와야 한다.

각 프레임 시작 위치에 상태 필드가 있다 (struct tpacket_hdr 참고). 이 필드가 0이면 그 프레임이 커널에서 사용할 수 있는 상태라는 뜻이다. 아니면 사용자가 읽을 수 있는 프레임이 있는 것이며 다음 플래그들이 적용된다.

캡처 과정:

include/linux/if_packet.h:

#define TP_STATUS_COPY          (1 << 1)
#define TP_STATUS_LOSING        (1 << 2)
#define TP_STATUS_CSUMNOTREADY  (1 << 3)
#define TP_STATUS_CSUM_VALID    (1 << 7)

TP_STATUS_COPY:

이 필드는 프레임이 (그리고 연관 메타 정보가) tp_frame_size보다 커서 잘렸음을 나타낸다. recvfrom()으로 이 패킷 전체를 읽을 수 있다.
이렇게 되게 하려면 setsockopt()PACKET_COPY_THRESH 옵션으로 먼저 켜두어야 한다.
recvfrom으로 읽을 수 있도록 버퍼링 할 수 있는 프레임 수는 일반 소켓처럼 제한한다. socket(7) 맨페이지의 SO_RCVBUF 옵션을 보라.

TP_STATUS_LOSING:

getsockopt()PACKET_STATISTICS 옵션으로 확인한 마지막 통계 이후로 패킷 버림이 있었음을 나타낸다.

TP_STATUS_CSUMNOTREADY:

현재는 체크섬이 하드웨어에서 이뤄지게 될 나가는 IP 패킷들에 쓴다. 따라서 그 패킷을 읽을 때는 체크섬을 확인하려 하지 않는 게 좋다.

TP_STATUS_CSUM_VALID:

이 플래그는 적어도 패킷의 전송 헤더 체크섬을 커널 쪽에서 이미 검증했음을 나타낸다. 이 플래그가 설정되어 있지 않으면 TP_STATUS_CSUMNOTREADY 역시 설정되어 있지 않다고 할 때 마음껏 자체적으로 체크섬을 확인해도 된다.

편리를 위해 다음 정의도 있다.

#define TP_STATUS_KERNEL        0
#define TP_STATUS_USER          1

커널이 모든 프레임을 TP_STATUS_KERNEL로 초기화하며, 커널에서 패킷 수신 시 버퍼에 넣고서 최소한 TP_STATUS_USER 플래그로 상태를 갱신한다. 그러면 사용자가 그 패킷을 읽을 수 있으며, 사용자는 패킷을 읽고서 상태 필드를 0으로 만들어서 커널이 다시 그 프레임 버퍼를 사용할 수 있도록 해야 한다.

사용자는 poll을 (다른 변종들도 사용 가능) 사용해서 링에 새 패킷이 있는지 확인할 수 있다.

struct pollfd pfd;

pfd.fd = fd;
pfd.revents = 0;
pfd.events = POLLIN|POLLRDNORM|POLLERR;

if (status == TP_STATUS_KERNEL)
    retval = poll(&pfd, 1, timeout);

상태 값을 먼저 확인하고 프레임을 poll 해도 경쟁 조건이 유발되지 않는다.

전송 과정:

전송에 쓰이는 정의들도 있다.

#define TP_STATUS_AVAILABLE        0 // 프레임 사용 가능
#define TP_STATUS_SEND_REQUEST     1 // 다음 send()에서 보낼 프레임
#define TP_STATUS_SENDING          2 // 현재 전송 중인 프레임
#define TP_STATUS_WRONG_FORMAT     4 // 프레임 형식이 올바르지 않음

먼저 커널이 모든 프레임을 TP_STATUS_AVAILABLE로 초기화한다. 패킷을 보내려는 사용자는 사용 가능한 프레임의 데이터 버퍼를 채우고, tp_len을 현재 데이터 버퍼 크기로 설정하고, 상태 필드를 TP_STATUS_SEND_REQUEST로 설정한다. 여러 프레임에 그렇게 할 수 있다. 전송 준비가 되면 사용자가 send()를 호출한다. 그러면 상태가 TP_STATUS_SEND_REQUEST인 모든 버퍼들이 네트워크 장치로 전달된다. 커널이 송신한 프레임 각각의 상태를 TP_STATUS_SENDING으로 갱신한다. 그리고 각각의 전송이 끝나면 버퍼 상태가 TP_STATUS_AVAILABLE로 돌아간다.

header->tp_len = in_i_size;
header->tp_status = TP_STATUS_SEND_REQUEST;
retval = send(this->socket, NULL, 0, 0);

사용자가 poll()을 사용해 버퍼가 사용 가능한지 (status == TP_STATUS_SENDING) 확인할 수도 있다.

struct pollfd pfd;
pfd.fd = fd;
pfd.revents = 0;
pfd.events = POLLOUT;
retval = poll(&pfd, 1, timeout);

어떤 TPACKET 버전이 있고 언제 사용해야 하는가?

int val = tpacket_version;
setsockopt(fd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val));
getsockopt(fd, SOL_PACKET, PACKET_VERSION, &val, sizeof(val));

여기서 'tpacket_version'은 TPACKET_V1(기본값), TPACKET_V2, TPACKET_V3일 수 있다.

TPACKET_V1:

  • setsockopt(2)로 따로 지정하지 않으면 기본값
  • RX_RING과 TX_RING 사용 가능

TPACKET_V1 --> TPACKET_V2:

  • TPACKET_V1에서의 unsigned long 사용으로 인한 64비트 문제를 정리해서 64비트 커널과 32비트 사용자 공간 같은 조합에서도 동작
  • 타임스탬프 해상도를 마이크로초 대신 나노초로
  • RX_RING과 TX_RING 사용 가능
  • tpacket2_hdr 구조체에 패킷에 대한 VLAN 메타 정보 있음 (TP_STATUS_VLAN_VALID, TP_STATUS_VLAN_TPID_VALID):
    • TP_STATUS_VLAN_VALID 비트가 tp_status 필드에 설정되어 있으면 tp_vlan_tci 필드에 유효한 VLAN TCI 값이 있음을 나타냄
    • TP_STATUS_VLAN_TPID_VALID 비트가 tp_status 필드에 설정되어 있으면 tp_vlan_tpid 필드에 유효한 VLAN TPID 값이 있음을 나타냄
  • TPACKET_V2로 전환하는 방법:
    1. struct tpacket_hdrstruct tpacket2_hdr로 교체
    2. 헤더 길이를 질의해서 저장
    3. 프로토콜 버전을 2로 설정하고, 이전처럼 링을 준비
    4. sockaddr_ll을 얻을 때 (void *)hdr + TPACKET_ALIGN(sizeof(struct tpacket_hdr)) 대신 (void *)hdr + TPACKET_ALIGN(hdrlen) 사용

TPACKET_V2 --> TPACKET_V3:

  • 유연한 RX_RING 버퍼 구현:
    1. 고정이 아닌 프레임 크기로 블록들을 구성할 수 있음
    2. read/poll이 (패킷 수준이 아니라) 블록 수준임
    3. 조용한 링크에서 사용자 공간 무한 대기를 피하기 위해 poll 타임아웃 추가
    4. 사용자 구성 가능 사항:
      1. block::timeout
      2. tpkt_hdr::sk_rxhash
  • RX 해시 데이터를 사용자 공간에서 사용 가능
  • TX_RING 동작은 개념적으로 TPACKET_V2와 비슷하다. tpacket2_hdr대신 tpacket3_hdr를, TPACKET2_HDRLEN 대신 TPACKET3_HDRLEN을 쓰면 된다. 현재 구현에서는 tpacket3_hdrtp_next_offset 필드를 반드시 0으로 설정해서 링이 가변 크기 프레임을 담고 있지 않다고 표시해야 한다. tp_next_offset에 0 아닌 값을 가진 패킷은 버려진다.

AF_PACKET 분산 모드

AF_PACKET 분산(fanout) 모드에서는 패킷 수신을 여러 프로세스들로 로드 밸런스 할 수 있다. 패킷 소켓의 mmap(2)과 조합해서도 동작한다.

현재 구현된 분산 정책들:

  • PACKET_FANOUT_HASH: skb의 패킷 해시로 소켓으로 스케줄
  • PACKET_FANOUT_LB: 순차 순환으로 소켓으로 스케줄
  • PACKET_FANOUT_CPU: 패킷이 도착한 CPU에 따라 소켓으로 스케줄
  • PACKET_FANOUT_RND: 무작위 선정으로 소켓으로 스케줄
  • PACKET_FANOUT_ROLLOVER: 한 소켓이 가득 차면 다음 소켓으로 돌림
  • PACKET_FANOUT_QM: skb에 기록된 queue_mapping으로 소켓으로 스케줄

David S. Miller의 간단 예시 코드 ("./test eth0 hash", "./test eth0 lb" 같은 식으로 하면 됨):

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/ioctl.h>

#include <unistd.h>

#include <linux/if_ether.h>
#include <linux/if_packet.h>

#include <net/if.h>

static const char *device_name;
static int fanout_type;
static int fanout_id;

#ifndef PACKET_FANOUT
# define PACKET_FANOUT                  18
# define PACKET_FANOUT_HASH             0
# define PACKET_FANOUT_LB               1
#endif

static int setup_socket(void)
{
        int err, fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));
        struct sockaddr_ll ll;
        struct ifreq ifr;
        int fanout_arg;

        if (fd < 0) {
                perror("socket");
                return EXIT_FAILURE;
        }

        memset(&ifr, 0, sizeof(ifr));
        strcpy(ifr.ifr_name, device_name);
        err = ioctl(fd, SIOCGIFINDEX, &ifr);
        if (err < 0) {
                perror("SIOCGIFINDEX");
                return EXIT_FAILURE;
        }

        memset(&ll, 0, sizeof(ll));
        ll.sll_family = AF_PACKET;
        ll.sll_ifindex = ifr.ifr_ifindex;
        err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
        if (err < 0) {
                perror("bind");
                return EXIT_FAILURE;
        }

        fanout_arg = (fanout_id | (fanout_type << 16));
        err = setsockopt(fd, SOL_PACKET, PACKET_FANOUT,
                         &fanout_arg, sizeof(fanout_arg));
        if (err) {
                perror("setsockopt");
                return EXIT_FAILURE;
        }

        return fd;
}

static void fanout_thread(void)
{
        int fd = setup_socket();
        int limit = 10000;

        if (fd < 0)
                exit(fd);

        while (limit-- > 0) {
                char buf[1600];
                int err;

                err = read(fd, buf, sizeof(buf));
                if (err < 0) {
                        perror("read");
                        exit(EXIT_FAILURE);
                }
                if ((limit % 10) == 0)
                        fprintf(stdout, "(%d) \n", getpid());
        }

        fprintf(stdout, "%d: Received 10000 packets\n", getpid());

        close(fd);
        exit(0);
}

int main(int argc, char **argp)
{
        int fd, err;
        int i;

        if (argc != 3) {
                fprintf(stderr, "Usage: %s INTERFACE {hash|lb}\n", argp[0]);
                return EXIT_FAILURE;
        }

        if (!strcmp(argp[2], "hash"))
                fanout_type = PACKET_FANOUT_HASH;
        else if (!strcmp(argp[2], "lb"))
                fanout_type = PACKET_FANOUT_LB;
        else {
                fprintf(stderr, "Unknown fanout type [%s]\n", argp[2]);
                exit(EXIT_FAILURE);
        }

        device_name = argp[1];
        fanout_id = getpid() & 0xffff;

        for (i = 0; i < 4; i++) {
                pid_t pid = fork();

                switch (pid) {
                case 0:
                        fanout_thread();

                case -1:
                        perror("fork");
                        exit(EXIT_FAILURE);
                }
        }

        for (i = 0; i < 4; i++) {
                int status;

                wait(&status);
        }

        return 0;
}

AF_PACKET TPACKET_V3 예시

AF_PACKET의 TPACKET_V3 링 버퍼는 자체 메모리 관리를 해서 고정 아닌 프레임 크기를 사용하도록 구성할 수 있다. 블록을 기반으로 하여 TPACKET_V2 및 이전에서처럼 링 단위가 아니라 블록 단위로 폴링이 동작한다.

TPACKET_V3가 다음 효과를 가져온다고 한다.

  • CPU 사용량 약 15~20% 감소
  • 패킷 캡처 속도 약 20% 증가
  • 패킷 밀도 약 2배로 증가
  • 포트 모아서 분석
  • 정적이지 않은 프레임 크기로 전체 패킷 페이로드 캡처

따라서 패킷 분산과 함께 사용하기에 좋은 방안이다.

Chetan Loke의 lolpcap에 기반한 Daniel Borkmann의 최소 예시 코드 (gcc -Wall -O2 blob.c로 컴파일 해서 "./a.out eth" 등과 같이 해보면 됨):

/* 처음부터 새로 작성했지만, 커널에서 사용자 공간으로의
 * API 사용법은 lolpcap에서 잘라 가져왔음:
 *  Copyright 2011, Chetan Loke <loke.chetan@gmail.com>
 *  License: GPL, version 2.0
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <poll.h>
#include <unistd.h>
#include <signal.h>
#include <inttypes.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

#ifndef likely
# define likely(x)              __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
# define unlikely(x)            __builtin_expect(!!(x), 0)
#endif

struct block_desc {
        uint32_t version;
        uint32_t offset_to_priv;
        struct tpacket_hdr_v1 h1;
};

struct ring {
        struct iovec *rd;
        uint8_t *map;
        struct tpacket_req3 req;
};

static unsigned long packets_total = 0, bytes_total = 0;
static sig_atomic_t sigint = 0;

static void sighandler(int num)
{
        sigint = 1;
}

static int setup_socket(struct ring *ring, char *netdev)
{
        int err, i, fd, v = TPACKET_V3;
        struct sockaddr_ll ll;
        unsigned int blocksiz = 1 << 22, framesiz = 1 << 11;
        unsigned int blocknum = 64;

        fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
        if (fd < 0) {
                perror("socket");
                exit(1);
        }

        err = setsockopt(fd, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
        if (err < 0) {
                perror("setsockopt");
                exit(1);
        }

        memset(&ring->req, 0, sizeof(ring->req));
        ring->req.tp_block_size = blocksiz;
        ring->req.tp_frame_size = framesiz;
        ring->req.tp_block_nr = blocknum;
        ring->req.tp_frame_nr = (blocksiz * blocknum) / framesiz;
        ring->req.tp_retire_blk_tov = 60;
        ring->req.tp_feature_req_word = TP_FT_REQ_FILL_RXHASH;

        err = setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &ring->req,
                         sizeof(ring->req));
        if (err < 0) {
                perror("setsockopt");
                exit(1);
        }

        ring->map = mmap(NULL, ring->req.tp_block_size * ring->req.tp_block_nr,
                         PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0);
        if (ring->map == MAP_FAILED) {
                perror("mmap");
                exit(1);
        }

        ring->rd = malloc(ring->req.tp_block_nr * sizeof(*ring->rd));
        assert(ring->rd);
        for (i = 0; i < ring->req.tp_block_nr; ++i) {
                ring->rd[i].iov_base = ring->map + (i * ring->req.tp_block_size);
                ring->rd[i].iov_len = ring->req.tp_block_size;
        }

        memset(&ll, 0, sizeof(ll));
        ll.sll_family = PF_PACKET;
        ll.sll_protocol = htons(ETH_P_ALL);
        ll.sll_ifindex = if_nametoindex(netdev);
        ll.sll_hatype = 0;
        ll.sll_pkttype = 0;
        ll.sll_halen = 0;

        err = bind(fd, (struct sockaddr *) &ll, sizeof(ll));
        if (err < 0) {
                perror("bind");
                exit(1);
        }

        return fd;
}

static void display(struct tpacket3_hdr *ppd)
{
        struct ethhdr *eth = (struct ethhdr *) ((uint8_t *) ppd + ppd->tp_mac);
        struct iphdr *ip = (struct iphdr *) ((uint8_t *) eth + ETH_HLEN);

        if (eth->h_proto == htons(ETH_P_IP)) {
                struct sockaddr_in ss, sd;
                char sbuff[NI_MAXHOST], dbuff[NI_MAXHOST];

                memset(&ss, 0, sizeof(ss));
                ss.sin_family = PF_INET;
                ss.sin_addr.s_addr = ip->saddr;
                getnameinfo((struct sockaddr *) &ss, sizeof(ss),
                            sbuff, sizeof(sbuff), NULL, 0, NI_NUMERICHOST);

                memset(&sd, 0, sizeof(sd));
                sd.sin_family = PF_INET;
                sd.sin_addr.s_addr = ip->daddr;
                getnameinfo((struct sockaddr *) &sd, sizeof(sd),
                            dbuff, sizeof(dbuff), NULL, 0, NI_NUMERICHOST);

                printf("%s -> %s, ", sbuff, dbuff);
        }

        printf("rxhash: 0x%x\n", ppd->hv1.tp_rxhash);
}

static void walk_block(struct block_desc *pbd, const int block_num)
{
        int num_pkts = pbd->h1.num_pkts, i;
        unsigned long bytes = 0;
        struct tpacket3_hdr *ppd;

        ppd = (struct tpacket3_hdr *) ((uint8_t *) pbd +
                                       pbd->h1.offset_to_first_pkt);
        for (i = 0; i < num_pkts; ++i) {
                bytes += ppd->tp_snaplen;
                display(ppd);

                ppd = (struct tpacket3_hdr *) ((uint8_t *) ppd +
                                               ppd->tp_next_offset);
        }

        packets_total += num_pkts;
        bytes_total += bytes;
}

static void flush_block(struct block_desc *pbd)
{
        pbd->h1.block_status = TP_STATUS_KERNEL;
}

static void teardown_socket(struct ring *ring, int fd)
{
        munmap(ring->map, ring->req.tp_block_size * ring->req.tp_block_nr);
        free(ring->rd);
        close(fd);
}

int main(int argc, char **argp)
{
        int fd, err;
        socklen_t len;
        struct ring ring;
        struct pollfd pfd;
        unsigned int block_num = 0, blocks = 64;
        struct block_desc *pbd;
        struct tpacket_stats_v3 stats;

        if (argc != 2) {
                fprintf(stderr, "Usage: %s INTERFACE\n", argp[0]);
                return EXIT_FAILURE;
        }

        signal(SIGINT, sighandler);

        memset(&ring, 0, sizeof(ring));
        fd = setup_socket(&ring, argp[argc - 1]);
        assert(fd > 0);

        memset(&pfd, 0, sizeof(pfd));
        pfd.fd = fd;
        pfd.events = POLLIN | POLLERR;
        pfd.revents = 0;

        while (likely(!sigint)) {
                pbd = (struct block_desc *) ring.rd[block_num].iov_base;

                if ((pbd->h1.block_status & TP_STATUS_USER) == 0) {
                        poll(&pfd, 1, -1);
                        continue;
                }

                walk_block(pbd, block_num);
                flush_block(pbd);
                block_num = (block_num + 1) % blocks;
        }

        len = sizeof(stats);
        err = getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &stats, &len);
        if (err < 0) {
                perror("getsockopt");
                exit(1);
        }

        fflush(stdout);
        printf("\nReceived %u packets, %lu bytes, %u dropped, freeze_q_cnt: %u\n",
               stats.tp_packets, bytes_total, stats.tp_drops,
               stats.tp_freeze_q_cnt);

        teardown_socket(&ring, fd);
        return 0;
}

PACKET_QDISC_BYPASS

pktgen과 비슷하게 많은 패킷으로 망에 부하를 줘야 할 필요가 있다면 소켓 생성 후에 다음 옵션을 설정할 수 있을 것이다.

int one = 1;
setsockopt(fd, SOL_PACKET, PACKET_QDISC_BYPASS, &one, sizeof(one));

이로 인한 효과로 PF_PACKETᅟ을 통해 보내는 패킷이 커널의 qdisc 계층을 건너뛰고 드라이버로 직접 들어가게 된다. 이는 패킷이 버퍼링 되지 않고, tc 규율들이 무시되고, 유실 증가가 발생할 수 있고, 그 패킷이 더는 다른 PF_PACKET 소켓들에게 보이지 않는다는 의미이다. 이런 점들에 주의한다고 하면, 일반적으로 시스템의 다양한 구성 요소에 스트레스 테스트를 할 때 유용할 수 있다.

기본적으로 PF_PACKET 소켓에서 PACKET_QDISC_BYPASS가 꺼져 있으므로 명시적으로 켜야 한다.

PACKET_TIMESTAMP

PACKET_TIMESTAMP 설정은 mmap(2) 한 RX_RING 및 TX_RING에서의 패킷 메타 정보에서 타임스탬프의 원천을 결정한다. NIC가 하드웨어에서 패킷에 타임스탬프를 찍을 수 있다면 그 하드웨어 타임스탬프를 사용하도록 요청할 수 있다. 참고: SIOCSHWTSTAMP로 하드웨어 타임스탬프 생성을 켜줘야 할 수도 있다 (Documentation/networking/timestamping.txt의 관련 정보 참고).

PACKET_TIMESTAMPSO_TIMESTAMPING에서와 같은 정수 비트 필드를 받는다.

int req = SOF_TIMESTAMPING_RAW_HARDWARE;
setsockopt(fd, SOL_PACKET, PACKET_TIMESTAMP, (void *) &req, sizeof(req));

mmap(2) 한 링 버퍼에서 그 타임스탬프는 tpacket{,2,3}_hdr 구조체의 tp_sectp_{n,u}sec 멤버에 저장된다. 어떤 종류의 타임스탬프를 받은 것인지 알려면 tp_status 필드를 다음 비트들과 이진 OR 해 보면 된다.

TP_STATUS_TS_RAW_HARDWARE
TP_STATUS_TS_SOFTWARE

이는 대응하는 SOF_TIMESTAMPING_*과 동등하다. RX_RING에서 어느 쪽도 설정되어 있지 않다면 (즉, PACKET_TIMESTAMP를 설정하지 않았다면) PF_PACKET의 처리 코드 내에서 소프트웨어 방식을 호출한 것이다 (덜 정확함).

TX_RING에서 타임스탬프를 얻는 것은 다음과 같이 이뤄진다. i) 링 프레임을 채운다. ii) 가령 블로킹 모드로 sendto()를 호출한다. iii) 해당 프레임들의 상태가 각각 갱신되어 프레임이 응용으로 넘어오기를 기다린다. iv) 그 프레임들을 차례로 돌면서 개별 hw/sw 타임스탬프를 얻는다.

전송 타임스탬프가 켜져 있는 경우에만 이 비트들이 TP_STATUS_AVAILABLE과 이진 OR로 결합되므로 응용에서 그걸 꼭 확인해야 한다! (가령 첫 단계로 !(tp_status & (TP_STATUS_SEND_REQUEST | TP_STATUS_SENDING))을 확인해서 프레임이 응용에게 속하는지 보고, 다음 단계에서 tp_status에서 타임스탬프 종류를 추출할 수 있다.)

타임스탬프에 관심이 없어서 꺼 두었다면 TP_STATUS_AVAILABLE, TP_STATUS_WRONG_FORMAT을 확인하는 것으로 충분하다. TX_RING에서 TP_STATUS_AVAILABLE만 설정돼 있다면 tp_sectp_{n,u}sec 멤버가 유효한 값을 담고 있지 않은 것이다. TX_RING에서는 기본적으로 타임스탬프를 생성하지 않는다!

하드웨어 타임스탬프에 대한 추가 정보는 include/linux/net_tstamp.h 및 Documentation/networking/timestamping을 보라.

기타 사항

고마운 분들

Josse Brandeburg, 문법/절차 오류 정정.