1年生全体のページへ

Linuxシステムにおいては、ユーザー空間のプロセスがネットワークを使用する場合は他と同じくシステムコールを使用して、カーネルの機能を呼び出す。 このシステムコールは、一般にソケットAPIとも呼ばれており、socket, send, recv, setsockopt等のUnix系カーネルに共通したシステムコール群を有している。

では、このユーザープロセスから発行されるシステムコールは、どの様に処理されるのだろうか。 これについて、システムコールを起点として、処理の各プロトコルへの委託と その委託先の関数がどこで、どの様に初期化、設定されているのかという基本的な流れを見てみようと思う。

Linuxでは、システムコールは、以下の特別なマクロを用いて実装される。


#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
    

SYSCALL_DEFINEnは引数をn個持つシステムコールを宣言するためのマクロで、すべてのシステムコールはこれを用いて実装される。 ソケットAPIは様々あるが、カーネル及び、libcではsocketcallと呼ばれる単一のシステムコールにどのソケットAPIを使用するか選択して呼び出す。 このことによってバイナリの大きさが減少する。しかし、この様な実装はifdefで残されているだけで、実際はこの様なシステムコールでは無く、個別に実装されている。


SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
    unsigned long a[AUDITSC_ARGS];
    unsigned long a0, a1;
    int err;
    unsigned int len;

    /* 省略 */

    a0 = a[0];
    a1 = a[1];

    /* 変数callに合わせて適切なシステムコールを呼び出すだけ */
    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;
    case SYS_LISTEN:
        err = sys_listen(a0, a1);
        break;
    case SYS_ACCEPT:
        err = sys_accept4(a0, (struct sockaddr __user *)a1,
                  (int __user *)a[2], 0);
        break;
    /* 以下省略... */
    default:
        /* 意味不明のシステムコールを指定された場合エラーを返す */
        err = -EINVAL;
        break;
    }
    return err;
}
    

上記から分かるように、このシステムコールは、他のソケットAPIを呼び出すだけである。

socketcallの呼び出しのグラフ:dot言語とgraphvizで出力

上のグラフは、socketcallの呼び出しの一連の流れを表している。

次に、ソケットのFDを取得するために、真っ先に呼び出す、socketシステムコールを見てみよう。


SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;

    /* 省略: フラグの設定、不備の訂正等... */

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

    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    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;
}
    

Linuxは、前述の通りsocketcall以外のソケットAPIも、システムコールとして実装されている。

socketシステムコールでは、カーネル側のソケットを表すsocket構造体を生成する。 又、ソケットはLinuxカーネルの内部では、ファイルと同じように実装されており、同じくFD に紐づけされる。これを行うのがsock_map_fdである。


static int sock_map_fd(struct socket *sock, int flags)
{
    struct file *newfile;
    int fd = get_unused_fd_flags(flags);
    if (unlikely(fd < 0))
        return fd;

    newfile = sock_alloc_file(sock, flags, NULL);
    if (likely(!IS_ERR(newfile))) {
        fd_install(fd, newfile);
        return fd;
    }

    put_unused_fd(fd);
    return PTR_ERR(newfile);
}
    

get_unused_fd_flagsで、使用されていないシステムのFDを返す。 そのあと上記の様に、Linuxのファイル構造体であるstruct file;を割り当てて、成功した場合は、先程のFDを使用する。 これで、このソケットはソケットAPIの処理を行う準備が完了した。

カーネル側のデータ構造を見る前に、もう一つシステムコールを見てみる。 以下に、connectシステムコールを示す。


SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
        int, addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;

    /* file descriptorからsocket構造体を取得 */
    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (!sock)
        goto out;

    /* 省略: アドレスのカーネル空間への移動、セキュリティーモジュールに権限の問い合わせ等 */

    err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                 sock->file->f_flags);
out_put:
    fput_light(sock->file, fput_needed);
out:
    return err;
}
    

sockfd_lookup_light関数はFDから、実際のsocket構造体を所得する関数である。 注目を要するのは以下の部分である。


    err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags);
    

このconnectに限らず、総てのソケットAPIシステムコールは、本当の処理を、そのsocket構造体のopsメンバーに委託している。 opsメンバーは、ソケットAPIの総ての関数への関数ポインターを有しており、カーネルがプロトコルに応じた実際の関数を設定している。 ようやくここで、ここまで出てきた、データ構造を見ていく。

socketシステムコールで出てきた、socket構造体は、カーネル側の核となるデータ構造の一つである。 以下に、socket構造体の定義を示す。


/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */
struct socket {
    socket_state        state;

    kmemcheck_bitfield_begin(type);
    short           type;
    kmemcheck_bitfield_end(type);

    unsigned long       flags;

    struct socket_wq __rcu  *wq;

    struct file     *file;
    struct sock     *sk;
    const struct proto_ops  *ops;
};
    

fileメンバーは、前述の通り、ファイルとして扱う為のメンバーである。 他に状態、フラグ、またシステムコールの様な外部とのインターフェースではなく、内部の処理を行うために切り出された、 sock構造体skがあり、内部処理との橋渡しを行っている。

次に、先程のopsメンバーの型である、struct proto_opsの定義を以下に示す。


struct proto_ops {
    int     family;
    struct module   *owner;
    int     (*release)   (struct socket *sock);
    int     (*bind)      (struct socket *sock,
                      struct sockaddr *myaddr,
                      int sockaddr_len);
    int     (*connect)   (struct socket *sock,
                      struct sockaddr *vaddr,
                      int sockaddr_len, int flags);
    int     (*socketpair)(struct socket *sock1,
                      struct socket *sock2);
    int     (*accept)    (struct socket *sock,
                      struct socket *newsock, int flags, bool kern);
    int     (*getname)   (struct socket *sock,
                      struct sockaddr *addr,
                      int *sockaddr_len, int peer);
    unsigned int    (*poll)      (struct file *file, struct socket *sock,
                      struct poll_table_struct *wait);
    int     (*ioctl)     (struct socket *sock, unsigned int cmd,
                      unsigned long arg);
    int     (*listen)    (struct socket *sock, int len);
    int     (*shutdown)  (struct socket *sock, int flags);
    int     (*setsockopt)(struct socket *sock, int level,
                      int optname, char __user *optval, unsigned int optlen);
    int     (*getsockopt)(struct socket *sock, int level,
                      int optname, char __user *optval, int __user *optlen);
    int     (*sendmsg)   (struct socket *sock, struct msghdr *m,
                      size_t total_len);
    int     (*recvmsg)   (struct socket *sock, struct msghdr *m,
                      size_t total_len, int flags);
    int     (*mmap)      (struct file *file, struct socket *sock,
                      struct vm_area_struct * vma);
    ssize_t     (*sendpage)  (struct socket *sock, struct page *page,
                      int offset, size_t size, int flags);
    ssize_t     (*splice_read)(struct socket *sock,  loff_t *ppos,
                       struct pipe_inode_info *pipe, size_t len, unsigned int flags);
    int     (*set_peek_off)(struct sock *sk, int val);
    int     (*peek_len)(struct socket *sock);
    int     (*read_sock)(struct sock *sk, read_descriptor_t *desc,
                     sk_read_actor_t recv_actor);
};
    

前述の様に、様々な関数ポインタがある中、connectへの関数ポインタもあるのが分かる。 connect以外のシステムコールでも、全てこの様に委託されるのが、データ構造からも分かる。

では、これらの関数はどのように設定されるのだろうか。

各種プロトコルは、それぞれのstruct proto_opsをカーネル側に登録しており、ソケットを作成するときに、その引数によって適切に opsメンバーが設定される。以下にIPv4とTCPで通信する際に実際に設定される値をnet/ipv4/af_inet.cから以下に引用する。


const struct proto_ops inet_stream_ops = {
    .family        = PF_INET,
    .owner         = THIS_MODULE,
    .release       = inet_release,
    .bind          = inet_bind,
    .connect       = inet_stream_connect,
    .socketpair    = sock_no_socketpair,
    .accept        = inet_accept,
    .getname       = inet_getname,
    .poll          = tcp_poll,
    .ioctl         = inet_ioctl,
    .listen        = inet_listen,
    .shutdown      = inet_shutdown,
    .setsockopt    = sock_common_setsockopt,
    .getsockopt    = sock_common_getsockopt,
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,
    .mmap          = sock_no_mmap,
    .sendpage      = inet_sendpage,
    .splice_read       = tcp_splice_read,
    .read_sock     = tcp_read_sock,
    .peek_len      = tcp_peek_len,
#ifdef CONFIG_COMPAT
    .compat_setsockopt = compat_sock_common_setsockopt,
    .compat_getsockopt = compat_sock_common_getsockopt,
    .compat_ioctl      = inet_compat_ioctl,
#endif
};
    

このコードでは、各関数ポインターに、ソケットの種類に応じた適切な関数を設定している。 この用法は、オブジェクト指向におけるメソッドの体をなしており、この変数はいわゆる仮想関数テーブル(vtable)の様な役割である。 このinet_stream_opsは以下のinetsw_arrayという配列に初期化される。


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,
    },

    /* 省略... */
};
    

そしてinet_register_protosw関数でカーネルに設定される。 したがって、inetsw_arrayに新しいエントリーを追加することにより、どの種類の通信にどの処理を割り当てるのかが、 各コードで統一的に行うことができ、容易に新たなプロトコルや様々なプロトコルに対応することができるのである。

システムコール委託の様子:dot言語とgraphvizで出力

上のグラフは、一連の処理の委託の流れを表す。

以上で、システムコールを起点として、次々に呼び出される関数が、実際のプロトコルに委託される様子を見てきた。 オブジェクト指向のポリモーフィズム的な要素が多用されている様子がわかったと思う。

ネットワークの処理は、システムコールを起点とする上の層と、NIC(Network Interface Card)からの割り込みを起点として、実際のパケットを吸い上げ、 上の層に渡す下の層に大きく別れており、全体としては、数十万、数百万行の規模がある。 今回紹介したのは、その内のごく一部である。総て解説するには、分厚い本一冊が楽々できるのである。

ワタシハリナックスチョットデキル
みんなでLinuxちょっとできるようになろう。