Network · Linux v6.6 · source + diagram note

TX path

준비된 skb가 qdisc 또는 noqueue를 지나 driver 전송 시작으로 가는 조건부 경로.

전체 흐름: 이미 만들어진 skb의 송신 선택

__dev_queue_xmit()은 호출자가 장치·우선순위와 버퍼 내용을 준비한 skb를 받는다. TX 큐를 고른 뒤 qdisc의 enqueue 유무로 경로가 갈린다. RX의 NAPI는 이 TX 함수의 필수 선행 단계가 아니다.

그림 1. qdisc 경유와 noqueue 경로는 조건 분기
caller prepared skb__dev_queue_xmit()skb 소유권을 인수하고 TX queue의 qdisc를 읽는다.
YES · qdisc
__dev_xmit_skb()조건부 bypass 또는 enqueue/run; 잠금 경로의 비활성 qdisc는 drop, NOLOCK은 별도
dequeue / sch_direct_xmit실행 가능할 때 driver start 경로로 연결
NO · noqueue
UP → 재귀 → skb 검증 → TX lock → stopped 검사검사를 통과하면 dev_hard_start_xmit()으로 진행한다.
dev_hard_start_xmit()남은 skb는 이 경로에서 오류 처리한다.
어느 쪽도 __dev_queue_xmit() 반환을 NIC 송신 완료나 상대방 수신 확인으로 바꾸지 않는다.

근거: qdisc 선택 L4331–4337, noqueue L4339–4397.

소스 코드 위치

소비 판정은 v6.6 include/linux/netdevice.h의 dev_xmit_complete()와 함께 대조했다. 이는 송신 성공이나 상대방 수신 확인이 아니다.

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: net/core/dev.c
Linux v6.6: net/sched/sch_generic.c
Linux v6.6: net/ipv4/ip_output.c

설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

대표 코드

두 상자는 각각 v6.6의 net/core/dev.c에서 이어지는 원문이다. 첫 상자는 qdisc 선택까지이며 noqueue 본문은 발췌 뒤에 이어진다. 둘째 상자는 함수 전체다.

__dev_queue_xmit: skb 소비 계약과 qdisc 선택

원본 코드: net/core/dev.c L4256–4337
/**
 * __dev_queue_xmit() - transmit a buffer
 * @skb:	buffer to transmit
 * @sb_dev:	suboordinate device used for L2 forwarding offload
 *
 * Queue a buffer for transmission to a network device. The caller must
 * have set the device and priority and built the buffer before calling
 * this function. The function can be called from an interrupt.
 *
 * When calling this method, interrupts MUST be enabled. This is because
 * the BH enable code must have IRQs enabled so that it will not deadlock.
 *
 * Regardless of the return value, the skb is consumed, so it is currently
 * difficult to retry a send to this method. (You can bump the ref count
 * before sending to hold a reference for retry if you are careful.)
 *
 * Return:
 * * 0				- buffer successfully transmitted
 * * positive qdisc return code	- NET_XMIT_DROP etc.
 * * negative errno		- other errors
 */
int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
	struct net_device *dev = skb->dev;
	struct netdev_queue *txq = NULL;
	struct Qdisc *q;
	int rc = -ENOMEM;
	bool again = false;

	skb_reset_mac_header(skb);
	skb_assert_len(skb);

	if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_SCHED_TSTAMP))
		__skb_tstamp_tx(skb, NULL, NULL, skb->sk, SCM_TSTAMP_SCHED);

	/* Disable soft irqs for various locks below. Also
	 * stops preemption for RCU.
	 */
	rcu_read_lock_bh();

	skb_update_prio(skb);

	qdisc_pkt_len_init(skb);
	tcx_set_ingress(skb, false);
#ifdef CONFIG_NET_EGRESS
	if (static_branch_unlikely(&egress_needed_key)) {
		if (nf_hook_egress_active()) {
			skb = nf_hook_egress(skb, &rc, dev);
			if (!skb)
				goto out;
		}

		netdev_xmit_skip_txqueue(false);

		nf_skip_egress(skb, true);
		skb = sch_handle_egress(skb, &rc, dev);
		if (!skb)
			goto out;
		nf_skip_egress(skb, false);

		if (netdev_xmit_txqueue_skipped())
			txq = netdev_tx_queue_mapping(dev, skb);
	}
#endif
	/* If device/qdisc don't need skb->dst, release it right now while
	 * its hot in this cpu cache.
	 */
	if (dev->priv_flags & IFF_XMIT_DST_RELEASE)
		skb_dst_drop(skb);
	else
		skb_dst_force(skb);

	if (!txq)
		txq = netdev_core_pick_tx(dev, skb, sb_dev);

	q = rcu_dereference_bh(txq->qdisc);

	trace_net_dev_queue(skb);
	if (q->enqueue) {
		rc = __dev_xmit_skb(skb, q, dev, txq);
		goto out;
	}

읽기 기준: 주석의 “반환값과 무관하게 skb 소비”를 먼저 고정한다. q->enqueue가 있으면 __dev_xmit_skb()로, 없으면 뒤의 noqueue 분기로 간다.

줄별 핵심: L4268–4275은 skb 소비·반환 계약, L4331–4337은 qdisc 선택이다. 그 뒤의 noqueue 본문은 이 상자에 포함하지 않았다.

dev_hard_start_xmit: 미전송 목록 반환

원본 코드: net/core/dev.c L3579–3605
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;
}

라인별 주석

원본 코드: net/core/dev.c, v6.6 3579–3605행. 왼쪽 번호는 위 발췌문의 줄 번호이며 고정 원문과 대조한 행만 설명한다.

01struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,

송신 대기 목록의 첫 skb와 대상 네트워크 장치를 받는다. 이 인자 자체가 NIC 전송 완료를 뜻하지 않는다.

02 struct netdev_queue *txq, int *ret)

txq는 사용할 송신 큐이고 ret은 결과 코드를 써 돌려줄 포인터다. 함수가 직접 반환하는 skb 포인터와 구분한다.

04 struct sk_buff *skb = first;

아직 처리하지 않은 목록의 첫 skb를 현재 처리 대상으로 잡는다.

05 int rc = NETDEV_TX_OK;

반복을 시작할 때 결과 코드를 NETDEV_TX_OK로 초기화한다. 이 값만으로 드라이버 호출이 끝났다는 뜻은 아니다.

07 while (skb) {

현재 skb가 있는 동안 목록을 한 항목씩 처리한다.

08 struct sk_buff *next = skb->next;

현재 skb가 아래 콜백 경로에서 소비될 수 있으므로 다음 항목을 호출 전에 보관한다.

10 skb_mark_not_on_list(skb);

현재 skb의 목록 연결 표시를 정리한다. 다음 항목의 주소는 앞줄의 next에 이미 보관했다.

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

xmit_one()을 통해 드라이버 시작 콜백 경로로 현재 skb를 넘긴다. next 유무는 more 인자로 함께 전달되며, 물리 송신 완료를 여기서 확인하지 않는다.

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

dev_xmit_complete(rc)는 rc < NET_XMIT_MASK일 때 skb가 소비된 것으로 분류한다. 음수 오류도 참일 수 있으므로 전송 성공 판정이 아니며 NIC의 비동기 완료 통지도 아니다.

13 skb->next = next;

현재 skb가 소비되지 않은 결과라면 보관한 next를 다시 이어 남은 목록을 유지한다.

14 goto out;

현재 skb를 가리킨 채 반환 위치로 이동한다. 반환된 목록을 어떻게 처리할지는 호출 경로가 결정한다.

17 skb = next;

현재 skb가 소비된 경로에서 보관한 다음 skb로 처리 대상을 옮긴다.

18 if (netif_tx_queue_stopped(txq) && skb) {

TX 큐가 멈췄고 다음 skb도 남았는지 함께 검사한다. 둘 중 하나라도 거짓이면 이 분기로 빠지지 않는다.

19 rc = NETDEV_TX_BUSY;

이미 소비된 현재 skb와 구분해, 큐 중지 때문에 남은 다음 skb에 대한 BUSY 결과 코드를 설정한다.

20 break;

남은 skb를 유지하고 반복을 멈춘다. 바로 위 분기는 다음 skb가 있을 때만 실행된다.

24out:

현재 skb 미소비 분기, 큐 중지에 따른 반복 종료, 정상 종료가 합류하는 반환 위치다.

25 *ret = rc;

시작 콜백 결과 또는 남은 목록에 대한 BUSY 코드를 호출자가 준 저장 위치에 기록한다.

26 return skb;

남은 첫 skb를 반환한다. 현재 skb가 미소비면 현재 항목, 큐 중지 뒤면 다음 항목, 모두 처리됐으면 NULL이다.

읽기 기준: xmit_one() 반환 코드가 현재 skb 미소비를 뜻하면 현재 skb를, 소비 뒤 TX queue가 중지되고 다음 skb가 남으면 다음 skb를 반환한다. dev_xmit_complete(rc)는 NIC의 비동기 TX completion을 확인하는 함수가 아니다.

줄별 핵심: L3588–3594는 callback 결과와 목록 복원, L3596–3605는 queue stop과 잔여 skb 반환이다.

함수별 분석: qdisc 정책과 driver start의 간격

__dev_queue_xmit()은 skb를 생성하지 않는다. qdisc가 있더라도 모든 skb가 backlog에 삽입되는 것은 아니다. 빈 qdisc의 bypass 조건에서는 sch_direct_xmit()으로 바로 갈 수 있다.

그림 2. qdisc 내부의 bypass·enqueue와 noqueue의 직접 호출
q->enqueue 있음
__dev_xmit_skb()TCQ_F_CAN_BYPASS와 빈 큐 등 조건 확인
bypass 또는 enqueue잠금 경로의 비활성 qdisc는 drop; NOLOCK은 별도. bypass는 sch_direct_xmit, enqueue 뒤 run은 조건부
q->enqueue 없음
noqueue 검사UP → 재귀 → skb 검증 → TX lock → stopped 검사
dev_hard_start_xmit()xmit_one() → netdev_start_xmit()

qdisc도 결국 조건에 따라 driver 호출에 도달한다. qdisc enqueue 시점과 driver callback 시점을 같은 사건으로 세지 않는다. qdisc 경로 L3774–3873.

함수입력/역할반환 뒤 의미근거
__dev_queue_xmit완성된 skb를 받아 queue·qdisc/noqueue 선택skb는 반환값과 무관하게 소비됨; 배송 완료 아님L4256–4337
__dev_xmit_skbNOLOCK 분기 별도; 잠금 경로에서 비활성 qdisc는 drop; bypass/enqueue는 조건부qdisc 처리 결과; 즉시 driver 호출은 조건부L3774–3873
dev_hard_start_xmitskb 목록에서 xmit_one 반복미전송 목록과 callback 결과를 caller에게 반환L3579–3605

구조체와 소유권

packet 객체, qdisc 정책, device TX queue, driver ring은 서로 다른 층이다. __dev_queue_xmit()에 넘긴 skb는 반환값과 무관하게 소비된다. 반면 그 아래의 dev_hard_start_xmit()는 미소비 잔여 목록의 시작 포인터와 결과 코드 *ret를 따로 돌려준다. 두 함수의 계약을 섞어 읽으면 안 된다.

그림 3. skb의 소유권과 관찰 대상은 서로 이동한다
packetstruct sk_buff호출자가 구성해 전달. qdisc·driver 경로에서 소비·보류·drop 가능.
policy / queueQdisc + netdev_queueqdisc의 enqueue 정책과 TX queue의 stopped 상태를 별도로 본다.
device-specificTX ring + completiondriver가 전송을 수락한 뒤의 자원 회수는 driver별 비동기 경로다.

드라이버 queue의 byte limit(BQL)은 적용 드라이버의 별도 상태이며, 이 dev.c 발췌만으로 모든 장치의 사용을 단정할 수 없다.

객체여기서 확인할 것잘못 연결하기 쉬운 상태
sk_buff__dev_queue_xmit: 반환값과 무관하게 소비. dev_hard_start_xmit: 미소비 현재 skb 또는 아직 처리하지 않은 다음 skb부터 잔여 목록 반환호출자 재시도용 원래 skb가 남는다고 가정
Qdiscenqueue 유무와 bypass/queueing 조건qdisc 존재 = 반드시 backlog 삽입
netdev_queue선택한 TX queue, stopped 상태qdisc backlog와 driver ring full을 동일시
driver TX ringndo_start_xmit 이후 driver별 완료/회수dev_queue_xmit 반환 = HW 완료

실행 단계와 결과가 확정되는 시점

qdisc의 enqueue, driver의 전송 수락, 실제 장치 완료는 서로 다른 사건이다. dev_xmit_complete(rc)라는 이름은 driver callback의 반환 코드 분류이며, wire completion 신호가 아니다.

그림 4. 함수 반환과 비동기 TX completion 사이의 시간 경계
software pathqdisc 선택/실행enqueue될 수도, 조건부 bypass될 수도 있다.
driver callbackndo_start_xmit 결과소비 결과 또는 BUSY/잔여 skb. 이것이 dev.c의 관측 경계다.
later · driver-specificTX completion장치 전송·descriptor 회수는 드라이버별 완료 경로에서 확인한다.
시간 순서가 같더라도 qdisc queueing과 driver ring queueing은 별개 통계다.
지점확정된 것아직 확정되지 않은 것
q->enqueue 처리qdisc의 반환 코드 또는 bypass 실행driver 수락·장치 완료
xmit_one / dev_hard_start_xmitdriver start callback 반환과 미전송 목록실제 송신 완료·상대 수신
driver별 TX completion해당 driver가 완료 확인·ring 자원 회수상대방 application 전달

불변 조건과 실패 사례

skb 소비 계약

__dev_queue_xmit()은 반환값과 관계없이 skb를 소비한다. 같은 포인터로 무심코 재시도하면 use-after-free 또는 중복 송신을 만들 수 있다.

qdisc는 조건부

q->enqueue가 없으면 noqueue 경로다. 있더라도 TCQ_F_CAN_BYPASS 조건에서는 backlog에 넣지 않고 바로 실행할 수 있다.

미전송 목록

dev_hard_start_xmit()은 현재 skb 미소비이면 현재 skb를, 소비 뒤 queue stop과 다음 skb가 함께 있으면 다음 skb부터 돌려준다. 그 목록의 후속 처리 책임은 호출 경로에 따라 다르다.

버전별 분기

noqueue에서는 dev_xmit_complete(rc)가 false이면 남은 skb를 오류 경로에서 폐기한다(L4365–4397). 이 처리를 다른 버전의 코드로 대체하지 않는다.

v6.6 timestamp 분기에는 SKBTX_SCHED_TSTAMP 조건이 사용된다. 둘의 코드가 완전히 같다고 취급하지 않는다.

읽기 전용 계측과 검증

전역 tracer/filter를 바꾸지 않고 현 상태와 지원 이벤트를 먼저 읽는다. 이벤트가 꺼져 있으면 이 명령만으로 새 실행 기록은 얻지 못한다. 계측이 필요하면 보드 소유자의 절차에 따라 범위와 복구 방법을 정한다.

읽기 전용 확인구분할 사건한계
cat /sys/kernel/tracing/events/net/net_dev_queue/formatTX queue tracepoint의 필드존재 여부는 구성에 따름
cat /sys/kernel/tracing/events/net/net_dev_xmit/formatdriver start 결과 tracepoint의 필드HW completion 아님
tc -s qdisc show dev <iface>qdisc backlog·dropdriver ring 상황을 직접 증명하지 않음
ip -s link show dev <iface>, ethtool -S <iface>device/driver counter이름·의미는 driver마다 다름
안전한 시작:
cat /sys/kernel/tracing/current_tracer
cat /sys/kernel/tracing/events/net/net_dev_queue/enable
tc -s qdisc show dev <iface>
ethtool -S <iface>

다음에 확인할 질문

  • 해당 보드의 qdisc는 enqueue, bypass, noqueue 중 실제 어느 경로를 밟는가?
  • NETDEV_TX_BUSY와 queue stop 때 남는 skb 목록은 호출 경로가 어떻게 처리하는가?
  • driver의 TX completion은 어떤 IRQ/NAPI/worker에서 ring descriptor와 skb를 회수하는가?
  • qdisc drop, driver drop, ring stop을 어느 counter 조합으로 구분할 수 있는가?