
🌈 个人主页:Zfox_
🔥 系列专栏:Linux

目录
- 一:🔥 再谈信号的捕捉
- 二:🔥 穿插话题 - 操作系统是怎么运⾏的
- 三:🔥 缺⻚中断?内存碎⽚处理?除零野指针错误?
- 四:🔥 如何理解内核态和⽤⼾态
- 五:🔥 可重入函数
- 六:🔥 使用信号对全局变量进行操作出现的问题(volatile)
- 七:🔥 SIGCHLD信号
- 八:🔥 共勉
一:🔥 再谈信号的捕捉
关于信号捕捉有三种方式:
signal(2, handler); // 自定义捕捉
signal(2, SIG_IGN); // 忽略一个信号
signal(2, SIG_DFL); // 信号的默认处理动作
SIG_IGN
是一个特殊的宏,用于指示系统忽略该信号。
信号可能不会被立即处理,而是在合适的时候处理,那么合适的时候是什么时候呢?
先给结论:从进程的内核态返回到用户态的时候,进行处理。
💦 简单来说,执行自己的代码,访问自己的数据,这就叫做用户态。
💦 当我们进入系统调用时,我们以操作系统的身份来执行时,此时就进入了内核态,操作系统把我们的底层工作做完,做完这些工作后返回到我们的调用处,继续执行下面的代码,但是操作系统,由内核态返回到用户态时,在返回的这个时候信号的检测和处理
如果信号的处理动作是⽤⼾⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。
由于信号处理函数的代码是在⽤⼾空间的,处理过程⽐较复杂,举例如下:
- ⽤⼾程序注册了
SIGQUIT
信号的处理函数sighandler
。 - 当前正在执⾏
main
函数, 这时发⽣中断或异常切换到内核态。 - 在中断处理完毕后要返回⽤⼾态的
main
函数之前检查到有信号SIGQUIT
递达。 - 内核决定返回⽤⼾态后不是恢复
main
函数的上下⽂继续执⾏,⽽是执⾏sighandler
函数,sighandler
和main
函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独⽴的控制流程。 sighandler
函数返回后⾃动执⾏特殊的系统调⽤sigreturn
再次进⼊内核态。- 如果没有新的信号要递达,这次再返回⽤⼾态就是恢复
main
函数的上下⽂继续执⾏了
🦋 关于信号捕捉的细节部分(sigaction函数)
🦁 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); // 已废弃,不用关心
};
🎯 sigaction 函数和 signal 的明显区别:
- 当前如果正在对 2 号信号进行处理,默认 2 号信号会被自动屏蔽,对2号信号处理完成的时候,会自动解除对 2 号信号的屏蔽。为什么?这是因为,操作系统不允许同一个信号被连续处理。
- 如果 2 号信号处理完毕后,会自动解除对 2 号信号的屏蔽
下面是一段示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
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 PrintPending()
{
sigset_t pending;
::sigpending(&pending);
std::cout << "Pending :";
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, 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);
break;
}
}
int main()
{
struct sigaction act, oact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
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();
PrintPending();
::pause();
}
return 0;
}
🌳 我对3,4,5,6,7号信号也同时做了屏蔽,此时发送2号信号,pending值也是由0置为1的。
二:🔥 穿插话题 - 操作系统是怎么运⾏的
🦋 硬件中断
• 中断向量表就是操作系统的⼀部分,启动就加载到内存中了
• 通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
• 由外部设备触发的,中断系统运⾏流程,叫做硬件中断
🦋 时钟中断
☁️ 定义:Linux 时钟中断是指在 Linux 操作系统中,系统定时器周期性地触发中断,这个中断被称为时钟中断。时钟中断源于硬件定时器,通常由计算机的主板芯片或处理器芯片提供,通过定时器计数器来实现定时中断功能。
功能:
-
维护系统时间:每当一个时钟中断发生时,内核会更新系统时间的计数值。这个计数值可以是自世界时间开始的毫秒数,也可以是自系统启动以来的滴答数(tick)。通过定时更新系统时间,系统可以保持时间的准确性,为用户提供可靠的时间信息。
-
任务调度:在多任务操作系统中,内核需要决定哪个进程将获得CPU的控制权。时钟中断提供了一个计时器,每当中断发生时,内核会检查当前运行的进程是否到达了它应该运行的时间片。如果一个进程的时间片用完了,内核就会重新选择下一个要运行的进程,并切换上下文,将控制权交给新的进程。这样保证了系统中进程的公平调度,提高了系统的整体性能。
-
计算进程执行时间:每当一个进程或线程被抢占,切换到另一个进程或线程时,时钟中断记录下了抢占发生的时间。通过记录不同进程和线程的执行时间,可以分析其调度情况,了解系统中进程的运行情况,为性能优化提供依据。
🦋 OS 死循环
💜 如果是这样,操作系统不就可以躺平了吗?对,操作系统⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可.操作系统的本质:就是⼀个死循环!
void main(void) /* 这⾥确实是void,并没错。 */
{
/* 在startup 程序(head.s)中就是这样假设的。 */
...
/*
* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返
* 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任
* 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),
* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没
* 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。
*/
for (;;)
pause();
} // end main
🦋 小结
🦋 如何理解系统调用
软中断
• 上述外部硬件中断,需要硬件设备触发。
• 有没有可能,因为软件原因,也触发上⾯的逻辑?有!
• 为了让操作系统⽀持进⾏系统调⽤,CPU
也设计了对应的汇编指令 (int 0x80
或者 syscall
), 可以让CPU内部触发中断逻辑。
所以:
问题:
• ⽤⼾层怎么把系统调⽤号给操作系统? - 寄存器(⽐如EAX)
• 操作系统怎么把返回值给⽤⼾?- 寄存器或者⽤⼾传⼊的缓冲区地址
• 系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法
• 系统调⽤号的本质:数组下标
可是为什么我们⽤的系统调⽤,从来没有⻅过什么 int 0x80 或者 syscall 呢?都是直接调⽤上层的函数的啊?
• 那是因为 Linux 的 gnu C 标准库,给我们把⼏乎所有的系统调⽤全部封装了。
三:🔥 缺⻚中断?内存碎⽚处理?除零野指针错误?
- 缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。
📌 所以:
- 操作系统就是躺在中断处理例程上的代码块!
- CPU内部的软中断,⽐如
int 0x80
或者syscall
,我们叫做陷阱
- CPU内部的软中断,⽐如除零/野指针等,我们叫做
异常
。(所以,能理解“缺⻚异常”为什么这么叫了吗?)
四:🔥 如何理解内核态和⽤⼾态
结论:
• 操作系统⽆论怎么切换进程,都能找到同⼀个操作系统!换句话说操作系统系统调⽤⽅法的执⾏,是在进程的地址空间中执⾏的!
-
内核态: 0-4G 范围的虚拟空间地址都可以操作,尤其是对 3-4G 范围的⾼位虚拟空间地址必须由内核态去操作。
-
3G - 4G 部分⼤家是共享的(指所有进程的内核态逻辑地址是共享同⼀块内存地址),是内核态的地址空间,这⾥存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。
-
关于特权级别,涉及到段,段描述符,段选择⼦,DPL,CPL,RPL等概念, ⽽现在芯⽚为了保证兼容性,已经⾮常复杂了,进⽽导致OS也必须得照顾它的复杂性,这块我们不做深究了。
-
⽤⼾态就是执⾏⽤⼾ [0,3] GB 时所处的状态
-
内核态就是执⾏内核 [3,4] GB 时所处的状态
-
区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
-
⼀般执⾏ int 0x80 或者 syscall 软中断,CPL会在校验之后⾃动变更
-
这样会不会不安全??
五:🔥 可重入函数
六:🔥 使用信号对全局变量进行操作出现的问题(volatile)
int gflag = 0;
void changedata(int signo)
{
std::cout << "get a signo:" << signo << ", change gflag 0->1" << std::endl;
gflag = 1;
}
int main() // 没有任何代码对gflag进行修改!!!
{
signal(2, changedata);
while(!gflag); // while不要其他代码
std::cout << "process quit normal" << std::endl;
}
七:🔥 SIGCHLD信号
子进程退出时,不是静悄悄的退出的,会给父进程发送信号–SIGCHLD信号。
下面是一段示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void notice(int signo)
{
std::cout << "get a signal: " << signo << " pid: " << getpid() << std::endl;
while (true)
{
pid_t rid = waitpid(-1, nullptr, WNOHANG); // 阻塞啦!!--> 非阻塞方式
if (rid > 0)
{
std::cout << "wait child success, rid: " << rid << std::endl;
}
else if (rid < 0)
{
std::cout << "wait child success done " << std::endl;
break;
}
else
{
std::cout << "wait child success done " << std::endl;
break;
}
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing~" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
std::cout << "I am child process, pid: " << getpid() << std::endl;
sleep(3);
exit(1);
}
}
// father
while (true)
{
DoOtherThing();
sleep(1);
}
return 0;
}
这段代码创建了多个子进程,并在子进程结束时通过 SIGCHLD 信号进行处理。
- 当
SIGCHLD
信号被捕获时,notice
函数会被调用。这个函数会进入一个无限循环,尝试使用 waitpid 以非阻塞方式 (WNOHANG) 等待任何已终止的子进程。这是合理的,因为它允许父进程在子进程终止时及时回收资源,同时不阻塞父进程的其他操作。
八:🔥 共勉
以上就是我对 【Linux】进程信号全攻略(二)
的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉