全文约 3036 字,预计阅读时长: 9分钟
信号
- 信号是进程之间事件异步通知的一种方式,属于软中断。过程:信号产生,信号识别,处理处理。
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在
signal.h 中找到,例如其中有定义 #define SIGINT 2
Ctrl-C 产生的信号只能发给前台进程。 Shell可以同时运行一个前台进程和任意多个后台进程,- 一个命令后面加个
& 可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。 - 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
- 对于相当一部分信号而言,当进程收到的时候,默认的处理动作是终止当前进程。
- 对进程而言,有一些信号不能被捕捉和忽略,如:
kill -9 - 进程收到信号,不是立即处理的,而是在合适的时候。
- 信号的处理:
- 默认方式(部分终止进程,部分有特定的功能)
- 忽略信号
- 自定义方式:捕捉信号
- 站在语言角度:程序崩溃;站在系统角度,进程受到了信号。
信号发送
- 信号的产生有如下的方式:
- kill 命令产生
kill -l 1—31普通信号;34—64实时信号,响应要求级别特别强的信号,一旦发出进程必须响应。 - 键盘产生
- 由软件条件产生信号:闹钟
- 程序异常 、硬件异常产生
- 当你的进程触发错误时,基本都有对应的软硬件监控,cpu下的状态寄存器,内存和页表mmu等,会被OS识别到,然后给目标进程发送信号,来达到终止进程的目的。
- 信号的产生,在进程的运行的任何时间点都可以产生,有可能进程正在做更重要的事情。
- 因为信号不是立即处理的,所以信号在进程的PCB里保存着。
- 对进程而言:是否有信号、是什么信号
- 存储方式:位图,无符号整形,1在比特位中的位置意味着是哪个信号,有没有1意味着有没有信号。
- 是谁发的,如何发;直接简介通过OS向进程发信号。
- 发送信号的本质,相当于写对应进程的PCB的位图。因为OS是进程的管理者,OS是由这个能力和义务的。
- 由软件条件产生信号:闹钟定时终止
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
----
int a =0;
void add(int signo)
{
cout<<signo<<endl;
cout<<a<<endl;
exit(1);
}
int main()
{
for(int i =1,i<32;i++)
{
signal(i,add);
}
alarm(1);
while(true)
{
a++;
}
return 0;
}
void abort(void);
int raise(int sig);
int kill(pid_t pid, int sig);
---
int main(int argc,char* argv[])
{
kill(atoi(argv[2]),atoi(argv[1]));
}
CC=g++
LDFLAGS=-std=c++11 -g
Src=mysignal.cc
Bin=mysignal
$(Bin):$(Src)
$(CC) -o $@ $^ $(LDFLAGS)
.PHONY:clean
clean:
rm -f $(Bin)
core dump文件
- 核心转储,OS将进程运行时的核心数据dump到磁盘上,方便用户调试使用。快速定位BUG,标注了错误出现在了哪一行。
- 一般而言,核心转储是关闭的。自己写出来的错误才会有core dump 标志位设置,不是所有的信号都设置
ulimit -a
ulimit -c 1024
gdb mytest
gdb core-file core.....
---
int main()
{
...
int s=0;
pid_t ret = waitpid(-1,&s,0);
if(ret>0)
{
cout<<((s>>7)&0x1)<<endl;
}
0x7F:7个高电平,1个低电平 0111 1111
}
信号设置
- bolock 位图:代表是否哪种信号阻塞(屏蔽)
- pending 位图表示:有哪种未决信号
- sighandler数组表示:对应信号的处理方式(递达)。
sigset_t 系统提供的数据类型,用来存储或设置位图中的信号,称为信号集。- 修改设置位图中的标志位,需要一系列系统提供的信号集操作函数:
#include <signal.h>
int sigemptyset(sigset_t *set); ---初始化,位图中标志全部请0
int sigfillset(sigset_t *set); --全部置1
int sigaddset (sigset_t *set, int signo); ---指定位置设置1
int sigdelset(sigset_t *set, int signo); ---指定位置设置0
int sigismember(const sigset_t *set, int signo); ---判断特定信号是否被设置
----都是成功返回0,出错返回-1
sigprocmask :设置阻塞信号集。
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); - 参数:
oset :非空指针,返回修改之前的信号屏蔽字。 set :非空指针,则更改进程的信号屏蔽字; how :指示如何更改,参数的可选值:
SIG_BLOCK :将set信号添加到阻塞位图中。SIG_UNBLOCK :解除阻塞信号setSIG_SETMAXK :将当前信号屏蔽字设置成 set 信号。- 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending :读取当前进程的未决信号集,通过set参数传出:int sigpending(sigset_t *set);
- 阻塞 2号信号
- 不断获取pending信号集,并打印
- 发送2号信号给进程
- 过一段时间,解除对2号信号的阻塞
- 2号信号立马会被递达,执行默认动作。
- 依旧打印pending未决信号集
void show_pending(sigset_t *pending)
{
for(int i = 1; i <= 31; i++){
if(sigismember(pending, i)){
cout << "1";
}
else{
cout <<"0";
}
}
cout << endl;
}
int main()
{
sigset_t in;
sigemptyset(&in);
sigaddset(&in, 2);
sigprocmask(SIG_SETMASK, &in, NULL);
int count = 0;
sigset_t pending;
while(true){
sigpending(&pending);
show_pending(&pending);
sleep(1);
if(count == 20){
sigprocmask(SIG_SETMASK, NULL, &in);
cout << "my: ";
show_pending(&in);
cout << "recover default: ";
show_pending(&out);
}
count++;
}
- Linux下:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
信号处理
- 进行信号递达的时间:从内核态返回用户态时,尝试信号检测与捕捉执行。
- 内核态与用户态:
??进程的地址空间0-3G是用户空间,3-4G是内核空间;操作系统提供的系统调用接口内核空间中,而内核空间中的代码数据在物理内存上放着,因此有一个内核级页表维护这个映射关系;再由于进程有多个,操作系统只有一个,因此内核级页表只有一个,且是共享的。 ??用户自己的代码数据,通过接口访问内核代码数据,系统会自动进行身份切换,进入内核空间进行一系列操作。此时进程在用户空间的状态就叫用户态,在内核空间的状态叫做内核态。CPU中存在一个与 权限相关的寄存器数据标识所处的状态。 - 故操作系统设计时:OS从内核态切换至用户态,会检测信号集是否需要被处理。
- 当去执行自定义信号捕捉的方法时,是需要切换至用户态的。因为内核态权限时很高的,如果此时有人利用这个bug会去进行大量危险的操作,进行程序替换等,破坏系统或用户的数据等。
- 递达的处理方法一般有三种:
- 默认(大部分终止进程)
- 忽略
- 自定义信号捕捉:
signal() 、sigaction signal :捕获进程递达的信号,进行怎样的处理
void (*signal(int sig, void (*func)(int)))(int) - 参数:
sig – 在信号处理程序中作为变量使用的常量信号码,有些特定选项(异常终止、除0或算术溢出、野指针等)
SIGINT :中断信号常用,由用户产生;也就是kill -l 列表里的 1 — 31 的普通信号。 func – 一个指向函数的指针,也可以是下面预定义函数之一:
SIG_DFL :默认的信号处理程序,大部分默认终止进程。SIG_IGN :忽视信号。
void handler(int signo)
{
std::cout << "get a signal: " << signo << std::endl;
exit(0);
}
int main()
{
for(int i=1; i < 32; i++){
signal(i, handler);
}
.....
sigaction :你想捕获哪一个信号,结构体:你想怎么处理这个信号,返回老的信号捕捉方法。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
signo 是指定信号的编号。- 若
act 指针非空,则根据 ac t修改该信号的处理动作。 - 若
oact 指针非 空,则通过oact传出该信号原来的处理动作,不需要可以设置为NULL。 - act和oact指向
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);
};
---
struct sigaction s1;
s1.sa_handler = handler;
s1.sa_flags =0;
sigemptyset(s1.sa_mask);
s1.sa_sigaction=NULL;
S1.sa_restorer = NULL;
sigaction(SIGINT,&act,NULL);
void handler(int signo).....
可重入函数
- 当前运行进程收到信号的处理方法,而此时进程收到信号进行递达处理,之前正在运行的函数栈帧销毁,造成资源泄露等。
- 一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
- 告诉编译器不要将内存中的变量优化到cpu的寄存器中,cpu找数据时,去内存里找。解决寄存器和内存数据不一致的问题。
- 保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作.
- 使用:
volatile int flag = 0;
SIGCHLD信号
- 用
wait 和waitpid 函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。两种方式各有缺点。 - 其实子进程在终止时会给父进程发
SIGCHLD 信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,调用wait 清理子进程即可。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
- 是否需要wait子进程:
- 僵尸进程的内存泄漏
- 是否需要获得子进程的退出码:不关心就算了。
|