信号是进程之间事件异步通知的一种方式,属于软中断,本质也是数据 。
关于信号:
信号的产生对于进程来说是异步的。
因为信号随时可能产生,所以信号产生前,进程可能在做优先级更高的事情,不能立马处理这个信号,所以进程需要将这个信号记录下来,在合适的时候去处理这个信号,所以进程具有记录信号的能力。
进程是如何记录对应的信号,如何去处理?
linux中共有62个信号,前31个为普通信号,34到64为实时信号
进程只需要管理1-31这些信号,通过哪种数据结构进行管理呢?
位图 一个整数有32个比特位,足够对这些信号进行管理,所以进程对这些信号的管理可以转换为对位图进行操作
回顾以前的学习,我们一共见过三次信号
1.在以前学习的过程中,我们使用过kill -9 进程pid 结束进程(可以终止后台进程)
我们也使用过 键盘ctrl + c 终止一个进程(只能终止前台进程),实际上是给进程一个信号
2.在waitpid时父进程在等待子进程退出时,如果子进程异常退出,父进程的错误码会被设置,错误码中的比特位中有进程退出信号
3.我们在使用管道时,如果写端关闭,读端的进程会受到13号信号,异常终止。
上述讲过,在运行一个进程时,可以通过键盘 ctrl + c 或者ctrl + \ 来进行对进程发送信号。
① kill 发送一个信号给其他进程
#include
#include
int kill(pid_t pid, int sig);
② raise 哪个进程调用这个接口就给哪个进程发送任意信号
#include
int raise(int sig);
③ abort 使调用进程接受信号而异常终止
include <stdlib.h>
void abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值。
过某种软件(OS),来触发信号发送,系统层面设置定时器,或者某种操作而导致条件不就绪的场景。
例如在进程间通信中:当读端不读,而且关闭了读端fd,但是写端一直在写,最终写进程会收到sigpipe(13)信号。
alarm: 一定时间后向进程发送14号信号
#include
unsigned int alarm(unsigned int seconds);
// 返回值为0或还剩多长时间闹钟结束
使用下面这段代码,显然可以看出这里对空指针进行了解引用操作,这里肯定会发生程序错误。
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"进程收到了:"<<signo<<"信号导致的"<<endl;
exit(1);
}
int main()
{
//signal(SIGSEGV,handler); //捕捉信号
int* p=nullptr;
*p=10;
cout<<"div zero..."<<endl;
return 0;
}
运行结果: 发生段错误
使用signal接口对信号进行捕捉,是11号信号
这里的硬件异常的本质是:程序中对空指针或者野指针的解引用,会去访问这块内存,而在页表中没有映射关系,相对应的硬件MMU就会出现异常。
1.信号产生的方式虽然不同,但是最终一定都是通过OS向目标进程发送的信号!
2,由于收到信号后可能不会立即执行对应操作,在Linux内核中使用变量会保存信号。
3.进程中,采用位图来标识进程是否收到信号。
4.所以OS发送信号的本质是向指定进程的task_struct中的信号位图写入比特为1,所以信号的发送也可称为信号的写入。
如果必要,操作系统会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘中,还会记录程序在哪里异常,方便后期调试。
在云服务器上,将数据转储到磁盘上的功能默认是被关掉的。
ulimit -a 查看
我们自己打开 使用命令 ulimit -c 10240
如果现在运行,上面会导致程序崩溃的代码,core dump 就会起作用了,并在当前目录下形成core文件
而在gdb调试时,通过使用dump保存的文件,就会给出崩溃原因和在哪里崩溃:这种调试方式称为事后调试。
我们可以看到这些信号有的会让进程直接退出,有的会发生 核心转储,OS可以将该进程在异常的时候,核心代码部分进行核心转储,将内存中进程的相关数据,全部dump到磁盘中
#include
#include
#include
#include
#include
int main()
{
if(fork() == 0)
{
while(1)
{
printf("I am child hahaha\n");
int a = 10;
a /= 0;
}
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d, exit sig: %d, core dump: %d\n", (status>>8)&0xFF, status&0x7F, (status>>7)&1);
return 0;
}
1.实际执行信号的处理动作称为信号递达(Delivery)
2.信号从产生到递达之间的状态,称为信号未决(Pending)。
3.进程可以选择阻塞 (Block )某个信号。
4.被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
5.注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
这是操作系统设置的类型,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态, 。
sigset_t类型的变量不能单独使用,必须要配合特定的系统调用接口使用
#include
int sigemptyset(sigset_t *set); // 将信号集所有的位清0
int sigfillset(sigset_t *set); // 初始化位图,将信号集所有的位置为1
int sigaddset (sigset_t *set, int signo); // 添加信号到信号集
int sigdelset(sigset_t *set, int signo); // 从信号集中删除信号
int sigismember(const sigset_t *set, int signo); // 是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集) 。
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1
// set:输入型参数
// oset:输出型参数
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。
如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
how参数的可选值 :
SIG_BLOCK:将set中的信号添加到信号屏蔽字中
SIG_UNBLOCK:将set中的信号从信号屏蔽字中解除阻塞
SIG_SETMASK:将信号屏蔽字设置为set
该系统调用不对pending表修改,而仅仅是获取进程的pending位图
#include
int sigpending(sigset_t *set); // 参数为输出型参数
使用:
#include
#include
#include
#include
using namespace std;
void show_pending(sigset_t *set)
{
for(int i=1;i<31;i++)
{
if(sigismember(set,i))cout<<1;
else cout<<0;
}
cout<<endl;
}
void hander(int signo)
{
cout<<"2号信号已被递达,处理完成"<<endl;
}
int main()
{
signal(2,hander); //自定义捕获2号信号
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,2);//将2号信号加入
sigprocmask(SIG_BLOCK,&set,&oset);//阻塞2号信号
sigset_t peding;
int count=0;
while(true)
{
sigemptyset(&peding);
sigpending(&peding);//获取pending位图
show_pending(&peding);//打印pending位图
sleep(1);
count++;
if(count==10) //10秒后解除对2号信号的阻塞
{
sigprocmask(SIG_SETMASK,&oset,NULL);//恢复2号信号
cout<<"2号信号恢复,可以被递达"<<endl;
}
}
return 0;
}
信号什么时候被处理?
① 一个信号之前被block,他解除block后会被立即递达。
② 从内核态转回到用户态,进行信号检测并处理信号
用户态:用户代码和数据被访问或者执行的时候,所处的状态。自己写的代码全部都是在用户态执行。
内核态:执行OS的代码和数据时,进程所处的状态。OS的代码的执行全部都是在内核态执行(例如系统调用)。
主要区别:权限大小,内核态权限远远大于用户态。
用户态使用的是用户级页表,只能访问用户数据和代码;内核态使用的是内核级页表,只能访问内核数据和代码。
CPU内有寄存器保存了当前进程的状态。
所谓系统调用:就是进程的身份转化成为内核,然后根据内核页表找到对应函数执行。
内核级页表是一个全局的页表,它用来维护操作系统的代码与进程之间的关系。因此,在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是一样的内容。
理解进程切换
1.在当前进程的进程地址空间中的内核空间,找到操作系统的代码和数据。
2.执行操作系统的代码,将当前进程的代码和数据剥离下来,并换上另一个进程的代码和数据。
sigaction:类似signal方法,捕捉信号,自定义信号
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// act:新的处理动作
// oldact:原来的处理动作
// act 结构体
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
//当某个信号的处理函数被调用时, 内核自动将当前信号加入进程的信号屏蔽字, 当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时, 如果这种信号再次产生, 那么它会被阻塞到当前处理结束为止(即同一个信号不能被嵌套使用)。
//如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号, 则用sa_mask字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。
//sa_flags字段包含一些选项, 本章的代码都把sa_flags设为0, sa_sigaction是实时信号的处理函数, 本章不详细解释这两个字段
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
例子:
#include
#include
int flag=0; //全局变量
void hander(int signo)
{
printf("change flag 0 to 1\n");
flag=1;
}
int main()
{
signal(SIGINT,hander); //自定义捕获处理2号信号
while(!flag); //不做任何处理,等待信号
printf("main quie 正常\n");
return 0;
}
编译器没有优化 接收到2号信号正常结束
编译器优化后 接收到2号信号不会结束
原因:
解决方法
在flag变量前加上volatile就可防止这种情况的发生。
为了避免出现僵尸进程,父进程需要使用wait或waitpid函数等待子进程结束,父进程可以阻塞等待子进程结束,也可以非阻塞地查询的是否有子进程结束等待清理,即轮询的方式。采用第一种方式,父进程阻塞就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
其实,子进程在终止时会给父进程发生SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理动作,这样父进程就只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait或waitpid函数清理子进程即可。
我们可以写一个自定义信号处理方式,来实现父进程对子进程的回收,并且不需要轮询等待子进程退出。不过需要考虑的是,如果同时创建多个子进程,在子进程退出时候会向父进程发送信号,在处理SIGCHLD的时候,可能会收到另一个子进程发来的退出信号,此时,我们正在处理上一个子进程退出的自定义信号处理,可能会导致多个子进程一起退出时,父进程只能处理少数几个子进程的退出信号。
#include
#include #include #include #include #include volatile int flag=0; int id; void handler(int signo) { printf("捕捉到一个信号:%d, who: %d\n",signo,getpid()); sleep(5); //可以同时处理多个进程退出 while(1) { pid_t ret=waitpid(-1,NULL,WNOHANG); if(ret>0) { printf("wait success ,ret: %d, id: %d\n",ret,id); }else break; //没有子进程了 } printf("hander done...\n"); } int main() { signal(SIGCHLD,handler); int i=1; for(;i<=10;i++) { id=fork(); if(id==0) { //child int cnt=5; while(cnt) { printf("我是子进程,我的pid:%d,ppid: %d\n",getpid(),getppid()); sleep(1); cnt--; } exit(0); } } while(1)sleep(1); return 0; }
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程。