西安网站建设优化,河北邯郸特色美食,绵阳网站的建设,软文推广套餐目录
信号保存
信号其他相关常见概念
在内核中的表示
信号集操作函数
信号处理
信号处理流程
操作系统是如何运行起来的#xff1f;
硬件中断
时钟中断
死循环
软中断
内核态与用户态
信号捕捉的操作
可重入函数
volatile
SIGCHLD信号 信号保存
信号其他相关…目录
信号保存
信号其他相关常见概念
在内核中的表示
信号集操作函数
信号处理
信号处理流程
操作系统是如何运行起来的
硬件中断
时钟中断
死循环
软中断
内核态与用户态
信号捕捉的操作
可重入函数
volatile
SIGCHLD信号 信号保存
信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery)信号从产生到递达之间的状态称为信号未决(Pending)进程可以选择阻塞 (Block )某个信号。若某个信号被阻塞了这个信号产生了一定会将这个信号pending保存但是永远不递达执行除非解除阻塞。这里的阻塞与之前的scanf、cin等lO完全不同。被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动 作。阻塞是不会被递达的而忽略是会被递达的。所以这两个概念是不同阶段的概念。
在内核中的表示
信号在内核中的表示示意图 在每个进程的PCB中都会维护3张表
a. pending
称为pending位图是一个位图[131]表示当前进程收到的信号列表理论上使用一个int就行了但是这里使用的是一个自定义的类型。这里[1, 31]是因为普通信号就是1-31 比特位的位置信号编号 比特位的内容1/0是/否收到对应的信号。
b. handler
handler_t XXX[N]函数指针数组 信号编号-1就是函数指针数组的下标对应的内容就是处理这个信号的方法 所以signal(2handler的本质就是根据第一个参数去修改handler表中的内容
c. block
是一个位图[1,31] 比特位的位置信号编号 比特位的内容1/0是/否阻塞对应的信号
如果想阻塞一个信号与当前进程是否收到这个信号有没有关系呢是没有关系的因为是两张位图。
当我们将这3张表横着看就可以知道每一个信号的处理方法了。之前说过进程能够识别信号是内置的因为内核中有相关的数据结构所以是内置的。
我们来看看在Linux内核中这3张表的结构
struct task_struct {/*signalhandlers*/struct sighand_struct *sighand;sigset_t blocked;struct sigpending pending;
};
typedef struct{unsigned long sig[_NSIG_WORDS];
}sigset_t;
sigset_t是一个结构体就是结构体套了一个数组本质就是一个位图。不会直接使用一个整数因为那样可延展性太差了。
struct sighand_struct{atomic_tcount;struct k_sigactionaction[_NSIG];// #define _NSIG64spinlock_tsiglock;
};
struct k_sigaction{struct __new_sigaction sa;void_user* ka_restorer;
};
struct __new_sigaction{__sighandler_t sa_handler; // 这个就是函数指针数组unsigned longsa_flags;void(*sa_restorer)(void) ;new_sigset_t sa_mask;
};
typedef void (*__sighandler_t)(int);
struct sigpending {struct list_head list;sigset_t signal; // 位图
};
可以看到两张位图的数据类型都是sigset_t而sigset_t里面就是一个unsigned long类型的数组sigsig[0]是一个unsigned long类型类型的整数这个整数有64个比特位就能表示64个信号当然我们这里只看1-31号信号。因为数据的类型都是sigset_t所以未来要访问这两张表都需要使用sigset_t类型来操作。sigset_t是OS提供给用户的一个数据类型。这里的sig[1]、sig[2]等是可以用来扩展位图容量的这也正是为什么不使用整数的原因整数的延展性太差了。
从上图来看每个信号只有一个bit的未决标志非0即1不记录该信号产生了多少次只记录这个信号是否产生过阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储sigset_t称为信号集这个类型可以表示每个信号的“有效或”无效状态在阻塞信号集中有效和无效”的含义是该信号是否被阻塞而在未决信号集中“有效和“无效的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集(block表)也叫做当前进程的信号屏蔽字(SignalMask)这里的屏蔽应该理解为阻塞而不是忽略。
信号集操作函数
接下来的操作都是围绕这3张表展开的因为这3张表是OS的内核数据结构不允许我们直接访问需要通过系统调用来访问并且我们之前的操作也是在操作这3张表。有两张表是位图我们是不建议使用位操作直接对两张表就行操作的Linux操作系统也提供了一组函数可以对两个信号集直接就行比特位级的操作。sigset_t是OS提供给用户的一个数据类型所以用户是可以用它来定义一些对象的但是不建议自己对定义出的对象进行操作而是使用针对这个数据类型的接口更好这批接口就是对位图的增删查改。
#include signal.hint sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 将信号集全部置为1
int sigaddset(sigset_t *set, int signum); // 向指定的信号集中添加信号
int sigdelset(sigset_t *set, int signum); // 从指定的信号集中移除信号
int sigismember(const sigset_t *set, int signum); // 判断一个信号是否在指定的信号集中sigprocmask
sigprocmask是用来检查和修改信号屏蔽字(block表)的系统调用
#include signal.hint sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how的取值有3个
SIG_BLOCKset包含了我们希望添加到信号屏蔽字的信号相当于mask mask | setSIG_UNBLOCKset包含了我们希望从当前信号屏蔽字解除阻塞的信号mask mask~setSIG_SETMASK设置当前信号屏蔽字为set指向的值相当于mask set
set是一个输入型参数也是一个信号屏蔽字如何修改block表是由how和set共同决定的。oldset是一个输出型参数。是保存的老的信号屏蔽字方便恢复。调用成功返回0失败返回-1并设置errno。
sigpending
sigpending是获取当前进程的未决信号集(pending表)的系统调用
#include signal.hint sigpending(sigset_t *set);
set的一个输出型参数调用成功返回o失败返回-1。不需要提供修改pend表的方法因为之前学的5种产生信号的方法都会修改pending表。
之前学的signal就是修改handler表的。
现在通过系统调用我们已经能够操作这3个表了。
我们来写一段综合代码首先将2号信号屏蔽然后不断地获取并打印pending表然后再给这个进程发送2号信号此时2号信号肯定不给被递达也就不会调用它的handler方法但是我们可以看到pending表的变化。
void PrintPending(const sigset_t pending)
{std::cout curr pending list [ getpid() ]: ;for(int signo 31; signo 0; signo --){if(sigismember(pending, signo))std::cout 1 ;else std::cout 0 ;}std::cout std::endl;
}int main()
{// 1. 对2号信号进行屏蔽// block和oblock都是栈上的临时变量可能是乱码所以我们手动清0一下sigset_t block, oblock;sigemptyset(block);sigemptyset(oblock);// 2. 添加2号信号// 注意此时并没有设置进内核中只是将2号信号添加到了我们自己的block位图中sigaddset(block, 2);// 3. 设置进入内核中sigprocmask(SIG_SETMASK, block, oblock);while(true){// 4. 获取内核中的pending表sigset_t pending;sigpending(pending);// 5. 打印PrintPending(pending);sleep(1);}return 0;
} 因为2号信号被屏蔽了所以不会被递达所以一直是1。
上面的代码种2号信号会一直是1现在让其10秒后变成0。我们一开始仍然屏蔽2号信号等到10秒后解除屏蔽。此时就可以利用oldset了。
int main()
{// 1. 对2号信号进行屏蔽// block和oblock都是栈上的临时变量可能是乱码所以我们手动清0一下sigset_t block, oblock;sigemptyset(block);sigemptyset(oblock);// 2. 添加2号信号// 注意此时并没有设置进内核中只是将2号信号添加到了我们自己的block位图中sigaddset(block, 2);// 3. 设置进入内核中sigprocmask(SIG_SETMASK, block, oblock);int cnt 0;while(true){// 4. 获取内核中的pending表sigset_t pending;sigpending(pending);// 5. 打印PrintPending(pending);sleep(1);cnt ;if(cnt 10){std::cout 解除对2号信号的屏蔽 std::endl;sigprocmask(SIG_SETMASK, oblock, nullptr);}}return 0;
} 此时会发现解除屏蔽后进程立刻就退出了。因为我们之前已经向进程发送过2号信号等到2号信号解除屏蔽后立刻就会处理2号信号而2号信号的默认行为就是让进程退出所以进程就退出了
若我们想看到2号信号解除屏蔽后的pending表就不能让进程退出。可以自定义2号信号的行为或者直接忽略2号信号。
int main()
{::signal(2, SIG_IGN);// 1. 对2号信号进行屏蔽// block和oblock都是栈上的临时变量可能是乱码所以我们手动清0一下sigset_t block, oblock;sigemptyset(block);sigemptyset(oblock);// 2. 添加2号信号// 注意此时并没有设置进内核中只是将2号信号添加到了我们自己的block位图中sigaddset(block, 2);// 3. 设置进入内核中sigprocmask(SIG_SETMASK, block, oblock);int cnt 0;while(true){// 4. 获取内核中的pending表sigset_t pending;sigpending(pending);// 5. 打印PrintPending(pending);sleep(1);cnt ;if(cnt 10){std::cout 解除对2号信号的屏蔽 std::endl;sigprocmask(SIG_SETMASK, oblock, nullptr);}}return 0;
} 可以看到解除对2号信号的屏蔽之后2号信号被成功递达了。
信号处理
信号处理有3种方式默认、忽略、自定义前面谈的都是自定义对于默认和忽略并没有详谈
信号处理流程 我们之前说过当一个进程收到了一个信号不一定是立即处理而是等到合适的时候再处理这个合适的时候是什么时候呢
是进程从内核态切换回用户态的时候这时候回检测当前进程的Pending表和block表决定是否处理收到的信号若需要处理再结合handler表处理信号。OS的运行状态用户态、内核态。
当我们写代码时代码中有while、for、if、定义变量等都是将代码编译在地址空间的代码区此时是用户态的代码中的系统调用或者封装了系统调用的库函数如printf等此时进程会进入内核态来完成系统调用此时是内核态的。所以用户态就是CPU在执行我们自己写的代码内核态就是CPU在执行OS的代码。
信号处理的流程在执行主控制流程的某条指令时因为中断、异常或系统调用进入内核用户态-内核态)内核处理异常准备回用户模式之前先处理当前进程中的信号其实就是检测3张表若pending表全为0或者pending表为1的信号的block表为1也就是没有需要处理的信号则直接回到用户模式从系统调用的下一条开始执行若有需要处理的信号此时回查看handler表若这个信号的handler表中是SIG_DLFSIG_DLF是终止进程或者让进程暂停此时就将进程的状态设置成对应的状态若是SIG_IGN会将Pending表对应位置由1改回o然后继续向后运行所以若是SIG_DLF或者SIG_IGN在内核态返回用户态时就已经处理完信号了若是自定义的函数当处理方式是自定义时会根据do_signal跳转到用户态的自定义函数哪里并执行这个方法完成信号处理然后再回到内核态调用sys_sigreturn返回用户态从系统调用的下一条开始执行。 信号自定义捕捉会涉及4次用户态、内核态的切换。
操作系统是如何运行起来的
OS是计算机开机之后启动的第一个程序并且这个程序只要我们不关机就不会被关闭
硬件中断 当代计算机的外设是非常多的所以外设并不是直接连接CPU的而是连接在了中断控制器上如键盘被按下之后此时外设就准备就绪了就会向中断控制器发起中断中断控制器就能够得到发起中断设备的中断号也就是知道是那一台设备发送的中断而中断控制器是直接连接CPU的得到中断号后就会向CPU发送中断首先通知CPUCPU得知中断后就会去中断控制器中获取中断号。
OS为了能够处理每一个设备在设计的时候就提供了一个中断向量表我们把它理解成一个函数指针数组即可IDT[N]当我们要执行这个中断向量表中的某一个方法时就需要使用下标来访问这个下标就称为中断号中断向量表中会有对各种外设的中断从处理方法这些方法是OS内置的或者从外设对应的驱动程序中获取的。中断向量表就是操作系统的一部分启动就加载到内存中了。通过外部硬件中断操作系统就不需要对外设进行任何周期性的检测或者轮询。由外部设备触发的中断系统运行流程叫做硬件中断。
现在CPU获取了中断号而OS内部又有这个表此时就可以处理中断了。
在硬件上CPU得知有中断后CPU此时可能正在执行某项任务寄存器上都是这个任务的数值此时会先将这些上下文信息保存将CPU变成空闲状态称为CPU保护现场在软件上OS会拿着CPU获取的这个中断号去查中断向量表获取处理中断的方法。
在硬件和软件上做的事情称为中断处理历程1.保护线程 2.根据中断号n查表 3.调用对应的中断方法。中断处理完毕之后会恢复现场继续之前的工作。这样OS再也不关心外设是否准备好了外设自己准备自己的OS自己忙自己的等到外设准备好了之后会通过CPU主动告知OS
举几个例子 1.假设现在要访问磁盘从磁盘中读取数据假设读取某一个文件OS就会告诉磁盘这个文件对应的LBA地址磁盘就开始准备了OS就忙它自己的准备好了之后就会向CPU发送中断OS就会知道磁盘准备好了OS就会根据中断号去执行对应的方法将外设的数据拷贝到内存的缓冲区当中 2.假设现在要打印OS将要打印的数据放在内核缓冲区当中若显示器还没有准备好OS不管它继续忙自己的事情等到显示器准备好了之后就会像CPU发送中断OS就会知道显示器准备好了OS就会根据中断号去执行对应的方法将缓冲区内的数据刷新到外设。
时钟中断 进程可以在操作系统的指挥下被调度被执行那么操作系统自已被谁指挥被谁推动执行呢 外部设备可以触发硬件中断但是这个是需要用户或者设备自己触发有没有自己可以定期触发的设备
在外部设备中有一个叫时钟源的东西它会不断地给CPU发送中断每次中断间间隔的时间非常非常短这个时钟源也是有中断号的假设为n如果在中断向量表中给下标为n的位置放的方法是进程调度。因为它不断地给CPU发送中断所以OS会不断地执行它对应的中断服务也就是不断地进行进程调度。所以OS为什么能够跑起来呢就是因为时钟中断一直在推动OS进行进行调度。所以OS是基于中断向量表进行工作的。
当代的x86芯片认为时钟源若要通过中断控制器向CPU发送中断太慢了并且还会占用中断控制器的资源所以当代的CPU已经将时钟源集成到CPU内部了所以CPU的参数里面会有一个指标叫主频CPU的主频表示每秒钟可完成的时钟周期数。主频越高理论上CPU执行指令的速度越快。简单而言主频就是CPU每秒时钟中断的次数当次数越多说明OS进行进程调度的频率比较高进行进程调度的频率比较高则说明处理任何中断服务的速度都比较快CPU响应速度比较快所以效率高。所以OS就在硬件的推动下自动调度了。
死循环
如果是这样操作系统不就可以躺平了吗对操作系统自己不做任何事情需要什么功能就向中断向量表里面添加方法即可。操作系统的本质就是一个死循环所以OS只需将中断向量表初始化完成OS直接让自己进入死循环什么都不做等待别人给它发送中断即可。
void main(void)
{for(;;)pause();
}
之前写这个代码就是用信号的方式模拟出了OS的执行过程
int main()
{gfuncs.push_back([](){std::cout 我是一个内核刷新操作 std::endl;});gfuncs.push_back([](){std::cout 我是一个检测进程时间片的操作如果时间片到了我会切换进程 std::endl;});gfuncs.push_back([](){std::cout 我是一个内存管理操作定期清理操作系统内部的内存碎片 std::endl;});alarm(1);signal(SIGALRM, hanlder);while(true){pause();std::cout 我醒来了... std::endl;gcount ;}
什么是时间片时钟中断的频率是固定的假设每隔1纳秒发送一次时间中断。我们可以给每一个进程设置一个时间片假设当前进程的时间片是1微秒。也就是说当前进程的时间片是时钟中断频率的1000倍。
struct task_struct
{int count 1000; // 时间片
}
所以时间片的本质就是一个int我们前面说了时钟中断的中断处理是进程调度但是这个进程调度不一定是切换进程只是让正在被调度的时间片--只要当减为0了才会被切换。所以时间片的本质就是PCB内部的一个计数器。
软中断
上面说的外设触发中断或者时钟中断都是外部硬件中断需要硬件设备触发。有没有可能因为软件的原因也触发上面的逻辑呢有如除0、野指针、缺页中断(这3个都是异常)、系统调用等。我们之前的都是外设通知CPU然后CPU和OS一起执行中断处理历程可以在CPU内部设置CPU支持的指令让其在没有外设通知的情况下自己执行一次中断处理流程。
我们以系统调用为例其他的也是类似的。为了让操作系统支持进行系统调用(除0、野指针等都是一样的)CPU也设计了对应的汇编指令(int 0x80(32位下)或者syscall(64位下))可以让CPU内部触发中断逻辑。因为是汇编指令所以就可以写到软件当中了。所以未来就可以使用软件来触发CPU执行中断方法。用软件推动CPU执行中断的方法称为软中断。说得通俗一点硬件中断就是给了CPU一个中断号让CPU执行中断处理方法CPU可以自己形成一个唯一的中断号使用汇编指令让CPU在不需要外部设备驱动的前提下自动就能执行中断处理方法这就是软中断。int的中断号是0x80。
既然中断向量表中可以各种硬件的中断服务、进程调度的方法等就一定可以在中断向量表中设计一个系统调用的入口函数sys_function并将0x80 对应的函数指针设置为这个函数。OS会提供大量的系统调用OS是将这些系统调用的函数指针放在一个数组当中形成一个系统调用表。未来要调用那个系统调用使用这个系统调用的数组下标即可这个数组下标称为系统调用号。系统调用的入口函数有一个参数就可以根据这个参数去调用对应的系统调用。
void sys_function(int index)
{sys_call_table[index]();
}
所以软中断就是CPU执行到特定的指令如intOx80或syscall这两者都是写在可执行程序内部的触发软中断主动请求内核服务也就是去执行中断向量表中对应的函数。
问题
1.用户写的代码中调用了系统调用我们现在知道了调用系统调用后会让cPU执行intOx80或者syscall来触发软中断然后软中断会去中断向量表中调用系统调用的入口函数sys_function然后根据传入的系统调用号调用对应的系统调用也就是说OS需要知道一个系统调用的系统调用号此时才能在调用sys_function时进行传参用户层怎么把系统调用号给操作系统呢 此时是使用寄存器的如EAX)在触发软中断之前会先将要调用的系统调用的系统调用号写到CPU的一个寄存器当中设置系统调用参数完成后再执行int0x80或syscall触发软中断此时就可以根据寄存器中的值进行调用对应的系统调用了。
2. 系统调用的返回值如何返回给用户呢是通过寄存器或者用户传入的缓冲区地址。
通过上面两个问题我们可以知道OS在用户和内核之间传递信息使用的是CPU的寄存器实际上C/C中调用函数传参函数的返回值都是利用CPU的。
系统调用的过程其实就是先将系统调用号写入寄存器然后根据系统调用号自动查表执行对应的方法。所以系统调用也是通过中断完成的。
其实Linux内核提供的系统调用接口是系统调用号约定的传递参数返回值的寄存器组成的但是这样使用是十分困难的所以对这些系统调用进行了封装也就是GNU glibc这个库给我们提供了系统调用的C语言封装版。我们之前使用的系统调用fork都是经过GNU glibc封装过后的当我们调用fork时会自动完成将系统调用号放到寄存器中触发软中断执行对应的中断方法等一切操作。看看fork的实现 刚刚我们说的是在系统调用的角度看待软中断实际上只要是软件问题都可以触发软中断。
注意这些触发软中断就与syscall等无关了syscal是系统调用触发软中断时会调用的像除0、野指针、缺页中断等是因为触发了异常。
缺页中断内存碎片处理除零野指针错误这些问题全部都会被转换成为CPU内部的软中断OS会提前给这些问题设置对应的中断处理方法并且每一个每一个问题都有对应的中断号然后走中断处理例程完成所有处理。有的是进行申请内存填充页表进行映射的。有的是用来处理内存碎片的有的是用来给目标进行发送信号杀掉进程等等。
总结
操作系统是躺在中断处理历程上的代码块CPU内部的软中断比如intOx80或者syscall我们叫做陷阱CPU内部的软中断比如除零/野指针/缺页中断等我们叫做异常。所以缺页中断也叫做缺页异常
内核态与用户态
A函数中调用了B函数怎么在调用完B函数后向下继续执行A函数的代码呢 会将A函数调用B的下一句语句先入栈当调用完B函数弹栈后根据栈内的信息继续执行A函数的下一个语句。 进程访问用户区直接就可以进行访问不用系统调用使用的是虚拟地址。页表不仅仅存在用户页表还存在内核页表。我们作为普通用户我们最关心内核区中的内容就是系统调用因为我们只能通过系统调用访问OS。我们在调用我们自己的函数时是通过虚拟地址调用动态库中的方法时是通过虚拟地址偏移量所以调用系统调用时是不是也要使用系统调用的虚拟地址呢(因为系统调用会通过内核页表映射到内核区所以我们是可以通过内核区拿到系统调用的虚拟地址的)实际上我们并不关心系统调用的地址无论是虚拟地址还是物理地址因为我们只需要知道系统调用号OS会自已索引查找系统调用。未来在代码中调用了系统调用与glibc一链接就会有系统调用号。我们调用系统调用时当通过系统调用号拿到了系统调用后调用系统调用与调用普通函数是一样的都是压栈、弹栈等并且传参、返回值也是通过CPU。所以我们调用任何函数库系统调用等都是在我们自己进程的地址空间中进行调用。当一个OS中有很多个进程时每个进程的进程地址空间的用户区是各不相同的但是内核区是相同的。操作系统无论怎么切换进程都能找到同一个操作系统换句话说操作系统系统调用方法的执行是是在进程的地址空间中执行的
当我们处于用户态时我们可不可以使用汇编去修改CPU中如pc指针的值让我们去当问3-4GB的空间呢是不行的要进入内核区需要将用户态切换成内核态。每一个进程都有内核区无论是通过哪一个进程的地址空间进入内核都是通过软中断进入OS的。实际上一个进程处于用户态还是内核态是由CPU上的Cs段寄存器决定的Cs段寄存器上有2个bit位0表示内核态1表示用户态称为CPL。处于用户态时只允许访问用户区因为CPU中集成了MMU在虚拟地址到物理地址的转化时用户态只允许0-3GB的转化当处于内核态时才允许3-4GB的转化。所以用户态-内核态硬件上修改CPU寄存器上的值软件上执行int0x80或syscall。当调用int0x80或syscall时CPU会自己修改寄存器上的值。
因为有了MMU所以在用户态时是不能通过3-4GB中的虚拟地址区转化的。那我们可不可以获取3-4GB中的一个系统调用的地址并调用int0x80将用户态切换成内核态不就可以访问了吗 是不行的。因为当我们调用了int0x80后直接就从寄存器中读取系统调用号了并不会管下面访问3-4GB的地址。通俗一点说访问自己的代码时是用户态当问系统代码时是内核态。关于特权级别涉及到段段描述符段选择子DPLCPLRPL等概念这块我们不做深究了现在芯片为了保证兼容性已经非常复杂了进而导致OS也必须得照顾它的复杂性。
用户是如何进入内核态的
时钟/外设中断异常(除0、野指针、缺页中断等)陷阱(int 0x80、syscall)
总结OS是如何运行的 当刚开机只有OS一个软件在运行时OS就会停在它的pauseCPU也是闲置的。但是我们之前也说过哪怕一个进程也没有OS还是会定期地将内核缓冲区内的数据刷新到外设此时也没有异常等情况OS要怎么做呢实际上OS在创建时会先fork出一些子进程这些子进程的任务就是定期地检查各个进程的缓冲区内的数据并进行刷新这些子进程与未来用户创建的进程一样都是要被OS调度的进程这种进程称为OS的内核固定历程。我们上一节介绍的闹钟OS为什么知道闹钟超时了就是因为有这些进程在检查。当用户创建了一些进程时只要还有进程没退出OS的死循环就不会被执行了因为CPU一直都在调度我们运行的那个进程。 所以OS就是一个混合体既有自己的固定历程又躺在中断向量表上的一个软件集合。
我们再来补充信号捕捉的一些细节 1.在执行信号处理函数sighandler时为什么要做权限切换呢直接在内核态执行不就行了吗 因为信号处理函数是可以用户自定义的如果这个自定义函数中有一些非法操作如删除用户、删除root的配置文件、给自己赋权等让OS来执行这些非法操作是不行的这样就是用户利用OS的更高权限来做一些非法的事情是不安全的所以必须进行权限切换。
2.当我们执行完信号处理函数后为什么又要切换回内核态再切换回用户态而不是执行完信号处理函数就直接继续向后执行呢 如果在一个函数中想调用另一个函数只需要知道函数名但是若想从一个函数执行完毕返回另一个函数这两个函数必须存在调用关系因为当一个函数调用另一个函数时会将调用函数的下一条指令入栈调用完成之后弹栈就可以回来了。而我们这里的main和sighandler是没有关系的但是main函数和内核是有关系的因为陷入内核了所以必须先返回内核再返回用户执行下一条指令。OS怎么知道中断、异常、系统调用等处理完成后执行那一条指令呢当我们遇到中断、异常、系统调用等CPU会将下一条指令放入到pc指针中。
信号捕捉的操作
我们之前介绍过signal其实还要另外一个sigaction功能更加丰富
#include signal.hint sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {void (*sa_handler)(int); // 简单信号处理函数void (*sa_sigaction)(int, siginfo_t *, void *); // 高级信号处理函数sigset_t sa_mask; // 执行处理函数时要阻塞的信号int sa_flags; // 修改行为的标志位void (*sa_restorer)(void); // 已废弃
};
sigaction本质就是更改handler表中特定下标的内容并且可以带回老的处理方法。sigaction是OS在用户级给我们提供的一个结构体sigaction中第一个参数就是信号具体的处理函数第二个是处理实时信号的我们不管第四个设为0即可不用管第五个也不管。第三个参数下面会详细介绍。
void handler(int signo)
{std::cout get a sig: signo std::endl;exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler handler;::sigaction(2, act, oact);while(true){pause();}return 0;
} 可以看到当没有使用sa_mask时与signal是非常类似的
为了理解sa_mask再来看看信号保存
根据pending表可知信号保存使用的是位图如果在一个信号没有被递达之前又接收到了这个信号甚至多次接收到呢此时这个进程只能记录下接收到了这个信号并不能记录接收到了几个。其实也只需要记录一次因为大部分信号都是让进程暂停或者退出。当我们在调用信号的处理函数时是有可能陷入内核的比如上面的信号处理函数中有cout若刚好刷新就会调用write这个系统调用从而陷入内核若我们现在正在执行这个信号的处理函数并且这个函数的处理函数处理的时间比较长又不断接收到了这个信号是不是会一直调用这个处理函数一直递归导致栈溢出呢
会发现并不会出现这个情况。OS不允许信号处理的方法进行嵌套如何做到的 --- 当某一个信号正在被处理时OS会自动把这个信号的block位设置为1当信号处理完成才设置回0。所以前面说的处理完成后还要回到内核回到内核需要完成的任务之一还有将block表设置为0。所以只支持串行处理并不支持嵌套处理。串行是指处理完一个信号后再处理这个信号。嵌套是指处理这个信号的同时还处理这个信号。
怎么证明会修改block表呢 可以将block表打印出来看看要打印出block表可以使用sigprocmask这个接口可以对block表进行设置不能直接拿到block表我们可以利用它可以拿到老表的特性我们可以设置一个无关信号然后获取老的表即可拿到。这里直接将表置空了。
void PrintBlock()
{sigset_t set, oset;sigemptyset(set);sigemptyset(oset);sigprocmask(SIG_BLOCK, set, oset);std::cout block: ;for(int signo 31; signo 0; signo --){if(sigismember(oset, signo))std::cout 1;else std::cout 0;}std::cout std::endl;
}void handler(int signo)
{// 使用静态变量证明函数被重复调用了static int cnt 0;cnt ;while(true){std::cout get a sig: signo , cnt: cnt std::endl;PrintBlock();sleep(1);}exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler handler;::sigaction(2, act, oact);while(true){PrintBlock();pause();}return 0;
} 可以看到最先开始时block表全是0ctrl c让进程收到2号信号后2号位置就变成了1。当然还有一些其他位置的也被设置成了1这里就不管了。
如果我们想在进程处理信号时自定义屏蔽一些信号呢此时就可以使用sa_mask了。
void PrintBlock()
{sigset_t set, oset;sigemptyset(set);sigemptyset(oset);sigprocmask(SIG_BLOCK, set, oset);std::cout block: ;for(int signo 31; signo 0; signo --){if(sigismember(oset, signo))std::cout 1;else std::cout 0;}std::cout std::endl;
}void handler(int signo)
{// 使用静态变量证明函数被重复调用了static int cnt 0;cnt ;while(true){std::cout get a sig: signo , cnt: cnt std::endl;PrintBlock();sleep(1);}exit(1);
}int main()
{struct sigaction act, oact;act.sa_handler handler;// 在处理2号信号时将3、4、5、6号信号都屏蔽掉sigemptyset(act.sa_mask);sigaddset(act.sa_mask, 4);sigaddset(act.sa_mask, 5);sigaddset(act.sa_mask, 6);sigaddset(act.sa_mask, 7);::sigaction(2, act, oact);while(true){PrintBlock();pause();}return 0;
} 所以sa_mask就是在我们处理信号时指定需要主动屏蔽的一些信号。当信号处理完成后就会自动恢复这个又怎么证明呢
void handler(int signo)
{// 使用静态变量证明函数被重复调用了static int cnt 0;cnt ;while(true){std::cout get a sig: signo , cnt: cnt std::endl;PrintBlock();sleep(1);break; // 收到2号信号后只让其打印一遍}// exit(1);
} 可以看到处理完后就恢复正常了。
我们知道进程收到信号会将这个信号在pen开始处理信号时清0还是在处理完信号时清0呢 是开始处理信号时就清0了。因为如果是处理完信号后再清0如如何区分这是刚刚处理的信号送还是处理信号过程中收到的信号呢
可重入函数 假设现在有一个全局的链表main函数中正在执行头插操作若刚刚执行完将新结点的next指向要插入位置的下一个结点此时信号来了并且进程去执行信号处理了。此时就处在用户态并没有从内核态切换回用户态怎么会处理信号呢因为进程会切换执行完这一句刚好进程切换了进程切换是因为时钟中断触发时钟中断时就会有从内核态切换回用户态从而处理信号。若此时这个信号的处理方法也是向这个全局链表进行头插。信号处理完也就是头插完成后会继续之前的头插。此时会造成信号处理时头插的结点丢失。这就是一种内存泄露。这个问题在于当信号处理时执行插入函数并且main函数也在执行插入函数同一个函数被两个执行流重复进入了。一个函数被两个及以上执行流同时进入了我们称这个函数被重入了。之前多进程时父子进程同时进入一个函数是重入但是对于数据会发生写时拷贝不会出错。像刚刚的函数被重入后会出问题称这样的函数为不可重入函数若没出问题则称这个函数为可重入函数。
什么样的函数是可重入函数什么样的函数是不可重入函数呢 函数使用了全局资源包括static则是不可重入的。使用了mallocfree使用了lO标准库都是不可重入的。当一个函数不使用任何的全局资源则是可重入的。我们遇到的大部分库函数大部分是不可重入的像STL中的接口基本上都是不可重入的因为STL中会有全局的空间配置器并且会自动扩容使用了全局资源所以是不可重入的。可重入与不可重入是一种特性没有好坏之分。大部分函数是不可重入的带_r的就是可重入的(有一些函数会有可重入版和不可重入版)。
volatile
volatile是C语言的一个关键字C也是支持的
int flag 0;void change(int signo)
{flag 1;printf(change flag 0 - 1\n);
}int main()
{signal(2, change);while(!flag);printf(我是正常退出的!\n);return 0;
} 我们使用C程序来写。对于上面这个程序是有两个执行流的第一个执行流是main第二个执行流是信号捕捉的执行流但是这两个执行流是属于同一个进程的。
我们可以让编译器对我们的代码进行优化 -O0表示不进行优化1表示优化并且后面的数字越大优化的级别越高并且会发现当我们进行优化后让进程收到2号信号进程不会退出了。
我们先来谈为什么之前会退当我们定义了一个全局变量时是会在进程地址空间的已初始化数据区开辟一段空间的并且也会在物理内存真正开辟一个空间。当我们的主逻辑也就是main这个执行流在进行while时是计算吗是计算CPU的计算有两种一种是算术计算也就是加减乘除另一种是逻辑计算为真为假。但是CPU没办法直接对内存的数据进行计算所以CPU内部会有很多的寄存器要进行计算首先需要将需要计算的值从内存拷贝到CPU的寄存器上面如eax然后CPU开始判断然后将下一条要执行的语句写在pc指针中flag为0就在while内部不为0就在while外部。下一次要判断时继续将flag拷贝到CPU的寄存器中。当flag变成1时就退出了。所以没有优化时每一次判断都会从内存拷贝到CPU内。
当编译器优化时为什么不退编译器优化时会定义一个寄存器变量就是编译器认为flag的值不会变在CPU内部使用一个寄存器存储这个变量这样在while判断时直接去查看CPU寄存器中的值即可不需要再将内存中的值拷贝到CPU当中。像上面我们通过信号改变的是内存中的flagCPU内的flag并没有变化所以不退。寄存器优化屏蔽了内存的可见性。
volatile称为易变关键字 可以看到加了volatile后即使加了优化也是会直接退的。volatile修饰变量保持变量的内存可见性叫易变是因为这个变量就是易变的所以要保持内存可见性。
所有的关键字都是给编译器看的编译成汇编、二进制时是没有关键字的。实际上只要不是被执行的都是给编译器看的。
SIGCHLD信号
进程一章讲过用wait和waitpid函数清理僵尸进程父进程可以阻塞等待子进程结束也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式父进程阻塞了就不能处理自己的工作了采用第二种方式父进程在处理自己的工作的同时还要记得时不时地轮询一下程序实现复杂。其实子进程在终止时会给父进程发SIGCHLD信号是17号信号该信号的默认处理动作是忽略父进程可以自定义SIGCHLD信号的处理函数这样父进程只需专心处理自己的工作不必关心子进程了子进程终止时会通知父进程父进程在信号处理函数中调用wait清理子进程即可。
先来验证一下子进程退出时是否会给父进程发送17号信号。
void handler(int signo)
{std::cout get a signo: signo std::endl;
}int main()
{signal(SIGCHLD, handler);if(fork() 0){sleep(5);std::cout 子进程退出 std::endl;exit(0);}while(true) ;return 0;
} 可以看到子进程确实向父进程发送了17号信号。此时没有对子进程进行回收子进程退出后仍然处于僵户状态。
因为子进程在退出时会向父进程发送信号所以我们可以基于信号对子进程进行回收。直接去17号信号的自定义函数中等待子进程即可。
void handler(int signo)
{std::cout get a signo: signo , I am: getpid() std::endl;pid_t rid ::waitpid(-1, nullptr, 0);if(rid 0){std::cout 子进程退出了, 回收成功, child id: rid std::endl;}
}int main()
{signal(SIGCHLD, handler);if(fork() 0){sleep(5);std::cout 子进程退出 std::endl;exit(0);}while(true) ;return 0;
} 此时就回收成功了可以看到是没有僵尸状态的进程的。
使用信号来回收虽然好但是上面的代码是有几个问题的
刚刚只有一个子进程若有多个子进程呢 若这多个子进程同时退出此时是存在风险的。因为保存信号使用的是位图无法记录有几个信号同时传过来。并且当信号处理函数正在被调用时若此时发送多个相同信号只会记录一个。
我们现在一次性创建10个子进程。
void handler(int signo)
{std::cout get a signo: signo , I am: getpid() std::endl;pid_t rid ::waitpid(-1, nullptr, 0);if (rid 0){std::cout 子进程退出了, 回收成功, child id: rid std::endl;}
}int main()
{signal(SIGCHLD, handler);for (int i 0; i 10; i){if (fork() 0){sleep(5);std::cout 子进程退出 std::endl;exit(0);}}while (true);return 0;
} 可以看到此时只成功回收了7个子进程剩下3个成为了僵尸进程。
为了解决这个问题可以让信号处理函数一直循环。
void handler(int signo)
{std::cout get a signo: signo , I am: getpid() std::endl;while (true){pid_t rid ::waitpid(-1, nullptr, 0);if (rid 0){std::cout 子进程退出了, 回收成功, child id: rid std::endl;}}
} 可以看到此时子进程确实全部回收成功了。waitpid(-1,nullptr0)是阻塞等待它会一直等待直到至少有一个子进程退出然后回收它并返回该子进程的PID由于while循环中反复调用所以大部分情况下可以处理所有已退出的子进程。但是如果17号信号在waitpid期间触发仍然有可能导致部分信号丢失所以最好使用非阻塞等待waitpid(-1nullptrWNOHANG)。当我们换成非阻塞等待后不需要考虑信号能不能被父进程成功接收写入pending表中只要父进程创建的子进程中还有僵户进程就可以进行回收。因为非阻塞等待会主动检查所有子进程状态直接回收僵户进程不依赖信号通知。但是阻塞等待是不会的主动检查所有子进程的状态的而是被动等待子进程退出事件。
在上面的阻塞等待中我们创建了10个进程只退出了7个会怎么样呢当我们已经回收了退出的7个是否还会继续等待第8个呢是会等待的因为是阻塞等待此时又没有进程退出所以父进程会一直阻塞在哪里所以应该使用非阻塞等待。
进程回收的做法
阻塞等待非阻塞等待基于信号 非阻塞等待若我们只是不想要僵户进程不关心子进程的执行情况可以父进程调用sigaction/signal将SIGCHLD的处理动作置为SIG_IGN这样fork出来的子进程在终止时会自动清理掉不会产生僵户进程也不会通知父进程。这种做法只能在Linux下使用
对于上面的第4点。我们对于17号信号默认不就是IGN吗为什么还要手动来一次呢 因为默认的IGN和手动设置的IGN是不同的东西这是在Linux中被做了特殊处理系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的但这是一个特例。此方法对于Linux可用但不保证在其它UNIX系统上都可用。 文章转载自: http://www.morning.jhyfb.cn.gov.cn.jhyfb.cn http://www.morning.3jiax.cn.gov.cn.3jiax.cn http://www.morning.cfybl.cn.gov.cn.cfybl.cn http://www.morning.trrhj.cn.gov.cn.trrhj.cn http://www.morning.bysey.com.gov.cn.bysey.com http://www.morning.rpzqk.cn.gov.cn.rpzqk.cn http://www.morning.srbl.cn.gov.cn.srbl.cn http://www.morning.zcxjg.cn.gov.cn.zcxjg.cn http://www.morning.rqxtb.cn.gov.cn.rqxtb.cn http://www.morning.rmppf.cn.gov.cn.rmppf.cn http://www.morning.bhbxd.cn.gov.cn.bhbxd.cn http://www.morning.llfwg.cn.gov.cn.llfwg.cn http://www.morning.thrcj.cn.gov.cn.thrcj.cn http://www.morning.qnxkm.cn.gov.cn.qnxkm.cn http://www.morning.ychrn.cn.gov.cn.ychrn.cn http://www.morning.fwzjs.cn.gov.cn.fwzjs.cn http://www.morning.kphyl.cn.gov.cn.kphyl.cn http://www.morning.kqxng.cn.gov.cn.kqxng.cn http://www.morning.prznc.cn.gov.cn.prznc.cn http://www.morning.qnlbb.cn.gov.cn.qnlbb.cn http://www.morning.cbpmq.cn.gov.cn.cbpmq.cn http://www.morning.dpqwq.cn.gov.cn.dpqwq.cn http://www.morning.fmrwl.cn.gov.cn.fmrwl.cn http://www.morning.txnqh.cn.gov.cn.txnqh.cn http://www.morning.dxzcr.cn.gov.cn.dxzcr.cn http://www.morning.wqbzt.cn.gov.cn.wqbzt.cn http://www.morning.dansj.com.gov.cn.dansj.com http://www.morning.bpwfr.cn.gov.cn.bpwfr.cn http://www.morning.xhsxj.cn.gov.cn.xhsxj.cn http://www.morning.rcrnw.cn.gov.cn.rcrnw.cn http://www.morning.bxyzr.cn.gov.cn.bxyzr.cn http://www.morning.btblm.cn.gov.cn.btblm.cn http://www.morning.pwrkl.cn.gov.cn.pwrkl.cn http://www.morning.sqqhd.cn.gov.cn.sqqhd.cn http://www.morning.wrqw.cn.gov.cn.wrqw.cn http://www.morning.bnpn.cn.gov.cn.bnpn.cn http://www.morning.dgxrz.cn.gov.cn.dgxrz.cn http://www.morning.lhqw.cn.gov.cn.lhqw.cn http://www.morning.plqsc.cn.gov.cn.plqsc.cn http://www.morning.pflry.cn.gov.cn.pflry.cn http://www.morning.qlckc.cn.gov.cn.qlckc.cn http://www.morning.jzgxp.cn.gov.cn.jzgxp.cn http://www.morning.lrflh.cn.gov.cn.lrflh.cn http://www.morning.xmjzn.cn.gov.cn.xmjzn.cn http://www.morning.nqbs.cn.gov.cn.nqbs.cn http://www.morning.lfxcj.cn.gov.cn.lfxcj.cn http://www.morning.yzzfl.cn.gov.cn.yzzfl.cn http://www.morning.krhkb.cn.gov.cn.krhkb.cn http://www.morning.gjssk.cn.gov.cn.gjssk.cn http://www.morning.yhtnr.cn.gov.cn.yhtnr.cn http://www.morning.ypcbm.cn.gov.cn.ypcbm.cn http://www.morning.dhqyh.cn.gov.cn.dhqyh.cn http://www.morning.vibwp.cn.gov.cn.vibwp.cn http://www.morning.qichetc.com.gov.cn.qichetc.com http://www.morning.snmth.cn.gov.cn.snmth.cn http://www.morning.fkgcd.cn.gov.cn.fkgcd.cn http://www.morning.mmosan.com.gov.cn.mmosan.com http://www.morning.huxinzuche.cn.gov.cn.huxinzuche.cn http://www.morning.ysrtj.cn.gov.cn.ysrtj.cn http://www.morning.dbsch.cn.gov.cn.dbsch.cn http://www.morning.tbzcl.cn.gov.cn.tbzcl.cn http://www.morning.rnfn.cn.gov.cn.rnfn.cn http://www.morning.wtyqs.cn.gov.cn.wtyqs.cn http://www.morning.tfcwj.cn.gov.cn.tfcwj.cn http://www.morning.fy974.cn.gov.cn.fy974.cn http://www.morning.hxycm.cn.gov.cn.hxycm.cn http://www.morning.qxwrd.cn.gov.cn.qxwrd.cn http://www.morning.wklyk.cn.gov.cn.wklyk.cn http://www.morning.jtqxs.cn.gov.cn.jtqxs.cn http://www.morning.dmcqy.cn.gov.cn.dmcqy.cn http://www.morning.kbdrq.cn.gov.cn.kbdrq.cn http://www.morning.ksgjy.cn.gov.cn.ksgjy.cn http://www.morning.yysqz.cn.gov.cn.yysqz.cn http://www.morning.bpknt.cn.gov.cn.bpknt.cn http://www.morning.mwnch.cn.gov.cn.mwnch.cn http://www.morning.deanzhu.com.gov.cn.deanzhu.com http://www.morning.grfhd.cn.gov.cn.grfhd.cn http://www.morning.kntbk.cn.gov.cn.kntbk.cn http://www.morning.qrhh.cn.gov.cn.qrhh.cn http://www.morning.kzrbd.cn.gov.cn.kzrbd.cn