最近遇到一个php问题,在32位系统上面上传大文件; 32系统??? 为什么要用32系统?php都放弃32位系统的适配了! 是啊,世事无常,就是这么无奈,一般服务器早就64位了,但还有一些殊的的系统,比如在 arm 架构32系统, 还有些32位嵌入式的系统,有php的需求,32位系统上面 php7 上支持文件size最大是2G,为了能够满足这个需求,看了两天php7源码,然后有些想法,和改动,能够暂时实现部分的大文件上传的接口。
32位系统能够支持大文件的方案大概有两种:
1、自己实现php module,然后完成fread、fwrite、fseek、ftaill、filesize、fstat 这些接口,然后编译成php module;这种方案,
优点是:对php原有代码侵入很少,是松耦合,使用到再进行加载;
缺点是:工作量有些大,只有某些特定场景能用,毕竟用户量太少,感觉工作有些不值;
2、修改php 源码,对read、fwrite、fseek、ftaill、filesize、fstat 接口进行修改,使其能够实现32位系统的适配;
通过看源码,时间关系,我选择了第二种方案,这种方案需要重新编译php 代码,如果你的使用场景无法编译php源码,那大概率只能选择实现方案一,如果你实现请评论区交流哈;
解决方案的步骤:
Step 1: 确认32位系统的glibc是否支持64位接口,比如:fseeko/ftello/fstat/lstat?等,测试代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/sysmacros.h>
int main(int argc, char *argv[])
{
struct stat sb;
if (argc != 2)
{
fprintf(stderr, "Usage: %s <pathname>\n", argv[0]);
exit(EXIT_FAILURE);
}
if (lstat(argv[1], &sb) == -1)
{
perror("lstat");
exit(EXIT_FAILURE);
}
printf("ID of containing device: [%lx,%lx]\n",
(long)major(sb.st_dev), (long)minor(sb.st_dev));
printf("File type: ");
switch (sb.st_mode & S_IFMT)
{
case S_IFBLK:
printf("block device\n");
break;
case S_IFCHR:
printf("character device\n");
break;
case S_IFDIR:
printf("directory\n");
break;
case S_IFIFO:
printf("FIFO/pipe\n");
break;
case S_IFLNK:
printf("symlink\n");
break;
case S_IFREG:
printf("regular file\n");
break;
case S_IFSOCK:
printf("socket\n");
break;
default:
printf("unknown?\n");
break;
}
printf("I-node number: %ld\n", (long)sb.st_ino);
printf("Mode: %lo (octal)\n",
(unsigned long)sb.st_mode);
printf("Link count: %ld\n", (long)sb.st_nlink);
printf("Ownership: UID=%ld GID=%ld\n",
(long)sb.st_uid, (long)sb.st_gid);
printf("Preferred I/O block size: %ld bytes\n",
(long)sb.st_blksize);
printf("File size: %lld bytes\n",
(long long)sb.st_size);
printf("Blocks allocated: %lld\n",
(long long)sb.st_blocks);
printf("Last status change: %s", ctime(&sb.st_ctime));
printf("Last file access: %s", ctime(&sb.st_atime));
printf("Last file modification: %s", ctime(&sb.st_mtime));
exit(EXIT_SUCCESS);
}
注意编译参数:gcc -g -o fstat -D_FILE_OFFSET_BITS=64 fstat.c
关于-D_FILE_OFFSET_BITS 简绍,请参考:
Feature Test Macros (The GNU C Library)
仔细阅读关于:_LARGEFILE_SOURCE?_FILE_OFFSET_BITS 的相关内容, 在php交叉编译也是必须要加的参数,关于php编译参数请参考github 的readme, 关于php大文件请参考:
PHP: Introduction - Manual
然后生成一个大文件比如5G的文件,然后读取大小,看看与实际的size是否相同,如果相同,说明glibc 支持64位相关接口,如果不支持,可能要升级glibc;如果glibc 支持以上
Step 2:?
我用的是php 7.4.28的源码, 大家根据自己的使用的代码:
?git clone -b php-7.4.28 --depth=5? https://github.com/php/php-src
大概理了一下php API 调用的顺序, 以fseek为例:
ext/standard/file/file.c
1. PHPAPI PHP_FUNCTION(fseek)
=====>
main/php_stream.h
2. #define php_stream_seek(stream, offset, whence) _php_stream_seek((stream), (offset), (whence))
=====>
main/streams/streams.c
3. PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence){
...
ret = stream->ops->seek(stream, offset, whence, &stream->position);
...
}
=====>
main/php_stream.h
/* operations on streams that are file-handles */
4. typedef struct _php_stream_ops {
/* stdio like functions - these are mandatory! */
ssize_t (*write)(php_stream *stream, const char *buf, size_t count);
ssize_t (*read)(php_stream *stream, char *buf, size_t count);
int (*close)(php_stream *stream, int close_handle);
int (*flush)(php_stream *stream);
const char *label; /* label for this ops structure */
/* these are optional */
int (*seek)(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset);
int (*cast)(php_stream *stream, int castas, void **ret);
int (*stat)(php_stream *stream, php_stream_statbuf *ssb);
int (*set_option)(php_stream *stream, int option, int value, void *ptrparam);
} php_stream_ops;
=====>
5. ext/standard/file/file.c
PHPAPI PHP_FUNCTION(fseek)
{
zval *res;
zend_long whence = SEEK_SET;
double offset;
php_stream *stream;
ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_RESOURCE(res)
Z_PARAM_DOUBLE(offset)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(whence)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
PHP_STREAM_TO_ZVAL(stream, res);
RETURN_LONG(php_stream_seek(stream, offset, (int) whence));
}
这是调用流程,看了php的代码,还是非常精妙的的,特别是最后调用glibc的函数,用了函数指针,以及PHP_STREAM_TO_ZVAL(stream, res); 中的函数指针的映射;
我们可以发现影响长度的offset定义非常关键,所以看一下,offset的定义:
Zend/zeng_long.h
/* Integer types. */
#ifdef ZEND_ENABLE_ZVAL_LONG64
typedef int64_t zend_long;
typedef uint64_t zend_ulong;
typedef int64_t zend_off_t;
# define ZEND_LONG_MAX INT64_MAX
# define ZEND_LONG_MIN INT64_MIN
# define ZEND_ULONG_MAX UINT64_MAX
# define Z_L(i) INT64_C(i)
# define Z_UL(i) UINT64_C(i)
# define SIZEOF_ZEND_LONG 8
#else
typedef int32_t zend_long;
typedef uint32_t zend_ulong;
typedef int32_t zend_off_t;
# define ZEND_LONG_MAX INT32_MAX
# define ZEND_LONG_MIN INT32_MIN
# define ZEND_ULONG_MAX UINT32_MAX
# define Z_L(i) INT32_C(i)
# define Z_UL(i) UINT32_C(i)
# define SIZEOF_ZEND_LONG 4
#endif
Zend为php语法解释器,在zend_long.h中我们发现了zend_off_t 的定义,在32位系统中定义为4个byte, 在64位系统中定位8个byte;
关键问题就出在这里,4个byte有符号的最大值为:2147483647, 所以当文件大小大于这个值时,都会被强制类型转换导致数据截断,所以关键是调整zend_off_t 定义,调整为int64_t,即可满足fseek的支持单文件,还有几处需要修改成 8字节宽度,有的地方为了改动很少的代码,我巧用了double类型,这样double在32位系统定义为8字节,而且也满足php语法检测,具体patch如下:
diff --git a/Zend/zend_long.h b/Zend/zend_long.h
index 3b651e69..16f36092 100644
--- a/Zend/zend_long.h
+++ b/Zend/zend_long.h
@@ -40,7 +40,7 @@ typedef int64_t zend_off_t;
#else
typedef int32_t zend_long;
typedef uint32_t zend_ulong;
-typedef int32_t zend_off_t;
+typedef int64_t zend_off_t;
# define ZEND_LONG_MAX INT32_MAX
# define ZEND_LONG_MIN INT32_MIN
# define ZEND_ULONG_MAX UINT32_MAX
diff --git a/ext/standard/file.c b/ext/standard/file.c
index 3bd34216..f5105c3a 100644
--- a/ext/standard/file.c
+++ b/ext/standard/file.c
@@ -1265,7 +1265,7 @@ PHPAPI PHP_FUNCTION(rewind)
PHPAPI PHP_FUNCTION(ftell)
{
zval *res;
- zend_long ret;
+ double ret;
php_stream *stream;
ZEND_PARSE_PARAMETERS_START(1, 1)
@@ -1278,7 +1278,7 @@ PHPAPI PHP_FUNCTION(ftell)
if (ret == -1) {
RETURN_FALSE;
}
- RETURN_LONG(ret);
+ RETURN_DOUBLE(ret);
}
/* }}} */
@@ -1287,12 +1287,13 @@ PHPAPI PHP_FUNCTION(ftell)
PHPAPI PHP_FUNCTION(fseek)
{
zval *res;
- zend_long offset, whence = SEEK_SET;
+ zend_long whence = SEEK_SET;
+ double offset;
php_stream *stream;
ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_RESOURCE(res)
- Z_PARAM_LONG(offset)
+ Z_PARAM_DOUBLE(offset)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(whence)
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
@@ -1592,7 +1593,7 @@ PHP_NAMED_FUNCTION(php_if_fstat)
#else
ZVAL_LONG(&stat_rdev, -1);
#endif
- ZVAL_LONG(&stat_size, stat_ssb.sb.st_size);
+ ZVAL_DOUBLE(&stat_size, stat_ssb.sb.st_size);
ZVAL_LONG(&stat_atime, stat_ssb.sb.st_atime);
ZVAL_LONG(&stat_mtime, stat_ssb.sb.st_mtime);
ZVAL_LONG(&stat_ctime, stat_ssb.sb.st_ctime);
diff --git a/ext/standard/filestat.c b/ext/standard/filestat.c
index be6b2dda..2ae1904d 100644
--- a/ext/standard/filestat.c
+++ b/ext/standard/filestat.c
@@ -879,7 +879,7 @@ PHPAPI void php_stat(const char *filename, size_t filename_length, int type, zva
case FS_INODE:
RETURN_LONG((zend_long)ssb.sb.st_ino);
case FS_SIZE:
- RETURN_LONG((zend_long)ssb.sb.st_size);
+ RETURN_DOUBLE((double)ssb.sb.st_size);
case FS_OWNER:
RETURN_LONG((zend_long)ssb.sb.st_uid);
case FS_GROUP:
@@ -936,7 +936,7 @@ PHPAPI void php_stat(const char *filename, size_t filename_length, int type, zva
#else
ZVAL_LONG(&stat_rdev, -1);
#endif
- ZVAL_LONG(&stat_size, stat_sb->st_size);
+ ZVAL_DOUBLE(&stat_size, stat_sb->st_size);
ZVAL_LONG(&stat_atime, stat_sb->st_atime);
ZVAL_LONG(&stat_mtime, stat_sb->st_mtime);
ZVAL_LONG(&stat_ctime, stat_sb->st_ctime);
以上patch,修改了fseek、ftell、fstate、filesize 函数适配了32位系统读取2Gb以上的大文件,但是千万不要忘记在php编译的configure过程中添加参数:
CFLAGS="-D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64”? ?这组编译参数一定加上,否则还是不能读写大文件, 如下图:
以上为我解决32位对php做了一些修改,之前网络上找了好久,没能找到好的解决方法,最后只能自己硬着头皮去看源码,如果有同学有更好的方案,可以评论区分享一下!
遗留问题:
1、由于项目时间的紧迫性,只能做到这里停止了,其实32位的适配做起来还有好多细节,比如copy就没有实现,牵扯到size_t;改动比较大,最后php同学通过调用命令去实现,跟php的copy比起来,性能上多了开销;
2、只做了linux上面的适配,具体windows 32位,没有做相关测试,不知是否能够支持,如果有同学做了相关适配可以评论区提供哈;
|