ASP做购物网站视频,dede医院网站模板,汕头选择免费网站优化,上海建筑设计研究院#x1f493;博主CSDN主页:麻辣韭菜#x1f493; ⏩专栏分类#xff1a;Linux知识分享⏪ #x1f69a;代码仓库:Linux代码练习#x1f69a; #x1f339;关注我#x1faf5;带你学习更多Linux知识 #x1f51d;
目录
前言
1. 信号的处理时机
1.1用户… 博主CSDN主页:麻辣韭菜 ⏩专栏分类Linux知识分享⏪ 代码仓库:Linux代码练习 关注我带你学习更多Linux知识
目录
前言
1. 信号的处理时机
1.1用户态与内核态 1.2再谈地址空间 1.3 信号处理过程 1. 4 内核如何实现信号的捕捉 2. sigaction
功能
参数
结构体定义
注意事项
信号处理函数
代码演示
3. 额外知识
3.1可重入函数
3.2 volatile
3.3 SIGCHLD 信号 前言 信号产生到处理这是有一个过程的从上篇信号保存我们知道了有一个handler表 里面存放的是对信号处理的方法。 那什么时候调用这些方法本篇就揭晓处理信号的时机。 1. 信号的处理时机
直接说结论当我们用户从内核态返回到用户态时进行信号的检测与处理。
那什么是用户态什么又是内核态
1.1用户态与内核态 用户态我们写的代码被执行时就是用户态。 内核态执行系统代码时就是内核态。
如何理解内核态 我们之前所学的fork创建子进程。 open read witre close这些系统调用接口被执行时进程就从用户态切换成了内核态。 在调用这些系统调用接口。其实有都有一条汇编指令int 80 用户态切换成内核态 也就是说执行我们代码时是会产生中断或者异常当CPU在处理中断或异常时会根据中断描述符表Interrupt Descriptor Table, IDT中的描述符的DPL来决定是否需要进行上下文切换。如果当前的Code Segment的DPL高于或等于IDT中描述符的DPL那么CPU将不会切换到一个新的特权级别否则会进行上下文切换。 回想之前的进程地址空间图 高地址的 3G 到 4G是内核空间所以我们需要从新再谈地址空间才能更好的理解用户状态的切换。 1.2再谈地址空间 每个进程都会有自己的地址空间和用户级页表因为进程具有独立性。 从上图可以看到每个地址空间的3G到4G是内核空间。 也就是说执行操作系统代码以及系统调用接口的代码和数据是在这一个区域。 那是不是每个进程都要有内核级的页表 当然不用。内核级页表只有一份 每一个进程 在0到3GB 所看到的代码和数据是不一样的。内核级页表只有一份就意味着3GB到4GB这段空间所有进程看到的都是同样的代码和数据OS所以 不管怎么切换进程 这段空间的内容是不变的。 那进程又是如何被调度的 不要忘了OS也是软件啊。并且还是一个死循环等待指令软件。 当然等待指令也是需要硬件的。时钟硬件 每格一段时间就会向cpu发送信息。OS这时就会知道那个进程的时间片到了 然后通过一系列系统调用函数 switch_to() 宏以及底层的 context_switch() 函数。进行上下文切换。 1.3 信号处理过程 这里需要解释用户自定义为什么要回到用户模式执行处理方法。 有人就会说 在第2步时已经在内核态了 可以执行信号处理的方法。为什么还要回到用户态 因为OS对除了自己的系统调用对任何的代码都不相信所以OS检测到是用户定义方法时。会切换状态。变成用户态用户态执行信号处理。再次切换成内核态 那为什么还要返回到内核态 因为进程的上下文还在内核态中自定义函数栈帧和进程的栈帧不是同一个。拿什么返回上下文的数据 上面的图有点复杂 我们用简化的版图 数学上的无穷符号 1. 4 内核如何实现信号的捕捉 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。 如果用户程序注册了SIGQUIT信号的处理函数 sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返 回后自动执行特殊的系统调用sigreturn 再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。 2. sigaction
#include signal.h
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 功能
sigaction() 系统调用用于改变进程接收特定信号时采取的动作。
参数
signum: 指定信号的标识符可以是任何有效的信号除了 SIGKILL 和 SIGSTOP。act: 如果非空表示要安装的新信号动作来源于 act。oldact: 如果非空表示先前的信号动作将被保存在 oldact 中。
结构体定义
struct sigaction {void (*sa_handler)(int); // 传统信号处理函数void (*sa_sigaction)(int, siginfo_t *, void *); // SA_SIGINFO 标志设置时使用的信号处理函数sigset_t sa_mask; // 信号掩码定义了信号处理函数执行时哪些信号应当被屏蔽int sa_flags; // 信号处理标志void (*sa_restorer)(void); // 已废弃不应使用
};
注意事项
结构体中 sa_handler 和 sa_sigaction 不应同时被赋值它们是互斥的。sa_restorer 成员已废弃POSIX 标准中没有指定该成员。sa_flags 中设置 SA_SIGINFO 标志时使用 sa_sigaction 成员否则使用 sa_handler。
信号处理函数
sa_handler: 可以是 SIG_DFL默认动作、SIG_IGN忽略信号或者指向信号处理函数的指针。处理函数仅接收信号编号作为参数。如果 sa_flags 包含 SA_SIGINFO则 sa_sigaction 指定信号处理函数该函数接收三个参数信号编号、指向 siginfo_t 的指针包含信号的详细信息以及指向 ucontext_t 的指针通常被转换为 void * 类型包含了信号发生时的上下文信息。
代码演示
#include iostream
#include cstring
#include unistd.h
#include signal.h
using namespace std;
void PrintPending()
{sigset_t set;sigpending(set);for (int signo 31; signo 1; signo--){if (sigismember(set, signo)){cout 1;}elsecout 0;}cout endl;
}void handler(int signo)
{cout catch a signal ,signal number: signo endl;while (true){PrintPending();sleep(1);}
}int main()
{// 1.使用sigaction函数struct sigaction act, oact;// 1.1初始化 act oact。memset(act, 0, sizeof(act));memset(oact, 0, sizeof(oact));// 2.1 初始化 并设置信号屏蔽集sigemptyset(act.sa_mask);sigaddset(act.sa_mask, 1);sigaddset(act.sa_mask, 3);sigaddset(act.sa_mask, 4);// 3.自定义handler函数act.sa_handler handler;//3.1 给2号信号注册自定义动作。sigaction(2, act, oact);while (true){cout I am a process: getpid() endl;sleep(1);}// 4. 发送信号return 0;
} 可以从运行结果来看我们发送4号信号确实被阻塞了。但是未决表中有信信息。 3. 额外知识
掌握额外知识在后面线程章节中能更好的理解线程。
3.1可重入函数 有没有一种可能 两个进程同时执行容器 list 的插入函数 insert 当进程1开辟好空间时刚好进程1的调度时间片到了切换成进程2。而进程2调度时间刚好可以把insert这个函数执行完毕。然后进程1再次被调度执行insert。这时进程2的insert函数调用new开辟的空间就会出现内存泄漏。 当然大家也不用担心。
我们学过的99%的函数都是不可被重入。
那有没有需要重入的场景
操作系统服务操作系统提供的某些服务如内存分配器必须是可重入的因为它们可能会被不同的进程或线程同时调用。
那什么样的函数可以重入 不使用全局变量或静态变量可重入函数不依赖于全局或静态变量来存储状态因为这些变量可能被多个线程共享从而导致数据竞争。 不执行I/O操作I/O操作可能会涉及系统资源如文件句柄这些资源在多线程环境中可能会引起冲突。 不调用其他不可重入的函数如果可重入函数调用了不可重入的函数它自身也就不再是可重入的了。 不返回指向内部数据结构的指针函数返回的指针不应该指向函数内部的局部变量或分配的内存因为这些内存在函数返回后可能会被释放。 不产生副作用可重入函数不应该改变程序的其他部分的状态除了其返回值。 使用局部变量所有需要的变量都应该在函数内部分配通常是在栈上。 使用互斥锁或原子操作如果必须访问共享资源可重入函数应该使用互斥锁mutexes或原子操作来确保线程安全。 避免使用动态内存分配虽然技术上可行但动态内存分配如使用malloc或new可能会使函数不可重入因为这些操作可能会依赖于全局或静态状态。 快速执行可重入函数应该设计成执行时间尽可能短以减少它被中断的可能性。 信号安全如果函数设计用于响应信号如在UNIX系统中它应该是信号安全的即它可以在信号处理的上下文中安全调用。 3.2 volatile
#include iostream
#include cstring
#include unistd.h
#include signal.h
using namespace std;
int n 0;
void handler(int signo)
{cout catch a signal ,signal number: signo endl;n 1;
}int main()
{signal(2, handler);while (!n);cout process quit normal endl;return 0;
} 初步结果符合预期2 号信号发出后循环结束程序正常退出 那是因为当前编译器默认的优化级别很低没有出现意外情况 通过指令查询 g优化级别的相关信息
man g
: /O1 其中数字越大优化级别越高理论上编译出来的程序性能会更好
那我们再改成 -O1试试
g -o $ $^ -g -O1 -stdc11 编译成功 运行得到下面的结果。 此时得到了不一样的结果2 号信号发出后对于 falg 变量的修改似乎失效了 从上面的实验的我们知道编译肯定是优化了不然为什么while循环一直不退出。 为什么优化了就会出现 n 的值修改失效 为了防止编译器过度优化votatile 这个关键字就出现了。它的作用就是防止编译过度优化。 volatile int n 0;
这时我们在编译 3.3 SIGCHLD 信号
在进程控制学习期间 我们知道父进程必须等待子进程的退出然后回收。以前没有信号的概念那子进程退出后会给父进程发信号吗
当然会发信号 子进程退出后会给父进程发送 SIGCHLD 信号
SIGCHLD 是普通信号的17号位
下面我们用一段代码证明
#include iostream
#include cstring
#include unistd.h
#include signal.h
#include sys/wait.h
#include sys/types.h
using namespace std;void handler(int signo)
{cout catch a signal ,signal number: signo endl;}int main()
{signal(17,handler);for(int i 0 ; i10 ; i){pid_t pid fork();if(pid 0){while(true){cout I am a child process: getpid() ppid: getppid() endl;sleep(1);break;}cout child quit !!! endl;exit(0);}sleep(1);}while (true){cout I am father process: getpid() endl;sleep(1);}return 0;
}从面结果来看确实回收了子进程。但是这个代码有问题。不要忘了pending表的位图17只有一位。如果回收多个进程那么OS只会做一次信号处理而其他的子进程没有回收就造成内存泄漏。那如何解决 自定义捕捉函数中采取 while 循环式回收有很多进程都需要回收没问题排好队一个个来就好了这样就可以确保多个子进程同时发出 SIGCHLD 信号时可以做到一一回收 #include iostream
#include cstring
#include unistd.h
#include signal.h
#include sys/wait.h
#include sys/types.h
using namespace std;void handler(int signo)
{pid_t rid;while ((rid waitpid(-1, nullptr, WNOHANG)) 0){cout I am proccess: getpid() catch a signo: signo child process quit: rid endl;}}int main()
{signal(17,SIG_IGN);for(int i 0 ; i10 ; i){pid_t pid fork();if(pid 0){while(true){cout I am a child process: getpid() ppid: getppid() endl;sleep(1);break;}cout child quit !!! endl;exit(0);}sleep(1);}while (true){cout I am father process: getpid() endl;sleep(1);}return 0;
}这里说一说为什么用 signal 函数参数为什么是 SIG_IGN
这里的忽略是个特例只是父进程不对其进行处理但只要设置之后子进程在退出时由 操作系统 对其负责自动清理资源并进行回收不会产生 僵尸进程
原理
父进程的 PCB 中有关僵尸进程处理的标记位会被修改子进程继承父进程的特性子进程在退出时操作系统检测到此标记位发生了改变会直接把该子进程进行释放。
注意 SIG_IGN 只在linux中有效其他系统无效比如UNIX