전체 흐름: 이미 만들어진 skb의 송신 선택
__dev_queue_xmit()은 호출자가 장치·우선순위와 버퍼 내용을 준비한 skb를 받는다. TX 큐를 고른 뒤 qdisc의 enqueue 유무로 경로가 갈린다. RX의 NAPI는 이 TX 함수의 필수 선행 단계가 아니다.
dev_hard_start_xmit()으로 진행한다.__dev_queue_xmit() 반환을 NIC 송신 완료나 상대방 수신 확인으로 바꾸지 않는다.소스 코드 위치
소비 판정은 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 선택
/**
* __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: 미전송 목록 반환
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행. 왼쪽 번호는 위 발췌문의 줄 번호이며 고정 원문과 대조한 행만 설명한다.
struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,송신 대기 목록의 첫 skb와 대상 네트워크 장치를 받는다. 이 인자 자체가 NIC 전송 완료를 뜻하지 않는다.
struct netdev_queue *txq, int *ret)txq는 사용할 송신 큐이고 ret은 결과 코드를 써 돌려줄 포인터다. 함수가 직접 반환하는 skb 포인터와 구분한다.
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의 목록 연결 표시를 정리한다. 다음 항목의 주소는 앞줄의 next에 이미 보관했다.
rc = xmit_one(skb, dev, txq, next != NULL);xmit_one()을 통해 드라이버 시작 콜백 경로로 현재 skb를 넘긴다. next 유무는 more 인자로 함께 전달되며, 물리 송신 완료를 여기서 확인하지 않는다.
if (unlikely(!dev_xmit_complete(rc))) {dev_xmit_complete(rc)는 rc < NET_XMIT_MASK일 때 skb가 소비된 것으로 분류한다. 음수 오류도 참일 수 있으므로 전송 성공 판정이 아니며 NIC의 비동기 완료 통지도 아니다.
skb->next = next;현재 skb가 소비되지 않은 결과라면 보관한 next를 다시 이어 남은 목록을 유지한다.
goto out;현재 skb를 가리킨 채 반환 위치로 이동한다. 반환된 목록을 어떻게 처리할지는 호출 경로가 결정한다.
skb = next;현재 skb가 소비된 경로에서 보관한 다음 skb로 처리 대상을 옮긴다.
if (netif_tx_queue_stopped(txq) && skb) {TX 큐가 멈췄고 다음 skb도 남았는지 함께 검사한다. 둘 중 하나라도 거짓이면 이 분기로 빠지지 않는다.
rc = NETDEV_TX_BUSY;이미 소비된 현재 skb와 구분해, 큐 중지 때문에 남은 다음 skb에 대한 BUSY 결과 코드를 설정한다.
break;남은 skb를 유지하고 반복을 멈춘다. 바로 위 분기는 다음 skb가 있을 때만 실행된다.
out:현재 skb 미소비 분기, 큐 중지에 따른 반복 종료, 정상 종료가 합류하는 반환 위치다.
*ret = rc;시작 콜백 결과 또는 남은 목록에 대한 BUSY 코드를 호출자가 준 저장 위치에 기록한다.
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()으로 바로 갈 수 있다.
TCQ_F_CAN_BYPASS와 빈 큐 등 조건 확인sch_direct_xmit, enqueue 뒤 run은 조건부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_skb | NOLOCK 분기 별도; 잠금 경로에서 비활성 qdisc는 drop; bypass/enqueue는 조건부 | qdisc 처리 결과; 즉시 driver 호출은 조건부 | L3774–3873 |
dev_hard_start_xmit | skb 목록에서 xmit_one 반복 | 미전송 목록과 callback 결과를 caller에게 반환 | L3579–3605 |
구조체와 소유권
packet 객체, qdisc 정책, device TX queue, driver ring은 서로 다른 층이다. __dev_queue_xmit()에 넘긴 skb는 반환값과 무관하게 소비된다. 반면 그 아래의 dev_hard_start_xmit()는 미소비 잔여 목록의 시작 포인터와 결과 코드 *ret를 따로 돌려준다. 두 함수의 계약을 섞어 읽으면 안 된다.
드라이버 queue의 byte limit(BQL)은 적용 드라이버의 별도 상태이며, 이 dev.c 발췌만으로 모든 장치의 사용을 단정할 수 없다.
| 객체 | 여기서 확인할 것 | 잘못 연결하기 쉬운 상태 |
|---|---|---|
sk_buff | __dev_queue_xmit: 반환값과 무관하게 소비. dev_hard_start_xmit: 미소비 현재 skb 또는 아직 처리하지 않은 다음 skb부터 잔여 목록 반환 | 호출자 재시도용 원래 skb가 남는다고 가정 |
Qdisc | enqueue 유무와 bypass/queueing 조건 | qdisc 존재 = 반드시 backlog 삽입 |
netdev_queue | 선택한 TX queue, stopped 상태 | qdisc backlog와 driver ring full을 동일시 |
| driver TX ring | ndo_start_xmit 이후 driver별 완료/회수 | dev_queue_xmit 반환 = HW 완료 |
실행 단계와 결과가 확정되는 시점
qdisc의 enqueue, driver의 전송 수락, 실제 장치 완료는 서로 다른 사건이다. dev_xmit_complete(rc)라는 이름은 driver callback의 반환 코드 분류이며, wire completion 신호가 아니다.
dev.c의 관측 경계다.| 지점 | 확정된 것 | 아직 확정되지 않은 것 |
|---|---|---|
q->enqueue 처리 | qdisc의 반환 코드 또는 bypass 실행 | driver 수락·장치 완료 |
xmit_one / dev_hard_start_xmit | driver start callback 반환과 미전송 목록 | 실제 송신 완료·상대 수신 |
| driver별 TX completion | 해당 driver가 완료 확인·ring 자원 회수 | 상대방 application 전달 |
불변 조건과 실패 사례
__dev_queue_xmit()은 반환값과 관계없이 skb를 소비한다. 같은 포인터로 무심코 재시도하면 use-after-free 또는 중복 송신을 만들 수 있다.
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/format | TX queue tracepoint의 필드 | 존재 여부는 구성에 따름 |
cat /sys/kernel/tracing/events/net/net_dev_xmit/format | driver start 결과 tracepoint의 필드 | HW completion 아님 |
tc -s qdisc show dev <iface> | qdisc backlog·drop | driver 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 조합으로 구분할 수 있는가?