以下是阿鲤对Linux下进程信号的总结,希望对大家有所帮助;若有误请慷慨指出。
在生活中我们处处离不开信号,很多东西都需要有信号来给我们传达信息。当我们听到上栗铃声我们就知道要上课了;当我们看到有人向我们挥手我们就知道他再向你打招呼;以上这些这些都是生活中信号的体现。那么什么是进程间信号呢?
1:信号概念
2:信号的种类
3:信号的产生
4:信号在进程中的注册和注销
5:信号的捕捉处理
6:信号的阻塞
7:函数的可重入与不可重入
8:volatile关键字
9:SIGCHLD信号
注:以下的代码实现均为centos7环境;
一:进程信号概念:
1:什么是进程信号
进程信号是一个软件中断,通知程序发生了某个事件,打断进程当前的操作,去处理这件事。
2:进程信号的特点
1:信号是多种多样的,每一个信号对应一个事件
2:所能识别的信号必须是合理的
二:信号的种类
1:查看信号的种类:使用kill -l命令
我们可以看到上面有62种信号(32 33 不存在),其中前31种信号是借鉴unix而来的是,每一个均对应一个操作;而后面的信号是面向用户的,并没有对应的事件,所以没有真正的命名。
所以有一个分类,1-31为非可靠信号;34-64是可靠信号。
三:信号的产生
1:软件产生:
1:kill命令:kill+signal number pid
2:int kill(pid_t pid, int sig);接口 发送一个sig给pid进程 eg如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
int main()
{
kill(getpid(), SIGQUIT);
while(1)
{
printf("BelongAl\n");
}
return 0;
}
上面的死循环程序因为开始就发送了SIGQUIT信号(相当于Ctrl+l),所以直接退出报错。
3: int raise(int sig); 发送一个信号给当前进程;eg如下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
int main()
{
raise(SIGTERM);
while(1)
{
printf("BelongAl\n");
}
return 0;
}
上面的死循环程序因为开始就发送了SIGTERM信号(相当于Ctrl+z),所以直接终止程序。
4:void abort(void); 直接抛异常的信号:相当于6号信号(SIGABRT的封装);c++中的异常就是使用该信号实现的。
5:unsigned int alarm(unsigned int seconds); 给调用进程再seconds秒后发送一个信号(相当于定时器);sleep函数就是使用alarm函数实现的。
6:int sigqueue(pid_t pid, int sig, const union sigval value);给一个进程发送一个信号,发送信号的同时还可以传递一个数据。
2:硬件产生:通关键盘,鼠标等,例如:Ctrl + c/|/z
四:信号在进程中的注册和注销
1:我们知道了信号的产生,那么信号在进程中是怎样表示的呢?
在pcb中有一个未决的信号集合,pending集合,信号的注册就是指在这个pending集合中标记对应信号数值的二进制位为1;
2:可靠信号和非可靠信号的注册方式
非可靠信号:1-31为非可靠信号,若信号还未注册,则注册一下,若已经注册,则什么都不做;所以会存在信号丢失
可靠信号:24-64为可靠信号,每次注册信号,不管是否已经注册,每次都会添加一个sigqueue节点
3:信号的注销
非可靠信号:删除sigqueue节点,直接将pending位图置为0。
可靠信号:删除全部的sigqueue节点,将pending位图置为0(所以每删除一次就要判断一次)。
五:信号的捕捉处理
1:首先介绍一下signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:信号pid
handler: 信号处理方式(函数指针)。 SIG_IGN 忽略处理 SLG_DFL 默认处理 也可自定义
2:默认处理
即默认对处理方式, SLG_DFL 默认处理
3:忽略处理
首先我们写这样一段代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
int main()
{
//修改信号的处理方式
//SIG_IGN 忽略处理 SLG_DFL 默认处理
signal(SIGINT, SIG_IGN); //将SIGINT(Ctrl+c)忽略处理
while(1)
{
printf("BelongaL\n");
sleep(1);
}
return 0;
}
然后运行这段程序,你会发现使用Ctrl+c不能使程序退出
最后可使用Ctrl+|退出;
4:自定义处理
在pcb中存在一个action数组,里面存储了每一个信号的默认处理方式(函数指针),即 SLG_DFL;所以我们在自定义使用时那个typedef void (*sighandler_t)(int);里的int就是信号值。(操作系统自己传入的)。
注:信号的处理是先通过pending位图中找到要处理的信号,然后再pcb中找到sighand_struct中找到action数组,然后通过action数组找到对应的函数指针,找到处理函数。
eg:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sigcb(int signum)
{
printf("signum: %d\n",signum);
}
int main()
{
signal(SIGINT, sigcb);//Ctrl+c 被重定义
signal(SIGQUIT, sigcb);//Ctrl+| 被重定义
while(1)
{
printf("BelongaL\n");
sleep(1);
}
return 0;
}
最后使用kill命令停止程序;
5:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);函数
signum:信号的pid
act:新的执行方式
oldact:保存旧的执行方式
eg:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sigcb(int signum)
{
printf("recv signum:%d\n",signum);
}
int main()
{
struct sigaction newact;
struct sigaction oldact;
newact.sa_handler = sigcb;
newact.sa_flags = 0;
sigemptyset(&newact.sa_mask);//信号集合,位图,临时阻塞其他信号// 清空
//修改信号的处理动作为newact 原来的动作使用oldact保存
sigaction(SIGINT, &newact, &oldact);
while(1)
{
printf("BelongAl\n");
sleep(1);
}
return 0;
}
6:sigaction的说明
1:sigaction是直接修改默认处理动作,而signal是修改action数组中的函数指针
2:signal是对sigaction的封装
3:下面是sigaction结构体的说明
struct sigaction {
void (*sa_handler)(int);//所替换的函数类型1
void (*sa_sigaction)(int, siginfo_t *, void *);//所替换掉函数类型2
sigset_t sa_mask;//信号机,保证其他的信号不影响新动作
int sa_flags;//0-使用类型一 SA_SIGINFO时使用类型二
void (*sa_restorer)(void);
};
7:自定义信号的捕捉流程:
信号的处理是在程序运行从内核态切换回用户态之前,默认/忽略直接在内核中完成处理,而用户自定义信号处理方式,则需要返回用户态执行回调函数,完成后返回内核态,最终没有信号处理,再返回程序主控流程。
用户是怎样从用户态切换到内核态运行的方式:中断, 异常, 系统调用接口
六:信号的阻塞
1:概念:阻止一个信号的抵达,信号依然可以注册,只是暂时不处理
2:方式:再pcb中有一个blocked位图(阻塞信号集合),凡是添加到这个集合中的信号,都表示需要阻塞,暂时不处理。(会组织pending位图对handler数组的访问)
3:操作 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);函数
1:how:当前对block集合进行的操作
SIG_BLOCK:将set集合中的信号添加到block进程阻塞信号集合中, bolck = block|set;表示阻塞set集合中的信号以及原有的阻塞信号,并且将原有的阻塞信号返回到old集合中(便于还原)。
SIG_UNBLOCK:将set集合中的信号接触阻塞 block&(~set)
SIG_SETMASK:直接将block集合中的信号修改为set集合中的信号 block = set
2:set:信号集合
3:oldset:以前的集合
eg:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sigcb(int signum)
{
printf("rexv a signal:%d\n",signum);
}
int main()
{
//修改2号和4号信号
signal(2, sigcb);
signal(40, sigcb);
//阻塞所有信号
sigset_t set, old;
sigemptyset(&set);//清空信号集合
sigemptyset(&old);//清空信号集合
sigfillset(&set);//将所有信号都添加到set集合中
//sigaddset(int signum, sigset_t *set) : 将指定信号添加到set集合中
sigprocmask(SIG_BLOCK, &set, &old);
printf("press enter to continue\n");
getchar();//在按下回车之前,程序卡在这里
sigprocmask(SIG_UNBLOCK, &set, NULL);//解除所有信号
return 0;
}
然后我们运行此程序,并在另一端向该进程传递三次40号信号和三次2号信号,结果如下
你可以发现,40号信号处理了三次,而2号信号处理了一次;这就是可靠信号和不可靠信号的体现。
4:注意:
1:查看/usr/include/bits/signum.h文件里的信号信息,你会发现9号和19号信号是不可被阻塞,不可被定义即不可被忽略的。
2:20号信号和19号信号均是停止信号,但是20号信号时键盘产生的,而19号是命令产生的。
七:函数的可重入与不可重入
1:竞态条件:在多个执行流中,程序的竞争执行;
首先看一下下面这段代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
int a = 0, b = 0;
void test()
{
a++;
sleep(3);
b++;
printf("sum:%d\n",a+b);
}
void sigcb(int signum)
{
test();
}
int main()
{
signal(SIGINT, sigcb);
test();
return 0;
}
你会发现如果在运行过程中使用率Ctrl+c(SIGINT),就会导致程序出现问题;这就是竞争的一个体现。
2:函数的重入:在多个执行流中,进入同一个函数进行处理
2.1:函数的不可重入:在函数重入之后会造成数据二义或者引起程序逻辑混乱,则这个函数是一个不可重入函数
2.2:函数的可重入::在函数重入之后不会造成数据二义并且不引起程序逻辑混乱,则这个函数是一个可重入函数
3.3:因为函数重入的特性,我们在使用函数的时候就要考虑这个函数是否可重入,所有可能给程序带来的不确定性。(例如malloc/free都是不可重入函数,所以在多个执行流中进行操作时要格外小心)
3.4:函数的可重入与不可重入的判定点:一个函数是否对全局数据进行了非原子性的操作。、
八:volatile关键字
修饰一个变量,是这个变量保持内存可见性,每次对变量进行访问都需要重新从内存加载变量数据防止编译器过度优化。
eg:请看下面这段代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
int a = 1;
void sigcb(int signu)
{
a = 0;
printf("a = %d\n",a);
}
int main()
{
signal(SIGINT, sigcb);
while(a)
{
}
return 0;
}
这段代码我们可以看出,是一个死循环,但是我们使用了Ctrl+c就会使得这个循环终止,但是如果使用gcc -O2 test.c -o test进行编译,就会发生退不出来;因为优化等级过高,因为a的使用率很高,所以直接将a放到了寄存器,每次使用都会从寄存器拿去数据。
九:SIGCHLD信号
我们都知道僵尸进程,是因为子进程退出后发送SIGCHLD信号给父进程,但是因为SIGCHLD信号的默认处理方式就是忽略,因此在之前的程序中并没有感受到操作系统的通知,因此只能固定的使用进程等待来避免僵尸进程;但是在这个过程中父进程一直是阻塞的,只能一直等待子进程退出。
那么我们是不是可以将SIGCHLD信号的处理方式自定义,并且在自定义函数调用watpid,这样的话就当子进程退出的时候,则自动回调处理了,父进程就不需要一直等待了。实现如下
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<wait.h>
W>void sigcb(int signum)
{
while(waitpid(-1, NULL, WNOHANG) > 0)/*使用循环,一次将所有退出的子进程均进行资源释放。若没有则返回0,退出。(因为SIGCHLD是非可靠信号,会丢失事件,只会执行一次回调函数)*/
{
}
}
int main()
{
signal(SIGCHLD, sigcb);
pid_t pid = fork();
if(pid == 0)
{
sleep(5);
exit(0);
}
//若不对SIGCHLD信号进行替换,就需要一直在这里使用waitpid()函数进行等待。
使用WNOHANG设置waitpid为非阻塞,这样就不会卡住
来源:CSDN
作者:belongAL
链接:https://blog.csdn.net/belongHWL/article/details/103659844