「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。 それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。 世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。
本稿では、旧版第21章で解説されていたソケットインターフェースについて、カーネルv6.8のコードをベースに主にデータ構造を中心に解説します。(前回の続きになります。)
執筆者 : 須田 哲志、稲葉 貴昭
※ 「新Linuxカーネル解読室」連載記事一覧はこちら
はじめに
前回の記事では、socket構造体とsock構造体にクローズアップして、関連するデータ構造とともにソケット操作処理の流れを見てきました。
しかし、socket構造体やsock構造体をとりまくデータ構造は図1のようにもっと多く存在します。
特にsocket構造体とsock構造体が「ソケットの実体である」として見てきましたが、実際にはこれらの構造体はある構造体のメンバ変数として存在しています。
※図1はsocket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
により生成されるデータ構造の例です。プロトコルによって一部データ構造は変化します。
今回はsocket(2)
システムコールによってこれらの各種データ構造がどのように生成され、関連づけられていくのかを見ていきたいと思います。
前回の記事でも同様でしたが、ソケット生成のついてもプロトコルによって処理が異なる部分が多分にあります。
前回と同様に本記事でもsocket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
を実行したときの処理を追っていきたいと思います。
それでは早速、ソケット生成の全体の流れを俯瞰するために、socket(2)
システムコールのソースコード(__sys_socket()
関数)を見てみましょう。
int __sys_socket(int family, int type, int protocol) { struct socket *sock; int flags; sock = __sys_socket_create(family, type, update_socket_protocol(family, type, protocol)); ... return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); }
__sys_socket()
関数では大きく2つの処理ブロックに分かれていることがわかります。
__sys_socket_create()
socket構造体やsock構造体等のソケットの実体となるデータ構造を構築/生成するsock_map_fd()
file構造体やdentry構造体を生成しファイル抽象化処理を行う
本記事でも2つの処理ブロックそれぞれについて解説していきたいと思います。
なお、先に全体の処理の流れを知りたい方は、本記事の最後に関数の呼び出し関係と処理内容について図示しています(図14)ので、そちらを参照ください。
1. socket構造体とsock構造体の生成
本章では__sys_socket()
関数の1つ目処理ブロックである、__sys_socket_create()
についてみていきます。
__sys_socket() ├── __sys_socket_create() ... 本章で解説 └── sock_map_fd() ... 次章で解説
また、本章ではデータ生成/関連付けという観点において、プロトコル依存部分と非依存部分にわけて解説したいと思います。 図1を簡略化したものに解説範囲を重ねると図2のようになります。
実際にソースコードレベルでも、おおよそ以降で解説する流れでデータ生成/関連付けが行われます。
1.1. socket構造体の生成
最初にsocket構造体の生成処理についてみてみましょう。
socket構造体は__sys_socket_create()
関数を起点とした、以下のフローで生成されます。
__sys_socket_create() └── __socket_create() ├── sock_alloc() : └── ... : └── sock_mnt->mnt_sb->s_op->alloc_inode ... sock_alloc構造体(socket構造体)の生成
フローと言いつつ途中経路をかなり省略してしまっていますが、最終的にはalloc_inode
に登録されているコールバック関数でsocket構造体は生成されます。
ソケットはsockfsという疑似ファイルシステムとして実装されており、sock_mnt
(vfsmount構造体)がまさにsockfsを表現しています。
そして、続くmnt_sb->s_op
はsockfsに対する(スーパーブロックの)操作であり、socket構造体はinodeを確保する文脈(=alloc_inode
)で生成されていることがわかります。
実際にalloc_inode
に登録されているsock_alloc_inode()
関数では、socket構造体をinode構造体と一緒に、socket_alloc構造体として生成(=メモリ確保)しています。
struct socket_alloc { struct socket socket; struct inode vfs_inode; };
static struct inode *sock_alloc_inode(struct super_block *sb) { struct socket_alloc *ei; ei = alloc_inode_sb(sb, sock_inode_cachep, GFP_KERNEL); // 筆者コメント: socket構造体を含むsocket_alloc構造体を生成 ... return &ei->vfs_inode; }
このようにsocket構造体は疑似ファイルシステム(sockfs)によって、inodeを確保する文脈で生成されます。
1.2. プロトコル依存部の処理
本節ではプロトコルに依存した以下のデータ構造がどのように生成/関連付けされていくかを見ていきます。
プロトコルに依存したデータ部分は大きく次の2段階で構築されます。
データ生成関数の特定
図4の水色で示された領域のデータ構築処理(関数)はプロトコルファミリーごとに用意されています。
まずは、このデータ生成関数を特定する必要があります。プロトコル依存データの構築
プロトコルに適した各種データ構造(図4の水色領域内の3つのデータ構造)を生成/関連付けさせます。
1.2.1 プロトコル依存部分のデータ構築関数の特定: inet_create()
前述のとおり、まずはプロトコルファミリーごとに用意されているデータ生成関数を特定する必要があります。
socket(2)
システムコールでは第1引数にプロトコルファミリーが指定されているので、これを使って生成関数を特定することになります。
データ生成関数は以下のnet_families[family]->create
にコールバック関数として登録されており、
net_families
配列のエントリーを特定することで、関数が特定されることになります。
__sys_socket_create() └── __socket_create() ├── sock_alloc() | └── ... | └── sock_mnt->mnt_sb->s_op->alloc_inode ... 前節で解説 └── net_families[family]->create ... プロトコルファミリーごとのデータ生成関数
ここで、net_families
配列について見ておきましょう。
net_families
は「net_proto_family構造体へのポインタ」の配列になっています。
static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
struct net_proto_family { int family; int (*create)(struct net *net, struct socket *sock, int protocol, int kern); struct module *owner; };
定義からnet_proto_family構造体はプロコルファミリー(.family
)と、それに対応するデータ生成関数(.create
)を対応付けるための構造体であることがわかります。
詳細は割愛しますが、各プロコルファミリーが初期化時にsock_register()
を呼び出すことで、.family
をインデックスとして自身のnet_proto_family構造体がnet_families
配列に登録されます。
すなわち、図5に示すようにsocket(2)
システムコールの第1引数で渡されるプロトコルファミリーをインデックスとして、データ生成関数を特定することができます。
図5から今回の例(socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
)では、net_families[PF_INET]->create
登録されているinet_create()
が実行されることがわかります。
1.2.2 プロトコル依存データの構築
前節でデータ生成関数がinet_create()
関数であることが分かりました。
本節ではinet_create()
関数内の処理についてみていきます。
依存データのすべて: inet_protosw構造体
inet_create()
では、図4に示したプロトコル依存部分(水色領域内)のデータが生成/関連付けされます。
ここで、プロトコル依存部分の3つのデータ構造について、それぞれ必要な処理と情報等を整理してみましょう。
構造体 | 必要な処理 | 処理に必要な情報 | 実際の情報 |
---|---|---|---|
sock構造体(XXX_sock構造体) | 生成(=メモリ確保) | 確保すべきメモリサイズ | sizeof(struct tcp_sock) |
proto_ops構造体 | 関連付け(≠メモリ確保) | プロトコルに対応したデータ構造のポインタ | &net_stream_ops |
proto構造体 | 関連付け(≠メモリ確保) | プロトコルに対応したデータ構造のポインタ | &tcp_prot |
実はこれらの「処理に必要な情報」が集約されている構造体があります。 それが以下のinet_protosw構造体になります。
struct inet_protosw { struct list_head list; /* These two fields form the lookup key. */ unsigned short type; /* This is the 2nd argument to socket(2). */ unsigned short protocol; /* This is the L4 protocol number. */ struct proto *prot; const struct proto_ops *ops; unsigned char flags; /* See INET_PROTOSW_* below. */ };
蛇足ですが、inet_protosw構造体を管理するinetsw
配列(詳細は後述)の宣言を見ると次のようなコメントがあります。
/* The inetsw table contains everything that inet_create needs to * build a new socket. */ static struct list_head inetsw[SOCK_MAX];
コメントにもinetsw
配列(inet_protosw構造体)にはinet_create()
の処理に必要なものがすべて詰まっているという旨が書かれていますね。
話を戻してinet_protosw構造体について見ていきます。
inet_protosw構造体には、.type
と.protocol
というメンバ変数を持っていることがわかります。
この.type
と.protocol
にはソケットタイプとプロトコル(番号)が入り、それぞれsocket(2)
の第2,3引数に対応した値が入ります。
すなわち、inet_protosw構造体はプロトコル(+ソケットタイプ)とプロトコル依存のデータ構造等を対応付けていると言えます。
実際に今回の例(socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
)で用いられるinet_protosw構造体のオブジェクトを見てみましょう。
static struct inet_protosw inetsw_array[] = { { .type = SOCK_STREAM, .protocol = IPPROTO_TCP, .prot = &tcp_prot, .ops = &inet_stream_ops, .flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK, }, ... };
たしかに.type
と.protocol
の値がsocket(PF_INET, SOCK_STREAM, IPPROTO_TCP)
の第2,3引数と一致していることがわかりますね。
そして図7に示すとおり、.prot
や.ops
の値がそのまま、プロトコルに対応したデータ構造になっていることや、確保すべきメモリサイズがデータを辿ることで特定できることがわかります。
つまり、適切なinet_protosw構造体のオブジェクトを特定できれば、プロトコル依存のデータ部分は生成/関連づけができるということになります。 それでは、このプロトコルに対応したinet_protosw構造体のオブジェクトはどのように特定しているのでしょうか。
inet_protosw構造体の特定
ここではinet_protosw構造体のオブジェクトがどのように管理されているのかについて見ていきます。
inet_protosw構造体のオブジェクトはinetsw
配列とリストによって管理されています。
/* The inetsw table contains everything that inet_create needs to * build a new socket. */ static struct list_head inetsw[SOCK_MAX];
inetsw
配列はlist_head構造体の配列で図8のように各エントリーがリストの先頭要素を指し示す構造になっています。
inet_protosw構造体は同一.type
ごとにリストとしてまとめられ、各リストの先頭要素をinetsw
配列の各エントリーが指し示しています。
このようにデータを構成することで、.type
(inetsw配列のエントリーの特定)→.protocol
(リストの要素の特定)の順で目的のinet_protosw構造体を見つけ出すことができます。
最後に本節の流れを図9にまとめました。
補足: sock構造体とtcp_sock構造体の関係
ここまでの解説で、sock構造体を生成する処理では「「sock構造体を含むtcp_sock構造体」を生成する」という表現をしていました。 これは、その通りではあるのですが実際にソースコードを追ってみると、一見、sock構造体のみを生成しているように見えます。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern) { struct sock *sk; ... sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern); // 筆者コメント: ここでtcp_sock構造体を生成 ... }
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
でtcp_sock構造体のメモリを確保していますが、
関数の返り値がsock構造体へのポインタ型になっているため、一見するとsock構造体のメモリを確保しているように思えます。
(sk_alloc()
という関数名もsock構造体のメモリ領域を確保するかに思えます。)
ここでtcp_sock構造体の構造を見てみましょう。
tcp_sock構造体では先頭のメンバ変数の構造体が順次ネストしており、最終的にはsock構造体にたどり着くことがわかります。 図中の簡略版に示すよう、構造体の先頭のメンバ変数(構造体)が順次ネストしている場合、これらのポインタ値は同一になります。 そのため、確保したtcp_sock構造体のメモリ領域の先頭アドレスをsock構造体のポインタとして扱っても整合することになります。
2. ソケットのファイル抽象化処理
前章では__sys_socket_create()
関数を起点に処理を辿り、ソケットの実体となるsocket構造体とsock構造体のデータ生成過程を見てきました。
本章ではソケットのファイル抽象化処理としてfile構造体の生成について見ていきます。
まずは以下のsock_map_fd()
関数を見てみましょう。
__sys_socket() ├── __sys_socket_create() ... 前章で解説 └── sock_map_fd() ... 本章で解説
static int sock_map_fd(struct socket *sock, int flags) { struct file *newfile; int fd = get_unused_fd_flags(flags); // 筆者コメント: ファイルディスクリプタを確保 ... newfile = sock_alloc_file(sock, flags, NULL); // 筆者コメント: file構造体を生成 if (!IS_ERR(newfile)) { fd_install(fd, newfile); // 筆者コメント: ファイルディスクリプタとfile構造体を紐づけ return fd; } ... }
sock_map_fd()
関数では主に以下の流れで処理を行っていることがわかります。
1. get_unused_fd_flags()
でファイルディスクリプタを確保
2. sock_alloc_file()
でfile構造体を生成
3. fd_install()
でファイルディスクリプタとfile構造体を紐づけ
それではfile構造体を生成しているsock_alloc_file()
関数を見てみます。
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname) { struct file *file; if (!dname) dname = sock->sk ? sock->sk->sk_prot_creator->name : ""; file = alloc_file_pseudo(SOCK_INODE(sock), sock_mnt, dname, O_RDWR | (flags & O_NONBLOCK), &socket_file_ops); // 筆者コメント: file構造体の生成 ... file->f_mode |= FMODE_NOWAIT; sock->file = file; // 筆者コメント: socket構造体にfile構造体のポインタを登録 file->private_data = sock; // 筆者コメント: file構造体にsocket構造体のポインタを登録 stream_open(SOCK_INODE(sock), file); return file; }
file構造体の生成処理はalloc_file_pseudo()
関数で行っています。
alloc_file_pseudo()
関数を呼び出す際にfile構造体に関連付けられるデータ構造を引数で渡していることがわかります。
特に前回の記事で解説した、ソケットをファイル操作のためのシステムコール(read(2)
/write(2)
など)で扱えるようにするための関数テーブルであるsocket_file_ops
もここで紐づけていることがわかります。
このようにデータ構造を生成/関連付けを行うことで、各種プロトコルに対応したデータが構築されます。
最後に
本記事ではsocket(2)
システムコールのデータ生成処理という観点で解説を行いました。
図解を心がけた結果、ソースコードはあまり掲載(参照)しないスタイルの記事になってしまいました。
そこで最後に全体的な関数の呼び出し関係と処理内容の対応を図14に示しました。(結局図示...。)
次回予告
次回からはデバイスドライバーを起点とするパケットの受信処理に切り込んでいきたいと思います。