热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

OutofMemory(OOM)处理流程

  目前设备后台打印出如上log,然后串口ssh等都不能登录,设备死机,必须要断电重启才行!然而一开始设计是出现内存不足后,会首先kill掉比较耗费内存的进程,确保设备部挂

 

   目前设备后台打印出如上log, 然后串口 ssh等都不能登录,设备死机,必须要断电重启才行!

然而一开始设计是出现内存不足后,会首先kill 掉比较耗费内存的进程,确保设备部挂机。但是此时好像有点不一样了。所以来看看oom的内核代码,看下应该怎样处理。


为什么会有OOM killer

  当我们使用应用时,需要申请内存,即进行 malloc 的操作,进行 malloc 操作如果返回一个 非NULL 的操作表示申请到了可用的内存。事实上,这个地方是可能存在问题的。

  当我们在用户空间申请内存时,一般使用 malloc ,是不是当 malloc 返回为空时,没有可以申请的内存空间就会返回呢?答案是 否定 的。在 malloc 申请内存的机制中有如下一段描述

By default, Linux follows an optimistic memory allocation strategy.
This means that when
malloc() returns non-NULL there is no guarantee
that the memory really
is available. This is a really bad bug. In
case it turns out that the system is out of memory, one or more processes
will be killed by the infamous OOM killer. In
case Linux is employed
under circumstances
where it would be less desirable to suddenly lose
some randomly picked processes, and moreover the kernel version
is
sufficiently recent, one can
switch off this overcommitting behavior
using a command like:
# echo
2 > /proc/sys/vm/overcommit_memory
See also the kernel Documentation directory, files vm
/overcommit-accounting
and sysctl
/vm.txt.

  描述中说明了在Linux中当malloc返回的是非空时,并不代表有可以使用的内存空间。Linux系统允许程序申请比系统可用内存更多的内存空间,这个特性叫做 overcommit 特性,这样做可能是为了系统的优化,因为不是所有的程序申请了内存就会立刻使用,当真正的使用时,系统可能已经回收了一些内存。但是,当你使用时Linux系统没有内存可以使用时,OOM Killer就会出来让一些进程退出。

Linux下有3种Overcommit的策略(参考内核文档: Documentation/vm/overcommit-accounting ),可以在 /proc/sys/vm/overcommit_memory 配置(可以取0,1和2三个值,默认是0)。



  • 0: 启发式策略 ,比较严重的Overcommit将不能得逞,比如你突然申请了128TB的内存。而轻微的overcommit将被允许。另外,root能Overcommit的值比普通用户要稍微多。

  • 1: 永远允许overcommit ,这种策略适合那些不能承受内存分配失败的应用,比如某些科学计算应用。

  • 2: 永远禁止overcommit ,在这个情况下,系统所能分配的内存不会超过 swap+RAM*系数 (/proc/sys/vm/overcmmit_ratio,默认50%,你可以调整),如果这么多资源已经用光,那么后面任何尝试申请内存的行为都会返回错误,这通常意味着此时没法运行任何新程序。


 如何保护一个进程不被OOM killer杀掉呢?

我们可以修改 /proc/

/oom_adj 的值,这里的默认值为0,当我们设置为-17时,对于该进程来说,就不会触发OOM机制,被杀掉:

echo -17 > /proc/$(pidof debugbin)/oom_adj

这里为什么是-17呢?这和Linux的实现有关系。在Linux内核中的oom.h文件中,可以看到下面的定义:

/* /proc/

/oom_adj set to -17 protects from the oom-killer */
#define OOM_DISABLE (-17)
/* inclusive */
#define OOM_ADJUST_MIN (-16)
#define OOM_ADJUST_MAX 15

这个oom_adj中的变量的范围为15到-16之间。越大越容易被kill。oom_score就是它计算出来的一个值,就是根据这个值来选择哪些进程被kill掉的。

总之,通过上面的分析可知,满足下面的条件后,就是启动OOM机制。



  • VM里面分配不出更多的page(注意linux kernel是延迟分配page策略,及用到的时候才alloc;所以malloc + memset才有效)。

  • 用户地址空间不足,这种情况在32bit机器上user space超过了3GB,在64bit机器上不太可能发生。


如何选择要kill掉的进程

  只要存在overcommit,就可能会有OOM killer。

  Linux系统的选择策略也一直在不断的演化。我们可以通过设置一些值来影响OOM killer做出决策。Linux下每个进程都有个OOM权重,在/proc/

/oom_adj里面,取值是-17到+15,取值越高,越容易被干掉。

最终OOM killer是通过 /proc/

/oom_score 这个值来决定哪个进程被干掉的。这个值是系统综合进程的内存消耗量、CPU时间(utime + stime)、存活时间(uptime - start time)和oom_adj计算出的,消耗内存越多分越高,存活时间越长分越低。

  总之,总的策略是:损失最少的工作,释放最大的内存同时不伤及无辜的用了很大内存的进程,并且杀掉的进程数尽量少。 另外,Linux在计算进程的内存消耗的时候,会将子进程所耗内存的一半同时算到父进程中。


/proc/

/oom_score_adj

The value of /proc/

/oom_score_adj is added to the badness score before it
is used to determine which task to kill. Acceptable values range from -1000
(OOM_SCORE_ADJ_MIN) to
+1000 (OOM_SCORE_ADJ_MAX). This allows userspace to
polarize the preference
for oom killing either by always preferring a certain
task or completely disabling it. The lowest possible value,
-1000, is
equivalent to disabling oom killing entirely
for that task since it will always
report a badness score of
0.

    在计算最终的 badness score 时,会在计算结果是中加上 oom_score_adj ,这样用户就可以通过该在值来保护某个进程不被杀死或者每次都杀某个进程。其取值范围为-1000到1000 。

如果将该值设置为-1000,则进程永远不会被杀死,因为此时 badness score 永远返回0。


/proc/

/oom_adj

The value of /proc/

/oom_score_adj is added to the badness score before it
For backwards compatibility with previous kernels,
/proc/

/oom_adj may also
be used to tune the badness score. Its acceptable values range
from -16
(OOM_ADJUST_MIN) to
+15 (OOM_ADJUST_MAX) and a special value of -17
(OOM_DISABLE) to disable oom killing entirely
for that task. Its value is
scaled linearly with
/proc/

/oom_score_adj.

该设置参数的存在是为了和旧版本的内核兼容。其设置范围为-17到15。

注意 :内核使用以上两个接口时,如果更改其中一个,另一个会自动跟着变化。

内核的实现方式为:



  • 写oom_score_adj时,内核里都记录在变量 task->signal->oom_score_adj 中;

  • 读oom_score_adj时,从内核的变量 task->signal->oom_score_adj 中读取;

  • 写oom_adj时,也是记录到变量 task->signal->oom_score_adj 中,会根据oom_adj值按比例换算成oom_score_adj。

  • 读oom_adj时,也是从内核变量 task->signal->oom_score_adj 中读取,只不过显示时又按比例换成oom_adj的范围。


/proc/

/oom_score

This file can be used to check the current score used by the oom-killer is for
any given

. Use it together with /proc/

/oom_score_adj to tune which
process should be killed
in an out-of-memory situation.

  OOM killer机制主要根据该值和 /proc/

/oom_score_adj 来决定杀死哪一个进程的。

也就是oom_score_adj是一个对oom_score动态调整的加权

3.1 /proc/

/oom_adj & /proc/

/oom_score_adj- Adjust the oom-killer score
--------------------------------------------------------------------------------
These file can be used to adjust the badness heuristic used to
select which process gets killed in out of memory conditions.

 

3.2 /proc/

/oom_score - Display current oom-killer score
-------------------------------------------------------------
This file can be used to check the current score used by the oom
-killer is for any given

. Use it together with /proc/

/oom_score_adj to tune which process should be killed in an out-of-memory situation.
其他控制oom killer的行为

 /proc/sys/vm/oom_dump_tasks

  可以取值为0或者非0(默认为1),表示是否在发送oom killer时,打印task的相关信息。


 /proc/sys/vm/oom_kill_allocating_task

  可以取值为0或者非0(默认为0),0代表发送oom时,进行遍历任务链表,选择一个进程去杀死,而非0代表,发送oom时,直接kill掉引起oom的进程,并不会去遍历任务链表。


. /proc/sys/vm/panic_on_oom

  当发送out of memory时,该值允许或者禁止内核panic。(默认为0)



  • 0: 发生oom时,内核会选择性的杀死一些进程,然后尝试着去恢复。

  • 1: 发送oom时,内核直接panic。(如果一个进程使用mempolicy、cpusets来现在内存在一个nodes中消耗,则不会发生panic)

  • 2: 发送oom时,内核无条件直接panic

panic_on_oom=2+kdump,一起作用时,这样用户就可以分析出为什么会发送oom的原因了。


 完全关闭 OOM killer

如果需要的话,可以完全关闭 OOM killer(不推荐用在生产环境下):

# sysctl -w vm.overcommit_memory=2
# echo
"vm.overcommit_memory=2" >> /etc/sysctl.con

参考inux-out-of-memory-killer

oom脚本

#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
printf
"%2d %5d %s\n" \
"$(cat $proc/oom_score)" \
"$(basename $proc)" \
"$(cat $proc/cmdline | tr '

#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
printf
"%2d %5d %s\n" \
"$(cat $proc/oom_score)" \
"$(basename $proc)" \
"$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done
2>/dev/null | sort -nr | head -n 10' ' ' | head -c 50)"
done
2>/dev/null | sort -nr | head -n 10

display omm score

#!/bin/bash
# Displays running processes
in descending order of OOM score
# (skipping those with both score and adjust of zero).
# https:
//dev.to/rrampage/surviving-the-linux-oom-killer-2ki9

contents
-or-0 () { if [ -r "$1" ] ; then cat "$1" ; else echo 0 ; fi ; }
{
header
='# %8s %7s %9s %5s %5s %5s %s\n'
format
="$(echo "$header" | sed 's/^./ /')"
declare
-a lines output
IFS
=$'\r\n' command eval 'lines=($(ps -e -o user,pid,rss))'
shown
=0 ; omits=0
for n in $(eval echo "{1..$(expr ${#lines[@]} - 1)}") ; do # 1..skip header
line
="${lines[$n]}"
case "$line" in *[0-9]*)
set $line ; user=$1 ; pid=$2 ; rss=$3 ; shift 3
oom_score
=$( contents-or-0 /proc/$pid/oom_score)
oom_adj
=$( contents-or-0 /proc/$pid/oom_adj)
oom_score_adj
=$(contents-or-0 /proc/$pid/oom_score_adj)
if [ -f /proc/$pid/oom_score ] && \
[
0 -ne $oom_score -o 0 -ne $oom_score_adj -o 0 -ne $oom_adj ]
then
output[${#output[@]}]
="$( \
printf "$format" \
"$user" \
"$pid" \
"$rss" \
"$oom_score" \
"$oom_score_adj" \
"$oom_adj" \
"$(cat /proc/$pid/cmdline | tr '\0' ' ' )" \
)
"
(( ++shown ))
else
((
++omits ))
fi
;;
esac
done
printf
"$header" '' '' '' OOM OOM OOM ''
printf
"$header" User PID RSS Score ScAdj Adj \
"Command (shown $shown, omits $omits)"
for n in $(eval echo "{0..$(expr ${#output[@]} - 1)}") ; do
echo
"${output[$n]}"
done
| sort -k 4nr -k 5rn
}

 


OOM处理的基本流程简单描述如下:



  • 先通过函数 blocking_notifier_call_chain 遍历用户注册的通知链函数,如果通知链的callback函数能够处理OOM,则直接退出OOM killer操作。

  • 如果引起OOM的进程(current)拥有pending SIGKILL,或者正在退出,则选择current进程。

  • 检查发生OOM时,内核是否需要panic,如果panic,这后续代码就不会执行。

  • 如果设置了 sysctl_oom_kill_allocating_task ,并且 current->mm 不为空,current的 oom_score_adj != OOM_SCORE_ADJ_MIN ,且可以杀死current,则直接杀死current进程,释放内存。

  • 调用 select_bad_process 选择一个最优的进程p去杀

  • 如果 p == null, 即没有进程可杀,内核则直接panic,否则调用函数 oom_kill_process 去kill选择选择的进程p。



  • 那 select_bad_process 如何选择一个可以杀死的进程呢?



    1. 该函数遍历所有的进程和线程 for_each_process_thread(g, p)

    2. 针对每一个线程:该函数先利用 oom_scan_process_thread 检查线程的类别,排除一些特殊的线程,然后对可以作为候选的线程进行评分。

    3. 最后返回评分最高的线程。



/**
* out_of_memory - kill the "best" process when we run out of memory
* @oc: pointer to struct oom_control
*
* If we run out of memory, we have the choice between either
* killing a random task (bad), letting the system crash (worse)
* OR try to be smart about which process to kill. Note that we
* don't have to be perfect here, we just have to be good.
*/
bool out_of_memory(struct oom_control *oc)
{
struct task_struct *p;
unsigned
long totalpages;
unsigned
long freed = 0;
unsigned
int uninitialized_var(points);
enum oom_constraint cOnstraint= CONSTRAINT_NONE;
if (oom_killer_disabled)
return false;
blocking_notifier_call_chain(
&oom_notify_list, 0, &freed);
if (freed > 0)
/* Got some memory back in the last second. */
return true;
/*
* If current has a pending SIGKILL or is exiting, then automatically
* select it. The goal is to allow it to allocate so that it may
* quickly exit and free its memory.
*
* But don't select if current has already released its mm and cleared
* TIF_MEMDIE flag at exit_mm(), otherwise an OOM livelock may occur.
如果当前进程有pending的SIGKILL(9)信号,或者正在退出,则选择当前进程来kill,
* 这样可以最快的达到释放内存的目的。
*/
if (current->mm &&
(fatal_signal_pending(current)
|| task_will_free_mem(current))) {
mark_oom_victim(current);
try_oom_reaper(current);
return true;OOM_SCORE_ADJ_MIN
}
/*
* The OOM killer does not compensate for IO-less reclaim.
* pagefault_out_of_memory lost its gfp context so we have to
* make sure exclude 0 mask - all other users should have at least
* ___GFP_DIRECT_RECLAIM to get here.
*/
if (oc->gfp_mask && !(oc->gfp_mask & (__GFP_FS|__GFP_NOFAIL)))
return true;
/*
* Check if there were limitations on the allocation (only relevant for
* NUMA) that may require different handling.
* 检查是否有限制,有几种不同的限制策略,仅用于NUMA场景
*/
constraint
= constrained_alloc(oc, &totalpages);
if (constraint != CONSTRAINT_MEMORY_POLICY)
oc
->nodemask = NULL;
// 检查是否配置了/proc/sys/kernel/panic_on_oom,如果是则直接触发panic
check_panic_on_oom(oc, constraint, NULL);
/*
* 检查是否配置了oom_kill_allocating_task,即是否需要kill current进程来
* 回收内存,如果是,且current进程是killable的,则kill current进程。
*/
if (sysctl_oom_kill_allocating_task && current->mm &&
!oom_unkillable_task(current, NULL, oc->nodemask) &&
current
->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
get_task_struct(current);
oom_kill_process(oc, current,
0, totalpages, NULL,
"Out of memory (oom_kill_allocating_task)");
return true;
}
// 根据既定策略选择需要kill的process。
p = select_bad_process(oc, &points, totalpages);
/* Found nothing?!?! Either we hang forever, or we panic. */
if (!p && !is_sysrq_oom(oc)) {
dump_header(oc, NULL, NULL);
panic(
"Out of memory and no killable processes...\n");
/*
* 如果没有选出来,即没有可kill的进程,那么直接panic
* 通常不会走到这个流程,但也有例外,比如,当被选中的进程处于D状态,或者正在被kill
*/
}
// kill掉被选中的进程,以释放内存。
if (p && p != (void *)-1UL) {
oom_kill_process(oc, p, points, totalpages, NULL,
"Out of memory");
/*
* Give the killed process a good chance to exit before trying
* to allocate memory again.
* 在重新分配内存之前,给被kill的进程1s的时间完成exit相关处理,通常情况下,1s应该够了。
*/
schedule_timeout_killable(
1);
}
return true;
}

 

out_of_memory

  ->select_bad_process

    ->oom_badness

oom_badness用于计算进程的“点数”,点数最高者被选中

/**
* oom_badness - heuristic function to determine which candidate task to kill
* @p: task struct of which task we should calculate
* @totalpages: total present RAM allowed for page allocation
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
/*
* 计算进程"点数"(代表进程被选中的可能性)的函数,点数根据进程占用的物理内存来计算
* 物理内存占用越多,被选中的可能性越大。root processes有3%的bonus。
*/
unsigned
long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned long totalpages)
{
long points;
long adj;
if (oom_unkillable_task(p, memcg, nodemask))
return 0;
p
= find_lock_task_mm(p);// 确认进程是否还存在
if (!p)
return 0;
/*
* Do not even consider tasks which are explicitly marked oom
* unkillable or have been already oom reaped.
*/
adj
= (long)p->signal->oom_score_adj;
//如果将该值设置为-1000,则进程永远不会被杀死,因为此时 badness score 永远返回0。
if (adj == OOM_SCORE_ADJ_MIN ||
test_bit(MMF_OOM_REAPED,
&p->mm->flags)) {
task_unlock(p);
return 0;
}
/*
* The baseline for the badness score is the proportion of RAM that each
* task's rss, pagetable and swap space use.
*/ // 点数=rss(驻留内存/占用物理内存)+pte数+交换分区用量
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
atomic_long_read(
&p->mm->nr_ptes) + mm_nr_pmds(p->mm);
task_unlock(p);
/*
* Root processes get 3% bonus, just like the __vm_enough_memory()
* implementation used by LSMs.
*//*
* root用户启动的进程,有总 内存*3% 的bonus,就是说可以使用比其它进程多3%的内存
* 3%=30/1000
*/
if (has_capability_noaudit(p, CAP_SYS_ADMIN))
points
-= (points * 3) / 100;
/* Normalize to oom_score_adj units 归一化"点数"单位*/
adj
*= totalpages / 1000;
points
+= adj;
/*
* Never return 0 for an eligible task regardless of the root bonus and
* oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
*/
return points > 0 ? points : 1;
}

memblock机制

顺便说一下memblock机制:

  memblock管理算法 将可用可分配的内存在 memblock.memory 进行管理起来,已分配的内存在 memblock.reserved 进行管理,只要内存块加入到 memblock.reserved 里面就表示该内存已经被申请占用了。所以有个关键点需要注意,内存申请的时候,仅是把被申请到的内存加入到 memblock.reserved 中,并不会在 memblock.memory 里面有相关的删除或改动的操作,这也就是为什么申请和释放的操作都集中在 memblock.reserved 的原因了。

这个算法效率并不高,但是这是合理的,毕竟在初始化阶段没有那么多复杂的内存操作场景,甚至很多地方都是申请了内存做永久使用的。

  以前会使用memblock分配一块内存 用来检测设备是不是断电重启!


memblock内存申请和释放

memblock算法下的内存申请和释放的接口分别为 memblock_alloc() 和 memblock_free() 。



  • memblock_alloc 操作 memblock.reserved

  • memblock_free 操作 memblock.reserved

  • memblock_remove 操作 memblock.memory

 

http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!!

但行好事 莫问前程

--身高体重180的胖子



推荐阅读
  • 树莓派Linux基础(一):查看文件系统的命令行操作
    本文介绍了在树莓派上通过SSH服务使用命令行查看文件系统的操作,包括cd命令用于变更目录、pwd命令用于显示当前目录位置、ls命令用于显示文件和目录列表。详细讲解了这些命令的使用方法和注意事项。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • Java学习笔记之使用反射+泛型构建通用DAO
    本文介绍了使用反射和泛型构建通用DAO的方法,通过减少代码冗余度来提高开发效率。通过示例说明了如何使用反射和泛型来实现对不同表的相同操作,从而避免重复编写相似的代码。该方法可以在Java学习中起到较大的帮助作用。 ... [详细]
  • 原理:dismiss再弹出,把dialog设为全局对象。if(dialog!null&&dialog.isShowing()&&!(Activity.)isFinishing()) ... [详细]
  • SpringBoot整合SpringSecurity+JWT实现单点登录
    SpringBoot整合SpringSecurity+JWT实现单点登录,Go语言社区,Golang程序员人脉社 ... [详细]
  • LVS实现负载均衡的原理LVS负载均衡负载均衡集群是LoadBalance集群。是一种将网络上的访问流量分布于各个节点,以降低服务器压力,更好的向客户端 ... [详细]
  • 本文详细介绍了在Centos7上部署安装zabbix5.0的步骤和注意事项,包括准备工作、获取所需的yum源、关闭防火墙和SELINUX等。提供了一步一步的操作指南,帮助读者顺利完成安装过程。 ... [详细]
  • GSIOpenSSH PAM_USER 安全绕过漏洞
    漏洞名称:GSI-OpenSSHPAM_USER安全绕过漏洞CNNVD编号:CNNVD-201304-097发布时间:2013-04-09 ... [详细]
  • 本文介绍了在RHEL 7中的系统日志管理和网络管理。系统日志管理包括rsyslog和systemd-journal两种日志服务,分别介绍了它们的特点、配置文件和日志查询方式。网络管理主要介绍了使用nmcli命令查看和配置网络接口的方法,包括查看网卡信息、添加、修改和删除配置文件等操作。 ... [详细]
  • Python脚本编写创建输出数据库并添加模型和场数据的方法
    本文介绍了使用Python脚本编写创建输出数据库并添加模型数据和场数据的方法。首先导入相应模块,然后创建输出数据库并添加材料属性、截面、部件实例、分析步和帧、节点和单元等对象。接着向输出数据库中添加场数据和历程数据,本例中只添加了节点位移。最后保存数据库文件并关闭文件。文章还提供了部分代码和Abaqus操作步骤。另外,作者还建立了关于Abaqus的学习交流群,欢迎加入并提问。 ... [详细]
  •     这里使用自己编译的hadoop-2.7.0版本部署在windows上,记得几年前,部署hadoop需要借助于cygwin,还需要开启ssh服务,最近发现,原来不需要借助cy ... [详细]
  • 大坑|左上角_pycharm连接服务器同步写代码(图文详细过程)
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了pycharm连接服务器同步写代码(图文详细过程)相关的知识,希望对你有一定的参考价值。pycharm连接服务 ... [详细]
  • Hadoop2.6.0 + 云centos +伪分布式只谈部署
    3.0.3玩不好,现将2.6.0tar.gz上传到usr,chmod-Rhadoop:hadophadoop-2.6.0,rm掉3.0.32.在etcp ... [详细]
  • linux 禁止指定ip访问
    linux中如何禁止指定的ip访问呢?比如被别人暴力破解,被别人使用不同的密码尝试登录:所以我想直接禁用这些ip的访问.怎么办呢?解决方案:修改配置文件etchosts.deny把 ... [详细]
  • 一、修改注册表去掉桌面图标小箭头1按下win+R组合快捷键,打开windows10系统的“运行”窗口,输入“regedit”,打开注册表编辑器,找到HKEY_CLASSES_ROOT\lnkfi ... [详细]
author-avatar
手机用户2502906317
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有