Network · Linux v6.6 · source + diagram note

netdev

struct net_device의 등록, IFF_UP 열기·닫기, unregister 뒤 참조 정리.

전체 흐름

struct net_device의 등록, IFF_UP에 따른 열기·닫기, 등록 해제는 같은 함수 체인이 아니다. register_netdev()는 등록 wrapper이고, __dev_change_flags()는 별도 관리 요청, netdev_run_todo()는 unregister 뒤의 참조 정리다.

그림 1. 등록 · 관리 up/down · 해제의 세 경계
① 등록
register_netdev() rtnl_lock_killable()
→
register_netdevice() 이름 · ndo_init · kobject · 목록 · notifier
→
NETREG_REGISTERED 등록 성공 ≠ 열린 장치
등록 완료만으로 ndo_open, link/carrier up이나 패킷 송수신이 성립하지 않는다.
② 이후 별도 IFF_UP 관리 요청
IFF_UP 변화 __dev_change_flags()
→
__dev_open() present · PRE_UP · 조건부 ndo_open
→
결과 성공: up · qdisc / 실패: START 전 조기 반환·후 해제
③ 별도 unregister 경로
unregister_netdev() 해제 요청 · dismantle
→
열려 있다면 close 먼저 START 해제 · qdisc 종료 · ndo_stop 조건부
→
unlist_netdevice() 그 뒤 목록 제거 · UNREGISTERING
→
netdev_run_todo() RCU · ref 대기 · 조건부 free

세 줄은 조건과 시간이 다른 경로다. unregister 때 이미 down이면 ndo_stop도 호출되지 않는다.

경계 코드의 상태 구분할 점
등록 NETREG_REGISTERED · NETDEV_REGISTER ndo_open은 별도 요청
열기·닫기 IFF_UP · __LINK_STATE_START driver callback은 조건부
등록 해제 UNREGISTERING → UNREGISTERED todo는 등록 함수의 후속 단계가 아님

소스 코드 위치

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

원본 코드:
Linux v6.6: net/core/dev.c
Linux v6.6: include/linux/netdevice.h
Linux v6.6: drivers/net/ethernet/*

설명: net/core/dev.c의 제어 경로, 구조체 정의와 개별 드라이버 callback 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

GitHub 원본:

net/core/dev.c include/linux/netdevice.h drivers/net/ethernet/*

대표 코드

register_netdevice

원본 코드: net/core/dev.c:10062-10229 · v6.6 · 로컬 원문
register_netdevice 함수 전체 (168줄)
int register_netdevice(struct net_device *dev)
{
	int ret;
	struct net *net = dev_net(dev);

	BUILD_BUG_ON(sizeof(netdev_features_t) * BITS_PER_BYTE <
		     NETDEV_FEATURE_COUNT);
	BUG_ON(dev_boot_phase);
	ASSERT_RTNL();

	might_sleep();

	/* When net_device's are persistent, this will be fatal. */
	BUG_ON(dev->reg_state != NETREG_UNINITIALIZED);
	BUG_ON(!net);

	ret = ethtool_check_ops(dev->ethtool_ops);
	if (ret)
		return ret;

	spin_lock_init(&dev->addr_list_lock);
	netdev_set_addr_lockdep_class(dev);

	ret = dev_get_valid_name(net, dev, dev->name);
	if (ret < 0)
		goto out;

	ret = -ENOMEM;
	dev->name_node = netdev_name_node_head_alloc(dev);
	if (!dev->name_node)
		goto out;

	/* Init, if this function is available */
	if (dev->netdev_ops->ndo_init) {
		ret = dev->netdev_ops->ndo_init(dev);
		if (ret) {
			if (ret > 0)
				ret = -EIO;
			goto err_free_name;
		}
	}

	if (((dev->hw_features | dev->features) &
	     NETIF_F_HW_VLAN_CTAG_FILTER) &&
	    (!dev->netdev_ops->ndo_vlan_rx_add_vid ||
	     !dev->netdev_ops->ndo_vlan_rx_kill_vid)) {
		netdev_WARN(dev, "Buggy VLAN acceleration in driver!\n");
		ret = -EINVAL;
		goto err_uninit;
	}

	ret = dev_index_reserve(net, dev->ifindex);
	if (ret < 0)
		goto err_uninit;
	dev->ifindex = ret;

	/* Transfer changeable features to wanted_features and enable
	 * software offloads (GSO and GRO).
	 */
	dev->hw_features |= (NETIF_F_SOFT_FEATURES | NETIF_F_SOFT_FEATURES_OFF);
	dev->features |= NETIF_F_SOFT_FEATURES;

	if (dev->udp_tunnel_nic_info) {
		dev->features |= NETIF_F_RX_UDP_TUNNEL_PORT;
		dev->hw_features |= NETIF_F_RX_UDP_TUNNEL_PORT;
	}

	dev->wanted_features = dev->features & dev->hw_features;

	if (!(dev->flags & IFF_LOOPBACK))
		dev->hw_features |= NETIF_F_NOCACHE_COPY;

	/* If IPv4 TCP segmentation offload is supported we should also
	 * allow the device to enable segmenting the frame with the option
	 * of ignoring a static IP ID value.  This doesn't enable the
	 * feature itself but allows the user to enable it later.
	 */
	if (dev->hw_features & NETIF_F_TSO)
		dev->hw_features |= NETIF_F_TSO_MANGLEID;
	if (dev->vlan_features & NETIF_F_TSO)
		dev->vlan_features |= NETIF_F_TSO_MANGLEID;
	if (dev->mpls_features & NETIF_F_TSO)
		dev->mpls_features |= NETIF_F_TSO_MANGLEID;
	if (dev->hw_enc_features & NETIF_F_TSO)
		dev->hw_enc_features |= NETIF_F_TSO_MANGLEID;

	/* Make NETIF_F_HIGHDMA inheritable to VLAN devices.
	 */
	dev->vlan_features |= NETIF_F_HIGHDMA;

	/* Make NETIF_F_SG inheritable to tunnel devices.
	 */
	dev->hw_enc_features |= NETIF_F_SG | NETIF_F_GSO_PARTIAL;

	/* Make NETIF_F_SG inheritable to MPLS.
	 */
	dev->mpls_features |= NETIF_F_SG;

	ret = call_netdevice_notifiers(NETDEV_POST_INIT, dev);
	ret = notifier_to_errno(ret);
	if (ret)
		goto err_ifindex_release;

	ret = netdev_register_kobject(dev);
	write_lock(&dev_base_lock);
	dev->reg_state = ret ? NETREG_UNREGISTERED : NETREG_REGISTERED;
	write_unlock(&dev_base_lock);
	if (ret)
		goto err_uninit_notify;

	__netdev_update_features(dev);

	/*
	 *	Default initial state at registry is that the
	 *	device is present.
	 */

	set_bit(__LINK_STATE_PRESENT, &dev->state);

	linkwatch_init_dev(dev);

	dev_init_scheduler(dev);

	netdev_hold(dev, &dev->dev_registered_tracker, GFP_KERNEL);
	list_netdevice(dev);

	add_device_randomness(dev->dev_addr, dev->addr_len);

	/* If the device has permanent device address, driver should
	 * set dev_addr and also addr_assign_type should be set to
	 * NET_ADDR_PERM (default value).
	 */
	if (dev->addr_assign_type == NET_ADDR_PERM)
		memcpy(dev->perm_addr, dev->dev_addr, dev->addr_len);

	/* Notify protocols, that a new device appeared. */
	ret = call_netdevice_notifiers(NETDEV_REGISTER, dev);
	ret = notifier_to_errno(ret);
	if (ret) {
		/* Expect explicit free_netdev() on failure */
		dev->needs_free_netdev = false;
		unregister_netdevice_queue(dev, NULL);
		goto out;
	}
	/*
	 *	Prevent userspace races by waiting until the network
	 *	device is fully setup before sending notifications.
	 */
	if (!dev->rtnl_link_ops ||
	    dev->rtnl_link_state == RTNL_LINK_INITIALIZED)
		rtmsg_ifinfo(RTM_NEWLINK, dev, ~0U, GFP_KERNEL, 0, NULL);

out:
	return ret;

err_uninit_notify:
	call_netdevice_notifiers(NETDEV_PRE_UNINIT, dev);
err_ifindex_release:
	dev_index_release(net, dev->ifindex);
err_uninit:
	if (dev->netdev_ops->ndo_uninit)
		dev->netdev_ops->ndo_uninit(dev);
	if (dev->priv_destructor)
		dev->priv_destructor(dev);
err_free_name:
	netdev_name_node_free(dev->name_node);
	goto out;
}

이 함수는 net_device 등록 진입점이다. RTNL 보유 전제, 초기 상태 검사, 이름 할당, driver 초기화와 오류 정리가 실제 본문에 있다. 등록 성공은 네트워크 선로의 link up이나 패킷 송수신 완료를 뜻하지 않는다.

netdev_run_todo

원본 코드: net/core/dev.c:10422-10484 · v6.6 · 로컬 원문
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);
		dev->reg_state = NETREG_UNREGISTERED;
		write_unlock(&dev_base_lock);
		linkwatch_forget_dev(dev);
	}

	while (!list_empty(&list)) {
		dev = netdev_wait_allrefs_any(&list);
		list_del(&dev->todo_list);

		/* paranoia */
		BUG_ON(netdev_refcnt_read(dev) != 1);
		BUG_ON(!list_empty(&dev->ptype_all));
		BUG_ON(!list_empty(&dev->ptype_specific));
		WARN_ON(rcu_access_pointer(dev->ip_ptr));
		WARN_ON(rcu_access_pointer(dev->ip6_ptr));

		if (dev->priv_destructor)
			dev->priv_destructor(dev);
		if (dev->needs_free_netdev)
			free_netdev(dev);

		if (atomic_dec_and_test(&dev_net(dev)->dev_unreg_count))
			wake_up(&netdev_unregistering_wq);

		/* Free network device */
		kobject_put(&dev->dev.kobj);
	}
}

확인 사항: 이 void 함수는 등록 해제 todo 목록을 스냅샷으로 떼고 RTNL을 푼 뒤 RCU callback과 남은 참조를 기다린다. reg_state 전환, 조건부 free_netdev와 kobject_put을 순서대로 읽는다.

함수별 분석

register_netdevice()는 등록하고, netdev_run_todo()는 unregister가 넘긴 객체의 늦은 정리를 수행한다. 둘을 직접 호출 체인으로 그리면 open/stop과 해제 조건을 숨기게 된다.

그림 2. open의 성공·실패와 별도의 stop
up 요청 분기
IFF_UP 변했나? 변화 없으면 open 호출 없음
→
__dev_open() 장치 present · NETDEV_PRE_UP 검사
→
START 설정 조기 실패가 없을 때
→
주소 검증 → ndo_open 각 callback이 있고 앞 검사 성공 시
열기 성공 dev->flags |= IFF_UP · dev_activate()
열기 실패 START 전: present/PRE_UP 조기 반환
START 후: 주소 검증/ndo_open 오류 → START 해제
down은 별도 요청이다. unregister 때는 IFF_UP인 장치만 먼저 닫는다.
down 요청 또는 unregister 중 열린 장치
dev_close_many() IFF_UP인 장치 선별
→
코어 quiesce START 해제 · qdisc 비활성
→
ndo_stop 존재 시 dev->flags &= ~IFF_UP · NETDEV_DOWN

열기 실패는 등록 취소를 뜻하지 않는다. stop callback 유무와 드라이버의 실제 NAPI/IRQ 정리는 따로 검증한다.

함수·사건 원문 근거 실제 결과
register_netdev() rtnl_lock_killable() RTNL wrapper; open 없음
register_netdevice() kobject·reg_state · NETDEV_REGISTER notifier 실패는 unregister unwind
__dev_open() present·PRE_UP·선택적 ndo_open 성공에만 up/qdisc; START 전 실패는 조기 반환, 설정 후 실패는 해제
dev_close_many() IFF_UP 선별·선택적 ndo_stop START 해제 · qdisc 종료 · down 통지
netdev_run_todo() UNREGISTERING·RCU·ref 조건부 free · kobject put

구조체와 필드

기존 net_device → napi_struct → netdev_queue → ndo_ops 직렬 그림은 연관 요소를 함수 실행 순서처럼 보이게 한다. 이들은 장치 객체의 상태·자원·callback 집합이다.

그림 3. net_device를 중심으로 본 상태와 연관 자원
struct net_device
등록 상태 · 관리 flags · datapath state · driver callback · 연관 자원
reg_state REGISTERED → UNREGISTERING → UNREGISTERED; IFF_UP와 독립
flags & IFF_UP 관리 up/down 상태; 물리 link/carrier와 다름
state / __LINK_STATE_START netif_running() 경계; open/close가 set/clear
netdev_ops ndo_init · ndo_open · ndo_stop · ndo_uninit 선택적 callback
napi_list · queues net_device의 연관 poll/queue 자원; 수명 단계 이름이 아님
needs_free_netdev · refs todo 뒤 free 여부와 참조 대기 조건

NAPI의 구체적 활성화·중지는 해당 드라이버 ndo_open/ndo_stop 구현과 대조해야 한다.

필드·관계 쓰기/읽기 경계 주의
reg_state dev_base_lock 아래 reg_state 대입 notifier 오류면 unregister unwind 가능
IFF_UP·START dev->flags |= IFF_UP / dev->flags &= ~IFF_UP carrier up은 별개
netdev_ops ndo_open / ndo_stop 각 callback 존재 시 호출
needs_free_netdev todo의 조건부 free 해제 즉시 메모리 반환 아님

실행 단계와 상태 변화

reg_state의 수명 단계와 IFF_UP의 관리 상태는 독립이다. 등록 성공 후에도 down일 수 있으며, unregister_netdevice_many_notify()는 열린 장치만 먼저 닫고 목록에서 제외한다.

그림 4. 등록 상태와 조건부 객체 회수
등록 상태
UNINITIALIZED driver 초기 준비
→
REGISTERED 등록·공개 뒤
→
UNREGISTERING 열린 장치 close 뒤 목록 제거
→
UNREGISTERED todo에서 ref 대기
REGISTERED 동안 IFF_UP은 별도 요청으로 up ↔ down 전환한다.
needs_free_netdev = true todo가 free_netdev 호출
needs_free_netdev = false todo가 직접 free 호출하지 않음

todo는 RCU callback과 남은 참조를 기다린다. per-net dev_unreg_count와 linkwatch_forget_dev()은 이 버전의 구현이다.

사건 근거 결과
등록 kobject·reg_state · NETDEV_REGISTER notifier 실패 시 unwind
열기 IFF_UP 변화 · ndo_open 결과 성공 up / START 전 실패는 미설정, 설정 후 실패는 해제
닫기·해제 IFF_UP 선별 · unlist stop 후 UNREGISTERING
늦은 정리 RCU·ref 대기 · free 조건 UNREGISTERED · 조건부 free · kobject put

불변 조건과 실패 사례

등록 ≠ 열기

NETDEV_REGISTER 뒤 ndo_open이 자동 실행되지 않는다. 별도 IFF_UP 요청과 open 결과를 확인한다.

callback 조건

ndo_open/ndo_stop은 ops에 있을 때 호출된다. 코어 stop 호출만으로 driver의 NAPI/IRQ 정리 품질을 단정할 수 없다.

등록 오류

kobject 등록 실패와 NETDEV_REGISTER notifier 실패의 정리 경로가 다르다. 오류 레이블을 함께 본다.

해제 수명

todo는 RTNL 해제 후 RCU callback·netdev 참조를 기다린다. 메모리 회수는 needs_free_netdev와 device ref에 좌우된다.

계측과 검증

전역 tracefs 설정을 바꾸는 명령은 다른 추적 사용자에게 영향을 준다. 먼저 읽기 전용 관찰로 등록, 관리 up, 패킷 전송을 구분한다.

관찰 답할 질문 한계
ip -d link show dev <ifname> 장치가 보이고 IFF_UP인가? ndo_open 호출 자체를 증명하지 않음
드라이버 로그·callback 반환값 ndo_open/ndo_stop이 호출됐는가? 드라이버 계측에 의존
ethtool -S <ifname> TX/RX/drop counter가 변하는가? data path 지표
unregister 지연 시 참조 점검 어떤 netdev ref가 남았는가? 소유한 실험 환경에서 범위를 정해 계측

추가 확인 사항

  • NETDEV_REGISTER notifier 실패는 어느 unregister 오류 정리로 이어지는가?
  • IFF_UP 요청 뒤 down이면 present, NETDEV_PRE_UP, 주소 검증, ndo_open 중 어느 단계가 실패했는가?
  • unregister 지연 시 stop 이후 NAPI/IRQ와 남은 netdev 참조를 어떻게 분리해 확인할 수 있는가?