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

信息网站开发网络公司有没有专门做兼职的网站

信息网站开发网络公司,有没有专门做兼职的网站,网站如何合理建设seo,备案网站分布地点目录 一、信号概念 二、信号的作用 三、信号的特性 四、信号捕捉初识 五、信号产生 #xff08;一#xff09;通过终端按键产生信号 #xff08;二#xff09;硬件中断 #xff08;三#xff09;系统调用产生信号 1. kill 函数 2. raise 函数 3. abort 函数 …目录 一、信号概念 二、信号的作用 三、信号的特性 四、信号捕捉初识 五、信号产生 一通过终端按键产生信号 二硬件中断 三系统调用产生信号 1. kill 函数 2. raise 函数 3. abort 函数 四由软件条件产生信号 1. alarm 设置闹钟 2. 测试算力 五硬件异常产生信号 1. 除 0 导致异常 2. 状态寄存器 3. 野指针导致异常 六核心转储 1. 核心转储概念 2. 打开关闭核心转储 3. 核心转储作用 六、信号保存 一概念 二具象理解 三在内核中的表示 四sigset_t 信号集 五信号集操作函数 1. sigset_t 类型的操作函数 2. sigprocmask 3. sigpending 七、信号处理 一信号的处理时机 二何为合适的时机  三用户态和内核态 1. 概念 2. 内核态与用户态的转化 3. 重谈进程地址空间 四信号的处理流程 五信号的捕捉 1. 内核如何实现信号的捕捉 2. 信号捕捉函数 - sigcation 八、小结 一、信号概念 信号 是信息传递的承载方式一种信号往往代表着一种执行动作用来通知进程系统中发生了一个某种类型的事件。比如 鸡叫  天快亮了闹钟  起床、完成任务红绿灯  红灯停绿灯行 信号是多种多样的并且一个信号对应一个事件这样才能知道收到一个信号后到底是一个什么事件应该如何处理这个信号。 当然这些都是生活中的 信号当产生这些 信号 时我们会立马想到对应的 动作 这是因为 我们认识并能处理这些信号。 但是对于计算机来说是死的不具有意识常态。于是程序员们给操作系统植入了一批 指令一个指令表示一种特殊动作而这些指令就是 信号进程信号。 通过 kill -l 查看当前系统中的信号集合表 这些就是当前系统中的 进程信号一共62个其中1-31号信号为 普通信号用于 分时操作系统剩下的34-64号信号为 实时信号用于 实时操作系统。二者区别 实时信号产生之后立刻处理。多个实时信号一起产生时会被记录下来不会丢失。普通信号在短时间内只保存有无信号产生在后续合适时间里再处理有可能会丢失。 分时操作系统根据时间片实行公平调度适用于个人电脑以及手机系统。实时操作系统高响应适合任务较少、需要快速处理的平台比如汽车车机、火箭发射控制台。 普通信号只保存它有无产生实时信号可以保持很长时间。 因为我们的系统属于 分时操作系统所以本篇博客只介绍普通信号不涉及实时信号。 当然也不是全部研究部分信号只做了解即可。 二、信号的作用 在之前也是略微使用过了信号 kill -9 pid   终止进程运行kill -19 pid 暂停进程运行kill -18 pid 恢复进程运行 就连常用的 ctrl c 和 ctrl d 热键本质上也是 信号。 查看信号对应的信息指令 man 7 signal具体功能可以查阅本篇博客 Linux中的31个普通信号 三、信号的特性 进程信号由 信号编号 执行动作 构成一个信号对应一种动作对于进程来说动作无非就这几种终止进程、暂停进程、恢复进程3 个信号就够用了啊为什么要搞这么多信号 创造信号的目的不只是控制进程还要便于管理进程进程的终止原因有很多种如果一概而论的话对于问题分析是非常不友好的所以才会将信号细分化搞出这么多信号目的就是为了方便定位、分析、解决问题。 并且 普通信号 就 31 个这就是意味着所有普通信号都可以通过位图数据结构存储在一个 int 中表示是否收到该信号信号的保存 所以信号被细化了不同的信号对应不同的执行动作虽然大部分最终都是终止进程。 信号有这么多个并且多个进程可以同时产生多个信号操作系统为了管理先描述、再组织在 PCB 中增加了 信号相关的数据结构signal_struct在这个结构体中必然存在一个 位图结构 uint32_t signals 存储 1~31 号信号的有无信息 //信号结构体源码部分 struct signal_struct {atomic_t sigcnt;atomic_t live;int nr_threads;wait_queue_head_t wait_chldexit; /* for wait4() *//* current thread group signal load-balancing target: */struct task_struct *curr_target;/* shared signal handling: */struct sigpending shared_pending;/* thread group exit support */int group_exit_code;/* overloaded:* - notify group_exit_task when -count is equal to notify_count* - everyone except group_exit_task is stopped during signal delivery* of fatal signals, group_exit_task processes the signal.*/int notify_count;struct task_struct *group_exit_task;/* thread group stop support, overloads group_exit_code too */int group_stop_count;unsigned int flags; /* see SIGNAL_* flags below *//** PR_SET_CHILD_SUBREAPER marks a process, like a service* manager, to re-parent orphan (double-forking) child processes* to this process instead of init. The service manager is* able to receive SIGCHLD signals and is able to investigate* the process until it calls wait(). All children of this* process will inherit a flag if they should look for a* child_subreaper process at exit.*/unsigned int is_child_subreaper:1;unsigned int has_child_subreaper:1;//…… };信号的处理方式 进程的对于信号的执行动作是可自定义的默认为系统预设的 默认动作  执行默认动作即操作系统给信号设定的默认动作忽略信号执行自定义动作用户修改了操作系统设定的默认动作改成了自己想要的动作,操作系统为我们提供一个信号处理函数signal,可以要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch) 一个信号。 进程在没有收到信号时就已经知道了一个信号应该怎么被处理了这说明进程能够识别并处理信号。信号对于进程来说是随时都有可能产生的因此进程与信号是异步的当信号产生时进程可能正在执行优先级更高的事情这时进程并不能立即处理信号需要在合适的时候再进行处理因此在这个空窗期内信号要能够被保存起来这说明进程具有记录信号的能力进程记录的信号可能有很多个因此进程需要用一种数据结构去管理所有的信号在Linux下对于信号的管理采用的是位图结构比特位的位置代表信号的编号。所以所谓的发送信号本质就是直接修改特定进程的信号位图中的特定的比特位。由0 - 1进程信号的位图结构本质还是属于task_struct里面的数据因此对于进程信号的位图结构里面的数据的修改只能有操作系统来完成即无论有多少种信号产生的方式最终都必须让OS来完成最后的发送过程!信号并不是立即处理的它会在合适的时间段进行统一处理 所以 进程信号 可以分为三步信号产生 — 信号保存 — 信号处理  四、信号捕捉初识 signal 函数可以用来 修改信号的执行动作也叫注册自定义执行动作 signal 调用成功返回上一个执行方法的值其实就是下标后面介绍失败则返回 SIG_ERR并设置错误码。 参数1 就是信号编号为 int 单纯地传递 信号名也是可以的因为信号名其实就是信号编号的宏定义。 参数2 是一个函数指针意味着需要传递一个 参数为 int 返回值为空的函数对象。 参数 int 是执行动作的信号编号 void handler(int) //其中的函数名可以自定义显然signal 函数是一个 回调函数当信号发出时会去调用相应的函数也就是执行相应的动作 我们先对 2 号信号注册新动作在尝试按下 Ctrl C看看它发出的究竟是不是 2 号信号 void handler(int signo) {cout catch a signal signo endl; }int main() {signal(2, handler);while (true){cout 我是一个进程我正在运行..... | pid: getpid() endl;sleep(1);}return 0; } 当我们修改 2 号信号的执行动作后再次按下 Ctrl C尝试终止前台进程结果失败了执行动作变成了我们设定的新动作而不是原先的终止进程。 这足以证明 Ctrl C 就是在给前台进程发出 2 号信号Ctrl C 失效后可以通过 Ctrl   \ 终止进程发出的是 3 号信号3 号信号在发出后会生成 核心转储 文件 普通信号只有 31 个如果把所有普通信号的执行动作都改了会发生什么呢难道会得到一个有着 金刚不坏 之身的进程吗 void handler(int signo) {cout catch a signal signo endl; }int main() {// 给所有普通信号设定自定义方法for(int i1;i32;i) signal(i, handler);while (true){cout 我是一个进程我正在运行..... | pid: getpid() endl;sleep(1);}return 0; } 大部分信号的执行动作都被修改了但 9 号信号没有因为 9 号信号是 SIGKILL专门用于杀死进程只要是进程他都能干掉 。 19 号信号 SIGSTOP 也无法修改执行动作所以前面说过9 号 SIGKILL 和 19 号 SIGSTOP 信号是很特殊的经过特殊设计不能修改其执行动作  五、信号产生 一通过终端按键产生信号 通俗来说就是命令行操作。 在Linux下输入命令可以在Shell下启动一个前台进程当我们想要终止一个前台进程时我们可以按下 Ctrl C 来进行终止这个前台进程其实这个 Ctrl C 也是一个信号它对应的信号的2号信号SIGINT这个信号对应的默认处理动作就是终止当前的前台进程。 示例一段死循环代码 #include iostream #include unistd.h using namespace std;int main() {while(true){cout 我是一个进程我正在运行…… | PID: getpid() endl;sleep(1);}return 0; } 用户按下 Ctrl C这个键盘输入产生一个硬件中断 被OS获取解释成信号发送给目标前台进程前台进程因为收到信号进而引起进程退出。 Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl C 这种控制键产生的信号。Ctrl C 终止的是当前正在运行的前台进程如果在程序运行时加上  表示让其后台运行成为后台进程此时会发现无法终止进程 像这种后台进程 Ctrl C 是无法终止的可以通过 kill -9 PID 发出 9 信号终止它。 二硬件中断 当我们从键盘按下  Ctrl C 时发生了这些事CPU 获取到键盘 “按下” 的信号调用键盘相应的 “方法” 从键盘中读取数据读取数据后解析然后发出 2 号信号。 其中 CPU 捕获键盘 “按下” 信号的操作称为 硬件中断。 CPU 中有很多的针脚不同的硬件对应着不同的针脚每一个针脚都有自己的编号硬件与针脚一对一相连并通过 中断控制器比如 8259进行控制当我们按下键盘后 中断控制器首先给 CPU 发送信息包括键盘对应的针脚号然后 CPU 将获取到的针脚号中断号写入 寄存器 中最后根据 寄存器 里的 中断号去 中断向量表 中查表找到对应硬件的方法执行它的读取方法就行了 这样 CPU 就知道是 键盘 发出的信号然后就会去调用 键盘 的执行方法通过键盘的读取方法读取到 Ctrl C  这个信息转化后就是 2 号信号执行终止前台进程的动作。 键盘被按下 和 键盘哪些位置被按下 是不一样的 首先键盘先按下CPU 确定对应的读取方法其次才是通过 读取方法 从键盘中读取数据 注键盘读取方法如何进行读取这是驱动的事我们不用关心 硬件中断 的流程与 进程信号 的流程相同同样是 先检测到信号然后再去执行相应的动作不过此时发送的是 中断信号执行的是 调用相应方法罢了。 信号 与 动作 的设计方式很实用操作系统只需要关注是否有信号发出发出后去中断向量表中调用相应的方法即可不用管硬件是什么样、如何变化做到了 操作系统 与 硬件 间的解耦。 三系统调用产生信号 1. kill 函数 返回值成功返回 0失败返回 -1 并设置错误码 参数1待操作进程的 PID 参数2待发送的信号 下面来简单用一下程序运行 5 秒后自己把自己杀死 #include iostream #include unistd.h #include signal.h using namespace std;int main() {int n 1;while(true){cout 我是一个进程运行了 n 秒 | PID: getpid() endl;sleep(1);n;if(n 5) kill(getpid(), SIGKILL);}return 0; } kill 函数当然也可以发送其他信号这里就不一一展示了其实命令行中的 kill 命令就是对 kill 函数的封装kill 信号编号 PID 其中的参数2、3不正是 kill 函数所需要的参数吗所以我们可以尝试自己搞一个 mykill 命令 #include iostream #include string #include cstdlib #include cerrno #include cstring #include unistd.h #include signal.h #include sys/types.h using namespace std;void Usage(const std::string proc) {cout Usage: std::endl;cout proc 信号编号 目标进程 std::endl; }int main(int argc, char *argv[]) {// 参数严格限制if (argc ! 3){Usage(argv[0]);exit(-1);}pid_t pid atoi(argv[2]);int signo atoi(argv[1]);int return_val kill(pid, signo);if(return_val -1) {cout 错误码 errno 错误信息 strerror(errno) endl;}return 0; }2. raise 函数 发送信号的还有一个 raise 函数这个函数比较奇怪只能 自己给自己发信号 返回值成功返回 0失败返回 非0 就只有一个参数待发送的信号 可以这样理解raise 是对 kill 函数的封装每次传递的都是自己的 PID int main() {sleep(1);cout 我要被暂停了我的PID是 getpid() endl;raise(19);cout 我要继续运行了我的PID是 getpid() endl;return 0; } 我们用 raise 函数给当前进程发送暂停信号 19 SIGTOP暂停以后我们可以在命令行中给进程发送继续运行 18 SIGCONT 信号 3. abort 函数 abort 是 C 语言提供的一个函数它的作用是 给自己发送 6 号 SIGABRT 信号 abort 函数使当前进程接收到信号而异常终止abort 函数其实是向进程发送6号信号SIGABRT 就像exit函数一样abort 函数总是会成功的所以没有返回值值得注意的是就算6号信号被捕捉了调用abort 函数还是会退出进程。 int main() {int n 1;while (true){cout 我是一个进程已经运行了 n 秒 PID: getpid() endl;sleep(1);n;if(n 5) abort();}return 0; } 同样是终止进程C语言 还提供了一个更好用的函数exit所以 abort 用的比较少了解即可. 总的来说系统调用中举例的这三个函数关系是kill 包含 raise raise 包含 abort 作用范围是在逐渐缩小的。 四由软件条件产生信号 其实这种方式我们之前就接触过了管道读写时如果读端关闭那么操作系统会发送信号终止写端这个就是 软件条件 引发的信号发送发出的是 13 号 SIGPIPE 信号。这里主要介绍 alarm 函数和SIGALRM信号。 1. alarm 设置闹钟 系统为我们提供了 闹钟报警alarm 这个 闹钟 可不是用来起床的而是用来 定时 的 返回值如果上一个闹钟还有剩余时间则返回剩余时间否则返回 0 参数想要设定的时间单位是秒 调用alarm 函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发14号信号SIGALRM信号, 该信号的默认处理动作是终止当前进程。 int main() {alarm(5); // 设定一个五秒的闹钟int n 1;while (true){cout 我是一个进程已经运行了 n 秒 PID: getpid() endl;sleep(1);n;}return 0; } 闹钟默认是一次性的如果想要多次使用则可以使用自举的方式更改 14 号 SIGALRM 信号的执行动作让闹钟不断响起自举  void handler(int signo) {cout catch a signal: signo endl;int n alarm(10);cout 上一个闹钟剩余时间: n endl; }int main() {signal(SIGALRM, handler);alarm(10);// 设定一个十秒的闹钟while (true){cout 我是一个进程我正在运行..... | pid: getpid() endl;sleep(1);}return 0; } 提前结束闹钟会返回上次设定闹钟的剩余时间 系统中不止一个闹钟所以 OS 需要 先描述再组织将这些闹钟管理起来。 2. 测试算力 可以借助闹钟简单测试一下当前服务器的算力。如何简单粗暴的测试算力 设个 1 秒后响起的闹钟看看程序能将一个值累加至多少 int main() { // 测试算力alarm(1);int n 0;while(true) {cout n endl;}return 0; } 可以看到累加到了十几万次这个值是不固定的。其实还可以累加到更多主要是因为当前程序涉及了 IO这是非常耗时间的可以取消 IO并修改 SIGALRM 的执行动作为打印变量看看能累加多少次 int n 0;void handler(int signo) {cout n endl;exit(1); }int main() { // 测试算力signal(SIGALRM, handler);alarm(1);while(true) n;return 0; } 可以看到取消 IO 后累加了 5 亿多次接近 4000 倍的差距。 通过这个简单的小程序证明了一件事IO 是非常慢的能不 IO 就不 IO  注因为当前是云服务器存在 网络延迟 的影响所以实际差异更大 注意 闹钟是一次性的只能响一次 五硬件异常产生信号 硬件异常产生信号是指硬件产生了错误并以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如在写程序最常遇到的各种报错比如 除 0、野指针。 1. 除 0 导致异常 当前进程执行了除以0的指令CPU的运算单元会产生异常内核将这个异常解释为SIGFPE信号发送给进程。 int main() {// 除0异常int a 10;a / 0;return 0; } 在编译的时候我们收到了一个警告除0问题然后我们不管接着运行我们的代码然后我们的程序就崩溃了系统提示是浮点异常问题其实这个浮点异常问题对应的就是我们的硬件异常它对应的信号是8号信号SIGFPE。 我们是否可以设定自定义方法让除0异常合法 void handler(int signo) {cout 程序除0但是不终止进程 endl;// exit(1); }int main() {// 除0异常signal(SIGFPE, handler);int a 10;a / 0;return 0; }结果一直在死循环似的发送信号明明只发生了一次 除 0 行为 想要明白背后的原理需要先认识一下 状态寄存器。 2. 状态寄存器 在 CPU 中存在很多 寄存器其中大部分主要用来存储数据信息用于运算除此之外还存在一种特殊的 寄存器 》 状态寄存器这个 寄存器 专门用来检测当前进程是否出现错误行为如果有就会把 状态寄存器位图结构中对应的比特位置 1意味着出现了 异常。 比如上面的 除 0 代码发生异常后CPU 将 状态寄存器 修改变成 异常状态操作系统检测到 异常 后会向进程发送 8 号信号即使我们将原来 8 号信号默认的终止进程动作修改了打印动作但 因为状态寄存器比特位仍然为1处于异常状态所以操作系统才会不断发送  8 号信号所以才会死循环式的打印。 能让 状态寄存器 变为 异常 的都不是小问题需要立即终止进程然后寻找、解决问题。 毕竟如果让 除 0 变为合法那最终的结果是多少呢所以操作系统才会不断发送信号目的就是 终止进程的运行。 3. 野指针导致异常 int main() {int* ptr nullptr;*ptr 10;return 0; }系统提示我们发生了段错误对于野指针问题其实也是我们进程收到了操作系统发送的信号而崩溃的这个信号是11号信号SIGSEGV而这一次硬件异常的是MMU单元内存管理单元 野指针问题主要分为两类 指向不该指向的空间权限不匹配比如只读的区域偏要去写 由于我们进程使用的地址都是虚拟地址当我们进程的代码实际被执行时需要进行虚拟地址到物理地址的转换而这个转换就要借助 MMU 这个硬件来进行转换 指向不该指向的空间这很好理解就是页表没有将 这块虚拟地址空间 与 真实物理地址空间 建立映射关系此时进行访问时 MMU 识别到异常于是 MMU 直接报错操作系统识别到 MMU 异常后向对应的进程发出终止信号。提一嘴C语言中对于越界 读 的检查不够严格属于抽查行为因此野指针越界读还不一定报错但越界写是一定会报错的。权限不匹配页表中除了保存映射关系外还会保存该区域的权限情况比如 是否命中 / RW 等权限。当发生操作与权限不匹配时比如 nullptr 只允许读取并不允许其他行为此时解引用就会触发 MMU 异常操作系统识别到后同样会对对应的进程发出终止信号。 页表中的属性 是否命中RW 权限UK 权限不必关心 所以对于0地址可能操作系统根本没有给0地址建立映射关系或者建立了映射关系但是操作系统不会允许0地址处发生写入而当我们进行*p 10时是需要进行写入的MMU 在地址转换时发现权限不一致进而引发给异常报告给了操作系统然后操作系统向我们的的进场发送SIGSEGV信号。 总结一旦引发硬件层面的问题操作系统会直接发信号立即终止进程。 六核心转储 1. 核心转储概念 Linux中提供了一种系统级别的能力当一个进程在出现异常的时候OS可以将该进程在异常的时候核心代码部分进行 核心转储即将内存中进程的相关数据全部 dump 到磁盘中一般会在当前进程的运行目录下形成 core .pid 这样的二进制文件核心转储 文件。 对于某些信号来说当终止进程后需要进行 core dump 产生核心转储文件 比如3号 SIGQUIT、4号 SIGILL、5号 SIGTRAP、6号 SIGABRT、7号 SIGBUS、8号 SIGFPE、11号 SIGSEGV、24号 SIGXCPU、25号 SIGXFSZ、31号 SIGSYS 都是可以产生核心转储文件的。 不同信号的动作Action Trem- 单纯终止进程Core- 先发生核心转储生成核心转储文件前提是此功能已打开再终止进程 但在前面的学习中我们用过 3、6、8、11 号信号都没有发现 核心转储 文件啊 难道是我们的环境有问题吗 确实当前环境确实有问题因为它是 云服务器而 云服务器 中默认是关闭核心转储功能的。 2. 打开关闭核心转储 通过指令 ulimit -a 查看当前系统中的资源限制情况 ulimit -a可以看到当前系统中的核心转储文件大小为 0即不生成核心转储文件 通过指令手动设置核心转储文件大小 ulimit -c [指定大小]现在可以生成核心转储文件了 就拿之前的 野指针 代码测试因为它发送的是 11 号信号会产生 core dump 文件 核心转储文件是很大的而有很多信号都会产生核心转储文件所以云服务器一般默认是关闭的 云服务器上是可以部署服务的一般程序发生错误后需要立刻被云服务器中的检测程序发现并及时重启。 如果打开了核心转储一旦程序 不断挂掉、又不断重启每一次进程挂掉都会生成一个 core 文件进而导致硬盘占满导致系统 IO 异常最终会导致整个服务器挂掉的。 还有一个重要问题是 core 文件中可能包含用户密码等敏感信息不安全。 关闭核心转储很简单设置为 0 就好了 ulimit -c 03. 核心转储作用 此大的核心转储文件有什么用呢 答案是 调试 没错核心转储文件可以调试并且直接从出错的地方开始调试 这种调试方式叫做 事后调试 调试方法 gcc / g 编译时加上 -g 生成可调试文件运行程序生成 core-dump 文件gdb 程序 进入调试模式core-file core.file 利用核心转储文件快速定位至出错的地方 之前在 进程这一篇章中我们谈到了 当进程异常退出时被信号终止不再设置退出码而是设置 core dump 位 及 终止信号  父进程可以借此判断子进程是否产生了 核心转储 文件当系统生成 core 文件时标志位就被置 1 否则被置 0 。 六、信号保存 信号从产生到执行并不会被立即处理这就意味着需要一种 “方式” 记录信号是否产生对于 31 个普通信号来说一个 int 整型就足以表示所有普通信号的产生信息了信号还有可能被 “阻塞”对于这种多状态、多结果的事物操作系统会将其进行描述、组织、管理这一过程称为 信号保存 阶段。          一概念 信号 传递过程信号产生 - 信号未决 - 信号递达 信号产生Produce由四种不同的方式发出信号。信号未决 Pending信号从产生到递达之间的状态。信号递达Delivery进程收到信号后实际执行信号的处理动作。 在这三种过程之前均有可能出现 信号阻塞 的情况 信号阻塞 是一种手段可以发生在 信号处理 前的任意时段。进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态直到进程解除对此信号的阻塞才执行递达的动作。 注意阻塞和忽略是不同的只要信号被阻塞就不会递达而忽略是在递达之后可选的一种处理动作。二者的效果差不多什么都不干但前者是 干不了后者则是 不干了需要注意区分。 二具象理解 将 信号传递 的过程比作 网上购物 可以抽象出以下概念 信号产生在某某购物平台上下达了订单信号未决订单下达后快递的运输过程信号递达快递到达驿站后你对于快递的处理动作信号阻塞快递运输过程中堵车了 只要你下单了你的手机上肯定会有 物流信息未决信息已记录当 快递送达后信号递达物流记录 不再更新。 而 堵车 是一件不可预料的事情也就是说在下单后快递可能一会儿送达没有阻塞可能五天送达阻塞 - 解除阻塞有可能永不送达因为快递可能永远堵车阻塞。 堵车也有可能在你下单前发生信号产生前阻塞。 至于 信号递达后的处理动作 如何理解呢 快递送达后正常拆快递默认动作快递送达后啥也不干就是玩忽略快递送达后直接把快递退回去用户自定义 当然用户自定义的情况可以有很多种也有可能是直接把快递扔了。 综上网购的整个过程可以看作 信号传递过程本文探讨的是 信号保存阶段即 物流信息。 三在内核中的表示 对于传递中的信号来说需要存在三种状态表达 信号是否阻塞信号是否未决信号递达时的执行动作 在内核中每个进程都需要维护这三张与信号状态有关的表block 表、pending 表、handler 表在操作系统内核中有三张表两张是位图结构block 和 pending 一张是函数指针数组结构。 block表该位图结构里面的对应位置的比特位是否为1代表了该信号是否被阻塞。pending表该位图结构里面的对应位置的比特位是否为1代表了该信号是否是未决状态。handler表该表里面存放的是函数指针对应下标里面的函数指针表示收到该信号要调用的函数是哪一个。 如何记录信号已产生 - 未决表中对应比特位置置为 1 对于信号的状态修改其实就是修改 位图 中对应位置的值0/1。 假设已经获取到了信号的 pending 表只需要进行位运算即可pending | (1 (signo - 1))其中的 signo 表示信号编号-1 是因为信号编号从 1 开始需要进行偏移 如果想要取消 未决 状态也很简单pending (~(1 (signo - 1))) 至于 阻塞 block表与 pending 表 一模一样。 对于上图的解读 整个三张表里面数据在逻辑上是横向传递的。每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。 SIGHUP 信号未被阻塞未产生一旦产生了该信号pending 表对应的位置置为 1当信号递达后执行动作为默认。SIGINT 信号被阻塞已产生pending 表中有记录此时信号处于阻塞状态无法递达一旦解除阻塞状态信号递达后执行动作为忽略该信号。SIGQUIT 信号被阻塞未产生即使产生了也无法递达除非解除阻塞状态执行动作为自定义。 信号在 产生 之前可以将其 阻塞信号在 产生 之后未决依然可以将其 阻塞 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理? POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的常规信号在递达之前产生多次只计一次而实时信号在递达之前产生多次可以依次放在一个队列里。  信号里面的SIG_DFL表示的是执行默认动作SIG_IGN表示的是执行忽略动作。它们的定义如下  /* Type of a signal handler. */ typedef void (*__sighandler_t) (int);/* Fake signal functions. */ #define SIG_ERR ((__sighandler_t) -1) /* Error return. */ #define SIG_DFL ((__sighandler_t) 0) /* Default action. */ #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */默认动作就是将 0 强转为函数指针类型忽略动作则是将 1 强转为函数指针类型分别对应 handler 函数指针数组表中的 0、1 下标位置除此之外还有一个 错误 SIG_ERR 表示执行动作为 出错。 四sigset_t 信号集 无论是 block 表 还是 pending 表都是一个位图结构依靠 除、余 完成操作为了确保不同平台中位图操作的兼容性将信号操作所需要的 位图 结构封装成了一个结构体类型其中是一个 无符号长整型数组 /* A sigset_t has a bit for each signal. */# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct{unsigned long int __val[_SIGSET_NWORDS];} __sigset_t;#endif注_SIGSET_NWORDS 大小为 32所以这是一个可以包含 32个 无符号长整型 的数组而每个 无符号长整型 大小为 4 字节即 32比特至多可以使用 1024 个比特位。 sigset_t 是信号集未决和阻塞标志可以用相同的数据类型 sigset_t 来存储可以通过信号集操作函数进行获取对应的信号集信息信号集 的主要功能是表示每个信号的 “有效” 或 “无效” 状态在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 block 表 通过信号集称为 阻塞信号集或信号屏蔽字屏蔽表示阻塞pending 表 通过信号集中称为 未决信号集 如何根据 sigset_t 位图结构进行比特位的操作 假设现在要获取第 127 个比特位首先定位数组下标对哪个数组操作127 / (8 * sizeof (unsigned long int)) 3求余获取比特位对哪个比特位操作127 % (8 * sizeof (unsigned long int)) 31对比特位进行操作即可 假设待操作对象为 XXX置 1XXX._val[3] | (1 31)置 0XXX._val[3] (~(1 31)) 所以可以仅凭 sigset_t 信号集对 1024 个比特位进行任意操作。 五信号集操作函数 对于 信号 的 产生或阻塞 其实就是对 block 和 pending 两张表的 增删改查 增| 操作将比特位置为 1删 操作将比特位置为 0改| 或  操作灵活变动查判断指定比特位是否为 1 即可 1. sigset_t 类型的操作函数 sigset_t 类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现从使用者的角度是不必关心的,使用者只能调用以下系统接口来操作sigset_t 变量,而不应该直接操作它的内部数据 #include signal.hint sigemptyset(sigset_t *set); //初始化信号集 int sigfillset(sigset_t *set); //初识化信号集 int sigaddset(sigset_t *set, int signum); //增 int sigdelset(sigset_t *set, int signum); //删 int sigismember(const sigset_t *set, int signum); //查 函数sigemptyset初始化 set 所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。函数sigfillset初始化 set 所指向的信号集,使其中所有信号的对应bit置位1,表示 该信号集的有效信号包括系统支持的所有信号。函数sigaddset是将 set 所指向的信号集里面信号signum对应的比特位置为1函数sigdelset是将 set 所指向的信号集里面信号signum对应的比特位置为0sigismember是一个布尔函数,用于判断一个信号集 set 中是否包含signum信号,若包含则返回1,不包含则返回0,出错返回-1。注意在使用sigset_t t类型的变量之前一定要调用sigemptyset或sigfillset做初始化使信号集处于确定的状态。初始化sigset_t 变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。 这些函数都是 成功返回 0失败返回 -1 至于参数非常简单无非就是 待操作的信号集变量、待操作的比特位。 2. sigprocmask sigprocmask调用函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。 返回值成功返回 0失败返回 -1 并将错误码设置 参数1对 屏蔽信号集 的操作 参数    功能SIG_BLOCK    set 包含了我们希望添加到当前信号屏蔽字的信号相当于mask mask | setSIG_UNBLOCK    set 包含了我们希望从当前信号屏蔽字中解除阻塞的信号相当于mask (~set)SIG_SETMASK    设置当前进程的 block 表为 set 信号集中的 block 表相当于 mask set 参数2就是一个信号集主要从此信号集中获取屏蔽信号信息。 参数3也是一个信号集是一个输出型参数系统在给block信号集设置新的信号集时会将老的信号集的内容提取出来将拷贝到oldset里面。相当于给你操作后反悔的机会 演示程序1将 2 号信号阻塞尝试通过 键盘键入 发出 2 信号 #include iostream #include cassert #include unistd.h #include signal.h using namespace std;int main() {sigset_t set, oset;// 初始化信号集sigemptyset(set);sigemptyset(oset);// 阻塞2号信号sigaddset(set, 2);// 设置当前进程的 block 表sigprocmask(SIG_BLOCK, set, oset);// 死循环while (true){cout 我是一个进程正在运行 endl;sleep(1);}return 0; } 显然当 2 号信号被阻塞后是 无法被递达 的进程也就无法终止了. 演示程序2在程序运行五秒后解除阻塞状态打印当前进程的信号屏蔽字  #include iostream #include cassert #include unistd.h #include signal.h using namespace std;static void showBlock(const sigset_t *set) {int signo 1;for (; signo 31; signo){if(sigismember(set, signo)) cout 1;else cout 0;}cout endl; }int main() {// 创建信号集sigset_t set, oset;// 初始化信号集sigemptyset(set);sigemptyset(oset);// 阻塞2号信号sigaddset(set, 2);//设置进入进程谁调用设置谁sigprocmask(SIG_SETMASK, set, oset);//1、2号信号没有反应//2、老的block位图应该是全0cout 老的block位图是;showBlock(oset);int cnt 1;while (true){cout 目前的block位图是;showBlock(set);if(cnt 5){// 恢复原来的block位图sigprocmask(SIG_SETMASK, oset, set);}sleep(1);}return 0; } 现象在 2 号信号发出、程序运行五秒解除阻塞后信号才被递达进程被终止但为什么没有打印原来的block位图这个问题稍后再说。 如何证明信号已递达 当 n 5 时解除阻塞状态程序立马结束并只打印了 五条 语句证明在第五秒时程序就被终止了至于如何进一步证明需要借助 未决信号表 3. sigpending 对于pending表我们无法修改只能通过系统调用进行读取当前进程的未决信号集。 返回值成功返回 0失败返回 -1 并将错误码设置 参数待获取的 未决信号集 如何根据 未决信号集 打印 pending 表 使用函数 sigismember 判断当前信号集中是否存在该信号如果存在输出 1否则输出 0如此重复将 31 个信号全部判断打印输出即可 static void showPending(const sigset_t pending) {int signo 1;for (; signo 31; signo){if(sigismember(pending, signo)) cout 1;else cout 0;}cout endl; }int main() {// 创建信号集sigset_t set, oset;// 初始化信号集sigemptyset(set);sigemptyset(oset);// 1、阻塞2号信号sigaddset(set, 2);sigprocmask(SIG_SETMASK, set, oset);int cnt 1;// 2、获取进程的pending信号集合while (true){// 获取进程的 未决信号集sigset_t pending;sigemptyset(pending);int n sigpending(pending);assert(n 0);(void)n;cout 目前pending位图是;showPending(pending);if(cnt 5){// 恢复原来的pending位图sigprocmask(SIG_SETMASK, oset, set);}sleep(1);}return 0; } 结果当 2 号信号发出后当前进程的 pending 表中的 2 号信号位被置为 1表示该信号属于 未决 状态并且在五秒之后阻塞结束信号递达进程终止。 疑问当阻塞解除后信号递达应该看见 pending 表中对应位置的值由 1 变为 0但为什么没有看到即原来的pending表。现象同上述的block表一样。 很简单因为当前 2 号信号的执行动作为终止进程进程都终止了当然看不到解决方法给 2 号信号先注册一个自定义动作即可别急着退出进程 #include iostream #include cassert #include unistd.h #include signal.h using namespace std;static void showBlock(const sigset_t *set) {int signo 1;for (; signo 31; signo){if(sigismember(set, signo)) cout 1;else cout 0;}cout endl; }static void showPending(const sigset_t pending) {int signo 1;for (; signo 31; signo){if(sigismember(pending, signo)) cout 1;else cout 0;}cout endl; }static void handler(int signo) {cout signo 号信号已经递达了 endl; }int main() {// 修改2号信号的执行动作signal(2, handler);// 创建信号集sigset_t set, oset;// 初始化信号集sigemptyset(set);sigemptyset(oset);// 1、阻塞2号信号sigaddset(set, 2);sigprocmask(SIG_SETMASK, set, oset);int cnt 1;// 2、获取进程的pending信号集合while (true){// 获取进程的 未决信号集sigset_t pending;sigemptyset(pending);int n sigpending(pending);assert(n 0);(void)n;cout 目前pending位图是;showPending(pending);if(cnt 5){// 恢复原来的pending位图cout 恢复原来的信号屏蔽字;sigprocmask(SIG_SETMASK, oset, set);showPending(pending);}sleep(1);}return 0; } 先将信号 阻塞信号发出后无法 递达始终属于 未决 状态当阻塞解除后信号可以 递达信号处理之后未决表中不再保存信号相关信息因为已经处理了。 综上信号在发出后在处理前都是保存在 未决表 中的 注意 针对信号的 增删改查 都需要通过 系统调用 来完成不能擅自使用位运算sigprocmask、sigpending 这两个函数的参数都是 信号集前者是 屏蔽信号集后者是 未决信号集在对 信号集 进行增删改查前一定要先初始化信号在被解除 阻塞状态 后很快就会 递达 了 七、信号处理 一信号的处理时机 分两种情况 普通情况指 信号没有被阻塞直接产生记录未决信息后再进行处理 在这种情况下信号是不会被立即递达的也就无法立即处理需要等待合适的时机。 特殊情况当信号被 阻塞 后信号 产生 时记录未决信息此时信号被阻塞了也不会进行处理当阻塞解除后信号会被立即递达此时信号会被立即处理。  二何为合适的时机  信号的产生是 异步 的 也就是说信号可能随时产生当信号产生时进程可能在处理更重要的事此时贸然处理信号显然不够明智。因此信号在 产生 后需要等进程将 更重要 的事忙完后合适的时机才进行 处理。 合适的时机进程从 内核态 返回到 用户态 时会在操作系统的指导下对信号进行检测及处理。至于处理动作分为默认动作、忽略、用户自定义。 我们需先熟悉何为内核态跟用户态才能明白合适的时机 三用户态和内核态 1. 概念 用户态执行用户所写的代码时就属于 用户态 内核态执行操作系统的代码时就属于 内核态 自己写的代码被执行很好理解操作系统的代码是什么 操作系统也是一款软件而且是一款专注于搞管理的软件也是由大量代码构成的。在对进程进行调度、执行系统调用、异常、中断、陷阱等都需要借助操作系统之手。此时执行的就是操作系统的代码此时进程便处于内核态。 也就是说用户态 与 内核态 是两种不同的状态必然存在相互转换的情况 2. 内核态与用户态的转化 用户态 切换为 内核态 当进程时间片到了之后进行进程切换动作调用系统调用接口比如 open、close、read、write 等产生异常、中断、陷阱等 内核态 切换为 用户态 进程切换完毕后运行相应的进程系统调用结束后异常、中断、陷阱等处理完毕 信号的处理时机就是 内核态 切换为 用户态也就是 当把更重要的事做完后进程才会在操作系统的指导下对信号进行检测、处理。 3. 重谈进程地址空间 进程地址空间 是虚拟的依靠 页表MMU机制 与真实的地址空间建立映射关系并且每个进程都有自己的 进程地址空间不同 进程地址空间 中地址可能冲突但实际上地址是独立的。 在以前我们只讨论了[0, 3]G的用户空间并没有对[3, 4]G的内核空间进行讨论。在谈论用户空间时提到用户空间的地址要经过页表映射到物理地址这个用户空间的页表其实其真实名称是用户级页表。                                                         对于内核空间来说也有一张页表也负责将内核空间的地址映射到物理地址中这个页表的名称是内核级页表。 为什么要区分 用户态 与 内核态 内核空间中存储的可是操作系统的代码和数据权限非常高绝不允许随便一个进程对其造成影响。区域的合理划分也是为了更好的进行管理。 内核空间里面存放的是操作系统代码和数据 所以执行操作系统的代码及系统调用其实就是在使用这 1 GB 的内核空间。 进程间具有独立性比如存在用户空间中的代码和数据是不同的难道多个进程需要存储多份 操作系统的代码和数据 吗 当然不用内核空间比较特殊所有进程最终映射的都是同一块区域也就是说进程只是将 操作系统代码和数据 映射入自己的 进程地址空间 而已。而 内核级页表 不同于 用户级页表专注于对 操作系统代码和数据 进行映射是很特殊的。 当我们执行诸如 open 这类的 系统调用 时会跑到 内核空间 中调用对应的函数而 跑到内核空间 就是 用户态 切换为 内核态 了。用户空间切换至内核空间 由于操作系统的代码和数据是不能够被轻易访问的所以在代码中如果要执行操作系统的代码和数据需要先进行状态转化由用户态转化为内核态才能成功执行那么这个状态转换是怎么实现的呢 在 CPU 中存在一个 CR3 寄存器这个 寄存器 的作用就是用来表征当前处于 用户态 还是 内核态 当寄存器中的值为 3 时表示正在执行用户的代码也就是处于用户态。当寄存器中的值为 0 时表示正在执行操作系统的代码也就是处于内核态。 通过一个 寄存器表征当前所处的 状态修改其中的 值就可以表示不同的 状态 重谈 进程地址空间 后得到以下结论 所有进程的用户空间 [0, 3] GB 是不一样的并且每个进程都要有自己的 用户级页表 进行不同的映射。所有进程的内核空间 [3, 4] GB 是一样的每个进程都可以看到同一张内核级页表从而进行统一的映射看到同一个 操作系统。无论进程如何切换[3,4]GB不变看到的都是OS的内容与进程切换无关也就是说进程切换其实切换的是[0, 3]G的用户空间里面的内容和用户级页表。操作系统运行的本质其实就是在该进程的 内核空间内运行的。最终映射的都是同一块区域系统调用 的本质其实就如同调用动态库中的函数通过内核空间中的地址进行跳转调用。 那么进程又是如何被调度的呢 操作系统的本质 - 操作系统也是软件啊并且是一个死循环式等待指令的软件。 - 存在一个硬件操作系统时钟硬件每隔一段时间向操作系统发送时钟中断。进程被调度就意味着它的时间片到了操作系统会通过时钟中断检测到是哪一个进程的时间片到了然后通过系统调用函数 schedule() 保存进程的上下文数据然后选择合适的进程去运行。 四信号的处理流程 当在 内核态 完成某种任务后需要切回 用户态此时就可以对信号进行 检测 并 处理 了 情况1信号被阻塞信号产生/未产生 信号都被阻塞了也就不需要处理信号此时不用管直接切回 用户态 就行了。 下面的情况都是基于 信号未被阻塞 且 信号已产生 的前提 情况2当前信号的执行动作为 默认 大多数信号的默认执行动作都是 终止进程此时只需要把对应的进程干掉然后切回 用户态 就行了 情况3当前信号的执行动作为 忽略 当信号执行动作为 忽略 时不做出任何动作直接返回 用户态  情况4当前信号的执行动作为用户自定义  这种情况就比较麻烦了用户自定义的动作位于 用户态 中也就是说需要先切回 用户态把动作完成了重新坠入 内核态最后才能带着进程的上下文相关数据返回 用户态。 在 内核态 中也可以直接执行 自定义动作为什么还要切回 用户态 执行自定义动作 因为在 内核态 可以访问操作系统的代码和数据自定义动作 可能干出危害操作系统的行为。在 用户态 中可以减少影响并且可以做到溯源。 为什么不在执行完 自定义动作 直接后返回进程 因为 自定义动作 和 待返回的进程 属于不同的堆栈是无法返回的。并且进程的上下文数据还在内核态中所以需要先坠入内核态才能正确返回用户态。 注意 用户自定义的动作需要先切换至 用户态 中执行执行结束后还需要坠入 内核态。 通过一张图快速记录信号的 处理 过程 五信号的捕捉 1. 内核如何实现信号的捕捉 如果信号的执行动作为 用户自定义动作当信号 递达 时调用 用户自定义动作这一动作称为 信号捕捉。 用户自定义动作 是位于 用户空间 中的 当 内核态 中任务完成准备返回 用户态 时检测到信号 递达并且此时为 用户自定义动作需要先切入 用户态 完成 用户自定义动作 的执行因为 用户自定义动作 和 待返回的函数 属于不同的 堆栈 空间它们之间也不存在 调用与被调用 的关系是两个 独立的执行流需要先坠入 内核态 通过 sigreturn() 坠入再返回 用户态 通过 sys_sigreturn() 返回。 上述过程可以总结为下图 2. 信号捕捉函数 - sigcation 该函数是一个系统调用功能与signal()函数类似但是功能会更加强大sigaction函数可以读取和修改指定信号相关联的处理动作比 signal() 功能更丰富 参数 第一个参数是要捕捉的信号第二个与第三个都是一个结构体参数但是第二个参数是输入型参数第三个是输出形参数。若act指针非空则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。 返回值 调用成功则返回0,出错则返回-1 这个函数的主要看点是 sigaction 结构体 第一个字段是函数指针这个函数就是我们捕捉完信号以后要执行的处理动作。第二个与第五个字段是实时信号的处理函数,这里我们不做详细解释可以直接设置为0第四个字段包含了一些选项一般默认设置为0。 重点可以看看第三个字段​​​​​​​ sa_mask 是一个信号屏蔽集当信号在执行 用户自定义动作 时可以将部分信号进行屏蔽直到 用户自定义动作 执行完成。 也就是说我们可以提前设置一批 待阻塞 的 屏蔽信号集当执行 signum 中的 用户自定义动作 时这些 屏蔽信号集 中的 信号 将会被 屏蔽避免干扰 用户自定义动作 的执行直到 用户自定义动作 执行完成。 #include iostream #include cassert #include cstring #include signal.h #include unistd.husing namespace std;static void showPending(const sigset_t pending) {// 打印 pending 表cout 当前进程的 pending 表为: ;int i 1;while (i 32){if (sigismember(pending, i)) cout 1;else cout 0;i;}cout endl; }static void handler(int signo) {cout signo 号信号确实递达了 endl;// 最终不退出进程int n 10;while (n--){// 获取进程的 未决信号集sigset_t pending;sigemptyset(pending);int ret sigpending(pending);assert(ret 0);(void)ret; // 欺骗编译器避免 release 模式中出错showPending(pending);sleep(1);} }int main() {cout 当前进程: getpid() endl;//使用 sigaction 函数struct sigaction act, oldact;//初始化结构体memset(act, 0, sizeof(act));memset(oldact, 0, sizeof(oldact));//初始化 自定义动作act.sa_handler handler;//初始化 屏蔽信号集sigaddset(act.sa_mask, 3);sigaddset(act.sa_mask, 4);sigaddset(act.sa_mask, 5);//给 2号 信号注册自定义动作sigaction(2, act, oldact);// 死循环while (true);return 0; }当 2 号信号的循环结束10 秒3、4、5 信号的 阻塞 状态解除立即被 递达进程就被干掉了 。 注意 屏蔽信号集 sa_mask 中已屏蔽的信号在 用户自定义动作 执行完成后会自动解除 阻塞 状态。 八、小结 信号产生阶段有四种产生方式包括 键盘键入、系统调用、软件条件、硬件异常。 信号保存阶段内核中存在三张表blcok 表、pending 表以及 handler 表信号在产生之后存储在 pending 表中。 信号处理阶段信号在 内核态 切换回 用户态 时才会被处理。
http://www.zqtcl.cn/news/89739/

相关文章:

  • 有哪些网站可以免费发布广告好用的搜索引擎
  • 兰州做it网站运营的怎么样网站系统说明书
  • 宜宾建功路桥建设有限公司网站成都住建局官网电话查询
  • 优秀原创设计网站杭州有哪些网络公司
  • 珠海网站建设推广方案wordpress 设置密码
  • 军事网站大全军事网wordpress怎么做开发
  • 怎么做免费的网站空间上海做家教网站有哪些
  • 海关年检要去哪个网站上做连云港东海网站建设
  • 怎么做网站和注册域名服装网站建设优点和缺点
  • 企业网站管理系统如何使用说明2021个人网站盈利模式
  • 展示型网站系统openvz wordpress
  • 做网站最简单徐州百度seo排名
  • 重庆网站建设案例g宝盆网站建设优惠
  • 深圳企业网站推广如何在百度发布文章
  • 寮步网站建设 优帮云淮安品牌网站建设
  • 做任务的设计网站wordpress远程图片本地换
  • 可以做审计初级题的网站五大搜索引擎 三大门户网站
  • 重庆沛宣网站建设wordpress首页缓存
  • 做盗版视频网站凡客网站规划与建设ppt
  • acm网站免费做如何注册或购买一个域名使用
  • 我如何做网络推广网站如何做推广晚上必看的正能量直播app
  • 如何修改asp网站免费可以绑定域名网站空间
  • 建设厅网站合同备案在哪里关于网站开发的文档
  • 成都网站建设qghl个人网页案例
  • 免费申请网站com域名网络营销工具的案例
  • 精品网站建设公西安高端网站建设哪家好
  • 专业的网站建设设计多个wordpress共用一个数据库前缀
  • 企业网站设计的基本内容包括哪些网站开发哪里接业务
  • 广州商城网站制作网站河南省漯河建设局网站
  • 旅游类网站开发设计报告全球采购