Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14996] ファイルの概要

このコミットは、Go言語のnetおよびsyscallパッケージにおいて、Linux環境でのソケット操作の効率と堅牢性を向上させるための変更を導入しています。具体的には、accept4システムコールとSOCK_CLOEXECフラグの利用を導入し、ソケットの作成および接続受け入れ時にファイルディスクリプタの非ブロッキング設定とClose-on-execフラグの設定をアトミックに行うように改善しています。これにより、競合状態の回避、パフォーマンスの向上、およびファイルディスクリプタリークの防止が図られています。

コミット

commit 31f58dce67b449e5a268714dace703a1dcd24035
Author: Ian Lance Taylor <iant@golang.org>
Date:   Mon Jan 28 08:54:15 2013 -0800

    net, syscall: use accept4 and SOCK_CLOEXEC on Linux
    
    R=golang-dev, bradfitz, mikioh.mikioh, dave, minux.ma
    CC=golang-dev
    https://golang.org/cl/7227043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/31f58dce67b4449e5a268714dace703a1dcd24035

元コミット内容

net, syscall: use accept4 and SOCK_CLOEXEC on Linux

このコミットは、Linux上でaccept4システムコールとSOCK_CLOEXECフラグを使用するようにnetおよびsyscallパッケージを変更します。

変更の背景

従来のUnix系システムでは、ソケットを作成(socket)したり、接続を受け入れ(accept)たりした後、そのファイルディスクリプタに対して追加の操作(非ブロッキング設定のためのfcntl(F_SETFL, O_NONBLOCK)や、Close-on-exec設定のためのfcntl(F_SETFD, FD_CLOEXEC))を行う必要がありました。これらの操作は、それぞれが独立したシステムコールとして実行されるため、以下のような問題を引き起こす可能性がありました。

  1. 競合状態 (Race Condition): socketacceptでファイルディスクリプタが作成されてから、Close-on-execフラグが設定されるまでのごく短い時間内に、もしforkexecが同時に発生した場合、子プロセスに意図せずソケットのファイルディスクリプタが継承されてしまう可能性があります。これはファイルディスクリプタリークやセキュリティ上の問題につながる可能性があります。
  2. パフォーマンスオーバーヘッド: 複数のシステムコールを連続して呼び出すことは、単一のシステムコールで同等の操作を行うよりもオーバーヘッドが大きくなります。特に高負荷なネットワークアプリケーションでは、このオーバーヘッドが顕著になる可能性があります。

Linuxカーネルは、これらの問題を解決するためにaccept4システムコールとSOCK_CLOEXECSOCK_NONBLOCKといったフラグを導入しました。このコミットは、Goの標準ライブラリがこれらの新しい機能を利用し、より効率的で堅牢なソケットプログラミングを実現することを目的としています。

前提知識の解説

  • ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、ファイルやソケットなどのI/Oリソースを識別するためにカーネルが割り当てる整数値です。
  • socketシステムコール: 新しいソケットを作成するためのシステムコールです。
  • acceptシステムコール: 接続待ち状態のソケット(リスニングソケット)に対して、新しいクライアントからの接続を受け入れ、その接続のための新しいソケットのファイルディスクリプタを返すシステムコールです。
  • accept4システムコール: Linux 2.6.28で導入されたacceptの拡張版です。acceptと同様に接続を受け入れますが、追加のflags引数を受け取ります。このフラグを使って、新しいソケットのファイルディスクリプタに対してSOCK_NONBLOCKSOCK_CLOEXECなどの属性をアトミックに設定できます。
  • SOCK_CLOEXECフラグ: ソケット作成時(socketaccept4)に指定できるフラグで、作成されるファイルディスクリプタにFD_CLOEXECフラグを自動的に設定します。FD_CLOEXECフラグが設定されたファイルディスクリプタは、execシステムコール(新しいプログラムを実行する際に呼び出される)が成功した際に自動的に閉じられます。これにより、子プロセスへの意図しないファイルディスクリプタの継承を防ぎ、リソースリークやセキュリティリスクを低減します。
  • SOCK_NONBLOCKフラグ: ソケット作成時(socketaccept4)に指定できるフラグで、作成されるファイルディスクリプタを非ブロッキングモードに設定します。非ブロッキングモードでは、I/O操作(読み書きや接続受け入れなど)がすぐに完了しない場合でも、呼び出し元をブロックせずにエラー(通常はEAGAINまたはEWOULDBLOCK)を返します。これにより、アプリケーションは複数のI/O操作を同時に処理できるようになります(例: イベントループやマルチプレキシング)。
  • syscall.SetNonblock(fd, true): 指定されたファイルディスクリプタfdを非ブロッキングモードに設定するGoの関数です。内部的にはfcntl(fd, F_SETFL, O_NONBLOCK)を呼び出します。
  • syscall.CloseOnExec(fd): 指定されたファイルディスクリプタfdFD_CLOEXECフラグを設定するGoの関数です。内部的にはfcntl(fd, F_SETFD, FD_CLOEXEC)を呼び出します。
  • syscall.ForkLock: Goのsyscallパッケージにおけるミューテックス(読み書きロック)です。forkシステムコールが呼び出される際に、ファイルディスクリプタの状態が安定していることを保証するために使用されます。特に、forkexecの間にファイルディスクリプタが閉じられないようにするために、Close-on-execフラグの設定と関連する操作を保護します。

技術的詳細

このコミットの主要な変更点は、Linux環境において、ソケットの作成と接続受け入れのプロセスを最適化し、より安全にすることです。

  1. socketシステムコールの最適化 (sysSocket関数):

    • 新しいnet/sock_cloexec.goファイルにsysSocket関数が導入されました。この関数は、syscall.Socketを呼び出す際にSOCK_NONBLOCKSOCK_CLOEXECフラグを同時に渡すことを試みます。
    • もしカーネルがこれらのフラグをサポートしている場合(Linux 2.6.27以降)、ソケットは単一のシステムコールで非ブロッキングかつClose-on-execとして作成されます。これにより、競合状態のリスクが排除され、システムコール呼び出しのオーバーヘッドが削減されます。
    • もしEINVALエラー(無効な引数)が返された場合、これはカーネルがこれらのフラグをサポートしていないことを意味します。この場合、従来の方式(syscall.Socketをフラグなしで呼び出し、その後にsyscall.CloseOnExecsyscall.SetNonblockを個別に呼び出す)にフォールバックします。このフォールバックパスでは、syscall.ForkLockを使用して、Close-on-exec設定中の競合状態を緩和します。
    • net/sock_posix.gosocket関数は、この新しいsysSocket関数を呼び出すように変更されました。
  2. acceptシステムコールの最適化 (accept関数):

    • 同様に、net/sock_cloexec.goaccept関数が導入されました。この関数は、まずsyscall.Accept4を呼び出すことを試み、SOCK_NONBLOCKSOCK_CLOEXECフラグを渡します。
    • もしカーネルがaccept4をサポートしている場合(Linux 2.6.28以降)、受け入れられたソケットは単一のシステムコールで非ブロッキングかつClose-on-execとして設定されます。
    • もしENOSYSエラー(システムコールが存在しない)が返された場合、これはカーネルがaccept4をサポートしていないことを意味します。この場合、従来のsyscall.Acceptを呼び出し、その後にsyscall.CloseOnExecsyscall.SetNonblockを個別に呼び出す方式にフォールバックします。ここでもsyscall.ForkLockが使用されます。
    • net/fd_unix.gonetFD.accept関数は、この新しいaccept関数を呼び出すように変更されました。これにより、従来のsyscall.SetNonblocksyscall.CloseOnExecの呼び出し、およびsyscall.ForkLockの利用が削除されました。
  3. syscallパッケージの変更:

    • src/pkg/syscall/syscall_linux.goAccept4関数が追加され、accept4システムコールをGoから呼び出せるようにラッパーが提供されました。
    • src/pkg/syscall/syscall_linux_386.go, src/pkg/syscall/syscall_linux_amd64.go, src/pkg/syscall/syscall_linux_arm.goには、各アーキテクチャ向けのaccept4システムコールの定義が追加されました。
    • src/pkg/syscall/zsyscall_linux_amd64.gosrc/pkg/syscall/zsyscall_linux_arm.goには、accept4システムコールを実際に呼び出すための低レベルな実装が追加されました。これらのファイルは通常、自動生成されます。
  4. プラットフォームごとの実装の分離:

    • net/sock_cloexec.go+build linuxタグを持ち、Linuxシステムでのみコンパイルされます。
    • net/sys_cloexec.goという新しいファイルも追加され、こちらは+build darwin freebsd netbsd openbsdタグを持ちます。このファイルには、accept4SOCK_CLOEXECフラグをサポートしないUnix系システム向けの従来のフォールバックロジック(syscall.ForkLockを使ったsocketacceptのラッパー)が含まれています。これにより、コードのプラットフォーム依存性が明確に分離され、保守性が向上しています。

これらの変更により、GoのネットワークスタックはLinux上でより効率的かつ安全に動作するようになります。特に、ファイルディスクリプタの作成と属性設定がアトミックに行われることで、マルチスレッド環境やfork/execを多用するアプリケーションでの潜在的な問題が軽減されます。

コアとなるコードの変更箇所

  • src/pkg/net/fd_unix.go: netFD.accept関数から、個別のsyscall.SetNonblocksyscall.CloseOnExec呼び出し、およびsyscall.ForkLockの使用が削除され、新しいacceptヘルパー関数に置き換えられました。
  • src/pkg/net/file_unix.go: newFileFD関数でsyscall.SetNonblockの呼び出しが追加されました。これは、accept4の導入とは直接関係ありませんが、ファイルディスクリプタの非ブロッキング設定の一貫性を保つための変更です。
  • src/pkg/net/sock_cloexec.go: 新規ファイル。Linux固有のsysSocketacceptヘルパー関数を定義し、SOCK_NONBLOCKSOCK_CLOEXECフラグ、およびaccept4システムコールの利用をカプセル化します。
  • src/pkg/net/sock_posix.go: socket関数が、新しいsysSocketヘルパー関数を呼び出すように変更されました。これにより、従来のsyscall.Socket呼び出しとそれに続くsyscall.CloseOnExecsyscall.ForkLockの使用が削除されました。
  • src/pkg/net/sys_cloexec.go: 新規ファイル。Linux以外のUnix系システム(Darwin, FreeBSD, NetBSD, OpenBSD)向けのsysSocketacceptヘルパー関数を定義します。これらは従来のsyscall.ForkLockを使った実装を含みます。
  • src/pkg/syscall/syscall_linux.go: Accept4関数が追加され、accept4システムコールをGoから呼び出すためのラッパーを提供します。
  • src/pkg/syscall/syscall_linux_386.go: _ACCEPT4定数とaccept4システムコールを呼び出すためのsocketcallラッパーが追加されました。
  • src/pkg/syscall/syscall_linux_amd64.go: accept4システムコールの定義が追加されました。
  • src/pkg/syscall/syscall_linux_arm.go: accept4システムコールの定義が追加されました。
  • src/pkg/syscall/zsyscall_linux_amd64.go: accept4システムコールの低レベルな実装が追加されました(自動生成ファイル)。
  • src/pkg/syscall/zsyscall_linux_arm.go: accept4システムコールの低レベルな実装が追加されました(自動生成ファイル)。

コアとなるコードの解説

このコミットの核心は、src/pkg/net/sock_cloexec.goに新しく追加されたsysSocketaccept関数にあります。

src/pkg/net/sock_cloexec.go

// +build linux

package net

import "syscall"

// Wrapper around the socket system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
func sysSocket(f, t, p int) (int, error) {
	s, err := syscall.Socket(f, t|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, p)
	// The SOCK_NONBLOCK and SOCK_CLOEXEC flags were introduced in
	// Linux 2.6.27.  If we get an EINVAL error, fall back to
	// using socket without them.
	if err == nil || err != syscall.EINVAL {
		return s, err
	}

	// See ../syscall/exec_unix.go for description of ForkLock.
	syscall.ForkLock.RLock()
	s, err = syscall.Socket(f, t, p)
	if err == nil {
		syscall.CloseOnExec(s)
	}
	syscall.ForkLock.RUnlock()
	if err != nil {
		return -1, err
	}
	if err = syscall.SetNonblock(s, true); err != nil {
		syscall.Close(s)
		return -1, err
	}
	return s, nil
}

// Wrapper around the accept system call that marks the returned file
// descriptor as nonblocking and close-on-exec.
func accept(fd int) (int, syscall.Sockaddr, error) {
	nfd, sa, err := syscall.Accept4(fd, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC)
	// The accept4 system call was introduced in Linux 2.6.28.  If
	// we get an ENOSYS error, fall back to using accept.
	if err == nil || err != syscall.ENOSYS {
		return nfd, sa, err
	}

	// See ../syscall/exec_unix.go for description of ForkLock.
	// It is okay to hold the lock across syscall.Accept
	// because we have put fd.sysfd into non-blocking mode.
	syscall.ForkLock.RLock()
	nfd, sa, err = syscall.Accept(fd)
	if err == nil {
		syscall.CloseOnExec(nfd)
	}
	syscall.ForkLock.RUnlock()
	if err != nil {
		return -1, nil, err
	}
	if err = syscall.SetNonblock(nfd, true); err != nil {
		syscall.Close(nfd)
		return -1, nil, err
	}
	return nfd, sa, nil
}

sysSocket関数:

  1. syscall.Socket(f, t|syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC, p): まず、SOCK_NONBLOCKSOCK_CLOEXECフラグをtype引数にOR結合してsocketシステムコールを呼び出します。これがLinux 2.6.27以降のカーネルでサポートされている高速パスです。
  2. if err == nil || err != syscall.EINVAL: もしエラーがnilであるか、EINVAL(無効な引数)でない場合、高速パスが成功したか、または別の種類のエラーが発生したことを意味します。この場合、結果をそのまま返します。
  3. フォールバックパス: EINVALエラーが発生した場合(カーネルがフラグをサポートしていない場合)、従来の方式にフォールバックします。
    • syscall.ForkLock.RLock(): forkexecの間の競合状態を防ぐために、ForkLockを読み込みロックします。
    • s, err = syscall.Socket(f, t, p): フラグなしでsocketシステムコールを呼び出します。
    • if err == nil { syscall.CloseOnExec(s) }: ソケット作成が成功した場合、Close-on-execフラグを個別に設定します。
    • syscall.ForkLock.RUnlock(): ロックを解放します。
    • if err = syscall.SetNonblock(s, true); err != nil: 最後に、非ブロッキングモードを個別に設定します。エラーが発生した場合はソケットを閉じます。

accept関数:

  1. syscall.Accept4(fd, syscall.SOCK_NONBLOCK|syscall.SOCK_CLOEXEC): まず、SOCK_NONBLOCKSOCK_CLOEXECフラグを渡してaccept4システムコールを呼び出します。これがLinux 2.6.28以降のカーネルでサポートされている高速パスです。
  2. if err == nil || err != syscall.ENOSYS: もしエラーがnilであるか、ENOSYS(システムコールが存在しない)でない場合、高速パスが成功したか、または別の種類のエラーが発生したことを意味します。この場合、結果をそのまま返します。
  3. フォールバックパス: ENOSYSエラーが発生した場合(カーネルがaccept4をサポートしていない場合)、従来の方式にフォールバックします。
    • syscall.ForkLock.RLock(): ForkLockを読み込みロックします。
    • nfd, sa, err = syscall.Accept(fd): 従来のacceptシステムコールを呼び出します。
    • if err == nil { syscall.CloseOnExec(nfd) }: 接続受け入れが成功した場合、Close-on-execフラグを個別に設定します。
    • syscall.ForkLock.RUnlock(): ロックを解放します。
    • if err = syscall.SetNonblock(nfd, true); err != nil: 最後に、非ブロッキングモードを個別に設定します。エラーが発生した場合はソケットを閉じます。

これらの関数は、Goのnetパッケージ内のソケット作成および接続受け入れロジックから呼び出されることで、Linux環境でのソケット操作を透過的に最適化します。

関連リンク

参考にした情報源リンク