ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

信号知识补充_可重入_volatile_SIGCHLD

信号知识补充_可重入_volatile_SIGCHLD 信号补充三话题可重入函数、volatile、SIGCHLD一、可重入函数1.1 场景同一个函数两条执行流信号捕捉引入了一个新场景你的代码里有一个hello()函数main 执行流可能调用它当 2 号信号被捕捉时handler 执行流也可能调用它。注意 handler 执行流的特殊性宏观上两条执行流是串行/交叉执行的单进程不是多线程并发main 执行流是必然要跑的handler 执行流是可有可无的——收到信号才执行独立于 main1.2 事故现场链表头插被重入单链表头插只需两条指针操作voidinsert(node_t*p){p-nexthead;// 第一步headp;// 第二步}事故过程配合课件逐步图 0→4main 执行流insert(node1)刚走完第一步node1-next headnode1还没彻底插入链表此时来信号转入handler 执行流✔ 两句指针操作之间没有系统调用也能捕捉——时钟中断随时可能打断进程从内核返回用户态时就会检查信号概率低但可能handler 里insert(node2)两步顺利走完head指向 node2node2 插入成功handler 返回main 继续走第二步head node1—— head 直接指向 node1结果node2 丢失内存泄漏1.3 概念定义函数被重入同一个函数被两个以上的执行流重复进入一个还没执行完另一个也进来了不可重入函数重入后会导致代码出异常如上面的 insert—— 本质是访问了公共资源全局链表、全局变量可重入函数随便重入代码不出任何问题关键认知可重入/不可重入描述的是函数的特点不是优缺点没有好坏之分大部分函数都是不可重入的调用了 malloc/free —— malloc 用全局链表管理堆调用了标准 I/O 库函数 —— 很多实现以不可重入方式使用全局数据结构C STL 容器 —— 空间配置器自动扩容内部大量链表节点的指针操作可重入函数只使用自己的临时变量与外部没有任何关系 最简单的规避办法不要让 handler 和 main 同时对同一数据结构做操作要么全让 main 插要么全让 handler 插。这个概念是为后面多线程做铺垫的到时还会再讲。二、volatile 关键字2.1 实验信号修改全局变量#includeiostream#includeunistd.h#includesignal.hintflag0;// 全局变量由信号修改voidhandler(intsigno){std::cout更改全局变量: flag - 1std::endl;flag1;}intmain(){signal(2,handler);while(!flag);// main 里并不会对 flag 进行修改std::coutprocess quit normal!std::endl;return0;}预期ctrlc → handler 把 flag 改 1 → while 退出 → 正常结束。默认编译-O0符合预期 ✔加-O1优化后ctrlc 打印了更改全局变量: 0 - 1但 while 循环死活不退出✖g test.cpp-O1# gcc/g 优化级别-O0默认基本不优化-O1 -O2 -O3逐级增高2.2 为什么 -O1 下循环不退出先补底层常识——CPU 做任何计算算术运算和逻辑运算两类判真判假也属于逻辑运算都绕不开三个阶段把变量从物理内存加载到CPU 寄存器在寄存器里做计算/判断需要的话写回内存纯检测可不写回任何变量不管全局还是临时一定保存在物理内存中。正常情况下while(!flag)每次判断都重复内存→寄存器→比较这套动作handler 把内存里的 flag 改成 1下次检测自然能读到。问题在于编译器看不到 handler 和 main 的关系编译器只分析 main 执行流flag 在 main 里只检测、从不修改handler 改它这件事编译器识别不出来两者没有直接调用关系于是 -O1 以上编译器自作聪明把 flag优化进寄存器相当于给了 register 建议加载一次之后while 每次判断只查寄存器不再访存——省掉访存过程提高运算速度handler 把内存里的 flag 改成了 1但寄存器里还是 0→ 寄存器覆盖了变量的真实情况 →内存不可见了→ 循环永不退出 所谓优化就是编译器把 C 翻译成汇编时自作主张地改写汇编把从内存 load 到寄存器那条指令删掉直接 compare loop。2.3 volatile保持内存可见性volatileintflag0;加上 volatile 后即使-O3编译ctrlc 也能正常退出。volatile 的作用大白话编译器你爱怎么优化怎么优化但被 volatile 修饰的变量必须占据一块内存空间每次检测都必须从内存重新加载到寄存器再判断不许只查寄存器——这叫做保证内存空间可见性。 为什么专门讲它① 信号场景是难得能把 volatile 讲清楚的场景②经典面试题。平时自己写代码大概率遇不到。三、SIGCHLD 信号3.1 子进程退出不是安安静静的回顾进程等待父进程必须回收子进程否则产生僵尸进程内存泄漏可以阻塞等待也可以非阻塞轮询但各有问题。现在补一个事实子进程终止时会给父进程发送 SIGCHLD 信号17 号。细节子进程暂停也会发今天只关注终止。这属于五种信号产生方式里的软件条件OS 发现有子进程退出 → 给父进程发信号。以前怎么没感觉查 man 手册可知SIGCHLD 的默认动作是 Ign忽略——信号发了只是父进程默认不理它。3.2 验证捕捉 SIGCHLDvoidnotice(intsig){std::coutfather get a signal: sigstd::endl;}intmain(){signal(SIGCHLD,notice);// 父进程注册与子进程无关pid_t idfork();if(id0){std::coutI am child, exitstd::endl;sleep(3);exit(3);// 子进程 3 秒后退出}waitpid(id,nullptr,0);// 父进程阻塞等待std::coutI am father, exitstd::endl;return0;}运行输出I am child, exit→father get a signal: 17→I am father, exit。验证成功子进程退出确实会给父进程发 SIGCHLD。3.3 应用一基于信号捕捉的子进程回收既然子进程退出会通知父进程那把回收工作放进信号捕捉函数父进程不就不用干等了吗——父跑父的子跑子的子退了发信号父收信号时顺手回收voidrecycle(intsig){while(true){pid_t nwaitpid(-1,nullptr,WNOHANG);// -1: 回收任意退出的子进程if(n0)break;// 本轮已退出的全部回收完elseif(n0){// 等待失败没有子进程了std::coutwait errorstd::endl;break;}// n 0成功回收一个继续循环}}signal(SIGCHLD,recycle);两个设计细节缺一不可① 为什么要 while 循环回收普通信号本质是位图只计一次。10 个子进程同一时刻退出、短时间内发来 10 个 SIGCHLDpending 位图里还是只有一个比特位被置 1捕捉动作可能只执行一次——只 waitpid 一次会漏掉其他 9 个实测只回收一次监控脚本看到一堆僵尸。所以必须循环回收直到没有可回收的为止。② 为什么要 WNOHANG非阻塞考虑 10 个子进程6 个退出、4 个不退的场景站在上帝视角你知道退了 6 个但父进程不知道有几个退了借钱比喻张三连续 6 天每天借 100第 7 天他还来不来他还来——他不知道你没钱了回收完 6 个后第 7 次还会调 waitpid如果此时用默认的阻塞式等待第 7 个子进程没退 → waitpid 卡住 →handler 永远不返回 → 主进程也回不来实测主进程直接卡死改成WNOHANG 非阻塞轮询没退的直接返回 0 → breakhandler 正常返回已退的全回收未退的不死磕✔ 实测效果10 个进程 6 退 4 暂停6 个退出瞬间全部回收父进程继续跑自己的之后手动kill -9一个个干掉剩余子进程每杀一个就自动回收一个父进程不受任何影响。waitpid 复习waitpid(pid, status, options)—— pid0 等指定子进程pid-1 等任意一个与 wait 等效options0 阻塞optionsWNOHANG 非阻塞。3.4 应用二更优方案直接 SIG_IGN 忽略如果不关心子进程的退出信息只想让它不产生僵尸还有更省事的办法源于 Unix 的做法signal(SIGCHLD,SIG_IGN);// 父进程显式置为忽略效果fork 出来的子进程终止时被系统自动清理不产生僵尸父进程连回收都不管。实测 10 个子进程退出后自动全部回收父进程什么都没做。✔ 适用纯粹不想产生僵尸✖ 缺点拿不到子进程退出信息要退出码还是得用 wait/waitpid阻塞/非阻塞/信号捕捉那套注意此方法对 Unix 可用不保证其他类 Unix 系统上可用3.5 最后一个疑问默认就是 Ign再设一次 SIG_IGN 不是脱裤子放屁吗✖ 不是。抠字眼书上说的是 SIGCHLD 的默认动作是 Ign —— 即处理动作是SIG_DFL缺省而这个缺省动作恰好是忽略手动signal(SIGCHLD, SIG_IGN)—— 是把处理动作显式改成忽略SIG_DFL默认碰巧表现为忽略和 SIG_IGN用户设置的忽略是两码事内核层面区别对待处理方式子进程退出时SIG_DFL系统默认表现为忽略变僵尸等父进程回收SIG_IGN用户显式设置忽略自动回收不产生僵尸 理解类比定义了一个 int 变量和int 变量为 0是两码事——变量已定义有缺省状态不等于它的值是 0。所有信号的默认处理动作都是 SIG_DFL只不过 SIGCHLD 的 SIG_DFL 执行起来恰好是忽略而已。四、总结话题一句话结论可重入函数函数被两个以上执行流重复进入出问题不可重入访问了全局资源没事可重入只用临时变量是特点不是优缺点大部分函数不可重入volatile编译器高优化级别会把只读不改的变量优化进寄存器 → 内存不可见volatile 强制每次从内存读保持内存可见性面试题SIGCHLD子进程终止给父进程发 17 号信号回收方案handler 里while waitpid(-1, NULL, WNOHANG)循环防位图丢失非阻塞防卡死不关心退出信息就直接SIG_IGN自动回收至此信号部分全部完结。Linux 四座大山已翻三座进程地址空间、文件系统ELF 动静态库、OS 如何运行最后一座大山多线程。
返回列表