信号本质是一种通知机制。
而进程要处理信号,必须具备“识别”信号的能力(能看到+能处理)。
一般而言,信号的产生相对于进程而言是异步的。信号随机产生,进程可能不能立即处理,所以进程要能够临时地记录下信号,以便后续合适的时候进行处理。
如何理解信号被进程保存?
进程要能知道是什么信号,以及是否产生。
进程具有保存信号的相关数据结构(位图),在进程PCB内部保存了信号的位图字段。
如何理解信号发送的本质?
操作系统向目标进程写信号,即操作系统直接修改进程PCB中的指定的位图结构,完成“发送”信号的过程。
对于信号处理的常见的有三种方式:
SIG_DFL
)SIG_IGN
)void handler(int signum)
{
cout << "["<< getpid() << "] handler正在处理signum: " << signum << endl;
}
void test1()
{
signal(SIGINT, handler); // 特定信号的处理动作,一般只有一个
// signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作
while(true)
{
cout << "["<< getpid() << "] test正在运行中..." << endl;
sleep(1);
}
}
如何理解键盘组合键形成信号?
键盘的工作方式是通过中断的方式进行的。
操作解释组合键信息,查找进程列表找到前台运行的进程,然后写入对应的信号到进程内部的位图结构中。
Linux的所有信号可以通过kill -l
指令查看。
如图所示,[1, 31]是普通信号,[34, 64]是实时信号。man 7 signal
可以查看信号的具体信息。
核心转储:
一般而言,云服务器(生产环境)的核心转储功能是被关闭的。
core dump标记位:
core dump标志,用于标志是否发生核心转储。是在进程发生某种异常的时候,由操作系统将当前进程在内存中的核心数据转储到磁盘中(表现是生成了一个core.pid
的文件),可以便于调试使用。
void test2()
{
while(true)
{
cout << "["<< getpid() << "] test正在运行中..." << endl;
sleep(1);
int a = 1 / 0;
cout << "hello world" << endl;
}
}
使用core文件可以在gdb调试环境下快速定位出程序出错的地方。(SIGFPE
-8号信号)
还有很多信号的行为有core dump标记位的设置。
void test3()
{
pid_t pid = fork();
if(pid == 0)
{
cout << "I am child" << endl;
int a = 1 / 0;
exit(1);
}
int status = 0;
waitpid(pid, &status, 0);
cout << "father pid: " << getpid() << " | " << "child pid: " << pid << \
" | " << "child exit signal: " << (status & 0x7f) << " | " << "child core dump: " << ((status >> 7) & 1) << endl;
}
如何理解发送信号的系统调用接口?
用户调用系统接口,操作获取参数,向目标进程写对应信号,即修改目标进程的信号标记位,进程后续对信号做出处理动作。
// 简单模拟kill接口来实现kill指令
static void Use(const string& proc)
{
cout << "Usage:\r\n\t" << proc << " signum pid" << endl;
}
// ./mykill signum pid
void test4(int argc, char* argv[])
{
if(argc != 3)
{
Use(argv[0]);
exit(1);
}
int signum = atoi(argv[1]);
pid_t pid = atoi(argv[2]);
kill(pid, signum);
}
void test5()
{
cout << "I am running..." << endl;
sleep(1);
raise(9);
}
void test6()
{
cout << "I am running..." << endl;
sleep(1);
abort(); // 通常用来终止进程
}
对于管道文件,当读端不仅不读,还关闭了,那么写端再写也就没有意义了。操作系统会自动中断对应的写端进程(通过发送信号SIGPIPE
的方式)。
/*
* SIGPIPE 信号验证
* 1.创建匿名管道
* 2.父进程读,子进程写
* 3.父子进程可以通信一段时间(非必要)
* 4.父进程关闭读端 && waitpid(),子进程一直写
* 5.子进程会退出,父进程拿到子进程退出的status,提取退出信号
*/
void Test1()
{
// 创建管道
int pipefd[2] = {0};
int ret = pipe(pipefd);
if(ret != 0)
{
perror("pipe");
exit(1);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 构建单向通行的信道,父进程读,子进程写
// 父进程 -- 读
// 关闭写端
close(pipefd[1]);
// 不断读取信息
char receive[128] = {0};
while(true)
{
ssize_t size = read(pipefd[0], receive, 127);
if(size > 0)
{
cout << "father receive: " << receive << endl;
if(strcmp(receive, "hello world5") == 0)
{
// 关闭读端
close(pipefd[0]);
break;
}
}
else if(size == 0)
{
break;
}
else
{
perror("read");
exit(4);
}
}
// 等待子进程
int status = 0;
pid_t wpid = waitpid(pid, &status, 0);
if(wpid == -1)
{
perror("waitpid");
exit(3);
}
cout << "father exit code: " << (status & 0x7f) << " | " << "father core dump: " << ((status >> 7) & 1) << endl;
}
else if(pid == 0)
{
// 子进程 -- 写
// 关闭读端
close(pipefd[0]);
int count = 0;
while(true)
{
// 不断写变化的信息
string msg = "hello world" + to_string(count++);
write(pipefd[1], msg.c_str(), msg.size());
sleep(1);
}
}
else
{
perror("fork");
exit(2);
}
}
管道是软件,因为管道是文件在内存级的实现。像这种读端关闭,写端还在写,就属于软件条件不满足的情况,此时操作系统就会向写端进程发送SIGPIPE
信号。
// 定时器功能
uint64_t count = 0;
vector<function<void()>> v;
void showCount()
{
cout << "final count: " << count << endl;
}
void logUser()
{
if(fork() == 0)
{
execl("/usr/bin/who", "who", nullptr);
exit(1);
}
wait(nullptr);
}
void handler(int signum)
{
(void)signum;
for(auto& f : v)
{
f();
}
alarm(1);
}
void test7()
{
alarm(1); // 时间一到就会进行时钟中断,向进程发送时钟信号
signal(SIGALRM, handler);
v.emplace_back(showCount);
v.emplace_back(logUser);
while(++count);
}
如何理解软件条件给进程发送信号?
操作系统先识别到某种软件条件触发或者不满足,然后构建信号,发送给指定进程。
如何理解除0操作?
计算机中做计算的是CPU这个硬件,CPU内部是有寄存器的,包括状态寄存器(位图结构),有对应的状态标记位。操作系统会自动进行计算完毕之后的检测(状态寄存器的检测),如果识别到有问题,就去找当前在运行进程的pid,向其发送信号,进程后续会做出处理。
如何理解野指针或越界问题?
无论是野指针还是越界访问,都是必须通过地址,找到目标位置。
语言层面的地址,都是虚拟地址,要将其转化成物理地址(页表+硬件MMU[Memory Management Unit])。
野指针,越界都属于非法地址,MMU在转化的时候会报错。(不只是CPU中有寄存器,几乎所有外设和常见的硬件,都可能存在寄存器,所以MMU中也是有寄存器的)
void handler(int signum)
{
cout << "signum: " << signum << endl;
sleep(1);
}
void test8()
{
signal(SIGFPE, handler);
int a = 1 / 0;
while(true) sleep(1);
}
void test9()
{
signal(SIGSEGV, handler); // 11) SIGSEGV - Invalid memory reference
int* p = nullptr;
*p = 1;
while(true) sleep(1);
}
上面程序为什么死循环?
因为寄存器中的异常一直没有得到解决。
所有的信号,都有它的来源,但最终都是会被操作系统识别,解释,并发送的。
一个信号被处理,是怎样的一个过程呢?
handler是一个函数指针数组,数组的下标和信号的编号相对应。
block位图,结构和pending位图一样,block位图的含义是对应信号是否被屏蔽阻塞。
操作系统不允许用户直接进行位图的操作,但提供了操作位图的方法。
sigemptyset & sigfillset
用于初始化set信号集。sigaddset & sigdelset
用于设置特定信号signum的比特位信息。sigismember
用于判断特定信号signum是否在set信号集中。static void handler(int signum)
{
cout << "[" << getpid() << "] " << "signum: " << signum << endl;
}
void test10()
{
// 9 & 19 信号仍有效
for(int signum = 1; signum <= 31; ++signum)
{
signal(signum, handler);
}
cout << "pid: " << getpid() << endl;
while(true) sleep(1);
}
# sendSignal.sh
# chomd +x 添加执行权限
i=1
pid=$(pidof test)
while [ $i -le 31 ]
do
if [ $i -eq 9 -o $i -eq 19 ]
then
let ++i
continue
fi
kill -$i $pid
echo "kill -$i $pid"
let ++i
sleep 1
done
如果把所有的信号都进行block,不断获取并打印当前进程的pending信号集,进程会怎样?
static void handler(int signum)
{
cout << "[" << getpid() << "] " << "signum: " << signum << endl;
}
static void showPending(const sigset_t& pending)
{
for(int signum = 1; signum <= 31; ++signum)
{
if(sigismember(&pending, signum))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void test11()
{
// 0.捕捉2号信号,以便测试
signal(2, handler);
// 1.定义信号集对象(位图)
sigset_t bset, obset;
sigset_t pending;
// 2.初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
// 3.添加要进行屏蔽的信号
sigaddset(&bset, 2 /*SIGINT*/);
// 4.设置bset到内核中对应的进程PCB内[默认情况下进程不会对任何信号进行block]
int ret = sigprocmask(SIG_BLOCK, &bset, &obset);
if(ret == -1)
{
perror("sigprocmask");
exit(1);
}
cout << "[" << getpid() << "] " << "block signum 2 success" << endl;
// 5.重复打印当前进程的pending信号集
int count = 0;
while(true)
{
// 5.1.获取当前进程的pending信号集
sigpending(&pending);
// 5.2.显示pending信号集中没有被递达的信号
showPending(pending);
sleep(1);
++count;
if(count == 10)
{
int ret = sigprocmask(SIG_SETMASK, &obset, nullptr);
if(ret == -1)
{
perror("sigprocmask");
exit(2);
}
cout << "[" << getpid() << "] " << "recover signum 2 success" << endl;
}
}
}
static void blockSignal(int signum)
{
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset, signum);
int ret = sigprocmask(SIG_BLOCK, &bset, nullptr);
if(ret == -1)
{
perror("sigprocmask");
exit(1);
}
}
void test12()
{
// 9 & 19 信号仍有效
for(int signum = 1; signum <= 31; ++signum)
{
blockSignal(signum);
}
sigset_t pending;
while(true)
{
sigpending(&pending);
showPending(pending);
sleep(1);
}
}
SIGKILL
/SIGSTOP
信号 无法被自定义,无法被阻塞,无法被忽略。
这里主要使用sigaction
中的第一个和第三个参数。
void showPending(sigset_t* pending)
{
for(int signum = 1; signum <= 31; ++signum)
{
if(sigismember(pending, signum)) cout << "1";
else cout << "0";
}
cout << endl;
}
void handler(int signum)
{
cout << "[" << getpid() << "] " << "signum: " << signum << endl;
sigset_t pending;
while(true)
{
sigpending(&pending);
showPending(&pending);
sleep(1);
}
}
void test13()
{
// 内核数据类型,在用户栈空间定义的
struct sigaction act, oact;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = handler;
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);
// 设置进当前进程的PCB中
sigaction(2, &act, &oact);
cout << "default action: " << (int)oact.sa_handler << endl;
while(true) sleep(1);
}
处理信号的时候,执行自定义的动作,如果在处理信号期间,又来了同样的信号,操作系统又该如何处理?操作系统的处理方式可以让我们意识到block信号集存在的道理。
信号产生之后,可能无法立即被处理,而是在合适的时候处理。
至于合适的时候,是从内核态返回用户态的时候,进行信号的检测和处理。
内核级页表,可以被所有进程看到。
用户态是一个受管控的状态(权限有限)。内核态是一个操作系统执行自己代码的状态,具备很高的优先级。
用户态可以通过系统调用接口,异常等进入内核态,这也不过是从用户地址空间跃迁到内核地址空间之后,执行操作系统自己的代码。所以,内核也是在所有进程地址空间的上下文中跑的。
所有的程序指令都要经由CPU来执行,CPU又是如何来区分内核态和用户态的呢?
CPU中的CR3寄存器用于表示当前CPU的执行权限是用户级还是内核级。
信号处理的整个流程?
信号处理的整体可以分为两个层级,四次切换。
如果操作系统是执行SGI_DFL
,SGI_IGN
的处理,内核级就可以完成,但是如果处理自定义捕捉handler的动作,就需要到用户级去处理(只有自定义处理方式的信号会在用户态进行处理)。当然内核级的权限很高,有能力将自定义捕捉动作在内核级就处理掉,但是操作系统并不会这样做。因为操作系统是不相信用户的,不相信用户所写的代码的。
可重入函数:
信号捕捉,并没有创建新的进程或者线程。
函数是否可重入的关键在于函数内部是否对全局数据进行了不受保护的非原子操作,其中原子操作指的是一次完成,中间不会被打断的操作,表示操作过程是安全的。
如果用户级程序在访问某一全局资源时,正好陷入内核,然后从内核态返回到用户态时进行信号检测,进行handler方法处理,此时如果同样访问这一资源,就可能出现冲突。能导致这类问题发生的函数便是不可重入函数。
可重入函数 和 不可重入函数 是函数的一种特征,并没有对错之分。
volatile:
让CPU保持内存的可见性。
void test14()
{
volatile const int i = 0;
int* pi = (int*)&i;
*pi = 1;
cout << "i: " << i << endl;
cout << "*pi: " << *pi << endl;
}
void handler(int signum)
{
cout << "[" << getpid() << "] " << "signum: " << signum << endl;
}
// 证明子进程退出,会向父进程发送信号
void test15()
{
signal(SIGCHLD, handler);
if(fork() == 0)
{
cout << "child pid: " << getpid() << endl;
sleep(3);
exit(0);
}
while(true) sleep(1);
}
// 不等待子进程,并且让子进程退出后,自动释放
void test16()
{
signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略
if(0 == fork())
{
cout << "child: " << getpid() << endl;
sleep(5);
exit(0);
}
while(true)
{
cout << "father: " << getpid() << endl;
sleep(1);
}
}