「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種類に大別できます。ここでは前者のことをDo、後者のことをDumpと呼びます。 そのどちらでも処理依頼を送信後に処理結果(Doの場合は例えばルートの追加の成功/失敗、Dumpの場合は例えばネットワークインターフェースの一覧)を必要に応じて受け取ることができます。
2に関して、各Netlinkファミリーは、内部に複数個のマルチキャストグループを定義することができます。ユーザーはそのうちで関心のあるグループを「購読」して、カーネル内部でそのグループに関する状態変化が起きたときに通知を受け取るようにすることができます2。
例えばrtnetlinkにはRTNLGRP_IPV4_ROUTEというマルチキャストグループがありますが、これはその名の通り「ipv4のルーティングに関するグループ」で、購読するとipv4のルーティングに関するイベントの通知を受け取ることができます。
グループを購読するにはbindシステムコールやsetsockoptシステムコールを使用し、実際に通知を受け取るにはrecvmsg等を実行します。
メッセージフォーマット
Netlinkで送受信されるデータは、ユーザーからカーネルへの要求だけでなくカーネルからユーザーへの応答やイベント通知においても、ヘッダー(struct nlmsghdr)とそれに続くペイロード、という構造を取ります。nlmsghdrのnlmsg_lenにはヘッダーとペイロードを合わせたメッセージ全体のサイズが記載されている必要があります。
ペイロードには、各ファミリーが定義するデータ(rtnetlinkであればstruct rtmsgやstruct 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_ERRORやNLMSG_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_pidとnl_groups共に0を指定します。
マルチキャストグループを購読したい場合、nl_groupsに適切な値を設定したうえでbindの引数として渡します。nl_groupsはビットマスクとして使用されます。
カーネル内部
まず、Netlinkのカーネル内主要データ構造等を確認します。その後、Do、Dump、マルチキャストそれぞれの例として、ip route add、ip route show、ip monitor route実行時のstrace出力のようなものを足がかりに解説します。
カーネル内のNetlink主要データ構造等

netlink_table
この構造体は、ファミリーごとの処理ハンドラや、そのファミリーに属するソケット群といった情報を管理します。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等を実行したときなどです。
netlink_sock
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_tableのgroups及びnetlink_socketのgroupsについて補足します。
まず、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->inputをnetlink_rcvに設定する - もし
nl_table[unit]がまだ初期化されていない(=nl_table[unit].registered == 0の)場合、cfg->bindをnl_table[unit].bindに代入する等のもろもろのtableの初期化を行う registeredの値をインクリメントする
rtnetlink
rtnetlinkに関する実装はnet/core/rtnetlink.cにあります。
rtnetlinkもNetlink本体と同様、ディスパッチャとしての構造を持っています。
つまり、ルーティングテーブルへの追加やインターフェース設定といった実際の処理は、それ専用のモジュールに委任されています。rtnetlinkは受け取ったメッセージに応じて、対応するモジュールの実装を見つけて実行する役割を担います。
実際の処理内容を管理するのがrtnl_linkという構造体で、rtnl_msg_handlersというrtnl_linkを要素に持つ二次元配列のようなグローバル変数が実体を保持しています

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構造体は以下のように定義されていて、doit、dumpitがそれぞれ「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_typeがRTM_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_insertでnetlink_tableのhashに挿入している- 送信方式(ユニキャスト/マルチキャスト)に応じた関数呼び出し
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の受信等で関係していたのですが)ので、それについても記載します。
rtnetlink_dump_start
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_flagsにNLM_F_DUMPビットが立っているので、rtnetlink_rcv_msgのdumpitを呼び出す分岐に入ります。ちなみにnlmsg_typeはRTM_GETROUTEですのでrtnl_get_linkで取り出されるrtnl_linkは先程のDoの例とは別のものです。
このrtnl_linkのdumpitには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_dumpはcb->dumpに次のような動作であることを期待していることがわかります。
- すべてのデータを詰め込むことに成功した場合は0を返す
- バッファのサイズが十分でない場合、途中までデータを詰め込んでから
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 > 0でcb->dump実行の分岐に入らないので、コールバックを実行することなくDumpの処理結果のみをskbに書き込んで受信キューに追加し、nlk->cb_runningをfalseにして「dumpが完了した」状態に移行します。
2の場合は、このnetlink_dump終了までは上記の「1かつskbにNLMSG_DONEを書き込む余裕がない場合」と同じですが、次回以降のnetlink_dump呼び出しではもう一度cb->dumpが実行されることになります。その後の流れはこれまでの説明と同じです。
netlink_recvmsg
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_dumpはnetlink_recvmsgを発行し続ければいつか再開するということがわかります。6
以下のシーケンス図はDumpの実行から3回目のrecvmsgでDumpが終了した場合の流れです。この図は
- ユーザーが
sendmsgでDumpの実行を要求する - 一回目の
netlink_dumpが実行されるが、skbが十分なサイズでないためcb->dumpは途中で終了(sendmsgは成功) - ユーザーが
recvmsgでDumpで取得されるべきデータを取り出そうとする - 先程の
netlink_dumpで処理されたデータがユーザーに渡されて解放される - 空きが十分できたので
netlink_dumpが再開 cb->dumpが最後まで実行されるが、完了通知のメッセージを乗せる余裕がskbになかった- ユーザーが2度目の
recvmsg - 完了通知を除いたデータがユーザー空間に
netlink_dumpが再開されるが、今回はcb->dumpは呼び出されない。完了通知のみがskbに詰め込まれてcb_runningがfalseに というケースが書かれています。

マルチキャスト (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_ROUTE、RTNLGRP_IPV6_ROUTE、RTNLGRP_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を呼び出します。この関数は
nlk->netlink_bindがNULLでない場合それを実行しnetlink_update_subscriptionsでmc_listの更新とnlk->groups[0]の下位32ビットをgroups(nl_groups)の値で上書きし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_ROUTE(rtnl_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に通知をつなげます。
これでユーザーが通知を受け取ることができるようになりました。
- その場合はNETLINK_USERをファミリーに指定します。NETLINK_USERのみがユーザーからユーザーへの送信が許可されています。↩
- ちなみにユーザーからマルチキャスト送信を試みることも可能ですが、NETLINK_USER以外のファミリーで許可されていないので、本記事でマルチキャストといった場合はユーザーがカーネルからメッセージを受信する通信を意味します。↩
-
本記事では
sock構造体については解説しません。気になる方はこちらの記事をご覧ください。↩ - ソケット関係のシステムコールがどうやってアドレスファミリーの実装を呼び出すかについても、注釈3のリンク先をご覧ください。↩
-
このときの
dumpitにはrtnl_dumpitが登録されています。linkのdumpitであったinet_dump_fibはこのrtnl_dumpitの中で呼ばれるようになっています。↩ -
sk_rmem_allocやsk_rcvbufについての説明はこちらの記事に記載があります。↩