Open vSwitchのソースコードを読む(2) Kernel Module
2013-05-14 by Daisuke Kotani最初にKernel moduleを読んでいきます。パケットの転送をするのですから、パケットの入力の部分、転送ルールを検索し適用する部分、パケットの出力をする部分があるはずです。また、ovs-vswitchdと通信している部分もあるはずです。それらがどのように処理されているのかを探していくところから始めます。
ソースコードを読む前に
datapath/READMEがあるので、これを一通り読みます。datapathはbridgeのようなもの、vportsはdatapathにつながるポート(たぶんインターフェイスと対応付けられるんだと思います)で、それぞれのdatapathにはflow tableがあって、flow tableはパケットヘッダの値を基にして作られたflow keyを使って検索するということが分かります。Flow keyの設計の話も書いてありますが、流れを追う上ではあまり重要でなさそうなので、これはとりあえず無視します。
初期化
Kernel moduleの初期化をする関数はmodule_initで指定されているはずなので、まずこれがどこに記述されているのか探します。
$ grep module_init datapath/*
datapath/datapath.c:module_init(dp_init);
ということで、datapath/datapath.cのdp_init関数を読むと、次の関数が順に呼ばれています。
- genl_exec_init()
- ovs_workqueues_init()
- ovs_flow_init()
- ovs_vport_init()
- register_pernet_device(&ovs_net_ops)
- register_netdevice_notifier(&ovs_dp_device_notifier)
- dp_register_genl()
- schedule_delayed_work(&rehash_flow_wq, REHASH_FLOW_INTERVAL)
genl_exec_initはdatapath/genl_exec.cにあります。datapath/genl_exec.cには、何かの関数をGeneric Netlinkを介して実行するためのものが書かれているようです。どこで使っているのかは不明。
ovs_workqueues_initはdatapath/linux/compat/include/workqueue.cにあります。何かworkを順次実行するThreadを走らせているようです。
ovs_flow_initはdatapath/flow.cにあります。flow_cacheを作っているだけのようです。datapath/flow.cは名前からflow tableやflow key関連の処理が書かれているのかな、と推測できます。
ovs_vport_initはdatapath/vport.cにあります。いろんな種類のvport(netdev, internal, gre, gre64, vxlan)の初期化をしているようです。後で詳しく見ます。
register_pernet_deviceはLinuxのNetwork Namespaceが作られたり削除されたりしたときに呼び出される関数のポインタを登録するものです。今回の目的にはあまり関係しなさそうなので、とりあえず無視します。
register_netdevice_notifierは、ネットワークインターフェイスが作られたり削除されたりしたときに呼び出される関数のポインタを登録するものです。ovs_dp_device_notifierの定義はdatapath/dp_notify.cに以下のようにあります。
struct notifier_block ovs_dp_device_notifier = {
.notifier_call = dp_device_event
};
dp_device_event関数がすぐ上にあるので見てみると、インターフェイスがなくなった時にOpen vSwitch内でポートを削除する処理をしていそう、ということが分かります。
dp_register_genlはdatapath/datapath.cの2207行目あたりにあります。General Netlinkを使って送られてくるOVSの制御メッセージとそれに対応する関数のポインタなどが設定されているようです。
vport, netdev
Open vSwitchでは以下の5種類のvportがあります。
- netdev
- internal_dev
- gre
- gre64
- vxlan
netdev はたぶんNICに対応するもの、internal_devはOSがパケットを送受信するために使うもの、greとgre64はGREトンネル、vxlanはVXLANのトンネルを処理するのかな?という気がします。とりあえずnetdevを見ていきます。
vportに関する処理の関数はvport_ops構造体に関数ポインタとして入っています。netdevの場合は、datapath/vport-netdev.cに以下の定義があります。
const struct vport_ops ovs_netdev_vport_ops = {
.type = OVS_VPORT_TYPE_NETDEV,
.flags = VPORT_F_REQUIRED,
.init = netdev_init,
.exit = netdev_exit,
.create = netdev_create,
.destroy = netdev_destroy,
.get_name = ovs_netdev_get_name,
.get_ifindex = ovs_netdev_get_ifindex,
.send = netdev_send,
};
このうち、ovs_vport_initで呼ばれているのは.initなので、netdev_init関数を見ます。この関数の処理は実質的には1行だけです。
br_handle_frame_hook = netdev_frame_hook;
br_handle_frame_hookはbridgeの処理をする関数ポインタを代入しておくLinux kernelの変数です(参考)。netdev_frame_hook関数のポインタを代入しています。
パケットの入力処理
Linux kernelでは、bridgeが有効だと、br_handle_frame_hookに代入されている関数を呼びます。Open vSwitchの場合はdatapath/vport-netdev.cのnetdev_frame_hook関数でした。
static rx_handler_result_t netdev_frame_hook(struct sk_buff **pskb)
{
struct sk_buff *skb = *pskb;
struct vport *vport;
if (unlikely(skb->pkt_type == PACKET_LOOPBACK))
return RX_HANDLER_PASS;
vport = ovs_netdev_get_vport(skb->dev);
netdev_port_receive(vport, skb);
return RX_HANDLER_CONSUMED;
}
netdev_frame_hook関数の中ではnetdev_port_receive関数を呼んでいるだけのようなので、次にnetdev_port_receive関数を読みます。
static void netdev_port_receive(struct vport *vport, struct sk_buff *skb)
{
if (unlikely(!vport))
goto error;
if (unlikely(skb_warn_if_lro(skb)))
goto error;
/* Make our own copy of the packet. Otherwise we will mangle the
* packet for anyone who came before us (e.g. tcpdump via AF_PACKET).
* (No one comes after us, since we tell handle_bridge() that we took
* the packet.) */
skb = skb_share_check(skb, GFP_ATOMIC);
if (unlikely(!skb))
return;
skb_push(skb, ETH_HLEN);
if (unlikely(compute_ip_summed(skb, false)))
goto error;
vlan_copy_skb_tci(skb);
ovs_vport_receive(vport, skb);
return;
error:
kfree_skb(skb);
}
netdev_port_receive関数では、パケットをコピーした後、ovs_vport_receive関数を呼んでいるようです。
ovs_vport_receive関数はdatapath/vport.cにあります。
void ovs_vport_receive(struct vport *vport, struct sk_buff *skb)
{
struct vport_percpu_stats *stats;
stats = this_cpu_ptr(vport->percpu_stats);
u64_stats_update_begin(&stats->sync);
stats->rx_packets++;
stats->rx_bytes += skb->len;
u64_stats_update_end(&stats->sync);
if (!(vport->ops->flags & VPORT_F_FLOW))
OVS_CB(skb)->flow = NULL;
if (!(vport->ops->flags & VPORT_F_TUN_ID))
OVS_CB(skb)->tun_key = NULL;
ovs_dp_process_received_packet(vport, skb);
}
statsの更新をした後、ovs_dp_process_received_packet関数を呼んでいます。関数名がパケットをどう処理するか決定する部分に近くなってきている気がします(^^)
ovs_dp_process_received_packet関数は、datapath/datapath.cの200行目付近にあります。長めなので抜粋します。
void ovs_dp_process_received_packet(struct vport *p, struct sk_buff *skb)
{
struct datapath *dp = p->dp;
struct sw_flow *flow;
struct dp_stats_percpu *stats;
u64 *stats_counter;
int error;
stats = this_cpu_ptr(dp->stats_percpu);
if (!OVS_CB(skb)->flow) {
struct sw_flow_key key;
int key_len;
/* Extract flow from 'skb' into 'key'. */
error = ovs_flow_extract(skb, p->port_no, &key, &key_len);
(省略)
/* Look up flow. */
flow = ovs_flow_tbl_lookup(rcu_dereference(dp->table),
&key, key_len);
if (unlikely(!flow)) {
struct dp_upcall_info upcall;
(省略)
ovs_dp_upcall(dp, skb, &upcall);
consume_skb(skb);
stats_counter = &stats->n_missed;
goto out;
}
OVS_CB(skb)->flow = flow;
}
stats_counter = &stats->n_hit;
ovs_flow_used(OVS_CB(skb)->flow, skb);
ovs_execute_actions(dp, skb);
out:
(省略)
}
ovs_flow_extract関数でパケットからflow keyを抽出して、それを使ってovs_flow_tbl_lookup関数でflow tableを検索、見つかったらovs_execute_actions関数を、見つからなかったらovs_dp_upcall関数を実行しているようです。
ovs_dp_upcall関数は、Kernel moduleのflow tableに一致するフローがなかった場合にuserlandのプロセス(ovs-vswitchd)にパケットを送るもの、ovs_execute_actionsはflow tableの一致するエントリについているフローを実行するものです。
flow keyやflow tableに関しては後で詳しく見ることにして、ovs_execute_actions関数とovs_dp_upcall関数をそれぞれ追っていきます。
パケットの出力処理
パケットに指定された処理を適用するovs_execute_actions関数はdatapath/actions.cにあります。一部抜粋します。
/* Execute a list of actions against 'skb'. */
int ovs_execute_actions(struct datapath *dp, struct sk_buff *skb)
{
struct sw_flow_actions *acts = rcu_dereference(OVS_CB(skb)->flow->sf_acts);
struct loop_counter *loop;
int error;
(省略)
error = do_execute_actions(dp, skb, acts->actions,
acts->actions_len, false);
(省略)
return error;
}
省略した部分には、loopしすぎないように制御するコードが入っていました。どこでどうループするのか分かっていませんが... do_execute_actions関数以降は、パケットの書き換えやuserlandのプロセスへのupcall(ovs_dp_upcallを使っています)の処理が書かれています。最終的には出力されるパケットは、最終的にdo_output関数が呼ばれます。
static int do_output(struct datapath *dp, struct sk_buff *skb, int out_port)
{
struct vport *vport;
if (unlikely(!skb))
return -ENOMEM;
vport = ovs_vport_rcu(dp, out_port);
if (unlikely(!vport)) {
kfree_skb(skb);
return -ENODEV;
}
ovs_vport_send(vport, skb);
return 0;
}
ovs_vport_send関数が呼ばれています。これはdatapath/vport.cにあります。
int ovs_vport_send(struct vport *vport, struct sk_buff *skb)
{
int sent = vport->ops->send(vport, skb);
if (likely(sent)) {
struct vport_percpu_stats *stats;
stats = this_cpu_ptr(vport->percpu_stats);
u64_stats_update_begin(&stats->sync);
stats->tx_packets++;
stats->tx_bytes += sent;
u64_stats_update_end(&stats->sync);
}
return sent;
}
vport->ops->send関数が呼ばれています。これは、vport_ops構造体の中のsendという関数ポインタが指している関数です。 netdevの場合のvport_ops構造体は以下の通りでした。
const struct vport_ops ovs_netdev_vport_ops = {
.type = OVS_VPORT_TYPE_NETDEV,
.flags = VPORT_F_REQUIRED,
.init = netdev_init,
.exit = netdev_exit,
.create = netdev_create,
.destroy = netdev_destroy,
.get_name = ovs_netdev_get_name,
.get_ifindex = ovs_netdev_get_ifindex,
.send = netdev_send,
};
ということで、datapath/vport-netdev.c内のnetdev_send関数を見ると、dev_queue_xmit関数(パケットをデバイスの送信キューに積むもの)を呼んでいます。
パケットをユーザランドに渡す処理
flow tableに該当のエントリがなかったパケット、ユーザランドのプロセスに渡すように指定されたパケットはovs_dp_upcall関数に渡されます。この関数はdatapath/datapath.cの260行目付近にあります。ちょっと長いので抜粋します。
int ovs_dp_upcall(struct datapath *dp, struct sk_buff *skb, const struct dp_upcall_info *upcall_info)
{
struct dp_stats_percpu *stats;
int dp_ifindex;
int err;
(省略)
if (!skb_is_gso(skb))
err = queue_userspace_packet(ovs_dp_get_net(dp), dp_ifindex, skb, upcall_info);
else
err = queue_gso_packets(ovs_dp_get_net(dp), dp_ifindex, skb, upcall_info);
if (err)
goto err;
return 0;
err:
(省略)
return err;
}
skb_is_gsoかどうかによってqueue_userspace_packet関数かqueue_gso_packets関数が呼ばれています。GSOに関してはこちらが参考になると思います。queue_gso_packets関数の中でqueue_userspace_packet関数が呼ばれているので、次はqueue_userspace_packet関数を見ていきます。datapath/datapath.cの346行目付近にあります。一部抜粋します。
static int queue_userspace_packet(struct net *net, int dp_ifindex, struct sk_buff *skb, const struct dp_upcall_info *upcall_info)
{
struct ovs_header *upcall;
struct sk_buff *nskb = NULL;
struct sk_buff *user_skb; /* to be queued to userspace */
struct nlattr *nla;
unsigned int len;
int err;
(省略)
len = sizeof(struct ovs_header);
len += nla_total_size(skb->len);
len += nla_total_size(FLOW_BUFSIZE);
if (upcall_info->cmd == OVS_PACKET_CMD_ACTION)
len += nla_total_size(8);
user_skb = genlmsg_new(len, GFP_ATOMIC);
if (!user_skb) {
err = -ENOMEM;
goto out;
}
upcall = genlmsg_put(user_skb, 0, 0, &dp_packet_genl_family,
0, upcall_info->cmd);
upcall->dp_ifindex = dp_ifindex;
(省略)
if (upcall_info->userdata)
nla_put_u64(user_skb, OVS_PACKET_ATTR_USERDATA,
nla_get_u64(upcall_info->userdata));
nla = __nla_reserve(user_skb, OVS_PACKET_ATTR_PACKET, skb->len);
skb_copy_and_csum_dev(skb, nla_data(nla));
genlmsg_end(user_skb, upcall);
err = genlmsg_unicast(net, user_skb, upcall_info->portid);
out:
kfree_skb(nskb);
return err;
}
General Netlinkを使ってパケットを送信していることがgenlmsg_put、genlmsg_unicast関数などから分かります。dp_packet_genl_familyは初期化の段階でdp_register_genl関数でGeneral Netlinkに登録していましたし、それを使っているのでしょう(きっと)。
次回の予定
Kernel moduleの初期化から、パケットの入力から出力までの流れを一通り読んだつもりです。まだflow tableの検索の部分を見ていないので、次回はflow tableを見ていきます。
- Open vSwitchのソースコードを読む(11) flow table のエントリ
- Open vSwitchのソースコードを読む(10) flow table の検索
- Open vSwitchのソースコードを読む(9) dpif
- Open vSwitchのソースコードを読む(8) OpenFlow channel の管理
- Open vSwitchのソースコードを読む(7) ovs-vswitchd の ofproto
Tweet