如何在后台做网站分页,国内推广平台,dede网站安装教程,wordpress交易系统目录
一、信号
1.1、生活中的信号
1.2、Linux中的信号
二、信号处理常见方式
三、信号的产生
3.1、简单理解信号的保存和发送
3.2、键盘产生信号
3.3、核心转储
3.4、系统调用接口产生信号
3.4.1、kill
3.4.2、raise
3.4.3、abort
3.5、软件条件产生信号
3.6、硬…目录
一、信号
1.1、生活中的信号
1.2、Linux中的信号
二、信号处理常见方式
三、信号的产生
3.1、简单理解信号的保存和发送
3.2、键盘产生信号
3.3、核心转储
3.4、系统调用接口产生信号
3.4.1、kill
3.4.2、raise
3.4.3、abort
3.5、软件条件产生信号
3.6、硬件异常产生信号
四、信号的保存
4.1、相关概念
4.2、信号保存——三个数据结构
4.3、信号集——sigset_t
4.3、信号操作函数 一、信号
1.1、生活中的信号 在生活中我们很容易能够想到常见的一些信号。比如红绿灯手机闹钟上下课铃声转向灯等等。我们人不仅能够识别它们还能够知道不同的信号对应的下一步动作应该怎么做。比如红灯停绿灯行上课铃响就上课下课铃响就下课转向灯告诉别人我要转的方向。 那么我们是怎么识别并知道这些信号并且知道信号发出后接下来的动作应该是怎么样的呢首先这当然是规定过的交通部门规定了红灯停绿灯行而如果交通部门规定红灯行绿灯停那么我们也就只能照做。其次我们从出生开始大人们就不断告诉我们要红灯停绿灯行久而久之我们就记住了特定场景下的信号以及后续我们需要做到动作并且终身不忘。 而且即使我们没有在过马路而是在吃饭我们也能够知道应该如何处理红绿灯信号。 再比如如果我的9点的闹钟响了但是我没有立即起床而是30分钟后再起床。这就说明当信号产生的时候我们不一定会立即执行后续动作但是我记住了闹钟响过了我该起床了后面我再执行起床的动作。 上面就是一些生活中的信号以及我们对待信号的方式。下面我们就来看看Linux中的信号。
1.2、Linux中的信号 什么是Linux信号 Linux信号本质是一种通知机制是用户或者操作系统通过发送一定的信号来通知进程某件事已经发生你可以后续对其进行处理。 Linux信号的特点 结合上面生活中的信号的特点Linux信号有如下特点 a. 进程能够识别信号即能够看到信号发送给了自己并且知道后续的处理动作。 b. 进程能够识别信号已经由Linux设计者提前设计好了并且规定了各种信号的后续处理动作。 c. 信号的产生是随机的信号产生时进程可能正在做自己的事所以进程不一定会立即对信号进行处理。 d. 因为进程不一定立即处理信号所以进程一定能够将信号记住后续再进行处理。 e. 进程会在合适的时候处理信号什么时候合适后面会讲。 g. 一般而言信号的产生相对于进程是异步的。 信号查看我们可以通过 kill -l 命令查看Linux中有哪些信号 其中1~31号信号是普通信号34~64是实时信号。我们在平时使用中使用的最多的是普通信号。 二、信号处理常见方式
为了方便后面的讲解我们首先了解一下信号处理的常见方式
1、执行该信号的默认处理动作进程自带的Linux设计者写好的逻辑。
2、用户自己提供一个信号处理函数要求在进行信号处理时使用用户自己定义的方式处理信号这种方式称为捕捉Catch一个信号。
3、忽略该信号。
我们可以通过 man 7 signal 查看信号的默认处理动作 value信号编号 action默认处理动作。
三、信号的产生
3.1、简单理解信号的保存和发送 为了下面我们讲解信号的产生这里我们先简单地理解一下信号的保存。 前面讲到过信号产生后进程不一定会立即处理信号而是在之后的某个合适的时间对信号进行处理。所以在这中间的一段时间里我们必须对信号进行保存。 对于保存进程只需要知道是否有这个信号就可以对信号进行处理所以我们可以使用位图来对信号进行保存。0就代表该比特位对应的信号没有产生1就代表产生了该信号。这样在之后进程只需要遍历一遍位图就可以知道产生了哪些信号然后进行处理。 该位图在进程的PCB中属于内核数据只有操作系统能够修改所以信号的发送就是os把信号对应的比特位的数字由0改成1。 当然关于信号的保存和发送我们会在下面的内容中进行详细的讲解这里只是有一个概念。
3.2、键盘产生信号 在之前讲进程等待时我们知道使用 Ctrl c 的组合键能够终止一个进程而且我们也讲了其本质就是通过向进程发送2号信号来终止进程的。下面我们就来证明一下 我们使用自定义函数将信号进行捕捉signal
#include signal.h
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);RETURN VALUE
signal() returns the previous value of the signal handler, or SIG_ERR on error. In the event of an error, errno is set to indicate the cause.
#include iostream
#include signal.h
#include unistd.husing namespace std;void catchsig(int signum)
{cout 进程捕捉到了一个信号 signum pid getpid() endl;
}int main()
{signal(SIGINT, catchsig);while (true){cout 我是一个进程我正在运行 pid getpid() endl;sleep(1);}return 0;
} 通过对比上面两张图我们发现Ctrl c 和发送2号命令都调用了我们自定义的处理动作。所以 Ctrl c的本质就是发送2号命令。 3.3、核心转储 上面的一张图在信号的默认动作action中term表示只终止进程而还有的信号的动作是core这个动作不仅会终止进程还可以发生核心转储。这个与我们前面的进程等待的内容又有些关联了。 上图是进程等待中父进程获取子进程信息的status位图结构。低7位保存信号之前有一个core dump标志该比特位表示是否发生了核心转储。 核心转储当进程出现某种异常时是否由os将当前进程在内存中的相关核心数据转存到磁盘中。 一般来说云服务器上的核心转储功能是被关闭了的。而我们可以使用ulimit -a 命令查看core文件ulimit -c 大小 命令打开云服务器的核心转储功能。 那么核心转储有什么作用呢我们使用下面的代码来看看
#include iostream
#include signal.h
#include unistd.husing namespace std;void catchsig(int signum)
{cout 进程捕捉到了一个信号 signum pid getpid() endl;
}int main()
{signal(SIGQUIT, catchsig);while (true){cout 我是一个进程我正在运行 pid getpid() endl;sleep(1);int a 100;a / 0;}return 0;
} 运行代码后生成了core文件且以进程pid为后缀。 我们知道程序出错了而有了core文件后我们不用去一行一行找出错位置使用core文件在gdb下可以直接定位出错位置如下 3.4、系统调用接口产生信号
3.4.1、kill
NAMEkill - send signal to a processSYNOPSIS#include sys/types.h#include signal.hint kill(pid_t pid, int sig);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):kill(): _POSIX_C_SOURCE 1 || _XOPEN_SOURCE || _POSIX_SOURCE 其实我们常常使用的kill命令的底层所调用的就是该函数下面我们可以模拟实现一下 kill命令的实现。
#include iostream
#include cassert
#include sys/types.h
#include signal.husing namespace std;static void Usage(const string proc)
{cout \nUsage: proc pid signo\n endl;
}// ./mykill 2 pid
int main(int argc, char *argv[])
{if (argc ! 3){Usage(argv[0]);exit(1);}int signo atoi(argv[1]);int sigpid atoi(argv[2]);int n kill(sigpid, signo);assert(n 0);return 0;
}
3.4.2、raise
作用进程让os给自己发送某一个信号。
NAMEraise - send a signal to the callerSYNOPSIS#include signal.hint raise(int sig);DESCRIPTIONThe raise() function sends a signal to the calling process or thread. In a single-threaded program it is equivalent tokill(getpid(), sig);
#include iostream
#include cassert
#include unistd.h
#include sys/types.h
#include signal.hint main()
{cout 我开始运行了 endl;sleep(2);raise(2);return 0;
}
3.4.3、abort
作用让os给自己发一个6号信号。其实abort的底层也是去调用 raise6去实现的。
NAMEabort - cause abnormal process terminationSYNOPSIS#include stdlib.hvoid abort(void);
#include iostream
#include cassert
#include unistd.h
#includestdlib.h
#include sys/types.h
#include signal.husing namespace std;int main()
{cout 我开始运行了 endl;sleep(2);abort();return 0;
} 所以总的来说系统调用接口产生信号的具体过程就是 用户调用系统接口——os执行对应的代码——os向目标进程写入信号——修改信号对应的比特位——进程后续对信号进行处理。
3.5、软件条件产生信号 ~ 管道 在前面的进程间通信的管道中我们讨论了一个问题对于正在通信的两个进程当管道的读端不读了而且读端关闭了但是写端一直在写。这时写就没有任何意义了。我们验证了在这个时候os会通过发送13号信号的方式终止进程。因为管道是一个通过文件在内存级的实现所以管道是一个软件所以这种情况就是软件条件不满足而产生信号的一种情况。 ~ 设置闹钟 alarm #include unistd.h
unsigned int alarm(unsigned int seconds); 调用alarm函数可以设定一个闹钟也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号该信号的默认处理动作是终止当前进程。这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。闹钟一旦触发了将会自动移除。
我们可以使用该函数写一个能够测试自己的电脑CPU的计算能力的代码
#include iostream
#include cassert
#include unistd.h
#include stdlib.h
#include sys/types.h
#include signal.husing namespace std;int count 0;void sigcath(int sig)
{cout final count: count endl;
}int main()
{alarm(1);signal(SIGALRM, sigcath);while (true)count;return 0;
} 我们也可以写一个代码来让os帮助我们每隔1秒就可以显示cout最新的计算结果
#include iostream
#include cassert
#include unistd.h
#include stdlib.h
#include sys/types.h
#include signal.husing namespace std;uint64_t count 0;void sigcath(int sig)
{cout final count: count endl;alarm(1);
}int main()
{alarm(1);signal(SIGALRM, sigcath);while (true)count;return 0;
}
3.6、硬件异常产生信号 ~ 除0错误 我们来看一看下面的代码 #include iostream
#include cassert
#include unistd.h
#include stdlib.h
#include sys/types.h
#include signal.husing namespace std;void hander(int sig)
{cout 我捕捉了一个信号 sig endl;sleep(1);
}int main()
{signal(SIGFPE, hander);int a 100;a / 0;return 0;
} 运行结果如下 我们知道了如果代码中出现了除0错误os会给进程发送8号信号那么是怎么产生并发送的呢 首先计算以及各种信息的处理是由CPU这个硬件进行的。CPU中有一个寄存器叫做状态寄存器它含有一个位图该位图上有溢出标记位。 CPU在进行计算时发现代码中出现了除0错误因此将溢出标记位由0改为1进程异常CPU将该进程切出。os会自动进行计算完成后检测状态寄存器当检查到溢出标记位为1时os就会提取当前正在运行的进程的PID给其发送8号信号。 那么为什么会是死循环打印呢 上面讲到溢出标记位由0改为1后CPU就会将该进程切出因为寄存器里面的数据是该进程的上下文所以位图也会跟随进程一起切出。但是我们虽然将信号进行了捕捉但是并没有让进程退出所以这个进程只是被切出去了当CPU正常进行调度时再次调度该进程上下文恢复上去os立马识别到了溢出标记位还是1再次打印如此反复。 所以为了解决这个问题我们要在捕捉函数最后加上 exit让进程退出。 ~ 野指针和越界访问 我们知道指针变量必须通过地址才能找到目标位置。而我们语言上的地址是虚拟地址所以我们前面讲了通过页表将物理地址和虚拟地址建立联系。但是事实上我们是通过页表MMUmemory manger unit一个硬件的方式将物理地址和虚拟地址建立联系的所以当代码中出现了野指针或者越界访问时因为这是一个非法地址那么MMU一定会报错它会将自己内部的寄存器进行标识os就能够检测到且知道是哪个进程的地址转化出错了。 四、信号的保存
4.1、相关概念
a. 信号递达进程对信号的处理动作称为信号递达。
b. 信号未决信号从产生到递达之间的这个状态称为信号未决。
c. 信号阻塞被阻塞的信号产生时将保持在未决状态直到进程解除对此信号的阻塞才执行递达的动作。
阻塞和忽略是不同的只要信号被阻塞就不会递达而忽略是在递达之后可选的一种处理动作。
4.2、信号保存——三个数据结构 前面我们讲到在进程的PCB中存在一种位图是用来保存信号的但是事实上有3种数据结构与信号是相关的。他们分别是pending位图block位图typedef void(*handler_t)(int signo)handler_t handler[32]{0}结构。 pending位图该位图就是我们常说的用来保存信号的位图。 block位图该位图比特位的位置与信号标号一一对应比特位的内容代表该信号是否阻塞。 typedef void(*handler_t)(int signo)handler_t handler[32]{0}这个是一个函数指针数组这个数组在内核中有指针指向它这个数组称为当前进程所匹配的信号递达的所有方法数组下标代表信号的编号数组的每一个元素都是一个函数指针存函数地址指向信号对应的处理方法。 4.3、信号集——sigset_t 上面讲到的三个结构都是属于进程PCB是内核数据结构。所以os必定不会让用户直接访问这三个结构更不能够让用户直接进行位移操作。那么如果用户想要得到pending和block位图该怎么办呢于是Linux就提供了一种数据类型信号集——sigset_t用户可以直接使用。
4.3、信号操作函数 既然Linux提供了信号集那么必定也通过了与之相关的各种方法让用户能够去操作这样用户根本就不需要关系在内核中这些结构到底是怎么样的。下面的5个函数就是对信号集进行操作的函数。
#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 sigismemberconst sigset_t *set, int signo);
sigpending获取当前进程的 pending 信号集。信号发送的本质就是对pending位图进行修改。
NAMEsigpending - examine pending signalsSYNOPSIS#include signal.hint sigpending(sigset_t *set);读取当前进程的未决信号集通过set参数传出。调用成功则返回0出错则返回-1。
sigprocmask 读取或更改进程的信号屏蔽字(阻塞信号集) block
NAMEsigprocmask - examine and change blocked signalsSYNOPSIS#include signal.hint sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
下表说明了how参数的可选值及其作用
选项作用SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号SIG_SETMASK设置当前信号屏蔽字为set所指向的信号
注9号信号是不能被捕捉或阻塞的。