这个专栏其实是博主在复习操作系统和计算机网络时候的笔记,所以如果是博主比较熟悉的知识点,博主可能就直接跳过了,但是所有重要的知识点,在这个专栏里面都会提到!而且我也一定会保证这个专栏知识点的完整性,大家可以放心订阅~
本质是一种通知机制,用户或OS通过发送一定的信号,通知进程,某些事件已经发生,你可以再后续进行处理!
结合进程,信号的一些结论:
信号处理的常见方式
man 7 signal # 可以查看信号的描述
信号发送的本质
信号位图是在task_struct里面保存的,谁能改pcb,只有OS有这个资格!因为pcb 是内核数据结构。
本质:OS向目标进程写信号!OS直接修改pcb中指定位图结构,完成“发送”信号的过程!
这里有个回调函数,通过回调的方式,修改对应的信号捕捉的方法。
signal函数调用细节:
仅仅只是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作!
意思就是,调用完signal catchSig并没有被调用 只有等到信号来了,才会被调用!
#include <iostream>
#include <signal.h>
#include <unistd.h>
void catchSig(int sig)
{
// 此时的sig就是捕捉到的信号的编号
std::cout << "进程捕捉到到了一个信号: " << sig << ", pid: " << getpid() << std::endl;
}
int main()
{
signal(SIGINT, catchSig);
while (true)
{
std::cout << "running ... pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
细节比较复杂,这里不详细说了,具体可以看回整理好的笔记。
用这个系统调用就可以了。
static void Usage(std::string proc)
{
std::cout << "Usage:\r\n\t" << proc << "signumber processid" << std::endl;
}
int main(int argc, char **argv)
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signumber = atoi(argv[1]);
int procid = atoi(argv[2]);
kill(procid, signumber);
return 0;
}
abort()
也可以用来终止一个进程
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,"以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
int main()
{
// 这里就可以计算count能计算多少次了(可以吗?)
alarm(1);
int count = 0;
while (true)
{
std::cout << "count: " << count++ << std::endl;
}
return 0;
}
重要概念
信号抵达的自定义处理方法:
其实调用signal
函数其实就是把pending表填上我们传过去的信号编号,然后handler表填上处理方法。
int main()
{
// 处理信号
signal(2, SIG_IGN);
signal(2, SIG_DFL);
return 0;;
}
/* Fake signal functions. */
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
这里就是对应的处理方法。
所以,再重复一次:调用signal不是调用handler方法 而是把东西填到pcb里面的这些表中而已。
发送信号:填pending
处理信号:首先在pending里面找,找到了,说明有这个信号,但是此时,不是直接调用对应的 handler,而是先检查一下,是否有被block。
void catchSig(int signum)
{
std::cout << "捕获到了一个信号: " << signum << std::endl;
}
int main()
{
for (int i = 0; i <= 31; i++)
{
signal(i, catchSig); // 尝试捕捉所有的信号
}
while (true)
{
std::cout << "pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
尝试捕捉所有的信号。
发现,9号信号是不能被捕捉的!
结论:9号信号不能被捕捉!
写个程序看看是不是这么回事就可以了。
用这几个函数就可以了。
static void showPending(sigset_t &pending)
{
for (int sig = 1; sig <= 31; sig++)
{
if (sigismember(&pending, sig))
std::cout << "1"; // 说明在这个集合里面
else
std::cout << "0";
}
std::cout << "\n";
}
int main()
{
// 1. 定义信号集对象
sigset_t bset, obset;
sigset_t pending;
// 2. 初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3. 添加我们需要屏蔽的信号
sigaddset(&bset, 2);
// 4. 设置set到内核对应的进程中 [默认情况: 进程不会block任何信号]
int n = sigprocmask(SIG_BLOCK, &bset, &obset);
assert(!n);
(void)n;
// 5. 重复循环打印当前进程的pending信号集
while (true)
{
sigpending(&pending); // 获取当前进程的信号集
showPending(pending); // 打印当前进程的信号集
sleep(1);
}
return 0;
}
因为我只设置了2号信号屏蔽,所以呢,如果我发送其他信号,进程是会结束的。
这个的验证很简单,和前面的代码都一样的,稍微修改一下就行,然后我们会发现,9号信号是不能被屏蔽的,19号也不可以。
上面第七节,我们介绍完了信号是如何保存的,下面我们要学一下信号时如何被处理的。
void handler(int signum)
{
std::cout << "捕获了一个信号: " << signum << std::endl;
}
int main()
{
// 内核数据类型,但是我们下面这些定义是在user栈上定义的
struct sigaction act, oact;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
// 把上面定义的东西丢到进程的pcb里面去
sigaction(2, &act, &oact);
std::cout << "default action: " << (int)oact.sa_handler << std::endl;
while (true)
sleep(1);
return 0;
}
现在提出一个问题:如果处理信号的时候,执行自定义动作, 如果处理信号期间,又来了同样的信号, OS如何处理?
通过上面的文字,现在我们来证明这个问题:
信号捕捉,并没有创建新的进程和线程。
该函数重入了:一个函数在同一时间被多个执行流同时进入。
假设,比如我在调用链表插入这个insert函数, 调用到一半,进程被陷入内核,然后回来的 时候,需要处理2号信号,刚好2号信号,就 是调用这个insert函数, 此时insert函数被重入!
如果一个函数,被重入不会出现问题,这种函数叫做可重入函数。
如果像上面这个insert函数,被重入会出现问题,这样的函数叫做不可重入函数
我们写一个代码,看下这个场景。
int flag = 0;
void changeFlag(int signum)
{
(void)signum;
std::cout << "change flag: " << flag;
flag = 1;
std::cout << "->" << flag << std::endl;
}
int main()
{
signal(2, changeFlag); // 收到二号信号去改一下全局的flag
while(!flag) // 如果flag还是0
{
sleep(1);
}
std::cout << "进程正常退出后: " << flag << std::endl;
return 0;
}
这个代码很简单,如果发送2号信号,flag就会被改成1,然后循环进不去,进程就会结束。
这是合理的结果。
然后如果我改一下makefile
test:mysignal.cc
g++ -o $@ $^ -std=c++11 -O3
.PHONY:clean
clean:
rm -f test
-O3
代表最高的优化级别。