linux: socketとNet名前空間

LinuxカーネルのNet名前空間の辺りを読んでみます。

まず、抑えておくべきNet名前空間はどんなものかというとこです。ここを抑えておかないと話が進まないですね。 Net名前空間struct netです。これは比較的大きめの構造体です。メンバ変数としてipv4ipv6などの名前が見えます。unxはunitドメインソケットのデータっぽいですね。そんな感じでこのstruct netがネットワークの主要な構造体と鳴っています。

 45 struct net {
 46         atomic_t                passive;        /* To decided when the network
 47                                                  * namespace should be freed.
 48                                                  */
 4
〜略〜
 85 
 86         struct net_device       *loopback_dev;          /* The loopback */
 87         struct netns_core       core;
 88         struct netns_mib        mib;
 89         struct netns_packet     packet;
 90         struct netns_unix       unx;
 91         struct netns_ipv4       ipv4;
 92 #if IS_ENABLED(CONFIG_IPV6)
 93         struct netns_ipv6       ipv6;
 94 #endif
〜略〜

では、どこから見ていくのが良いかというところですが、たまたま見たsocket(2)がちょうど良さ気だったのでここからスタートです。

socket(2)では実際にsocketを作成するのは別のところになります。ここではsock_create()を呼びます。

1241 
1242         retval = sock_create(family, type, protocol, &sock);
1243         if (retval < 0)
1244                 goto out;

そしてsock_create()を見るとcurrentタスクのnsproxyからnet_nsにアクセスしています。ということで、いつ名前空間にアクセスするのかというところは分かりました。

1210 int sock_create(int family, int type, int protocol, struct socket **res)
1211 {
1212         return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
1213 }

あとはこれがどう使われるかですね。__sock_create()では以下の場所で使います。pfは struct net_proto_familyです。ということでこのcreate()はプロトコル依存になります。ここではipv4の場合を見てみたいと思います。

1172         err = pf->create(net, sock, protocol, kern);
1173         if (err < 0)
1174                 goto out_module_put;

ipv4でのcreate()はnet/ipv4/af_inet.cにて設定されています。この場合はinet_create()ですね。

979 static const struct net_proto_family inet_family_ops = {
980         .family = PF_INET,
981         .create = inet_create,
982         .owner  = THIS_MODULE,
983 };

inet_create()はinet_family_opsの定義と同じくaf_inet.cにあります。inet_create()の処理の中ではsk_alloc()の引数に使用しています(user名前空間も使っているのですが、今はnet名前空間だけ見てます)。

320         sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
321         if (!sk)
322                 goto out;

sk_alloc()はnet/core/sock.cにあります。ここではsock_net_set()の2番めの引数に使っているget_net()にstruct netを渡しています。

1410                 sock_lock_init(sk);
1411                 sock_net_set(sk, get_net(net));
1412                 atomic_set(&sk->sk_wmem_alloc, 1);

get_net()は何をしているかというと、自身の参照カウンタを1増やしてから自分自身を返します。socketを作っているので参照数を増やす必要があるってことですね。

176 static inline struct net get_net(struct net net) 177 { 178 atomic_inc(&net->count); 179 return net; 180 }

そして、参照数を増やしたところでsock_net_set()が呼ばれます。これはwrite_pnet()を呼び出します。

2190 void sock_net_set(struct sock *sk, struct net *net)
2191 {
2192         write_pnet(&sk->sk_net, net);
2193 }
2194 

write_pnet()は何をするかというと、possible_net_t構造体のメンバ変数netに現在使用している(さっき参照数を増やした)net名前空間を設定します。

240 static inline void write_pnet(possible_net_t *pnet, struct net *net)
241 {
242 #ifdef CONFIG_NET_NS
243         pnet->net = net;
244 #endif
245 }
246 

socket(2)でNet名前空間を使う主なところはこんなところです。次にこれを使う時のコードを読んでみます。先ほどwrite_pnet()で名前空間を設定したので、読み出しにはread_pnet()ということが想像できます。 read_pnet()はwrite_pnet()のすぐ下にありますし。read_pnet()はこのようになっています。

247 static inline struct net *read_pnet(const possible_net_t *pnet)
248 {
249 #ifdef CONFIG_NET_NS
250         return pnet->net;
251 #else
252         return &init_net;
253 #endif
254 }

read_pnet()がどこで使われているかはlxrで調べるのが簡単なので検索してみるとまあまあります。 適当に見てみると、sock_net()という関数が見つかったのでここから調べてみます。

2183 static inline
2184 struct net *sock_net(const struct sock *sk)
2185 {
2186         return read_pnet(&sk->sk_net);
2187 }
2188 

ラッパー関数っぽいですし、これから調べるのが良さそうですね。これを呼んででいるのを探して、ここから呼び出し元を遡っていくわけです。そこで見つけたbind()だとsock->ops->bindを呼び、

1385                         if (!err)
1386                                 err = sock->ops->bind(sock,
1387                                                       (struct sockaddr *)
1388                                                       &address, addrlen);

sock->opssocket構造体のstruct proto_opsです。

118         const struct proto_ops  *ops;

tcpipv4の場合は[af_inet.c](http://lxr.free-electrons.com/source/net/ipv4/af_inet.c?v=4.1#L898で関数を設定しています。

894 const struct proto_ops inet_stream_ops = {
895         .family            = PF_INET,
896         .owner             = THIS_MODULE,
897         .release           = inet_release,
898         .bind              = inet_bind,
899         .connect           = inet_stream_connect,
900         .socketpair        = sock_no_socketpair,

では、inet_bindを見てみます。ここでは最初にNet名前空間を取得します。

419 int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
420 {
421         struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
422         struct sock *sk = sock->sk;
423         struct inet_sock *inet = inet_sk(sk);
424         struct net *net = sock_net(sk);
425         unsigned short snum;
426         int chk_addr_ret;

inet_bind()の中でnetを使っているのはここらへんです。inet_addr_type()の引数と、

447 
448         chk_addr_ret = inet_addr_type(net, addr->sin_addr.s_addr);

sysctlのチェック。

58         if (!net->ipv4.sysctl_ip_nonlocal_bind &&
459             !(inet->freebind || inet->transparent) &&
460             addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
461             chk_addr_ret != RTN_LOCAL &&
462             chk_addr_ret != RTN_MULTICAST &&
463             chk_addr_ret != RTN_BROADCAST)
464                 goto out;
465 

inet_addr_type()は何をしているのか? 処理はこれで__inet_dev_addr_type()を呼び出しています。

241 unsigned int inet_addr_type(struct net *net, __be32 addr)
242 {
243         return __inet_dev_addr_type(net, NULL, addr);
244 }

__inet_dev_addr_type()はfib_get_table()の呼び出しでfib_table構造体の取得に使っています。

228         local_table = fib_get_table(net, RT_TABLE_LOCAL);
229         if (local_table) {

fib_get_tableはこうなっています。Net名前空間にあるipv4のデータよりfib_table_hashテーブルの要素にアクセスするという使い方でした。

209 static inline struct fib_table *fib_get_table(struct net *net, u32 id)
210 {
211         struct hlist_node *tb_hlist;
212         struct hlist_head *ptr;
213 
214         ptr = id == RT_TABLE_LOCAL ?
215                 &net->ipv4.fib_table_hash[TABLE_LOCAL_INDEX] :
216                 &net->ipv4.fib_table_hash[TABLE_MAIN_INDEX];
217 
218         tb_hlist = rcu_dereference_rtnl(hlist_first_rcu(ptr));
219 
220         return hlist_entry(tb_hlist, struct fib_table, tb_hlist);
221 }

ここまで読んでみてわかったのはsocketのcreate時に名前空間をsocket構造体からアクセスできる場所に保存し、そのsocketを使った操作を実行するときにcreateで設定しておいた名前空間にアクセスして、何かしらの固有なデータを読んだりするということでした。

こうやって見てみるとNet名前空間に関してはCONFIG_NET_NSの有無にかかわらず、カーネルの処理にオーバーヘッドはなさそうですね。ただ、pnet_write()はCONFIG_NET_NSがNの場合は空の関数になるので呼び出し自体が最適化で消されるとか、read_pnet()なら関数呼び出してはなくてinit_netにアクセスというような最適化が入るかもしれませんが。その場合は関数呼び出しのオーバーヘッドはあるでしょうね。 CONFIG_NET_NSはYだけど、たんにNet名前空間を作っていないだけなら作っている場合と比較してオーバーヘッドはなさそうですが。

データサイエンティスト養成読本 機械学習入門編 (Software Design plus)

データサイエンティスト養成読本 機械学習入門編 (Software Design plus)