전체 흐름
struct net_device의 등록, IFF_UP에 따른 열기·닫기, 등록 해제는 같은 함수 체인이 아니다. register_netdev()는 등록 wrapper이고, __dev_change_flags()는 별도 관리 요청, netdev_run_todo()는 unregister 뒤의 참조 정리다.
세 줄은 조건과 시간이 다른 경로다. 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
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
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과 해제 조건을 숨기게 된다.
START 후: 주소 검증/ndo_open 오류 → START 해제
열기 실패는 등록 취소를 뜻하지 않는다. 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 집합이다.
등록 상태 · 관리 flags · datapath state · driver callback · 연관 자원
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()는 열린 장치만 먼저 닫고 목록에서 제외한다.
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 결과를 확인한다.
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 참조를 어떻게 분리해 확인할 수 있는가?