镇平微网站建设,网络推广营销平台系统,做网站域名的好处是什么,自己建网站多少钱注意#xff1a;首先需要提醒一个事情#xff0c;本节提及的进程信号和下节的信号量没有任何关系#xff0c;请您区分对待。 1.信号概念
1.1.生活中的信号
我们在生活中通过体验现实#xff0c;记忆了一些信号和对应的处理动作#xff0c;这意味着信号有以下相关的特点首先需要提醒一个事情本节提及的进程信号和下节的信号量没有任何关系请您区分对待。 1.信号概念
1.1.生活中的信号
我们在生活中通过体验现实记忆了一些信号和对应的处理动作这意味着信号有以下相关的特点
我们可以识别出信号知道后续对应的执行动作。即使没有收到特定的信号我们也知道当下应该继续做什么。收到信号后不一定就要立刻执行需要先被临时记忆/临时存储起来。
1.2.进程中的信号
那什么是 Linux 信号呢信号的本质是一种异步通知机制由“程序员/操作系统”发送信号给进程。 补充异步和同步 异步通常和同步一起作比较理解在之前我们讲的管道通信就具有同步机制管道必须等待写端写入数据后才能让读端读走所有数据也就是说读端必须等待写端反过来也一样否则进行阻塞。但是信号是异步机制的进程没有办法预料到何时会接受到信号信号的产生是是随机的、缺乏规律的进程不能一直因为一个可能连发送都不会发送的信号一直进入阻塞状态因此信号的产生对于进程而言是异步的。 总结来说双方并没有在互相等待就是异步若有一方会进行等待就是同步。 而进程是接受信号的载体因此要谈信号就离不开进程的基础而进程要处理信号 (1)就必须要有识别信号的能力 (2)以及有接受到信号对应的处理方式 进程接受到的信号是随机/异步的在信号没有产生之前进程依旧在做自己的任务。 进程忙碌的任务可能非常重要此时进程无法根据信号做出改变除非信号非常重要因此信号也就有优先级的区分信号会被暂时存储起来。 补充信号在某些角度来看也可以理解为进程通信的一种方式但是为了我们知识的模块化信号我单独独立出来作为一个小模块。 2.信号分类
得到信号后在信号处理逻辑上可以将信号分为
默认处理进程默认自带的处理信号的方式由程序写好的逻辑忽略处理有些信号对进程有害或者进程没有对应处理逻辑的就需要忽略该信号自定义处理自定义捕捉某些信号也就是程序员自己定义某些处理方法
在操作系统中可以使用 kill -l 看到关于信号的列表并且有对应的解释说明
# 查看系统的信号
$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN1 36) SIGRTMIN2 37) SIGRTMIN3
38) SIGRTMIN4 39) SIGRTMIN5 40) SIGRTMIN6 41) SIGRTMIN7 42) SIGRTMIN8
43) SIGRTMIN9 44) SIGRTMIN10 45) SIGRTMIN11 46) SIGRTMIN12 47) SIGRTMIN13
48) SIGRTMIN14 49) SIGRTMIN15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 其中 [1, 31] 为普通信号注意没有 0 号信号最为常用后面 [34, 64] 为实时信号注意没有 32 和 33 信号比较少用除非是某些特殊的行业比如车载操作系统。特定信号的详细描述则可以使用指令 man 7 signal 来查看
# 查看特定信号的详细描述
Signal Value Action Comment
信号 值 行为 注释
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background processThe signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.Action 就是接收到信号后的行为有以下的默认行为和忽略行为默认的大部分都行为是终止进程
TermTermination表示终止或终结在 Linux 或类 Unix 系统中TERM 信号用于请求进程正常终止Corecore dump表示生成核心转储核心转储是操作系统在程序异常终止比如由于内存越界、除零等错误时生成的一个包含程序内存映像的文件这个文件可以帮助开发者诊断程序异常终止的原因说白了就是把进程在内存中运行的数据作拷贝后面有补充解释StopStop表示表示停止用于暂停一个进程的执行使其进入停止状态此时进程会被挂起ContContinue 在进程信号中通常表示继续执行这个信号用于恢复一个之前被停止的进程的执行当一个进程收到停止信号如 SIGSTOP而被暂停后可以通过发送 SIGCONT 信号使其继续执行IgnIgnored表示信号被忽略系统不会采取任何处理动作 补充“进程基础”的“进程等待”中我们曾经在返回型参数 status 中提到“…还有 1 个比特位是 core dump 标志这个我们之后讲信号再来谈及是否具备 core dump…”也就是这里提到的“核心转储”那么这个标志位有什么用呢 一般而言云服务器的核心转储功能是被关闭的我们可以使用命令 ulimit -a 来确认该命令显示当前 shell 进程的所有资源限制这其中包括硬件资源、用户资源以及各种其他限制其中就包括核心转储资源的限制。 您也可以选择使用 ulimit -c 10240 手动打开10240 是核心转储的设定大小该设定只在本次会话有效其他设定方法待补充…。 此时我们再发送给进程一些带有 Core 行为的信号就会发现一些不同对同一份代码做不同的信号发送 //核心转储的验证代码
#include iostream
#include signal.h
#include unistd.h
using namespace std;void CatchSig(int sigNum) //该回调函数被调用的时候会将编号传递给该函数的 sig
{cout 进程捕捉到信号 sigNum [pid]为: getpid() 正在处理... \n;
}int main()
{//signal(2, CatchSig); //当 SIGINT 被触发时就会调用后面的函数signal(SIGINT, CatchSig); //当 SIGINT 被触发时就会调用后面的函数特定信号的处理动作一般只有一个//而在循环开始前signal() 即使被调用了在没有接受到信号之前不会调用 CatchSig()//因此 signal() 仅仅是修改进程对特定信号的后续处理动作而不是直接调用对应的处理动作while(true){cout 我是进程run... pid: getpid() \n;sleep(1);}return 0;
}没有核心转储之前同目录下的两个会话/bash # bash1
$ g main.cpp
$ ./a.out
我是进程run...pid:13517
我是进程run...pid:13517
我是进程run...pid:13517
...
# -----------------
# bash2
$ kill -8 13517
# -----------------
# bash1
...
我是进程run...pid:13517
Floating point exception开启核心转储之后同目录下的两个会话/bash # bash1
$ ulimit -c 10240
$ ulimit -a
core file size (blocks, -c) 10240
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7902
max locked memory (kbytes, -l) unlimited
max memory size (kbytes, -m) unlimited
open files (-n) 100002
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7902
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
$ ./a.out
我是进程run...pid:15752
我是进程run...pid:15752
...
# -----------------
# bash2
$ kill -8 15752
$ ls
a.out core.15752 main.cpp
# -----------------
# bash1
...
我是进程run...pid:15752
Floating point exception (core dumped)可以看到 kill 后多了一个 core.15752 文件这个文件的大小比较大进程出异常后由 OS 将该进程在内存中存储的核心数据转存到该文件中因此该文件的主要意义是为了调试里面全都是二进制文本编辑器打开后就是乱码。 而我使用的是云服务下的 Centos7 在实际生产中云服务器一般就是生产环境当代码项目写完并且进行合并后在发布环境中编译运行然后交给测试团队在测试环境中测试因此一般默认关闭核心转储功能以提高空间利用效率。 如果我们使用 -g 选项编源代码意思是在编译过程中包含调试信息运行二进制文件后假设出现除零错误导致生成 core 文件则使用 gdb 后使用命令 core-file core文件名 就可以直接定位到出异常的地方。 # 使用 core 文件来事后调试core 文件的作用
$ cat main.cpp
#include iostream
#include signal.h
#include unistd.h
using namespace std;int main()
{
while(true)
{cout 我是进程run... pid: getpid() \n;sleep(1);int a 100;a / 0; //除零错误会引发 OS 发出 8 号 SIGFPE 信号cout run here... \n;
}
return 0;
}$ g main.cpp -g
main.cpp: In function ‘int main()’:
main.cpp:14:11: warning: division by zero [-Wdiv-by-zero]a / 0; //除零错误会引发 OS 发出 8 号 SIGFPE 信号^$ ./a.out
我是进程run...pid:21167
Floating point exception (core dumped)$ ls
a.out core.21167 main.cpp$ gdb a.out
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type show copying
and show warranty for details.
This GDB was configured as x86_64-redhat-linux-gnu.
For bug reporting instructions, please see:
http://www.gnu.org/software/gdb/bugs/...
Reading symbols from /home/ljp/LimouGitFile/limou-c-test-code/my_code_2023_12_20/a.out...done.
(gdb) core-file core.21167
[New LWP 21167]
Core was generated by ./a.out.
Program terminated with signal 8, Arithmetic exception.
#0 0x00000000004008aa in main () at main.cpp:14
14 a / 0; //除零错误会引发 OS 发出 8 号 SIGFPE 信号
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64因此我们遗漏的那一个标志位的作用就是判断是否有 core 动作是否发生了核心转储用来区分 Trem 的终止和 Core 的终止。 # 检验核心转储标志位
$ ls
main.cpp$ cat main.cpp
#include iostream
#include stdlib.h
#include unistd.h
#include wait.h
using namespace std;int main()
{pid_t id fork();if(id 0){//子进程sleep(1);int a 100;a / 0;exit(0);}int status 0;waitpid(id, status, 0);cout 父进程pid: getpid() \n 子进程pid id \n exit sig: (status 0x7F) \n is core?: ((status 7) 1) \n;return 0;
}$ g main.cpp
main.cpp: In function ‘int main()’:
main.cpp:16:11: warning: division by zero [-Wdiv-by-zero]a / 0;^$ ./a.out
父进程pid:28256
子进程pid28257
exit sig:8
is core?:1开启了核心转储功能后若有发生核心转储则该标记为设置为 1其他具有核心转储行为的信号也有类似的输出结果而若关闭了核心转储功能就绝不会将该标记设为 1 。 3.信号捕捉
3.1.信号捕捉接口
//系统调用 signal() 声明
#include signal.h
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//1.signum信号编号
//2.handler注册信号处理方式
//这里是使用回调的方式修改对应的信号处理方法而该函数返回值返回的是一个旧的函数指针也就是注册失败之前老的处理方法这个返回值很少使用要么返回旧处理方法的函数指针要么返回强转后的 SIG_ERR(错误)、SIG_DFL(默认)、SIG_IGN(忽略)另外该回调函数也被称为“捕捉方法”在 Linux 中signal() 用于设置信号处理程序允许程序对接收到的信号做出响应。信号本身是一种用于通知进程发生某些事件的软件中断机制其主要用途包括
捕获和处理信号通过 signal() 您可以为特定的信号如 SIGINT、SIGTERM 等注册自定义的信号处理程序。这样当进程接收到注册的信号时相应的处理函数将被调用允许程序执行特定的操作改变信号的行为有些信号具有默认、忽略的处理行为例如 SIGTERM 通常用于请求进程正常终止通过使用 signal()你可以改变默认的信号处理行为使其执行自定义的操作。 补充这也就是为什么 shell 进程不会被 [ctrlc] 简单的杀死。 怎么使用这个函数呢我们来尝试一下
//使用 signal() 调用
#include iostream
#include signal.h
#include unistd.h
using namespace std;void CatchSig(int sigNum) //该回调函数被调用的时候会自动将编号传递给该函数的 sig
{cout 进程捕捉到信号 sigNum [pid]为: getpid() 正在处理... \n;
}int main()
{//signal(2, CatchSig); //当 SIGINT 被触发时就会调用后面的函数signal(SIGINT, CatchSig); //当 SIGINT 被触发时就会调用后面的函数特定信号的处理动作一般只有一个//而在循环开始前signal() 即使被调用了在没有接受到信号之前不会调用 CatchSig()//因此 signal() 仅仅是修改进程对特定信号的后续处理动作而不是直接调用对应的处理动作//这也就是为什么称为“注册”而不是“调用”或者“设置”的原因前者使用起来更加准确注册不代表被立刻调用如果进程没有接受到该信号那么这个方法也就永远不会被调用while(true){cout 我是进程run... \n;sleep(1);}return 0;
}[ctrlc] 可以终止前台进程实际发送的是信号 2)SIGINT而这个进程运行起来后就不能使用 [ctrlc] 强行终止了包括使用 kill -2 进程ID 或者 kill SIGINT 进程ID 也都不能使用了但是我们可以使用 [ctrl/] 发送 3)SIGQUIT 信号进行退出。
3.2.信号捕捉细节
这里有几个问题需要我们思考 如何理解信号被进程保存起来呢 信号不就是一些值么进程要保存一个数据那还不简单只要内部要可以保存进程信号的相关数据结构就可以。而使用位图来保存就再合适不过了可以去 task_struct{/*...*/}; 里去找找。 如何理解信号被发送呢 由于发送信号的过程涉及到修改内核级别的数据结构因此只有操作系统自己才有资格进行改动。我们自己手动发送信号亦或者是其他来源的信号最后都是由操作系统向目标进程发/写信号修改 task_struct{/*...*/}; 指定的位图结构也只有操作系统有这个权力我们只不过是借助操作系统的“手”罢。 如何理解组合键 [ctrlz] 变成信号呢 键盘工作原理是中断机制操作系统识别到组合键后根据进程列表找到前台运行的进程然后写入发送对应信号到进程内部的位图结构中而至于信号什么时候被进程处理就交给进程自己衡量。 补充 1中断机制 中断的概念实际是计算机组成原理中常见的知识硬件或者软件进行 IO 操作时几乎都会触发中断机制。 当一个中断事件发生时CPU 会立即停止执行当前的指令序列保存当前执行上下文的状态然后转向处理中断的代码。处理中断的代码通常由操作系统内核中的中断服务例程ISRInterrupt Service Routine来处理。 在 x86 架构的计算机系统中中断的触发可以由硬件设备如时钟中断、键盘中断、硬盘中断等或软件通过软中断指令引起当然并不是所有的硬件设备或者软件都会有中断信号。 一旦中断被触发CPU 会暂停当前任务的执行跳转到中断服务例程的地址执行与中断相关的代码处理中断事件。处理完成后CPU 恢复之前的执行上下文继续执行原来的程序。 总体而言中断允许系统对异步事件进行响应而不需要轮询或等待提高了系统的实时性和效率也可防止一些危险行为。 而键盘输入就是通过不同键盘的中断机制调用键盘的中断代码处理得到了键盘发送的数据。这个过程很像信号或者说中断是软硬件层次的实现机制信号是纯软件层次的实现机制。甚至可以说信号是对中断的软件模拟。 另外操作系统的执行本身也是基于中断的硬件发出的中断用于向 CPU 发出请求要求 CPU 暂停当前正在执行的任务并转而执行特定的处理程序而加载操作系统的相关代码本质上是某些硬件不断触发中断信号迫使操作系统运行起来。 补充 28259 集成电路 该电路是一个可编程中断控制器PICProgrammable Interrupt Controller。Intel 8259 芯片是早期个人计算机和其他计算机系统中常见的中断控制器之一。 该芯片通常被用于处理外部设备的中断请求例如键盘、鼠标、计时器等。通过中断控制器计算机系统能够有效地管理和响应多个中断源确保正确地分发中断请求并允许程序员对中断的处理进行编程。 8259 芯片的主要特点和功能包括 多级中断优先级具有多个中断请求IRQ线每个中断线都有一个优先级。8259 可以配置为级联连接允许处理多个级别的中断请求。中断屏蔽允许通过设置中断屏蔽位来禁用或启用特定的中断源从而控制中断的响应。中断请求IRQ处理负责接收和处理外部设备发送的中断请求并将其传递给 CPU。级联连接可以连接多个 8259 芯片形成级联的中断控制体系从而支持更多的中断源。
3.3.信号捕捉疑惑
使用过 signal() 后这里有个问题我先提出来对所有信号进行自定义捕捉会怎么样我们可不可以让一个进程对所有的信号都实施同一种捕捉方法
4.信号发送
信号最终都是由操作系统发送的但这不代表信号是由操作系统产生的操作系统应该是一个第三号令者负责解析和将某些数据、特征、操作转化为信号发送进程。因此我们必须了解信号的产生条件。
4.1.通过终端按键产生信号
通过键盘的按键输入让 OS 解析按键组合和解析转化为信号然后发送信号给进程这我们已经用过很多次了不再过多介绍。 补充在 Linux 终端中您可以使用一些键盘快捷键来发送信号给运行中的程序。以下是一些常用的快捷键及其对应的信号 CtrlC发送 SIGINT 信号通常用于中断运行中的程序。CtrlZ发送 SIGTSTP 信号将运行中的程序挂起到后台并停止它的执行。Ctrl\发送 SIGQUIT 信号通常用于退出运行中的程序并生成 core dump 文件。CtrlD发送 EOFEnd-of-File信号通常用于表示输入结束如果在终端中输入则可用于退出终端。 这些快捷键可以在终端中使用用于与正在运行的程序进行交互。在上述快捷键中CtrlC 和 CtrlZ 是最常用的分别用于中断程序和挂起程序。这些快捷键的行为可能会受到终端设置和运行的 shell 的影响不同的 shell 和终端模拟器可能有不同的表现。这些快捷键实际就是调用了下文将要提到的 kill() 系统调用。 4.2.通过系统调用产生信号
4.2.1.发送信号给所有进程
我们之前在使用 kill 的时候实际上就是在命令行中发生信号而系统中也存在一个 kill() 的系统调用。
//系统调用 kill() 声明
#include signal.h
int kill(pid_t pid, int sig); //向目标进程发送 sig 信号实际上命令行里的 kill 指令底层就是 kill() 的调用我们可以自己模拟一个
//模拟实现 kill 命令
#include iostream
#include string
#include stdlib.h
#include signal.h
using namespace std;static void Usage(string proc)
{cout Usage:\r\n\t proc signumber processid ;
}
int main(int argc, char* argv[])
{if(argc ! 3){Usage(argv[0]);exit(1);}int signumber atoi(argv[1]);int procid atoi(argv[2]);kill(procid, signumber);return 0;
}4.2.2.发送信号给当前进程
还有一个系统调用是 raise()可以向调用该函数的进程发送信号也就是进程自己发送信号给自己。
//系统调用 raise() 声明
#include signal.h
int raise(int sig);
//向当前调用该函数的进程发送 sig 信号等同于 kill(getpid(), sig)4.2.3.发送中断信号给进程
另外还有一个让我们眼熟的系统调用 abort() 调用我们经常能在断言的时候看到它 //库函数 abort() 声明
#include stdlib.h
void abort(void);
//给进程自己发送确定的 abort 信号也就是 6)SIGABRT 信号自己终止自己那使用 abort() 终止进程和使用 exit()/_exit() 终止进程有什么区别呢有很大的一点在于6)SIGABRT 会进行 core 行为更加方便我们调试。
4.3.通过软件条件产生信号
4.3.1.管道软件条件
在我们后面要学习学习的管道中如果一直让写端写入管道而读端不仅不读甚至还把读端关闭了这样 OS 会通过发送信号 13)SIGPIPE 的方式终止写端进程我们可以自己尝试验证一下
//演示例子待补充...而操作系统因为管道这个软件之所以叫软件是因为管道是通过文件在内存的实现而由于管道的读端被关闭而写端依旧在写入因此软件条件不满足系统就发送了可以终止的信号。 注意管道是属于后面进程间通信的知识您可以等后续学习了管道再回来这里详细查看在管道章节也会提及这一细节。 4.3.2.闹钟软件条件
还有一种软件条件就是使用闹钟该闹钟由 alarm() 设定
//闹钟接口声明
#include unistd.h
unsigned int alarm(unsigned int seconds);
//seconds 秒后给当前进程发送 14)SIGALRM 信号默认是终止当前进程该函数如果 seconds 为零则表示取消当前运行的闹钟返回值剩下的秒数。
//如果再次调用了 alarm 函数设置了新的闹钟则后面定时器的设置将覆盖前面的设置即之前设置的秒数被新的闹钟时间取代因此只有最后一次调用 alarm() 所设置的时间参数会生效。//使用闹钟接口
#include iostream
#include unistd.h
#include signal.hunsigned int Seconds 100;
int n 0;void alarmHandler(int signum)
{n alarm(0);std::cout n std::endl;alarm(n); //重新设置闹钟并且 n 为上一个闹钟运行剩下的秒数
}int main()
{signal(SIGALRM, alarmHandler);alarm(Seconds);std::cout Alarm set for Seconds seconds std::endl;while (true){sleep(1);}return 0;
}
我们还可以通过这个调用来粗略计算 CPU 的性能
//计算 CPU 的性能
#include iostream
#include stdio.h
#include unistd.h
#include signal.h
using namespace std;unsigned long long count 0;void CatchSig(int sigNum) //该回调函数被调用的时候会将编号传递给该函数的 sig
{cout 捕捉到信号为: sigNum 本进程pid为: getpid() 结果为: count \n;
}int main(int argc, char* argv[])
{alarm(1);signal(SIGALRM, CatchSig);while(true) count;return 0;
}需要注意的是该闹钟被触发后就会被自动移除那如果需要定期完成怎么办呢重新设置一个就好
//计算 CPU 的性能
#include iostream
#include stdio.h
#include unistd.h
#include signal.h
using namespace std;unsigned long long count 0;void CatchSig(int sigNum) //该回调函数被调用的时候会将编号传递给该函数的 sig
{cout 捕捉到信号为: sigNum 本进程pid为: getpid() 结果为: count \n;alarm(1);
}int main(int argc, char* argv[])
{alarm(1);signal(SIGALRM, CatchSig);while(true) count;return 0;
}补充我们可以根据这个闹钟来实现一些定时的自动化操作比如自动提交和更新 git 仓库而 nohup 命令可以让程序忽略 SIGHUP 信号这样即使终端关闭程序也可以继续在后台运行 SIGHUP 是指 Hangup 信号它是 POSIX 系统中的一个标准信号。这个信号通常与终端相关联当用户从终端退出时终端会向正在其上运行的进程发送 SIGHUP 信号。在传统的 Unix 系统中当用户从终端注销或终端会话意外中断时会发送 SIGHUP 信号。 对于守护进程daemon或在后台运行的进程来说SIGHUP 信号意味着终端的断开通常会被解释为程序应该重新初始化自身或者重读配置文件等。因此它常常用于重新启动或重新加载进程。 4.4.通过硬件异常产生信号
操作系统是怎么发现除零错误的呢进行计算的是 CPU 硬件内部的加法器有很多寄存器其中就有一个除零状态寄存器记录本次的计算状态。
如果对应的除零错误的状态寄存器被设置为 1则证明出现除零错误由于在进行计算的是 CPU 这个硬件而不是操作系统则说明该异常是由硬件产生再被操作系统接受转化成信号发送给进程。
因此除零错误本质是硬件异常而非软件问题。
//篡改除零错误的处理行为
#include iostream
#include signal.h
#include unistd.h
using namespace std;void CatchSig(int signum)
{cout 拦截成功获得 sig: signum \n; //尽管发送了 3 号信号但是该进程没有被杀死再次调度恢复上下文时又会重复发送 3 号信号
}int main()
{signal(8, CatchSig);int a 10;a / 0; //硬件设置了除零标志但是发送信号后该上下文数据依旧被进程带走return 0;
}对于 C/C 程序员来说最为常见的硬件异常就是段错误导致 OS 转化该异常发出了 SIGSEGV 信号例如“野指针解引用再进行写入”这种行为可以在软硬件上有不同但本质一样的理解
硬件理解就是虚拟地址通过页表和内存管理单元硬件MMU转化为物理地址时发现物理地址的映射是错误的或者说没有权限因此设置好 MMU 寄存器/硬件电路由操作系统转化为异常信号来发送。软件理解就是内存通常被划分为不同的段例如代码段、数据段、栈段等。每个段都有其特定的权限例如读、写、执行等。如果程序试图执行不允许的内存访问操作操作系统会向程序发送一个 “段错误” 信号导致程序终止运行。 补充大部分异常都是为了让进程可以正常结束最多打印一些日志信息很少是可以在抛出异常后可以被解决的… 5.信号阻塞
在学习信号阻塞之前我们先来根据前面的内容得到几个关于进程信号的术语
信号递达Delivery实际执行信号的处理动作被称为“信号递达”信号未决Pending信号从产生到发生信号递达之间的状态被称为“信号未决”信号阻塞/信号屏蔽Block进程可以选择阻塞某个信号也就是“信号阻塞/信号屏蔽”被阻塞的信号产生时将会保持在信号未决状态知道进程解除对该进程的阻塞才可以执行信号递达的动作 注意需要区分好“阻塞”和“忽略”只要信号被阻塞就不会被递达而忽略是在递达之后的一种可选的信号处理动作。 5.1.内核表示
递达、未决、阻塞实际上可以表现在内核中进程 PCB 的 task_struct{/*...*/}; 内包含三个表 block_array{} 位图中的每一位代表对应的信号是否被阻塞为 1 代表该信号被阻塞pending_array{} 位图中的每一位代表有没有接受到对应的信号为 1 代表接收到某信号handler_array{} 表就是信号对应的处理行为/回调函数
实际上系统调用 signal() 的原理就是根据信号 sig以复杂度 O ( 1 ) O(1) O(1) 的速度找到对应的 handler_array{} 位图然后将对应的处理方法的函数指针填入该数组中这就完成了信号的自定义捕捉动作。
而在没有填充自定义的捕捉动作之前内部早就填写好了默认处理行为SIG_DFL和忽略处理行为SIG_IGN这两个宏实际上就是
//宏 SIG_DFL 和 SIG_IGN 的定义
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
//还有一个 SIG_ERR 就是 -1 的强转您可以查阅一下内核的代码因此检测进程的步骤就是
先检测是否接受到信号遍历 pending_array 是否有被设置若步骤 1 检测出有未决信号则检查该信号是否被阻塞检查 block_array{} 数组是否有被设置若步骤 2 检查到信号没被阻塞则执行信号的处理行为根据索引执行存储在 handler_array{} 中的回调函数
上述的三个表就可以对信号的接受进行检测只需要看到未决标志被设置即可这意味着发信号实际是对进程结构体对象的数据成员进行写入也可以做到进行存储来延后运行只需要设置阻塞标志即可这意味着所谓阻塞就是对进程结构体对象的数据成员进行写入。 补充上述描述只是针对普通信号如果对于实时信号将实时信号对应的编号和行为设置整体作为队列元素也是可行的… 5.2.接口调用
5.2.1.sigset_t 类型
在了解接口之前您首先需要知道 sigset_t 类型该类型是操作系统提供的可以理解为位图类型操作系统不允许程序员直接使用这个类型对位图进行修改但是可以使用接口让操作系统自己修改而我们接下来就会使用与之相关的两种接口
直接对位图本身进行操作的接口根据系统接口来完成特定的功能
从我们先前的理解来看每个信号只有一个 bit 表示未决标志、阻塞标志并不记录该信号产生、阻塞了多少次。
而这两个标志都可以分别用相同的数据类型 sigset_t 存储为一个“信号集”
在未决信号集pending set中有效和无效代表该信号是否处于未决状态在阻塞信号集block set中有效和无效代表该信号是否处于阻塞状态阻塞信号集也叫作当前进程的信号屏蔽字 signal mask这里的屏蔽一定要理解为阻塞的意思默认情况下进程不会对任何信号进行阻塞
5.2.2.信号集调用声明
//信号集接口调用
#include signal.h
int sigemptyset(sigset_t* set); //清空置为零
int sigfillset(sigset_t* set); //全设置为一
int sigaddset(sigset_t* set, int signo); //将特定信号添加到信号集中
int sigdelset(sigset_t* set, int signo); //将特定信号从信号集中删除
int sigismember(sigset_t* set, int signo); //判定信号是否在信号集中int sigpending(sigset_t* set); //获取当前调用进程的 pending 信号集合交给用户
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset); //检查并且更改阻塞信号集
//这里的 how 表示如何做:
//(1)SIG_BLOCK:把 set 中设置好的阻塞信号集添加到 oldset 中
//(2)SIG_UNBLOCK:把 set 中设置好的阻塞信号集从 oldset 中删去
//(3)SIG_SETMASK:把 oldset 替换为 set
//oldset 是输出型参数返回修改前的阻塞信号集可以利用这个做阻塞信号集恢复不使用的话可以填入 NULL注意信号集只是对信号的位图表示我们只能通过信号集来修改信号为阻塞而无法直接设置信号是否未决我们负责发送信号即可系统会自动帮我们写入的。 补充虽然我还没有看过这里位图底层实现但我大概猜测和我在 C 位图中的实现是类似的。 5.2.3.信号集接口测试
除了熟悉接口我们要解决一些问题
对所有信号进行自定义捕捉会怎么样如果对所有信号进行自定义捕捉那是不是就可以写出一个恶意程序也就是运行起来不被异常和指令杀掉的进程呢pending 位图是怎么变化的如果将 2)SIGINT 号信息进行阻塞并且不断获取当前进程的 pending 信号集如果我们突然发送 2)SIGINT 信号就应该可以看到 pending 信号集将对应的比特位置为 1对所有信号进行阻塞会怎么样如果我们将所有的信号都进行阻塞这样运行起来的进程哪怕是接受到信号也无法处理那这样是不是也写出了一个恶意程序呢
5.2.3.1.对所有信号进行自定义捕捉会怎么样
//篡改所有信号的处理行为
#include iostream
#include signal.h
#include unistd.h
using namespace std;void CatchSig(int signum)
{cout 拦截成功获得 sig: signum \n;
}int main()
{//更改所有的 handler 方法表for(int i 1; i 31; i)signal(i, CatchSig);//死循环运行while(true){cout 当前进程 pid: getpid() 运行 ing... \n;sleep(1);}return 0;
}我们可以看到除了 9)SIGKILL 信号其他的信号都能被修改处理行为为什么 9)SIGKILL 不可以修改呢因为该信号属于管理员级别的信号无法使用系统调用做修改因此这种恶意程序是不被操作系统允许的这也很合理。
5.2.3.2.pending 位图是怎么变化的
//观察 pending 的变化
#include iostream
#include signal.h
#include unistd.h
#include stdlib.h
#include stdio.h
#include cassert
using namespace std;void ShowPending(sigset_t pending)
{cout 进程 getpid() 打印一次 pending 集合 \n;for(int sig 1; sig 31; sig){if(sigismember(pending, sig))cout 1 ;elsecout 0 ;if(sig % 10 0)cout \n;fflush(stdout);}cout \n;
}int main()
{//1.定义两个新旧信号集sigset_t bset, obset;//2.初始化两个信号集sigemptyset(bset);sigemptyset(obset);//3.添加要阻塞/屏蔽的信号sigaddset(bset, 2); //2)SIGINT//4.设置屏蔽字int n sigprocmask(SIG_BLOCK, bset, obset); assert(n 0), (void)n;cout block 2)SIGINT 信号成功 \n;//5.重复打印 pending 信号集while(true){//5.1.获取 pending 信号集sigset_t pending;sigemptyset(pending);sigpending(pending);//5.2.打印 pending 信号集并且观察 2)SIGINT 信号是否被阻塞ShowPending(pending);sleep(3);}return 0;
}当我们发送 2)SIGINT 信号时就会修改位图。
# 观察 pending 信号集的打印结果
# bash1
$ ls
main.cpp
$ g main.cpp
$ ./a.out
block 2 号信号成功
进程 1916 打印一次 pending 集合0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
进程 1916 打印一次 pending 集合0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
进程 1916 打印一次 pending 集合0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
进程 1916 打印一次 pending 集合0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 # bash2
$ kill -2 1916# bash1
# ...
进程 1916 打印一次 pending 集合0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
# ...而我们还可以利用 obset 来恢复对 2)SIGINT 信号的阻塞。
//观察取消阻塞屏蔽后现象的代码1
#include iostream
#include signal.h
#include unistd.h
#include stdlib.h
#include stdio.h
#include cassert
using namespace std;void ShowPending(sigset_t pending)
{cout 进程 getpid() 打印一次 pending 集合 \n;for(int sig 1; sig 31; sig){if(sigismember(pending, sig))cout 1;elsecout 0;}cout \n;
}int main()
{//1.定义信号集sigset_t bset, obset;//2.初始化sigemptyset(bset);sigemptyset(obset);//3.添加要阻塞屏蔽的信号sigaddset(bset, 2); //SIGINT//4.设置内核中关于信号的结构int n sigprocmask(SIG_BLOCK, bset, obset);assert(n 0), (void)n;cout block 2)SIGINT 信号成功 \n;//5.重复打印 pending 信号集int count 0;while(true){//5.1.获取 pending 信号集sigset_t pending;sigemptyset(pending);sigpending(pending);//5.2.打印 pending 信号集ShowPending(pending);sleep(1);count;//5.3.恢复 pending 信号集解除 2)SIGINT 信号的屏蔽if(count 20){int n sigprocmask(SIG_UNBLOCK, bset, obset);assert(n 0), (void)n;cout ublock 2)SIGINT 信号成功 \n;}}return 0;
}但是为什么需要阻塞后进程就会被终止呢很简单2)SIGINT 信号是一个可以让进程终止的信号如果取消了阻塞进程检测到该信号未决后发生了信号递达因此就有直接终止了进程要想观察到打印出来的 pending 位图重新置为 0可以修改 2) SIGINT 信号的捕捉方法
//观察取消阻塞屏蔽后现象的代码2
#include iostream
#include signal.h
#include unistd.h
#include stdlib.h
#include stdio.h
#include cassert
using namespace std;void ShowPending(sigset_t pending)
{cout 进程 getpid() 打印一次 pending 集合 \n;for(int sig 1; sig 31; sig){if(sigismember(pending, sig))cout 1;elsecout 0;}cout \n;
}void handler(int sig)
{cout 捕捉信号: sig \n;
}int main()
{//0.修改 2 号信号的捕捉方法signal(2, handler);//1.定义信号集sigset_t bset, obset;//2.初始化sigemptyset(bset);sigemptyset(obset);//3.添加要阻塞屏蔽的信号sigaddset(bset, 2); //SIGINT//4.设置内核中关于信号的结构int n sigprocmask(SIG_BLOCK, bset, obset);assert(n 0), (void)n;cout block 2 号信号成功 \n;//5.重复打印 pending 信号集int count 0;while(true){//5.1.获取 pending 信号集sigset_t pending;sigemptyset(pending);sigpending(pending);//5.2.打印 pending 信号集ShowPending(pending);sleep(1);count;//5.3.恢复 pending 信号集解除 2 号信号的屏蔽if(count 20){int n sigprocmask(SIG_UNBLOCK, bset, obset);assert(n 0), (void)n;cout ublock 2 号信号成功 \n;}}return 0;
}
再次发送 2)SIGINT 信号就可以观察到 1 置为 0 的现象了。 补充 1这里有一个小问题直觉上应该是先解除对 2)SIGINT 信号的阻塞然后进行捕捉 # 关于打印顺序的异或
进程 5465 打印一次 pending 集合
0100000000000000000000000000000
捕捉信号:2
ublock 2 号信号成功
进程 5465 打印一次 pending 集合
0000000000000000000000000000000但是我们的代码输出是反着来的变成了先捕捉然后解除 2)SIGINT 信号的阻塞。这个的原因是什么呢只是我们代码书写顺序问题罢了只有调用完 sigprocmask() 后才能打印后面的提示信息因此可以将打印提示的代码调整到系统调用前面。 补充 2观察系统调用我们会发现没有对 pending 位图进行直接修改的接口只有对 block 位图进行直接修改的接口因为没有必要我们在发送信号的时候操作系统会帮助我们设置好 pending 位图。 5.2.3.3.对所有信号进行阻塞会怎么样
//对所有信号进行阻塞
#include iostream
#include signal.h
#include unistd.h
#include stdlib.h
#include stdio.h
#include cassert
using namespace std;void ShowPending(sigset_t pending)
{cout 进程 getpid() 打印一次 pending 集合 \n;for(int sig 1; sig 31; sig){if(sigismember(pending, sig))cout 1;elsecout 0;}cout \n;
}int main()
{//屏蔽所有信号sigset_t best;for(int sig 0; sig 31; sig){sigemptyset(best);sigaddset(best, sig);int n sigprocmask(SIG_BLOCK, best, nullptr);assert(n 0), (void) n;}//不断获取 pending 信号集并且打印sigset_t pending;while(true){sigpending(pending);ShowPending(pending);sleep(1);}return 0;
}可以使用一个简单的 shell 脚本来测试信号阻塞后 pending 位图的输出结果。
i1; id$(pidof a.out); while [ $i -le 31 ]; do kill -$i $id; echo send signal $i; let i; sleep 1; done我们发现 9)SIGKILL 信号依旧是比较特殊的我们无法对其进行阻塞/屏蔽那我们就重新设计一个 shell 脚本来跳过 9) SIGKILL 信号实际上 19)SIGSTOP 信号也一样是特殊的我们也将其屏蔽
# 不发送 9)SIGKILL 号和 19)SIGSTOP 信号给进程
namea.out
i1
id$(pidof $name)
while [ $i -le 31 ]
do if [ $i -eq 9 ];thenlet icontinuefiif [ $i -eq 19 ];thenlet icontinuefikill -$i $idecho send signal $i:kill -$i $idlet isleep 1
done最终结论9)SIGKILL 信号极度特殊我们无法使用接口将其阻塞和屏蔽。
6.信号原理 6.1.信号处理的时机
通过前面的学习我们还需要解答几个问题
信号产生后可能无法被立刻处理此时该信号就会被存储起来那么这个时候是什么时候又是怎么判定的信号处理的整个流程是怎么样的
首先我们要知道信号的相关数据字段一定在进程 PCB 内部也就是在内存里属于内核范畴的操作因此处理信号的时候就一定要在内核态的时候进行处理。而在内核态的时候就会把接受到的无法立即处理的信号存储起来返回用户态的时候再进行信号检测和信号处理。
其中如果一份代码在运行的过程中涉及到内核操作、进程调度、系统调用等操作时就处于内核状态而执行用户代码打印、循环等操作时就处于用户状态。
用户态是被 OS 管控的状态“管控”是指资源限制、权限限制内核态具备非常高的优先级。
那我们怎么细致区分两者呢还记得进程空间分为内核空间和用户空间不而我们之前提到的页表就是用户页表用户地址空间会通过用户页表映射到物理空间中。而还有一张内核级页表可以简单认为是被所有的进程共享的因为操作系统只有一份就可以将操作系统需要的代码和数据映射到物理空间中。
因此任何一个进程想要使用系统调用就需要用到系统调用的代码而系统调用的代一定再内核里一旦有了系统级的页表就可以映射到系统调用的实现代码因为进程内部就可以直接跳转到内核空间通过页表找到加载进内存的系统调用的代码。
因此每个进程想要调用系统调用就可以直接根据自己的进程地址空间和系统级页表来找到系统调用的实现代码。
CPU 内部就有寄存器来区分用户态和内核态。
无论进程如何切换每个进程都可以找到操作系统的代码。
6.2.执行自定义捕捉
信号处理最好理解的是默认和忽略这两者都在内核态交给操作系统自己调度即可最重要的反而是自定义的捕捉动作。操作系统检测到该信号有自定义的捕捉方法就会去执行用户自定义的捕捉方法。
那么问题来了此时处于什么状态内核态还是用户态答案是内核态因为做信号检测的动作都是操作系统。
那当前的状态能不能直接执行 user handler 方法呢能只要操作系统原因访问用户的代码时完全可以的但是也没有必要呢没必要为什么呢
如果自定义的处理行为有一些盗取数据、删除数据等极端操作那么操作系统就不能直接以内核态执行用户的代码操作系统无法相信用户的代码因此执行用户方法需要从内核态转为用户态来调用用户的自定义捕捉动作。
只有处于用户态将用户的代码执行完了才可以通过特殊的系统调用 sigreturn() 来重新跳转到对应的内核态。
6.3.执行回调前阻塞
//系统调用 sigaction() 声明
#include signal.h
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
//(1)signum:需要捕捉的对应信号
//(2)act:类型名字和函数名字可以一样这是一个输入型参数内部有回调函数
//(3)oldact:输出型函数曾经对该信号的旧的处理方法我们来细细研究一下 struct sigaction{/*...*/};。
//内核中关于 struct sigaction 的定义有删减和修改
struct sigaction
{void (*sa_handler)(int); //信号捕捉的回调函数指针类型sigset_t sa_mask; //重点是一个信号集/位图int sa_flags; //暂时不深入void (*sa_sigaction)(int, siginfo_t*, void*); //暂时不深入是实时信号的处理函数void (*sa_restorer)(void); //暂时不深入
};我们可以使用 sigaction() 实现和 signal() 一样类似的调用不过这个函数的使用您先跳过先看完后面有关阻塞的内容会提及两个位图后再回过头来看这个函数的使用。
//使用 sigaction() 做类似 signal() 的行为
#include iostream
#include signal.h
#include unistd.h
using namespace std;void handler(int signum)
{cout 获取信号: signum \n;sleep(10); //尝试在 10s 内不断发送 2 号信号
}
int main()
{signal(2, SIG_IGN);struct sigaction act, oact;act.sa_flags 0;sigemptyset(act.sa_mask);act.sa_handler handler;sigaction(2, act, oact);while (true){cout pid: getpid() \n 新捕捉方法: (int)(oact.sa_handler) \n 旧捕捉方法: (int)(act.sa_handler) \n;sleep(1);}return 0;
}那如果执行 handler 方法中又来了一个同类型的信号怎么办多个呢实际上在 Linux 里有个设计在任何时刻只能处理一层信号。
当某个信号的处理函数被调用后内核自动将当前信号加入进程的信号屏蔽字当信号处理函数返回时自动恢复原来的信号屏蔽字这样旧保证在处理某个信号的时候如果该种信号再次产生那么就会被阻塞到当前处理结束为止。
若在调用信号处理函数时除了当前信号被屏蔽还希望自动屏蔽别的信号则使用 sm_mask 字段说明这些需要额外屏蔽的信号当当前信号处理结束时才会自动恢复原理的信号屏蔽字。
因此 sigaction() 和 signal() 就产生了区别。
我们还可以写其他的代码来验证一下
//多次发送 2 号信号观察 pending 表的变化
#include iostream
#include signal.h
#include unistd.h
using namespace std;void ShowPending(sigset_t pending)
{cout 进程 getpid() 打印一次 pending 集合 \n;for(int sig 1; sig 31; sig){if(sigismember(pending, sig))cout 1;elsecout 0;}cout \n;
}void handler(int signum)
{cout 获取信号: signum \n;sigset_t pending;int count 5;while(count){sigpending(pending);ShowPending(pending);sleep(1);count--;}
}int main()
{signal(2, SIG_IGN);struct sigaction act, oact;act.sa_flags 0;sigemptyset(act.sa_mask);act.sa_handler handler;sigaction(2, act, oact);while (true){cout pid: getpid() \n 新捕捉方法: (int)(oact.sa_handler) \n 旧捕捉方法: (int)(act.sa_handler) \n;sleep(1);}return 0;
}还可以添加更多在调用完捕捉动作前被屏蔽的信号
//多次发送 2 号信号和其他信号观察 pending 表的变化
#include iostream
#include signal.h
#include unistd.h
using namespace std;void ShowPending(sigset_t pending)
{cout 进程 getpid() 打印一次 pending 集合 \n;for(int sig 1; sig 31; sig){if(sigismember(pending, sig))cout 1;elsecout 0;}cout \n;
}void handler(int signum)
{cout 获取信号: signum \n;sigset_t pending;int count 30;while(count){sigpending(pending);ShowPending(pending);sleep(1);count--;}
}int main()
{signal(2, SIG_IGN);struct sigaction act, oact;act.sa_flags 0;sigemptyset(act.sa_mask);act.sa_handler handler;sigaddset(act.sa_mask, 3);sigaddset(act.sa_mask, 4);sigaddset(act.sa_mask, 5);sigaddset(act.sa_mask, 6);sigaddset(act.sa_mask, 7);sigaction(2, act, oact);while (true){cout pid: getpid() \n 新捕捉方法: (int)(oact.sa_handler) \n 旧捕捉方法: (int)(act.sa_handler) \n;sleep(1);}return 0;
}
文章转载自: http://www.morning.xldpm.cn.gov.cn.xldpm.cn http://www.morning.gqcsd.cn.gov.cn.gqcsd.cn http://www.morning.mzskr.cn.gov.cn.mzskr.cn http://www.morning.qsy38.cn.gov.cn.qsy38.cn http://www.morning.gyqnc.cn.gov.cn.gyqnc.cn http://www.morning.czxrg.cn.gov.cn.czxrg.cn http://www.morning.ljzqb.cn.gov.cn.ljzqb.cn http://www.morning.pdmml.cn.gov.cn.pdmml.cn http://www.morning.tzlfc.cn.gov.cn.tzlfc.cn http://www.morning.ljzgf.cn.gov.cn.ljzgf.cn http://www.morning.ylrxd.cn.gov.cn.ylrxd.cn http://www.morning.zrkws.cn.gov.cn.zrkws.cn http://www.morning.rpdmj.cn.gov.cn.rpdmj.cn http://www.morning.nkjpl.cn.gov.cn.nkjpl.cn http://www.morning.ylkkh.cn.gov.cn.ylkkh.cn http://www.morning.kjyqr.cn.gov.cn.kjyqr.cn http://www.morning.mqss.cn.gov.cn.mqss.cn http://www.morning.vvdifactory.com.gov.cn.vvdifactory.com http://www.morning.qrqdr.cn.gov.cn.qrqdr.cn http://www.morning.sfwcx.cn.gov.cn.sfwcx.cn http://www.morning.dbcw.cn.gov.cn.dbcw.cn http://www.morning.nsrtvu.com.gov.cn.nsrtvu.com http://www.morning.rdsst.cn.gov.cn.rdsst.cn http://www.morning.jkrrg.cn.gov.cn.jkrrg.cn http://www.morning.wjrq.cn.gov.cn.wjrq.cn http://www.morning.mrxqd.cn.gov.cn.mrxqd.cn http://www.morning.kclkb.cn.gov.cn.kclkb.cn http://www.morning.dybth.cn.gov.cn.dybth.cn http://www.morning.fbnsx.cn.gov.cn.fbnsx.cn http://www.morning.kkwbw.cn.gov.cn.kkwbw.cn http://www.morning.2d1bl5.cn.gov.cn.2d1bl5.cn http://www.morning.sbyhj.cn.gov.cn.sbyhj.cn http://www.morning.nrpp.cn.gov.cn.nrpp.cn http://www.morning.qpfmh.cn.gov.cn.qpfmh.cn http://www.morning.fbylq.cn.gov.cn.fbylq.cn http://www.morning.zhqfn.cn.gov.cn.zhqfn.cn http://www.morning.hrtct.cn.gov.cn.hrtct.cn http://www.morning.tjsxx.cn.gov.cn.tjsxx.cn http://www.morning.fjzlh.cn.gov.cn.fjzlh.cn http://www.morning.kwksj.cn.gov.cn.kwksj.cn http://www.morning.fwblh.cn.gov.cn.fwblh.cn http://www.morning.dwwbt.cn.gov.cn.dwwbt.cn http://www.morning.lfdzr.cn.gov.cn.lfdzr.cn http://www.morning.rkdnm.cn.gov.cn.rkdnm.cn http://www.morning.mzzqs.cn.gov.cn.mzzqs.cn http://www.morning.pmlgr.cn.gov.cn.pmlgr.cn http://www.morning.dbfp.cn.gov.cn.dbfp.cn http://www.morning.wnjrf.cn.gov.cn.wnjrf.cn http://www.morning.mtzyr.cn.gov.cn.mtzyr.cn http://www.morning.smyxl.cn.gov.cn.smyxl.cn http://www.morning.kpgbz.cn.gov.cn.kpgbz.cn http://www.morning.nwpnj.cn.gov.cn.nwpnj.cn http://www.morning.fypgl.cn.gov.cn.fypgl.cn http://www.morning.snnwx.cn.gov.cn.snnwx.cn http://www.morning.hcsnk.cn.gov.cn.hcsnk.cn http://www.morning.tslxr.cn.gov.cn.tslxr.cn http://www.morning.qywfw.cn.gov.cn.qywfw.cn http://www.morning.ywtbk.cn.gov.cn.ywtbk.cn http://www.morning.lhjmq.cn.gov.cn.lhjmq.cn http://www.morning.qbfqb.cn.gov.cn.qbfqb.cn http://www.morning.kjrp.cn.gov.cn.kjrp.cn http://www.morning.cyysq.cn.gov.cn.cyysq.cn http://www.morning.wmglg.cn.gov.cn.wmglg.cn http://www.morning.ggjlm.cn.gov.cn.ggjlm.cn http://www.morning.sskhm.cn.gov.cn.sskhm.cn http://www.morning.pprxs.cn.gov.cn.pprxs.cn http://www.morning.dbnrl.cn.gov.cn.dbnrl.cn http://www.morning.hdrsr.cn.gov.cn.hdrsr.cn http://www.morning.hsrch.cn.gov.cn.hsrch.cn http://www.morning.zyslyq.cn.gov.cn.zyslyq.cn http://www.morning.rwfp.cn.gov.cn.rwfp.cn http://www.morning.ymqfx.cn.gov.cn.ymqfx.cn http://www.morning.dybth.cn.gov.cn.dybth.cn http://www.morning.rknjx.cn.gov.cn.rknjx.cn http://www.morning.nytqy.cn.gov.cn.nytqy.cn http://www.morning.kgphc.cn.gov.cn.kgphc.cn http://www.morning.dmwck.cn.gov.cn.dmwck.cn http://www.morning.tpdg.cn.gov.cn.tpdg.cn http://www.morning.tqpnf.cn.gov.cn.tqpnf.cn http://www.morning.smyxl.cn.gov.cn.smyxl.cn