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

凡高网站建设宠物网站建设策划方案

凡高网站建设,宠物网站建设策划方案,关于单位建设网站的申请,9国产精华最好的产品目录 一、生活中的信号 背景知识 生活中有没有信号的场景呢#xff1f; 是不是只有这些场景真正的放在我面前的时候#xff0c;我才知道怎么做呢#xff1f; 进程在没有收到信号的时候#xff0c;进程知道不知道应该如何识别哪一个是信号#xff1f;以及如何处理它 是不是只有这些场景真正的放在我面前的时候我才知道怎么做呢 进程在没有收到信号的时候进程知道不知道应该如何识别哪一个是信号以及如何处理它 但是对于日常生活中信号场景产生的时候我们立即可以做出相应的动作吗 既然信号不能被立即处理已经到来的信号是不是应该暂时被保存起来 二、技术应用角度的信号  验证信号 键盘CTRLC的本质就是我们向指定进程发送2号信号如何证明呢 验证下键盘中的按键对应哪些信号 让不同的信号执行不同的动作 三、信号产生前 1.键盘产生信号 2.程序中存在异常问题产生信号导致进程收到信号退出。  为什么我会收到信号呢 当进程崩溃退出的时候最想知道什么 如何证明core dump是被关掉的呢 不一定所有的信号都会形成core dump文件 3.通过系统调用接口产生信号 kill系统调用  raise调用 abort函数 4.软件条件也能产生信号         alarm 为什么第一种计数很慢呢 如何理解OS给进程发送信号 四、信号产生中 信号其他相关常见概念 忽略和阻塞有区别吗 信号在内核中的表示 信号处理的过程 代码验证信号 sigset_t 信号集操作函数 sigprocmask sigpending 五、信号发送后 为什么是合适的时候 什么是合适的时候信号什么时候被处理 对内核态与用户态的理性认识        信号处理 ​编辑为什么一定要切换成用户态才能执行信号捕捉方法 sigaction函数 六、可重入函数  七、volatile 如何解决这个问题呢 为什么是这种现象呢 八、SIGCHLD信号 一、生活中的信号 信号和信号量两个有关系吗 毫无关系就如同老婆和老婆饼毫无关系。 背景知识 生活中有没有信号的场景呢 肯定有eg:闹钟红绿灯烽火台的烽火你妈的脸色信号枪鸡哥叫声...当这些个场景触发的时候我立马就知道我接下来该做什么eg:闹钟响会意识到该起床了红绿灯变绿意识到该过马路了鸡哥叫意识到你太美...  这些场景都是给人看的。 是不是只有这些场景真正的放在我面前的时候我才知道怎么做呢 其实和场景是否被触发没有直接关联的对于信号的处理动作我们早就知道了甚至远远早于信号的产生。我们是怎么知道该怎么做的呢我们对特定事件的反应是被教育的结果!被教育的本质是你记住了。          在linux中信号的产生 - 信号是给进程发的 -进程要在合适的时候要执行对应的动作。  进程在没有收到信号的时候进程知道不知道应该如何识别哪一个是信号以及如何处理它 必须知道。它是怎么知道的呢曾经编写OS的工程师在写进程源代码的时候就设置好了。 进程具有识别并处理信号的能力远远早于信号的产生         但是对于日常生活中信号场景产生的时候我们立即可以做出相应的动作吗 答案是否定的听到鸡叫你不一定会立马起来学习听到闹钟你有可能会关了闹钟继续睡觉或者压根管都不管继续睡觉听到信号枪你有可能因为在想其他事情也不会立马起跑....在生活中我们收到某种“信号”的时候并不一定是立即处理的。因为信号随时都会产生但是我当前可能做着更重要的事情eg:你在等外卖的时候正在调试代码。外卖小哥敲门的时候你正调试到关键时刻所以你就没有立马开门而是让外卖小哥多等了几分钟你才拿的外卖。 在linux中进程收到某种信号的时候并不是立即处理的而是在合适的时候。 既然信号不能被立即处理已经到来的信号是不是应该暂时被保存起来 应该eg:你在等外卖的时候正在调试代码。外卖小哥敲门的时候你正调试到关键时刻所以你就没有立马开门而是让外卖小哥多等了几分钟你才拿的外卖在小哥等待过程中你就记着外卖小哥在等你这个事情作为人我们会将信号保存到大脑里面。 在linux中进程收到信号之后需要先将信号保存起来以供在合适的时候处理保存信号的地方就是进程控制块struct task_struct中 信号本质也是数据信号的发送就是往进程task_struct内写入信号数据。 task_struct是一个内核数据结构可以定义进程对象。内核不相信任何人只相信自己那么是谁向进程的task_struct内写入信号数据呢 答案是OS无论我们的信号如何发送本质都是在底层通过OS发送的 二、技术应用角度的信号  验证信号 对于这样一段代码 #includestdio.h #includeunistd.h int main() { while(1) { printf(hello world\n); sleep(1); } } 执行结果显而易见的是一直在打印hello world。但如果我想终止这个进程我们就可以直接Ctrlc 在linux中我们可以用kill -l 查看信号列表一共62个信号因为这里main没有32,33 前31个称为普通信号(1-31)后31个称为实时信号(34-64)  键盘CTRLC的本质就是我们向指定进程发送2号信号如何证明呢 首先了解一个函数signal 作用修改指定的一个信号每个进程对信号的处理是有默认动作的我们可以通过该函数修改进程对信号的默认处理动作 。第一个参数是int类型刚刚kill -l查看的信号我们除了可以看见信号名称外还能看到编号在系统中信号编号就是整数每一个信号都是被#define定义出来的我们既可以使用信号名又可以使用数字。这里的参数就是信号编号。第二个参数是返回值为void参数为int的函数指针。对上述代码进行改进 执行结果 我们首先看到进程一直跑并没有调用handler这个函数只有当我们CtrlC的时候此时就执行了handler函数并且发现确实是对18537这个进程发送了2号信号。 我们发送2号信号也是同样的结果  ps:crtl\ 终止这个进程 验证下键盘中的按键对应哪些信号 执行结果 我们发现Ctrl c 对应2号信号  Ctrl \ 对应3号  Ctrl z 对应20号。 我们如果运行./mytest 叫做该进程在后台运行。我们发现此时crtlc/ \ / z是没有任何结果的。 如何使该进程停下呢  我们可以使用kill -9 命令杀死该进程。  信号的产生方式其中一种就是通过键盘产生键盘产生的信号只能用来终止前台进程也就是说只能用来终止那些阻塞你执行命令的程序。后台进程我们可以使用kill命令杀死进程。 一般而言进程收到信号的处理方式有三种 1.默认动作一部分是终止自己暂停等等。2.忽略动作是一种信号处理的方式只不过动作就是什么也不干。3.自定义动作我们刚刚使用的signal方法就是修改信号的处理动作默认变为自定义动作。我们一般把自定义动作称为信号的捕捉。 让不同的信号执行不同的动作 执行结果 我们发现crtlc和crtl\ 或者发送2号或者3号信号都执行我们设置的动作但是9号信号缺不符合我们的预期没有打印出对应的动作因为9号信号不可以被捕捉自定义 三、信号产生前 信号产生的方式有哪些呢 1.键盘产生 2.进程异常也能产生信号 1.键盘产生信号 比如在程序运行的时候我们可以通过键盘的ctrlccrtl\crtlz终止进程 2.程序中存在异常问题产生信号导致进程收到信号退出。  eg: 进程异常产生信号 在vs下这叫做程序的崩溃准确来说这个叫做进程的崩溃。 进程为什么会崩溃呢 当我们去掉上面的注释再次运行。 我们发现此时程序直接收到11这个信号在疯狂打印。 我们稍微的改下handler让其执行完switch后直接退出进程。 再次执行 对比刚开始的代码起初我们运行是会进程崩溃的但是现在收到了11号信号。因此可以得出程序崩溃就是因为收到了11号信号。如何证明呢 我们将代码进行修改 执行结果  程序正常运行当我们发送11号信号的时候进程果然崩溃。 我们的进程在运行时崩溃的根本原因在于程序在运行时出现了野指针问题导致进程收到了11号信号进而导致进程崩溃。 eg2: 我们发现终止的时候收到了8号信号。 我们不进行信号捕捉  此时就会告诉我们浮点数指针异常。 在windows或者linux下进程崩溃的本质是进程收到了对应的信号然后进程执行信号的默认处理动作杀死进程。 为什么我会收到信号呢 除0操作对应CPU野指针对应内存只要是硬件就要受到CPU的管理工作。而你自己的代码只要除0了是CPU给你除0了CPU内部是有状态寄存器的会记录运算结果。如果除0了就会在硬件上有所体现当进行野指针访问的时候我们要把虚拟地址转换成物理地址就要借助页表MMU。当虚拟地址和物理地址没有对应的关系那么帮助转化的硬件也会报错。所以软件上面的错误通常体现在硬件或者其他软件上一旦进程里面有异常信息是在硬件上体现的。而OS是硬件的管理者OS就要对硬件的“健康”负责。代码是你的进程里的代码这些代码异常了导致硬件不健康OS就会直接对你的进程发送信号在合适的时候终止进程。 我们目前见到的所有异常都是OS对你的进程发信号导致的。语言层面的捕捉异常就是对信号的处理。 当进程崩溃退出的时候最想知道什么 无疑是崩溃的原因。我们可以用waitpid中的status的次低7位获取到进程退出的信号知道信号我们就知道了崩溃的原因。除此之外我们还想知道进程是在哪一行崩溃的。 在linux中当一个进程退出的时候它的退出码和退出信号都会被设置正常情况。 当一个进程异常的时候进程的退出信号会被设置表明当前进程退出的原因。 如果必要OS会设置退出信息中的core dump标志位并将进程在内存中的数据转储到磁盘当中方便我们后期调试。 可是对于以下崩溃的代码并没有看到任何的现象。默认情况在linux云服务器core dump这项技术是被关掉的。  如何证明core dump是被关掉的呢 使用ulimit -a查看系统资源我们发现第一个core file size这个是被关掉的。 我们自己手动设置下让其允许core dump 此时我们在运行刚刚有/0错误的程序发现多了一个core dumped,这就叫做我的进程代码被core dump了。 此时在当前路径下,我们发现多了一个core文件这个4338就是刚刚崩溃进程的pid,打开这个文件发现是一堆乱码。这个文件将来是可以帮助我们调试的。 进行调试 首先将我们的Makefile 增加一个 -g选项允许你的程序被gdb调试 开始调试 输入core-file core.pid,就可以看到该进程崩溃的原因并且是在第几行崩溃了 我们先让程序出异常然后在用gdb调试直接用core-file 命令得到了错误的原因和错误的行数。这种方案我们称为事后调试。 同时我们也能充分理解core dump标志。进程如果异常的时候被core dumpcore dump该位置被设置成1。 不一定所有的信号都会形成core dump文件 证明如下 先把程序改正确 我们发送2号信号此时进程退出了但是并没有收到core dump  我们发送3号信号发现是有的 我们发送9号信号发现进程被杀死了但是没有core dumo 所以不一定所有信号都有core dump 文件但是只要因为信号终止那么该信号都会被设置但有没有core dump文件是由core dump标志位决定。  core dump位变成1证明当前我们的子进程是会形成core dump文件的。  3.通过系统调用接口产生信号 kill系统调用  第一个参数传进程的pid第二个参数传信号。功能就是我要给某个进程发送信号。 我们模拟实现一个kill命令。 我们用一个sleep 进程做测试 我启动了一个sleep进程。我用我自己的kill命令给这个sleep进程发送9号信号那么这个sleep进程就被kill掉了。此时就叫做采用系统调用对目标进程发送信号。 raise调用 给自己发一个信号。 eg:给自己发送一个8号信号 abort函数 给自己发送一个确定的信号叫做abort信号。abort是6号信号。abort函数使当前进程接收到信号而异常终止。 eg: 我们对所有信号进行捕捉看看abort是那个信号  执行结果    证明abort确实是6号信号。 4.软件条件也能产生信号         软件条件通过某种软件OS来触发信号的发送系统层面设置定时器或者某种操作导致条件不就绪等这样的场景下触发信号的发送。 进程间通信当读端不光不读而且关闭了读端的fd写端一直在写最终写进程会收到sigpipe13就是一种典型的软件条件触发的信号发送。OS发现此时不具备写入的条件直接终止。 alarm 设置一个计时器会延迟的向我们发送一个信号。比如second设置成10就代表着过10秒后给我们发送一个sigalrm信号也就是14号信号。  这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。 eg:证明alarm是14号信号  eg:alarm的返回值是0的情况。  执行结果 eg:alarm的返回值是非0的情况。 eg:统计一下1秒以后我们的计算机能把数据count累加到多少   执行结果统计到7万多  我们稍微修改下代码它的功能也是进行计数。 执行结果 我们发现此次计数达到了5亿多次。效率翻了7000倍。 为什么第一种计数很慢呢 根本原因就是有IO。根据体系结构如果是第二种情况是不会进行体系访问的计算都是纯CPU的最多和内存交互一点点效率是非常高的。而第一种情况加了printfprintf是IO函数会向显示器上打印而且消息传出是经过网络的服务器在远端你在本地所以变的更慢了。 所以有IO和没有IO基本上是数量级上的差别。所以程序出现了大量的IO一定要考虑下效率问题。 信号产生的方式种类虽然非常多但是无论产生信号的方式千差万别但最终一定是通过OS向目标进程发送的信号。换句话说键盘产生的实际上不是键盘产生的是键盘让OS产生的比如crtlc,是OS将你的组合键解释成信号发送给目标进程操作异常了并非是异常导致自己给自己发信号而是因为异常引发了硬件错误被OS知道OS向目标进程发信号系统调用就是OS提供的接口底层就是OS在发信号所谓的软件条件都是OS检测到某种异常发送信号。这些全都是OS所做的。 产生信号的方式本质都是OS发送的 如何理解OS给进程发送信号 首先了解下进程保存信号的方式信号的编号是有规律的[1,31]进程中采用位图来表示该进程是否收到信号 OS给进程发送信号-OS发送数据信号给task_struct-本质是OS向指定进程的task_struct中的信号位图写入比特位1即完成信号的发送。信号的发送换一种说法就是信号的写入。  四、信号产生中 信号在产生的时候不会立即进行处理而是在合适的时候在此信号就会被保存起来。现在我们着重讨论下信号保存的状态。 信号其他相关常见概念 实际执行信号的处理动作称为信号递达(Delivery) 信号递达的凡是有三种自定义捕捉默认忽略。信号从产生到递达之间的状态,称为信号未决(Pending)。 本质是这个信号被暂存在task_struct信号位图中这就是未决状态。进程可以选择阻塞 (Block)某个信号。 阻塞的本质是OS允许进程暂时屏蔽指定的信号屏蔽带来的结果有两种 1.该信号依旧是未决的。2.该信号不会被递达直到解除阻塞方可递达。 忽略和阻塞有区别吗 忽略是递达的一种方式。阻塞是没有递达是一种独立状态。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。 信号在内核中的表示 信号在内核中的表示示意图共有三张表block表pending表handler表我们刚刚说的产生信号的本质是OS向指定进程的task_struct中的信号位图写入比特位1即完成信号的发送-信号的写入。这里写入的进程的表称为信号的pending位图。所以确认一个进程是否收到信号我们称为pending。换句话说OS发送信号的本质就是修改目标进程的pending位图。 handler表叫做void(*handler[31])(int) ,它叫做函数指针数组里面放的是默认忽略自定义的函数指针地址(这个函数指针数组有多少个看操作系统) 。handler根据信号标号去索引函数指针数组就直接可以找到处理方法。SIG_DFL叫做处理该信号被默认SIG_IGN就叫做处理该信号被忽略。 所以signal函数的原理就是根据信号编号索引这个数组下标然后把handler函数的地址填到对应的下标内容处此时对指定信号的处理方法就发生作用了。就是把函数地址传进去特定信号的特定方法我们就有了。 我们在系统中查找下 SIG_DFL SIG_IGN 我们可以看到它俩就是被0和1做了一个__sighandler_t这个函数指针的强转。 所以如果在OS层面上发现这个地址值强转过来是0那就是默认是1就是忽略。否则填的一定是具体函数地址最终表明就是自定义捕捉。 block表本质上也是位图结构。叫做uint32_t block; 比特位的位置代表信号的编号。比特位的内容代表代表信号是否被阻塞。1就代表被阻塞0代表没有。阻塞位图也叫作信号屏蔽字。 信号处理的过程 一个信号任何时候都可以被pending但是递达不递达由block说了算。 总结识别一个信号采用三元组方式是否被block是否被pendinghandler方法是什么结合这三个信息我们就可以知道这个信号该被怎么处理这三个信息合起来就叫做进程是可以识别信号的。 代码验证信号 不要只认为有接口才算是系统调用也要意识到OS也会给用户提供数据类型配合系统调用来完成。 sigset_t 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。 信号集操作函数 sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的 #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); 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。函数sigfifillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfifillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。 sigprocmask 调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集修改的是进程的block表 #include signal.h int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值:若成功则为0,若出错则为-1 set是输入性参数oset是输出型参数。 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。         eg:我们屏蔽一个2号信号  执行结果2号信号是crtlc,现在crtlc与发送2号信号对该进程都是不起作用的。    eg: 现在我们在屏蔽下9号信号 执行结果我们发现9号信号是无法被屏蔽的 9号信号被称之为管理员信号是不能被屏蔽和自定义捕捉的必须永远遵守默认行为。所以不存在一个将所有信号都屏蔽的进程。 sigpending 返回值:若成功则为0,若出错则为-1。不对pending位图做修改而只是单纯的获取进程的pending位图。这个参数就是一个输出型参数。pending位图是由OS进行修改的  由以上函数我们可以做一个小实验如果我的进程预先屏蔽掉2号信号因为2号信号递达了默认动作会退出进程不断的获取当前进程的pending位图并打印显示000000000然后手动发送2号信号因为2号信号不会被递达所以不断获取当前进程的pending位图并打印显示010000000 代码如下我们先屏蔽2号信号我们pending这个位图不断获取当前进程的pending,最后在检测一下当前进程的pending的信号。 执行结果 起初没有发送任何信号一直是0000000当我们发送2号信号的时候变成了0100000000。我们直观的看到了信号被pending的状态。 观察恢复2号信号发生的情况。我们设置了一个计时器20秒号恢复2号信号。 在20秒内发送一个2号信号但是我们并没有观察到由0变1在由1变0而是在20秒的时候直接退出了因为2号信号递达的默认动作就是终止进程所以看不到现象。 我们想要看到现象只需要对2号信号进行自定义捕捉一下。 执行结果我们观察到了我们预期的现象。而且是先把信号给处理了再进场printf的打印 五、信号发送后 信号被发送后不是被立即处理的而是在合适的时候这个合适是什么时候为什么是合适的时候 为什么是合适的时候 因为信号的产生是异步的它在任何时候都可能产生信号在信号产生期间我的进程可能一直都在运行当前进程可能会在做着更重要的事情。所以我们会将信号做延时处理这个取决于OS和进程。 什么是合适的时候信号什么时候被处理 因为信号是被保存在进程的PCB中pending位图里所谓的处理一定要进行检测递达默认忽略自定义。当进程从内核态返回到用户态的时候进行上面的检测并且处理工作。 用户态就是用户代码和数据被访问或者执行的时候所处的状态。我们自己写的代码全部都是在用户态执行的。内核态执行OS的代码和数据时计算机所处的状态就叫做内核态。OS的代码的执行全部是在内核态。 他们的主要区别在于权限内核态的权限远大于用户态如果一个用户的进程出现越界野指针的问题OS可以直接将用户对应的进程通过进程杀死。如果OS内出现了越界野指针的问题OS就直接可能崩溃调了因为内核和用户相比较用户状态是被管控的状态当用户出问题的时候OS可以控制它。但如果是OS本身的问题极有可能导致OS直接崩溃。我们实际上写代码的时候我们可能在不断的从用户态切入内核态在从内核态切换用户态。最典型的表现就是调用系统调用。比如open函数的实现是在OS里面我们调用的是open系统调用接口我们上层调用open得到返回值打印这个返回值等待都是用户态代码当我们陷入内核的时候除了进入到系统函数本身还要发生一个身份的变化由用户变成了OS。 用户调用系统调用的时候除了进入函数身份也会发生变化用户身份变成内核身份 。 对内核态与用户态的理性认识        以上是我们关于用户和内核态感性的认识接下来是理性的认识。用户的身份是以进程为代表的。 用户的数据和代码一定要加载到内存那么OS的数据和代码呢 也是一定要加载到内存中。eg:电脑开机就是把OS的代码和数据加载到内存中。 OS的代码是怎么被执行到的呢 假设只有一个CPU我们之前学到的页表是用户级页表每个进程都有一份而在OS启动之后也会有一个系统级别的页表。每个进程的进程地址空间都会有3G的用户空间和1G的内核空间因为我们要保证每个进程看到的都是4G的空间每个用户的页表是不同的进而能映射到不同的位置来保证进程的独立性。而另外一张系统级页表也叫内核页表而且整个系统只有一份内核进程被所有进程共享也就意味着每个进程可以将自己的整个内核空间的代码和数据经过内核页表映射到OS的物理内存的不同位置从而能让这个进程能找到OS的代码和数据。这样就能保证每个进程既能看到它的代码又能看到OS的代码。在CPU中有个CR3寄存器为0的时候代表OS为3的时候代表普通用户进程在执行的时候想要知道自己是用户态还是内核态就查这个CR3寄存器。 进程具有了地址空间是能看到用户和内核的所有内容的不一定能访问。如果CR3为0就代表这个进程能访问1G的内核空间就是内核态。否则就只能访问3G的用户空间是用户态。 CPU内有寄存器保存了当前进程的状态。用户态使用的是用户级页表只能访问用户数据和代码内核态使用的是内核级页表只能访问内核级的数据和代码 进程间无论如何切换我们能保证我们一定可以找到同一个OS因为我们每个进程都有3~4G的地址空间使用同一张内核页表。 所谓的系统调用OS给你提供了一种方法让你在你的代码里用接口当你实际访问这个方法时因为要系统调用所以OS可以将你的身份变成内核级通过又一种叫做中断实现的虽然要系统调用但系统调用的实现没有起始地址有然后进程切换成内核态就允许访问内核页表然后我在系统调用通过内核页表找到OS的代码然后在进程的上下文中执行系统调用。总结一下就是进程的身份转化成为内核态然后根据内核页表找到系统函数执行就行了。 在大部分情况下实际上我们OS都是可以在进程的上下文中直接运行的因为OS的代码被映射到了进程的地址空间。eg:进程切换当时间片到了的时候我们立马把当前进程的状态改成内核态然后执行内核态的进程切换的代码就相当于OS把你的进程从CPU上拿下来在把另一个进程放上去而且并不影响OS因为每个进程的进程地址空间内核空间是一样的。         信号处理 程序有很多的方式进入内核比如自身是死循环或者调用了系统调用等一定存在从用户到内核再从内核到用户。 eg:我们的代码某一行里有系统调用就要陷入内核执行对应系统调用的代码执行完毕之后在返回到用户态这就完成了一次系统调用而且完成了一次从用户态到内核态内核态到用户态的状态切换。 实际上执行信号处理在内核态执行完系统调用返回到时候要对该进程进行信号检测假设某个信号没有被block但是被pending了接下来就要处理它一共有三种方案默认忽略自定义 如果是默认比如是终止进程就直接把进程的相关资源释放掉就可以了我现在就是内核态如果是暂停我把进程状态设置成stop并把进程的PCB放到等待队列里就可以了然后再直接返回用户态下一行。如果是忽略将pending由1置0直接返回用户态的下一行代码如果是自定义捕捉在用户态有我们实现的handler方法我们就需要从内核态直接返回到用户态执行handler方法当handler方法执行完我们不能直接返回下一行代码必须保证是从内核返回到下一行因此我们执行完handler需要再次返回到内核执行一个系统调用sys_sigreturn().返回用户态的下一行代码。 这就是信号处理的完全过程。 现在我们进行高度抽象 为什么一定要切换成用户态才能执行信号捕捉方法 用户执行OS的代码是不可能的因为用户身份不够但是目前我是OS我为什么不直接执行用户的代码呢OS在理论上是可以执行用户态的代码的因为OS权限高。但是OS不相信任何人 1.把自己保护好用户只能调用OS提供的接口。2.轻易不执行用户的代码。eg:如果我的handler里封装了个rm -rf \ . 如果OS执行了此时就相当于OS以它的身份把整个系统干掉了。所以如果以内核身份执行用户代码会存在用户会写一些恶意代码的情况。所以OS不能轻易执行他人代码。 sigaction函数 作用上是和signl一模一样的  #include signal.h int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 这个函数要比signl复杂因为它考虑了实时信号act是一个输入性参数就是说你想对这个信号执行什么动作你可以把你的动作方法填入到这个结构体里当信号就绪时执行oact是一个输出型参数你设置这个信号的老的方法是什么它会带回老的信号的方法不想要设为NULL。  sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体: 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。 sigaction结构体 我们主要关系这个两个第一个就是函数方法。 eg:对2号信号做处理  表handler方法设置成忽略SIG_IGN  执行结果 将handler方法设置成默认SIG_DFL 执行结果 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 eg: 我们处理2号信号屏蔽下3号为了方便演示我们将信号处理放法设置成死循环、PS平时我们的handker方法不能设置成死循环如果设置了也一定要设置退出。  执行结果在开始执行2号信号的方法后再次执行2号信号3号信号是没有反应的 如果我把2号信号屏蔽了我给你的进程发送100个2号信号此时你的进程只能记住一个Linux对普通信号是可能丢失的2个以上因为记录信号的标志位只有一个比特位如果把2号信号屏蔽了发送100个OS最终只记住一个(最新的那一个发一次写一个)。不可能丢失的信号叫做实时信号在内核中是以链表队列的形式来把所有的实时信号链到PCB里面的来一个链接一个。本质是底层数据结构的区别。  六、可重入函数  首先我们看下带头结点的单链表的insert 我们之前写这个代码是没有任何问题的是在单执行流下但现在多了多执行流。多进程也有多执行流但是进程具有独立性所以不会造成任何问题。当我们执行我们的进程代码逻辑时因为系统调用时间片到了进程切换等各种可能的因素陷入内核当我从内核返回用户态我要做信号检测检测时可能要做信号捕捉当我执行信号捕捉的时候就进入了另外一种执行流。如果我们在整个进程的生命执行流中没有收到任何信号就说明信号捕捉函数决定没被调用。我们有可能只执行我们进程的代码也有可能因为收到信号执行信号捕捉流程。所以在单进程也可能存在多执行流的情况。它们两个是毫不相关的执行流。 现在我们看这个代码我们要在main函数中执行头插insert然后就是把node1插入到这个链表中在insert函数中第一步就是让node1这个节点指向head指向的第一个节点但是刚刚做完这一步的时候 信号来了恰好这个进程捕捉这个信号代码在任何时候都有可能进行信号捕捉从用户到内核态在任何时间都是可能发生的因为进程有可能因为时间片到了进行进程切换不一定是必须系统调用才可能发生于是我就不执行下一步让head指向node1了而是直接处理信号去了处理信号的时候发现这个信号捕捉函数是要进行插入node2node2于是就插入完了信号捕捉完了就返回到上一次执行的语句继续向后执行于是执行了head指向node1。此时head指向的是node2但是回来以后head指向了node1。最终造成node2找不到了也就是node2节点丢失也称之为内存泄露问题。导致这个问题的根本原因就在于主执行流在执行插入的时候因为信号到来信号捕捉执行流也执行了插入。这种现象就成为insert函数被重复进入了。 insert函数一旦重入有可能出现问题该函数就叫做不可重入函数。 insert函数一旦重入不会出现问题该函数就叫做可重入函数。 所以可重入与不可重入是描述函数的概念。 我们学到的大部分函数STLboost库中的函数大部分都是不可重入的。 如果一个函数符合以下条件之一则是不可重入的: 调用了malloc或free,因为malloc也是用全局链表来管理堆的。 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。 可重入函数是指在函数内部不使用任何共享变量或状态每次调用函数时都能够获得相同的输出结果并且多个线程可以同时调用这个函数不需要加锁进行同步。 eg: #include stringstd::string reentrantFunction(const std::string str) {// 临界区代码// 可重入函数std::string result str str;return result; }在上面的例子中这个可重入函数内部没有使用任何共享变量或状态因此可以被多个线程同时调用而不会出现线程安全的问题。 在多线程环境中如果可重入函数内部没有使用任何共享变量或状态那么它就是线程安全的可以被多个线程同时调用。这是因为每个线程在调用可重入函数时都会拥有自己的栈空间函数内部的局部变量、参数等都会被复制到线程自己的栈空间中从而保证线程之间不会互相干扰。 需要注意的是如果可重入函数内部涉及到共享变量或状态那么就需要考虑加锁的问题了以保证线程安全。在实际应用中需要根据具体情况来决定是否需要在可重入函数中使用锁。 七、volatile 这个关键字是C语言中重要的关键字。 首先我们看下这段代码 执行结果完全符合预期  但是有一个问题刚刚我们的编译器是常规情况看到的就是这种现象但是我们的编译器是有各种优化的。gcc默认普通编译可以让用户自己设置优化级别存在O0-O4的优化级别的。 现在我们进行O3优化          我们发现在编译器经过优化后对同一份代码展现了不同的优化结果。根本原因在于编译器优化不能甄别出你的代码存在多执行流情况。我们的编译器看来认为main函数就是一个执行流handler这访问了flag但是编译器是识别不到的。所以它只发现在main函数这里while只对flag做检测意味着flag的值永远是0。编译器会将这个flag在编译器编译的时候优化到寄存器中也就是说不需要在经历冗长的寻址。一般的flag是全局变量是变量就应该在进程运行时在内存开辟空间换句话说CPU识别flag就一定要从内存里读flag读到CPU内而在CPU内做判断判断完后再从CPU内读取flag经过这样不断的读取检测flag。但是编译器发现没有任何人在main函数中对flag修改所以就把flag优化到寄存器中不再做内存级别的访问了直接识别CPU寄存器相关的信息。 如何解决这个问题呢 我们可以给这个flag加一个volatile. 执行结果我们照样是O3的优化级别但我们的结果又正常了。  为什么是这种现象呢 编译器是O3的优化级别。一般来说flag是全局变量是变量就应该在进程运行时在内存开辟空间换句话说CPU识别flag就一定要从内存里读flag读到CPU内而在CPU内做判断判断完后再从CPU内读取flag经过这样不断的读取检测flag。但是编译器发现没有任何人在main函数中对flag修改所以就把flag优化到寄存器中不再做内存级别的访问了直接识别CPU寄存器相关的信息因为内存和CPU的效率差别是很大的。 我们的编译器看来认为main函数就是一个执行流handler访问了flag但是编译器是识别不到的因为它改的并不是CPU寄存器的flag而改的是内存当中的flag。所以在优化场景下CPU和内存之间出现了寄存器缓存了一段数据而屏蔽了内存中数据的场景。所以它只发现在main函数这里while只对flag做检测意味着flag的值永远是0。这个是优化后的结果但并不是我们想要的。 volatile的作用告诉编译器不要对我这个变量做任何优化读取数据必须贯穿式的读取内存不要读取中间缓冲区寄存器中的数据保存内存的可见性ps:还有个作用是指令重排了解下即可 八、SIGCHLD信号 多进程中我们可以用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。eg:刚开始运行没有任何反应但是子进程一旦退出因为对SIGCHLD做捕捉了所以父进程在while循环期间一定能收到一个SIGCHLD的信号进而执行打印逻辑执行结果我们借助一个监控脚本观察 while :; do ps axj | head -1 ps axj | grep test |grep -v grep; sleep 1; echo ##################################; done 我们确实证明子进程退出的时候会向父进程发送SIGCHLD信号。换言之如果我是可以直接在handler方法里调用waitpid回收子进程的。此时父进程就不用主动等待子进程退出。 但是父进程就不想回收压根就不关心这个子进程的退出码等信息并且子进程退出的时候不形成僵尸进程不要影响父进程。我们就可以显示设置忽略17号信号。 执行结果此时子进程直接退出且没有形成僵尸进程。  所以不是所有的场景都需要我们去等待要按照场景去使用。这个方法只在linux下是有效的。 eg:使用信号的自定义捕捉进行等待 #include stdio.h #include stdlib.h #include signal.h void handler(int sig) {pid_t id;while ((id waitpid(-1, NULL, WNOHANG)) 0){printf(wait child success: %d\n, id);}printf(child is quit! %d\n, getpid()); } int main() {signal(SIGCHLD, handler);pid_t cid;if ((cid fork()) 0){ // childprintf(child : %d\n, getpid());sleep(3);exit(1);}while (1){printf(father proc is doing some thing!\n);sleep(1);}return 0; } 我用的是while循环和WNOHANG非阻塞等待用循环的原因是为了满足各种子进程退出的情况eg:我创建了10个子进程10个子进程同时退出了每个子进程都同时向父进程发送信号可是pending位图只有一个比特位记录信号如果只wait一次就只能wait一个子进程剩下9个就wait不到。通过while循环我们就可以把所有的子进程都读到。 用非阻塞的原因假如你是阻塞等待有10个子进程5个退出了5个没退出。你循环读也没有任何问题但是当你读第6次的时候子进程没退出你就在信号捕捉函数这里卡住了。卡住之后你就永远回不来了子进程不退出父进程不返回这就叫做阻塞等待。可是不是只有5个进程退出吗为什么还要读第六次因为刚刚我们是在上帝视角我们知道有5个进程退出了但是如果我们不知道呢eg:你向你爸要零花钱你第一天向你爸要了100你爸给你了第2天你又向你爸要了100你爸给你了当第三天的时候你肯定还会问你爸要。同理当你读取一个子进程你就继续读只有当你读取失败的时候你才知道底层没有子进程退出了。所以这里要用非阻塞。
http://www.hkea.cn/news/14346564/

相关文章:

  • 网站开发项目外包宁夏建网站报价
  • 网站可以用cdr做吗阜蒙县建设学校官网网站
  • 网站建设与网页制作的实验目的阜宁城乡建设局网站
  • 怎样找人做网站软件定制解决方案
  • 网站开发人员需要去做原型吗做毕业设计网站教程
  • 站点与网站有什么区别wordpress图片打叉
  • 服务器可以做网站网站怎么上线
  • 齐河专业企业网站建设微股东微网站制作平台
  • 孝感公司做网站wordpress主题网店
  • iis 访问网站需要进行身份验证网站建设用户核心
  • 帝国cms下载类网站怎么做wordpress 外贸
  • 适合毕设做的简单网站网站搭建好有什么内容可以修改
  • 福州网站建设名列前茅国外网站建设企业
  • 网站怎么做一级域名跳转网站兼容ie代码
  • 东莞市建设局网站首页广州外贸网站建设推广
  • 济南学网站建设哪里好rss订阅wordpress
  • 企业建设营销型网站有哪些步骤进口网站建设
  • 网站开发学生鉴定表网站分页效果
  • 龙之向导外贸网站asp.net网站制作步骤
  • 服装设计有哪些网站网站月流量
  • 集团网站推广旅游网站系统建设方案
  • 怎么做提卡密网站万游昆明做网站哪家好
  • 网站建设中可能升级河北网诚网站建设
  • 免费建站工具网站原型图是什么
  • 哪个网站做员工增员做响应式网站哪家公司好
  • 成都专业网站建设公司重庆营销型网站开发
  • 定制开发网站如何报价wordpress4.8漏洞
  • 手机门户网站wordpress的数据库主机
  • 文化网站设计经典案例学市场营销后悔死了
  • 可以找酒店案例的网站学网络运营去哪里学