← System Programming DUJINLABS.COM

Socket · Linux userspace / kernel ABI

UDP datagram 경계와 오류

UDP의 message boundary, truncation, connected UDP, path MTU, packet loss·duplication·reordering을 recvmsg metadata와 함께 봅니다.

Series
33 / 37
Build
cc -std=c17 -Wall -Wextra -O2 udp_meta.c -o udp_meta
Run
./udp_meta
Kernel
Linux 6.18.37 LTS

recvfrom buffer가 datagram보다 작으면 나머지를 다음 read에서 받을 수 있는가?

UDP는 datagram 경계를 보존한다. receive buffer가 작으면 초과 부분은 잘리고 같은 datagram의 나머지를 다음 recv에서 이어 받을 수 없다. MSG_TRUNC와 recvmsg 반환 길이를 이용해 truncation을 명시적으로 처리한다.

send 성공은 delivery를 보장하지 않는다. loss, duplicate, reorder, path MTU, checksum/ICMP 오류를 application protocol이 필요에 따라 sequence, retry, acknowledgement로 보완한다.

구조 그림

그림 1. UDP receive queue의 datagram 경계와 truncation
receive queue
datagram A · 16Bdatagram B · 5Bdatagram C · 12B
recv buffer · 8B
A0A1A2A3A4A5A6A7
A의 나머지
A8A9A10A11A12A13A14A15
다음 recv
datagram B부터 시작

각 skb가 datagram 하나다. 16-byte datagram을 8-byte buffer로 읽으면 뒤 8 byte는 다음 recv에 남지 않고 버려진다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
sendto 한 datagram 구성
IP layer route/MTU
network loss/reorder 가능
UDP queue datagram 단위
recvmsg payload + peer/meta

stream offset 대신 datagram 하나의 길이와 peer 주소를 state 단위로 다룬다. truncation은 partial progress가 아니라 message 손실이다.

그림 3. 커널 내부에서 지나가는 주요 지점
udp_sendmsg destination/length
ip_make_skb packet 구성
udp_queue_rcv_skb socket queue
udp_recvmsg 한 skb 소비
MSG_TRUNC buffer보다 큰 길이

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
net/ipv4/udp.c udp_sendmsg(), udp_recvmsg() datagram 생성과 한 message 단위 receive
net/ipv4/udp.c udp_queue_rcv_skb() socket receive queue와 drop 조건
net/ipv4/ip_output.c ip_make_skb(), ip_append_data() IP packet/fragment/MTU 처리

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 udp_meta.c -o udp_meta
01#define _GNU_SOURCE
02#include <arpa/inet.h>
03#include <stdio.h>
04#include <string.h>
05#include <sys/socket.h>
06#include <unistd.h>
07
08int main(void)
09{
10    int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
11    struct sockaddr_in local = { .sin_family = AF_INET,
12        .sin_addr = { .s_addr = htonl(INADDR_LOOPBACK) } };
13    if (fd < 0 || bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
14        return 1;
15    socklen_t local_len = sizeof(local);
16    getsockname(fd, (struct sockaddr *)&local, &local_len);
17
18    const char payload[] = "0123456789abcdef";
19    if (sendto(fd, payload, sizeof(payload) - 1, 0,
20               (struct sockaddr *)&local, sizeof(local)) < 0)
21        return 1;
22
23    char small[8];
24    struct iovec iov = { .iov_base = small, .iov_len = sizeof(small) };
25    struct msghdr message = { .msg_iov = &iov, .msg_iovlen = 1 };
26    ssize_t n = recvmsg(fd, &message, MSG_TRUNC);
27    printf("datagram=%zd copied=%zu truncated=%s\n", n, sizeof(small),
28           n > (ssize_t)sizeof(small) ? "yes" : "no");
29    close(fd);
30    return n < 0;
31}

코드 조각별 설명

실제 코드 10행SOCK_DGRAM | SOCK_CLOEXEC

connection 없이 datagram socket을 만들고 exec 상속을 막는다.

실제 코드 16행getsockname(fd

port 0 bind로 kernel이 고른 ephemeral local port를 읽어 loopback destination으로 사용한다.

실제 코드 19행sizeof(payload) - 1

문자열 terminating NUL은 wire protocol에 포함하지 않는다. datagram 길이는 sendto 호출 하나가 정한다.

실제 코드 23행char small[8]

16-byte datagram보다 작은 receive buffer로 truncation을 의도적으로 만든다.

실제 코드 26행MSG_TRUNC

Linux에서는 실제 datagram 길이를 반환하게 해 buffer보다 컸는지 알 수 있다. 복사된 byte는 buffer 크기뿐이다.

세부 동작

01

connected UDP는 reliability 기능이 아니다

connect는 default peer를 설정하고 다른 source datagram filtering, send/recv 사용, 일부 asynchronous error 전달을 편하게 한다. handshake나 delivery guarantee를 추가하지 않는다.

peer 변경이 필요하면 connect를 다시 호출하거나 sendto destination을 사용한다.

02

large datagram은 MTU 문제를 만든다

IP fragmentation은 한 fragment만 잃어도 전체 datagram을 잃고 middlebox에서 제한될 수 있다. DF/path MTU discovery 조건에서는 EMSGSIZE가 돌아올 수 있다.

application은 작은 packet size와 fragmentation/reassembly 상한을 정한다.

03

queue overflow는 receiver가 모를 수 있다

application이 느리면 socket receive buffer가 가득 차 새 datagram이 drop된다. SO_RXQ_OVFL ancillary data와 system UDP counters로 drop을 관찰할 수 있다.

buffer만 키우기보다 processing batch, CPU affinity, packet rate limit을 함께 조정한다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
UDP socketsocket/bind에서 endpoint가 생기고 close까지 receive queue를 소유한다local/peer, rcvbuf, error queue
datagram skbnetwork receive부터 recv/drop까지 message 단위로 존재한다length, source, checksum, timestamp
ancillary metadatarecvmsg control buffer에 복사되어 해당 datagram과 함께 소비된다pktinfo, timestamp, overflow

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
payload 뒤가 사라짐receive buffer보다 큰 datagram truncationMSG_TRUNC와 실제 length
가끔 packet 누락network 또는 socket queue dropsequence 번호, netstat, SO_RXQ_OVFL
send EMSGSIZEpath MTU보다 큰 datagram/DFerror queue와 discovered MTU

직접 확인

  1. MSG_TRUNC를 제거했을 때 반환 길이와 truncation 검출 가능성을 비교한다.
  2. SO_TIMESTAMPNS와 IP_PKTINFO를 켜 source/destination interface metadata를 recvmsg control buffer에서 읽는다.
  3. 작은 SO_RCVBUF와 빠른 sender로 drop을 유도해 sequence gap과 SO_RXQ_OVFL를 기록한다.
실행./udp_meta
추적strace -e trace=socket,bind,getsockname,sendto,recvmsg,close ./udp_meta

원문