Skip to content

Commit fa6d97d

Browse files
main/streams/streams: use copy_file_range() on Linux (#8413)
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.
1 parent 4ec92c6 commit fa6d97d

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

configure.ac

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ PHP_CHECK_FUNC(socketpair, socket, network)
364364
PHP_CHECK_FUNC(htonl, socket, network)
365365
PHP_CHECK_FUNC(gethostname, nsl, network)
366366
PHP_CHECK_FUNC(gethostbyaddr, nsl, network)
367+
PHP_CHECK_FUNC(copy_file_range)
367368
PHP_CHECK_FUNC(dlopen, dl, root)
368369
PHP_CHECK_FUNC(dlsym, dl, root)
369370
if test "$ac_cv_func_dlopen" = "yes"; then

ext/standard/tests/file/stream_copy_to_stream.phpt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ stream_filter_append($src, "string.rot13", STREAM_FILTER_READ);
1212

1313
$dest = fopen($new_file, 'w');
1414
var_dump(stream_copy_to_stream($src, $dest, 0));
15+
var_dump(ftell($src));
16+
var_dump(ftell($dest));
1517
fclose($src); fclose($dest);
1618

1719
var_dump(file_get_contents($new_file));
@@ -24,6 +26,8 @@ stream_filter_append($src, "string.rot13", STREAM_FILTER_READ);
2426

2527
$dest = fopen($new_file, 'w');
2628
var_dump(stream_copy_to_stream($src, $dest, -1));
29+
var_dump(ftell($src));
30+
var_dump(ftell($dest));
2731
fclose($src); fclose($dest);
2832
if (WIN) {
2933
var_dump(str_replace("\r\n","\n", file_get_contents($new_file)));
@@ -39,6 +43,8 @@ stream_filter_append($src, "string.rot13", STREAM_FILTER_READ);
3943

4044
$dest = fopen($new_file, 'w');
4145
var_dump(stream_copy_to_stream($src, $dest));
46+
var_dump(ftell($src));
47+
var_dump(ftell($dest));
4248
fclose($src); fclose($dest);
4349

4450
if (WIN) {
@@ -54,6 +60,8 @@ $src = fopen($initial_file, 'r');
5460

5561
$dest = fopen($new_file, 'w');
5662
var_dump(stream_copy_to_stream($src, $dest));
63+
var_dump(ftell($src));
64+
var_dump(ftell($dest));
5765
fclose($src); fclose($dest);
5866

5967
if (WIN) {
@@ -69,6 +77,8 @@ $src = fopen($initial_file, 'r');
6977

7078
$dest = fopen($new_file, 'w');
7179
var_dump(stream_copy_to_stream($src, $dest, 1000000));
80+
var_dump(ftell($src));
81+
var_dump(ftell($dest));
7282
fclose($src); fclose($dest);
7383

7484
if (WIN) {
@@ -85,6 +95,8 @@ $src = fopen($initial_file, 'r');
8595

8696
$dest = fopen($new_file, 'w');
8797
var_dump(stream_copy_to_stream($src, $dest, 10));
98+
var_dump(ftell($src));
99+
var_dump(ftell($dest));
88100
fclose($src); fclose($dest);
89101

90102
if (WIN) {
@@ -100,6 +112,8 @@ $src = fopen($initial_file, 'r');
100112

101113
$dest = fopen($new_file, 'w');
102114
var_dump(stream_copy_to_stream($src, $dest, -1));
115+
var_dump(ftell($src));
116+
var_dump(ftell($dest));
103117
fclose($src); fclose($dest);
104118

105119
if (WIN) {
@@ -113,38 +127,52 @@ echo "Done\n";
113127
?>
114128
--EXPECTF--
115129
int(0)
130+
int(0)
131+
int(0)
116132
string(0) ""
117133
int(%d)
134+
int(134)
135+
int(134)
118136
string(134) "Nabgure qnl
119137
Jura gur cnvaf bs yvsr jba'g one zl jnl
120138
V'yy oernx gurfr punvaf
121139
Gung ubyq zr qbja
122140
V'yy grne lbh qbja vagb zl cevingr uryy
123141
"
124142
int(%d)
143+
int(134)
144+
int(134)
125145
string(134) "Nabgure qnl
126146
Jura gur cnvaf bs yvsr jba'g one zl jnl
127147
V'yy oernx gurfr punvaf
128148
Gung ubyq zr qbja
129149
V'yy grne lbh qbja vagb zl cevingr uryy
130150
"
131151
int(%d)
152+
int(134)
153+
int(134)
132154
string(134) "Another day
133155
When the pains of life won't bar my way
134156
I'll break these chains
135157
That hold me down
136158
I'll tear you down into my private hell
137159
"
138160
int(%d)
161+
int(134)
162+
int(134)
139163
string(134) "Another day
140164
When the pains of life won't bar my way
141165
I'll break these chains
142166
That hold me down
143167
I'll tear you down into my private hell
144168
"
145169
int(%d)
170+
int(10)
171+
int(10)
146172
string(10) "Another da"
147173
int(%d)
174+
int(134)
175+
int(134)
148176
string(134) "Another day
149177
When the pains of life won't bar my way
150178
I'll break these chains
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
stream_copy_to_stream() tests with interleaved read/write calls
3+
--FILE--
4+
<?php
5+
define('WIN', substr(PHP_OS, 0, 3) == 'WIN');
6+
7+
$initial_file = __DIR__.'/bug38086.txt';
8+
$new_file = __DIR__.'/stream_copy_to_stream_interleaved.txt';
9+
10+
$src = fopen($initial_file, 'r');
11+
12+
$dest = fopen($new_file, 'w');
13+
14+
var_dump(fread($src, 10));
15+
var_dump(fwrite($dest, "foo"));
16+
var_dump(stream_copy_to_stream($src, $dest, 10));
17+
var_dump(ftell($src));
18+
var_dump(ftell($dest));
19+
var_dump(fread($src, 10));
20+
var_dump(fwrite($dest, "bar"));
21+
var_dump(stream_copy_to_stream($src, $dest, 10));
22+
var_dump(ftell($src));
23+
var_dump(ftell($dest));
24+
fclose($src); fclose($dest);
25+
26+
if (WIN) {
27+
var_dump(str_replace("\r\n","\n", file_get_contents($new_file)));
28+
} else {
29+
var_dump(file_get_contents($new_file));
30+
}
31+
unlink($new_file);
32+
33+
echo "Done\n";
34+
?>
35+
--EXPECTF--
36+
string(10) "Another da"
37+
int(3)
38+
int(10)
39+
int(20)
40+
int(13)
41+
string(10) " pains of "
42+
int(3)
43+
int(10)
44+
int(40)
45+
int(26)
46+
string(26) "fooy
47+
When thebarlife won't"
48+
Done

main/streams/streams.c

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,90 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de
15551555
return SUCCESS;
15561556
}
15571557

1558+
#ifdef HAVE_COPY_FILE_RANGE
1559+
1560+
/* TODO: on FreeBSD, copy_file_range() works only with the
1561+
undocumented flag 0x01000000; until the problem is fixed
1562+
properly, copy_file_range() is not used on FreeBSD */
1563+
#ifndef __FreeBSD__
1564+
if (php_stream_is(src, PHP_STREAM_IS_STDIO) &&
1565+
php_stream_is(dest, PHP_STREAM_IS_STDIO) &&
1566+
src->writepos == src->readpos &&
1567+
php_stream_can_cast(src, PHP_STREAM_AS_FD) == SUCCESS &&
1568+
php_stream_can_cast(dest, PHP_STREAM_AS_FD) == SUCCESS) {
1569+
/* both php_stream instances are backed by a file
1570+
descriptor, are not filtered and the read buffer is
1571+
empty: we can use copy_file_range() */
1572+
1573+
int src_fd, dest_fd;
1574+
1575+
php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0);
1576+
php_stream_cast(dest, PHP_STREAM_AS_FD, (void*)&dest_fd, 0);
1577+
1578+
/* clamp to INT_MAX to avoid EOVERFLOW */
1579+
const size_t cfr_max = MIN(maxlen, (size_t)SSIZE_MAX);
1580+
1581+
/* copy_file_range() is a Linux-specific system call
1582+
which allows efficient copying between two file
1583+
descriptors, eliminating the need to transfer data
1584+
from the kernel to userspace and back. For
1585+
networking file systems like NFS and Ceph, it even
1586+
eliminates copying data to the client, and local
1587+
filesystems like Btrfs and XFS can create shared
1588+
extents. */
1589+
1590+
ssize_t result = copy_file_range(src_fd, NULL,
1591+
dest_fd, NULL,
1592+
cfr_max, 0);
1593+
if (result > 0) {
1594+
size_t nbytes = (size_t)result;
1595+
haveread += nbytes;
1596+
1597+
src->position += nbytes;
1598+
dest->position += nbytes;
1599+
1600+
if ((maxlen != PHP_STREAM_COPY_ALL && nbytes == maxlen) ||
1601+
php_stream_eof(src)) {
1602+
/* the whole request was satisfied or
1603+
end-of-file reached - done */
1604+
*len = haveread;
1605+
return SUCCESS;
1606+
}
1607+
1608+
/* there may be more data; continue copying
1609+
using the fallback code below */
1610+
} else if (result == 0) {
1611+
/* end of file */
1612+
*len = haveread;
1613+
return SUCCESS;
1614+
} else if (result < 0) {
1615+
switch (errno) {
1616+
case EINVAL:
1617+
/* some formal error, e.g. overlapping
1618+
file ranges */
1619+
break;
1620+
1621+
case EXDEV:
1622+
/* pre Linux 5.3 error */
1623+
break;
1624+
1625+
case ENOSYS:
1626+
/* not implemented by this Linux kernel */
1627+
break;
1628+
1629+
default:
1630+
/* unexpected I/O error - give up, no
1631+
fallback */
1632+
*len = haveread;
1633+
return FAILURE;
1634+
}
1635+
1636+
/* fall back to classic copying */
1637+
}
1638+
}
1639+
#endif // __FreeBSD__
1640+
#endif // HAVE_COPY_FILE_RANGE
1641+
15581642
if (maxlen == PHP_STREAM_COPY_ALL) {
15591643
maxlen = 0;
15601644
}

0 commit comments

Comments
 (0)