Amazon Linux 2(Kernel 5.10)でPHP 8.4をコンパイルする際のundefined reference to `copy_file_range'の解決方法

2025年05月06日

Amazon Linux 2のサポート期限が2026年06月30日まで延長されましたね!
まだまだ現役だってことでPHP 8.4の環境を作ろうとしたんですが、コンパイルできずにハマったので対処法を書き記しておきます。

遭遇したエラー

以前書いた「複数バージョンのPHPを共存させる方法(Part2)」と同じ感じでAmazon Linux 2上で複数バージョンのPHPを動かしていて、
PHP 8.4で動かしたい案件が出てきたのでPHP 8.4.6をコンパイルすることにしました。
コンパイルに必要なソフトを揃えつつconfigure → makeと進んでいくと、以下のようなエラーが出てしまいました。

# make
・・・略・・・
main/streams/streams.o: In function `_php_stream_copy_to_stream_ex':
/usr/local/src/php-8.4.6/main/streams/streams.c:1648: undefined reference to `copy_file_range'
collect2: error: ld returned 1 exit status
make: *** [sapi/cli/php] Error 1

「copy_file_range」って関数が見つからないよってエラーですね。
これですね、いろいろ試してみると

Amazon Linux 2(Kernel 4.14) OK
Amazon Linux 2(Kernel 5.10) NG
Amazon Linux 2023(Kernel 6.1) OK

という感じで一部の環境でのみ起こる現象でした。

解決方法

PHPのソースフォルダ直下にあるconfigureファイルの17254行目を以下の様に編集してください。

printf "%s\n" "#define HAVE_COPY_FILE_RANGE 1" >>confdefs.h
↓
# printf "%s\n" "#define HAVE_COPY_FILE_RANGE 1" >>confdefs.h

行全体をコメントアウトしてあげる感じですね。
viエディタで編集する場合は「17254」と打った後に大文字で「G」と入力するとその行番号まで飛べます。

編集したらもう一度configureし直して、make → make installと進んでください。
今度はmakeがうまく行くかと思います。

なぜ一部の環境でエラーが起こるのか?

この謎を解くにはPHPのソースを見ていく必要があります。
実際にエラーが起こっているPHP 8.4.6の「/main/streams/streams.c」の1648行目を見にいってみましょう。


PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *dest, size_t maxlen, size_t *len STREAMS_DC)
{
	char buf[CHUNK_SIZE];
	size_t haveread = 0;
	size_t towrite;
	size_t dummy;

	if (!len) {
		len = &dummy;
	}

	if (maxlen == 0) {
		*len = 0;
		return SUCCESS;
	}

#ifdef HAVE_COPY_FILE_RANGE
	if (php_stream_is(src, PHP_STREAM_IS_STDIO) &&
			php_stream_is(dest, PHP_STREAM_IS_STDIO) &&
			src->writepos == src->readpos) {
		/* both php_stream instances are backed by a file descriptor, are not filtered and the
		 * read buffer is empty: we can use copy_file_range() */
		int src_fd, dest_fd, dest_open_flags = 0;

		/* copy_file_range does not work with O_APPEND */
		if (php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0) == SUCCESS &&
				php_stream_cast(dest, PHP_STREAM_AS_FD, (void*)&dest_fd, 0) == SUCCESS &&
				/* get dest open flags to check if the stream is open in append mode */
				php_stream_parse_fopen_modes(dest->mode, &dest_open_flags) == SUCCESS &&
				!(dest_open_flags & O_APPEND)) {

			/* clamp to INT_MAX to avoid EOVERFLOW */
			const size_t cfr_max = MIN(maxlen, (size_t)SSIZE_MAX);

			/* copy_file_range() is a Linux-specific system call which allows efficient copying
			 * between two file descriptors, eliminating the need to transfer data from the kernel
			 * to userspace and back. For networking file systems like NFS and Ceph, it even
			 * eliminates copying data to the client, and local filesystems like Btrfs and XFS can
			 * create shared extents. */
			ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, cfr_max, 0);  // ←エラーが起こっている1648行目
			if (result > 0) {
// ・・・略・・・
			}
		}
	}
#endif // HAVE_COPY_FILE_RANGE
// ・・・略・・・
}

PHPのstream_copy_to_stream関数の実装箇所ですね。
赤文字のところが問題の1648行目です。

1648行目の手前の英文をGoogle翻訳にかけるとこんな感じです。

copy_file_range() は Linux 固有のシステムコールで、2 つのファイル記述子間の効率的なコピーを可能にし、
カーネルからユーザー空間へのデータ転送を不要にします。
NFS や Ceph などのネットワークファイルシステムでは、クライアントへのデータコピーも不要になり、
Btrfs や XFS などのローカルファイルシステムでは共有エクステントを作成できます。

要はこのLinux側が用意してくれている関数を使うと効率よくコピーできるよって事みたいですね。
そしてエラーが起こったり起こらなかったりする原因の一つが青文字のifdefでの条件分岐です。
C言語のソースをコンパイルする時に
「この条件の場合にこの部分をコンパイル対象に含める」
というようなことができるプリプロセッサという仕組みがあって、
通常のif文がプログラムの実行時に都度条件分岐をするのに対して、
プリプロセッサを使うとコンパイル時に条件分岐させてプログラム自体を場合分けさせることができます。

#ifdef HAVE_COPY_FILE_RANGE」は「HAVE_COPY_FILE_RANGE」という名前の定数のようなものが定義してあったら
#ifdef~#endifまでの間のソースをコンパイル対象にするよ、というものですね。
PHPだと

define("XXX", true);

if (defined("XXX")) {
    echo "ある場合";
}

というような処理を実行時にやれますが、コンパイル時にif文を動かしちゃうって感じですね。

そしてこの「HAVE_COPY_FILE_RANGE」を定義しているのが解決方法のところで書いたconfigureファイルの17254行目です。
該当箇所を抜粋するとこのような感じです。

#ifndef __linux__
# error "unsupported platform"
#endif
#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif
#include <linux/version.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,3,0)
# error "kernel too old"
#endif
#include <unistd.h>
int main(void)
{
(void)copy_file_range(-1, 0, -1, 0, 0, 0);
return 0;
}

_ACEOF
if ac_fn_c_try_compile "$LINENO"
then :
php_cv_func_copy_file_range=yes
else case e in #(
e) php_cv_func_copy_file_range=no ;;
esac
fi
rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
;;
esac
fi
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $php_cv_func_copy_file_range" >&5
printf "%s\n" "$php_cv_func_copy_file_range" >&6; }
if test "x$php_cv_func_copy_file_range" = xyes
then :

printf "%s\n" "#define HAVE_COPY_FILE_RANGE 1" >>confdefs.h

fi

赤文字の行がコメントアウトした行です。
「copy_file_range関数が使える環境か?」という判定を行い、使える環境だったら「HAVE_COPY_FILE_RANGE」を定義し、
C言語のソースの方でcopy_file_range関数を使った処理をコンパイル対象にしていたって感じですね。

そして青文字の「#if LINUX_VERSION_CODE < KERNEL_VERSION(5,3,0)」という判定が環境によってエラーが起こっていた原因のうちのひとつです!
Linux Kernelのバージョンが5.3.0よりも低いかどうかチェックし、低ければ「使えない」と判定しています。

https://man7.org/linux/man-pages/man2/copy_file_range.2.html
を見ると、

■原文

VERSIONS
A major rework of the kernel implementation occurred in Linux 5.3.
Areas of the API that weren't clearly defined were clarified and
the API bounds are much more strictly checked than on earlier
kernels.

HISTORY
Linux 4.5, but glibc 2.27 provides a user-space emulation when it
is not available.

■Google翻訳

VERSIONS
Linux 5.3 ではカーネル実装の大幅な見直しが行われました。
API の明確に定義されていなかった領域が明確化され、
API の境界は以前のカーネルよりもはるかに厳密にチェックされるようになりました。

HISTORY
Linux 4.5 ですが、glibc 2.27 では、それが利用できない場合にユーザー空間エミュレーションが提供されます。

という記述がありました。
あんま詳しくないのでだいぶ意訳込みですが、
Linux Kernelが5.3.0よりも古い時代からcopy_file_range関数が存在していて、
環境によっては正式なcopy_file_range関数が実装されてるわけじゃないけど、同じ挙動を再現するようにしていました。
(ブラウザのIEとかChrome、FirefoxとかみたいにHTMLを表示するソフトごとにちょいちょい違いがあった・・・的な感じ。)
OSが正式に提供する機能がコレだよってリストをLinux Kernel 5.3のタイミングで整理したから、5.3以降ならcopy_file_range関数をジャンジャン使ってくださいね!
・・・って感じでしょうか?

このLinux公式の案内に乗っかってPHP側で「#if LINUX_VERSION_CODE < KERNEL_VERSION(5,3,0)」という条件分岐が生まれたということかと思います。
ただ、これだけだとコンパイルエラーになる原因とは言えませんよね。
5.3からcopy_file_range関数が正式に使えるようになったのに、5.10の環境で「見つからない」ってどういうこっちゃ?って感じですよね!
実はもうひとつの原因があるのです!

もう一つの原因

PHPのコンパイル時には「copy_file_range関数が見つからない」とエラーってしまいましたが、
本当に使えないのか確かめてみることにしました。

エラーが起こったAmazon Linux 2(Kernel 5.10)の環境でmanコマンドで調べてみました

# man copy_file_range
・・・略・・・
EXAMPLE
    #define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>

static loff_t
copy_file_range(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags)
{
return syscall(__NR_copy_file_range, fd_in, off_in, fd_out,
off_out, len, flags);
}

int
main(int argc, char **argv)
{
・・・略・・・
}

こんな感じに使えるよっていう説明がでてきたのでちゃんと使えそうですね。
試しにcopy_file_range関数を使ったC言語のソースを書いてみます。

■ソース(check.c)

manコマンドのサンプルとPHPのconfigureに書いてあったcopy_file_range関数が使えるかのチェック処理を合体させた感じです。

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
static loff_t copy_file_range(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags) { return syscall(__NR_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags); } int main() { printf("CHECK\r\n"); (void)copy_file_range(-1, 0, -1, 0, 0, 0); printf("OK\r\n"); return 0; }

■コンパイル

# gcc -o check check.c

■実行結果

# ./check
CHECK
OK

問題なくKernel 5.10の環境で使えそうですね。

PHPをコンパイルする際にcopy_file_range関数が見つからないという事は、ヘッダーのインクルードや関数のプロトタイプ宣言が足りないってことでしょうから、
試しにPHPのソースの「/main/streams/streams.c」の33行目の後に以下の記述を足してみます。

#include <fcntl.h>
#include "php_streams_int.h"

/* 追加 ~~~ここから~~~ */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
static loff_t copy_file_range(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags)
{
return syscall(__NR_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags);
}
/* 追加 ~~~ここまで~~~ */

/* {{{ resource and registration code */
/* Global wrapper hash, copied to FG(stream_wrappers) on registration of volatile wrapper */

この状態で再度makeをしてみると・・・なんと・・・いけました!
さらにmake installもうまく行き、

# /usr/local/lib/php8.4/bin/php -v
PHP 8.4.6 (cli) (built: May 6 2025 08:52:33) (ZTS)
Copyright (c) The PHP Group
Zend Engine v4.4.6, Copyright (c) Zend Technologies

# /usr/local/lib/php8.4/bin/php -r 'echo phpversion() . "\r\n";'
8.4.6

という感じでちゃんとPHP 8.4を動かすことができました!

ただ、この記述って必ず必要なものなのか?ってのが気になってきますよね。
という事でAmazon Linux 2023(Kernel 6.1)の環境でPHPのソースに手を加えずにコンパイルしてみると、なんとうまく行ったんですよね!
別にAmazon Linux 2023環境でcopy_file_range関数が使われていないというわけではありません。

configureした際に表示されるメッセージで

checking for nanosleep... yes
checking for getaddrinfo... yes
checking for copy_file_range... yes
checking for strlcat... no
checking for strlcpy... no

の赤文字の部分が「yes」になっていれば使われています。
Amazon Linux 2(Kernel 4.14)環境では「no」になっており、
Amazon Linux 2(Kernel 5.10)とAmazon Linux 2023(Kernel 6.1)では「yes」になっていました。
これはいったいどういう事なんだということでもう一度
https://man7.org/linux/man-pages/man2/copy_file_range.2.html
の「VERSIONS」セクションを見てみましょう。

■原文

VERSIONS
    A major rework of the kernel implementation occurred in Linux 5.3.
    Areas of the API that weren't clearly defined were clarified and
    the API bounds are much more strictly checked than on earlier
    kernels.

    Since Linux 5.19, cross-filesystem copies can be achieved when
    both filesystems are of the same type, and that filesystem
    implements support for it. See BUGS for behavior prior to Linux
    5.19.

    Applications should target the behaviour and requirements of Linux
    5.19, that was also backported to earlier stable kernels.

なんかちょっと気になる文章がありますね。
青文字のところを翻訳してみます。

■青文字部分をGoogle翻訳

Linux 5.19 以降では、両方のファイルシステムが同じタイプで、
そのファイルシステムがサポートを実装している場合、
ファイルシステム間のコピーが可能です。
Linux 5.19 より前の動作については、バグを参照してください。

アプリケーションは、以前の安定カーネルにもバックポートされている Linux 5.19 の動作と要件をターゲットにする必要があります。


おお、これはなんかアレですね、5.19より前にバグがあるってことは、5.10が関係してそうですね。
バグのところを見に行ってみましょう。

■原文

BUGS
   In Linux 5.3 to Linux 5.18, cross-filesystem copies were
   implemented by the kernel, if the operation was not supported by
   individual filesystems.  However, on some virtual filesystems, the
   call failed to copy, while still reporting success.

■Google翻訳

バグ
    Linux 5.3 から Linux 5.18 では、個々のファイルシステムでサポートされていない場合、
    ファイルシステム間のコピーはカーネルによって実装されていました。
    しかし、一部の仮想ファイルシステムでは、コピーは成功と報告されるものの、
    呼び出しは失敗していました。

ん~ちょっと予想と違いましたね。
一部の環境ではcopy_file_range関数がうまく処理できてなくても成功したよって返してくるって事かと思いますが、
今回の場合はコンパイルに失敗してるのでそもそも呼び出すところまで行ってないんですよね。
なのでこのバグの説明が今回の現象とは一致してないんですが、一部のバージョンでうまく呼び出せてない、参照できていないっていう状況は似てるんですよね。

また、VERSIONSセクションに
アプリケーションは、以前の安定カーネルにもバックポートされている Linux 5.19 の動作と要件をターゲットにする必要があります。
というように5.19以降を対象にしてねとあるので、
PHPのソース上に実装されている「5.3.0」判定は「5.19.0」で判定した方が良い様な・・・?って感じですね。
ここら辺はちょっと事情に詳しくないので何ともですね。

これ以上は調べても情報を見つけられなかったのでちょっとモヤモヤっとしますが、これで調査は終了です。

まとめ

  • PHP 8.4.6のソースに、Linux Kernelが5.3.0以降の場合にcopy_file_range関数を使うようにする条件分岐がある。

  • Linux Kernelの情報を見ると5.3から5.18の間にcopy_file_range関数の呼び出しに問題があったらしい。
    • ※今回の現象と状況が似てるっちゃ似てるけど、完全には一致していないので微妙なところ。
      • でも個人的にはこれがアタリじゃないかなぁと思うところですね。
  • PHPのソースにcopy_file_range関数への参照(関数のプロトタイプ宣言)を足せばコンパイルできるようになるが、
    copy_file_range関数が使えるようになったとしても多少パフォーマンスが上がるくらいなので
    configureを書き換えてLinux Kernelが4.14の環境と同様に使わないようにした方が安全だと思われる。

という感じですね!

さっさとAmazon Linux 2023に移行してれば遭遇せずに済みましたが、
Amazon Linux 2の開発用サーバーで動かしてる案件(PHP 5.3案件含む・・・)が結構たくさんあってちょっとしんどいのと、
Amazon Linux 2のサポート期間が伸びたのでもうちょっとAmazon Linux 2クンには頑張ってもらっちゃおうかなぁと思います!
もうちょっと待てばAmazon Linux 2026とか出るかもしれませんしね!

Forever Amazon Linux 2!
書いた人:木本
コメント一覧
コメントはまだありません。
コメントを投稿する
お名前
E-Mail
[必須]コメント
Top