✨✨ 欢迎大家来到小伞的大讲堂✨✨
🎈🎈养成好习惯,先赞后看哦~🎈🎈
所属专栏:LInux_st
小伞的主页:xiaosan_blog制作不易!点个赞吧!!谢谢喵!!
1.信号
1.1什么是信号
信号是信息传递的承载方式,是操作系统用于通知进程发生某些事件或要求进程执行特定动作的机制。信号通常代表某种特定的执行动作,类似于生活中的各种信号:
-
鸡叫 => 天快亮了
-
闹钟 => 起床、完成任务
-
红绿灯 => 红灯停,绿灯行
计算机并不能跟我们一样能自主根据命名而行动,所以为了使进程能在合适的时机执行特定的操作,程序员们为操作系统设计了一个机制,允许操作系统给进程发送“信号”,这些信号会触发相应的动作。
这些信号就像一组特定的指令,每个信号表示一个特殊的动作。进程接收到这些信号后,就会做出相应的反应。
1.2 一个样例
// sig.cc
#include <iostream>
#include <unistd.h>
int main()
{
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
这是一个简单的死循环,我们通常会使用ctrl+c进行终止,而ctrl+c其实就是通过硬件中断向操作系统发送终止信号。
1.3 signal系统函数
而其实,Ctrl+C的本质是向前台进程发送 SIGINT即2号信号,我们证明一下,这里需要引入一
个系统调用函数
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
signum:信号编号[后⾯解释,只需要知道是数字即可]
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法
通过kill -l 查看我们的当前系统的信号集
其中SIGINT为2号信号
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ", 我获得了⼀个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT, handler);
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
这里进程为什么不能退出了
ctrl+c为2号信号,前台进程接收到2号信号,根据地址跳转到hander方法,而不会执行原方法
- 要注意的是,signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!!
- Ctrl-C产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
1.4 信号概念
信号是进程之间事件异步通知的⼀种⽅式,属于软中断。
1.4.1 查看信号
每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义
#define SIGINT 2
1.4.2 信号分类
普通信号(1~31 号信号):
主要用于分时操作系统,适合个人电脑等环境。信号只是简单地表示某个动作发生,进程响应时并不需要保存信号的详细内容或持续时间。它们用于一般的进程管理和任务调度。
实时信号(34~64 号信号):
主要用于实时操作系统(RTOS),适用于对响应时间要求极高的场景,如车载系统、火箭发射控制台等。实时信号能够保存信号的内容和持续时间,提供更精确的任务控制,适合需要快速响应的任务
1.为何关注 1~31 号信号:
由于大多数用户操作系统(如个人电脑、服务器)使用 分时操作系统,这些系统的进程管理通常依赖于 普通信号。因此,我们只需关注 1~31 号信号,而不需要深入了解 实时信号。普通信号涵盖了如进程终止、暂停、继续等常见的操作
1.4.3 信号处理
处理方式存在三种:
- 忽略此信号
- 执行默认信号
- 捕捉hander信号
1.忽略此信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber
<< std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGINT /*2*/, SIG_IGN); // 设置忽略信号的宏
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
2.执行默认信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber
<< std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
//signal(SIGINT /*2*/, SIG_IGN); // 设置忽略信号的宏
signal(SIGINT /*2*/, SIG_DFL); // 执行默认信号的宏
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
3.捕获hander信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是:" << getpid() << ", 我获得了⼀个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程:" << getpid() << std::endl;
signal(SIGINT, handler);
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
2.产生信号
2.1 通过按键产生
- Ctrl+C (SIGINT) 已经验证过,这⾥不再重复
- Ctrl+\(SIGQUIT)可以发送终⽌信号并⽣成core dump⽂件
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGQUIT /*3*/, handler);
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
- Ctrl+Z(SIGTSTP)可以发送停⽌信号,将当前前台进程挂起到后台等。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
std::cout << "我是: " << getpid() << ", 我获得了⼀个信号: " << signumber << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGTSTP /*20*/, handler);
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
}
2.2 理解OS如何获取信号
2.3 调用系统命令向进程发送信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
while (true)
{
//kill调用系统命令向进程发送信号
sleep(1);
}
}
1285203是 sig 进程的pid。之所以要再次回车才显示 Segmentation fault,是因为在
1285203进程终止掉之前已经回到了Shel提示符等待用户输入下一条命令,SheLL不希望
Segmentationfault信息和用户的输入交错在一起,所以等用户输入命令之后才显示。
指定发送某种信号的kil命令可以有多种写法,上面的命令还可以写成kil-11
1285203,11是信号SIGSEGV的编号。以往遇到的段错误都是由非法内存访问产生的,而这个
程序本身没错,给它发SIGSEGV也能产生段错误。
2.4 使用函数产生信号
2.4.1 kill函数
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error,
-l is returned, and errno is set appropriately.
//成功返回0,失败返回-1
//使用kill函数
2.4.2 raise函数
raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这⼀处打印
std::cout << "获取了⼀个信号: " << signumber << std::endl;
}
// mykill -signumber pid
int main()
{
signal(2, handler); // 先对2号信号进⾏捕捉
// 每隔1S,⾃⼰给⾃⼰发送2号信号
while (true)
{
sleep(1);
raise(2);
}
}
2.4.3 abort函数
abort 函数使当前进程接收到信号而异常终止。
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
// 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void handler(int signumber)
{
// 整个代码就只有这⼀处打印
std::cout << "获取了⼀个信号: " << signumber << std::endl;
}
// mykill -signumber pid
int main()
{
signal(SIGABRT, handler);
while (true)
{
sleep(1);
abort();
}
}
abort给⾃⼰发送的是固定6号信号,虽然捕捉了,但是还是要退出
2.5 由软条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数
和 SIGALRM信号。
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
RETURN VALUE
alarm() returns the number of seconds remaining until any previously
scheduled alarm was due to be delivered, or zero if there was no previ‐
ously scheduled alarm.
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发
SIGALRM信号,该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
2.5.1 基本alarm验证-体会IO效率问题
程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。
必要的时候,对SIGALRM信号进行捕捉
// IO 多
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
int count = 0;
alarm(1);
while (true)
{
std::cout << "count : "
<< count << std::endl;
count++;
}
return 0;
}
// IO 少
#include <iostream>
#include <unistd.h>
#include <signal.h>
long int count = 0;
void handler(int signumber)
{
std::cout << "count : " << count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
count++;
}
return 0;
}
- 闹钟回响一次,默认进程终止
- 有IO效率低
2.5.2 重复设置闹钟
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>
using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;
// 把信号 更换 成为 硬件中断
void hanlder(int signo)
{
for (auto &f : gfuncs)
{
f();
}
std::cout<< "gcount : " << gcount << std::endl;
int n = alarm(13); // 重设闹钟,会返回上⼀次闹钟的剩余时间
std::cout << "剩余时间 : " << n << std::endl;
}
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); // ⼀次性的闹钟,超时alarm会⾃动被取消
signal(SIGALRM, hanlder);
while (true)
{
pause();
std::cout << "我醒来了..." << std::endl;
gcount++;
}
}
// 窗⼝ 1
whb@bite:~/code/test$ ./alarm
我的进程pid是: 216982
剩余时间 : 13 // 提前唤醒它,剩余
时间
剩余时间 : 0
剩余时间 : 0
剩余时间 : 0
// 窗⼝ 2
$ kill -14 216982
- 闹钟设置一次,起效一次
- 可利用循环或hander方法重复设置
2.5.3 alarm在内核中的数据结构
系统闹钟,其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_t_base_s *base;
}
- 定时器超时时间expires和处理方法function。
- 操作系统管理定时器,采用的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为"堆结构"。
2.6 硬件中断
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
2.6.1 模拟除零引发硬件中断
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
sleep(2);
}
// v1
int main()
{
signal(SIGFPE, handler); // (8) SIGFPE
sleep(1);
int a = 10;
a /= 0;
while (1);
return 0;
}
2.6.2 访问野指针引发硬件中断
//访问野指针引发的硬件中断
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
sleep(2);
}
int main()
{
signal(SIGSEGV, handler);
sleep(1);
int *p = NULL;
*p = 100;
while (1)
;
return 0;
}
我们发现当我们除零或者访问野指针引发的硬件中断,并没有中断一次就结束,而是重复一直
除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还
保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信
号的现象。访问非法内存其实也是如此。
2.6.3 子进程退出core dump
1. 什么是core dump
我们知道所有的程序最终运行起来,都会变成进程,进程在运行时可能会异常终止或崩溃,而Linux操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump(中文有的翻译成核心转储)。
保存的这个文件通常是:该进程的同目录下以core.PID的方式命名的文件。
2. core dump的使用
2.1 开启core dump
在Linux下core dump选项一般是被关闭的,我们可以通过ulimit -a
查看当前Linux下系统资源的限制。
可以看到,core file size
的大小是0,这说明系统不允许我们生成core file
文件 ,我们可以使用命令设置生成的core file
文件的大小的最大限制。
配置core dump创建地址
2.2. 生成core file文件
在Linux下有很多信号我们可以使用kill -l
查看:
但是并不是所有的信号引起的退出都会产生core file
文件,只有有core
标志的信号引起的退出才会产生core file
文件,我们可以通过 man 7 signal
查看信号的详细信息
man 7 signal
11
号信号SIGSEGV
是一个段错误的信号,当你的进程有内存越界等问题时,通常会收到该信号,可以看到该信号是有core
标志的。2
号信号SIGINT
其实就是我们常用的Ctrl + C 键产生的信号,可以看到该进程是没有core标志的,下面我们用代码来验证:
3.保存信号
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞(Block)某个信号。
- 阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作,
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
3.1 在内核中的表示
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- IGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
//Linux内核
struct task_struct
{
...
/* signal handlers */
struct sighand_struct *sighand;
sigset_t blocked struct sigpending pending;
...
}
struct sighand_struct
{
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};
struct __new_sigaction
{
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void); /* Not used by Linux/SPARC */
__new_sigset_t sa_mask;
};
struct k_sigaction
{
struct __new_sigaction sa;
void __user *ka_restorer;
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending
{
struct list_head list;
sigset_t signal;
};
3.2 sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(SignalMask),这里的“屏蔽应该理解为阻塞而不是忽略。
3.3 信号集操作函数
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 sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
3.2.1 sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main() {
sigset_t new_mask, old_mask;
// 初始化新信号集,只包含 SIGINT
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
// 设置新的信号屏蔽字,阻塞 SIGINT 信号
if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT signal is blocked. Sleeping for 5 seconds...\n");
sleep(5);
//阻塞过程中,我们ctrl+c会发现,进程并没有退出,而是等了5秒后退出,也没有打印后续代码
// 解除对 SIGINT 的阻塞
if (sigprocmask(SIG_UNBLOCK, &new_mask, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT signal is unblocked.\n");
return 0;
}
阻塞过程中,我们ctrl+c会发现,进程并没有退出,而是等了5秒后退出,也没有打印后续代码
3.2.2 sigpending
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
3.2.3 示例代码:
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
void PrintPending(sigset_t &pending)
{
std::cout << "curr process[" << getpid() << "]pending: ";
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout<< "\n";
}
void handler(int signo)
{
std::cout << signo << " 号信号被递达!!!" << std::endl;
std::cout << "-------------------------------" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "-------------------------------" << std::endl;
}
int main()
{
// 0. 捕捉2号信号
signal(2, handler); // ⾃定义捕捉
// signal(2, SIG_IGN); // 忽略⼀个信号
//signal(2, SIG_DFL); // 信号的默认处理动作
// 1. 屏蔽2号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT); // 我们有没有修改当前进⾏的内核block表呢???1 0
// 1.1 设置进⼊进程的Block表中
sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进⾏的内核block表,完成了对2号信号的屏蔽!
int cnt = 15;
while (true)
{
// 2. 获取当前进程的pending信号集
sigset_t pending;
sigpending(&pending);
// 3. 打印pending信号集
PrintPending(pending);
cnt--;
// 4. 解除对2号信号的屏蔽
if (cnt == 0)
{
std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
sleep(1);
}
}
ctrl+c出发SIG_INT信号,由于2号信号的阻塞,不能触发hander,当cnt==0时,解除2号信号的屏蔽,执行hander方法。
程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按CtrI-C将会使SIGINT信号处于未决状态,按Ctr-仍然可以终正程序,因为SIGQUIT信号没有阻塞。
4.捕捉信号
4.1 信号捕捉流程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
- 用户程序注册了SIGQUIT信号的处理函数 sighandler。
- 前正在执行main函数,这时发生中断或异常切换到内核态。
- 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
- 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
- sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
- 果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
5. 操作系统是如何运行的
5.1 硬件中断
- 断向量表就是操作系统的一部分,启动就加载到内存中了
- 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
- 由外部设备触发的,中断系统运行流程,叫做硬件中断
//Linux内核源码
void trap_init(void)
{
int i;
set_trap_gate(0, ÷_error); // 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1, &debug);
set_trap_gate(2, &nmi);
set_system_gate(3, &int3); /* int3-5 can be called from all */
set_system_gate(4, &overflow);
set_system_gate(5, &bounds);
set_trap_gate(6, &invalid_op);
set_trap_gate(7, &device_not_available);
set_trap_gate(8, &double_fault);
set_trap_gate(9, &coprocessor_segment_overrun);
set_trap_gate(10, &invalid_TSS);
set_trap_gate(11, &segment_not_present);
set_trap_gate(12, &stack_segment);
set_trap_gate(13, &general_protection);
set_trap_gate(14, &page_fault);
set_trap_gate(15, &reserved);
set_trap_gate(16, &coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱⻔。 for (i = 17; i < 48; i++)
set_trap_gate(i, &reserved);
set_trap_gate(45, &irq13); // 设置协处理器的陷阱⻔。
outb_p(inb_p(0x21) & 0xfb, 0x21); // 允许主8259A 芯⽚的IRQ2 中断请求。
outb(inb_p(0xA1) & 0xdf, 0xA1); // 允许从8259A 芯⽚的IRQ13 中断请求。
set_trap_gate(39, ¶llel_interrupt); // 设置并⾏⼝的陷阱⻔。
}
void rs_init(void)
{
set_intr_gate(0x24, rs1_interrupt); // 设置串⾏⼝1 的中断⻔向量(硬件IRQ4 信号)。
set_intr_gate(0x23, rs2_interrupt); // 设置串⾏⼝2 的中断⻔向量(硬件IRQ3 信号)。
init(tty_table[1].read_q.data); // 初始化串⾏⼝1(.data 是端⼝号)。
init(tty_table[2].read_q.data); // 初始化串⾏⼝2。
outb(inb_p(0x21) & 0xE7, 0x21); // 允许主8259A 芯⽚的IRQ3,IRQ4 中断信号请求。
}
5.2 时钟中断
// Linux 内核0.11
// main.c
sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c)
// 调度程序的初始化⼦程序。
void sched_init(void)
{
... set_intr_gate(0x20, &timer_interrupt);
// 修改中断控制器屏蔽码,允许时钟中断。
outb(inb_p(0x21) & ~0x01, 0x21);
// 设置系统调⽤中断⻔。
set_system_gate(0x80, &system_call);
...
}
// system_call.s
_timer_interrupt :
...
; // do_timer(CPL)执⾏任务切换、计时等⼯作,在kernel/shched.c,305 ⾏实现。
call _do_timer; // 'do_timer(long CPL)' does everything from
// 调度⼊⼝
void do_timer(long cpl)
{
... schedule();
}
void schedule(void)
{
... switch_to(next); // 切换到任务号为next 的任务,并运⾏之。
}
5.3 软中断
- 上述外部硬件中断,需要硬件设备触发。
- 有没有可能,因为软件原因,也触发上面的逻辑?有!
- 为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int或者syScall),可以让CPU内部触发中断逻辑。
- 户层怎么把系统调用号给操作系统?-寄存器(比如EAX)
- 操作系统怎么把返回值给用户?-寄存器或者用户传入的缓冲区地址
- 系统调用的过程,其实就是先intOx80、Syscal陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法
- 系统调用号的本质:数组下标!
// sys.h
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
extern int sys_setup(); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit(); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork(); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read(); // 读⽂件。 (fs/read_write.c, 55)
extern int sys_write(); // 写⽂件。 (fs/read_write.c, 83)
extern int sys_open(); // 打开⽂件。 (fs/open.c, 138)
extern int sys_close(); // 关闭⽂件。 (fs/open.c, 192)
extern int sys_waitpid(); // 等待进程终⽌。 (kernel/exit.c, 142)
extern int sys_creat(); // 创建⽂件。 (fs/open.c, 187)
extern int sys_link(); // 创建⼀个⽂件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink(); // 删除⼀个⽂件名(或删除⽂件)。 (fs/namei.c, 663)
extern int sys_execve(); // 执⾏程序。 (kernel/system_call.s, 200)
extern int sys_chdir(); // 更改当前⽬录。 (fs/open.c, 75)
extern int sys_time(); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod(); // 建⽴块/字符特殊⽂件。 (fs/namei.c, 412)
extern int sys_chmod(); // 修改⽂件属性。 (fs/open.c, 105)
extern int sys_chown(); // 修改⽂件宿主和所属组。 (fs/open.c, 121)
extern int sys_break(); // (-kernel/sys.c, 21)
extern int sys_stat(); // 使⽤路径名取⽂件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek(); // 重新定位读/写⽂件偏移。 (fs/read_write.c, 25)
extern int sys_getpid(); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount(); // 安装⽂件系统。 (fs/super.c, 200)
extern int sys_umount(); // 卸载⽂件系统。 (fs/super.c, 167)
extern int sys_setuid(); // 设置进程⽤⼾id。 (kernel/sys.c, 143)
extern int sys_getuid(); // 取进程⽤⼾id。 (kernel/sched.c, 358)
extern int sys_stime(); // 设置系统时间⽇期。 (-kernel/sys.c, 148)
extern int sys_ptrace(); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm(); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat(); // 使⽤⽂件句柄取⽂件的状态信息。(fs/stat.c, 47)
extern int sys_pause(); // 暂停进程运⾏。 (kernel/sched.c, 144)
extern int sys_utime(); // 改变⽂件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty(); // 修改终端⾏设置。 (-kernel/sys.c, 31)
extern int sys_gtty(); // 取终端⾏设置信息。 (-kernel/sys.c, 36)
extern int sys_access(); // 检查⽤⼾对⼀个⽂件的访问权限。(fs/open.c, 47)
extern int sys_nice(); // 设置进程执⾏优先权。 (kernel/sched.c, 378)
extern int sys_ftime(); // 取⽇期和时间。 (-kernel/sys.c,16)
extern int sys_sync(); // 同步⾼速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill(); // 终⽌⼀个进程。 (kernel/exit.c, 60)
extern int sys_rename(); // 更改⽂件名。 (-kernel/sys.c, 41)
extern int sys_mkdir(); // 创建⽬录。 (fs/namei.c, 463)
extern int sys_rmdir(); // 删除⽬录。 (fs/namei.c, 587)
extern int sys_dup(); // 复制⽂件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe(); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times(); // 取运⾏时间。 (kernel/sys.c, 156)
extern int sys_prof(); // 程序执⾏时间区域。 (-kernel/sys.c, 46)
extern int sys_brk(); // 修改数据段⻓度。 (kernel/sys.c, 168)
extern int sys_setgid(); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid(); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal(); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid(); // 取进程有效⽤⼾id。 (kenrl/sched.c, 363)
extern int sys_getegid(); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct(); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys(); // (-kernel/sys.c, 82)
extern int sys_lock(); // (-kernel/sys.c, 87)
extern int sys_ioctl(); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl(); // ⽂件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx(); // (-kernel/sys.c, 92)
extern int sys_setpgid(); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit(); // (-kernel/sys.c, 97)
extern int sys_uname(); // 显⽰系统信息。 (kernel/sys.c, 216)
extern int sys_umask(); // 取默认⽂件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot(); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat(); // 取⽂件系统信息。 (fs/open.c, 19)
extern int sys_dup2(); // 复制⽂件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid(); // 取⽗进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp(); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)
extern int sys_setsid(); // 在新会话中运⾏程序。 (kernel/sys.c, 206)
extern int sys_sigaction(); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask(); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask(); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid(); // 设置真实与/或有效⽤⼾id。 (kernel/sys.c,118)
extern int sys_setregid(); // 设置真实与/或有效组id。 (kernel/sys.c, 51)
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = {sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid};
// 调度程序的初始化⼦程序。
void sched_init(void)
{
...
// 设置系统调⽤中断⻔。
set_system_gate(0x80, &system_call);
}
_system_call :
cmp eax, nr_system_calls - 1; // 调⽤号如果超出范围的话就在eax 中置-1 并退出。
ja bad_sys_call
push ds; // 保存原段寄存器值。
push es
push fs
push edx; // ebx,ecx,edx 中放着系统调⽤相应的C 语⾔函数的调⽤参数。
push ecx; // push %ebx,%ecx,%edx as parameters
push ebx; // to the system call
mov edx, 10h; // set up ds,es to kernel space
mov ds, dx; // ds,es 指向内核数据段(全局描述符表中数据段描述符)。
mov es, dx mov edx, 17h; // fs points to local data space
mov fs, dx; // fs 指向局部数据段(局部描述符表中数据段描述符)。
; // 下⾯这句操作数的含义是:调⽤地址 = _sys_call_table + %eax * 4。参⻅列表后的说明。
; // 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了⼀个包括72 个
; // 系统调⽤C 处理函数的地址数组表。
call[_sys_call_table + eax * 4] push eax; // 把系统调⽤号⼊栈。
mov eax, _current; // 取当前任务(进程)数据结构地址??eax。
; // 下⾯97-100 ⾏查看当前任务的运⾏状态。如果不在就绪状态(state 不等于0)就去执⾏调度程序。; // 如果该任务在就绪状态但counter[??]值等于0,则也去执⾏调度程序。
cmp dword ptr[state + eax], 0; // state
jne reschedule
cmp dword ptr[counter + eax],0; // counter
je reschedule; // 以下这段代码执⾏从系统调⽤C 函数返回后,对信号量进⾏识别处理。
ret_from_sys_call:
5.4 缺页中断?内存碎片化处理?除零野指针错误?
void trap_init(void)
{
int i;
set_trap_gate(0, ÷_error); // 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1, &debug);
set_trap_gate(2, &nmi);
set_system_gate(3, &int3); /* int3-5 can be called from all */
set_system_gate(4, &overflow);
set_system_gate(5, &bounds);
set_trap_gate(6, &invalid_op);
set_trap_gate(7, &device_not_available);
set_trap_gate(8, &double_fault);
set_trap_gate(9, &coprocessor_segment_overrun);
set_trap_gate(10, &invalid_TSS);
set_trap_gate(11, &segment_not_present);
set_trap_gate(12, &stack_segment);
set_trap_gate(13, &general_protection);
set_trap_gate(14, &page_fault);
set_trap_gate(15, &reserved);
set_trap_gate(16, &coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱⻔。
for (i = 17; i < 48; i++)
set_trap_gate(i, &reserved);
set_trap_gate(45, &irq13); // 设置协处理器的陷阱⻔。
outb_p(inb_p(0x21) & 0xfb, 0x21); // 允许主8259A 芯⽚的IRQ2 中断请求。
outb(inb_p(0xA1) & 0xdf, 0xA1); // 允许从8259A 芯⽚的IRQ13 中断请求。
set_trap_gate(39, ¶llel_interrupt); // 设置并⾏⼝的陷阱⻔。
}
缺页中断?内存碎片处理?除零野指针错误?全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。
5.5 内核态与用户态
- 内核态与用户态通过页表跳转执行的
- 操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说操作系统系统调用方法的执行,是在进程的地址空间中执行的!
- 用户态就是执行用户[0,3]GB时所处的状态
- 核态就是执行内核[3,4]GB时所处的状态
- 区分就是按照CPU内的CPL决定,CPL的全称是CurrentPrivilegeLevel,即当前特权级别。
- 一般执行intOx8θ或者syScaLl软中断,CPL会在校验之后自动变更
6. volatile
标准情况下,键入CTRL-C,2号信号被捕捉,执行自定义动,flag==1,循环结束
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag)
;
printf("process quit normal\n");
return 0;
}
优化情况下,键入CTRL-C,2号信号被捕捉,执行自定义动作,修改fLag=1,但是while条
件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显,while循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。while检
测的flag其实已经因为优化,被放在了CPU寄存器当中,并非内存中的flag。如何解决呢?很明显需要volatile
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag)
;
printf("process quit normal\n");
return 0;
}
#include <stdio.h>
#include <signal.h>
//声明这个变量可能被更改,不能被优化,需要从内存中读取
volatile int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while (!flag)
;
printf("process quit normal\n");
return 0;
}
Volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该
变量的任何操作,都必须在真实的内存中进行操作