socket()システムコールがソケットを確保するまで
Linuxのネットワークインタフェースに採用されている、BSDソケットですが、Where is the socket()?を参考に、socket()システムコールの深追いをしました。細かいところでも間違っていたら突っ込んでください。カーネルのバージョンは、2.6.20-16-genericです。
socktest.c
#include <sys/socket.h> int main(int argc, char** argv){ int res = socket(AF_INET, SOCK_STREAM, 0); }
単純にソケットを作成するだけのプログラムです。
$ gcc socktest.c $ nm a.out ・・・ 08049588 d p.5756 U socket@@GLIBC_2.0 $
socket()は、glibc2.0のライブラリ関数であることがわかります。Linuxカーネル2.6解読室によれば、socket()システムコールの実体は、socketcall()だそうです。
int socketcall(int call, unsigned long *args);
第1引数callにソケット関数の種類、argsには大元の引数が渡されます。
しかし、これだけでは何をしているかわかりません。そこで、gdbを使って逆アセンブルを行うと、もうひと潜りしてシステムコール呼び出しの中身が見えてきます。
$ gcc -static -g socktest.c $ gdb a.out (gdb) disassemble socket Dump of assembler code for function socket: 0x0804ea50 <socket+0>: mov %ebx,%edx 0x0804ea52 <socket+2>: mov $0x66,%eax 0x0804ea57 <socket+7>: mov $0x1,%ebx 0x0804ea5c <socket+12>: lea 0x4(%esp),%ecx 0x0804ea60 <socket+16>: int $0x80 0x0804ea62 <socket+18>: mov %edx,%ebx 0x0804ea64 <socket+20>: cmp $0xffffff83,%eax 0x0804ea67 <socket+23>: jae 0x804f9a0 <__syscall_error> 0x0804ea6d <socket+29>: ret End of assembler dump. (gdb)
システムコールの呼び出しは、「int 0x80」で行われます。その際、汎用レジスタ%eax、%ebx、%ecx、%edxの値が引数として渡されます。
- %eaxはシステムコール番号。0x66は102(10)だから、102のシステムコール番号が渡されています(ソケット処理関数を指す)。
- %ebxには1がセットされます(ソケット処理のうち、socket()を指す)。
- http://search.luky.org/fol.2004/msg00199.htmlによれば、%ecxにはsocket()の引数が入るのですが、lea命令自体をよく理解できてません。
ひとまず進めます。
ソフトウェア割り込みが CPU に発行されると CPU はあらかじめ設定された割り込みハンドラを実行します。x86 Linux では 0x80 に設定された割り込みベクタがシステムコール起動処理に対応しています。割り込みを契機に CPU は特権モードに移ってスタックがカーネルスタックに切り替わり、eax レジスタからシステムコール番号を取得して対応するシステムコールを起動します。
Linux カーネルのコンテキストスイッチ処理を読み解く - naoyaのはてなダイアリー
というわけで、見てみます。
arch/i386/kernel/entry.S
ENTRY(system_call) ・・・ syscall_call: call *sys_call_table(,%eax,4) movl %eax,PT_EAX(%esp) # store the return value ・・・
で、sys_call_tableが%eaxを引数にして呼び出されて、
arch/i386/kernel/syscall_table.S
ENTRY(sys_call_table) ・・・ ・・・ .long sys_fstatfs /* 100 */ .long sys_ioperm .long sys_socketcall /* 102 */ .long sys_syslog ・・・
とここで、102に対応した関数はsys_socketcallだとわかます。この対応は、http://www.nk.rim.or.jp/~jun/lxasm/syscalltb.htmlに詳しいです。
Linuxのソケット処理は、sys_socketcall関数で一括処理されています。
先ほど%ebxに代入した0x1ですが、include/linux/net.hで、SYS_SOCKETとマクロ定義されていることがわかります。
・・・ #define SYS_SOCKET 1 /* sys_socket(2) */ #define SYS_BIND 2 /* sys_bind(2) */ ・・・
ここまでで、システムコールが呼び出され、システムコールの種類、がわかりました。以後、カーネル内部のソケット処理に突入します。ドキドキ
カーネル内部では、ソケットシステムコールは、sys_socketcall()で実装されています。SYS_SOCKETが第1引数として渡されます。
net/socket.c
・・・ asmlinkage long sys_socketcall(int call, unsigned long __user *args) { unsigned long a[6]; unsigned long a0, a1; int err; if (call < 1 || call > SYS_RECVMSG) return -EINVAL; /* copy_from_user should be SMP safe. */ if (copy_from_user(a, args, nargs[call])) return -EFAULT; err = audit_socketcall(nargs[call] / sizeof(unsigned long), a); if (err) return err; a0 = a[0]; a1 = a[1]; switch (call) { case SYS_SOCKET: err = sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; ・・・
引数callが、switch文で評価されます。ここでは新規ソケットを作成するために、sys_socket()が呼び出されます。argsには、socket()の引数が入っているはずなので、AF_INET、SOCK_STREAMと0です。ローカル変数に代入されて(なぜか先頭のふたつだけ)、sys_socket()に渡されます。sys_socket()は同じくsocket.cに、
asmlinkage long sys_socket(int family, int type, int protocol) { int retval; struct socket *sock; retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; retval = sock_map_fd(sock); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }
とあります。引数がわかりやすくなりました。a0(AF_INET)、a1(SOCK_STREAM)、a[2](0)はそれぞれ、アドレスファミリ、ソケットタイプ、プロトコルに対応ですね。ここらへんはソケットプログラミングの話です。
ここで重要なのはsock_create()。深追いしていきます。なお、各関数の順序は入れ替えてあります。
net/socket.c
・・・ int sock_create(int family, int type, int protocol, struct socket **res) { return __sock_create(family, type, protocol, res, 0); } ・・・ static int __sock_create(int family, int type, int protocol, struct socket **res, int kern) { ・・・ /* * Allocate the socket and allow the family to set things up. if * the protocol is 0, the family is instructed to select an appropriate * default. */ sock = sock_alloc(); if (!sock) { if (net_ratelimit()) printk(KERN_WARNING "socket: no more sockets\n"); return -ENFILE; /* Not exactly a match, but its the closest posix thing */ } sock->type = type; ・・・ } ・・・ static struct socket *sock_alloc(void) { struct inode *inode; struct socket *sock; inode = new_inode(sock_mnt->mnt_sb); if (!inode) return NULL; sock = SOCKET_I(inode); inode->i_mode = S_IFSOCK | S_IRWXUGO; inode->i_uid = current->fsuid; inode->i_gid = current->fsgid; get_cpu_var(sockets_in_use)++; put_cpu_var(sockets_in_use); return sock; } ・・・
new_inode()関数でinodeを確保し、そのinodeポインタを元に、ソケットを確保します。
include/net/sock.h
static inline struct socket *SOCKET_I(struct inode *inode) { return &container_of(inode, struct socket_alloc, vfs_inode)->socket; }
include/linux/kernel.h
/** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * */ #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
という具合に、底に辿り着きました。この、container_of()マクロで、カーネルはソケットを確保します。
ソケットの実体は、socket構造体(及びsock構造体)で、include/linux/net.hで次のように定義されています。
/** * struct socket - general BSD socket * @state: socket state (%SS_CONNECTED, etc) * @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc) * @ops: protocol specific socket operations * @fasync_list: Asynchronous wake up list * @file: File back pointer for gc * @sk: internal networking protocol agnostic socket representation * @wait: wait queue for several uses * @type: socket type (%SOCK_STREAM, etc) */ struct socket { socket_state state; unsigned long flags; const struct proto_ops *ops; struct fasync_struct *fasync_list; struct file *file; struct sock *sk; wait_queue_head_t wait; short type; };
そして、socket構造体は、socket_alloc構造体のメンバとして定義されています。
include/net/sock.h struct socket_alloc { struct socket socket; struct inode vfs_inode; };
socket構造体と共に、inode構造体もメンバとして定義されています。inode構造体は、先ほどnew_inode()関数で確保しました。このinodeを含んだsocket_alloc構造体の先頭アドレスがわかれば、socket構造体メンバへアクセスできます。ここで、container_ofマクロ関数の出番です。
このマクロについては、http://hira.main.jp/wiki/pukiwiki.php?container_of()%2Flinux2.6と、list_headの謎: #include <fujita.h>の説明がわかりやすいです。ここで実体があるのはinodeのみですが、構造体の型(struct socket_alloc)と、メンバ名(vfs_inode)を渡すことで、構造体自体の先頭アドレスが計算される仕組みです。つまり、inode構造体にsocket_alloc構造体を割り当てることで、socket構造体へのアクセスを実現する、ということでしょうか。
この理解でいくと、ここでsocket構造体の確保は完了です。つまり、システムコールレベルでいう、ソケットが作成されたということになります。
container_ofマクロは、こういった計算を行う汎用的なインタフェースとなっているため、カーネルの様々な箇所で使用されているようです。
以上、ユーザ空間におけるシステムコールから、カーネル空間でのソケット確保までを追ってみました。ソケットは、疑似ファイルシステムとして実装されているのですが、ファイルシステムの理解が乏しいため追えていません。inode自体の理解もまだまだ。続きが書けるように頑張ります。