Mountain Kernel 리팩터링: ABI를 깨지 말자

by gg582 · 2026-06-02 05:39:55 · 55 views

블로그 원본 diff → 현재 코드베이스 리팩터링 요약

원문: 비전공자도 쉽게 따라하는 리눅스 커널 해킹 — 지터를 잡아보자

본 문서는 해당 포스트의 패치가 별개 구조체 분리 + 추상화 헬퍼 도입 + 선택 로직 개선 형태로 리팩터링된 내용을 정리한다.


1. 핵심 구조 변경: dst_entry 직접 확장 → struct dst_power 분리

기존

struct dst_entry 맨 끝에 필드를 직접 박아 넣었다.

/* include/net/dst.h */
struct dst_entry {
    ...
    struct lwtunnel_state *lwtstate;
#endif
    /* USER ADDED */
    u64 ema_load;
    u64 ema_time_delta;
    u64 last_update_jiffies;
    unsigned int ema_k_factor;
    unsigned int power_cost_weight;
};

문제점

  • sizeof(struct dst_entry)가 커져서 캐시라인/페이지 경계를 넘을 위험
  • dst_entry를 포함하는 모든 경로 구조체(rtable, rt6_info, xfrm_dst 등)가 암묵적으로 같은 오프셋을 공유해야 함 → ABI 깨짐 위험

현재

struct dst_power별도 구조체로 분리하고, 실제 사용처(IPv4/IPv6 경로 구조체)에만 임베드한다.

/* include/net/dst.h */
struct dst_power {
    u64 ema_load;
    u64 ema_time_delta;
    u64 last_update_jiffies;
    unsigned int ema_k_factor;
    unsigned int power_cost_weight;
};

struct dst_power *dst_power_ptr(struct dst_entry *dst);
/* include/net/route.h */
struct rtable {
    struct dst_entry    dst;
    ...
    struct dst_power    power;   /* ← IPv4 경로에만 임베드 */
};

/* include/net/ip6_fib.h */
struct rt6_info {
    struct dst_entry    dst;
    ...
    struct dst_power    power;   /* ← IPv6 경로에만 임베드 */
};

이점

  • dst_entry의 크기/레이아웃이 그대로 유지됨 → 캐시라인과 ABI 보호
  • dst_power가 필요 없는 dst 파생 구조체에는 영향을 주지 않음
  • 향후 추가 프로토콜(SCTP, XFRM 등)에서도 선택적으로 임베드 가능

2. 접근 추상화: dst_power_ptr() 도입

기존

모든 코드가 dst->ema_load처럼 struct dst_entry를 직접 디레퍼런스.

현재

net/core/dst.c에 타입 분기 헬퍼를 중앙 집중화.

/* net/core/dst.c */
struct dst_power *dst_power_ptr(struct dst_entry *dst)
{
    if (!dst || !dst->ops)
        return NULL;

#if IS_ENABLED(CONFIG_IPV6)
    if (dst->ops->family == AF_INET6) {
        struct rt6_info *rt = (struct rt6_info *)dst;
        return &rt->power;
    }
#endif
    if (dst->ops->family == AF_INET) {
        struct rtable *rt = (struct rtable *)dst;
        return &rt->power;
    }
    return NULL;
}
EXPORT_SYMBOL(dst_power_ptr);

호출처 일원화

  • net/core/dev.cupdate_dst_ems_metrics()
  • net/ipv4/fib_semantics.ccalculate_lowpower_weight()
  • net/ipv6/route.ccalculate_lowpower_weight(), rt6_score_route()
  • net/ipv4/route.crt_dst_alloc() 초기화
  • net/ipv6/route.cip6_dst_alloc() 초기화
  • net/xfrm/xfrm_policy.cxfrm_bundle_create() 초기화
  • net/sctp/outqueue.ccalculate_lowpower_weight()

모든 곳에서 dst_power_ptr(dst) → NULL 체크 → 필드 접근 패턴으로 통일됨.


3. EMA 업데이트 로직

변경 폭은 적다. net/core/dev.cdev_hard_start_xmit() 성공 시 호출되는 update_dst_ems_metrics()dst_power_ptr()로 포인터를 얻어 쓰는 것 외에는 블로그 원본과 거의 동일하다.

static void update_dst_ems_metrics(struct dst_entry *dst, unsigned int tx_bytes)
{
    u64 cur_jiffies = get_jiffies_64();
    struct dst_power *p = dst_power_ptr(dst);
    u64 cur_load_rate;
    u64 delta_t;

    if (!p)
        return;

    delta_t = cur_jiffies - READ_ONCE(p->last_update_jiffies);
    if (!delta_t)
        return;

    cur_load_rate = tx_bytes / delta_t;

    WRITE_ONCE(p->ema_load,
               EMA_UPDATE(READ_ONCE(p->ema_k_factor),
                          READ_ONCE(p->ema_load), cur_load_rate));

    u64 diff = (cur_load_rate > READ_ONCE(p->ema_load))
        ? cur_load_rate - READ_ONCE(p->ema_load)
        : READ_ONCE(p->ema_load) - cur_load_rate;

    WRITE_ONCE(p->ema_time_delta,
               EMA_UPDATE(READ_ONCE(p->ema_k_factor),
                          READ_ONCE(p->ema_time_delta), diff));
    WRITE_ONCE(p->last_update_jiffies, cur_jiffies);
}

4. 멀티패스 선택 로직 개선 (fib_select_multipath)

기존

두 단계 루프로 구성.

  1. 1st loop: 모든 nexthop을 순회하며 calculate_lowpower_weight()를 평가 → max_ema_weight를 가진 단일 nexthop을 선별.
  2. 2nd loop: weight가 모두 0이면 기존 fib_nh_upper_bound + hash 기반 ECMP fallback.

문제: 루프가 중복되고, hash/saddr/upper_bound 같은 기존 정책과 lowpower 정책이 서로 독립적으로 동작함.

현재

단일 루프로 통합. nh_score 기반 우선순위 + tie-breaker 개념 도입.

void fib_select_multipath(struct fib_result *res, int hash,
                          const struct flowi4 *fl4)
{
    ...
    int score = -1;
    int lowpower_nh_index = -1;
    long max_ema_weight = -1;

    change_nexthops(fi) {
        int nh_score = 0;
        ...

        /* 1) 기존 정책 점수화 */
        if (saddr && nexthop_nh->nh_saddr == saddr)
            nh_score += 2;
        if (hash <= nh_upper_bound)
            nh_score++;

        /* 2) lowpower weight는 tie-breaker */
        dst = get_dst_entry_from_nhc(&nexthop_nh->nh_common);
        current_weight = calculate_lowpower_weight(dst);

        if (nh_score > score ||
            (nh_score == score && current_weight > max_ema_weight)) {
            score = nh_score;
            max_ema_weight = current_weight;
            lowpower_nh_index = nhsel;

            /* 3) 완벽한 매칭이면 즉시 리턴 */
            if (nh_score == 3 || (!saddr && nh_score == 1)) {
                res->nh_sel = nhsel;
                res->nhc = &nexthop_nh->nh_common;
                return;
            }
        }
    } endfor_nexthops(fi);

    if (lowpower_nh_index != -1) {
        res->nh_sel = lowpower_nh_index;
        res->nhc = fib_info_nhc(fi, lowpower_nh_index);
        return;
    }
    ...
}

개선점 요약

항목 AS-IS TO-BE
루프 수 2회 1회
정책 관계 독립 (weight 우선 → fallback) 통합 (score 우선 → weight가 tie-break)
완벽 매칭 없음 nh_score == 3 또는 (!saddr && 1)early return
saddr 우선 단순 boolean +2점 반영으로 강한 우선순위 부여
hash bound 단순 boolean +1점 반영
dead/linkdown 2nd loop에서 중복 체크 1st loop에서 한 번에 필터

즉, 기존 ECMP 정책(saddr affinity, hash bound)과 lowpower 메트릭이 충돌하지 않고 계층적으로 조화를 이룬다.


5. 초기화 패턴 일원화

rtable, rt6_info, xfrm_dst 생성 시 기존에는 각자 필드를 직접 찔러 넣었으나, 현재는 모두 dst_power_ptr()를 통해 동일한 패턴으로 초기화한다.

/* IPv4 예시 (net/ipv4/route.c) */
struct dst_power *p = dst_power_ptr(&rt->dst);
if (p) {
    WRITE_ONCE(p->ema_k_factor, ...);
    WRITE_ONCE(p->power_cost_weight, ...);
    WRITE_ONCE(p->ema_load, 0);
    WRITE_ONCE(p->ema_time_delta, 0);
    p->last_update_jiffies = 0;
}

xfrm_policy.c, net/ipv6/route.c에서도 동일한 템플릿을 재사용.


6. 사라진/유지된 부분

사라진 변경

  • netif_addr_lock_bh() 단순화: 블로그 원본에서 local_bh_disable() + spin_lock_nested()spin_lock_bh()로 교체한 diff는 현재 코드에 반영되지 않음. (커밋 히스토리상 별도 클린업으로 분리되었거나 실험 브랜치에서 제외됨)

그대로 유지

  • EMA_UPDATE(K, Old, New) 매크로 (net/core/dev.c 상단)
  • sysctl 인터페이스 (lowpower_ema_k_factor, lowpower_power_cost_weight)
  • 기본값 (512, 100)
  • IPv6 rt6_score_route()에서 weight를 m += weight 형태로 가산
  • SCTP/XFRM에서의 lowpower 필드 초기화 및 weight 함수 정의

7. 전체 흐름도

패킷 전송 (dev_hard_start_xmit)
    └── update_dst_ems_metrics(dst, skb->len)
            └── dst_power_ptr(dst) → struct dst_power *
            └── EMA_UPDATE(ema_load, ema_time_delta)

멀티패스 선택 (fib_select_multipath / rt6_score_route)
    └── get_dst_entry_from_nhc / get_dst_entry_from_fib6_nh
    └── calculate_lowpower_weight(dst)
            └── dst_power_ptr(dst)
            └── (ema_load + ema_time_delta) * power_cost_weight
    └── 기존 정책(saddr, hash bound)과 계층적 결합

8. 결론

원본 패치가 단순히 "dst_entry 끝에 필드를 붙이고 쓰자"는 형태였다면, 현재 코드베이스는 다음과 같은 엔지니어링 품질 향상을 이루었다.

  1. 캡슐화: struct dst_power 분리 + dst_power_ptr() 추상화
  2. ABI/캐시 안전: dst_entry 크기 변경 없이 필요한 구조체에만 임베드
  3. 정책 통합: fib_select_multipath를 단일 루프 + score 기반으로 개선하여 기존 ECMP 정책과 lowpower 메트릭이 자연스럽게 결합
  4. 템플릿화: 초기화/접근 코드가 dst_power_ptr() 기반으로 통일되어 유지보수성 향상
Back

Comments

No comments yet.