以下の内容はhttps://www.valinux.co.jp/blog/entry/20260219より取得しました。


新Linuxカーネル解読室 - Netlink

「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。

本稿では、Netlinkについてカーネルv6.14のコードをベースに解説します。

執筆者 : 岡本 涼二、高倉 遼

※ 「新Linuxカーネル解読室」連載記事一覧はこちら


はじめに

本記事ではLinuxが提供するIPCメカニズムの一つであるNetlinkについて解説します。 記事前半でNetlinkの仕様について簡単に説明し、後半でrtnetlinkを例にカーネル内部での実装について説明します。

Netlinkとは

NetlinkはLinuxが提供するIPCメカニズムで、ipコマンドやudevの実装に使われていることで有名です。 インターフェースとしてはソケットを採用していて、ユーザーはNetlink用のソケットを作成し、そのソケットに対してメッセージの送受信を行うことで対向のソケットを保持している相手と通信することができます。
Netlinkを使用してユーザープロセス間で通信することも可能ですが1、基本的にはユーザーとカーネル間で情報をやりとりするための機構で、本記事でもそれを前提として解説します(なお、カーネルとの通信の際に対向として指定されるソケットをこの文章内部では「カーネルのソケット」等と呼びます)。

Netlinkそれ自体は単にIPCのフレームワークで、カーネル内部にはそれを利用して何らかの機能を提供しているモジュール群があります。それらのモジュールのことをNetlinkファミリーと呼びます。

どのようなNetlinkファミリーがあるかを抜粋して記載します。一覧はmanページをご覧ください。

名前 説明
NETLINK_ROUTE ルーティングとリンクの更新を受信する、ipコマンドの実装で使われている
NETLINK_NETFILTER netfilter サブシステム
NETLINK_KOBJECT_UEVENT ユーザー空間へのカーネルメッセージ、udevの実装で使われている
NETLINK_GENERIC netlink を簡単に使用するための一般的なファミリー

本記事ではファミリーの実例としてNETLINK_ROUTEの対象であるrtnetlinkを取り上げます。

ソケットの作成にはsocketシステムコールを利用し、第一引数にAF_NETLINKを指定します。

netlink_socket = socket(AF_NETLINK, socket_type, netlink_family);

第二引数のsocket_typeはSOCK_RAWかSOCK_DGRAMである必要がありますが、両者の違いはなくどちらを選んでも同じです。 どのファミリーと通信するかはソケット作成時のsocketシステムコールに渡す第三引数、netlink_familyの値で指定します。

ユーザーからカーネルにメッセージを送信するには作成したソケットに対してsendmsg等のシステムコールを、反対にカーネルからのメッセージを受信するにはrecvmsg等のシステムコールを使用します。

通信モデル

通信モデルは次の2つに大別できます。

  1. ユーザーからカーネルに処理を依頼し、その結果を受信する
  2. カーネル内部で起きたイベントの通知を受け取る

1に関して、処理依頼はさらに

  • 例えばルートの追加といった、何らかのアクションの実行を要求するもの、
  • ネットワークインターフェースといったカーネルが持っているオブジェクトの情報の取得を要求するもの

の2種類に大別できます。ここでは前者のことをDo、後者のことをDumpと呼びます。 そのどちらでも処理依頼を送信後に処理結果(Doの場合は例えばルートの追加の成功/失敗、Dumpの場合は例えばネットワークインターフェースの一覧)を必要に応じて受け取ることができます。

2に関して、各Netlinkファミリーは、内部に複数個のマルチキャストグループを定義することができます。ユーザーはそのうちで関心のあるグループを「購読」して、カーネル内部でそのグループに関する状態変化が起きたときに通知を受け取るようにすることができます2
例えばrtnetlinkにはRTNLGRP_IPV4_ROUTEというマルチキャストグループがありますが、これはその名の通り「ipv4のルーティングに関するグループ」で、購読するとipv4のルーティングに関するイベントの通知を受け取ることができます。
グループを購読するにはbindシステムコールやsetsockoptシステムコールを使用し、実際に通知を受け取るにはrecvmsg等を実行します。

メッセージフォーマット

Netlinkで送受信されるデータは、ユーザーからカーネルへの要求だけでなくカーネルからユーザーへの応答やイベント通知においても、ヘッダー(struct nlmsghdr)とそれに続くペイロード、という構造を取ります。nlmsghdrnlmsg_lenにはヘッダーとペイロードを合わせたメッセージ全体のサイズが記載されている必要があります。

ペイロードには、各ファミリーが定義するデータ(rtnetlinkであればstruct rtmsgstruct ifinfomsg)や、エラー発生時のエラーメッセージ(struct nlmsgerr)などが入ります。1回のメッセージで複数のヘッダーとペイロードを送ることも可能です(マルチパートメッセージ)。

struct nlmsghdr {
    __u32 nlmsg_len;    /* Size of message including header */
    __u16 nlmsg_type;   /* Type of message content */
    __u16 nlmsg_flags;  /* Additional flags */
    __u32 nlmsg_seq;    /* Sequence number */
    __u32 nlmsg_pid;    /* Sender port ID */
};
名前 説明
nlmsg_pid 送信者のポートID
nlmsg_len ヘッダーとペイロードを含むメッセージ全体の長さ
nlmsg_type メッセージの種類
nlmsg_flags メッセージの追加情報を指定するフラグ
nlmsg_seq メッセージのシーケンス番号

ポートIDはソケットの識別子です。カーネルのソケットは常に0です。

nlmsg_typeには、NLMSG_ERRORNLMSG_DONEといったNetlink共通のメッセージタイプがありますが、各ファミリーが独自に拡張して定義した固有のメッセージタイプも存在します。

名前 説明
NLMSG_DONE マルチパートメッセージの終了を意味する
NLMSG_ERROR エラー通知。ペイロードにstruct nlmsgerr構造体が格納される

以下は、rtnetlinkが定義するメッセージタイプの抜粋です。

名前 説明
RTM_NEWROUTE 新しいルーティングエントリの追加
RTM_GETLINK ネットワークインターフェースの一覧取得
RTM_DELADDR ネットワークインターフェースからIPアドレスを削除

nlmsg_flagsでは以下の値(抜粋)をセットすることができます。もちろんORをとることで同時に複数指定することができます。

名前 説明
NLM_F_REQUEST メッセージが何かを要求するものである場合には必須
NLM_F_ACK 成功した場合応答を要求する
NLM_F_MULTI マルチパートメッセージの一部であることを主張
NLM_F_DUMP Dump要求

アドレスフォーマット

アドレスの指定の構造体は以下の通りです。

struct sockaddr_nl {
   sa_family_t     nl_family;  /* AF_NETLINK */
   unsigned short  nl_pad;     /* 0 */
   pid_t           nl_pid;
   __u32           nl_groups;
};

通信先にカーネルを指定する場合、nl_pidnl_groups共に0を指定します。
マルチキャストグループを購読したい場合、nl_groupsに適切な値を設定したうえでbindの引数として渡します。nl_groupsはビットマスクとして使用されます。

カーネル内部

まず、Netlinkのカーネル内主要データ構造等を確認します。その後、Do、Dump、マルチキャストそれぞれの例として、ip route add、ip route show、ip monitor route実行時のstrace出力のようなものを足がかりに解説します。

カーネル内のNetlink主要データ構造等

この構造体は、ファミリーごとの処理ハンドラや、そのファミリーに属するソケット群といった情報を管理します。nl_table(net/netlink/af_netlink.h)というグローバル変数にファミリーごとのこの構造体のインスタンスが保持されていて、ファミリーIDをインデックスとして nl_table[netlink_family] のようにしてインスタンスにアクセスできます。

struct netlink_table {
    struct rhashtable  hash;
    struct hlist_head  mc_list;
    struct listeners __rcu *listeners;
    unsigned int      flags;
    unsigned int      groups;
    struct mutex       *cb_mutex;
    struct module      *module;
    int            (*bind)(struct net *net, int group);
    void           (*unbind)(struct net *net, int group);
    void                    (*release)(struct sock *sk,
                       unsigned long *groups);
    int            registered;
};

メンバーの説明(抜粋)

名前 説明
hash このNetlinkファミリーに紐づいているソケットたち、キーにはnetns(ネットワーク名前空間に関連する構造体)とソケットのportid
mc_list hashに登録されているソケットたちのなかで、一つ以上のマルチキャストグループを購読しているソケットのリスト
listeners どのグループに購読者がいるかを管理するビットマップ
groups このファミリーのマルチキャストグループの数
bind, unbind, release ソケット作成時にそのソケットに登録される関数

hashでこのファミリーのソケットが管理されています。挿入処理はnetlink_insertという関数で、検索はnetlink_lookupという関数です。

ソケットの挿入タイミングは作成時ではなく、bindシステムコール実行時やbindせずにsendmsg等を実行したときなどです。

nl_tableがファミリー全体の情報を管理するのに対し、個々のソケットの状態を管理するのがnetlink_sock構造体です(net/netlink/af_netlink.h)。
これはsock構造体を包み込む(ラップする)形で定義されており、Netlink処理に必要な情報(ポートIDや購読状態など)を保持します。その構造からわかる通り、netlink_sockからsockにアクセスすることもできますし、逆にsockからnetlink_sockにアクセスすることもできます3

struct netlink_sock {
    /* struct sock has to be the first member of netlink_sock */
    struct sock        sk;
    unsigned long     flags;
    u32         portid;
    u32         dst_portid;
    u32         dst_group;
    u32         subscriptions;
    u32         ngroups;
    unsigned long     *groups;
    unsigned long     state;
    size_t         max_recvmsg_len;
    wait_queue_head_t   wait;
    bool           bound;
    bool           cb_running;
    int            dump_done_errno;
    struct netlink_callback    cb;
    struct mutex       nl_cb_mutex;

    void           (*netlink_rcv)(struct sk_buff *skb);
    int            (*netlink_bind)(struct net *net, int group);
    void           (*netlink_unbind)(struct net *net, int group);
    void           (*netlink_release)(struct sock *sk,
                           unsigned long *groups);
    struct module      *module;

    struct rhash_head  node;
    struct rcu_head        rcu;
};

メンバーの説明(抜粋)

名前 説明
portid このソケットのポートid。カーネルのソケットの場合は0、それ以外は基本的にはプロセスID
dst_portid このソケットの対向ソケットのポートid
dst_group このソケットの送信先のマルチキャストグループ。ユニキャストの場合は0
subscriptions 現在購読しているグループの数
ngroups このソケットが所属するNetlinkファミリーのマルチキャストグループ数
groups 自身の購読に関するビットマップ
cb_running Dumpが実行中かどうかを表すフラグ
cb コールバック関数などのDump実行時に必要なデータ
netlink_rcv このソケットがメッセージを受け取る際に呼び出される関数。カーネルのソケットでない場合、NULLが入る
netlink_bind, netlink_unbind, netlink_release nl_tableのbind, unbind, release

netlink_tablegroups及びnetlink_socketgroupsについて補足します。
まず、manページには以下のように書かれていますが、

Each netlink family has a set of 32 multicast groups

どうやら実際はその限りではないようで、現にrtnetlinkには39個のマルチキャストグループが定義されています。 ビットマップであるnetlink_socket.groupsは33個以上、場合によっては65個以上のグループを管理できるようになっていなければならず、したがって unsigned long *という型になっています(例えばファミリーのグループ数が1 ~ 64のときにはunsigned long groups[1]になり、65 ~ 128のときにunsigned long groups[2]となるイメージです)。

ユーザーが33番目以降のグループを購読したい場合は注意が必要です。というのもbindでは(sockaddr_nl.nl_groups__u32のため)1 ~ 32番目のグループまでしか指定できません。なので、それ以降のグループを購読したい場合はsetsockoptを使用する必要があります。

netlink_rcvについても補足します。このメンバーは、カーネルのソケットがメッセージを受信するときに実行される関数で、カーネル側がメッセージの内容に合わせて処理を行うために存在しています。なのでユーザーが作成するNetlinkソケットでは常にNULLで、値が入るのはカーネルソケットのみです。 ユーザーのソケットが何らかのメッセージを受け取る場合はsk.receive_queueからメッセージをデキューするだけです。エンキューは例えば__netlink_sendskbという関数で行われます。

初期化

最後に、nl_table各要素(=netlink_table)の初期化について説明します。

各Netlinkファミリーはネットワーク名前空間の初期化時に呼ばれる自身の初期化用の関数(rtnetlinkの場合はrtnetlink_net_init(net/core/rtnetlink.c))を定義していて、それらは最終的には__netlink_kernel_create(net/netlink/af_netlink.c)を呼び出します。

struct sock *
__netlink_kernel_create(struct net *net, int unit, struct module *module,
                        struct netlink_kernel_cfg *cfg)

この関数では以下の処理が行われますが、ここで作成されたソケットが「そのファミリーのそのネットワーク名前空間におけるカーネルのソケット」として登録されるということになります。

  • unitという変数名で渡される)Netlinkファミリーに紐づいたソケットを作成する
  • そのソケットのportidを0にし、cfg->inputnetlink_rcvに設定する
  • もしnl_table[unit]がまだ初期化されていない(= nl_table[unit].registered == 0の)場合、cfg->bindnl_table[unit].bindに代入する等のもろもろのtableの初期化を行う
  • registeredの値をインクリメントする

rtnetlinkに関する実装はnet/core/rtnetlink.cにあります。 rtnetlinkもNetlink本体と同様、ディスパッチャとしての構造を持っています。
つまり、ルーティングテーブルへの追加やインターフェース設定といった実際の処理は、それ専用のモジュールに委任されています。rtnetlinkは受け取ったメッセージに応じて、対応するモジュールの実装を見つけて実行する役割を担います。 実際の処理内容を管理するのがrtnl_linkという構造体で、rtnl_msg_handlersというrtnl_linkを要素に持つ二次元配列のようなグローバル変数が実体を保持しています

rtnl_msg_handlers

rtnl_linkのインスタンスを取り出す際にはrtnl_get_linkという関数が用いられます。protocolは例えばPF_INETといったいわゆるプロトコルファミリー、msgtypeはRTM_GETROUTEなどのrtnetlinkが拡張したnlmsg_typeで、この二つをインデックスとして使用してrtnl_msg_handlersにアクセスします。

static struct rtnl_link __rcu *__rcu *rtnl_msg_handlers[RTNL_FAMILY_MAX + 1];

static struct rtnl_link *rtnl_get_link(int protocol, int msgtype);

rtnl_link構造体は以下のように定義されていて、doitdumpitがそれぞれ「Doが要求されたときの処理」「Dumpが要求されたときの処理」に該当します。

typedef int (*rtnl_doit_func)(struct sk_buff *, struct nlmsghdr *,
                  struct netlink_ext_ack *);
typedef int (*rtnl_dumpit_func)(struct sk_buff *, struct netlink_callback *);

struct rtnl_link {
    rtnl_doit_func      doit;
    rtnl_dumpit_func    dumpit;
    struct module      *owner;
    unsigned int      flags;
    struct rcu_head        rcu;
};

rtnl_link登録の関数はrtnl_register_internal及びこの関数を使用して複数のrtnl_linkを一度に登録するrtnl_register_manyです。

rtnl_register_manyを呼んでいる例として、ip_fib_init(net/ipv4/fib_frontend.c)を掲載します。

static const struct rtnl_msg_handler fib_rtnl_msg_handlers[] __initconst = {
        {.protocol = PF_INET, .msgtype = RTM_NEWROUTE,
         .doit = inet_rtm_newroute},
        {.protocol = PF_INET, .msgtype = RTM_DELROUTE,
         .doit = inet_rtm_delroute},
        {.protocol = PF_INET, .msgtype = RTM_GETROUTE, .dumpit = inet_dump_fib,
         .flags = RTNL_FLAG_DUMP_UNLOCKED | RTNL_FLAG_DUMP_SPLIT_NLM_DONE},
};

void __init ip_fib_init(void)
{
        fib_trie_init();

        register_pernet_subsys(&fib_net_ops);

        register_netdevice_notifier(&fib_netdev_notifier);
        register_inetaddr_notifier(&fib_inetaddr_notifier);

        rtnl_register_many(fib_rtnl_msg_handlers);
}

Do (ip route add)

それではDoの例としてip route addを取り上げます。

ip route add 172.31.0.0/16 via 172.16.0.0

socket作成

socket作成時の出力は以下の通りです。

socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

このシステムコールはnetlink_create(net/netlink/af_netlink.c)を呼び出します。 内部でnl_table[NETLINK_ROUTE]bindなどをソケットに紐づけています。

netlink_rcvには何も代入されないので、NULLのままです。

static int netlink_create(struct net *net, struct socket *sock, int protocol,
              int kern)
{
    struct netlink_sock *nlk;
    int (*bind)(struct net *net, int group);
    void (*unbind)(struct net *net, int group);
    void (*release)(struct sock *sock, unsigned long *groups);
    ...

    bind = nl_table[protocol].bind;
    unbind = nl_table[protocol].unbind;
    release = nl_table[protocol].release;
    ...

    err = __netlink_create(net, sock, protocol, kern);
    ...

    nlk = nlk_sk(sock->sk);
    nlk->netlink_bind = bind;
    nlk->netlink_unbind = unbind;
    nlk->netlink_release = release;
out:
    return err;
    ...
}

routeの追加

実際にrtnetlinkに対してrouteの追加を依頼している部分を見てみましょう(nlmsg_typeRTM_NEWROUTEで呼ばれていたり、nlmsg_flagsにはNLM_F_REQUESTはあるけれどNLM_F_DUMPがなかったりしますね)。

sendmsg(
    3, 
    {
        msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000},
        msg_namelen=12, 
        msg_iov=[
            {
                iov_base=[
                    {nlmsg_len=44, nlmsg_type=RTM_NEWROUTE, nlmsg_flags=NLM_F_REQUEST|NLM_F_ACK|NLM_F_EXCL|NLM_F_CREATE, nlmsg_seq=1753942414, nlmsg_pid=0}, 
                    {rtm_family=AF_INET, rtm_dst_len=16, rtm_src_len=0, rtm_tos=0, rtm_table=RT_TABLE_MAIN, rtm_protocol=RTPROT_BOOT, rtm_scope=RT_SCOPE_UNIVERSE, rtm_type=RTN_UNICAST, rtm_flags=0}, 
                    [[{nla_len=8, nla_type=RTA_DST}, inet_addr("172.31.0.0")], [{nla_len=8, nla_type=RTA_GATEWAY}, inet_addr("172.16.0.0")]]
                ],
                iov_len=44
            }
        ], 
        msg_iovlen=1,
        msg_controllen=0,
        msg_flags=0
    }, 
    0
)

以下は、実際にroute追加を行うモジュールのエントリポイントまでのコールグラフです。inet_rtm_newrouteを目標にコードの流れを解説したいと思います4

netlink_sendmsg (*)
    └── netlink_unicast (*)
            └── netlink_unicast_kernel (*)
                    └── rtnetlink_rcv (**)
                            └── netlink_rcv_skb (*)
                                    └── rtnetlink_rcv_msg (**)
                                            └── inet_rtm_newroute (***)

(*)   -> net/netlink/af_netlink.c
(**)  -> net/core/rtnetlink.c
(***) -> net/ipv4/fib_frontend.c

netlink_sendmsgは以下のことを実行する関数です。

  • ソケットバッファ(skb)の確保及びmsgを確保したskbにコピー
  • 送信先と送信方式(ユニキャスト/マルチキャスト)を確定するのに必要な情報の取得(dst_portid, dst_group
  • netlink_autobindでportidを割り振り、netlink_insertnetlink_tablehashに挿入している
  • 送信方式(ユニキャスト/マルチキャスト)に応じた関数呼び出し
static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
    struct sock *sk = sock->sk;
    struct netlink_sock *nlk = nlk_sk(sk);
    DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);
    u32 dst_portid;
    u32 dst_group;
    struct sk_buff *skb;
    int err;
    ...
    if (msg->msg_namelen) {
        ...
        dst_portid = addr->nl_pid;
        dst_group = ffs(addr->nl_groups);
        err =  -EPERM;
        if ((dst_group || dst_portid) &&
            !netlink_allowed(sock, NL_CFG_F_NONROOT_SEND))
            goto out;
    } else {
        /* Paired with WRITE_ONCE() in netlink_connect() */
        dst_portid = READ_ONCE(nlk->dst_portid);
        dst_group = READ_ONCE(nlk->dst_group);
    }
    ...
    NETLINK_CB(skb).portid   = nlk->portid;
    ...

    if (dst_group) {
        refcount_inc(&skb->users);
        netlink_broadcast(sk, skb, dst_portid, dst_group, GFP_KERNEL);
    }
    err = netlink_unicast(sk, skb, dst_portid, msg->msg_flags & MSG_DONTWAIT);

    return err;
}

今回の例だと dst_groupは0なので、netlink_unicastのみが呼ばれることになります。

netlink_unicastからnetlink_rcv_skbが実行されるまでの流れは、

  • 送信先ソケットを検索(netlink_unicast)
  • そのソケットの所有者(カーネル/ユーザー)に応じた分岐(netlink_unicast_kernel/netlink_sendskb)(netlink_unicast)
  • 先程検索されたソケットのnetlink_rcv(= rtnetlink_rcv)の実行 (netlink_unicast_kernel)
  • rtnetlink_rcv_msgを引数としてnetlink_rcv_skbを呼び出す (rtnetlink_rcv)

となります。

netlink_rcv_skbは、メッセージが尽きるまで以下を繰り返す関数です(マルチパートメッセージとして一回で複数メッセージの送信が可能であるということを思い出してください)。

  • cbという変数名のコールバック関数(今回の場合はrtnetlink_rcv_msg)を実行する
  • その結果やフラグ等によってackメッセージをskbの所有者、つまりsendmsg実行者に通知
  • メッセージたちが入っているバッファをインクリメントする
int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *,
                           struct nlmsghdr *,
                           struct netlink_ext_ack *))
{
    struct netlink_ext_ack extack;
    struct nlmsghdr *nlh;
    int err;

    while (skb->len >= nlmsg_total_size(0)) {
        int msglen;

        memset(&extack, 0, sizeof(extack));
        nlh = nlmsg_hdr(skb);
        err = 0;
        ...
        /* Only requests are handled by the kernel */
        if (!(nlh->nlmsg_flags & NLM_F_REQUEST))
            goto ack
        ...
        err = cb(skb, nlh, &extack);
        if (err == -EINTR)
            goto skip;
ack:
        if (nlh->nlmsg_flags & NLM_F_ACK || err)
            netlink_ack(skb, nlh, err, &extack);

skip:
        msglen = NLMSG_ALIGN(nlh->nlmsg_len);
        if (msglen > skb->len)
            msglen = skb->len;
        skb_pull(skb, msglen);
    }

    return 0;
}

rtnetlink_rcv_msgは簡単に言うと、rtnl_get_link関数でrtnl_linkを取り出して、Dumpの場合はdumpitを、そうでない場合はdoitを実行する、というものです。

今回の場合だとdoitを実行することになりますが、そこには(前掲のip_fib_initから想像がつくと思いますが)inet_rtm_newrouteという関数が登録されています。

そしてinet_rtm_newroute以降の処理でカーネル内部のルーティングテーブルが更新されることになります。

static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
                 struct netlink_ext_ack *extack)
{
    struct net *net = sock_net(skb->sk);
    struct rtnl_link *link;
    enum rtnl_kinds kind;
    rtnl_doit_func doit;
    unsigned int flags;
    int family;
    int type;

    type = nlh->nlmsg_type;
    type -= RTM_BASE;


    family = ((struct rtgenmsg *)nlmsg_data(nlh))->rtgen_family;
    kind = rtnl_msgtype_kind(type);

    if (kind == RTNL_KIND_GET && (nlh->nlmsg_flags & NLM_F_DUMP)) {
        struct sock *rtnl;
        rtnl_dumpit_func dumpit;

      link = rtnl_get_link(family, type);
        dumpit = link->dumpit;
        flags = link->flags;

        rtnl = net->rtnl;
        if (err == 0) {
            struct netlink_dump_control c = {
                .dump       = dumpit,
                .min_dump_alloc = min_dump_alloc,
                .module     = owner,
                .flags      = flags,
            };
            err = rtnetlink_dump_start(rtnl, skb, nlh, &c);
        }
        return err;
    }

    link = rtnl_get_link(family, type);
    if (link && link->doit)
        err = link->doit(skb, nlh, extack);

    return err;
}

Dump (ip route show)

次に、Dumpの例としてip route showコマンドのstraceを見ていきます。

ip route show

ソケットの作成及びnetlink_sendmsgからrtnetlink_rcv_msgまでの処理はDoの例と変わらないので割愛します。

また今回の例ではルーティング情報の取得を行うために受信処理も絡んできます(Doでもack messageの受信等で関係していたのですが)ので、それについても記載します。

sendto(
    3, 
    [
        [
            {nlmsg_len=36, nlmsg_type=RTM_GETROUTE, nlmsg_flags=NLM_F_REQUEST|NLM_F_DUMP, nlmsg_seq=1756886588, nlmsg_pid=0},
            {rtm_family=AF_INET, rtm_dst_len=0, rtm_src_len=0, rtm_tos=0, rtm_table=RT_TABLE_UNSPEC, rtm_protocol=RTPROT_UNSPEC, rtm_scope=RT_SCOPE_UNIVERSE, rtm_type=RTN_UNSPEC, rtm_flags=0}, 
            [{nla_len=8, nla_type=RTA_TABLE}, RT_TABLE_MAIN]
        ], 
        {nlmsg_len=0, nlmsg_type=0 /* NLMSG_??? */, nlmsg_flags=0, nlmsg_seq=0, nlmsg_pid=0}
    ],
    156,
    0,
    NULL,
    0
)

今回の場合はnlmsg_flagsNLM_F_DUMPビットが立っているので、rtnetlink_rcv_msgdumpitを呼び出す分岐に入ります。ちなみにnlmsg_typeRTM_GETROUTEですのでrtnl_get_linkで取り出されるrtnl_linkは先程のDoの例とは別のものです。 このrtnl_linkdumpitにはinet_dump_fibが入っています。

rtnetlink_dump_start以降の関数コールの流れです。

rtnetlink_dump_start (**)
    └── netlink_dump_start (*)
            └── __netlink_dump_start (*)
                   └── netlink_dump (*)
                           └── rtnl_dumpit (**)
                                   └── inet_dump_fib (***)

(*)    -> net/netlink/af_netlink.c
(**)  -> net/core/rtnetlink.c
(***) -> net/ipv4/fib_frontend.c

ここではDumpのメイン処理であるnetlink_dumpについて説明します。 この関数はcb->dump5を実行して、その結果をユーザーソケットのreceive queueにつなげます。

そもそもDumpは例えば今回のようにルーティングテーブルの内容を書き出すといった、多くのメモリが必要となる可能性がある処理が行われると想定されます。なので、netlink_dumpは状況次第(関数内部で確保したソケットバッファを上回る量のデータをユーザーに渡さなければならない場合等)で処理の中断・再開ができるようになっています。

static int netlink_dump(struct sock *sk, bool lock_taken)
{
    struct netlink_sock *nlk = nlk_sk(sk);
    struct netlink_callback *cb;
    struct sk_buff *skb = NULL;
    ...
    cb = &nlk->cb;
    ...
    netlink_skb_set_owner_r(skb, sk);
    if (nlk->dump_done_errno > 0) {
        cb->extack = &extack;

        nlk->dump_done_errno = cb->dump(skb, cb);

        /* EMSGSIZE plus something already in the skb means
        * that there's more to dump but current skb has filled up.
        * If the callback really wants to return EMSGSIZE to user space
        * it needs to do so again, on the next cb->dump() call,
        * without putting data in the skb.
        */
        if (nlk->dump_done_errno == -EMSGSIZE && skb->len)
            nlk->dump_done_errno = skb->len;

        cb->extack = NULL;
    }

    if (nlk->dump_done_errno > 0 ||
        skb_tailroom(skb) <
            nlmsg_total_size(sizeof(nlk->dump_done_errno))) {
        mutex_unlock(&nlk->nl_cb_mutex);

        if (sk_filter(sk, skb))
            kfree_skb(skb);
        else
            __netlink_sendskb(sk, skb);
        return 0;
    }

    if (netlink_dump_done(nlk, skb, cb, &extack))
        goto errout_skb;
    ...
    if (sk_filter(sk, skb))
        kfree_skb(skb);
    else
        __netlink_sendskb(sk, skb);
    ...
        WRITE_ONCE(nlk->cb_running, false);
    module = cb->module;
    ...
    return 0;
    ...
}

上記を見ると、netlink_dumpcb->dumpに次のような動作であることを期待していることがわかります。

  1. すべてのデータを詰め込むことに成功した場合は0を返す
  2. バッファのサイズが十分でない場合、途中までデータを詰め込んでからskb->lenを設定したうえで-EMSGSIZEを返すあるいは直接skb->lenを返す

前者かつDump処理全体の結果であるNLMSG_DONEメッセージを書き込む余裕がskbにある(skb_tailroom(skb) < nlmsg_total_size(sizeof(nlk->dump_done_errno))がfalseになる)場合は、最終的にnlk->cb_runningがfalseになってdumpが完了します。

もし余裕がない場合、まずNLMSG_DONEメッセージを含めずにskbをソケットの受信キューに追加します。二度目のこの関数の呼び出しでnlk->dump_done_errno > 0cb->dump実行の分岐に入らないので、コールバックを実行することなくDumpの処理結果のみをskbに書き込んで受信キューに追加し、nlk->cb_runningをfalseにして「dumpが完了した」状態に移行します。

2の場合は、このnetlink_dump終了までは上記の「1かつskbにNLMSG_DONEを書き込む余裕がない場合」と同じですが、次回以降のnetlink_dump呼び出しではもう一度cb->dumpが実行されることになります。その後の流れはこれまでの説明と同じです。

skbがいっぱいになってしまった等の事情で中断されたDumpはどうやって再開されるのでしょうか。

その契機となるのがrecvmsg系のシステムコールから呼び出される、netlink_recvmsg関数です。

recvmsg(...)
static int netlink_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,
               int flags)
{
    struct sock *sk = sock->sk;
    struct netlink_sock *nlk = nlk_sk(sk);
    size_t copied, max_recvmsg_len;
    struct sk_buff *skb, *data_skb;
    int err, ret;

    copied = 0;

    skb = skb_recv_datagram(sk, flags, &err);

    data_skb = skb;

    copied = data_skb->len;

    err = skb_copy_datagram_msg(data_skb, 0, msg, copied);

    skb_free_datagram(sk, skb);

    if (READ_ONCE(nlk->cb_running) &&
        atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf / 2) {
        ret = netlink_dump(sk, false);
    }

out:
    netlink_rcv_wake(sk);
    return err ? : copied;
}

この関数では、ソケットのreceive_queueからskbを取り出してその中身をユーザー空間にコピーしその後skbを解放する、ということが行われますが、skb解放後にnetlink_dumpを再開する場合があります。 if (READ_ONCE(...))部分で再開に関する条件を確認していますが、sk_rmem_allocの値はskb_free_datagramの実行過程で減算されます。したがって、中断されたnetlink_dumpnetlink_recvmsgを発行し続ければいつか再開するということがわかります。6

以下のシーケンス図はDumpの実行から3回目のrecvmsgでDumpが終了した場合の流れです。この図は

  1. ユーザーがsendmsgでDumpの実行を要求する
  2. 一回目のnetlink_dumpが実行されるが、skbが十分なサイズでないためcb->dumpは途中で終了(sendmsgは成功)
  3. ユーザーがrecvmsgでDumpで取得されるべきデータを取り出そうとする
  4. 先程のnetlink_dumpで処理されたデータがユーザーに渡されて解放される
  5. 空きが十分できたのでnetlink_dumpが再開
  6. cb->dumpが最後まで実行されるが、完了通知のメッセージを乗せる余裕がskbになかった
  7. ユーザーが2度目のrecvmsg
  8. 完了通知を除いたデータがユーザー空間に
  9. netlink_dumpが再開されるが、今回はcb->dumpは呼び出されない。完了通知のみがskbに詰め込まれてcb_runningfalseに というケースが書かれています。

マルチキャスト (ip monitor route)

最後に、multicastの受信の例としてip monitor routeを取り上げます。

ip monitor route

先述の通りbindでもsetsockoptでも購読登録はできますが、今回はbindを例に解説します。

bind(3, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=0x4000440}, 12)

nl_groupsはビットマップです。ビットが立っている位置(インデックス)が、購読するグループに対応します。

rtnetlinkの場合はrtnetlink_group(include/uapi/linux/rtnetlink.h)という名前で以下のグループが定義されていますが、今回の値だとRTNLGRP_IPV4_ROUTERTNLGRP_IPV6_ROUTERTNLGRP_MPLS_ROUTEに対して購読を依頼しているのがわかります。

enum rtnetlink_groups {
    RTNLGRP_NONE,
#define RTNLGRP_NONE       RTNLGRP_NONE
    RTNLGRP_LINK,
#define RTNLGRP_LINK       RTNLGRP_LINK
    ...
    RTNLGRP_IPV4_ROUTE, // = 7
#define RTNLGRP_IPV4_ROUTE RTNLGRP_IPV4_ROUTE
    ...
    RTNLGRP_IPV6_ROUTE, // = 11
#define RTNLGRP_IPV6_ROUTE RTNLGRP_IPV6_ROUTE
    ...
    RTNLGRP_MPLS_ROUTE, // = 27
#define RTNLGRP_MPLS_ROUTE RTNLGRP_MPLS_ROUTE
    ...
}

bindシステムコールはnetlink_bindを呼び出します。この関数は

  1. nlk->netlink_bindNULLでない場合それを実行し
  2. netlink_update_subscriptionsmc_listの更新と
  3. nlk->groups[0]の下位32ビットをgroups(nl_groups)の値で上書きし
  4. netlink_update_listenersを実行しlistenersを更新する

という処理を実行します。

netlink_update_subscriptionsは、そのソケットが初めてなんらかのグループを購読する場合(!nlk->subscriptions && subscriptions)はtableのmc_listに追加し、すべての購読を停止した際にmc_listから取り除く、という処理を行います。

netlink_update_listenersではmc_listに登録されている全ソケットのgroupsビットマップの論理和(OR)を計算し、その結果がlisteners->masksに設定されます。

static int netlink_bind(struct socket *sock, struct sockaddr *addr,
            int addr_len)
{
    struct sock *sk = sock->sk;
    struct net *net = sock_net(sk);
    struct netlink_sock *nlk = nlk_sk(sk);
    struct sockaddr_nl *nladdr = (struct sockaddr_nl *)addr;
    int err = 0;
    unsigned long groups;
    bool bound;

    groups = nladdr->nl_groups;

    if (nlk->netlink_bind && groups) {
        int group;

        /* nl_groups is a u32, so cap the maximum groups we can bind */
        for (group = 0; group < BITS_PER_TYPE(u32); group++) {
            if (!test_bit(group, &groups))
                continue;
            err = nlk->netlink_bind(net, group + 1);
        }
    }
    netlink_table_grab();
    netlink_update_subscriptions(sk, nlk->subscriptions +
                     hweight32(groups) -
                     hweight32(nlk->groups[0]));
    nlk->groups[0] = (nlk->groups[0] & ~0xffffffffUL) | groups;
    netlink_update_listeners(sk);
    netlink_table_ungrab();

    return 0;
    ...
}

これでユーザー側はrecvmsg等を実行してカーネルからの通知を待つだけです。

カーネル側から通知が送られてくる流れも確認しましょう。

先程のip route addの例を再度取り上げます。inet_rtm_newrouteに成功すると、その内容がRTNLGRP_IPV4_ROUTEグループを購読しているユーザーに届きます。

inet_rtm_newroute (***)
    └── fib_table_insert (****)
            └──  rtmsg_fib (*****)
                    └── rtnl_notify (**)
                            └── nlmsg_notify (*)
                                    └── ... ── netlink_broadcast_filtered (*)
                                                        └── do_one_broadcast (*)
                                                                └── netlink_broadcast_deliver (*)
                                                                        └── __netlink_sendskb (*)

(*)    -> net/netlink/af_netlink.c
(**)  -> net/core/rtnetlink.c
(***) -> net/ipv4/fib_frontend.c
(****) -> net/ipv4/fib_trie.c
(*****) -> net/ipv4/fib_semantics.c

ルートの追加処理が完了すると、rtnl_notifyという関数が呼ばれます。

void rtmsg_fib(int event, __be32 key, struct fib_alias *fa,
           int dst_len, u32 tb_id, const struct nl_info *info,
           unsigned int nlm_flags)
{
        ...
        rtnl_notify(skb, info->nl_net, info->portid, RTNLGRP_IPV4_ROUTE,
            info->nlh, GFP_KERNEL);
        ...
}

この関数は、RTM_NEWROUTEを実行したソケットが所属しているネットワーク名前空間のカーネルソケットを通知の送信者としてnlmsg_notifyを呼び出します。

nlmsg_notifyは、groupが指定されているかどうかでユニキャスト送信かマルチキャスト送信かを切り替える関数です。今回はgroup=RTNLGRP_IPV4_ROUTErtnl_notifyに渡されたRTNLGRP_IPV4_ROUTEがそのままnlmsg_notifyに渡される)なのでマルチキャストの実行に移ります。

netlink_broadcast_filteredでは、いったんmc_listに登録されているすべてのソケットに対してdo_one_broadcastを実行しています。 実際にそのソケットが送信対象かどうかを判断するのはdo_one_broadcastの役目です。

int netlink_broadcast_filtered(struct sock *ssk, struct sk_buff *skb,
                   u32 portid,
                   u32 group, gfp_t allocation,
                   netlink_filter_fn filter,
                   void *filter_data)
{
        ...
    sk_for_each_bound(sk, &nl_table[ssk->sk_protocol].mc_list)
        do_one_broadcast(sk, &info);
        ...
}

do_one_broadcast内部で、そのソケットが今回のマルチキャスト送信の対象か(そもそもこのグループを購読しているか、ネットワーク名前空間が同一かあるいは異なっていても適切なcapabilityを持っているか...)を確認したあとで、対象のソケットに対して__netlink_sendskbを実行し、ユーザーのソケットのqueueに通知をつなげます。

これでユーザーが通知を受け取ることができるようになりました。


  1. その場合はNETLINK_USERをファミリーに指定します。NETLINK_USERのみがユーザーからユーザーへの送信が許可されています。
  2. ちなみにユーザーからマルチキャスト送信を試みることも可能ですが、NETLINK_USER以外のファミリーで許可されていないので、本記事でマルチキャストといった場合はユーザーがカーネルからメッセージを受信する通信を意味します。
  3. 本記事ではsock構造体については解説しません。気になる方はこちらの記事をご覧ください。
  4. ソケット関係のシステムコールがどうやってアドレスファミリーの実装を呼び出すかについても、注釈3のリンク先をご覧ください。
  5. このときのdumpitにはrtnl_dumpitが登録されています。linkdumpitであったinet_dump_fibはこのrtnl_dumpitの中で呼ばれるようになっています。
  6. sk_rmem_allocsk_rcvbufについての説明はこちらの記事に記載があります。



以上の内容はhttps://www.valinux.co.jp/blog/entry/20260219より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14