当前位置: 首页 > news >正文

【Linux】进程信号(1)

1. 信号快速认识

信号是进程之间事件异步通知的一种方式,属于软中断。

1-1 生活角度的信号

通过 “等待快递” 场景,类比信号的核心逻辑:

  • 能 “识别快递”:即使快递未到,也知道如何处理(对应进程识别信号是内核内置特性,信号未产生时,处理方法已准备好)。
  • 收到快递通知后可延迟取件:正在打游戏时,5min 后再取(对应信号产生后不立即处理,进程优先处理高优先级任务,在合适时处理)。
  • 延迟期间 “记住有快递”:收到通知到取件的时间窗内,明确有快递待处理(对应信号产生后,内核会保存信号状态)。
  • 处理快递的三种方式:
    1. 默认动作:打开快递使用商品;
    2. 自定义动作:将零食快递送给女朋友;
    3. 忽略:快递放床头,继续打游戏(对应信号处理的三种方式:默认、忽略、自定义,自定义也称信号捕捉)。
  • 快递到来异步:无法确定快递员打电话的时间(对应信号产生是异步的,进程无法预判信号到来时机)。

1-2 技术应用角度的信号

1-2-1 一个样例(默认信号处理)

// sig.cc
#include <iostream>
#include <unistd.h>
int main()
{while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
  • 操作与现象:
    1. 编译运行:g++ sig.cc -o sig./sig,进程循环打印信息;
    2. 按下Ctrl+C,进程退出。
  • 背后逻辑:
    1. 用户启动前台进程;
    2. Ctrl+C产生硬件中断,被 OS 获取并解释为信号,发送给前台进程;
    3. 前台进程收到信号,执行默认处理动作(退出)。

1-2-2 一个系统函数(signal 函数)与自定义信号处理

1. signal 函数
  • 功能:ANSI C 标准的信号处理函数,用于设置特定信号的处理动作。
  • 头文件:#include <signal.h>
  • 函数原型:
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    
  • 参数说明:
    • signum:信号编号(如 2 对应 SIGINT 信号);
    • handler:函数指针,指定信号的处理动作,收到对应信号时回调执行该函数。
2. 自定义信号处理示例(证明 Ctrl+C 对应 SIGINT 信号)
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}
int main()
{std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT /*2*/, handler);while (true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
  • 操作与现象:
    1. 编译运行:g++ sig.cc -o sig./sig,打印进程 ID 并循环输出信息;
    2. 按下Ctrl+C,打印 “我是:进程 ID, 我获得了一个信号: 2”,进程不退出,继续循环。

3. 思考与注意
  • 进程不退出的原因:通过signal(SIGINT, handler)将 SIGINT 信号的处理动作从默认(退出)改为自定义函数handler,收到信号时执行自定义逻辑而非退出。
  • 该例子说明:信号处理可自定义,需提前通过 signal 函数设置处理动作。
  • 生活类比:进程是 “你”,操作系统是 “快递员”,信号是 “快递”,发信号类似 “快递员打电话通知”。
  • 关键注意:
    1. signal 函数仅设置信号处理动作,不直接调用处理动作;若信号未产生,自定义函数不执行;
    2. Ctrl+C产生的信号仅发给前台进程;命令后加&可将进程放后台运行,Shell 无需等待进程结束即可接收新命令;
    3. 信号相对于进程控制流程是异步的,进程用户空间代码执行到任意位置都可能收到 SIGINT 信号;
    4. 前后台进程:
      1. 后台进程无法从标准输入中获取内容!
      2. 前台进程能从键盘获取标准输入,因为键盘只有一个,输入数据一定是给一个确定的进程的。
      3. 两者都能向标准输出上打印。
      4. Shell 可同时运行 1 个前台进程和多个后台进程。
    5. 补充一部分命令,前后台移动:
      1. 在要运行的命令末尾加上&符号即可让程序在后台运行。
      2. jobs查看所有的后台进程。
      3. fg 任务号,将特定的进程提到前台。
      4. ctrl+z:进程切换到后台。
      5. bg 任务号,让后台进程恢复运行。

1-3 信号概念

1-3-1 查看信号

  • 信号属性:每个信号有编号和宏定义名称,宏定义在signal.h中(如#define SIGINT 2)。
  • 信号分类:编号 34 以上为实时信号,本章仅讨论编号 34 以下的信号。
  • 查看方式:通过man 7 signal查看各信号的产生条件、默认处理动作等详细说明。

1-3-2 信号处理(三种可选动作)

1. 忽略此信号
  • 代码示例:
    #include <iostream>
    #include <unistd.h>
    #include <signal.h>
    void handler(int signumber)
    {std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
    }
    int main()
    {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT /*2*/, SIG_IGN); // 设置忽略信号的宏while (true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
    }
    
  • 操作与现象:编译运行后,按下Ctrl+C无反应,进程继续循环打印。
2. 执行该信号的默认处理动作
  • 代码示例:
    #include <iostream>
    #include <unistd.h>
    #include <signal.h>
    void handler(int signumber)
    {
    std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
    }
    int main()
    {
    std::cout << "我是进程: " << getpid() << std::endl;
    signal(SIGINT/*2*/, SIG_DFL);
    while(true){
    std::cout << "I am a process, I am waiting signal!" << std::endl;
    sleep(1);
    }
    }
    
  • 操作与现象:编译运行后,按下Ctrl+C,进程退出(执行 SIGINT 信号的默认动作)。
3. 自定义捕捉信号
  • 方式:提供信号处理函数,内核处理信号时切换到用户态执行该函数(即 1-2-2 中自定义信号处理的样例)。
  • 源码细节:
    #define SIG_DFL ((__sighandler_t) 0) /* Default action. */
    #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
    /* Type of a signal handler. */
    typedef void (*__sighandler_t) (int);
    
    本质是将 0、1 强转为__sighandler_t(信号处理函数指针)类型,分别表示默认动作、忽略信号。

2. 产生信号

信号的产生并非随机,而是由特定场景触发,主要分为终端按键、系统命令、函数调用、软件条件、硬件异常五大类,以下结合具体代码与操作,梳理各类信号的产生方式、现象及核心逻辑。

2-1 通过终端按键产生信号

终端按键触发的信号是用户与前台进程交互的常用方式,核心是通过键盘输入触发硬件中断,由操作系统解释为对应信号并发送给前台进程。

2-1-1 基本操作(3 种核心按键信号)

1. Ctrl+\(SIGQUIT,3 号信号)
  • 功能:发送终止信号,默认动作是 “终止进程 + 生成 core dump 文件”(用于事后调试),可通过signal函数自定义处理。
  • 代码示例:
    #include <iostream>
    #include <unistd.h>
    #include <signal.h>
    void handler(int signumber)
    {std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
    }
    int main()
    {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGQUIT/*3*/, handler); // 自定义SIGQUIT处理动作while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
    }
    
  • 操作与现象:
    • 编译运行:g++ sig.cc -o sig && ./sig,进程循环打印;
    • 按下Ctrl+\,打印 “我是:进程 ID, 我获得了一个信号: 3”,进程不退出;
    • 注释signal(SIGQUIT, handler)后重新运行,按下Ctrl+\,进程终止并提示Quit(执行默认动作)。
2. Ctrl+Z(SIGTSTP,20 号信号)
  • 功能:发送停止信号,默认动作是 “将前台进程挂起至后台”,可通过signal函数自定义处理。
  • 代码示例:
    #include <iostream>
    #include <unistd.h>
    #include <signal.h>
    void handler(int signumber)
    {std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
    }
    int main()
    {std::cout << "我是进程: " << getpid() << std::endl;signal(SIGTSTP/*20*/, handler); // 自定义SIGTSTP处理动作while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
    }
    
  • 操作与现象:
    • 编译运行后按下Ctrl+Z,打印 “我是:进程 ID, 我获得了一个信号: 20”,进程不退出;
    • 注释signal(SIGTSTP, handler)后重新运行,按下Ctrl+Z,进程挂起至后台,终端提示[1]+ Stopped ./sig
    • 查看后台进程:jobs(显示挂起的进程);如需恢复前台运行:fg 进程ID

2-1-2 核心原理与注意

  • OS 如何识别键盘输入:键盘输入触发硬件中断,中断控制器将中断信号发给 CPU,CPU 转去执行中断处理程序(OS 内核代码),内核解析按键含义并转换为对应信号,发送给前台进程。
  • 信号与硬件中断的类比:信号是 “软件层面的中断”—— 硬件中断发给 CPU,信号发给进程;两者均是 “突发通知”,但作用对象与层级不同。

2-2 调用系统命令向进程发信号

通过kill系统命令可主动向指定进程发送信号,无需依赖终端交互,核心是通过命令指定 “信号类型” 与 “目标进程 ID”。

操作示例(以 SIGSEGV 信号为例)

  1. 准备后台运行的死循环进程:
    #include <iostream>
    #include <unistd.h>
    #include <signal.h>
    int main()
    {while(true){ sleep(1); } // 死循环,后台运行
    }
    
  2. 编译与后台启动:
    $ g++ sig.cc -o sig
    $ ./sig &  # 后台运行,终端返回进程ID(如213784)
    
  3. 查看进程 ID:
    $ ps ajx | head -1 && ps ajx | grep sig  # 筛选sig进程信息
    PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
    211805 213784 213784 211805 pts/0 213792 S 1002 0:00 ./sig
    
  4. 发送 SIGSEGV(11 号信号,段错误信号):
    $ kill -SIGSEGV 213784  # 或 kill -11 213784(11是SIGSEGV编号)
    $  # 多按一次回车,终端提示 [1]+ Segmentation fault ./sig(进程因SIGSEGV终止)
    

关键说明

  • 信号命令的多种写法:kill -信号名称 进程ID(如kill -SIGSEGV 213784)或kill -信号编号 进程ID(如kill -11 213784),两者等价。
  • 终端提示延迟原因:发送信号时,Shell 可能已回到提示符等待输入,为避免信息交错,Shell 会在用户下次输入后再显示进程终止信息(如Segmentation fault)。

2-3 使用函数产生信号

通过系统调用函数可在代码中主动产生信号,常用函数包括kill(给指定进程发信号)、raise(给自己发信号)、abort(给自己发终止信号)。

2-3-1 kill 函数

  • 功能:给指定 PID 的进程发送指定信号,是kill命令的底层实现。

  • 头文件:#include <sys/types.h>#include <signal.h>

  • 函数原型:int kill(pid_t pid, int sig);

    • 参数:pid(目标进程 ID,正数为指定进程,-1 为所有进程)、sig(信号编号 / 名称);
    • 返回值:成功返回 0,失败返回 - 1(并设置errno)。
  • 代码示例(实现自定义 kill 命令):

    #include <iostream>
    #include <unistd.h>
    #include <sys/types.h>
    #include <signal.h>
    // 用法:./mykill -signumber pid(如./mykill -2 213784)
    int main(int argc, char *argv[])
    {if(argc != 3) // 检查参数数量{std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;return 1;}int signum = std::stoi(argv[1]+1); // 提取信号编号(跳过前缀“-”)pid_t pid = std::stoi(argv[2]);    // 提取目标进程IDint n = kill(pid, signum);       // 发送信号return 0;
    }
    

2-3-2 raise 函数

  • 功能:给当前进程发送指定信号(“自己给自己发信号”)。

  • 头文件:#include <signal.h>

  • 函数原型:int raise(int sig);

    • 参数:sig(信号编号 / 名称);
    • 返回值:成功返回 0,失败返回非 0。
  • 代码示例:

    #include <iostream>
    #include <unistd.h>
    #include <signal.h>
    void handler(int signumber)
    {std::cout << "获取了一个信号: " << signumber << std::endl;
    }
    int main()
    {signal(2/*SIGINT*/, handler); // 自定义2号信号处理while(true){sleep(1);raise(2); // 每秒给自己发一次2号信号}
    }
    
  • 运行效果:编译运行后,每秒打印 “获取了一个信号: 2”,进程持续运行。

2-3-3 abort 函数

  • 功能:使当前进程异常终止,底层是给自己发送SIGABRT 信号(6 号信号)

  • 头文件:#include <stdlib.h>

  • 函数原型:void abort(void);

    • 无参数,无返回值(进程必然终止,不会返回)。
  • 代码示例:

    #include <iostream>
    #include <unistd.h>
    #include <stdlib.h>
    #include <signal.h>
    void handler(int signumber)
    {std::cout << "获取了一个信号: " << signumber << std::endl;
    }
    int main()
    {signal(SIGABRT/*6*/, handler); // 自定义6号信号处理while(true){sleep(1);abort(); // 每秒给自己发一次SIGABRT信号}
    }
    
  • 运行效果:

    • 编译运行后,打印 “获取了一个信号: 6”,随后进程终止并提示Aborted(即使自定义处理,abort仍会强制终止进程);
    • 注释signal(SIGABRT, handler)后运行,直接提示Aborted(执行默认动作)。

2-4 由软件条件产生信号

软件条件指由软件内部状态或操作触发信号,典型场景包括 “管道断裂(SIGPIPE)”“定时器超时(SIGALRM)”,此处重点介绍alarm函数与 SIGALRM 信号。

2-4-1 alarm 函数基础

  • 功能:设定 “闹钟”,告诉内核在seconds秒后给当前进程发送SIGALRM 信号(14 号信号),默认处理动作是终止进程。
  • 头文件:#include <unistd.h>
  • 函数原型:unsigned int alarm(unsigned int seconds);
    • 参数:seconds(闹钟秒数,0 表示取消之前的闹钟);
    • 返回值:若之前有未到期的闹钟,返回剩余秒数;否则返回 0。

2-4-2 基本验证(体会 IO 效率)

通过两个代码示例对比 “IO 操作多少” 对计数结果的影响,验证 SIGALRM 的触发逻辑。

示例 1:IO 操作多(打印频繁)
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{int count = 0;alarm(1); // 1秒后发送SIGALRMwhile(true){std::cout << "count : " << count << std::endl; // 频繁IO(打印)count++;}return 0;
}
  • 运行效果:1 秒后进程被 SIGALRM 终止,计数结果较小(IO 操作耗时,循环次数少)。
示例 2:IO 操作少(仅计数)
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{std::cout << "count : " << count << std::endl; // 仅1次IOexit(0);
}
int main()
{signal(SIGALRM, handler); // 自定义SIGALRM处理(避免进程直接终止)alarm(1);while (true) { count++; } // 无额外IO,仅计数return 0;
}
  • 运行效果:1 秒后执行handler,打印计数结果(如count : 492333713),计数远大于示例 1(IO 少,循环速度快)。
结论:
  • SIGALRM 会准时触发(1 秒后必然发送);
  • IO 操作会消耗时间,导致相同时间内的循环次数减少(IO 效率低)。

2-4-3 设置重复闹钟

alarm是 “一次性闹钟”,若需重复触发,需在 SIGALRM 的处理函数中重新调用alarm,结合pause函数(等待信号)实现循环。

代码示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>
using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;void handler(int signo)
{for(auto &f : gfuncs) { f(); } // 执行预设函数(可扩展)std::cout << "gcount : " << gcount << std::endl;int remaining = alarm(1); // 重新设置1秒闹钟,返回上次剩余时间std::cout << "剩余时间 : " << remaining << std::endl;
}int main()
{alarm(1); // 首次设置闹钟signal(SIGALRM, handler);while (true){pause(); // 阻塞等待信号,收到信号后返回-1std::cout << "我醒来了..." << std::endl;gcount++;}
}
  • pause函数补充:

    • 原型:int pause(void);,功能是阻塞进程,直到收到一个 “能终止进程或触发自定义处理函数” 的信号;
    • 返回值:仅当信号被捕捉且处理函数返回时,返回 - 1(并设置errno=EINTR)。
  • 运行效果:

    • 终端 1 运行./alarm,首次 1 秒后打印 “gcount : 0”“剩余时间 : 0”,之后每秒重复;
    • 终端 2 执行kill -14 进程ID(主动发送 SIGALRM),终端 1 会提前打印,且 “剩余时间” 显示上次闹钟的剩余秒数(如 13)。
结论:
  • 闹钟需重复设置才会持续触发;
  • alarm(0)可取消当前闹钟,返回之前的剩余时间。

2-4-4 软件条件的核心理解

软件条件是 “由软件逻辑触发的信号”,如:

  • alarm的 “时间到期” 是软件定时器条件;
  • 向已关闭的管道写数据(触发 SIGPIPE)是软件状态条件;
  • 这些条件由内核检测,满足时内核主动向进程发送对应信号。

2-5 硬件异常产生信号

硬件异常(如 CPU 运算错误、内存访问错误)由硬件检测后通知内核,内核将异常解释为对应信号并发送给当前进程,典型场景包括 “除零(SIGFPE)”“非法内存访问(SIGSEGV)”。

2-5-1 模拟除零(SIGFPE,8 号信号)

  • 代码示例:
    #include <stdio.h>
    #include <signal.h>
    void handler(int sig)
    {printf("catch a sig : %d\n", sig); // 自定义8号信号处理
    }
    int main()
    {// signal(SIGFPE, handler); // 注释则执行默认动作(终止进程)sleep(1);int a = 10;a /= 0; // 除零操作,触发硬件异常while(1);return 0;
    }
    
  • 运行效果:
    • 注释signal(SIGFPE, handler):进程终止,提示Floating point exception (core dumped)
    • 启用signal(SIGFPE, handler):持续打印 “catch a sig : 8”(除零异常未清理,CPU 状态寄存器保持异常标记,内核反复发送 SIGFPE)。

2-5-2 模拟野指针(SIGSEGV,11 号信号)

  • 代码示例(默认动作):
    #include <stdio.h>
    #include <signal.h>
    void handler(int sig)
    {printf("catch a sig : %d\n", sig);
    }
    int main()
    {// signal(SIGSEGV, handler); // 注释则执行默认动作sleep(1);int *p = NULL; // 野指针(空指针)*p = 100;      // 非法内存写入,触发MMU硬件异常while(1);return 0;
    }
    
  • 运行效果:
    • 注释signal(SIGSEGV, handler):进程终止,提示Segmentation fault (core dumped)
    • 启用signal(SIGSEGV, handler):持续打印 “catch a sig : 11”(非法内存访问状态未清理,内核反复发送 SIGSEGV)。

2-5-3 Core Dump(核心转储)

1. 什么是 Core Dump?

进程异常终止时,将用户空间内存数据保存到磁盘文件(默认名core),用于事后调试(如通过gdb ./sig core查看崩溃时的内存状态)。

2. 启用 Core Dump

默认情况下,系统禁止产生 Core Dump(避免敏感信息泄露),需通过ulimit命令修改限制:

$ ulimit -c 1024  # 允许Core Dump文件最大为1024块(约512KB)
$ ulimit -a        # 查看当前限制,确认“core file size”为1024
3. 验证 Core Dump(以 SIGQUIT 为例)
  • 前台运行死循环进程,按下Ctrl+\(SIGQUIT 默认动作:终止 + Core Dump);
  • 终端生成core文件,可通过gdb ./sig core调试:

    bash

    $ gdb ./sig core
    (gdb) bt  # 查看崩溃时的函数调用栈
    
4. 子进程 Core Dump 检测
  • 代码示例:
    #include <iostream>
    #include <unistd.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <sys/wait.h>
    int main()
    {if (fork() == 0) // 子进程{sleep(1);int a = 10;a /= 0; // 除零,触发SIGFPE(默认Core Dump)exit(0);}int status = 0;waitpid(-1, &status, 0); // 等待子进程退出,获取状态// 解析状态:退出信号(低7位)、是否Core Dump(第8位)printf("exit signal: %d, core dump: %d\n", status&0x7F, (status>>7)&1);return 0;
    }
    
  • 运行效果:打印 “exit signal: 8, core dump: 1”(8 是 SIGFPE 编号,1 表示产生 Core Dump)。

2-6 总结思考

  1. 所有信号产生最终由 OS 执行的原因:OS 是进程的管理者,负责进程的创建、调度、资源分配,只有 OS 能访问进程的 PCB(保存信号相关状态),因此信号的 “产生、发送、状态记录” 必须由 OS 完成。
  2. 信号是否立即处理:否。进程会优先执行当前高优先级任务(如用户代码),仅在 “安全点”(如系统调用返回、中断处理结束)检查是否有未处理信号,再进行处理。
  3. 信号的临时记录位置:记录在进程的 PCB 中。PCB 中有 “信号位图”(每个位对应一个信号,1 表示信号已产生未处理)和 “信号处理动作表”(记录每个信号的处理方式)。
  4. 进程未收信号时是否知道处理方式:是。进程创建时,PCB 中的 “信号处理动作表” 会初始化(默认动作由内核预设,如 SIGINT 默认终止),即使信号未产生,进程已明确每个合法信号的处理方式。
  5. OS 向进程发送信号的完整过程
    1. 触发信号产生条件(如终端按键、函数调用、硬件异常);
    2. OS 内核检测到条件,确定目标进程与对应信号;
    3. 内核修改目标进程 PCB 中的 “信号位图”(将对应位置 1);
    4. 进程执行到 “安全点” 时,检查 PCB 中的信号位图;
    5. 若有未处理信号,内核根据 PCB 中的 “信号处理动作表” 执行对应动作(默认 / 忽略 / 自定义);
    6. 处理完成后,内核将信号位图中对应位清 0。
http://www.dtcms.com/a/443460.html

相关文章:

  • 男女做那个的的视频网站常见的网页设计工具
  • 做网站时管理员权限的页面wordpress标签前缀
  • 建网站找哪个平台好呢专业简历制作网站有哪些
  • 3.2.2 LangChain.js + LangGraph.js 实战
  • ARL资产侦察灯塔系统一键部署教程(2025最新)
  • 山东省环保厅官方网站建设项目网站建设好找工作吗
  • ui界面设计说明范文网站排名优化价格
  • SSM大学请假管理系统e9kl1(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • java基础-11 : 数据结构与算法
  • 洛谷P1036 [NOIP 2002 普及组] 选数 题解
  • 坂田做网站的公司业务员销售管理软件
  • 网站服务器基本配置微信管理系统在哪
  • C语言题目与练习解析:配套《数据在内存中的存储》
  • Effective STL 第1条:慎重选择容器类型
  • 网站做多长时间才有流量双线主机可以做彩票网站吗
  • 外贸型网站建设公司福州最好的网站建设公司
  • 规划网站建设的总体目标张家港网站设计制作早晨设计
  • 安徽高端网站建设免费国外网站模板
  • 国外做建筑平面图的网站网页如何发布
  • 外贸网站如何推广做网站界面用的软件
  • 英文建站系统广州购网站建设
  • 手机版的学习网站ppt模板简约
  • 企业法律平台网站建设方案中国机械加工网价位
  • 网站的建设课程网站栏目怎么做301定向
  • Python求2/1+3/2+5/3+8/5+13/8.....前20项的和
  • 第1集:为什么要开发AI邮件助手?痛点与价值分析
  • 珠海模板建站平台深圳福田在线
  • 广东网站建设方案带数据库的网站做
  • 网站首页不见怎么做网站风格设计原则
  • 查看网站后台登陆地址win主机wordpress重定向