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

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

このコミットは、Go言語のリンカ (cmd/ld) におけるファイル書き込み処理の堅牢性を向上させるものです。具体的には、出力ファイルへの書き込みが部分的にしか成功しなかった場合(ショートライト)に、エラーの詳細を正確に取得し、適切に処理するためのリトライロジックを導入しています。

コミット

commit 592b480746bd2ac2140980d298a50563d484733d
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jan 31 07:49:33 2013 -0800

    cmd/ld: retry short writes, to get error detail
    
    Fixes #3802.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7228066

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

https://github.com/golang/go/commit/592b480746bd2ac2140980d298a50563d484733d

元コミット内容

cmd/ld: retry short writes, to get error detail

Fixes #3802.

変更の背景

この変更は、Go言語のリンカ (cmd/ld) が出力ファイルを書き込む際に発生する可能性のある「ショートライト(short write)」問題に対処するために行われました。ショートライトとは、write() システムコールが要求されたバイト数よりも少ないバイト数を書き込んで成功を返す現象を指します。これは、ディスク容量の不足、ファイルシステムの制限、ネットワークファイルシステムの問題、またはその他のI/Oエラーなど、様々な理由で発生する可能性があります。

従来のリンカのコードでは、write() の戻り値が要求されたバイト数と一致しない場合、単にエラーとして処理されていましたが、そのエラーの詳細が十分に捕捉されていませんでした。Issue #3802("cmd/ld: write error: %r")では、このようなショートライトが発生した際に、具体的なエラーメッセージが不足していることが指摘されていました。特に、%r フォーマット指定子(Goの内部的なエラー報告メカニズム)が期待通りに機能せず、一般的な「write error」しか表示されない問題がありました。

このコミットの目的は、ショートライトが発生した場合に、write() をリトライすることで、最終的に完全な書き込みが成功するか、あるいは明確なエラー(例えば、ディスクフルなど)が発生するまで試行を続けることです。これにより、エラーが発生した際には、より詳細なエラー情報がシステムから取得され、ユーザーに報告されるようになります。これは、デバッグや問題解決の際に非常に重要です。

前提知識の解説

Go言語のリンカ (cmd/ld)

Go言語のコンパイルプロセスは、ソースコードから実行可能ファイルを生成する際に、複数のステージを経ます。cmd/ld はGoのリンカであり、コンパイラによって生成されたオブジェクトファイル(.o ファイル)やアーカイブファイル(.a ファイル)を結合し、最終的な実行可能バイナリを生成する役割を担います。リンカは、シンボル解決、再配置、セクションの結合など、低レベルな処理を行います。

write() システムコールとショートライト

write() は、Unix系システムにおける基本的なシステムコールの一つで、ファイルディスクリプタにデータを書き込むために使用されます。そのシグネチャは通常 ssize_t write(int fd, const void *buf, size_t count); のようになります。

  • fd: 書き込み先のファイルディスクリプタ。
  • buf: 書き込むデータが格納されているバッファへのポインタ。
  • count: 書き込みを要求するバイト数。

write() は、成功した場合に実際に書き込まれたバイト数を返します。この戻り値が count と等しくない場合、それは「ショートライト」と呼ばれます。ショートライトはエラーではありませんが、要求されたデータがすべて書き込まれていないことを意味します。通常、アプリケーションはショートライトを検知した場合、残りのデータを書き込むために write() をリトライする必要があります。戻り値が -1 の場合はエラーが発生しており、errno 変数にエラーコードが設定されます。

diag()errorexit()

Goのリンカの内部コードでは、エラー報告のために diag()errorexit() といった関数が使用されています。

  • diag(): エラーメッセージを出力するための関数。C言語の printf のようなフォーマット指定子をサポートしている可能性があります。
  • errorexit(): プログラムを終了させるための関数。通常、致命的なエラーが発生した場合に呼び出されます。

%r フォーマット指定子

Goの内部的なエラー報告メカニックにおいて、%r はシステムコールによって設定された最後のエラー(errno)に対応するエラー文字列を表示するために使用されることがあります。これは、C言語の strerror(errno) に相当する機能を提供し、より人間が読める形式でエラーの原因を特定するのに役立ちます。

技術的詳細

このコミットの主要な変更点は、ファイルへの書き込み処理をラップする新しいヘルパー関数 dowrite の導入です。

dowrite 関数の実装

static void
dowrite(int fd, char *p, int n)
{
	int m;
	
	while(n > 0) {
		m = write(fd, p, n);
		if(m <= 0) {
			cursym = S; // Sはシンボル関連のグローバル変数かマクロと推測される
			diag("write error: %r");
			errorexit();
		}
		n -= m;
		p += m;
	}
}

この dowrite 関数は以下のロジックで動作します。

  1. ループ処理: while(n > 0) ループは、書き込むべきバイト数 n がゼロになるまで、つまりすべてのデータが書き込まれるまで処理を続行します。
  2. write() 呼び出し: m = write(fd, p, n); で、指定されたファイルディスクリプタ fd に、バッファ p から最大 n バイトのデータを書き込もうとします。
  3. エラーチェック: if(m <= 0) は、write() がエラーを返した(m == -1)か、または何も書き込まなかった(m == 0、これは通常、ファイルの終端に達した場合や、非ブロッキングI/Oでデータが利用可能でない場合に発生するが、ここではエラーとして扱われる)場合に真となります。
    • この場合、diag("write error: %r"); を呼び出してエラーメッセージを出力し、errorexit(); でプログラムを終了させます。ここで %r が使用されることで、システムコールからの具体的なエラー情報が取得されることが期待されます。
  4. ショートライトの処理: m > 0 の場合、write() は少なくとも1バイトを書き込んだことを意味します。
    • n -= m;: 残りの書き込みバイト数を更新します。
    • p += m;: バッファポインタを、書き込まれたバイト数だけ進めます。これにより、次のループイテレーションでは、まだ書き込まれていないデータの先頭から書き込みが再開されます。

このループにより、write() がショートライトを返しても、残りのデータがすべて書き込まれるまでリトライが繰り返されます。これにより、一時的な問題や部分的な書き込みが原因で発生するエラーが適切に処理され、最終的なエラーが発生した場合には、より正確なエラーメッセージが報告されるようになります。

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

変更は src/cmd/ld/lib.c ファイルに集中しています。

--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -1447,6 +1447,23 @@ Yconv(Fmt *fp)
 
 vlong coutpos;
 
+static void
+dowrite(int fd, char *p, int n)
+{
+	int m;
+	
+	while(n > 0) {
+		m = write(fd, p, n);
+		if(m <= 0) {
+			cursym = S;
+			diag("write error: %r");
+			errorexit();
+		}
+		n -= m;
+		p += m;
+	}
+}
+
 void
 cflush(void)
 {
@@ -1455,13 +1472,8 @@ cflush(void)
 	if(cbpmax < cbp)
 		cbpmax = cbp;
 	n = cbpmax - buf.cbuf;
-	if(n) {
-		if(write(cout, buf.cbuf, n) != n) {
-			diag("write error: %r");
-			errorexit();
-		}
-		coutpos += n;
-	}
+	dowrite(cout, buf.cbuf, n);
+	coutpos += n;
 	cbp = buf.cbuf;
 	cbc = sizeof(buf.cbuf);
 	cbpmax = cbp;
@@ -1502,10 +1514,7 @@ cwrite(void *buf, int n)
 	cflush();
 	if(n <= 0)
 		return;
-	if(write(cout, buf, n) != n) {
-		diag("write error: %r");
-		errorexit();
-	}
+	dowrite(cout, buf, n);
 	coutpos += n;
 }
 

コアとなるコードの解説

dowrite 関数の追加

上記の「技術的詳細」セクションで説明した dowrite 関数が新しく追加されています。この関数は、ファイルディスクリプタ fd、書き込むデータへのポインタ p、書き込むバイト数 n を引数に取り、すべてのデータが書き込まれるまで write() システムコールをリトライします。エラーが発生した場合は diagerrorexit を呼び出します。

cflush 関数の変更

cflush 関数は、内部バッファ buf.cbuf の内容をファイルディスクリプタ cout にフラッシュする役割を担っています。 変更前は、write(cout, buf.cbuf, n) != n という形で、一度の write 呼び出しで全データが書き込まれたかどうかをチェックし、そうでなければエラーとしていました。 変更後は、新しく追加された dowrite(cout, buf.cbuf, n); を呼び出すように変更されています。これにより、cflushdowrite のリトライロジックの恩恵を受け、より堅牢な書き込み処理を行うようになります。

cwrite 関数の変更

cwrite 関数は、任意のバッファ buf から n バイトのデータをファイルディスクリプタ cout に書き込む役割を担っています。 cflush と同様に、変更前は write(cout, buf, n) != n という形で一度の write 呼び出しの成否をチェックしていました。 変更後は、dowrite(cout, buf, n); を呼び出すように変更されています。これにより、cwrite もまた dowrite のリトライロジックを利用し、部分的な書き込みが発生した場合でも適切に処理されるようになります。

これらの変更により、Goリンカのファイル書き込み処理全体が、ショートライトに対してより耐性を持つようになり、エラー発生時にはより詳細な情報を提供するようになりました。

関連リンク

参考にした情報源リンク

  • Go Issue #3802 (変更の背景と問題の詳細を理解するために参照)
  • Go CL 7228066 (コミットの具体的な変更内容とレビューコメントを理解するために参照)
  • Unix write() システムコールに関する一般的なドキュメント (ショートライトの概念と処理方法を理解するために参照)
  • Go言語のコンパイルプロセスとリンカに関する一般的な情報 (Goリンカの役割を理解するために参照)
  • C言語におけるエラーハンドリングと errno の使用に関する情報 ( %r フォーマット指定子の挙動を理解するために参照)