Open19

開発ログ:TCP/IP プロトコルスタック自作 with Rust

nukopynukopy
nukopynukopy

ソケット API とソケット

ソケットとは

ソケット socket とは、OS が提供する、プロセス間の通信エンドポイントである。同一ホスト内、またはネットワークを介した異なるホスト上のプロセス同士がデータをやり取りする際に、この「ソケット」が通信の出入り口の役割を果たす。

アプリケーション(=プロセス)において、ソケットはファイルディスクリプタとして扱うことができる。2 つのプロセス同士がソケットを介してデータをやりとりするため、両端のプロセスがファイルディスクリプタとしてソケットを管理している状態になっている。

  • 引用元:(Qiita, 2025/06) ソケット通信を一緒に理解しよう!!

https://qiita.com/fujifuji1414/items/6daa393a86582d81f0b5

先述したようにアプリケーションから見れば、ファイルディスクリプタ file descriptor として扱うことができる。以下の例では、fd がソケットである。

int fd = socket(AF_INET, SOCK_STREAM, 0);

ソケット API とは

ソケット API は、OS が提供する、アプリケーションからソケットを操作するための API である。

位置づけ

ソケット API は、位置づけとしてはユーザ空間とカーネル空間の境界に存在する。

アプリケーションはソケット API を呼び出すことで、カーネル内部の TCP/IP プロトコルスタックを利用できる。ソケット API は POSIX で定義され、ほとんどの UNIX 系 OS(Linux, BSD, macOS)や Windows に実装されている。

ソケット API により、アプリケーションはソケットをファイルディスクリプタとして扱えるようになるため、他のプロセスとの通信を通常のファイル I/O(ファイルへの読み書き)と同じインタフェースで実現することができる。

具体的な関数群

ソケット API を構成する主な関数(システムコール)は以下の通り:

  • socket() : ソケット生成(ファイルディスクリプタを返す)
  • bind() : ソケットに IP アドレスとポート番号を割り当てる
  • listen() : ソケットをサーバ用に待ち受け状態にする
  • accept() : 接続要求を受け付け、新しいソケットを返す
  • connect() : クライアントからサーバへの接続を開始する
  • send() / recv() : データ送受信
  • close() : ソケットを閉じる
nukopynukopy

ソケット API の定義と役割

POSIX における仕様の定義

  • POSIX 標準 (IEEE 1003.1, The Open Group Base Specifications)
    • ソケット API(socket(), bind(), connect(), listen(), accept(), send(), recv(), close() 等)の関数インターフェイスと挙動を標準として定義している。
    • もともとは 4.2 BSD における「BSD ソケット API」が起源だが、POSIX に取り込まれて事実上の標準となりました。

ソケット API の実装(実体)はどこにあるのか

C 言語のアプリケーションを例に。

宣言部

C 言語のアプリケーションを実装するうえでは、ソケット API(socket() などの関数群)の定義はヘッダファイル <sys/socket.h>
に定義されている。

int socket(int domain, int type, int protocol); のように、このヘッダファイルで関数プロトタイプを宣言を行っている。ヘッダファイルには関数本体の実装は存在せず、コンパイラに「こういう関数がある」と知らせる役割のみである。

ソケット API の実装

ソケット API の実装は libc(glibc, musl, BSD libc 等)に存在する。libc はシステムコールのラッパーであり、ソケット API の 関数シンボルとその実装を提供している。

たとえば socket() 関数の中で syscall(SYS_socket, …) を呼び出し、カーネル空間へ処理を委譲する。アプリケーションが呼んでいるのは libc の socket() 関数である。

もちろん低レベルな API である syscall(SYS_socket, ...) を直接呼び出すことでシステムはできるが、一般的なアプリケーションでこれをやることは少ないと思われる。

アプリケーションで libc の socket() -> syscall(SYS_socket, …) が実行されると、カーネルへ制御が移る。 CPU の命令(syscall 命令など)でユーザ空間からカーネル空間へコンテキストスイッチが行われる。

カーネル内部の実装

カーネルに制御が移ると、カーネルに実装されている sys_socket という関数が実行される。この関数では、struct socket を生成し、適切なプロトコルスタックに紐付ける。そして、新しいファイルディスクリプタをアプリケーションに返す。

nukopynukopy

ソケット API を使ってみる

「アプリケーションからソケット API を呼び出してソケットが生成されるまで」の流れを整理してみる。

ソースコード

以下にサンプルコードを示す。

2 つのソケットを生成してそのファイルディスクリプタの番号を出力するというかなりシンプルなコードである。

プロセス起動時に、プロセスにファイルディスクリプタ 0, 1, 2 が割り当てられるため、おそらく 2 つのソケットを生成したらファイルディスクリプタは 3, 4 が割り当てられるはずである。

create_socket.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // AF_INET 等
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int create_socket(void) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd == -1) {
        perror("socket");
        return -1;
    }

    return fd;
}

int main(void) {
    printf("Start creating sockets...\n");

    // IPv4/TCP ソケットを作る(プロトコルは 0 で自動選択)
    // fds: file descriptors
    // fd: file descriptor
    // create array of file descriptors
    int fds[2];
    for (int i = 0; i < 2; i++) {
        fds[i] = create_socket();
        if (fds[i] == -1) {
            perror("Failed to create socket");
            return 1;
        }
        printf("socket fd%d = %d\n", i, fds[i]);
    }

    // close all sockets
    for (int i = 0; i < 2; i++) {
        close(fds[i]);
    }
    printf("All sockets closed.\n");

    return 0;
}

ビルド & 実行

gcc create_socket.c -o main
./main

出力

Start creating sockets...
socket fd0 = 3
socket fd1 = 4
All sockets closed.

strace でシステムコールを追ってみる

gcc create_socket.c -o main
strace -o create_socket.log ./main
cat create_socket.log
  • create_socket.log
create_socket.log
execve("./main", ["./main"], 0xffffd0a08c90 /* 32 vars */) = 0
brk(NULL)                               = 0xca3e1ccf5000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xed52ae6ce000
faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=23783, ...}) = 0
mmap(NULL, 23783, PROT_READ, MAP_PRIVATE, 3, 0) = 0xed52ae6c8000
close(3)                                = 0
openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\360\206\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1722920, ...}) = 0
mmap(NULL, 1892240, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_DENYWRITE, -1, 0) = 0xed52ae4c7000
mmap(0xed52ae4d0000, 1826704, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0xed52ae4d0000
munmap(0xed52ae4c7000, 36864)           = 0
munmap(0xed52ae68e000, 28560)           = 0
mprotect(0xed52ae66a000, 77824, PROT_NONE) = 0
mmap(0xed52ae67d000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0xed52ae67d000
mmap(0xed52ae682000, 49040, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xed52ae682000
close(3)                                = 0
set_tid_address(0xed52ae6cefb0)         = 18451
set_robust_list(0xed52ae6cefc0, 24)     = 0
rseq(0xed52ae6cf600, 0x20, 0, 0xd428bc00) = 0
mprotect(0xed52ae67d000, 12288, PROT_READ) = 0
mprotect(0xca3de8bdf000, 4096, PROT_READ) = 0
mprotect(0xed52ae6d3000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0xed52ae6c8000, 23783)           = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0
getrandom("\x45\x9a\x98\x85\x7b\xc3\x8b\xf8", 8, GRND_NONBLOCK) = 8
brk(NULL)                               = 0xca3e1ccf5000
brk(0xca3e1cd16000)                     = 0xca3e1cd16000
write(1, "Start creating sockets...\n", 26) = 26
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
write(1, "socket fd0 = 3\n", 15)        = 15
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 4
write(1, "socket fd1 = 4\n", 15)        = 15
close(3)                                = 0
close(4)                                = 0
write(1, "All sockets closed.\n", 20)   = 20
exit_group(0)                           = ?
+++ exited with 0 +++

socket, close に絞ってみる:

strace -o create_socket.mini.log -e socket,close ./main
cat create_socket.mini.log 

出力。一部関係のない部分は省略。ソケット API を利用してsocket システムコールを発行できた。

create_socket.mini.log
...
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 4
close(3)                                = 0
close(4)                                = 0
nukopynukopy

全体像

登場人物

  • Application
    • ユーザ空間の実行バイナリ。
    • #include <sys/socket.h> により int socket(int domain, int type, int protocol); の宣言を得て、libc の socket() 関数を呼び出す。
  • libc(ソケット API 実装)
    • glibc/musl 等の動的ライブラリ。
    • ソケット API の socket() 関数の実体を提供し、内部でシステムコール(SYS_socket)を発行して Kernel に処理を委譲する。
    • ここが「POSIX ソケット API」を実装している主体。
  • Kernel
    • システムコール sys_socket を実装。
    • struct socket を生成し、プロトコルスタックに紐付け、fd を割り当てて返却します。

※ ヘッダ <sys/socket.h> は宣言のみ(コンパイル時の役割)。実体は libc。処理本体は Kernel。

ソケット生成の処理の流れ

  1. Application

    • #include <sys/socket.h> により socket() の関数プロトタイプを得る。
    • 実行時、socket(AF_INET, SOCK_STREAM, 0) を libc の socket() に解決して呼び出す。
  2. libc

    • socket() の内部でシステムコール(SYS_socket)を発行し、ユーザ空間からカーネル空間へ制御を移す。
    • 引数(AF_INET, SOCK_STREAM, 0)はそのまま Kernel に渡される。
  3. Kernel

    • sys_socket が呼ばれ、構造体 struct socket を 生成。
    • 対応するプロトコルスタック(例:TCP/IPv4)にソケットを紐付け。
    • 新規ファイルディスクリプタ(int 型)を割り当て、戻り値としてユーザ空間へ返す。
  4. libc → Application

    • libc は Kernel から受け取ったファイルディスクリプタ fdsocket() の戻り値として Application に返す。
    • 以後、アプリはこの fd に対して connect() / bind() / listen() / accept() / read() / write() / close() 等を行える。

シーケンス図

nukopynukopy

とりあえずテストプログラムの hexdump まで。

./test/step0.exe
14:40:27.068 [D] main: Hello, World! (test/step0.c:7)
+------+-------------------------------------------------+------------------+
| 0000 | 45 00 00 30 00 80 00 00 ff 01 bd 4a 7f 00 00 01 | E..0.......J.... |
| 0010 | 7f 00 00 01 08 00 35 64 00 80 00 01 31 32 33 34 | ......5d....1234 |
| 0020 | 35 36 37 38 39 30 21 40 23 24 25 5e 26 2a 28 29 | 567890!@#$%^&*() |
+------+-------------------------------------------------+------------------+
nukopynukopy

TCP/IP のネットワークインタフェース層を実装していく。プロトコルは Ethernet プロトコルを使用する。

※Ethernet が支配的だが他のリンクプロトコル用のネットワークデバイスも存在する

ここは IP パケットの出入り口である。IP パケットはネットワークデバイスを通じてネットワークインタフェース層のプロトコル(リンクプロトコル、データリンクプロトコル)によって運ばれる。

nukopynukopy

構造としては以下のようになる。

アプリケーション
→ カーネル [
    → ソケット API
    → TCP/IP プロトコルスタック
    → デバイスドライバ
] → ネットワークデバイス(NIC: Network Interface Card。物理 NIC。VM の場合、仮想 NIC → 物理 NIC という流れになる。)

物理 NIC のイメージ:

引用元:ネットワークエンジニアとして - サーバのハードウェア

https://www.infraexpert.com/info/server02.html