物流企业的网站模板,网站副标题怎么写,行业门户型网站,福永附近做网站公司绪论 “Do one thing at a time, and do well.”#xff0c;本章开始Linux系统其中信号是学习操作系统的基本下面将会讲到什么是信号、信号的多种产生方式、信号如何保存的、信号如何处理的、以及一些信号的细节。话不多说安全带系好#xff0c;发车啦#xff08;建议电脑… 绪论 “Do one thing at a time, and do well.”本章开始Linux系统其中信号是学习操作系统的基本下面将会讲到什么是信号、信号的多种产生方式、信号如何保存的、信号如何处理的、以及一些信号的细节。话不多说安全带系好发车啦建议电脑观看。 1.信号的概念 日常生活中有很多类似于信号的东西如红绿灯、下课铃声、闹钟声音等 他相当于是一种信息当在传递给你时你需要对他有认识并且知道其信号的意义是什么。这也表明了我们需要提前存储这些信号的概念对传来的信号能进行识别处理。 信号就是一种向目标进程发送通知消息的一种机制。 所以一个进程处理信号需要
当信号没有产生时就已经知道该怎么处理这个信号。信号的到来我们并不清楚所以信号到来时我可能正在进行的代码所信号的产生和进程是异步。 1. 异步可以理解为有两个执行流各自执行自己的执行流他跑他的、你跑你的进程能暂时保存好到来的信号
kill命令
向指定进程发信号kill signum(信号编号) pid进程的pid 具体serce的实现过程见 2.3信号能通过键盘输入产生的第一处代码查看所有信号kill -l 1. 其中没有0号信号一个进程就两种退出信息包括信号和退出码0/非0因为0号信号表示没有收到信号为了标识进程的正常结束。 2. 没有32、33信号普通信号1 ~ 31、实时信号34 ~ 64收到信号时立即处理不会出现信号丢失。 3. 所有信号都是一个个宏所以信号可以用数字和字符串表示。 下面对所有普通信号我们边学边认识 2.信号的产生
进程运行时分为
前台./xxx 在前台执行进程 1. 前台进程只能有一个键盘只有一个 2. 能接受键盘用户输入的指令后台./xxx 在后台启动该进程 1. 后台可以有多个进程 2. 一般放耗时较长的任务 3. 反之不能接受来自键盘的指令 其中[1]表示后台任务编号20103是进程的pid
通过指令查看后台任务jobs 后台任务不影响前台任务。 把进程从后台放回前台fg 后台任务编号jobs第一列 通过上图分析得 3. 把编号1的任务放到前台时发现pwd指令用不成了说明原本的shell是一个前台进程也证明前台进程只能有一个1号任务代替了shell进程所以shell指令也随之用不了了 4. ^ C表示的是ctrl c 能终止前台的进程 5. 当终止掉当前的前台进程后发现shell自动启动了所以shell是能被自动的提到前台的进程 2.1OS接收键盘的数据通过中断技术 中断技术很多外设都能对CPUCPU的针脚发送中断信息光电信号表示数据就绪发送来的光电信息就会被8259一个板子给到CPU的针脚有编号的又称中断号发送就绪信息保存到寄存器就能被程序读取就从硬件到了软件就能去读取信息了。
所以每个进程都有中断向量表数组的下标就和信号的编号是强相关的。 2.2处理信号方法有
忽略处理默认处理自定义捕捉处理 2.3通过系统调用发送信号 捕获信号的函数 sighandler_t signal(int signum, sighandler_t handler); 头文件#include signal.h 参数 1. signum是信号编号每个信号都有对应的编号 2. handler是一个函数指针 typedef void (* sighandler_t)(int) 1. 自定义的方法这个函数的类型就是void * (int) 其中我们需要知道该进程是否需要被终止掉当需要终止时我们需要终止exit1 进程否则进程就不会被终止将导致进程一直运行。 2. 默认方法SIG_DFL 3. 忽略方法SIG_IGN 其中SIG_DFL、SIG_IGN本质就是宏本质也是函数指针 把0强转为SIG_DFL把1强转为SIG_IGN最终在handler内部再转为int判断是否为0/1再分别处理。 给指定进发送指定信号 int kill(pid_t pid, int sig); 头文件: #include sys/types.h、#include signal.h 参数 1. pid进程pid 2. sig信号编号 给自己发送指定信号 1. int raise(int sig); 头文件 #include signal.h 参数 1. sig信号编号 代码
void handler(int signo)
{cout 捕获到信号第: signo 号信号 endl;sleep(1);exit(1);
}int main(int argc,char* argv[])
{signal(2,handler);raise(2);//给自己发送2号信号sleep(10);//若没有被终止则会休眠10s
}正常终止当前进程 void abort(void); 原理其实是向自己发送六号信号SIGABRT 头文件#include stdlib.h 2.4信号能通过键盘输入产生
键盘的输入的数据可能是数据也可能是组合键表示信号
ctrlc发送二号信号SIGINT终止进程自己从键盘输入后通过中断技术在中断向量表中找到直接所要执行的方法所以也表明二号信号会终止进程自己。1.ctrl z发送20号信号默认停止进程自己放回到后台暂停后台任务继续运行bg 后台编号 ctrl \发送3号信号默认终止进程
当我们从键盘输入就能产生信号给前台进程 证明当键盘输入ctrlc时就会产生二号信号给当前前台进程图中^C就是从键盘输入的ctrl c 代码
#include iostream
#include signal.h
#include unistd.h
using namespace std;void handler(int signo)//自定义处理函数handler
{cout 接收到 signo 号信号 endl;//打印是几号信号
}int main()
{signal(2, handler);//捕获信号的函数捕获到产生的信号并进行自定义处理函数handlerwhile (true){std::cout runing ... ,pid: getpid() std::endl;sleep(1);}return 0;
}2.4.1信号的存储
操作系统向目标进程发送信号实际上就是对pcb结构体中的位图成员进行改写 也就是pcb结构体中有一个位图的成员变量
struct task_struct
{//其余成员变量...uint32_t sigmap;//信号位图
}进程中会有一个信号的位图的成员存着接收到的信号 位图0000 0000 … 0000 1 ~ 31普通信号31位
所以实际上发信号其实就是OS找到进程pcb修改位图也就是写入信号如1号信号的写入0000 … 0001
所以总结来说每个进程都会有
函数指针数组中断向量表信号位图 有了这两个东西才能让进称对信号正常的接收和处理 所以我们从键盘到信号生效也就明了了 接收到中断信息后存储产生的信号然后要再通过存储的信号在中断向量表中找到对应的方法就能让信号产生对进程的对应效果 而只有OS能发送信号因为只有OS才能修改进程内的成员 查看信号的默认处理方式man 7 signal处理方法只有三种默认忽略自定义 打开后往下翻找到 其中默认处理方法就是Action一列 只有少量的信号不能被自定义捕捉如9号信号即使写了自定义方法也仍然会执行默认的Term终止进程 写一个通过中终端控制向目标进程发送信号的程序
#include iostream
#include signal.h
#include unistd.h
#include stdlib.h
#include string
using namespace std;int main(int argc,char* argv[])
{if(argc ! 3){cout 格式错误因为为./process signal processpid endl;exit(0);}int signum stoi(argv[1]1);//传进来的是-9int processpid stoi(argv[2]);kill(processpid,signum);//1跳过-得到数字return 0;
}
//编译时加上-stdc11SIGCONT 18从后台放回前台继续运行
2.5异常产生信号:
当是除0错误时会向该进程发送8号信号SIGFPE。 从虚拟地址中获取数据给到在内存中的进程在通过进程给到CPU处理CPU处理的过程中会有一个溢出标记位记录是否出现了错误若出现错误了OS就会处理该错误也就是发送信号给当前进程。
2.访问野指针错误段错误会向进程发送11号信号SIGSEGV。 附我们在学异常时实质上抛出异常的主要目的并不是处理异常而是让程序执行流正常结束还能打印错误。
2.6软件条件产生信号:
管道软件产生信号 匿名管道当读关闭掉时就会向写端发送信号SIGPIPE终止。 为什么说管道是一个软件因为他并没有涉及到底层硬件的处理管道空间属于是我打开的文件而文件的本质就是软件通过OS判断管道文件的数据结构当OS识别内核数据结构不满足条件时就会杀死掉进程。 此处判断管道软件的读端被关闭了底层是通过读文件描述符查看并没有查看CPU寄存器的硬件所以也就是软件问题当OS发现读端被关闭管道引用计数为1了所以软件条件不满足所以就会OS就会发送SIGPIPE给进程终止。闹钟软件产生信号 unsigned int alarm(unsigned int seconds);//闹钟函数 功能在second秒后给当前进程发送SIGALRM信号14号信号默认动作是终止进程。 头文件#includeunistd.h 闹钟本质上就是OS中描述的一个结构体也就是一个软件其中包含了许多成员变量信息结构体成员中肯定有一个存着时间的成员记录着当前闹钟的是否超时超时了就会响也就是发送信号给指向的进程。而这些闹钟的时间的存储就可能是在一个小堆中通过不断查看堆顶元素是否超时超时就pop出堆来进行闹钟的运行。 当这个时间一超时就会先闹钟中结构体存的成员进程的pid发送信号。 alarm(0)是取消上一个闹钟并返回他剩余的时间。 为什么软件也能产生信号 本质上是因为OS是软硬件资源的管理者软件出问题了时软件也能产生信号给OS让他处理进程。 2.7操作系统的一些更加深入的底层知识
在计算机内部有个纽扣电池一直在给某些硬件计数器供电记录时间所以最后开机时在计算处正确的时间。所有用户的行为最终都是以进程的形式在OS中表现的打开如何东西。所以操作系统只用把进程调度好就能完成所有的用户任务。CMOS能向CPU周期性的高频率的发送时钟中断。CPU的主频概念的产生硬件CMOS通过向CPU内发送超高频中断信息才让操作系统运行起来并执行其内部的代码调度方法所以操作系统本质也是代码程序也是通过硬件发送中断信息才能被执行对应的程序的。OS的本质就是个死循环while(1) { pause();//暂停进程只到来信号 }来保证操作系统在开机后一直运行。
总结产生信号的方式可以有很多但是发送信号只能由OS发送写信号 2.8Core Dump核心转储
core dump核心转储就是把进程退出的原因存储进硬盘Core Dump会形成一个 以在运行时代码中的崩溃处的核心上下文数据 的文件也就是问题存储进磁盘中在文件下形成core.pid命名的文件指定进程pid。
查看基本配置项包括了core dump产生的文件大小 指令ulimit -a 其中core file size项就表示了当前Core Dump产生文件的大小 ulimit -a 修改 core file size项并且这个修改只针对当前的shell进程 在gdb模式下查看core file文件内容 通过在gdb模式下core-file 对应的文件名直接查看转储错误的地方 core dump保存东西较多文件比较大并且每当运行一次程序出现问题就会产生一个所以为了防止一些特殊进程会不断重启异常重启循环不断的生成core file文件所以云服务器就默认关闭了所以若想启动的话就去修改基础配置中core file的文件大小即可。 而Action中Term终止和Core终止的区别 报Core错误的才是真正的异常错误并且Core比较严重用户还需自己进一步排查代码哪里错Term反之并且若有错误并且开启了core file size非0则会生成core file文件。 重要知识总结 1. 信号的产生都是经过操作系统的OS是进程的管理者 2. 信号并不是立即处理在合适的时候处理 3. 信号需要被暂时记录下来位图 4. 进程还没收到信号就知道如何处理信号维护一张函数指针数组表 5. OS本质并不是发信号而是写信号 3.信号的保存阻塞
信号的常见名称概念
信息递达实际执行信号的处理动作也就是合适的时候处理信号 处理信号的方法 信号的忽略自定义捕捉信号的默认 其中可以用signal函数 sighandler_t signal(int signum, sighandler_t handler);可见于2.3.1节处。 信号未决信号从产生到递达之间的状态也就回把信号保存在pcb的信号位图中是还未被执行时。理解为记录了老师布置的作业此时只是记录还未处理。信号阻塞阻塞时是表示一直处于未决状态暂时不递达直到解除对信号的阻塞。忽略本身没有阻塞他是一种未决状态属于递达的一种没来得及处理所以说信号未决信号不一定是阻塞。
即使没有收到某个信号也能设置信号的状态
进程结构体中会有三张表 也就是上面阻塞、未决、递达三个概念对应的表
pending未决位图表OS发送信号时本质就是在此处写信号。handler递达的函数指针数组对应信号的处理方法相当于signal的第二个参数。block阻塞位图表是否对特定的信号进行屏蔽阻塞
因为每个进程都提前有了上面的三个表所以也就让进程能认识信号处理信号 其中每一行就对应着一个信号的处理。 对于上面的表每个信号当产生信号时OS修改pending位图为1当处理后pending由1变回0若block为1就被阻塞了无法执行即使pending为1一旦block阻塞解除了就需要立即递达 允许OS向进程发送多次信号但其实本质也只记录一次信号普通信号实时信号则相反是通过一个队列来处理每一次信号。
3.1sigset_t信号集
sigset_t称为信号集。在未决表中每个信号只有一个bit的未决标志非0即1同理阻塞也是这样。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储这个类型由系统提供可以表示每个信号的有效或无效状态所以还能把block表和pending表称为阻塞信号集和未决信号集block表还能称为信号屏蔽字Signal Mask。
3.2信号集操作函数
下面是对set信号集进行处理的函数
在信号集set中全置空0int sigemptyset(sigset_t *set);在信号集set中全充满1int sigfillset(sigset_t *set);在信号集set中添加信号signo比特位处置为1int sigaddset (sigset_t *set, int signo);在信号集中删除指定信号signo置为0int sigdelset(sigset_t *set, int signo);判断指定的signo信号是否在该set信号集中int sigismemberconst sigset_t *set, int signo);
头文件#include signal.h
而其中sigset_t其实也就是个类型由系统提供为了形参的一个结构体方便我们的用于系统调用。 3.3读取或修改信号屏蔽字阻塞信号集的函数
int sigprocmask(int how, const sigset_t * set, sigset_t * oset); 头文件#include signal.h 返回值:若成功则为0,若出错则为-1 参数
how有三个选项标识符 1. SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号相当maskmask|set 2. SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号相当于maskmask~set把原本的位图mask按位与上传进来的set的取反意味着在set中的就会从mask中去除 3. SIG_SETMASK设置当前信号屏蔽字为set所指向的值相当于maskset也就是把传进来的位图set参数二替换掉原有的位图set根据参数一来自定义自己的set位图表传递进去oset是一个输出型参数返回老的mask位图老的block表的位图
在信号屏蔽时有两个信号时无法进行屏蔽的他们称为管理员信号 分别是9号和19号信号其余可自行测试是能被屏蔽的 测试代码
#include iostream
#include signal.h
#include unistd.h
#include stdlib.h
#include string
using namespace std;
void handler(int signo)
{cout signosigno endl;
}
int main()
{cout getpid getpid() endl;signal(2,handler);sigset_t block,oblock;sigemptyset(block);sigemptyset(oblock);for(int signo 1; signo 32 ;signo){sigaddset(block,signo);}sigprocmask(SIG_SETMASK,block,oblock);//此处才修改了系统的信号屏蔽字cout 已经屏蔽所有信号 endl;while(true){sleep(1);}return 0;
}指令操作以及结果
3.3读取未决信号集的函数
#include signal.h sigpendingsigset_t set 读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1 set也就还是一个输出型参数 实操代码及结果
void handler(int signo)
{cout signosigno endl;
}
void PrintPending(const sigset_t pending)
{for(int signo 31; signo 0 ;signo--){if(sigismember(pending,signo)){cout 1;}else{cout 0;}}cout endl;
}int main()
{signal(2,handler);cout getpid getpid() endl;sigset_t set,oset;sigemptyset(set);sigemptyset(oset);sigaddset(set,2);sigprocmask(SIG_BLOCK,set,oset);sigset_t pending;int cnt 0;while(true){sigpending(pending);PrintPending(pending);sleep(1);if(cnt 5){cout 解除屏蔽 endl;sigprocmask(SIG_SETMASK,oset,nullptr);}cnt;}return 0;
}信号在要处理方法前pending位图就会先变成0后才执行调用对应方法 4.信号的处理递达
4.1信号在合适的时候处理
进程从内核态返回到用户态的时候进行信号的检测和处理也就是进程从内核态返回用户态的时候进行信号的检测和信号的处理
用户态是一种受控的状态能够访问的资源时有限的内核态是一种操作系统的工作状态能够访问大部分系统资源用户只能访问用户空间0~3G内核态可以让用户以OS的身份访问内核空间3~4G
其中系统调用就是身份的变化在底层工作原理如下图
OS共用一个内核页表所以无论调度那个进程CPU都能很好的找到操作系统。所有的代码的执行都能在自己的进程地址空间内执行通过地址 跳转的方式进行调用和返回 信号处理的流程 它分为四步能忽略细节的看成 通过上图我们能总结出来 会有四次状态的交换进程从内核态返回用户态的时候进行信号的检测和信号的处理即使没有系统调用也同样会有状态的改变进程切换过程中会从内核到用户来执行对应的代码
信号捕捉处理函数针对于handler表 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 要处理的信号signum新修改后的方法act输出型参数返回修改前的act 返回值调用成功则返回0,出错则返回- 1 头文件#includesignal.h act类型是sigaction他是个结构体他的具体类型
struct sigaction {void(*sa_handler)(int);//主要考虑void(*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;//主要考虑int sa_flags;void(*sa_restorer)(void);
};成员 1. sa_handler就是对应的信号的handler方法 2. sa_mask处理某个信号时用来屏蔽额外的信号 3. 其他sa_sigaction、sa_flags、sa_restorer暂时不考虑
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 总结就是某一种信号在执行时若再次产生该信号产生的信号将被屏蔽 不被处理他并不会多次处理。 如果要不止屏蔽执行的信号而且还要屏蔽其他信号那么就设置sa_mask同步来屏蔽其余的信号。
实践证明sa_handler就是改变handler表内的方法
void handler(int signo)
{cout signosigno endl;
}int main()
{struct sigaction act,oact;act.sa_handler handler;//修改act的sa_handlersigaction(2,act,oact);//对对应signum信号通过act修改handler表while(true){sleep(1);}return 0;
}实践证明sa_mask的使用当把三号信号加入sa_mask中后在执行二号信号时不仅会屏蔽执行着的二号信号还会屏蔽sa_mask中的三号信号。 代码
void Print(const sigset_t pending)
{for(int signo 31 ; signo 0 ;signo--){if(sigismember(pending,signo))//查看signo信号是否在pending位图中{cout 1;}else{cout 0;}}cout endl;
}void handler(int signo)
{cout signo: signo endl;while(true){sigset_t pending;sigpending(pending);//返回得到pending位图Print(pending);sleep(1);}
}int main()
{cout getpid getpid() endl;struct sigaction act,oact;sigemptyset(act.sa_mask);sigaddset(act.sa_mask,3);//对3号信号进行了屏蔽act.sa_handler handler;//修改act的sa_handlersigaction(2,act,oact);//对对应signum信号通过act修改handler表while(true) sleep(1);return 0;
}若同时存在好几个信号被堵塞那么当解除堵塞后将会在一次信号检测过程中将多个信号进行处理。 附 系统调用的原理将系统调用编号存在寄存器通过这个编号后去函数调用的函数指针数组中去找对应方法。
5.信号的其他补充话题
5.1可重入函数
可重入函数允许被多个执行流重复进入不可重入函数不允许被多个执行流重复进入百分之90自定义的函数都是不可重入函数
下图中出现的问题就是因为多执行流重入而导致头插错误在执行完1后收到一个信号后进行立即处理并且里面也有插入函数从而导致错误node2找不到内存泄漏所以下面的函数就是不可重入函数。 一般而言只要函数用到了全局的变量或者容器数据结构而我们调用的malloc/free他们都是通过全局链表来管理堆的还有常用的标准I/O库函数printf…他们都是不可重入函数方式使用全局数据结构。
5.2volatile
通过下面代码产生的问题来解释volatile的用处
int flag 0;void handler(int signo)
{cout signo: signo endl;flag 1;cout change flag to: flag endl;
}int main()
{signal(2,handler);cout getpid getpid() endl;while(!flag);return 0;
}此处因为在main函数中flag只进行了读所以当提升优化级别时将会把flag的存进CPU寄存器的过程只进行一次优化所以当我们产生信号时修改的仅仅只是内存里的CPU内的将不会改变所以将不能成功退出。 所以为了避免这种情况我们可以对变量加上volatile让其内存可见性这样当内存的变量改变时CPU内部也会跟着改变
volatile int flag 0;//修改即可5.3SIGCHLD信号 子进程退出会僵尸需要父进程wait他他退出时会向父进程发送信号SIGCHLD信号 证明代码
#includesys/wait.h//wait的头文件void handler(int signo)
{cout signo: signo endl;
}int main()
{signal(SIGCHLD,handler);pid_t id fork();if(id 0){//子进程sleep(5);exit(1);}int cnt 10;while(cnt--){cout cnt endl;sleep(1);}wait(NULL);return 0;
}附 若要一次性退出多个进程 原理是在接收到信号后handler函数内进行子进程的循环等待这样就能在接收到信号后处理。但注意的是可能同时处理多个子进程因为子进程可能同时结束并发送SIGCHLD信号回顾前面信号当同时发来多个相同的信号时他不会同时处理相同的信号会被屏蔽所以通过循环的方式来处理让同时传进来的信号的子进程都被等待成功。
void handler(int signo)
{cout signo: signo endl;pid_t id;while(id waitpid(-1,nullptr,WNOHANG))//-1接收所有子进程WNOHANG非阻塞返回0表示等待不成功{if(id 0) break;cout 回收进程 id endl;}
}int main()
{signal(SIGCHLD,handler);for(int i 0; i 10;i){pid_t id fork();if(id 0){//子进程sleep(1);exit(1);}}int cnt 15;while(cnt--){cout cnt endl;sleep(1);}wait(NULL);return 0;
}但其实Linux操作系统上对SIGCHLD信号进行了特殊的处理Linux支持手动忽略SIGCHLD后来所有子进程都不要父进程等待了退出会自动回收。 具体如下 int main()
{signal(SIGCHLD,SIG_IGN);//Linux支持手动忽略SIGCHLD后来所有子进程都不要父进程等待了退出会自动回收for(int i 0; i 10;i){pid_t id fork();if(id 0){//子进程// sleep(5);exit(1);}}int cnt 10;// while(cnt--)// {// cout cnt endl;// sleep(1);// }while(true);wait(NULL);return 0;
}本章完。预知后事如何暂听下回分解。
如果有任何问题欢迎讨论哈
如果觉得这篇文章对你有所帮助的话点点赞吧
持续更新大量Linux细致内容早关注不迷路。