Linux v6.18.37 · 개념과 코드 읽기

송신 경로: 드라이버가 패킷을 받지 못했을 때 목록을 지키는 방법

이 코드는 어떤 문제를 푸나요?

소켓에서 만든 패킷은 라우팅과 qdisc 등을 거쳐 NIC 드라이버로 내려갑니다. 드라이버의 송신 큐가 꽉 차면 남은 패킷을 잃지 않고 나중에 다시 처리할 수 있어야 합니다. dev_hard_start_xmit 전체는 연결된 skb 목록을 하나씩 넘기면서 누가 각 패킷을 소유하는지 정리하는 경계입니다.

읽을 범위: v6.18.37 · net/core/dev.c · dev_hard_start_xmit 3882–3908행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.

먼저 알아둘 개념

qdisc

송신 패킷의 대기·순서·정책을 다루는 계층입니다. NIC의 하드웨어 디스크립터 링과는 별도입니다.

NETDEV_TX_BUSY와 남은 목록

xmit_one()의 결과에서 dev_xmit_complete(rc)가 거짓이면 현재 skb가 소비되지 않아 그 skb부터 돌려줍니다. 반대로 현재 skb가 소비된 뒤에도 송신 큐가 멈추고 다음 skb가 남아 있으면 이 함수가 *ret = NETDEV_TX_BUSY로 설정하고 다음 skb부터 반환합니다. 따라서 *ret만으로 현재 skb의 소비 여부를 단정하지 않고 반환된 목록의 시작을 함께 봐야 합니다.

패킷 소유권

드라이버가 패킷을 받아 소비한 뒤에는 상위가 그 skb를 마음대로 다시 사용하거나 해제하면 안 됩니다. 반대로 접수되지 않은 패킷은 남겨야 합니다.

전송 접수와 전송 완료

드라이버가 skb를 받았다는 사실은 실제 네트워크 전송이나 상대방 수신 완료와 다릅니다.

처음 읽을 때

설명용으로 skb 세 개가 연결됐다고 가정하고, 두 번째 skb가 미소비인 경우와 두 번째 skb를 소비한 직후 큐가 멈춘 경우를 각각 그려 보십시오. 반환 목록의 첫 skb가 달라지는지 확인하는 것이 핵심입니다.

더 깊이 살펴볼 때

skb->next를 미리 저장하고 목록 표시를 지운 뒤 콜백을 호출하는 순서에 주목하십시오. 콜백이 skb를 소비할 수 있으므로 호출 후에 원래 skb를 따라 다음 항목을 읽어서는 안 됩니다.

분기 그림으로 보는 남은 skb 목록

dev_hard_start_xmit()는 현재 skb를 분리해 xmit_one()에 넘긴 뒤 결과에 따라 남은 목록의 시작을 반환합니다. dev_xmit_complete(rc)가 거짓이면 현재 skb를 미소비로 다뤄 다시 연결하고, 참이면 현재 skb를 소비된 것으로 다뤄 다음 항목으로 갑니다. 참인 경우에는 음수 오류도 포함될 수 있어 전송 성공을 뜻하지 않습니다. 다음 항목이 남아 있는데 큐가 멈추면 그 목록을 반환합니다.

xmit_one 뒤 현재 skb가 미소비이면 다시 연결해 반환하고, 소비된 뒤 큐가 멈추면 다음 skb부터 반환하며, 나머지는 반복 후 목록이 비면 NULL을 반환하는 세 경로
그림 1. Linux v6.18.37 dev_hard_start_xmit(). 그림 크게 보기. 각 분기의 *ret와 반환 skb는 실제 네트워크 전달 완료와 다릅니다.
한 반복 뒤 반환할 남은 목록
상황*retreturn skb
현재 skb 미소비그 결과 rc현재 skb부터, next 재연결
현재 skb 소비, 큐 중지·다음 skb 존재NETDEV_TX_BUSY다음 skb부터
현재 skb 소비 뒤 목록 소진마지막 rcNULL
기존 단순화 그림·GIF 보기 (소비·미소비 분기 생략)

아래의 기존 3단계 그림과 애니메이션은 목록 준비와 드라이버 호출을 간략히 보여 주는 보조 자료입니다. xmit_one() 뒤 현재 skb의 소비 여부, 큐 중지와 다음 skb의 조합, 음수 오류에서도 소비될 수 있는 경우를 구분하지 않습니다. 특히 BUSY와 큐 중지를 같은 조건으로 읽지 마세요. 실제 반환 목록과 *ret는 위 SVG와 표를 기준으로 읽으세요. 기존 PNG 크게 보기 · 기존 GIF 원본 열기

송신 경로: 드라이버가 패킷을 받지 못했을 때 목록을 지키는 방법의 단계별 개념 그림
각 단계에 화살표 의미와 생략 범위를 표시했습니다. 주소·숫자 예제는 실제 장치 값을 뜻하지 않습니다.
1단계 설명

1단계 고정

GIF 원본 열기

1. 목록에서 하나를 준비합니다

현재 skb의 다음 포인터를 먼저 저장하고 현재 패킷의 목록 연결 표시를 정리합니다.

화살표는 목록 순회 순서입니다. 메모리 주소가 증가하는 방향을 뜻하지 않습니다.

2. 드라이버에 넘깁니다

xmit_one으로 하나를 발행합니다. 접수됐다면 다음 패킷으로 이동합니다.

화살표는 패킷 소유권이 드라이버 쪽으로 넘어갈 수 있는 호출 경계입니다.

3. 남은 목록을 반환합니다

BUSY 또는 큐 정지 시 미전송 skb부터 남겨 호출자가 재처리하도록 합니다.

되돌아가는 화살표는 미전송 패킷 반환입니다. 이미 접수된 패킷을 회수하는 뜻이 아닙니다.

dev_hard_start_xmit를 한 줄씩 읽기

줄 번호는 v6.18.37 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
				    struct netdev_queue *txq, int *ret)
{
	struct sk_buff *skb = first;
	int rc = NETDEV_TX_OK;

	while (skb) {
		struct sk_buff *next = skb->next;

		skb_mark_not_on_list(skb);
		rc = xmit_one(skb, dev, txq, next != NULL);
		if (unlikely(!dev_xmit_complete(rc))) {
			skb->next = next;
			goto out;
		}

		skb = next;
		if (netif_tx_queue_stopped(txq) && skb) {
			rc = NETDEV_TX_BUSY;
			break;
		}
	}

out:
	*ret = rc;
	return skb;
}
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,

송신할 skb 목록의 첫 항목과 대상 장치를 받습니다.

				    struct netdev_queue *txq, int *ret)

대상 송신 큐와 상태를 돌려줄 정수 포인터를 추가로 받습니다. 함수 반환 포인터는 남은 패킷을 나타냅니다.

	struct sk_buff *skb = first;

현재 처리할 skb를 목록의 시작으로 둡니다.

	int rc = NETDEV_TX_OK;

아직 실패가 없으므로 전송 접수 상태를 NETDEV_TX_OK로 준비합니다.

	while (skb) {

미처리 skb가 있는 동안 하나씩 처리합니다.

		struct sk_buff *next = skb->next;

드라이버가 현재 skb를 소비할 수 있으므로 다음 포인터를 호출 전에 저장합니다.

		skb_mark_not_on_list(skb);

현재 skb를 목록에 연결된 상태로 취급하지 않도록 표시를 정리합니다.

		rc = xmit_one(skb, dev, txq, next != NULL);

현재 패킷을 하나 전송합니다. 다음 패킷 유무도 넘겨 연속 발행 최적화에 사용할 수 있게 합니다.

		if (unlikely(!dev_xmit_complete(rc))) {

반환 코드가 NET_XMIT_MASK보다 작아 현재 skb가 소비된 것으로 분류되는지 검사합니다. 음수 오류도 소비된 경우에 포함됩니다. 이 조건의 부정은 미소비 분기이며, 실제 선로 전송 완료 검사가 아닙니다.

			skb->next = next;

접수되지 않은 패킷이면 다음 링크를 복원해 남은 목록이 끊어지지 않게 합니다.

			goto out;

현재 패킷부터 다시 처리할 수 있도록 반환 절차로 갑니다.

		skb = next;

현재 패킷 처리가 끝났으므로 미리 저장한 다음 skb로 이동합니다.

		if (netif_tx_queue_stopped(txq) && skb) {

큐가 정지했고 아직 남은 패킷도 있는지 함께 확인합니다.

			rc = NETDEV_TX_BUSY;

뒤의 패킷들을 지금 더 보낼 수 없다는 BUSY 상태를 설정합니다.

			break;

남은 목록을 유지한 채 순회를 끝냅니다.

out:

정상 종료와 접수 실패가 모이는 반환 위치입니다.

	*ret = rc;

마지막 콜백 결과 또는 다음 skb 목록의 BUSY 상태를 호출자가 준 변수에 기록합니다.

	return skb;

아직 전송하지 못한 첫 skb를 반환합니다. 모두 처리했으면 NULL입니다.

함께 생각해 볼 질문

드라이버 호출 뒤 skb->next를 읽으면 왜 위험하나요?

드라이버가 skb를 소비하거나 해제했을 수 있으므로 호출 전에 다음 포인터를 저장해야 합니다.

반환 skb가 NULL이면 상대가 받았나요?

이 목록에서 상위에 남길 패킷이 없다는 뜻입니다. 원격 수신을 확인하는 것은 아닙니다.

큐 정지와 현재 패킷의 BUSY는 같은 검사인가요?

현재 패킷을 접수하지 못한 경우와, 접수 후 다음 패킷부터 보낼 수 없는 경우를 별도로 처리합니다.

출처와 읽은 범위

소비 판정: v6.18.37 include/linux/netdevice.h의 dev_xmit_complete().

Linux stable v6.18.37 · net/core/dev.c

해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고

맨 위로 ↑