읽는 지도
network stack은 sk_buff가 driver, NAPI, protocol stack, socket, qdisc 사이를 이동하는 data-plane이다. throughput만 보면 안 되고, drop attribution과 CPU affinity를 같이 봐야 한다.
함수 이름보다 입력 객체와 출력 객체를 먼저 본다. 이 토픽에서 어떤 구조체가 생성, 연결, publish, retire되는지 표시한다.
정상 경로와 실패 경로를 같은 무게로 본다. kernel code의 품질은 성공 path보다 error unwind, hotplug, teardown에서 더 잘 드러난다.
왼쪽은 입력 또는 상위 계층이고, 오른쪽으로 갈수록 실제 상태 변경이 커진다. 문C식으로 읽을 때는 이 그림을 먼저 머리에 놓고 코드 조각을 끼워 넣는다.
원본 코드 좌표
Linux 6.18.37 LTS에서 열 파일
Linux 6.18.37 LTS: net/core/dev.c
Linux 6.18.37 LTS: include/linux/netdevice.h
Linux 6.18.37 LTS: drivers/net/ethernet/*
설명: 첫 파일은 보통 진입 함수가 있는 곳이고, 나머지는 구조체 정의, architecture glue, callback 구현을 확인할 때 같이 연다. 파일을 여러 개 놓고 봐야 이 토픽의 boundary가 보인다.
GitHub 원본:
net/core/dev.c include/linux/netdevice.h drivers/net/ethernet/*
대표 코드
register_netdevice
* Adam Sulmicki : Bug Fix : Network Device Unload
* A network device unload needs to purge
* the backlog queue.
* Paul Rusty Russell : SIOCSIFNAME
* Pekka Riikonen : Netdev boot-time settings code
* Andrew Morton : Make unregister_netdevice wait
* indefinitely on dev->refcnt
* J Hadi Salim : - Backlog queue sampling
* - netif_rx() feedback
*/
#include <linux/uaccess.h>
#include <linux/bitmap.h>
#include <linux/capability.h>
#include <linux/cpu.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/hash.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/sched/mm.h>
#include <linux/mutex.h>
#include <linux/rwsem.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/socket.h>
#include <linux/sockios.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/if_ether.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/ethtool.h>
#include <linux/skbuff.h>
#include <linux/kthread.h>
#include <linux/bpf.h>
#include <linux/bpf_trace.h>
#include <net/net_namespace.h>
#include <net/sock.h>
#include <net/busy_poll.h>
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
netdev_run_todo
* safely in order to wait for the netdev refcnt to drop to zero.
*
* We must not return until all unregister events added during
* the interval the lock was held have been completed.
*/
void netdev_run_todo(void)
{
struct net_device *dev, *tmp;
struct list_head list;
#ifdef CONFIG_LOCKDEP
struct list_head unlink_list;
list_replace_init(&net_unlink_list, &unlink_list);
while (!list_empty(&unlink_list)) {
struct net_device *dev = list_first_entry(&unlink_list,
struct net_device,
unlink_list);
list_del_init(&dev->unlink_list);
dev->nested_level = dev->lower_level - 1;
}
#endif
/* Snapshot list, allow later requests */
list_replace_init(&net_todo_list, &list);
__rtnl_unlock();
/* Wait for rcu callbacks to finish before next phase */
if (!list_empty(&list))
rcu_barrier();
list_for_each_entry_safe(dev, tmp, &list, todo_list) {
if (unlikely(dev->reg_state != NETREG_UNREGISTERING)) {
netdev_WARN(dev, "run_todo but not unregistering\n");
list_del(&dev->todo_list);
continue;
}
write_lock(&dev_base_lock);
읽는 법: 이 절편에서 먼저 볼 것은 return 값이 아니라 상태 변경이다. 어떤 lock을 잡은 뒤 어떤 필드를 바꾸는지, 실패하면 어느 label로 빠지는지, 그리고 바뀐 상태를 다음 호출자가 어떤 전제로 소비하는지 표시한다.
함수별 독해
struct net_device를 driver core와 network stack에 등록해 RX/TX datapath가 사용할 interface 객체를 publish한다.
이 섹션은 원본 코드 발췌를 함수 이름 단위로 끊어, 각 함수가 어떤 전제 조건을 만들고 다음 함수가 무엇을 소비하는지 추적한다.
각 노드는 독립 함수가 아니라 전제 조건을 생산하고 소비하는 연결점이다. 코드를 읽을 때는 노드 사이에서 어떤 필드가 바뀌는지 표시한다.
1. register_netdevice
register_netdevice 주변에서는 net_device를 중심으로 본다. 이 필드는 interface state and ops 역할을 하므로, 함수가 끝날 때 registered 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 name, sysfs, rtnl publish을 전제로 삼을 수 있다.
자주 틀리는 해석: register_netdev와 ndo_open을 같은 시점으로 봄
2. netdev_run_todo
netdev_run_todo 주변에서는 napi_struct를 중심으로 본다. 이 필드는 RX polling context 역할을 하므로, 함수가 끝날 때 registered 상태가 실제로 성립했는지 확인해야 한다.
원본 코드에서 볼 순서는 입력 범위 검증, 중심 필드 갱신, 다른 계층에 보이는 publish 지점, 실패 시 되돌림 순서다. 이 네 칸이 맞아야 다음 함수가 name, sysfs, rtnl publish을 전제로 삼을 수 있다.
자주 틀리는 해석: rtnl_lock 보호 범위를 놓침
| 함수 | 입력 | 상태 변경 | 검증 질문 |
|---|---|---|---|
| register_netdevice | net_device, caller context, subsystem 전제 조건 | registered: name, sysfs, rtnl publish | register_netdev와 ndo_open을 같은 시점으로 봄 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
| netdev_run_todo | napi_struct, caller context, subsystem 전제 조건 | registered: name, sysfs, rtnl publish | rtnl_lock 보호 범위를 놓침 문제를 코드상 어느 조건문 또는 error label에서 분리하는가 |
구조체 / 필드 해설
여기서는 “어떤 구조체가 있다”가 아니라 그 필드가 어느 단계에서 쓰기 가능하고 어느 단계부터 관찰 가능한지를 본다. 박사급 리뷰에서는 필드의 뜻보다 보호 규칙이 먼저다.
필드는 구조체 안에 흩어져 있지만, 실제 실행에서는 위 순서로 의미가 이어진다.
| 필드 | 읽는 법 | 확인 |
|---|---|---|
| net_device | interface state and ops | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| napi_struct | RX polling context | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| netdev_queue | TX queue state | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
| ndo_ops | driver callbacks | 누가 쓰고, 누가 보호하고, 언제 lifetime이 끝나는지 원본 코드에서 확인 |
상태 전이 그림
상태 전이를 따로 그리는 이유는 정상 path와 실패 path를 같은 표에서 보기 위해서다. 커널 regression은 흔히 마지막 상태가 아니라 중간 상태를 외부에 publish한 뒤 rollback하지 못해서 생긴다.
상태 전이는 단방향처럼 그렸지만, 실패 경로에서는 대부분 역순 rollback이 붙는다.
| 상태 | 의미 | 진입 조건 | 깨지는 지점 |
|---|---|---|---|
| allocated | driver private 포함 객체 생성 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| registered | name, sysfs, rtnl publish | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| opened | ndo_open으로 hardware 활성 | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
| unregistered | datapath quiesce and free | 앞 단계 함수가 전제 조건을 만들고 error path가 정리된 뒤 | 다음 단계가 이 상태를 너무 일찍 소비하거나 늦게 정리할 때 |
불변조건 / 실패 케이스
netdev 의 중심 객체가 publish된 뒤에는 마지막 참조가 사라지기 전까지 teardown path가 모든 callback, timer, IRQ, worker와 경합하지 않아야 한다.
상태 필드를 바꾼 뒤 다른 CPU나 하위 계층이 관찰할 수 있다면 lock, barrier, refcount, RCU 중 어느 장치가 visibility를 보장하는지 확인한다.
중간 단계 실패는 성공 단계의 역순으로 되돌아가야 한다. goto label이 많은 코드는 label 이름보다 어느 resource가 이미 획득됐는지를 표로 적는다.
embedded bring-up에서는 panic보다 silence, timeout, deferred probe, interrupt flood처럼 간접 증상으로 드러나는 경우가 많다.
register_netdev와 ndo_open을 같은 시점으로 봄
rtnl_lock 보호 범위를 놓침
unregister 중 NAPI/IRQ가 남는 race를 무시함
계측 / 검증
계측은 printk 위치 경쟁이 아니라 가설 검증이다. 먼저 위 상태표에서 멈춘 state를 정하고, 그 state를 바꾸는 함수와 그 결과를 소비하는 함수를 동시에 본다.
| 도구 | 보는 것 | 해석 |
|---|---|---|
| net_dev_start_xmit | netdev 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| netif_receive_skb | netdev 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| ip link | netdev 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
| ethtool -S | netdev 의 상태 전이가 어느 지점에서 멈추는지 확인 | 로그가 찍힌 위치를 완료 시점으로 단정하지 말고, 바로 앞뒤 필드 변경을 원본에서 확인 |
# 예시: tracefs가 켜진 보드에서 토픽별 event를 좁혀 본다.
mount -t tracefs nodev /sys/kernel/tracing
echo function_graph > /sys/kernel/tracing/current_tracer
echo ':mod:*' > /sys/kernel/tracing/set_ftrace_filter
cat /sys/kernel/tracing/trace_pipe
연구 질문
- netdev 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
- netdev 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
- 실험으로 확인한다면 'register_netdev와 ndo_open을 같은 시점으로 봄' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
- 실험으로 확인한다면 'rtnl_lock 보호 범위를 놓침' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?